C语言指针与数组进阶:sizeof、strlen详解及笔试题实战
C语言学习者在面对指针和数组时,往往容易混淆其底层内存模型与运算符优先级。本文系统梳理了sizeof与strlen的本质区别,并通过整型、字符型数组及二维数组的实例,拆解数组名作为地址与整体大小的边界条件。结合包含结构体对齐、大小端存储及逗号表达式的经典笔试题,提供从理论到实战的完整解析。
核心概念:sizeof 与 strlen
理解指针与数组的关键在于区分两个常用计算长度的工具:
sizeof:是C语言的一个操作符,用于判断对象或类型所占用的字节数。它不关心数据类型本身的语义(如是否以\0结尾),只关注内存布局。strlen:是<string.h>库函数,专门计算以空字符'\0'结尾的字符串长度。它从给定地址开始向后查找,直到遇见第一个\0停止,返回值不含\0自身。
数组名的特殊行为
通常情况下,数组名表示指向首元素的地址。但在以下两种特殊情况中,数组名代表整个数组:
- 作为
sizeof的唯一操作数时(例如sizeof(arr))。 - 使用取地址符
&作用于数组名时(例如&arr)。
除上述情况外,任何算术运算或解引用操作都会让数组名退化为指向首元素的指针。
第1步:一维整型数组的sizeof分析
针对代码:int a[] = {1,2,3,4};
#include<stdio.h>
int main()
{
int a[] = {1,2,3,4};
printf("%d\n", sizeof(a)); // 结果: 16 (4 * 4字节)
printf("%d\n", sizeof(a + 0)); // 结果: 8 (a退化为指针, 64位为8字节)
printf("%d\n", sizeof(*a)); // 结果: 4 (首元素int的大小)
printf("%d\n", sizeof(a[1])); // 结果: 4 (第二个元素int的大小)
printf("%d\n", sizeof(&a)); // 结果: 8 (整个数组的地址)
printf("%d\n", sizeof(*&a)); // 结果: 16 (*&a等价于a, 整个数组)
printf("%d\n", sizeof(&a + 1)); // 结果: 8 (&a+1跳过整个数组后的地址)
return 0;
}
解析要点:
- sizeof(a) 计算的是整个数组占用空间,即 4个元素 × 4字节 = 16。
- a+0 中 a 被解释为首元素地址(指针),因此结果是指针长度(32位下为4,64位下为8)。
- &a 是整个数组的地址,类型为 int (*)[4]。对其进行 +1 操作会跳过整个数组(16字节),但指针本身的大小依然是机器字长。
第2步:一维字符数组与strlen陷阱
分两种情况讨论:
情况A:无 \0 的初始化
char arr[] = {'a','b','c','d'};
// sizeof(arr) 结果为 4
// strlen(arr) 结果未定义/随机值,因为找不到 '\0' 终止符
情况B:字符串字面量初始化
char arr[] = "abcdef";
// sizeof(arr) 结果为 7 ('a'到'f'共6个字符 + 末尾隐藏的 '\0')
// strlen(arr) 结果为 6 (只统计 '\0' 之前的字符)
注意:
如果传入 arr 给 strlen,它会读取直到内存中偶然出现的 \0。
若尝试执行 strlen(*arr),由于 *arr 返回的是字符 'a'(ASCII码 97),函数会将 97 视为非法内存地址从而引发段错误(Segmentation Fault)。
第3步:指针变量的sizeof与strlen
针对代码:char *p = "abcdef";
printf("%d\n", sizeof(p)); // 结果: 8 (指针变量本身的大小)
printf("%d\n", sizeof(*p)); // 结果: 1 (指向的内容 'a' 的大小)
printf("%d\n", strlen(p)); // 结果: 6 (字符串有效长度)
printf("%d\n", strlen(p + 1)); // 结果: 5 (从 'b' 开始算,长度为5)
printf("%d\n", strlen(&p)); // 结果: 随机值 (&p是指针变量的地址,非字符串开头)
解析要点:
- p 只是一个存放地址的变量,无论指向哪里,sizeof(p) 始终等于指针类型的字节大小。
- &p 获取的是这个指针变量在栈上的内存地址,将其传给 strlen 会导致从头遍历该变量周围的内存直到遇到 \0,结果不可预测。
第4步:二维数组的层级推导
针对代码:int a[3][4] = {0};
二维数组可以看作“数组的数组”。这里 a 是由3个一维数组(每个含4个int)组成的。
printf("%d\n", sizeof(a)); // 结果: 48 (3行 * 4列 * 4字节)
printf("%d\n", sizeof(a[0])); // 结果: 16 (第一行的完整大小, 4*4)
printf("%d\n", sizeof(*(a+0))); // 结果: 16 (同 a[0])
printf("%d\n", sizeof(a[0]+1)); // 结果: 8 (a[0]退化为&a[0][0]的地址, +1后仍是地址)
printf("%d\n", sizeof(*(a+1))); // 结果: 16 (第二行&a[1][0]的地址解引用, 即a[1]的大小)
printf("%d\n", sizeof(a[3])); // 结果: 16 (类型推断: a[3]是第四行的类型, 即使越界不访问)
printf("%d\n", sizeof(*a)); // 结果: 16 (*a等同于a[0])
易错点:
- a+1 跳过一行(16字节),得到第二行的地址,其类型为 int(*)[4]。
- a[3] 虽然索引超出了实际分配的3行(0,1,2),但在编译阶段编译器能推导出它的类型是 int[4],因此 sizeof 依然合法并返回16。
指针进阶笔试题实战解析
笔试题1:数组指针运算
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
printf( "%d,%d", *(a + 1), *(ptr - 1));
输出结果: 2,5
推导:
1. *(a + 1):标准的首元素后移一位,指向元素2,解引用得 2。
2. (int *)(&a + 1):&a+1 跳过整个数组(20字节),此时 ptr 位于数组末尾之后。强制转换回 int* 后,ptr-1 向前移动一个 int(4字节),刚好指向最后一个元素 5。
笔试题2:类型强制转换与指针步长
struct Test { /* ... */ }*p; // 假设结构体大小为20字节
p = (struct Test*)0x100000;
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
输出结果:
1. 0x100014(16进制)。结构体指针加1,步长为结构体大小20字节(0x14)。
2. 0x100001。强转为整数,按整数值直接加1。
3. 0x100004。强转为 int*,步长为4字节。
笔试题3:混合运算与大小端存储
int a[4] = { 1, 2, 3, 4 };
int *ptr1 = (int *)(&a + 1);
int *ptr2 = (int *)((int)a + 1);
printf( "%x,%x", ptr1[-1], *ptr2);
输出结果: 4,2000000(基于小端模式)
推导:
1. ptr1[-1]:同理题1,ptr1 指向数组尾,-1 回到末尾元素 4。
2. *ptr2:(int)a 将地址视为整数相加1,再转回指针。这导致 ptr2 的起始地址比原数组起始地址多偏移了1个字节。
- 在小端模式下,整数 1 的十六进制表示为 01 00 00 00。
- 正常 ptr 读出的字节是 01 00 00 00(值为1)。
- ptr2 偏移1字节后,读取到的字节组合变为 00 00 00 02(假设下一个数是2,或者仅看当前数值的字节移位效果,此处需结合具体内存布局)。更准确地说,对于数字1(0x00000001),字节排列是小端序(LSB在前):01 00 00 00。ptr2 指向第二个字节 00。当它作为一个完整的 int 被读取时,它包含的是当前剩余部分 00 00 00 以及下一个元素 2 的低位 02。
根据图示逻辑,通常考查的是移位截断效应。若内存连续,读取内容可能受后续元素影响。本题标准答案通常对应 0x2000000 是基于特定的大/小端判定及数值排列,此处以图中提供的解析为准:ptr2 指向错位字节,组合出该结果。
笔试题4:逗号表达式陷阱
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p;
p = a[0];
printf( "%d", p[0]);
输出结果: 1
推导:
圆括号 () 内的 0, 1 是逗号表达式,其结果为最后一个值 1。因此数组初始化为 {1, 3, 5},即第一行是 {1, 3},第二行 {3, 5},第三行... 实际上只分配了三个子数组元素的位置,值为 {1}, {3}, {5} (每行缺省补0?不,初始化列表顺序覆盖)。
修正:数组 a[3][2] 被依次赋值为:
- a[0][0]=1, a[0][1]=3 (取自 (0,1)的结果1, 和 (2,3)的结果3)
- 等等,让我们仔细检查C初始化规则。 { (0,1), (2,3), (4,5) } 包含了3个逗号表达式。结果分别是1, 3, 5。 数组展开:a[0][0]=1, a[0][1]=3, a[1][0]=5, a[1][1]=0... ?
原文解释指出数组即为 {1, 3, 5} 填充入 3x2 区域,即 a[0][0]=1。所以 p[0] 为 1。
笔试题5:不同类型的数组指针运算
int a[5][5];
int(*p)[4];
p = a;
printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
输出结果: FFFFFFFC,-4(取决于具体实现,通常为差值)
推导:
这是一个经典的“错位”问题。
1. a 是 int[5][5],每一行跨度为 5个int(20字节)。
2. p 是 int (*)[4],每一行跨度为 4个int(16字节)。
3. 当赋值 p=a 时,它们指向同一内存地址(a[0][0])。
4. &p[4][2] 相当于从起点前进 4*p的行宽 + 2 个单位(其中单位是4个int)。偏移量 = 4 * 16字节 + 8字节 = 72字节。
5. &a[4][2] 相当于从起点前进 4*a的行宽 + 2 个单位(其中单位是5个int)。偏移量 = 4 * 20字节 + 8字节 = 88字节。
6. 内存地址差值 = 72 - 88 = -16 字节。
7. 指针减法结果以指针类型的大小为单位。左侧用的是 p 的类型(int[4]),右侧减法的右操作数决定最终结果的显示类型吗?不,通常是两者之一。在打印时,由于 &p[4][2] 参与计算,且题目通常考察的是相对于 p 步长的位移。 -16字节 / 4字节(每步) = -4。十六进制显示为 0xFFFFFFFC(补码)。
以上就是C语言指针与数组进阶详解的详细内容,更多关于C语言指针数组基础教程的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









