C 语言指针详解(3)
写在前面
这里我默认你看完了 C 语言指针详解(2),
现在尝试做一些试题吧~
sizeof 对比 strlen
sizeof计算变量或类型所占内存空间的大小, 单位字节. 只关注占⽤内存空间的⼤⼩,不在乎内存中存放什么数据strlen是库函数, 用来求字符串长度.strlen函数会⼀直向后找\0字符, 直到找到为⽌,所以可能存在越界查找
char *p = "abcdef"; |
我们来逐行分析这段代码:
char *p = "abcdef";
定义了一个指向字符串字面值"abcdef"的指针p, 字符串末尾有一个\0, 用于标志字符串的结束.printf("%d\n", strlen(p));
p是指向字符串"abcdef"的指针,strlen(p)会计算从p指向的位置开始,直到遇到\0为止的字符串长度。
结果: 6, 因为"abcdef"的长度是 6.printf("%d\n", strlen(p+1));
p+1指向的是字符串的第二个字符'b',strlen(p+1)会从"bcdef"开始计算长度,
结果: 5, 因为"bcdef"的长度是 5.printf("%d\n", strlen(*p));
*p解引用指针p,取的是p所指向的第一个字符,也就是'a',strlen需要一个char*类型的参数,
而这里传入了char类型, 会导致未定义行为.
可能的后果: 程序崩溃或输出随机值(strlen偶然处理了错误的地址)
结果: 未定义行为printf("%d\n", strlen(p[0]));
p[0]等价于*p, 结果也是'a', 和上一行代码的问题完全相同,仍然是未定义行为.
结果: 未定义行为printf("%d\n", strlen(&p));
&p是p的地址, 类型是char**(指向char*的指针).
strlen将&p解释为一个char*, 并从这个地址开始查找字符串终止符\0.
结果是未定义行为, 因为&p不是一个合法的字符串地址, 程序会崩溃或输出随机值.
p是一个指针, 指向字符串字面量"abcdef"的第一个字符,p的类型是char*,
&p是p的地址(也就是指针的地址), 它的类型是char**(指向char*的指针).
strlen的参数必须是一个char*指针, 并且这个指针必须指向一个以\0结尾的有效字符串,
如果传入一个无效的指针, 程序会引发未定义行为(可能崩溃, 也可能输出垃圾值)
strlen(&p)将&p传递给strlen,strlen将&p解释为一个char*,
并尝试从&p指向的地址开始, 找到第一个\0, 如果&p恰巧指向一个内存区域,
且在读取过程中没有遇到非法访问(如越界访问), 那么程序不会崩溃;
如果在内存中碰巧找到了一个\0,strlen会认为它是一个有效字符串, 并返回一个长度(可能是垃圾值)
printf("%d\n", strlen(&p+1));
&p+1是指向p后一个位置的指针,strlen将&p+1解释为一个char*, 尝试从该地址读取字符串.
这是未定义行为, 因为&p+1指向的内存内容不可预测.
结果: 输出随机值或程序崩溃printf("%d\n", strlen(&p[0]+1));
&p[0]等价于p,因为p[0]是字符串的第一个字符'a',&p[0]+1等价于p+1,
指向字符串的第二个字符'b',strlen(&p[0]+1)等价于strlen(p+1).
结果: 5, 因为"bcdef"的长度是 5.
总结:
- 代码中的
strlen(*p)和strlen(p[0])是不合法的(未定义行为) - 对于
strlen(&p)和strlen(&p+1), 虽然可能不会崩溃, 但仍然是未定义行为
int a[3][4] = {0}; |
需要注意的是sizeof是一个编译期操作, 它根据类型计算结果, 不涉及实际运行时的值.
我们来逐行分析这段代码(x64):
int a[3][4] = {0};
定义了一个 3 行 4 列的二维数组, 数组名是aprintf("%d\n",sizeof(a));
sizeof里单独放一个数组名, 表示计算整个数组的大小.
结果: 3 * 4 * 4 = 48 (字节)printf("%d\n",sizeof(a[0][0]));
a[0][0]是二维数组中第 1 行第 1 列的元素, 类型为int
结果: 4 (字节)printf("%d\n",sizeof(a[0]));
a[0]是数组a的第 1 行, 类型为int[4], 也可以理解为a[0]是第 1 行的数组名.
结果: 4 * 4 = 16 (字节)printf("%d\n",sizeof(a[0]+1));
a[0]是数组的第 1 行, 类型为int[4], 是第 1 行的数组名, 数组名又是指向首元素的指针,
a[0] + 1是一个指针运算,a[0]退化为指向第 1 行第 1 列的指针, 类型为int*,
a[0] + 1是指向第 1 行第 2 列的指针, 类型仍为int*
sizeof(a[0] + 1)计算的是指针的大小.
结果: 8 (字节)printf("%d\n",sizeof(*(a[0]+1)));
a[0] + 1是指向第 1 行第 2 列的指针, 类型为int*,
*(a[0] + 1)解引用这个指针, 得到第 1 行第 2 列的元素, 类型为int
结果: 4 (字节)printf("%d\n",sizeof(a+1));
a是数组名, 是数组首元素的地址, 二维数组的首元素是一维数组, 但在表达式a + 1中,
a退化为指针, 指向数组a的第 1 行,a + 1的类型为int(*)[4](指向一维数组的指针),
sizeof(a + 1)计算的是指针的大小.
结果: 8 (字节)printf("%d\n",sizeof(*(a+1)));
a + 1是一个指针, 指向数组a的第 2 行, 类型为int(*)[4],
*(a + 1)解引用这个指针, 得到数组a的第 2 行, 类型为int[4].
结果: 4 * 4 = 16 (字节)printf("%d\n",sizeof(&a[0]+1));
a[0]是数组第 1 行的数组名,&a[0]是指向数组a的第 1 行的指针, 类型为int(*)[4],
&a[0] + 1是指向数组a的第 2 行的指针, 类型仍然是int(*)[4],sizeof(&a[0] + 1)计算的是指针的大小.
结果: 8 (字节)printf("%d\n",sizeof(*(&a[0]+1)));
&a[0] + 1是指向数组a的第 2 行的指针, 类型是int(*)[4],
*(&a[0] + 1)解引用这个指针, 得到数组a的第 2 行, 类型为int[4]
结果: 4 * 4 = 16 (字节)printf("%d\n",sizeof(*a));
a是一个二维数组, 类型为int[3][4], 但在表达式中,a退化为指针(指向首元素),
指向数组a的第 1 行(二维数组的首元素是一维数组),*a解引用这个指针, 得到数组a的第 1 行, 类型为int[4].
结果: 4 * 4 = 16 (字节)printf("%d\n",sizeof(a[3]));
访问a[3]是越界行为, 但sizeof是编译期操作, 不会实际访问内存.
a[3] 被视为数组的第 4 行,类型为int[4]
结果: 4 * 4 = 16 (字节)
总结:
- 指针与数组的关系:
- 数组在表达式中常常会退化成指向首元素地址的指针, 但并不是所有情况都会退化
- 对于多维数组, 指针的层次和数组的结果密切相关, 理解每一级指针指向的对象是关键.
sizeof的本质:
sizeof是一个编译期操作, 仅根据类型计算大小, 而不会实际访问内存.- 越界访问在
sizeof中并不会触发运行时错误, 但在其他表达式中可能导致未定义行为.
- 未定义行为 UB:
- 未定义行为是 C 语言中需要特别警惕的问题, 即使代码能够运行出某些结果, 也无法保证这些结果在不同编译器或平台上的一致性.
- 避免将无效指针传递给函数, 或访问越界的数组元素.
写在最后
理解 C 语言的底层实现有助于掌握其他高级语言, 比如指针、内存布局等概念, 对编写高效、可靠的程序非常重要.
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Bradey 😏😏!