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

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

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

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

代码:

  • 首先你要清楚的一点是,这个字符数组中有几个元素,可以看到,后面的“abcdef”是字符串,对于字符串来说末尾是自带\0的,这个我之前也有通过调试带同学们看过,所以这个数组中有7个元素


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


printf("%d\n", sizeof(arr));
  1. 接下去arr既没有单独出现在sizeof()内部,也没有取地址,那么它指的就是首元素地址,看到如下图所示,一个地址的大小便是 4 / 8个字节


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

image.png

  1. 看到第三个,此时arr还是代表首元素地址,对其*解引用访问到的就是首元素【a】那么一个char类型的元素在内存中所占的字节数即为 1


printf("%d\n", sizeof(*arr));
  1. 第四个其实也是一样的,字符数组的第二个元素为【b】,所占的字节数也为 1


printf("%d\n", sizeof(arr[1]));
  1. 终于看到&数组名了,此时取出的是整个数组的地址,只要是地址的话即为 4 / 8个字节


printf("%d\n", sizeof(&arr));
  1. 一样,取出整个数组的地址后,接着向后偏移的话就会跳过一整个数组,那取到的便是\0后面的这块地址,既然是地址的话,请说出它的大小: 4 / 8个字节


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

image.png

  1. 最后,也是一样 ,偏移一个字节后来到了字符【b】的位置,其地址的大小也为 4 / 8个字节


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

image.png

运行结果:

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

image.png

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

image.png

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


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


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

image.png

  1. 那么首元素地址向后偏移0个字节,还是一样的结果,为 6


printf("%d\n", strlen(arr + 0));
  1. 下面两个一起来说,过程不再赘述,所传入strlen()都是数组的元素,但是因为strlen()只能接收一个地址,因此会出现非法访问


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

image.pngimage.png

  1. 取出整个数组的地址,向后去找\0,那答案也很明显就是 6


printf("%d\n", strlen(&arr));
  1. &arr取出了整个数组的地址,+ 1 跳过了整个数组,根据上面所讲其为 随机值,而且这个随机值的大小会是原本的减去7,因为跳过了整个数组的所有元素


printf("%d\n", strlen(&arr + 1));
  1. 从首元素地址向后偏移一个字节,就是&arr[1],向后遍历碰到\0为止,结果便是 5


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

运行结果:

image.png

4、字符指针【⭐】

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

代码:

  • 首先你要知道的是,这个字符指针p里面存放的是什么?前面我们在【指针进阶·提高篇】中有讲到过,若是将一个字符串给到一个字符指针做接收,那么这个字符指针里面存放的便是字符串中第一个字符的地址

image.png


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个字节


printf("%d\n", sizeof(p));
  1. 因为这个指针p的类型是char*,所以 + 1会跳过一个char类型的数据,此时就指向了字符串中的第二个字符所在的地址,那也是一样为 4 / 8个字节


printf("%d\n", sizeof(p + 1));
  1. 接下去两个一起说,对指针p进行解引用,此时就访问到了这块地址中所存放的内容【a】,那么一个char类型的数据在内存中占1个字节;第二个其实就是【a】,那它们的结果都是一样的,均为 1


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]


int arr[5] = {1,2,3,4,5};
int* p = arr;

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

  1. 好,接下去再来看这个,对指针p再去进行取地址&的操作,那我们【指针初阶】的时候时候讲二级指针时有说到过,一个一级指针可以接收普通变量的地址,一个二级指针则是可以接收一级指针的地址。那么此刻我对一个一级指针去取地址,它的类型就从char*转变成了char**


printf("%d\n", sizeof(&p));
  • 上面我有讲到过,一个指针在【解引用】或者【向后访问】的时候看得是它的指针类型,通过下面这张进行对比就可以很清晰地看出p在进行&取地址操作后就变成了一个二级字符指针,每次可以访问的数据个数即为一个char*类型。不过最后的结果还是一个地址的大小为 4 / 8个字节

image.png

可以再看看这张图👇

