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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: C生万物 | 指针进阶 · 炼狱篇

一、再谈指针大小

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

代码:

  • 首先给出接下去我要进行对比的代码


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环境下运行的结果如下

image.png

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

image.png【总结一下】:

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

image.png

二、难题攻坚战🗡

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

第一题【指针运算】

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


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

解析:

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

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

第二题【指针偏移】

下面代码的结果是:(     B     )


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;
}


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循环内部指针的偏移来访问到数组中的内容,对数组的值去进行一个修改,那此时会有几个值发生变化呢?


for (i = 0; i < 4; i++)
{
    *(p + i) = 0;
}
  • 在【指针初阶】我就有讲过对于指针的类型来说决定了一次可以访问多少个字节,那看到前面是short,对于短整型来说一次就可以访问两个字节的数据,又因为arr是一个整型数组,里面的每个元素在内存中所占的字节数都是4,那么这个for循环执行了4次后,就访问了8个字节的数据,即前两个数组元素被改成了【0】

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

image.png

image.pngimage.pngimage.png

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

运行结果:

image.png

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

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


int main()
{
  int a = 0x11223344;
    char *pc = (char*)&a;
    *pc = 0;
    printf("%x\n", a);
    return 0;
}


A.00223344
B.0
C.11223300
D.112233

解析:

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


*pc = 0;

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

  • 以下我是使用一行显示一个字节,这样可以方便观察修改的情况,因为VS是小端存放的,因此可以观察到原本的11223344放到内存中变成了44332211

image.png

  • 可以看到,通过*pc我们访问到了变量a的第一个字节,并且将其修改为【0】

image.png

  • 不过这个是在内存中的样子,此时若是要显示打印在屏幕上的话还要将其再做一个转换,所以最后显示的结果便是11223300

image.png

第四题【指针数组】

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


A.int* arr[10];
B.int* arr[];
C.int **arr;
D.int (*arr)[10];

解析:

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

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

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

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

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

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

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


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),==请你仅通过草稿纸验算的方式,计算出每个结果==

代码:


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


printf("%d\n", sizeof(a));
  1. 接下去第二个,此时a是并不是单独放在sizeof内部,而且也没有&,所以数组名a指的就是首元素地址,==对于一个地址来说我们在指针初阶部分讲了在内存中就是指针==,那对于指针来说即为 4 / 8,在32位平台下运行就是4个字节,在在32位平台下运行就是8个字节


printf("%d\n", sizeof(a + 0));
  1. 然后第三个,通过观察可以发现,并没有出现sizeof(数组名)&数组名这两种形态,所以a就是首元素地址,类型是int*那么*a就是对其进行解引用,获取到的便是【首元素】,类型是int,那一个整型的大小是多少呢?没错,就是 4个字节


printf("%d\n", sizeof(*a));
  1. 第四个,a指的是首元素地址,a + 1向后偏移了一个整型,即为第二个元素的地址,那就和第二个一样计算的是一个地址的大小,即指针的大小,为 4 / 8


printf("%d\n", sizeof(a + 1));
  1. 第五个很简单,就是计算数组中第二个元素的大小,那很简单,就是 4个字节


printf("%d\n", sizeof(a[1]));
  1. 第六个&a即为&数组名,取出是整个数组的地址,这个其实我在上面初讲指针的时候有提到过,整个数组的地址其实和数组的首元素的地址是一样的,那么整个数组的地址它也是一个地址,那只要是地址即为 4 / 8个字节


printf("%d\n", sizeof(&a));

image.png

  • 通俗一些来说,其实地址就像是门牌号一样,那数组中每个元素的地址和整个数组的地址并没有高低贵贱之分,而是,不是说数组的地址就来得高大上一些,它们一视同仁

image.png

  1. 小插曲,我们再来看第七个,第一眼就看到&a,那么还是一样取出的是整个数组的地址,那对整个数组的地址进行解引用得到的便是整个数组,因为数组的地址是存到到数组指针中的,它的类型即为int (*)[5]
  • 对一个整型指针解引用获取到的是一个整型
  • 对一个字符型指针解引用获取到的是一个字符
  • 对一个数组指针解引用获取到的是一个数组
  • 那么此时计算的便是一个数组的大小,即为 16,其实你也可以这么去看,&是取到这个数组的地址,*又对进行解引用,通过这个地址找到找到这里面所存放的内容,这么一来一去就产生了抵消,最后也就变成了sizeof(a),那便是我们上面说到过的,这种sizeof(数组名)的形式,==计算的也是整个数组的大小==


printf("%d\n", sizeof(*&a));
  1. 我先说第九个:很明显,就是去计算数组首元素地址的大小,为 4 / 8


printf("%d\n", sizeof(&a[0]));
  1. 好,下面两个一起说,好做一个对比,&a[0]上面讲过了,是取出数组首元素的地址,它的类型是int*,那对于一个整型指针来说,以此可以访问的字节数是4个字节,即数组中的一个元素,那么此时它就指向了2这个元素的地址处,它就等价于&a[1];对于&a来说,取出的是整个数组的地址,其类型为int (*)[4],那么它一次性可以访问的字节数即为整个数组的所有元素之和,此时它就指向了4后面的这块地址


printf("%d\n", sizeof(&a[0] + 1));
printf("%d\n", sizeof(&a + 1));
  • 可以看到,无论是指向哪里,它们都是一个地址,一个地址的大小就为 4 / 8字节

image.png


运行结果:

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

image.png

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

image.png

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

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

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

代码:


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


printf("%d\n", sizeof(arr));
  1. 第二个:arr并不是单独放在sizeof中的,那它就是数组名,数组名即为首元素地址,此时计算的就是第一个元素的地址,但只要地址的话即为 4 / 8字节


