1. 深入理解sizeof与strlen的区别
在C语言中,sizeof和strlen是两个经常被混淆的操作符/函数,但它们有着本质的区别。理解它们的差异对于指针和数组操作至关重要。
1.1 sizeof操作符的本质
sizeof是一个编译时操作符,用于计算对象或类型所占用的内存字节数。它的特点包括:
- 在编译时就能确定结果
- 对任何类型都能使用,包括基本类型、数组、结构体等
- 当用于数组时,返回整个数组的大小(字节数)
例如:
c复制int arr[5] = {1, 2, 3, 4, 5};
printf("%zu\n", sizeof(arr)); // 输出20(假设int为4字节)
注意:sizeof返回的类型是size_t,应该使用%zu格式说明符打印
1.2 strlen函数的特性
strlen是一个运行时函数,用于计算以null字符('\0')结尾的字符串的长度。它的特点包括:
- 必须在运行时执行
- 只适用于以'\0'结尾的字符串
- 返回的是字符数,不包括结尾的'\0'
例如:
c复制char str[] = "hello";
printf("%zu\n", strlen(str)); // 输出5
1.3 常见误区与注意事项
-
数组退化为指针:当数组名作为函数参数传递时,它会退化为指针,此时sizeof返回的是指针大小而非数组大小。
-
字符串字面量:sizeof("hello")返回6(包括'\0'),而strlen("hello")返回5。
-
未初始化的字符数组:如果字符数组不以'\0'结尾,使用strlen会导致未定义行为。
-
指针与数组的区别:
c复制char arr[] = "hello"; char *ptr = "hello"; printf("%zu %zu\n", sizeof(arr), sizeof(ptr)); // 6 8(64位系统)
2. 数组与指针的sizeof运算解析
2.1 一维数组的sizeof运算
考虑以下代码:
c复制int a[] = {1, 2, 3, 4};
printf("%zu\n", sizeof(a)); // 16 (整个数组)
printf("%zu\n", sizeof(a + 0)); // 8 (数组名退化为指针)
printf("%zu\n", sizeof(*a)); // 4 (第一个元素)
关键点:
a作为数组名单独出现时,表示整个数组a + 0中,数组名退化为指向首元素的指针*a解引用得到数组的第一个元素
2.2 二维数组的sizeof运算
二维数组的情况更为复杂:
c复制int b[3][4] = {0};
printf("%zu\n", sizeof(b)); // 48 (3×4×4)
printf("%zu\n", sizeof(b[0])); // 16 (第一行的大小)
printf("%zu\n", sizeof(b[0]+1)); // 8 (第一行第二个元素的地址)
理解二维数组的关键:
b[i]是一个一维数组,可以看作一行b[i][j]是具体的元素b可以看作是指向数组的指针(数组指针)
2.3 指针运算与sizeof
指针运算需要考虑指针类型:
c复制char *p = "hello";
printf("%zu\n", sizeof(p)); // 8 (指针大小)
printf("%zu\n", sizeof(*p)); // 1 (char类型大小)
printf("%zu\n", sizeof(p[1])); // 1 (第二个字符)
3. 指针与数组的复杂关系
3.1 数组名的两种含义
数组名在大多数情况下会退化为指向其首元素的指针,但有两种例外情况:
- 作为sizeof的操作数
- 作为&的操作数
例如:
c复制int arr[5];
// arr作为数组名
printf("%zu\n", sizeof(arr)); // 20
// arr退化为指针
int *p = arr;
3.2 指针运算的实质
指针运算基于指向的类型大小:
c复制int a[5] = {1, 2, 3, 4, 5};
int *ptr = (int*)(&a + 1); // 指向数组末尾之后
printf("%d\n", *(ptr - 1)); // 5
解释:
&a得到的是指向整个数组的指针&a + 1跳过整个数组ptr - 1回退一个int大小的位置
3.3 多维数组与指针
多维数组的指针运算需要特别注意:
c复制int aa[2][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *ptr1 = (int*)(&aa + 1); // 跳过整个二维数组
int *ptr2 = (int*)(*(aa + 1)); // 第二行的首元素
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1)); // 10,5
4. 复杂指针运算案例分析
4.1 指针数组与多级指针
考虑以下复杂指针结构:
c复制char *a[] = {"work","at","alibaba"};
char **pa = a;
pa++;
printf("%s\n", *pa); // "at"
内存布局:
a是一个指针数组,每个元素指向一个字符串常量pa指向a的第一个元素pa++后指向a的第二个元素
4.2 三级指针的解析
更复杂的例子:
c复制char *c[] = {"ENTER","NEW","POINT","FIRST"};
char **cp[] = {c + 3, c + 2, c + 1, c};
char ***cpp = cp;
printf("%s\n", **++cpp); // "POINT"
printf("%s\n", *--*++cpp + 3); // "ER"
printf("%s\n", *cpp[-2] + 3); // "ST"
printf("%s\n", cpp[-1][-1] + 1); // "EW"
逐步解析:
++cpp使cpp指向cp[1]**cpp解引用得到c[2]指向的"POINT"- 再次
++cpp后指向cp[2] *cpp得到c + 1,--后变为c*解引用得到"ENTER",+3跳过前3个字符得到"ER"
4.3 结构体指针运算
结构体指针运算需要考虑结构体大小:
c复制struct Test {
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
} *p = (struct Test*)0x100000;
printf("%p\n", p + 0x1); // 0x100014 (假设结构体大小为20)
printf("%p\n", (unsigned long)p + 0x1); // 0x100001
printf("%p\n", (unsigned int*)p + 0x1); // 0x100004
关键点:
- 指针运算基于指向类型的大小
- 强制类型转换会改变指针运算的行为
5. 常见陷阱与最佳实践
5.1 sizeof与strlen的常见错误
-
混淆字符数组与字符串:
c复制char arr[] = {'a', 'b', 'c'}; printf("%zu\n", strlen(arr)); // 未定义行为,没有'\0' -
错误计算字符串空间:
c复制char *str = "hello"; char buf[sizeof(str)]; // 错误,应该是strlen(str) + 1
5.2 指针运算的注意事项
-
指针越界:
c复制int a[5]; int *p = &a[5]; // 合法地址,但不能解引用 -
类型不匹配:
c复制int (*p)[4]; int a[5][5]; p = a; // 类型不匹配,可能引发问题
5.3 最佳实践建议
-
使用
sizeof(var)而不是sizeof(type),这样即使类型改变也不需要修改代码 -
对于字符串操作,始终确保有足够的空间存储'\0'
-
使用
typedef简化复杂指针类型的声明 -
在指针运算前,先明确指针的类型和指向的内容
-
使用
const修饰指针参数,避免意外修改
在实际开发中,理解这些概念对于编写正确、高效的C代码至关重要。特别是在处理内存操作、字符串处理和底层系统编程时,这些知识会成为你的强大工具。