C生万物 | 指针进阶 · 炼狱篇

一、再谈指针大小

在【指针初阶】的一开始,我就有讲到过对于指针的大小在32为平台下均为4个字节,在64位平台下均为8个字节上面在学习了各种指针的进阶操作后,我们再来看看

代码:

  • 首先给出接下去我要进行对比的代码
c 复制代码
int Add(int x, int y)
{
    return x + y;
}

int Sub(int x, int y)
{
    return x - y;
}

int* Open(int n)
{
    int* a = (int*)malloc(sizeof(int) * n);
    if (NULL == a)
    {
        perror("fail malloc");
        exit(-1);
    }
    return a;
}

int main(void)
{
    int a = 10;
    int* p = &a;
    int** pp = &p;

    double f = 3.14;
    double* ff = &f;
    double** fff = &ff;

    char ch = 'c';
    const char* pc = &ch;
    char* const pc2 = &ch;

    int a1 = 1;
    int b1 = 2;
    int c1 = 3;
    int d1 = 4;
    int e1 = 5;

    int* parr[5] = { &a1, &b1, &c1, &d1, &e1 };

    int b[5] = { 1,2,3,4,5 };
    int(*pb)[5] = &b;
    
    int n = 10;
    int* arr = Open(n);

    int (*pf)(int, int) = Add;
    int (*pfArr[2])(int, int) = { Add, Sub };
    int (*(*ppfArr)[2])(int, int) = &pfArr;

    printf("%d\n", sizeof(p));
    printf("%d\n", sizeof(pp));
    printf("%d\n", sizeof(ff));
    printf("%d\n", sizeof(fff));

    printf("%d\n", sizeof(pc));
    printf("%d\n", sizeof(pc2));

    printf("%d\n", sizeof(parr));
    printf("%d\n", sizeof(pb));

    printf("%d\n", sizeof(arr));
    printf("%d\n", sizeof(pf));
    printf("%d\n", sizeof(pfArr));
    printf("%d\n", sizeof(ppfArr));

    return 0;
}

运行结果:

  • x86环境下运行的结果如下
  • x64环境下运行的结果如下

【总结一下】:

  • 所以,一个指针的大小完全不是取决于它的类型,而是取决于平台,无论你是一级指针、二级指针、指针数组、数组指针等等,只要它在32位平台下,那么均为4个字节。因为在32位平台下,有32个地址总线,那么32位就可以表示【2^32^】的寻址范围,==即任何一个值都需要用32个1或0来表示==
  • "指针需要多大空间,取决于地址的存储需要多大空间" ,每一个数据的表示都是32位,1B又等于8b ,因此每一块地址都需要**【4B】**的空间去容纳,又因为在内存中地址值得其实就是指针,这也就是为何在32位平台下👉==指针均为4个字节==👈

二、难题攻坚战🗡

接下去是我在日常学生的作业题里跳出来的一些难题,放在这里与读者一同讨论一番

第一题【指针运算】

