指针——从入门到 "放弃"(手动滑稽)

一、什么是指针?

  • 指针就是地址,内存单元的编号。

二、指针变量和地址

1.(&)操作符为取地址操作符。

#include <stdio.h>
int main()
{
 int a = 10;
 &a;   
 printf("%p\n", &a);
 return 0;
}

在本段代码中, &a则表示为取出a的地址。

2.指针变量和(*)解引用操作符

#include <stdio.h>
int main()
{
 int a = 10;
 int* pa = &a;   //取出a的地址并存储到指针变量pa中
 return 0;
}

在本段代码中,* 是在说明 pa 是指针变量,int 是在说明 pa 指向的是整型(int)类型的对象。

#include <stdio.h>
int main()
{
 printf("%zd\n", sizeof(char *));
 printf("%zd\n", sizeof(short *));
 printf("%zd\n", sizeof(int *));
 printf("%zd\n", sizeof(double *));
 return 0;
}

指针变量的大小取决于地址的大小。32位平台下地址是32个bit位(即4个字节);64位平台下地址是64个bit位(即8个字节)。

【注】指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。

三、指针变量类型的意义

1.指针的解引用

指针的类型决定了对指针解引⽤的时候有多大的权限,即⼀次能操作几个字节。
下面请看这两段代码:

//代码1
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 int *pi = &n; 
 *pi = 0; 
 return 0;
}
//代码2
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 *pc = 0;
 return 0;
}

在这两段代码中,char* 的指针解引⽤就只能访问⼀个字节,而 int* 的指针的解引⽤就能访问四个字节。

四、const

1.const修饰变量

  • const修饰变量使得这个变量不能被修改。
    请看下面这段代码:
#include <stdio.h>
int main()
{
 int m = 0;
 m = 20;//m是可以修改的
 const int n = 0;
 n = 20;//n是不能被修改的
 return 0;
}

在这段代码中,n被const修饰过后是不能被修改的。否则就会报错。

2.const修饰指针

  • const放在*的左边,意思是不能通过指针来修饰指针指向的内容,但是指针变量本身是可以修改的。
  • const放在*的右边,意思是不能修改指针变量的指向,但是可以修改指针方向的内容。

请看下面这段代码:

#include <stdio.h>
int main()
{
 int m = 100;
 n = 10;
 const int * p = &n;   // const放在*的左边
 *p = 0; // 此行会报错
 p = &m;  // 此行可修改
 return 0;
}

在本段代码中, const放在*的左边,n 不可以通过 *p 被修改,但 p 可以修改。

#include <stdio.h>
int main()
{
 int m = 100;
 n = 10;
 int * const p = &n;  //  const放在*的右边
 *p = 0; // 此行可修改
 p = &m;  // 此行会报错
 return 0;
}

在本段代码中, const放在*的右边,p 不可以修改,但 p 指向的对象可以修改。

五、野指针

1.什么是野指针?

  • 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

2.造成野指针的原因

  • 指针未初始化
  • 指针越界访问
  • 指针指向的空间释放

3.如何规避野指针?

  • 指针初始化
  • 小心指针越界
  • 指针变量不再使⽤时,及时置NULL,指针使用之前检查有效性
  • 避免返回局部变量的地址

六、指针的传址调用

  • 传址调用是一种函数调用方式,它通过传递变量的内存地址作为参数,使得在函数内部可以直接修改原始变量的值。
  • 当使用传址调用时,被调用函数的形参将成为原始变量的指针,通过该指针可以访问和修改原始变量的内容。这种方式可以在函数内部修改变量的值,并且这些修改对于函数外部是可见的。
  • 要进行传址调用,需要在函数声明和定义时使用指针类型的形参,并使用取地址操作符"&"将原始变量的地址传递给函数。
    请看下面这段代码:
#include <stdio.h>
void Swap2(int*px, int*py)
{
 int tmp = 0;
 tmp = *px;
 *px = *py;
 *py = tmp;
}
int main()
{
 int a = 0;
 int b = 0;
 scanf("%d %d", &a, &b);
 printf("交换前:a=%d b=%d\n", a, b);
 Swap1(&a, &b);
 printf("交换后:a=%d b=%d\n", a, b);
 return 0;
}