image.png

  1. 看了上面的这些后,相信下面这个你也是手到擒来,因为&p是一个二级指针类型,+ 1便跳过了一个一级指针的大小,即一个char*的距离,那其实也就是这个字符串,到达了\0的后头,可它还是一个地址,只要是一个地址,大小即为 4 / 8个字节


printf("%d\n", sizeof(&p + 1));
  1. 但是下面这个就不一样了,因为p指向的是这个字符串的首字符,那&p[0]就是取出它所在的地址,类型为char*,那么 + 1便跳过了一个char类型的数据,来到了第二个字符的地址处,所以结果还是 4 / 8个字节


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

运行结果:

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

image.png

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

image.png


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

代码:

  • 首先它的内存布局没有更换,还是上面的这个


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


printf("%d\n", strlen(p));

image.png

  1. 那p的类型是char*,+ 1跳过的就是一个char类型的数据,来到了字符【b】的地址处,向后找\0的话就最后的结果即为 5


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

image.png

  1. 下面两个也一起说了,如果你上面看得认真的话这里一定很快就能反应过来,*p取到的就是字符【a】,那我们知道,给strlen()是不可以传入地址之外的其他数,那么这里就会产生非法访问


printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));

image.pngimage.png

  1. 接下去,我们来看看&p,这里一定要看清楚取到的谁的地址,这里并不是字符串的地址,而是指针p自己的地址,但是这个指针p只是存放了字符串首元素的地址,但是并不知道它里面有没有\0,所以在向后遍历的时候并不知何时结束,所以它的结果就是 随机值


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

image.png

  1. 然后再来看看&p + 1,上面说到指针p的类型是char*,在&取地址后它的类型就变成了char**,+ 1便会跳过一个char*类型的数据,那也就是这个字符指针,此时便指向了它末尾的这个位置,从这里向后去进行寻找\0的话还是存在一个不确定的因素,所以最后的结果还是 随机值


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

image.png


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

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


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

运行结果:

image.png

5、二维数组

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

  • 首先你要清楚下面这个二维矩阵是几行几列的,很明显是三行四列的

image.png

  • 然后我们再一一来讲说代码

代码:


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


printf("%d\n", sizeof(a));
  1. 接下去第二个,a[0][0]代表的是数组第一行第一列的元素,那这很简单,每个元素都是 4个字节


printf("%d\n", sizeof(a[0][0]));
  1. 这个第三题,为了让读者可以很好地理解,我打算从一维数组开始讲起
  • 首先对于下面的一维数组arr,使用arr[0]arr[1]arr[2]便可以访问到数组中的每个元素,因为arr此时就是数组名

image.png

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

  • 我们可以将二维数组的每一行当做它的一个元素,那么下面这个数组就有三个元素,那要去访问到每一行中的每列元素该怎么做呢?此时我们需要使用到数组名,看到右侧的a[0][j]a[1][j]a[2][j],通过对【j】去进行一个控制从而可以访问到每一列上的具体元素,那我们可以将前面的a[0]a[1]a[2]看作是一个整体,那它们即为每一行的数组名

image.png

  • 此时再来看下面这道题就很简单了,因为a[0]为第一行的数组名,而且它是单独放在sizeof()内部的,所以计算的便是第一行这一整行的大小,里面有4个元素,每个元素都是4个字节,那么结果即为 16


printf("%d\n", sizeof(a[0]));
  1. 接下去再来看下一个,此时a[0]并不是单独放在sizeof()内部,所以它指的就是首元素地址,即&a[0][0]这个地址,它的类型是int*,+ 便跳过了一个整型元素,来到了&a[0][1]的位置,那此时计算的就是一个地址的大小,即为 4 / 8个字节


printf("%d\n", sizeof(a[0] + 1));
  1. 下面这个就是对上一题所取到的&a[0][1]的地址进行解引用,此时取到的便是这个地址上的元素,去计算一下它的大小便是 4


