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 列的二维数组, 数组名是a
printf("%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 😏😏!