下面关于指针运算说法正确的是:( C

css 复制代码
A.整形指针+1,向后偏移一个字节
B.指针-指针得到是指针和指针之间的字节个数
C.整形指针解引用操作访问4个字节
D.指针不能比较大小

解析:

注意:此题说法不明确,整型指针的类型不一定就是int*,可能还有长整型、短整型

A. 错误,因为整型指针的类型为int*,所以 + 1会向后偏移4个字节 B. 错误,两个指针相减,指针必须指向一段连续空间,减完之后的结构代表两个指针之间相差元素的个数 C. 正确,整型指针指向的是一个整型的空间,解引用操作访问4个字节 D. 错误,指针中存储的是地址,地址可以看成一个数据,因此是可以比较大小的

第二题【指针偏移】

下面代码的结果是:( B

c 复制代码
int main()
{
    int arr[] = { 1,2,3,4,5 };
    short* p = (short*)arr;
    int i = 0;
    for (i = 0; i < 4; i++)
    {
        *(p + i) = 0;
    }

    for (i = 0; i < 5; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}
css 复制代码
A.1 2 3 4 5
B.0 0 3 4 5
C.0 0 0 0 5
D.1 0 0 0 0

解析:

但就从代码来看,你可以在脑海中模拟一下试试最后的结果会是多少🤔

  • 马上我们就来分析洗一下,首先看到一个arr数组,数组里面有5个元素,每个元素的类型都是int,然后取到arr的数组名,【数组名为首元素地址 】,那么它的类型就是int*,但是呢此时我将它的地址转换为short*,即短整型指针,给到对应的指针变量p,接下去通过for循环内部指针的偏移来访问到数组中的内容,对数组的值去进行一个修改,那此时会有几个值发生变化呢?
c 复制代码
for (i = 0; i < 4; i++)
{
    *(p + i) = 0;
}
  • 在【指针初阶】我就有讲过对于指针的类型来说决定了一次可以访问多少个字节,那看到前面是short,对于短整型来说一次就可以访问两个字节的数据,又因为arr是一个整型数组,里面的每个元素在内存中所占的字节数都是4,那么这个for循环执行了4次后,就访问了8个字节的数据,即前两个数组元素被改成了【0】

真的是这样的吗?我们可以通过【内存】来看看

  • 通过上面这四张图所对应的for循环四次执行过程,相信你一定明白了为什么访问四次只能改变两个数组元素,就在于short*类型的指针一次能访问的也就只有2个字节,访问4次是8个字节,那也刚好是2个数组元素的大小

运行结果:

第三题【指针访问字节数】

在小端机器中,下面代码输出的结果是:( C

c 复制代码
int main()
{
	int a = 0x11223344;
    char *pc = (char*)&a;
    *pc = 0;
    printf("%x\n", a);
    return 0;
}
css 复制代码
A.00223344
B.0
C.11223300
D.112233

解析:

  • 本题其实和第二题比较类似,变量a是一个十六进制的整数,&a取出它的地址后类型即为int*,然后将其强转为char*后令指针pc指向这块地址,但是指针pc却无法访问到变量a中所有的数据,因为char*类型的指针解引用一次只能访问1个字节
c 复制代码
*pc = 0;

一样,我们还是可以通过观察【内存】来看看*pc究竟修改了多少内容

  • 以下我是使用一行显示一个字节,这样可以方便观察修改的情况,因为VS是小端存放的,因此可以观察到原本的11223344放到内存中变成了44332211
  • 可以看到,通过*pc我们访问到了变量a的第一个字节,并且将其修改为【0】
  • 不过这个是在内存中的样子,此时若是要显示打印在屏幕上的话还要将其再做一个转换,所以最后显示的结果便是11223300

第四题【指针数组】

下面哪个是指针数组:( A

ini 复制代码
A.int* arr[10];
B.int* arr[];
C.int **arr;
D.int (*arr)[10];

解析:

  • 本题你可能会觉得很简单,完全没有比较讲,但是我在看同学们做下来的情况后,却发现这题也错得蛮多的,所以专门放在这里讲解一下

A. 这个没问题,是最标准的指针数组,arr和[]先结合,表明它是一个数组,数组有10个元素,每个元素都是一个int*类型的指针

B. 你可能会觉得它也是一个指针数组,但是放到VS中去编译一下是编不过的,报出了不允许使用不完整的类型 的错误,如果你看不明白这一点的话,说明C语言数组不过关,可以再回去看看,若是在定义数组的时候,没有指定数组大小的话,就一定要为其进行初始化,也就是要给出数组具体的内容,否则编译器都不知道要分配多少空间给他

C. 这是一个二级指针,并不是指针数组

D. 对于int (*arr)[10]来说,arr与*相结合了,所以它是一个指针,什么指针呢?朝外一看有一个[],表明这个指针指向一个数组的地址,数组里面有10个元素,每个元素都是的类型都是int。那很明显这就是一个【数组指针

第五题【数组指针 + 函数指针】

声明一个指向含有10个元素的数组的指针,其中每个元素是一个函数指针,该函数的返回值是int,参数是int*,正确的是( C

css 复制代码
A.(int *p[10])(int*)
B.int [10]*p(int *)
C.int (*(*p)[10])(int *)
D.int ((int *)[10])*p

解析:

A. 错误,()加的地方不对,编译报错,应该是这样int(*p[10])(int*);此时的p为一个数组,数组里面存放都是指针,而且均为函数指针,该函数指针指向的函数返回值是int,参数是int*。但是不符合题意,题面意思是p要为一个指针

B/D. []只能在标识符右边,双双排除

C. p首先和*结合,表明它是一个指针,指针朝外一看,它指向一个数组,数组有10个元素,去掉数组名后,可以看到每个元素的类型,为int(*)(int*),都是一个函数指针,并其他们都指向一个返回值是int,参数是int*的函数。即这是一个【指向函数指针数组的指针】,符合题目意思

三、指针和数组笔试题解析✒

本模块,我将通过sizeof()strlen()在指针与数组上的映射,来带你更加深入地理解它们在内存的分布

  1. 不了解sizeof的可以先了解一下 链接
  2. 不了解strlen的可以先了解一下 链接
  3. 数组相关可以先看看这篇文章 链接

sizeof() 是操作符,不是函数,它是用来计算对象或者类型创建的对象所占内存空间的大小

1、简易一维数组

首先第一个先简单一点,来个一维数组练练手 (doge),==请你仅通过草稿纸验算的方式,计算出每个结果==

代码:

c 复制代码
int main(void)
{
	//一维数组
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof(a + 0));
	printf("%d\n", sizeof(*a));
	printf("%d\n", sizeof(a + 1));
	printf("%d\n", sizeof(a[1]));
	printf("%d\n", sizeof(&a));
	printf("%d\n", sizeof(*&a));
	printf("%d\n", sizeof(&a + 1));
	printf("%d\n", sizeof(&a[0]));
	printf("%d\n", sizeof(&a[0] + 1));
	return 0;
}

解析:

算出来了嘛🤗,我们来一一分析一下

  • 首先第一点你要知道的就是数组名即为首元素地址,不过有两个例外
    • sizeof(数组名) ------ 数组名表示整个数组,计算的是整个数组的大小,单位是字节
    • &数组名 ------ 数组名表示数组名表示整个数组,取出的是整个数组的地址,取出的是整个数组的地址
  • 除了以上两点外直接出现数组名即为==首元素地址==

  1. 首先第一个,a作为数组名单独放在sizeof内部,此时计算的是数组的总大小,单位是【字节】,数组中有4个元素,每个元素的类型都是int,即4个字节,那结果就是 16
c 复制代码
printf("%d\n", sizeof(a));
  1. 接下去第二个,此时a是并不是单独放在sizeof内部,而且也没有&,所以数组名a指的就是首元素地址,==对于一个地址来说我们在指针初阶部分讲了在内存中就是指针==,那对于指针来说即为 4 / 8,在32位平台下运行就是4个字节,在在32位平台下运行就是8个字节
c 复制代码
printf("%d\n", sizeof(a + 0));
  1. 然后第三个,通过观察可以发现,并没有出现sizeof(数组名)&数组名这两种形态,所以a就是首元素地址,类型是int*那么*a就是对其进行解引用,获取到的便是【首元素】,类型是int,那一个整型的大小是多少呢?没错,就是 4个字节
c 复制代码
printf("%d\n", sizeof(*a));
  1. 第四个,a指的是首元素地址,a + 1向后偏移了一个整型,即为第二个元素的地址,那就和第二个一样计算的是一个地址的大小,即指针的大小,为 4 / 8
c 复制代码
printf("%d\n", sizeof(a + 1));
  1. 第五个很简单,就是计算数组中第二个元素的大小,那很简单,就是 4个字节
c 复制代码
printf("%d\n", sizeof(a[1]));
  1. 第六个&a即为&数组名,取出是整个数组的地址,这个其实我在上面初讲指针的时候有提到过,整个数组的地址其实和数组的首元素的地址是一样的,那么整个数组的地址它也是一个地址,那只要是地址即为 4 / 8个字节
c 复制代码
printf("%d\n", sizeof(&a));
  • 通俗一些来说,其实地址就像是门牌号一样,那数组中每个元素的地址和整个数组的地址并没有高低贵贱之分,而是,不是说数组的地址就来得高大上一些,它们一视同仁
  1. 小插曲,我们再来看第七个,第一眼就看到&a,那么还是一样取出的是整个数组的地址,那对整个数组的地址进行解引用得到的便是整个数组,因为数组的地址是存到到数组指针中的,它的类型即为int (*)[5]
    • 对一个整型指针解引用获取到的是一个整型
    • 对一个字符型指针解引用获取到的是一个字符
    • 对一个数组指针解引用获取到的是一个数组
  • 那么此时计算的便是一个数组的大小,即为 16 ,其实你也可以这么去看,&是取到这个数组的地址,*又对进行解引用,通过这个地址找到找到这里面所存放的内容,这么一来一去就产生了抵消,最后也就变成了sizeof(a),那便是我们上面说到过的,这种sizeof(数组名)的形式,==计算的也是整个数组的大小==
c 复制代码
printf("%d\n", sizeof(*&a));
  1. 我先说第九个:很明显,就是去计算数组首元素地址的大小,为 4 / 8
c 复制代码
printf("%d\n", sizeof(&a[0]));
  1. 好,下面两个一起说,好做一个对比,&a[0]上面讲过了,是取出数组首元素的地址,它的类型是int*,那对于一个整型指针来说,以此可以访问的字节数是4个字节,即数组中的一个元素,那么此时它就指向了2这个元素的地址处,它就等价于&a[1];对于&a来说,取出的是整个数组的地址,其类型为int (*)[4],那么它一次性可以访问的字节数即为整个数组的所有元素之和,此时它就指向了4后面的这块地址
c 复制代码
printf("%d\n", sizeof(&a[0] + 1));
printf("%d\n", sizeof(&a + 1));
  • 可以看到,无论是指向哪里,它们都是一个地址,一个地址的大小就为 4 / 8字节

运行结果:

  • 首先在32(x86)为平台下运行试试【指针大小为4个字节】
  • 然后在64(x64)为平台下运行试试【指针大小为8个字节】

好,看完整型数组后,我们来看看字符数组

2、不带 '\0' 的字符数组

  • 首先你要明确的一点就是这个数组里面有几个元素,在数组章节我就有着重讲到过,若是将一个字符数组定义成如下形式的话,末尾是不会带\0的,数组会根据初始化的内容来确定它里面的元素个数,所以下面这个数组的数组元素是6个而不是7个

代码:

c 复制代码
int main(void)
{
	//字符数组
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", sizeof(arr));
	printf("%d\n", sizeof(arr + 0));
	printf("%d\n", sizeof(*arr));
	printf("%d\n", sizeof(arr[1]));
	printf("%d\n", sizeof(&arr));
	printf("%d\n", sizeof(&arr + 1));
	printf("%d\n", sizeof(&arr[0] + 1));
	return 0;
}

解析:

  1. 首先第一个:很明显就是我们上面所提到的特殊模式。因此sizeof(数组名)计算的就是整个数组的大小,数组有6个元素,每个元素都是char类型的,在内存中占1个字节,那结果就是 6
c 复制代码
printf("%d\n", sizeof(arr));
  1. 第二个:arr并不是单独放在sizeof中的,那它就是数组名,数组名即为首元素地址,此时计算的就是第一个元素的地址,但只要地址的话即为 4 / 8字节
c 复制代码
printf("%d\n", sizeof(arr + 0));
  1. 第三个:arr既没有单独放在sizeof中,也没有&,那么它就是首元素地址,对首元素地址进行*解引用,此时获取到的就是首元素,数组的首元素是[a],类型是【char】,那大小即为 1
c 复制代码
printf("%d\n", sizeof(*arr));
  1. 第四个很简单,就是计算数组arr中第一个元素的大小,那也是 1个字节
c 复制代码
printf("%d\n", sizeof(arr[1]));
  1. 那下面这个呢? 很明显看到&数组名,那么取出的就是整个数组的地址,上面说过了,它还是一个地址,那么就是 4 / 8字节
c 复制代码
printf("%d\n", sizeof(&arr));
  1. 一样的,&arr取到整个数组的地址,因为其类型是一个数组指针,那么 + 1就跳过一个数组的大小,此时它就指向了字符[f]后面的这个地址,那既然是地址的话也还是 4 / 8字节
c 复制代码
printf("%d\n", sizeof(&arr + 1));
  1. 最后,&arr[0]取到的是数组首元素的地址,它的类型是int*,+ 1可以访问4个字节的大小,即为&arr[1],此时它算的还是一个地址的大小,那请说出答案!: 4 / 8字节
c 复制代码
printf("%d\n", sizeof(&arr[0] + 1));

运行结果:

  • 首先在32(x86)为平台下运行试试【指针大小为4个字节】
  • 然后在64(x64)为平台下运行试试【指针大小为8个字节】

看完sizeof()之后,我们再来看看strlen()

strlen() 是函数,它是用来求字符串长度的,计算的是字符串之前 '\0' 出现的字符个数,如果没有看到 '\0' 会继续往后找

代码:

c 复制代码
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr + 0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr + 1));
printf("%d\n", strlen(&arr[0] + 1));

解析:

  • 首先来看一下它的内存分布,可以看到它是内存中一块连续的空间,但是因为这个字符数组并没有\0,所以我们无法确定它的结束标志
  1. 那么我们首先来看第一个,arr放在strlen()内部,注意这里并不是sizeof()内部,而且也没有&数组名,所以arr表示的是数组的首元素地址,就是从字符a这个位置开始往后计算这个字符数组的长度,上面说过了,strlen()会向后查找直到\0为止,但是呢又因为这个字符数组内部本身并不存在\0,那它就会继续往后查找,==可是对于arr数组后面的这块位置是随机的,是否具有\0是不确定的==,因此最终的结果是 随机值
c 复制代码
printf("%d\n", strlen(arr));
  1. 好,接下去第二个其实和第一个是一样的,因为arr是首元素地址,+ 0之后的结果还是一样的,为 随机值
c 复制代码
printf("%d\n", strlen(arr + 0));
  1. arr依旧是首元素地址,那对首元素地址进行解引用获取到的就是【首元素】,首元素就是字符a,类型是char,但是strlen()要为其传入的是类型为char*的地址,所以strlen就会将a的ASCLL码值97当做地址进行传入
c 复制代码
printf("%d\n", strlen(*arr));
  • 对于ASCLL码我们它是美国国家标注协会ISO所定义的标准,那在我们C语言中就是已经存在了的,==它是属于内存中的一块固定地址,这块地址我们是无法去使用的,内存也不会将其分配给我们==,所以此时我们使用strlen()去访问这块地址的时候其实属于非法访问,调试一下看看💻
  • 可以看到我标出的位置0x00000061这个位置发生了冲突,这是在内存中以十六进制的形式来表示地址,将其转换为十进制表示即为97,那正好对应了我们上面所分析的为strlen()传入了字符a的ASCLL码值97,所以可以看出这块地址确实是无法访问的
  1. 那如果你清楚了上面这个,其实对于下面的这个也是一样的,arr[1]这个数组元素也不是一个地址,而是一个字符,此时会将b的ASCLL码值98传入strlen(),那此时我们去访问这块地址的时候也是属于非法访问
c 复制代码
printf("%d\n", strlen(arr[1]));

可以看到,最后结果也是 err,通过进制转换可以发现正好与b的ASCLL码值98相对应

  1. 可以看到,出现了&数组名的情况,那此时我们就获取到了整个数组的地址,那整个数组的地址和数组首元素的地址是一样的,都位于字符a这个位置,那么从这个位置向后找\0,就和第一题一样是不确定的,字符数组本身不具备\0,其他地址处也可能没有\0,因此最终的结果为 随机值
c 复制代码
printf("%d\n", strlen(&arr));
  1. 在上一题中,&arr取出了整个数组的地址,它的类型为int (*)[6],是一个数组指针,那一个数组指针 + 1就跳过了整个数组,来到了字符f后面的这块地址处,接着向后查找,去找\0,但结果我们知道,还是一个 随机值,不过这个随机值会比上面的这个随机值少6,因为要减去已经跳过的6个数组元素
c 复制代码
printf("%d\n", strlen(&arr + 1));
  1. 好接下去最后一个,首先取到的是数组的首元素地址,它的类型是char*,那么 + 1就会跳过一个数组的元素,来到&arr[1]这个为止,即字符b所在的地址处,此时继续向后查找还是一个 随机值 ,这个随机值会比上面的这个随机值少1,因为要减去已经跳过的1个数组元素a
c 复制代码
printf("%d\n", strlen(&arr[0] + 1));

运行结果:

  • 这里没有指针,我就直接在32为平台下运行了,将两个结果为err的注释掉后,最终的结果和我们上面分析的是一样的

看完了上面这些,你是否对指针和数组的理解又有了进一步的理解呢😉坐稳了,下一班车即将到达:car:

3、带 '\0' 的字符数组

好,看完了不带\0的字符数组后,我们再来看看带\0的字符数组

代码:

  • 首先你要清楚的一点是,这个字符数组中有几个元素,可以看到,后面的"abcdef"是字符串,对于字符串来说末尾是自带\0的,这个我之前也有通过调试带同学们看过,所以这个数组中有7个元素
c 复制代码
int main(void)
{
	char arr[] = "abcdef";
	printf("%d\n", sizeof(arr));
	printf("%d\n", sizeof(arr + 0));
	printf("%d\n", sizeof(*arr));
	printf("%d\n", sizeof(arr[1]));
	printf("%d\n", sizeof(&arr));
	printf("%d\n", sizeof(&arr + 1));
	printf("%d\n", sizeof(&arr[0] + 1));
	return 0;
}

解析:

  1. 好,首先来看第一个,出现了我们数组的sizeof(数组名),那么此刻求出的便是整个数组的大小,那上面说到过了这个数组中有7个元素。每个元素都是char类型,所以最后的结果就是 7
c 复制代码
printf("%d\n", sizeof(arr));
  1. 接下去arr既没有单独出现在sizeof()内部,也没有取地址,那么它指的就是首元素地址,看到如下图所示,一个地址的大小便是 4 / 8个字节
c 复制代码
printf("%d\n", sizeof(arr + 0));
  1. 看到第三个,此时arr还是代表首元素地址,对其*解引用访问到的就是首元素【a】那么一个char类型的元素在内存中所占的字节数即为 1
c 复制代码
printf("%d\n", sizeof(*arr));
  1. 第四个其实也是一样的,字符数组的第二个元素为【b】,所占的字节数也为 1
c 复制代码
printf("%d\n", sizeof(arr[1]));
  1. 终于看到&数组名了,此时取出的是整个数组的地址,只要是地址的话即为 4 / 8个字节
c 复制代码
printf("%d\n", sizeof(&arr));
  1. 一样,取出整个数组的地址后,接着向后偏移的话就会跳过一整个数组,那取到的便是\0后面的这块地址,既然是地址的话,请说出它的大小: 4 / 8个字节
c 复制代码
printf("%d\n", sizeof(&arr + 1));
  1. 最后,也是一样 ,偏移一个字节后来到了字符【b】的位置,其地址的大小也为 4 / 8个字节
c 复制代码
printf("%d\n", sizeof(&arr[0] + 1));

运行结果:

  • 首先在32(x86)为平台下运行试试【指针大小为4个字节】
  • 然后在64(x64)为平台下运行试试【指针大小为8个字节】

看完sizeof()后,再来看看strlen()是怎样的情况

c 复制代码
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr + 0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr + 1));
printf("%d\n", strlen(&arr[0] + 1));
  1. 首先arr并没有单独放在sizeof()内部,也没有&,所以数组名代表首元素地址,那从首元素地址往后找\0,最后的结果即为 6
c 复制代码
printf("%d\n", strlen(arr));
  1. 那么首元素地址向后偏移0个字节,还是一样的结果,为 6
c 复制代码
printf("%d\n", strlen(arr + 0));
  1. 下面两个一起来说,过程不再赘述,所传入strlen()都是数组的元素,但是因为strlen()只能接收一个地址,因此会出现非法访问
c 复制代码
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));

  1. 取出整个数组的地址,向后去找\0,那答案也很明显就是 6