printf("%d\n", sizeof(*(a[0] + 1)));
  1. 然后再来分析一下这个,a并没有单独放在sizeof()内部,也没有进行取地址的操作,所以它指的便是二维数组首元素的地址,那对于一个二维数组来说的首元素是什么呢?也就是第一行,那此时a取到的便是第一行的地址,因为需要存放一个数组的地址,所以它的类型便是一个数组指针类型即int (*)[4],那么一个数组指针 + 1跳过的便是一个数组,此时就来到了二维数组的第二行,取到的便是第二行的地址,但它终究还是个地址,只要是个地址的话大小即为 4 / 8个字节


printf("%d\n", sizeof(a + 1));
  1. 接下去便是对这一行的地址去进行解引用,那么也就得到了第二行这一整行,此时计算便是这一整行的大小,便为 16


printf("%d\n", sizeof(*(a + 1)));
  • 不过呢,对于上面这个其实有另一种思路,那就是我在上面讲字符指针时所说的指针解引用*与数组[]的转换公式,对于*(a + 1)可以转换为a[1],那这个我在上面有讲到过,即为二维数组第二行的数组名,那将其单独放在sizeof()内部形成sizeof(数组名),计算的也是第二行这整一行的大小
  1. 好接下去又出现我们前面所提的&数组名,因为a[0]为第一行的数组名,所以对它进行取地址就取到了这一整行的地址,它的类型也为一个数组指针int (*)[4],那 + 1的话也会跳过整个数组,此时也就来到了第二行,那么取到的便是第二行的地址,地址的大小即为 4 / 8个字节


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


printf("%d\n", sizeof(*(&a[0] + 1)));
  1. 接下去再来看这个,此时a并没有单独放在sizeof()内部,也没有进行取地址的操作,那么a所代表的就是首元素地址,即第一行的地址,如果你举得有点难以理解的话可以把*a看作是*(a + 0),那便可以将其转换为a[0],也就是第一行的数组名,sizeof(a[0])计算的便是第一行的大小,结果为 16


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


printf("%d\n", sizeof(a[3]));

运行结果:

image.png

延伸拓展:

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

  • 看下面的这段代码,定义了一个short短整型的变量num,还有一个整型变量a,然后在printf()打印语句中计算了num  = a + 5,那最后它的结果会是多少呢?


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()在计算处理的时候确实在【编译】的环节,此时里面的表达式早就被忽略了,因此最后的值计算的还是变量numimage.png
  • 那既然这里面的表达式没有执行的话,最后的结果就还是num一开始初始化的样子

image.png

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

笔试题1

代码:


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】

image.png

运行结果

  • 最后打印结果来看一下

image.png

相关文章
|
3月前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
3月前
|
C语言
指针进阶(回调函数)(C语言)
指针进阶(回调函数)(C语言)
|
3月前
|
存储 C语言 C++
指针进阶(函数指针)(C语言)
指针进阶(函数指针)(C语言)
|
3月前
|
编译器 C语言
指针进阶(数组指针 )(C语言)
指针进阶(数组指针 )(C语言)
|
3月前
|
搜索推荐
指针进阶(2)
指针进阶(2)
39 4
|
3月前
指针进阶(3)
指针进阶(3)
31 1
|
3月前
|
Java 程序员 Linux
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
34 0
|
24天前
|
C语言
【C初阶——指针5】鹏哥C语言系列文章,基本语法知识全面讲解——指针(5)
【C初阶——指针5】鹏哥C语言系列文章,基本语法知识全面讲解——指针(5)
|
24天前
|
C语言
【C初阶——指针4】鹏哥C语言系列文章,基本语法知识全面讲解——指针(4)
【C初阶——指针4】鹏哥C语言系列文章,基本语法知识全面讲解——指针(4)
|
24天前
|
存储 编译器 C语言
【C初阶——指针3】鹏哥C语言系列文章,基本语法知识全面讲解——指针(3)
【C初阶——指针3】鹏哥C语言系列文章,基本语法知识全面讲解——指针(3)
下一篇
DDNS