在本段代码中,就是通过传递变量a、b 的地址给 Swap2 函数进行传址调用,从而实现两个整型变量值的交换。

七、指针与数组

1.什么是数组名?

  • 数组名:就是数组首元素(第⼀个元素)的地址。
  • sizeof(数组名):sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
  • &数组名:这里的数组名表示整个数组,取出的是整个数组的地址。
    【注】除此之外,任何地方使用数组名,数组名都表示首元素的地址。

2.使用指针访问数组

请看下面这段代码:

#include <stdio.h>
int main()
{
   int arr[10] = {0};
   //输⼊
   int i = 0;
   int sz = sizeof(arr)/sizeof(arr[0]);
   //输⼊
   int* p = arr;
   for(i=0; i<sz; i++)
  {
     scanf("%d", p+i);
     //scanf("%d", arr+i);//也可以这样写
  }
   //输出
   for(i=0; i<sz; i++)
  {
     printf("%d ", *(p+i));
     // printf("%d ", p[i]);   // 也可以这样写
  }
   return 0;
}
  • 数组名arr是数组首元素的地址,可以赋值给p,其实数组名arr和p在这⾥是等价的。那我们可以使用arr[i]可以访问数组的元素。
  • 在本段代码中,在倒数第5行的位置,将*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i] 是等价于 *(p+i)。同理arr[i] 应该等价于 *(arr+i),数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的。

3.一维数组传参的本质

  • 通过上面的知识我们知道数组名是数组首元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组首元素的地址。
  • 所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。那么在函数内部我们写 sizeof(arr) 计算的是⼀个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分的本质是指针,所以在函数内部是没办法求的数组元素个数的。
    【总结】⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
    代码示例:
void test(int arr[])//参数写成数组形式,本质上还是指针
{
   printf("%d\n", sizeof(arr));
}
void test(int* arr)//参数写成指针形式
{
    printf("%d\n", sizeof(arr));//计算⼀个指针变量的⼤⼩
}
int main()
{
   int arr[10] = {1,2,3,4,5,6,7,8,9,10};
   test(arr);
   return 0;
}

4.二级指针

请看下面这段代码:

int main()
{
      int a = 10;
      int * p = &a;    // p 是一级指针变量
      int ** pp = &p;    // pp 是二级指针变量
      int *** ppp = &pp;    // ppp 是三级指针变量
      //....
      return 0;
}

在本段代码中,

  • a是整型变量,占用4个字节的空间,a是有自己的地址,&a拿到的就是a所占4个字节的第一个字节的地址;
  • p是指针变量,占用4/8个字节的空间,p也是有自己的地址,&p就拿到了p的地址。
  • pp也是指针变量,pp前有两个 * 则为二级指针变量,存放的是一级指针变量的地址;同理,ppp为三级指针变量,存放的是二级指针变量的地址。

5.指针数组

  • 整型数组是存放整型的数组、字符数组是存放字符的数组,那么,指针数组是什么?
  • 顾名思义,指针数组:是数组,是存放指针(地址)的数组。
int* parr[5];     // 存放整型指针的数组
char* pc[10];     // 存放字符型指针的数组

如上所示,int 存放整型指针数组、char 存放字符型指针数组。元素类型只要是指针类型就是指针数组。

6.数组指针

  • 字符指针是指向字符的指针,存放的是字符的地址; char ch = 'w'; char * pc = &ch;
  • 整型指针是指向整型的指针,存放的是整型的地址; int n = 100; int * p = &n;
    那么,数组指针又是什么呢?
  • 顾名思义,数组指针:是指针,是指向数组的指针,存放的是数组的地址。
    请看下面这行代码:
int (* p)[10] ;   

在这行代码中,p先和 * 结合,说明p是⼀个指针变量变量,然后指着指向的是⼀个大小为10个整型的数组。所以 p 是⼀个指针,指向⼀个数组,叫 数组指针
这里要注意:[ ] 的优先级要高于 * 号的,所以必须加上()来保证p先和 * 结合 。

  • 数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?就是我们之前学习的 &数组名。如下:
int arr[10] = {0};      
&arr;   // 得到的就是数组的地址
int (* p)[10] = &arr;    // 如果要存放个数组的地址,就得存放在数组指针变量中