c 复制代码
printf("%d\n", strlen(&arr));
  1. &arr取出了整个数组的地址,+ 1 跳过了整个数组,根据上面所讲其为 随机值,而且这个随机值的大小会是原本的减去7,因为跳过了整个数组的所有元素
c 复制代码
printf("%d\n", strlen(&arr + 1));
  1. 从首元素地址向后偏移一个字节,就是&arr[1],向后遍历碰到\0为止,结果便是 5
c 复制代码
printf("%d\n", strlen(&arr[0] + 1));

运行结果:

4、字符指针【⭐】

终于把数组讲完了,接下去我们来"玩玩指针"

代码:

  • 首先你要知道的是,这个字符指针p里面存放的是什么?前面我们在【指针进阶·提高篇】中有讲到过,若是将一个字符串给到一个字符指针做接收,那么这个字符指针里面存放的便是字符串中第一个字符的地址
c 复制代码
int main(void)
{
	char* p = "abcdef";
	printf("%d\n", sizeof(p));
	printf("%d\n", sizeof(p + 1));
	printf("%d\n", sizeof(*p));
	printf("%d\n", sizeof(p[0]));
	printf("%d\n", sizeof(&p));
	printf("%d\n", sizeof(&p + 1));
	printf("%d\n", sizeof(&p[0] + 1));
	return 0;
}

