本文深入解析C语言中sizeof与strlen的关键区别,并通过一系列典型指针笔试题帮助读者掌握指针在数组、字符串和内存运算中的应用技巧。
文章目录
一、sizeof和strlen的对比
1.1 sizeof操作符
sizeof是C语言中的单目操作符,用于计算变量或类型所占内存空间的大小(单位为字节)。它有以下重要特性:
-
计算对象可以是变量或类型
-
不关心内存中存储的具体数据
-
在编译期间计算结果
-
对数组名使用sizeof会返回整个数组的大小
#include <stdio.h>
int main() {
int a = 10;
printf("%zu\n", sizeof(a)); // 4 (int类型大小)
printf("%zu\n", sizeof a); // 4 (可省略括号)
printf("%zu\n", sizeof(int)); // 4 (类型大小)
double arr[10];
printf("%zu\n", sizeof(arr)); // 80 (10*8=80字节)
return 0;
}
1.2 strlen函数
strlen是C标准库函数(需包含 <string.h> ),用于计算字符串的长度(不包含结束符’\0’)。关键特性包括:
-
从给定地址开始向后扫描,直到遇到’\0’
-
如果没有找到’\0’,会继续向后扫描,可能导致越界访问
-
只能用于以’\0’结尾的有效字符串
#include <stdio.h>
#include <string.h>
int main() {
char arr1[3] = {'a', 'b', 'c'}; // 无结束符
char arr2[] = "abc"; // 自动添加'\0'
printf("%zu\n", strlen(arr1)); // 未定义行为(可能输出随机值)
printf("%zu\n", strlen(arr2)); // 3 (有效字符串)
return 0;
}
1.3 sizeof与strlen对比总结
特性 | sizeof | strlen |
---|---|---|
本质 | 编译时操作符 | 运行时库函数 |
参数 | 变量/类型 | 字符串指针 |
计算内容 | 内存占用大小 | '\0' 前的字符数量 |
是否关心内存数据 | 否 | 是(依赖'\0' ) |
数组名作为参数 | 返回整个数组大小 | 退化为指针 |
安全性 | 安全 | 可能越界访问 |
二、数组和指针笔试题解析
只有
sizeof(数组名)
和&数组名
才表示整个数组
2.1 一维数组解析
int a[] = {1,2,3,4};
//64位系统
printf("%zu\n", sizeof(a)); // 16 - 整个数组大小(4个int*4)
printf("%zu\n", sizeof(a+0)); // 8 - 首元素地址
printf("%zu\n", sizeof(*a)); // 4 - 第一个元素(int)
printf("%zu\n", sizeof(a[1])); // 4 - 第二个元素(int)
printf("%zu\n", sizeof(&a)); // 8 - 数组地址(指针)
printf("%zu\n", sizeof(&a+1)); // 8 - 跳过整个数组后的地址
printf("%zu\n", sizeof(&a[0])); // 8 - 首元素地址
printf("%zu\n", sizeof(&a[0]+1));// 8 - 第二个元素地址
2.2 字符数组解析
代码1:sizeof应用
char arr[] = {'a','b','c','d','e','f'};
printf("%zu\n", sizeof(arr)); // 6 - 整个数组大小
printf("%zu\n", sizeof(arr+0)); // 8 - 首元素地址
printf("%zu\n", sizeof(*arr)); // 1 - 第一个元素(char)
printf("%zu\n", sizeof(arr[1])); // 1 - 第二个元素
printf("%zu\n", sizeof(&arr)); // 8 - 数组地址
printf("%zu\n", sizeof(&arr+1)); // 8 - 跳过整个数组后的地址
printf("%zu\n", sizeof(&arr[0]+1));//8 - 第二个元素地址
代码2:strlen应用(危险!)
char arr[] = {'a','b','c','d','e','f'}; // 无结束符
printf("%zu\n", strlen(arr)); // 未定义!(可能一直找到内存中的'\0')
printf("%zu\n", strlen(arr+0)); // 同上
printf("%zu\n", strlen(*arr)); // 编译错误!(参数应为char*)
// 其他行类似,都会导致问题
让我们重点分析第三个代码片段让我们重点分析第三个代码片段
从strlen库函数
可以看出,其参数是char* str
,即传入的是地址。而*arr
表示的是第一个元素'a'
,这是一个字符而非地址。当字符'a'
被自动转换为ASCII
值97时,系统会尝试将这个数值作为内存地址访问。由于我们没有权限访问这个地址,程序就会崩溃终止。
代码3:有效字符串sizeof
char arr[] = "abcdef"; // 包含隐含的'\0'
printf("%zu\n", sizeof(arr)); // 7 - 6字符+1个'\0'
printf("%zu\n", sizeof(arr+0)); // 8 - 首元素地址
printf("%zu\n", sizeof(*arr)); // 1 - 首字符
printf("%zu\n", sizeof(arr[1])); // 1 - 第二个字符
printf("%zu\n", sizeof(&arr)); // 8 - 数组地址
printf("%zu\n", sizeof(&arr+1)); // 8 - 跳过整个数组
printf("%zu\n", sizeof(&arr[0]+1));//8 - 第二个字符地址
** 代码4:有效字符串strlen**
char arr[] = "abcdef"; // 包含隐含的'\0'
printf("%zu\n", strlen(arr)); // 6 - 到'\0'前的字符数
printf("%zu\n", strlen(arr+0)); // 6 - 同上
// printf("%zu\n", strlen(*arr)); // 错误!参数类型不匹配
// printf("%zu\n", strlen(arr[1]));// 错误!
printf("%zu\n", strlen(&arr)); // 6 - 与arr相同
printf("%zu\n", strlen(&arr+1)); // 未定义!(跳过整个数组)
printf("%zu\n", strlen(&arr[0]+1));// 5 - 从b开始计算
2.3 二维数组解析
int a[3][4] = {0};
printf("%zu\n", sizeof(a)); // 48 (3*4*4)
printf("%zu\n", sizeof(a[0][0])); // 4 (int)
printf("%zu\n", sizeof(a[0])); // 16 (第一行数组大小)
printf("%zu\n", sizeof(a[0]+1)); // 8 (第一行第二个元素地址)
printf("%zu\n", sizeof(a+1)); // 8 (第二行地址)
printf("%zu\n", sizeof(&a[0]+1)); // 8 (第二行地址)
printf("%zu\n", sizeof(*a)); // 16 (第一行数组大小)
printf("%zu\n", sizeof(a[3])); // 16 (类型大小,不访问内存)
在之前的文章中我们讨论过二维数组的指针问题。其中,a[i]代表数组名,即二维数组中某行对应的一维数组名称。因此,
sizeof(a[i])
和&a[i]
都是获取整个一维数组的地址。
数组名意义总结:
-
sizeof(数组名)
:整个数组的大小 -
&数组名
:整个数组的地址 -
其他情况:退化为首元素地址
三、指针运算笔试题解析
3.1 题目1:数组指针偏移
int a[5] = {1, 2, 3, 4, 5};
int *ptr = (int *)(&a + 1);
printf("%d,%d", *(a + 1), *(ptr - 1)); // 2,5
解析:
-
a+1
:首元素地址+1 → 第二个元素(2) -
&a+1
:跳过整个数组 → 指向数组末尾后 -
ptr-1
:回退一个int → 指向最后一个元素(5)
3.2 题目2:结构体指针运算
struct Test {
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p = (struct Test*)0x100000;
//结构体假设20个字节
//十六进制0x1代表1
int main() {
printf("%p\n", p + 0x1); // 0x100014 (+20字节,20转化为十六进制14)
printf("%p\n", (unsigned long)p + 0x1); // 0x100001
printf("%p\n", (unsigned int*)p + 0x1); // 0x100004
return 0;
}
结构体指针加1会跳过整个结构体的大小
解析:
-
指针+1:增加类型大小(20→0x14)
-
数值+1:增加1
-
整型指针+1:增加4字节
3.3 题目3:逗号表达式陷阱
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p = a[0];
printf("%d", p[0]); // 1
解析:
-
逗号表达式返回最后一个值
-
实际初始化:{1, 3, 5}(其他为0)
-
a[0]
指向第一行{1,3}(内存布局:{ {1,3},{5,0},{0,0} }
) -
p[0]
访问第一个元素1
3.4 题目4:数组指针类型差异
int a[5][5];
int(*p)[4] = a; // 类型为int(*)[4]
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
// FFFFFFFC,-4
从图中可见,
&p[4][2]
与&a[4][2]
之间相差4个int*
的内存空间。由于指针相减得到的是元素间隔数,且&a[4][2]
地址高于&p[4][2]
,因此&p[4][2] - &a[4][2] = -4
(printf("%d")
输出结果)。当使用printf("%p")
打印地址时,系统会将整数值-4转换为十六进制形式。在补码表示中,-4对应二进制11111111 11111111 11111111 11111100
,转换为十六进制即为FFFFFFFC
。
3.5 题目5:二维数组指针
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
解析
-
&aa+1
:指向数组末尾后 -
ptr1-1
:回退一个int → 最后一个元素10 -
aa+1
:第二行地址 -
*(aa+1)
:第二行首元素(6) -
ptr2-1
:第一行最后一个元素(5)
3.6 题目6:指针数组
char *a[] = {"work","at","alibaba"};
char **pa = a;
pa++;
printf("%s\n", *pa); // "at"
解析:
-
a
:指针数组,元素为char* -
pa
指向第一个指针(“work”) -
pa++
后指向第二个指针(“at”) -
*pa
解引用得到"at"
3.7 题目7:三级指针应用
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
指向cp[1]→c+2**++cpp
→*(c+2)→"POINT"
-
再次
++cpp
指向cp[2]→c+1-
--*++cpp
→–(c+1)→c+0 -
*--*++cpp
→*(c+0)→"ENTER" -
+3→"ER"
-
-
cpp[-2]
→*(cpp-2)→cp[0]→c+3-
*cpp[-2]
→*(c+3)→"FIRST" -
+3→"ST"
-
-
cpp[-1]
→*(cpp-1)→cp[1]→c+2-
cpp[-1][-1]
→*(c+2-1)→*(c+1)→"NEW" -
+1→"EW"
-
四、总结
掌握指针的核心要点:
-
理解指针
类型决定偏移量
-
区分数组名在
sizeof、&
操作中的特殊含义 -
明确
sizeof是编译时
操作,strlen是运行
时函数 -
注意指针运算的单位由基类型决定
-
多维数组本质是"
数组的数组
"
透彻理解这些概念将帮助你在C语言编程中游刃有余地处理指针和内存操作。
如果觉得内容对你有帮助,别忘了点赞 + 收藏,你的支持是持续分享的动力~ 咱们下篇技术文章再见!