1、【C语言】【进阶】数组,指针与退化
【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
背景
前两天逛博客,看到一篇描述 C 语言指针的文章
【C语言进阶】指针面试题详解(2)
里面的题目还蛮有意思,简单评论了两句,发现自己的想法也有某些漏洞,这里展开巩固一下
题目
题目如下:
#include <stdio.h>int main(void) {int a[5] = {1, 2,3, 4, 5};int* ptr = (int*)(&a + 1);printf("%d, %d\n",*(a + 1), *(ptr - 1));return 0;
}
数组,指针和数组指针
在分析题目之前,先额外看下 a 和 &a 的区别,这是一个非常经典的 C 语言问题,涉及到数组和指针的本质区别
#include <stdio.h>int main(void) {int a[5] = {1, 2, 3, 4, 5};printf("a: %p\n&a:%p\n", a, &a);return 0;
}
从类型上看:
- a 是一个数组名,在大多数表达式中(除了 sizeof(a)、&a 等情况),a 会自动退化为指向数组第一个元素的指针,此时 a 被当作是 int* 类型,指向 a[0]
- 而 &a 是对整个数组取地址,它的类型是 int (*)[5],即指向一个包含 5 个 int 的数组的指针
虽然它们的类型不同,但它们的数值地址是相同的,因为不管是指向第一个元素,还是指向一个包含数组的指针,这个数组和数组中的第一个元素的起始地址是一样的
编译后运行,可以看到两个数值地址是一样的
再做一个变体,打印它们的类型
#include <stdio.h>int main(void) {int a[5] = {1, 2, 3, 4, 5};printf("a: %ld\n&a:%ld\n", sizeof(a), sizeof(&a));return 0;
}
编译后运行
可以看到,sizeof(a) 占了 20 个字节,这里的环境为 x86_64,int 类型大小为 4 字节,4 * 5 = 20 字节,需注意,这里 sizeof(a) 中的 a 并没有退化成指针,而 &a 很明确,它就是一个指针,而且是指向一个包含数组的指针,这里简称数组指针,指针类型在 x86_64 环境中占 8 个字节,所以这里 sizeof(&a) 占了 8 个字节
C 标准中的定义
关于大多数情况下, a 退化成指针,而 sizeof(a),&a 中的 a 仍保留数组属性,这一点在 C 标准中是有规定的,C11 标准 ISO/IEC 9899:201x 中对这里的描述如下
有几个关键点:
- 大多数情况下,如果一个变量是数组类型(比如上面的例子 int a[5]),这个数组类型 a 会被自动退化成指向这个数组中第一个元素的指针,比如 int a[5] 中的 a,在表达式中会变成 &a[0],也就是一个 int* 类型的指针
- 但也有例外情况,比如在 sizeof,_Alignof,&,或者字符串数组初始化的时候,此时这个数组类型不会退化成指针
- 数组类型退化成指针后,不能出现在赋值语句的左边,比如不能写 a = b; 这样的赋值语句,数组也不能这样直接赋值
这就解释了上面那些现象,下面再额外讲下字符串数组初始化的这种情况
举个例子,比如下面这样的一个字符串数组
char str[] = "hello";
在这个例子中,str 是一个数组类型,而不是指针,在它初始化时,数组类型 str 不会退化成指针,而是作为数组完整地接收了字符串常量 hello 的内容,和上面这个例子类似的,比如还有下面这样的结构体类型,int l类型等等。
typedef struct {int a;int b;
} MyStruct;MyStrcut myStruct[] = {{1,2},{3,4}};
int data[] = {1, 2, 3, 4, 5};
当然如果是下面这样的就不行了,下面这种是指针赋值
char* str = "hello";
回归题目
有了上面的基础,再回过头看这个题目
#include <stdio.h>int main(void) {int a[5] = {1, 2, 3, 4, 5};int* ptr = (int*)(&a + 1);printf("%d, %d\n",*(a + 1), *(ptr - 1));return 0;
}
- 很明显,&a 作为数组类型,&a + 1 这里的 1 代表的是一个 int (*)[5] 的数组类型大小,所以 ptr 指向的是 a 所在内存地址往后 5 个 int 类型大小的地址,也就是 &a[4] 后面的那个位置
- 而 a 作为 int 类型的指针,指向了数组中的第一个元素,a + 1 这里的 1 代表的是一个int 类型大小,所以 *(a + 1),取的是数组中第二个元素的值
- 同样,ptr 作为 int 类型的指针,指向了数组中最后一个元素后面的地址,ptr - 1 这里的 1 同样代表的是一个 int 类型大小,所以 *(ptr - 1) 取的是数组中最后一个元素的值
今天就到这里,这个系列下篇 blog 继续探索 C 语言