解析:

  1. 既然p里面存放的是一个字符的地址,那它也是一个地址,既然是地址的话,就为 4 / 8个字节
c 复制代码
printf("%d\n", sizeof(p));
  1. 因为这个指针p的类型是char*,所以 + 1会跳过一个char类型的数据,此时就指向了字符串中的第二个字符所在的地址,那也是一样为 4 / 8个字节
c 复制代码
printf("%d\n", sizeof(p + 1));
  1. 接下去两个一起说,对指针p进行解引用,此时就访问到了这块地址中所存放的内容【a】,那么一个char类型的数据在内存中占1个字节;第二个其实就是【a】,那它们的结果都是一样的,均为 1
c 复制代码
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));

来仔细地分析一波它们的原理:mag:

  • 在数组章节其实有提到过,对于下面这样int* p = arr;其实【p】与【arr】是等价的,所以在通过for循环访问数组元素的有四种形式 ⇒ arr[i] == *(arr + i) == *(p + i) == p[i]
c 复制代码
int arr[5] = {1,2,3,4,5};
int* p = arr;

那其实上面的也可以类似地这么去解释 ⇒ *p == *(p + 0) == p[0],它们其实都是等价的

  1. 好,接下去再来看这个,对指针p再去进行取地址&的操作,那我们【指针初阶】的时候时候讲二级指针时有说到过,一个一级指针可以接收普通变量的地址,一个二级指针则是可以接收一级指针的地址。那么此刻我对一个一级指针去取地址,它的类型就从char*转变成了char**
c 复制代码
printf("%d\n", sizeof(&p));
  • 上面我有讲到过,一个指针在【解引用】或者【向后访问】的时候看得是它的指针类型,通过下面这张进行对比就可以很清晰地看出p在进行&取地址操作后就变成了一个二级字符指针,每次可以访问的数据个数即为一个char*类型。不过最后的结果还是一个地址的大小为 4 / 8个字节

可以再看看这张图👇

  1. 看了上面的这些后,相信下面这个你也是手到擒来,因为&p是一个二级指针类型,+ 1便跳过了一个一级指针的大小,即一个char*的距离,那其实也就是这个字符串,到达了\0的后头,可它还是一个地址,只要是一个地址,大小即为 4 / 8个字节
c 复制代码
printf("%d\n", sizeof(&p + 1));
  1. 但是下面这个就不一样了,因为p指向的是这个字符串的首字符,那&p[0]就是取出它所在的地址,类型为char*,那么 + 1便跳过了一个char类型的数据,来到了第二个字符的地址处,所以结果还是 4 / 8个字节
c 复制代码
printf("%d\n", sizeof(&p[0] + 1));

运行结果:

  • 首先在32(x86)为平台下运行试试【指针大小为4个字节】
  • 然后在64(x64)为平台下运行试试【指针大小为8个字节】

看完了sizeof(),那一定少不了strlen(),继续发车:car:

代码:

  • 首先它的内存布局没有更换,还是上面的这个
c 复制代码
printf("%d\n", strlen(p));
printf("%d\n", strlen(p + 1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p + 1));
printf("%d\n", strlen(&p[0] + 1));

解析:

  1. 因为p是指向这个字符串的首元素地址,那我们就从这里朝后面找\0,很明显一下子就找到了,那么最后的结果就是 6
c 复制代码
printf("%d\n", strlen(p));
  1. 那p的类型是char*,+ 1跳过的就是一个char类型的数据,来到了字符【b】的地址处,向后找\0的话就最后的结果即为 5
c 复制代码
printf("%d\n", strlen(p + 1));
  1. 下面两个也一起说了,如果你上面看得认真的话这里一定很快就能反应过来,*p取到的就是字符【a】,那我们知道,给strlen()是不可以传入地址之外的其他数,那么这里就会产生非法访问
c 复制代码
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));

  1. 接下去,我们来看看&p,这里一定要看清楚取到的谁的地址,这里并不是字符串的地址,而是指针p自己的地址,但是这个指针p只是存放了字符串首元素的地址,但是并不知道它里面有没有\0,所以在向后遍历的时候并不知何时结束,所以它的结果就是 随机值
c 复制代码
printf("%d\n", strlen(&p));
  1. 然后再来看看&p + 1,上面说到指针p的类型是char*,在&取地址后它的类型就变成了char**,+ 1便会跳过一个char*类型的数据,那也就是这个字符指针,此时便指向了它末尾的这个位置,从这里向后去进行寻找\0的话还是存在一个不确定的因素,所以最后的结果还是 随机值
c 复制代码
printf("%d\n", strlen(&p + 1));

💬 那我现在想问一个问题:上面这个&p&p + 1所查找的随机值是否存在联系?

  • 那有同学说,指针p在内存中占了4个字节嘛,64位就是8个字节,那这不就求出来了吗?其实这样算是有问题的,指针p里面存的什么你知道吗?万一在中间突然出现一个\0呢,因此这也是不确定的,它们之间并不存在联系
  1. 第六个其实和第二个是一样的,p里面存放的是【a】的地址,&p[0]那也是这块地址,+ 1后便指向【b】这块地址了,具体可以参照第二题的图示,最后的结果还是 5
c 复制代码
printf("%d\n", strlen(&p[0] + 1));

运行结果:

5、二维数组

最后,我们再来看看比较难以理解的二维数组

  • 首先你要清楚下面这个二维矩阵是几行几列的,很明显是三行四列的
  • 然后我们再一一来讲说代码

代码:

c 复制代码
int main(void)
{
	//二维数组
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof(a[0][0]));
	printf("%d\n", sizeof(a[0]));
	printf("%d\n", sizeof(a[0] + 1));
	printf("%d\n", sizeof(*(a[0] + 1)));
	printf("%d\n", sizeof(a + 1));
	printf("%d\n", sizeof(*(a + 1)));
	printf("%d\n", sizeof(&a[0] + 1));
	printf("%d\n", sizeof(*(&a[0] + 1)));
	printf("%d\n", sizeof(*a));
	printf("%d\n", sizeof(a[3]));
	return 0;
}

解析:

  1. 首先第一个就遇到了我们熟悉的sizeof(数组名),那计算的就是整个数组的大小,那这是一个二维数组,数组是三行四列的,总共十二个元素,每个元素的类型是int,为4个字节,那么总的大小就是 48
c 复制代码
printf("%d\n", sizeof(a));
  1. 接下去第二个,a[0][0]代表的是数组第一行第一列的元素,那这很简单,每个元素都是 4个字节
c 复制代码
printf("%d\n", sizeof(a[0][0]));
  1. 这个第三题,为了让读者可以很好地理解,我打算从一维数组开始讲起
  • 首先对于下面的一维数组arr,使用arr[0]arr[1]arr[2]便可以访问到数组中的每个元素,因为arr此时就是数组名

那对于二维数组呢?此时想去找到它里面的每个元素该怎么找,这个其实我在数组章节也有说起过

  • 我们可以将二维数组的每一行当做它的一个元素,那么下面这个数组就有三个元素,那要去访问到每一行中的每列元素该怎么做呢?此时我们需要使用到数组名,看到右侧的a[0][j]a[1][j]a[2][j],通过对【j】去进行一个控制从而可以访问到每一列上的具体元素,那我们可以将前面的a[0]a[1]a[2]看作是一个整体,那它们即为每一行的数组名
  • 此时再来看下面这道题就很简单了,因为a[0]为第一行的数组名,而且它是单独放在sizeof()内部的,所以计算的便是第一行这一整行的大小,里面有4个元素,每个元素都是4个字节,那么结果即为 16
c 复制代码
printf("%d\n", sizeof(a[0]));
  1. 接下去再来看下一个,此时a[0]并不是单独放在sizeof()内部,所以它指的就是首元素地址,即&a[0][0]这个地址,它的类型是int*,+ 便跳过了一个整型元素,来到了&a[0][1]的位置,那此时计算的就是一个地址的大小,即为 4 / 8个字节
c 复制代码
printf("%d\n", sizeof(a[0] + 1));
  1. 下面这个就是对上一题所取到的&a[0][1]的地址进行解引用,此时取到的便是这个地址上的元素,去计算一下它的大小便是 4