数组指针类型解析:

int (*p) [10] = &arr;
|  |   |
|  |   |
|  |   p指向数组的元素个数
|  p是数组指针变量名
p指向的数组的元素类型

7.函数指针

  • 函数指针:是指针,是指向函数的指针,存放的是函数的地址。
  • 函数名就是函数的地址,当然也可以通过 &函数名 的方式获得函数的地址。
    如果我们要将函数的地址存放起来,就得创建函数指针变量,那么函数指针变量的写法是什么呢?
    函数指针变量的写法其实和数组指针非常类似。请看下面代码:
void test()
{
   printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;
int Add(int x, int y)
{
   return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的

函数指针类型解析:

int (* pf3) (int x, int y)
|     |   ------------
|     |     |
|     |   pf3指向函数的参数类型和个数的交代
|   函数指针变量名
pf3指向函数的返回类型

8.函数指针数组

数组是一个存放相同类型数据的存储空间。例如:

int *arr[10];  //数组的每个元素是int*
  • 整型指针数组是数组,是存放整型指针的数组。
  • 当然,函数指针数组:也是数组,是存放函数指针的数组。
    示例如下:
int (*parr1[3])();

在这行代码中,parr1 先和 [ ] 结合,说明 parr1是数组;接着数组的内容是 int ( * )() 类型的函数指针。
完整示例代码:

int main()
{
    int* arr[10];   // 整型指针的数组 
    //  1、函数指针数组 
    int (* p1)(int, int) = Add;    
    int (* p1)(int, int) = Sub;   
    // 2、函数指针数组 —— 存放函数指针的数组
    int (* pArr[4])(int, int) = {Add; Sub};

    return 0;
}

9.typedef关键字

  • typedef 是用来类型重命名的,可以将复杂的类型简单化。
    请看下面这行代码:
typedef unsigned int uint;

本行代码中,是将unsigned int 重命名为uint,即将复杂的类型简单化。

  • 如果是指针类型,也是可以这样写。比如,将 int * 重命名为 ptr_t,示例如下:
typedef int* ptr_t;
  • 数组指针类型 int( * )[5]重命名parr_t,示例如下:
typedef int(*parr_t)[5];  //新的类型名必须在*的右边
  • 函数指针类型 void( * )(int) 类型重命名为 pf_t,示例如下:
typedef void(*pfun_t)(int);  //新的类型名必须在*的右边

10.函数指针类型的用途(转移表)

  • 代码案例:使用函数指针数组实现简单的计算器
#include <stdio.h>
int add(int a, int b)
{
    return a + b;
}
int sub(int a, int b)
{
    return a - b;
}
int mul(int a, int b)
{
    return a*b;
}
int div(int a, int b)
{
    return a / b;
}
int main()
{
    int x, y;
    int input = 1;
    int ret = 0;
    int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
    do
    {
        printf("*************************\n");
        printf(" 1:add 2:sub \n");
        printf(" 3:mul 4:div \n");
        printf(" 0:exit \n");
        printf("*************************\n");
        printf( "请选择:" );
        scanf("%d", &input);
        if ((input <= 4 && input >= 1))
        {
            printf( "输入操作数:" );
            scanf( "%d %d", &x, &y);
            ret = (*p[input])(x, y);
            printf( "ret = %d\n", ret);
         }
         else if(input == 0)
         {
            printf("退出计算器\n");
         }
         else
         {
            printf( "输⼊有误\n" ); 
         }
     }while (input);
     return 0;
}

八、回调函数

  • 回调函数是⼀个通过函数指针调用的函数。
  • 如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。
  • 回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的⼀方调用的,用于对该事件或条件进行响应。
    代码示例:
#include <stdio.h>
int add(int a, int b)
{
    return a + b;
}
int sub(int a, int b)
{
    return a - b;
}
int mul(int a, int b)
{
    return a * b;
}
int div(int a, int b)
{
    return a / b;
}
void calc(int(*pf)(int, int))
{
    int ret = 0;
    int x, y;
    printf("输⼊操作数:");
    scanf("%d %d", &x, &y);
    ret = pf(x, y);
    printf("ret = %d\n", ret);
}
int main()
{
    int input = 1;
    do
    {
       printf("******************
       printf(" 1:add 
       printf(" 3:mul 
       printf("******************
       printf("请选择:");
       scanf("%d", &input);
       switch (input)
       {
          case 1:
                calc(add);
                break;
          case 2:
                calc(sub);
                break;
          case 3:
                calc(mul);
                break;
          case 4:
                calc(div);
                break;
          case 0:
                printf("退出程序\n");
                break;
          default:
                printf("选择错误\n");
                break;
          }
      } while (input);
      return 0;
}

在这段代码中,请看void calc部分和case部分,我们把调用的函数的地址以参数的形式传递过去,使用函数指针接收,函数指针指向什么函数就调用什么函数,这里其实使用的就是回调函数的功能。

九、sizeof 和 strlen

1.sizeof

  • sizeof是计算变量所占内存空间大小的,单位是字节。如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。
    代码示例:
#inculde <stdio.h>
int main()
{
    int a = 10;
    printf("%d\n", sizeof(a));
    printf("%d\n", sizeof a);   // sizeof 后面是变量的时候()可以省略
    printf("%d\n", sizeof(int));
    return 0;
}

本段代码中,输出结果为4、4、4;显然, sizeof 只关注占用内存空间的大小,不关注内存中存放什么数据。

  • sizeof 在计算大小的时候其实是根据类型推算的。
  • sizeof 的操作数如果是一个表达式,表达式是不参与计算的。
    请看下面这段代码:
#inculde <stdio.h>
int main()
{
    short s = 10;  // 占2个字节
    int i = 2 ;    // 占4个字节
    int n = sizeof(s = i + 4);  // 截断
    printf("%d\n", n);   
    printf("%d\n", s);
    return 0;
}

在本段代码中,其结果为:n = 2;s = 10;分析如下:

  • 首先,在第6行代码中,i + 4 为整型 + 整型,结果还是整型,占4个字节;
    s 为short 类型,占2个字节,s = i + 4 则表示为 将4个字节放入到2个字节中,此时发生截断,最终为2个字节。
  • 其次,sizeof 在计算大小的时候是根据类型推算的,当sizeof 看到s为short类型时,其返回值就是short所占的字节大小(2)。所以,n = 2
  • 最后,第6行代码中,操作数 s = i + 4是 sizeof 的一个表达式,所以其表达式是不参与计算的。最终 s 的结果仍为 10

2.strlen

  • strlen是C语言库函数,功能是求字符串长度。函数原型如下:
size_t strlen (const char * str);

统计的是从 strlen 函数的参数 str 中这个地址开始向后, \0 之前字符串中字符的个数。
strlen 函数会⼀直向后找 \0 字符,直到找到为止,所以可能存在越界查找。
示例如下:

#include <stdio.h>
#include <string.h>
int main()
{
    char arr[] = "abc\0def";
    printf("%d\n", strlen(arr));

    return 0;
}

strlen 统计的是 \0 之前字符串中字符的个数。所以在本段代码中, \0 之前字符为abc,个数为3,最终其返回结果为 3.

3.sizeof 与 strlen 的对比

sizeof

  • sizeof 是操作符;
  • sizeof 计算操作数所占内存的大小,单位是字节;
  • 不关注内存中存放什么数据,sizeof 的操作数如果是一个表达式,表达式是不参与计算的。

strlen

  • strlen 是库函数,使用需要包含头文件string.h
  • strlen 是求字符串长度的,统计的是\0之前字符的个数
  • 关注内存是否有\0,如果没有 \0 ,就会持续往后找,可能会越界。

十、数组指针笔试练习题

1.一维数组

下面这段代码的输出结果是什么呢?

#include <stdio.h>
int main()
{
    int a[] = {1,2,3,4};     
    printf("%zd\n",sizeof(a));                       //  16
    printf("%zd\n",sizeof(a+0));                     //  4/8
    printf("%zd\n",sizeof(*a));                      //  4
    printf("%zd\n",sizeof(a+1));                     //  4
    printf("%zd\n",sizeof(a[1]));                    //  4
    printf("%zd\n",sizeof(&a));                      //  4/8
    printf("%zd\n",sizeof(*&a));                     //  16
    printf("%zd\n",sizeof(&a+1));                    //  4/8
    printf("%zd\n",sizeof(&a[0]));                   //  4/8
    printf("%zd\n",sizeof(&a[0]+1));                 //  4/8

    return 0;
}

【解析如下:】

  • sizeof(数组名):数组名表示整个数组,计算的是整个数组的大小,单位是字节。a 是一个数组,存放了4个int类型,一个int类型为4个字节,所以 sizeof(a)结果为16.
  • 数组名是数组首元素的地址。在 sizeof(a+0) 中,数组名a并没有单独放在sizeof内部,也没有&,所以a就是数组首元素的地址,是地址 大小就是 4/8 个字节。所以sizeof(a+0)结果为 4/8.
  • a是数组首元素的地址,也就是第一个元素的地址,在sizeof( * a)中,a==&a[0];* a 就是第一个元素,也就是 a[0];a的类型为整型,一个整型为4个字节,所以sizeof( * a)结果为4.
  • 在 sizeof(a+1) 中,数组名 a 没有单独放在sizeof内部,所以 a 就是数组首元素的地址,也就是 &a[0],则 a+1 就是第二个元素的地址,也就是 &a[1];是地址 大小就是 4/8 个字节。所以sizeof(a+1)结果为4/8.
  • a[1] 就是数组的第二个元素,在 sizeof(a[1]) 中,sizeof(a[1]) 就是计算第二个元素的大小,单位是字节,所以sizeof(a[1])结果为4.
  • &数组名:数组名表示整个数组,取出的是数组的地址。数组的地址也是地址,是地址 大小就是 4/8 个字节。所以sizeof(&a)结果为4.
  • ( * p)是访问一个数组的大小;&a 取出数组的地址,类型是数组指针,即:int ( * p)[4] = &a,此时p是指向一个大小为4的整型数组;在 sizeof( * &a) 中,对数组指针进行解引用( * &a) ,也就是访问的是整个数组,sizeof 的整个大小也就是16,所以sizeof( * &a) 结果为16.
    也可以这样看:* 和 & 相抵消,所以 sizeof( * &a) 就像于是 sizeof(a) ,大小为16字节,所以sizeof( * &a) 结果为16.
  • &a 取出的是数组的地址,&a+1 是跳过整个数组后的地址,是地址 大小就是 4/8 个字节。所以sizeof(&a+1)结果为4/8.
  • a[0] 为数组首元素,&a[0] 取的是数组首元素的地址,是地址 大小就是 4/8 个字节。所以sizeof(&a[0])结果为4/8.
  • &a[0] 是数组首元素的地址,&a[0]+1 就是数组第二个元素的地址,也就是 &a[1],是地址 大小就是 4/8 个字节;所以sizeof(&a[0]+1)结果为4/8.

2.字符数组

  1. 下面这段代码的输出结果是什么呢?
#include <stdio.h>
int main()
{
    char arr[] = {'a','b','c','d','e','f'};
    printf("%zd\n", sizeof(arr));                       // 6
    printf("%zd\n", sizeof(arr+0));                     // 4/8
    printf("%zd\n", sizeof(*arr));                      // 1
    printf("%zd\n", sizeof(arr[1]));                    // 1
    printf("%zd\n", sizeof(&arr));                      // 4/8
    printf("%zd\n", sizeof(&arr+1));                    // 4/8
    printf("%zd\n", sizeof(&arr[0]+1));                 // 4/8

    return 0;
}

【解析如下:】

  • char 类型的数据占1个字节。arr 数组中共有6个字符,所以sizeof(arr)的大小为6.
  • arr 是数组首元素的地址,arr+0 还是数组首元素的地址,是地址 大小就是 4/8 个字节。所以sizeof(arr+0)结果为4/8
  • arr 是数组首元素的地址,* arr 就是首元素,占一个字符大小,就是1字节,所以sizeof( * arr)结果为1.
  • arr[1] 为数组的第二个元素,大小为1字节,所以 sizeof(arr[1])结果为1.
  • &arr 是数组的地址,是地址 大小就是 4/8 个字节。所以sizeof(&arr)结果为4/8.
  • &arr+1 是跳过整个数组后的地址,指向的是 f 后面,是地址 大小就是 4/8 个字节。所以sizeof(&arr+1)结果为4/8.
  • &arr[0] 表示的是数组首元素的地址,&arr[0]+1 则为数组第二个元素的地址,是地址 大小就是 4/8 个字节。所以sizeof(&arr[0]+1)结果为4/8.
  1. 下面这段代码的输出结果是什么呢?
#include <stdio.h>
#include <string.h>
int main()
{
    char arr[] = {'a','b','c','d','e', 'f'};
    printf("%zd\n", strlen(arr));                       // 随机值
    printf("%zd\n", strlen(arr+0));                     // 随机值
    printf("%zd\n", strlen(*arr));                      // 非法访问(错误)
    printf("%zd\n", strlen(arr[1]));                    // 非法访问(错误)
    printf("%zd\n", strlen(&arr));                      // 随机值
    printf("%zd\n", strlen(&arr+1));                    // 随机值
    printf("%zd\n", strlen(&arr[0]+1));                 // 随机值

    return 0;
}

【解析如下:】

  • strlen 是求字符串长度的,统计的是 \0 之前字符的个数。在 strlen(arr) 中,因为数组中没有明确给出 \0 ,所以 strlen(arr)结果为随机值。
  • arr 为数组首元素地址,arr+0 仍为数组首元素地址,在 strlen(arr+0) 中,因为数组中没有明确给出 \0 ,所以strlen(arr+0)结果为随机值。
  • (* arr) 得到的是字符a,a 的ASCII码是97,strlen的参数是指针类型,传任何参数 strlen都会把它当作地址,此时会将传入的97当作地址,就会形成非法访问。所以strlen( * arr)结果会报错,非法访问。
  • arr[1] 为数组中的第二个元素,即:字符 b ,b 的ASCII码是98,同样,strlen会将传入的98当作地址,此时就会形成非法访问。所以 strlen(arr[1])结果会报错,非法访问。
  • &arr 是数组指针类型,strlen 的参数是 const char * ,此时将该类型传入strlen 中,strlen 得到的是数组起始位置的地址(首元素的地址)。因为数组中没有明确给出 \0 ,所以strlen(&arr)结果为随机值。
  • &arr+1 是跳过整个数组后的地址,指向的是 f 后面。因为数组中没有明确给出 \0 ,所以strlen(&arr+1)的结果仍为随机值。
  • &arr[0]+1 表示的是数值第二个元素的地址。因为数组中没有明确给出 \0 ,所以strlen(&arr[0]+1)的结果仍为随机值。
  1. 下面这段代码的输出结果是什么呢?
#include <stdio.h>
int main()
{
    char arr[] = "abcdef";
    printf("%llu\n", sizeof(arr));                     // 7
    printf("%llu\n", sizeof(arr+0));                   // 4/8
    printf("%llu\n", sizeof(*arr));                    // 1
    printf("%llu\n", sizeof(arr[1]));                  // 1
    printf("%llu\n", sizeof(&arr));                    // 4/8
    printf("%llu\n", sizeof(&arr+1));                  // 4/8
    printf("%llu\n", sizeof(&arr[0]+1));               // 4/8

    return 0;
}

【解析如下:】

  • "abcdef" 的内容为{ a, b, c, d, e, f, \0 }共7个内容,所以sizeof(arr)的大小为7.
  • arr 是数组首元素的地址,arr+0 还是数组首元素的地址,是地址 大小就是 4/8 个字节。所以sizeof(arr+0)结果为4/8.
  • arr 是数组首元素的地址,* arr 就是首元素,占一个字符大小,就是1字节,所以sizeof( * arr)结果为1.
  • arr[1] 为数组的第二个元素,大小为1字节,所以 sizeof(arr[1])结果为1.
  • &arr 是数组的地址,是地址 大小就是 4/8 个字节。所以sizeof(&arr)结果为4/8.
  • &arr+1 是跳过整个数组后的地址,指向的是 f 后面,是地址 大小就是 4/8 个字节。所以sizeof(&arr+1)结果为4/8.
  • &arr[0] 表示的是数组首元素的地址,&arr[0]+1 则为数组第二个元素的地址,是地址 大小就是 4/8 个字节。所以sizeof(&arr[0]+1)结果为4/8.
  1. 下面这段代码的输出结果是什么呢?
#include <stdio.h>
#include <string.h>
int main()
{
    char arr[] = "abcdef";
    printf("%lld\n", strlen(arr));                     // 6
    printf("%lld\n", strlen(arr+0));                   // 6
    printf("%lld\n", strlen(*arr));                    // 非法访问(错误)
    printf("%lld\n", strlen(arr[1]));                  // 非法访问(错误)
    printf("%lld\n", strlen(&arr));                    // 6
    printf("%lld\n", strlen(&arr+1));                  // 随机值
    printf("%lld\n", strlen(&arr[0]+1));               // 5

    return 0;
}

【解析如下:】

  • strlen 是求字符串长度的,统计的是 \0 之前字符的个数"abcdef" 的内容为{ a, b, c, d, e, f, \0 }。所以 strlen(arr)结果为6。
  • arr 为数组首元素地址,arr+0 仍为数组首元素地址,"abcdef" 的内容为{ a, b, c, d, e, f, \0 }所以strlen(arr+0)结果为6。
  • (* arr) 得到的是字符a,a 的ASCII码是97,strlen的参数是指针类型,传任何参数 strlen都会把它当作地址,此时会将传入的97当作地址,就会形成非法访问。所以strlen( * arr)结果会报错,非法访问。
  • arr[1] 为数组中的第二个元素,即:字符 b ,b 的ASCII码是98,同样,strlen会将传入的98当作地址,此时就会形成非法访问。所以 strlen(arr[1])结果会报错,非法访问。
  • &arr 是数组指针类型,strlen 的参数是 const char * ,此时将该类型传入strlen 中,strlen 得到的是数组起始位置的地址(首元素的地址)。因为数组内容为{ a, b, c, d, e, f, \0 },其中包含 \0 ,所以strlen(&arr)结果为6。
  • &arr+1 是跳过整个数组后的地址,指向的是 f 后面。因为数组中 f 后面是否有 \0 未知,所以strlen(&arr+1)的结果为随机值。
  • &arr[0]+1 表示的是数组第二个元素的地址。因为数组内容为{ a, b, c, d, e, f, \0 },其中包含 \0 。即从 b 开始向后查找,查找到 \0 之前====>b,c,d,e,f. 所以strlen(&arr[0]+1)的结果为5。

5.下面这段代码的输出结果是什么呢?

#include <stdio.h>
int main()
{
    char *p = "abcdef";
    printf("%lld\n", sizeof(p));                       // 4/8
    printf("%lld\n", sizeof(p+1));                     // 4/8
    printf("%lld\n", sizeof(*p));                      // 1
    printf("%lld\n", sizeof(p[0]));                    // 1
    printf("%lld\n", sizeof(&p));                      // 4/8
    printf("%lld\n", sizeof(&p+1));                    // 4/8
    printf("%lld\n", sizeof(&p[0]+1));                 // 4/8
    
    return 0;
}

【解析如下:】

  • p 是一个指针变量,sizeof(p) 就是指针变量的大小,为4/8字节。
  • p+1 是字符b的地址,是地址 大小就是 4/8 个字节。
  • *p 是首元素(首字符),大小是1字节。
  • p[0] == *(p+0),就是字符串中的首字符,大小为1字节。
  • &p 是 p 的地址,是地址 大小就是 4/8 个字节。
  • &p+1 是跳过p变量后的地址,是地址 大小就是 4/8 个字节。
  • &p[0]+1 是字符串中第二元素的地址,也就是 b 的地址,是地址 大小就是 4/8 个字节。

6.下面这段代码的输出结果是什么呢?

#include <stdio.h>
#include <string.h>
int main()
{
    char *p = "abcdef";
    printf("%lld\n", strlen(p));                       // 6
    printf("%lld\n", strlen(p+1));                     // 5
    printf("%lld\n", strlen(*p));                      // 非法访问(错误)
    printf("%lld\n", strlen(p[0]));                    // 非法访问(错误)
    printf("%lld\n", strlen(&p));                      // 随机值
    printf("%lld\n", strlen(&p+1));                    // 随机值
    printf("%lld\n", strlen(&p[0]+1));                 // 5
    
    return 0;
}

【解析如下:】

  • strlen 是求字符串长度的,统计的是 \0 之前字符的个数"abcdef" 的内容为{ a, b, c, d, e, f, \0 }。p 中存放的是 a 的地址,从 a 的地址开始向后访问,所以 strlen(p)结果为6。
  • p 为字符串中首元素地址,p+1 为字符串中第二个元素的地址,也就是 b 的地址,从 b 的地址开始向后访问,所以strlen(p+1)结果为5。
  • (* p) 得到的是字符a,a 的ASCII码是97,strlen的参数是指针类型,传任何参数 strlen都会把它当作地址,此时会将传入的97当作地址,就会形成非法访问。所以strlen( * p)结果会报错,非法访问。
  • p[0] == * (p+0) == * p,得到的是字符a,所以strlen(p[0])结果会报错,非法访问。
  • &p 是 p 的地址,从 p 所占空间的起始位置开始查找的,其返回结果是随机值。
  • &p+1 是跳过p变量后的地址,其返回结果为随机值。
  • &p[0]+1 表示的是字符串中第二个元素的地址。即从 b 开始向后查找,所以strlen(&p[0]+1)的结果为5。

3.二维数组

下面这段代码的输出结果是什么呢?

#include <stdio.h>
int main()
{
    int a[3][4] = {0};  
    printf("%d\n",sizeof(a));                          // 48
    printf("%d\n",sizeof(a[0][0]));                    // 4
    printf("%d\n",sizeof(a[0]));                       // 16
    printf("%d\n",sizeof(a[0]+1));                     // 4/8
    printf("%d\n",sizeof(*(a[0]+1)));                  // 4
    printf("%d\n",sizeof(a+1));                        // 16
    printf("%d\n",sizeof(*(a+1)));                     // 16
    printf("%d\n",sizeof(&a[0]+1));                    // 4/8
    printf("%d\n",sizeof(*(&a[0]+1)));                 // 16
    printf("%d\n",sizeof(*a));                         // 16
    printf("%d\n",sizeof(a[3]));                       // 16

return 0;
}

【解析如下:】

  • sizeof(a) 计算的是整个二维数组的大小,单位是字节。即:3×4×4=48字节。
  • a[0][0]) 是第一行第一个元素,大小为4个字节。
  • a[0] 是第一行的数组名,单独放到了 sizeof 中,计算的是第一行的大小,其中包括4个元素,大小为16(4×4)字节。
  • 在 sizeof(a[0]+1) 中,数组名 a[0] 没有单独放在sizeof内部,所以 a[0] 就是数组首元素的地址,也就是 &a[0][0],则 a[0]+1 就是第一行第二个元素的地址,也就是 &a[0][1],大小为4/8个字节。
  • a[0]+1 是第一行第二个元素的地址,则 a[0]+1 解引用就是第一行第二个元素,大小是4个字节。
  • 在 sizeof(a+1)中,数组名 a 没有单独放在sizeof内部,所以 a 就是数组首元素的地址,也就是第一行的地址,**则 a[0]+1 就是第二行的地址,也就是 &a[1],其中包括4个元素,大小为16个字节。
  • (* (a+1) )是第二行的地址 ,大小为16字节。
  • a[0] 是第一行的数组名,&a[0] 取出的是第一行的地址,&a[0]+1 得到的就是第二行的地址,是地址,大小就是4/8个字节。
  • &a[0]+1 为第二行的地址,对第二行的地址解引用,大小为16字节。
  • a 就是数组首元素的地址,也就是第一行的地址,对第一行的地址解引用,大小为16字节。
  • 在 sizeof(a[3]) 中,数组名 a[3] 单独放到 sizeof 中,计算的是第一行的大小,其中包括4个元素,大小为16(4×4)字节。

数组指针小结

以上就是数组指针的练习题,从以上练习中,我们得出数组名有以下几点意义:

  • 1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
  • 2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
  • 3. 除此之外所有的数组名都表示首元素的地址。

结尾

好了!本文到这里就结束了!以上内容就是对指针知识的学习,希望能够帮助你更好地学习指针。能够跟着本篇文章学习下来的小伙伴们,为你们点赞!请继续加油!

OK!完结撒花!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容