printf("%d\n", sizeof(arr + 0));
  1. 第三个:arr既没有单独放在sizeof中,也没有&,那么它就是首元素地址,对首元素地址进行*解引用,此时获取到的就是首元素,数组的首元素是[a],类型是【char】,那大小即为 1


printf("%d\n", sizeof(*arr));
  1. 第四个很简单,就是计算数组arr中第一个元素的大小,那也是 1个字节


printf("%d\n", sizeof(arr[1]));
  1. 那下面这个呢? 很明显看到&数组名,那么取出的就是整个数组的地址,上面说过了,它还是一个地址,那么就是 4 / 8字节


printf("%d\n", sizeof(&arr));
  1. 一样的,&arr取到整个数组的地址,因为其类型是一个数组指针,那么 + 1就跳过一个数组的大小,此时它就指向了字符[f]后面的这个地址,那既然是地址的话也还是 4 / 8字节


printf("%d\n", sizeof(&arr + 1));

image.png

  1. 最后,&arr[0]取到的是数组首元素的地址,它的类型是int*,+ 1可以访问4个字节的大小,即为&arr[1],此时它算的还是一个地址的大小,那请说出答案!: 4 / 8字节


printf("%d\n", sizeof(&arr[0] + 1));

运行结果:

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

image.png

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

image.png

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

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

代码:


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,所以我们无法确定它的结束标志

image.png

  1. 那么我们首先来看第一个,arr放在strlen()内部,注意这里并不是sizeof()内部,而且也没有&数组名,所以arr表示的是数组的首元素地址,就是从字符a这个位置开始往后计算这个字符数组的长度,上面说过了,strlen()会向后查找直到\0为止,但是呢又因为这个字符数组内部本身并不存在\0,那它就会继续往后查找,==可是对于arr数组后面的这块位置是随机的,是否具有\0是不确定的==,因此最终的结果是  随机值


printf("%d\n", strlen(arr));
  1. 好,接下去第二个其实和第一个是一样的,因为arr是首元素地址,+ 0之后的结果还是一样的,为 随机值


printf("%d\n", strlen(arr + 0));
  1. arr依旧是首元素地址,那对首元素地址进行解引用获取到的就是【首元素】,首元素就是字符a,类型是char,但是strlen()要为其传入的是类型为char*的地址,所以strlen就会将a的ASCLL码值97当做地址进行传入


printf("%d\n", strlen(*arr));
  • 对于ASCLL码我们它是美国国家标注协会ISO所定义的标准,那在我们C语言中就是已经存在了的,==它是属于内存中的一块固定地址,这块地址我们是无法去使用的,内存也不会将其分配给我们==,所以此时我们使用strlen()去访问这块地址的时候其实属于非法访问,调试一下看看💻

image.png

  • 可以看到我标出的位置0x00000061这个位置发生了冲突,这是在内存中以十六进制的形式来表示地址,将其转换为十进制表示即为97,那正好对应了我们上面所分析的为strlen()传入了字符a的ASCLL码值97,所以可以看出这块地址确实是无法访问的
  1. 那如果你清楚了上面这个,其实对于下面的这个也是一样的,arr[1]这个数组元素也不是一个地址,而是一个字符,此时会将b的ASCLL码值98传入strlen(),那此时我们去访问这块地址的时候也是属于非法访问


printf("%d\n", strlen(arr[1]));

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

image.png

  1. 可以看到,出现了&数组名的情况,那此时我们就获取到了整个数组的地址,那整个数组的地址和数组首元素的地址是一样的,都位于字符a这个位置,那么从这个位置向后找\0,就和第一题一样是不确定的,字符数组本身不具备\0,其他地址处也可能没有\0,因此最终的结果为 随机值


printf("%d\n", strlen(&arr));

image.png

  1. 在上一题中,&arr取出了整个数组的地址,它的类型为int (*)[6],是一个数组指针,那一个数组指针 + 1就跳过了整个数组,来到了字符f后面的这块地址处,接着向后查找,去找\0,但结果我们知道,还是一个 随机值,不过这个随机值会比上面的这个随机值少6,因为要减去已经跳过的6个数组元素


printf("%d\n", strlen(&arr + 1));

image.png

  1. 好接下去最后一个,首先取到的是数组的首元素地址,它的类型是char*,那么 + 1就会跳过一个数组的元素,来到&arr[1]这个为止,即字符b所在的地址处,此时继续向后查找还是一个 随机值,这个随机值会比上面的这个随机值少1,因为要减去已经跳过的1个数组元素a


printf("%d\n", strlen(&arr[0] + 1));

image.png

运行结果:

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

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

相关文章
|
6月前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
6月前
|
C语言
指针进阶(回调函数)(C语言)
指针进阶(回调函数)(C语言)
|
6月前
|
存储 C语言 C++
指针进阶(函数指针)(C语言)
指针进阶(函数指针)(C语言)
|
6月前
|
编译器 C语言
指针进阶(数组指针 )(C语言)
指针进阶(数组指针 )(C语言)
|
6月前
|
搜索推荐
指针进阶(2)
指针进阶(2)
55 4
|
6月前
指针进阶(3)
指针进阶(3)
48 1
|
6月前
|
Java 程序员 Linux
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
58 0
|
1月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
121 13
|
2月前
|
C语言
无头链表二级指针方式实现(C语言描述)
本文介绍了如何在C语言中使用二级指针实现无头链表,并提供了创建节点、插入、删除、查找、销毁链表等操作的函数实现,以及一个示例程序来演示这些操作。
37 0
|
3月前
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
143 4

热门文章

最新文章