c 复制代码
printf("%d\n", sizeof(*(a[0] + 1)));
  1. 然后再来分析一下这个,a并没有单独放在sizeof()内部,也没有进行取地址的操作,所以它指的便是二维数组首元素的地址,那对于一个二维数组来说的首元素是什么呢?也就是第一行 ,那此时a取到的便是第一行的地址,因为需要存放一个数组的地址,所以它的类型便是一个数组指针类型即int (*)[4],那么一个数组指针 + 1跳过的便是一个数组 ,此时就来到了二维数组的第二行,取到的便是第二行的地址,但它终究还是个地址,只要是个地址的话大小即为 4 / 8个字节
c 复制代码
printf("%d\n", sizeof(a + 1));
  1. 接下去便是对这一行的地址去进行解引用,那么也就得到了第二行这一整行,此时计算便是这一整行的大小,便为 16
c 复制代码
printf("%d\n", sizeof(*(a + 1)));
  • 不过呢,对于上面这个其实有另一种思路,那就是我在上面讲字符指针时所说的指针解引用*与数组[]的转换公式,对于*(a + 1)可以转换为a[1],那这个我在上面有讲到过,即为二维数组第二行的数组名,那将其单独放在sizeof()内部形成sizeof(数组名),计算的也是第二行这整一行的大小
  1. 好接下去又出现我们前面所提的&数组名,因为a[0]为第一行的数组名,所以对它进行取地址就取到了这一整行的地址,它的类型也为一个数组指针 int (*)[4],那 + 1的话也会跳过整个数组,此时也就来到了第二行,那么取到的便是第二行的地址,地址的大小即为 4 / 8个字节
c 复制代码
printf("%d\n", sizeof(&a[0] + 1));
  • 讲了这么多,我们这里可以来做一个小总结,如果再去自己观察的话可以发现下面这三个取到的都是二维数组第二行的地址
    • &a[1]
    • a + 1
    • &a[0] + 1
  1. 好,接下去我们来看下面这个,这也就是对第二行的地址进行解引用,此时也就取到了第二行,通过上面的总结,你可以将其看做是sizeof(*&a[1]),那么此时【*】和【&】就可以进行相互抵消变为sizeof(a[1]),这样来看的话其实更加清晰了,因为a[1]是第二行的数组名,sizeof(数组名)计算的便是整个第二行这个一维数组的大小,那结果就是 16
c 复制代码
printf("%d\n", sizeof(*(&a[0] + 1)));
  1. 接下去再来看这个,此时a并没有单独放在sizeof()内部,也没有进行取地址的操作,那么a所代表的就是首元素地址,即第一行的地址,如果你举得有点难以理解的话可以把*a看作是*(a + 0),那便可以将其转换为a[0],也就是第一行的数组名,sizeof(a[0])计算的便是第一行的大小,结果为 16
c 复制代码
printf("%d\n", sizeof(*a));
  1. 好,来看最后一个,看到下面这个a[3]有些同学可能会疑惑,这个二维数组不是只有三行吗,第三行的数组名为a[2],那a[3]岂不是越界了!

如果用正常的数组思维确实是这样,但是这个a[3]放在sizeof()内部却不会出现任何问题,接下去我来讲讲为什么

  • 要知道,对于任何一个表达式来说具有2个属性,一个是【值属性】,一个是【类型属性】,例如3 + 5 = 8,最后的这个8它的值属性就是数字8,类型属性即为int但对于【sizeof()】来说,它在计算的时候只需要知道【类型属性】就可以了,类似我们之前写过的sizeof(int)sizeof(char)等等,对这些内置类型就可以计算出它的大小,并没有去实际地创造出空间
  • 那么对于下面这个a[3]来说,虽然看上去存在越界,但是sizeof()并不关心你有没有越界,而是知道你的类型即可,那么a[3]便是二维数组的第四行,虽然没有第四行,但是类型是确定的,那么大小就是确定的,计算sizeof(数组名)计算的是整个数组的大小,结果便是 16
c 复制代码
printf("%d\n", sizeof(a[3]));

运行结果:

延伸拓展:

对于上面所讲到的sizeof(),我们再来拓展一下,之前在操作符章节有详细讲过,要时刻sizeof()它并不是一个函数,而是一个操作符!

  • 看下面的这段代码,定义了一个short短整型的变量num,还有一个整型变量a,然后在printf()打印语句中计算了num = a + 5,那最后它的结果会是多少呢?
c 复制代码
int main(void)
{
	short num = 20;
	int a = 1;
	printf("%d\n", sizeof(num = a + 5));
	printf("%d\n", num);
	return 0;
}
  • 通过运行结果可以看到,第一个结果是2,第二个结果是20。可能对于这两个结果你都有些诧异,但若是你知道一些规则的话就不会感到奇怪了,对于sizeof()内部的表达式是不会进行计算的,所以num = a + 5在sizeof()里头根本就不起作用,最后的结果计算的还是num在内存中所占的字节大小,那么对于short短整型来说在内存中所占的字节数为【2】

那可能还是有刨根问底的同学,我再讲得详细一些

  • 程序的编译链接章节有讲过一个.c.exe中间会经过【编译】+【链接】,最后才到【运行】,那对于num = a + 5这个表达式来说,是在最后的运行阶段才会去进行计算的,但是sizeof()在计算处理的时候确实在【编译】的环节,此时里面的表达式早就被忽略了,因此最后的值计算的还是变量num

  • 那既然这里面的表达式没有执行的话,最后的结果就还是num一开始初始化的样子

四、指针相关历年笔试真题汇总【更新中...】✍

笔试题1

代码:

c 复制代码
int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1);
    printf("%d,%d", *(a + 1), *(ptr - 1));
    return 0;
}
//程序的结果是什么?

解析:

来分析一下本题该如何进行计算

  • 首先创建了一个整型数组a,里面有5个元素,每个元素都int类型,接着看到下面&a取出了整个数组的地址,类型为一个数组指针int (*)[5],对它 + 1跳过整个数组来到【5】后面的这块地址处,接着将这个地址强制类型转换为int*,然后由指针ptr指向它
  • 然后我们来看输出打印语句,*(a +1)其实就是a[1],这里要注意,上面只是让ptr指向(int)(&a + 1)的这个地址,然后a并有动,现在的a代表的就是首元素地址,即&a[0],那么 + 1跳过四个字节便指向了数组元素2所在的这块地址,最后解引用便访问到了这块地址上的内容
  • 最后的话就是这个*(ptr - 1),因为其类型为一个整型指针,所以 +/- 1会跳过4个字节,那此时它就指向了数组元素5所在的这块地址,*解引用便访问到了【5】

运行结果

  • 最后打印结果来看一下

笔试题2

代码:

c 复制代码
struct Test
{
	int Num;
	char *pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
	printf("%p\n", p + 0x1);
	printf("%p\n", (unsigned long)p + 0x1);
	printf("%p\n", (unsigned int*)p + 0x1);
	return 0;
}
//程序的结果是什么?

解析:

来分析一下本题该如何进行计算

  • 首先是给到了一个结构体,然后使用这个结构体定义出来一个结构体指针p,对其进行偏移的操作,那既然是结构体的话,就要先知道其大小,这里题目就给出了为20个字节,如果不懂的同学可以看看校招热门考点 ------ 结构体内存对齐
  • 相信很多同学一看到这个0x1就懵了,不知道这是什么东西,0x的话代表一个十六进制,==在内存中我们表示地址一般用的都是十六进制==。那么题目给出条件说p的值为0x100000,我们知道进制之间是可以相互转换,其实这就是一个整型数值,那p是一个结构体,则怎么能指向一个整型地址呢,于是在三条打印语句的前面,我们还应该加上这句话,将这个地址强制类型转换成一个【结构体指针】类型
c 复制代码
p = (struct Test*)0x100000;
  • 那接下去我们就来分析一下三条打印语句最后会输出的结果是什么
    • 首先是p + 0x1,对于0x1上面讲到过了是一个十六进制,那它就是十进制的1,这个表达式相当于就是p + 1,此时对一个结构体指针 + 1的话跳过的便是一个结构体,那结构体的大小我们刚才算了是20个字节,转换成十六进制变为【14】,所以最后的结果就是0x100014
    • 接下去第二个 (unsigned long)p + 0x1,这里将这个结构体指针p强转成一个无符号的长整型,那么现在这个p就不再是一个指针类型了,它就是一个整型,0x1也是整型,两个整型相加也就是我们小学就学过的计算题,最后的结果便是0x100001
    • 最后第三个(unsigned int*)p + 0x1,这里将这个结构体指针p强转成一个整型指针,然后再 + 1,那指针 + 1我们知道取决于它所指向的元素类型为int,那么 + 1便跳过了4个字节,最后的结果便是0x100004

运行结果

  • 最后打印结果来看一下【十六进制会将前面的0x转换为00

笔试题3

代码:

c 复制代码
int main()
{
	int a[4] = { 1, 2, 3, 4 };
	int* ptr1 = (int*)(&a + 1);
	int* ptr2 = (int*)((int)a + 1);

	printf("%x, %x", ptr1[-1], *ptr2);
	return 0;
}

解析:

来分析一下本题该如何进行计算

  • 本题和第一题其实很类似,也是&a取到了整个数组的地址,然后 + 1跳过整个数组,再将其强转为int*类型的地址,便可以让ptr1指向这块地址
  • 第二个的话就有点新奇了,首先a既没有单独放在sizeof()内部,也没有&数组名,因此其代表的就是首元素的地址,那有同学就感到很奇怪,把一个地址强转为int,也就整型,真的可以吗?

💬 这当然是可以的,地址我们都是使用十六进制来表示的,强转为整型那其实就是转为十进制

  • 我们可以假设它的地址为0x00000015,强转之后就变成了 21,接下去再对这个整数 + 1那就变成了22,然后看到外面又有一个强制类型转换,转为int*,那也就是再把它转换成一个地址的形式,以十六进制来进行表示,即0x00000016
  • 如上你去对比一下上面这两个地址就可以知道,它们之间相差了一个字节的大小,那其实这样的操作使得ptr2指向了数组首元素地址的往后一个字节

光这么说说太抽象了,我们一起来画个图理解一下

  • 可以看到,这里我画出了这个数组在内存中的布局,因为放到内存中数组里面的每个数一定是以十六进制 的形式来进行存放,即0x 00 00 00 010x 00 00 00 02这样,又因为数组元素在内存中都是连续存放的,所以我们可以将它们放在并排的位置上,==而且对于VS来说是以【小端】的形式进行存放,因此可以看出我是倒着画的==
  • 首先ptr2上面有分析过了,- 1的话往前访问4个字节的数据,那么也就刚好来到了04这个地方;而对于ptr1来说,它指向的位置则是01向后数一个字节,即00这个位置。又因为这两个指针的类型都是int*,所以在打印的时候可以访问4个字节的数据
c 复制代码
printf("%x, %x", ptr1[-1], *ptr2);
  • 现在要使用printf()将结果打印在屏幕上了,那既然我们以小端的形式倒着存入内存中,拿出来也要以小端的形式倒着拿 ,那么拿出来后,前者便是02 00 00 00,后者便是00 00 00 04。打印在屏幕上的话就为【200000】和【4】,会自动去除前导的0

运行结果

  • 最后打印结果来看一下

延伸拓展【汇编观察】

  • 我们在打印语句中加上这两句代码,通过ptr1ptr2去修改数组中的一些内容
c 复制代码
ptr1[-1] = 1;
*ptr2 = 1;
  • 通过汇编可以查看到,数组a在内存中的存放形式,就是将我上图所画的内容分为四行即可
  • 接下去可以看到,通过ptr[-1] = 1这句代码,将数组中第四个元素改成了01 00 00 00,那么从内存中取出来便是00 00 00 01,那也就是【1】
  • 那对于ptr2来说,对其进行解引用便可以向后访问四个字节,可以看到数组第一个元素所占的后三个字节和数组第二个元素所占的第一个字节发生了修改【看红色标记】。我也将其改为了1,此时ptr2就实现了指定的字节访问并修改对应数据的操作

来看看最终的结果验证一下,确实就是像我分析的那样

笔试题4

代码:

c 复制代码
int main()
{
	int a[3][2] = { (0, 1), (2, 3), (4, 5) };	//逗号表达式
	int* p;		//*p一次访问四个字节
	p = a[0];
	printf("%d", p[0]);		//*(p + 0)
	return 0;
}
//程序的结果是什么?

解析:

来分析一下本题该如何进行计算

  • 首先可以看到,定义并初始化 了一个3行2列的二维数组,然后声明了一个指针,将二维数组的首元素地址即第一行的地址赋给指针p,最后打印p[0]
  • 那是否看出哪里有问题呢?其实在第一行代码就出现了问题,仔细观察数组初始化的大括号{},里面的(0, 1), (2, 3), (4, 5)是二维数组的初始化吗? 如果忘了就在看看数组章节的内容吧,正确的初始化方式应该是{{0, 1}, {2, 3}, {4, 5}};外面是大括号,里面的每行也是大括号

💬 那有同学问:那这个里面的小括号()是什么呢?数组有初始化吗?

  • 还记得我们在操作符章节介绍的【逗号】表达式吗?忘了就再去看一下,对于(0, 1)编译器会将其当做是一个表达式,这整个表达式最后的结果是最后一个逗号后面的表达式,也就是【1】,那对于后面的也是一样,所以数组最后的初始化结果应该是{1, 3, 5}

我们通过画图来理解一下

  • 下面就是这个二维初始化完后的样子,因为每行只有2个元素,所以5初始化的就是第二行的第一列。此时再往下看到p = a[0],那么p就指向了这个二维数组第一行的地址,其实也就是&a[0][0]
c 复制代码
p = a[0];
  • 此刻再去访问p[0]的话其实就是访问&a[0][0]这块地址上的内容,它也可以转换成*(p + 0),最后的结果就是【1】
c 复制代码
printf("%d", p[0]);	

运行结果

  • 最后打印结果来看一下

笔试题5

代码:

c 复制代码
int main()
{
	int a[5][5];
	int(*p)[4];
	p = a;
	printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
	return 0;
}
//程序的结果是什么?

解析:

来分析一下本题该如何进行计算

  • 首先声明了一个五行五列的二维数组,还有一个数组指针,这个数组指针指向一个有4个元素,每个元素都是int的数组。接下去把a赋值给到指针p,a是单独出现的,因此表示的就是首元素地址,即第一行的地址。但是细心的同学一定发现二维数组的每一列都是5个元素,但是数组指针却只能存放有4个元素的一维数组

💬 那这不是乱套了吗?

  • 我们通过打印来看看,确实可以看出编译器报出了类型不兼容的问题,但是这不会有很大的影响,既然p只能存放4个元素的一维数组,那最后一个不要不就好了👈

所以其实可以初步感受到本题不是那么容易,接下去我通过画图来进行分析

  • 首先看到打印语句中的&a[4][2],其为a数组第5行第2列的元素所在的地址,在下面我也整个二维数组画成了并排的样子,这其实就是它在内存中真实存放的样子,那我们很快就可以定位到a[4][2]这个元素,然后取到它所在的这块地址
  • &p[4][2]呢?刚才我们分析到了指针p只能存放元素个数为4的数组,那在【指针初阶】的时候有讲到过==数组指针的类型决定了它所能访问的字节个数==,去掉指针名后,我们可以看出它类型是int (*)[4],所以 + 1可以一次向后访问4个字节,那么 + 2,+ 3呢?看看下图就一目了然了
  • 接下去我们要去取到&p[4][2],当数组指针p进行了4次偏移后,我们可以找到p + 4的位置,那根据指针和数组的转换公式可以得知*(p + 4)就可以取到这一行,那*(*(p + 4) + 2)就相当于p[4][2],具体可以看上图,那么对这个数组元素取地址&也就取到了它所在的这块地址

  • 最后,我们就要去打印&p[4][2] - &a[4][2]的结果了,分别是以【%p】和【%d】的形式来进行打印
c 复制代码
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
  • 那在内存中我们知道,左边是低地址,右边是高地址 ,那么通过图示就可以看出&p[4][2]的地址其实是要比&a[4][2]来得小的,那么前者 - 后者的话就会是一个负数,二者都是地址,地址在内存中其实就是指针,那根据前面所学过的知识,两个指针相减计算的是它们之间所相差的元素个数,那么从图中很明显可以看出它们之间相差的元素个数即为 4
  • 那么使用%d进行打印的时候最后的结果就是【-4

💬 那使用%p进行打印呢?会是什么样子

  • 上面也有讲到过,若是使用%p进行打印的话最后就是以十六进制 的形式显示,如果你有自己自己看做数据在计算机内部的存储,那可以知道在计算机内部都是二进制,而且都是以补码 的形式在进行计算,不过输出到外设(显示器)上都是以原码的形式
  • 所以对于这个【-4】来说,我们要将其以%p也就是地址的形式打印出来,不过地址不讲究什么原、反、补的概念,所以它会将放到计算机内部的这个补码当做是地址进行打印,那我们还要将一串的二进制序列4位为一组转换成十六进制才可以,那最后的结果便是【FFFFFC】

运行结果

  • 最后打印结果来看一下

笔试题6

代码:

c 复制代码
int main()
{
	int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int* ptr1 = (int*)(&aa + 1);
	int* ptr2 = (int*)(*(aa + 1));
	printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
	return 0;
}
//程序的结果是什么?

解析:

来分析一下本题该如何进行计算

  • 首先这还是一个二维数组,我依旧是把它画成了内存并排的样子,也是为了能够让读者更加清楚数组在内存中的布局,首先第一个ptr上面也有讲到过很多了,这里便不再赘述;然后是ptr2,aa即为数组首元素地址,那也就是第一行的地址,其类型为int (*)[5],+ 1跳过一整行,此时也是指向了第二行的地址,再对其进行*解引用也就访问到了第二行,最后再将其转换为int*类型赋给ptr2
  • 那么打印语句就很好理解了,ptr1 - 1指针往前偏移了4个字节,指向了数组元素10所在的这块地址,*解引用也就拿到了【10】,ptr2 - 1也是同理,因为二维数组在内存中也是连续存放的,所以6前面的元素即为5,此时拿到了数组元素【5】

运行结果

  • 最后打印结果来看一下

笔试题7

代码:

c 复制代码
int main()
{
	char* a[] = { "work","at","alibaba" };
	char** pa = a;
	pa++;
	printf("%s\n", *pa);
	return 0;
}
//程序的结果是什么?

解析:

来分析一下本题该如何进行计算

  • 首先看到有一个数组a,它是一个【指针数组】,数组里面存放的每一个元素都是char*,从下图可以看来,数组里面的三个指针分别存放三个字符串的首元素地址。接下去又取出这个数组的首元素地址,使pa指向它,此时二级指针pa的类型即为char**,这第一颗*是在告诉我们pa所指向的类型是一个【char*】的地址,这后一个*则是在告诉我们pa它是一个指针
  • 那么此时pa++就跳过了一个【char*】的元素,指向了该指针数组a的第二个元素所在地址,然后又通过*pa解引用找到了第二个元素中的内容,然后一看它也指向一块地址,于是呢就顺着这个地址找到了"at"的【a】在内存中的地址,因为字符串在内存中的空间是连续的,所以最后使用%s就打印出了"at"这个字符串

运行结果

  • 最后打印结果来看一下

笔试题8【⭐】

最后来一道压轴题,看看你对指针的掌握是否真的透彻了!

代码:

c 复制代码
int main()
{
	char* c[] = { "ENTER","NEW","POINT","FIRST" };
	char** cp[] = { c + 3,c + 2,c + 1,c };
	char*** cpp = cp;
	printf("%s\n", **++cpp);
	printf("%s\n", *-- * ++cpp + 3);
	printf("%s\n", *cpp[-2] + 3);
	printf("%s\n", cpp[-1][-1] + 1);
	return 0;
}
//程序的结果是什么?

解析:

本题由于比较复杂,所以我全程通过图示来讲解,准备上车:car:

  • 首先看到,还是和上题一样,有一个指针数组分别存放了四个字符串的首字符地址,然后接下去,又有一个二级指针数组存放了这个指针数组每一行的地址,而且是倒着存放的,即cp的第一行存放的是c数组第四行的地址,cp的第四行存放的是c数组第一行的地址
  • 接下去呢又有一个三级字符指针指向了二级指针数组cp的首元素地址,即第一行的地址
  • 首先来看到第一个打印语句,首先++cpp,那么cpp就会向后访问一个一个char*的元素,来到了cp数组的第二行,接下去第一个*解引用就拿到了c + 2这个地址,那么就顺着这个地址找到了c数组所在的这行地址,然后第二个*解引用则是拿到了c数组这一行的地址中所存放的内容,一看是一个地址,便顺着这个地址找到了P,然后使用%s进行打印最终的结果便是【POINT】
c 复制代码
printf("%s\n", **++cpp);

可以看一下动图演示

好,接下去再来看第二个,这句的话应该算是最复杂了,不过我们一一分析也不是什么难事

  • 首先看到++cpp的优先级最高,那么它就来到了cp数组的第三个位置,接下去进行*解引用,就拿到了这一行数组中的内容,即c + 1,接下去又进行了一个操作是--,那也就是对我们取出的c + 1进行运算即c + 1 - 1 = c,那么此时里面存放的就不再是c + 1这块地址,而是c这块地址,顺藤摸瓜找到了这块地址后再对其进行解引用,此时也就找到了E所在的地址,最后再 + 3即向后偏移3个字节也就是3个字符,就来到了后面的E所在的位置,使用%s进行打印便打印出了后面的【ER】
c 复制代码
printf("%s\n", *-- * ++cpp + 3);

看一下动图演示


好,接下去第三句打印,我们再来看看

  • 首先对于下面的表达式可以写成这种形式* *(cpp - 2) + 3,意思就是先让cpp向前偏移2个char**的位置,然后找到这个地址中所存放的值为c + 3,顺着这个地址找到了数组c这一行所在的地址,可以看到最前面还有一个*解引用,那么就获取到了字符F的地址,最后再 + 3然后以%s的形式打印,结果便是【ST】
c 复制代码
printf("%s\n", *cpp[-2] + 3);

来看最后一个,对指针的功底也要很深厚✒

  • 对于下面的表达式可以写成*(*(cpp - 1) - 1) + 1,那么首先就是将cpp向前偏移一个char**的位置【注意上一题cpp的移动不会影响此题】,然后对其进行*引用拿到cp数组第二行的内容,为c + 2,接下去对这个内容再 - 1,即为c + 2 - 1 = c + 1,那么此时它便指向了数组c中c + 1的这块,然后别忘了最前面还有一个*解引用,那么此时就拿到了c + 1这块地址中所存放的内容,是一个地址,继续顺藤摸瓜便找到了N,但是最后还有一个 + 1,那么就偏到了E,以%s进行打印最后的结果即为【EW】
c 复制代码
printf("%s\n", cpp[-1][-1] + 1);

运行结果

  • 最后打印结果来看一下【全体起立!!!】

视频解说📺

这里附上这道题的视频讲解版,上传到b站了,同学们可自己配合文章观看学习📺

[video(video-wFU5FUcV-1683212540615)(type-bilibili)(url-player.bilibili.com/player.html...(image-https%3A%2F%2Fimg-blog.csdnimg.cn%2Fimg_convert%2F21fe1c1ff6a13ed6e32ade260b731ea8.jpeg)(title-%25E5%259B%25BE%25E8%25A7%25A3%25E6%258C%2587%25E9%2592%2588%25E7%25AC%2594%25E8%25AF%2595%25E9%25A2%2598%25E3%2580%2590%25E6%25AD%25A5%25E6%25AD%25A5%25E6%2595%2599%25E5%25AD%25A6%25EF%25BC%258C%25E9%2580%259A%25E4%25BF%2597%25E6%2598%2593%25E6%2587%2582%25E3%2580%2591 "https://player.bilibili.com/player.html?aid=528232688)(image-https://img-blog.csdnimg.cn/img_convert/21fe1c1ff6a13ed6e32ade260b731ea8.jpeg)(title-%E5%9B%BE%E8%A7%A3%E6%8C%87%E9%92%88%E7%AC%94%E8%AF%95%E9%A2%98%E3%80%90%E6%AD%A5%E6%AD%A5%E6%95%99%E5%AD%A6%EF%BC%8C%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%E3%80%91"))]

---------------------------------------------------【总结篇】 ---------------------------------------------------

✍总结与提炼

好,来总结一下本文学习到的内容:book:

【指针初阶 · 入门篇】

在初阶篇中我们初步认识了什么是指针,主要照顾到对指针不够了解的同学,可以先有一个基本的概念

  • 首先我们了解了什么是指针,知道了【指针】、【地址】、【内存】三者之间的关系,清楚了内存中的指针和我们口头上所说的指针有和区别。还记得一个指针是几个字节吗❓
  • 初步认识指针后我们进一步加深了对指针的理解,明白了不同类型的指针所存在的意义:1.访问字节的范围 2.类型决定步长。然后我们就来谈了谈【野指针】,知道了在写代码的时候为何会产生野指针,也明白了如何对野指针去进行一个规避
  • 对指针有了一个理解后便开始上手操作指针,使用指针去进行一个运算。当然也清楚了指针和数组之间的关联
  • 最后呢我们浅浅地谈了谈二级指针,知道了什么是二级指针,以及它和指针与普通变量之间的区别;

【指针进阶 · 提升篇】

在进阶篇中我们对指针有了进一步的认识,清楚了字符指针的作用,辨析了指针常量与常量指针、指针数组与数组指针、指针函数与函数指针

  • 对于字符指针而言,它既可以指向单个字符 ,也可以指向一个字符串,其保存的便是这个字符串首字符的地址。有了这些基础知识后,我们就做了一道【剑指offer】的题目,还记得常量字符串的地址在内存中保存几份吗🙄

  • 听说*const也会擦出🔥火花🔥 对于指针常量来说,指针自己就是一个常量,不可以修改它的指向,但却可以修改其所指向地址中的内容 ,例:int* const p;对于常量指针来说,其所指向的地址中的内容是一个常量,不可修改,但却可以去修改它的指向 ,例:const int* p。有记忆口诀后相信很快分清它们之间的区别了。最后,还记得为了买一份凉皮而分手的情侣吗🤣

  • 指针还是数组真是傻傻分不清😵 对于指针数组来说,它是一个【数组】,数组里面存放的每一个元素都是指针 ,例:int* arr[5];对于数组指针来说,它是一个【指针】,这个指针所指向的是一个数组的地址 ,例:int (*p)[5]

    • 有了这些基础知识后,将指针与数组同函数去进行一个结合,实现一个传参,最好每一个下去都自己思考着想一遍,应该传递什么样的实参,形参又用什么来接受,==是一级指针呢,还是二级指针?是指针数组呢,还是数组指针?==
  • 接下去,真正地将指针融入到了函数中。首先我们讲了简单一些的指针函数 ,它就是一个普通的函数,只是返回类型是一个指针。不过要小心,我们不可以返回函数内部所申请的临时空间,因为它除了当前函数的作用域就销毁了,因此我特意在堆上去申请空间。例:int* Open(int, int)

  • 然后就是较难理解的函数指针 了,它本质是一个指针,所指向的是一个函数的地址,例:int (*p)(int, int) = &Add当然你在把函数的一直给到指针的时候也可以不加&,那也就相当于是【赋值】了,因此在使用函数指针我们可以不加前面的*,原本的调用形式是(*p)(3, 4),但你也可以写成p(3, 4)

  • 有了上面的基础知识后,我讲解了两道《C陷阱与缺陷》中的代码题,听完我的分析后你还对它们存在恐惧吗😆

    c 复制代码
    (*(void (*)())0)();
    c 复制代码
    void (*signal(int, void(*)(int)))(int);
  • 指针和数组又碰到一起了,原来还有存放函数指针的数组😮,这简直是太奇妙了。有了它,在面对多个函数逻辑的时候,我们不需要再写一个庞大的switch...case语句,而是直接使用【函数指针数组】存放这些函数的地址,这样就可以通过数组下标的控制去访问对应的函数了,例:int (* pfArr[2])(int, int) = {Add, Sub};还记得我们实现的 转移表 吗?

  • 那既然有数组指针这个东西,可以存放一个数组的地址,那我们上面所说的函数指针数组的地址也可以存放到一个指针中去,它就叫做【指向函数指针数组的指针】,既然它是一个指针,就要和*先结合,我们不需做太大的改动,只需要在函数指针数组的基础上给指针名ppfunArr前面加上一个*即可,不过为了防止其和[]先结合记得加上()哦。例:void (*(*ppfunArr)[5])(const char*) = &pfunArr;

  • 提升篇的末尾,我又讲了一个东西叫做【回调函数】,它是函数指针的一个经典应用,在实际开发的场景中也是被广泛地使用。我们可以将一个函数的地址传递给另一个函数,那这就需要另一个函数提供一个函数指针的参数。在这个函数内部,如是在某种特定条件成立的情况下,我们便可以通过这个函数指针去找到这个函数的地址,那么此时这个被调用的函数就被称为回调函数 。知道了这些后,还记得回调函数使用的三种场景吗?

    • [ 场景一 ]:模拟计算器的加减乘除。这里我们没用用到函数指针数组,而是将一开始的计算器做了一个修改
    • [ 场景二 ]:模拟qsort函数。这个场景尤为重要,我画了大量的精力进行讲解,配合图示,希望读者可以理解回调函数被调用的整个流程
    • [ 场景三 ]:模拟文件下载模块。这个算是拓展模块,如果有学习过C++的同学可以看看,是一家公司某年的面试题

【指针进阶 · 炼狱篇】

在炼狱篇中我们对指针有了更加深层次的一个理解,主要是围绕指针与数组混搭的一些笔试题来进行学习

  • 首先的话我们又去看了看指针的大小,此时不仅仅是一开始的整型指针、字符型指针,我们还去观察了指针常量、常量指针、指针数组、数组指针、指针函数、函数指针以及二级指针等等,同是在32位平台下进行运行,它们的大小均为4个字节
  • 有了上面这些基础后,我们就可以进行大量的练习了,首先我给出了作业题中错的多而且需要经过一定的思考才可以做出来的一些题目,若是你仔细地看了这几道题的话,对指针运算、指针偏移、指针访问字节数、指针数组、数组指针以及函数指针的理解一定又能更上一层楼
  • 接下去,我们就进入了笔试题的学习,首先的话我们通过【数组与指针】的混合来辨析sizeof()strlen()之间的区别,通过回顾了前面的字符指针、字符数组、一维数组、二维数组,来很好地明确指针的大小是多少、一个数组元素的大小是多少、一个数组的大小又是多少,以及指针 + 1可以跳过几个字节,可以访问到后面的多少数据
  • 最后的话,我们就来到了指针相关历年笔试真题的学习,通过八道笔试真题的演练,相信你也清楚了指针在实际的校招中是如何去进行考察的,若是没有像我这样一步步地分析、画图、调试,想要解出来这些题目还是比较困难的,尤其是最后一题,涉及到三级指针,因此我专程录了一个视频📺做讲解,希望读者可以理解

📚推荐书籍阅读


==第一本:《C和指针》==

本书给出了很多编程技巧和提示,每章后面有针对性很强的练习,对初学指针的同学非常友好,推荐先行阅读

==第二本:《C陷阱与缺陷》==

本书分别从词法分析、语法语义、连接、库函数、预处理器、可移植性缺陷等几个方面分析了C编程中可能遇到的问题,适合有一定开发基础的C程序员进行阅读

==第三本:《深入理解C指针》==

本书专门研究指针,旨在提供比其他图书更全面和深入的C 指针和内存管理知识,适合进阶阅读学习

以上就是本文要阐述的所有内容,我花了两个月的时间整理了本文,诣在帮助广大读者可以真正学懂指针,了解指针,知道指针其实并不是那么可怕的,只要你去学会去理解、通过画图思考分析,总能够明白一些👉 ==你,也是可以学好指针的== 👈

非常感谢您对本文的阅读,如果疑问可于评论区提出或者私信我:rose::rose::rose:

相关推荐
代码雕刻家2 小时前
数据结构-3.9.栈在递归中的应用
c语言·数据结构·算法
Kalika0-04 小时前
猴子吃桃-C语言
c语言·开发语言·数据结构·算法
代码雕刻家4 小时前
课设实验-数据结构-单链表-文教文化用品品牌
c语言·开发语言·数据结构
龙图:会赢的4 小时前
[C语言]--编译和链接
c语言·开发语言
Cons.W6 小时前
Codeforces Round 975 (Div. 1) C. Tree Pruning
c语言·开发语言·剪枝
挥剑决浮云 -6 小时前
Linux 之 安装软件、GCC编译器、Linux 操作系统基础
linux·服务器·c语言·c++·经验分享·笔记
一颗星星辰7 小时前
C语言 | 第十章 | 函数 作用域
c语言·开发语言
꧁༺❀氯ྀൢ躅ྀൢ❀༻꧂7 小时前
实验4 循环结构
c语言·算法·基础题
从0至17 小时前
力扣刷题 | 两数之和
c语言·开发语言