深入理解指针(最终章):指针运算本质与典型试题剖析
✨ 用 清晰易懂的图解 帮你建立直观认知 ,用通俗的 代码语言 帮你落地理解, 让每个知识点都能 轻松get !
🚀 个人主页 :0xCode小新 · CSDN
🌱 代码仓库 :0xCode小新· Gitee
📌 专栏系列
- 📖 《c语言》
💬 座右铭 : “ 积跬步,以致千里。”
在之前的学习中,我们已经逐步揭开了指针的神秘面纱,从基本概念到多层指针,从指针与数组的关系到动态内存管理,一步步掌握了指针的核心用法与底层逻辑。指针作为C语言的灵魂,其灵活性与强大功能使我们能够更高效地操作内存与数据结构,但也伴随着复杂性与潜在风险。
本系列最终章——《深入理解指针(5)》将作为指针系列的收官之作,聚焦于sizeof
与strlen
的深度对比、数组与指针的经典笔试题解析,以及指针运算中的典型陷阱与技巧。通过这些实战性极强的题目,我们将进一步巩固对指针本质的理解,提升代码分析与调试能力,为后续学习复杂数据结构与系统级编程打下坚实基础。
让我们继续深入指针的最后一片领域,彻底掌握这一C语言中最具挑战也最富魅力的特性。
文章目录
- 1. sizeof和strlen的对比
- 1.1 sizeof
- 基本语法
- 核心特性
- 注意事项
- 1.2 strlen
- 基本语法
- 核心特性
- 1.3 sizeof 和 strlen的对比
- 2.数组和指针笔试题解析
- 2.1 一维数组
- 2.2 字符数组
- 2.3 二维数组
- 2.4 学习建议
- 3. 指针运算笔试题解析
- 3.1 题目1
- 3.2 题目2
- 3.3 题目3
- 3.4 题目4
- 3.5 题目5
- 3.6 题目6
- 3.7 题目7
- 结语
1. sizeof和strlen的对比
在学习C语言过程中中,sizeof
和 strlen
这对“孪生兄弟”总是让我们甚至有一定经验的开发者感到困惑。它们看似都与“大小”或“长度”有关,但其背后的原理和适用场景却天差地别,是各类笔试面试中高频出现的经典考点。那么该怎么理解他们才能避免在实际编程和代码阅读时掉入陷阱呢?
1.1 sizeof
sizeof
是 C 语言中的一个运算符(它不是函数),用于计算数据类型或变量所占用的内存字节数。它的结果是一个无符号整数(size_t
类型),具体值取决于操作数的类型和当前系统环境(如 32 位 / 64 位系统)。
基本语法
sizeof
有两种使用形式:
sizeof(数据类型)
:计算指定数据类型的大小sizeof(变量名)
或sizeof 变量名
:计算变量所对应类型的大小(括号可省略)
#include <stdio.h>int main() {int a;printf("int 类型大小:%zu\n", sizeof(int)); // 计算类型大小printf("变量 a 的大小:%zu\n", sizeof(a)); // 计算变量大小printf("变量 a 的大小:%zu\n", sizeof a); // 省略括号(仅变量可用)return 0;
}
核心特性
- 编译期计算
sizeof
的结果在编译阶段就已确定,不会在运行时计算。这意味着它不影响程序的运行效率,且不能用于计算动态分配内存(如 malloc
分配的空间)的大小。
示例:
int n = 10;
int arr[n]; // 变长数组
printf("%zu\n", sizeof(arr)); // 编译时无法确定,但 C99 允许运行时计算变长数组的大小
- 操作数不执行
sizeof
仅关注操作数的类型,不会执行操作数的表达式。
示例:
int a = 5;
printf("%zu\n", sizeof(a++));
printf("%d\n", a); // a 仍为 5,因为 a++ 未执行
- 与数组的特殊交互
当 sizeof
作用于数组名时,计算的是整个数组的总字节数(而非指针大小)。但数组名作为函数参数、参与运算时,会隐式转换为指向首元素的指针。(在之前文章中提到的数组名使用的两种特殊情况之一)
示例:
int arr[5] = {1,2,3,4,5};
printf("%zu\n", sizeof(arr)); // 整个数组大小:5 * 4 = 20
printf("%zu\n", sizeof(arr+0)); // 指针大小(4 或 8),因为 arr 参与了运算
- 与指针的关系
所有指针类型的 sizeof
结果在同一系统中相同(与指向的数据类型无关):
- 32 位系统:指针占 4 字节
- 64 位系统:指针占 8 字节
示例:
int* p1;
char* p2;
double* p3;
printf("%zu, %zu, %zu\n", sizeof(p1), sizeof(p2), sizeof(p3)); // 均为 4 或 8
- 与字符串的交互
字符串字面量(如 "abc"
)初始化数组时,会包含结尾的 '\0'
,sizeof
会计算包括 '\0'
在内的总字节数。
示例:
char str[] = "abc";
printf("%zu\n", sizeof(str)); // 输出 4
注意事项
sizeof
的结果类型是size_t
,打印时应使用格式符%zu
(C99 标准),而非%d
(可能导致警告或错误)。- 对函数名使用
sizeof
会报错,因为函数名不是数据类型或变量。 - 对空类型
void
使用sizeof
是非法的(void
表示 “无类型”)。
1.2 strlen
strlen
是 C 标准库 <string.h>
中提供的一个字符串长度计算函数,其核心作用是:
计算从指定内存地址开始,到第一个
'\0'
(字符串结束符)为止的字符个数(不包含'\0'
本身)。
基本语法
函数原型:
size_t strlen(const char *str);
- 参数
str
:指向字符串的指针(通常是字符串首元素地址,如数组名、字符指针)。 - 返回值:
size_t
类型(无符号整数,本质是unsigned int
或unsigned long
,取决于系统),表示字符串的有效长度(不含'\0'
)。
strlen
的逻辑非常简单,本质是 “遍历计数”,步骤如下:
- 接收一个起始内存地址(由
str
传入); - 从该地址开始,逐个检查内存中的字节内容;
- 遇到第一个
'\0'
时停止遍历; - 统计遍历过的字节数(即有效字符数),作为返回值。
关键注意:
strlen
完全依赖 '\0'
判断结束,不检查内存越界—— 如果传入的内存中没有 '\0'
,它会一直向后遍历,直到随机遇到 '\0'
为止,导致结果为 “随机值”。
核心特性
- 仅依赖
'\0'
结束,与 “容器大小(一般是数组)” 无关
strlen
不关心字符串存储在 “数组” 还是 “字符常量区”,也不关心容器(如数组)的实际大小,只认 '\0'
。
#include <string.h>
#include <stdio.h>int main() {char arr1[] = {'a', 'b', 'c', 'd', 'e', 'f'}; printf("strlen(arr1) = %zu\n", strlen(arr1)); // 随机值(直到内存中随机遇到 '\0')char arr2[] = "abcdef"; printf("strlen(arr2) = %zu\n", strlen(arr2)); // 6char arr3[] = {'a', 'b', 'c', 'd', 'e', 'f', '\0'}; printf("strlen(arr3) = %zu\n", strlen(arr3)); // 6return 0;
}
- 参数必须是 “指向字符的有效地址”,不能是 “字符值”
strlen
的参数是 const char *
(字符指针),必须传入内存地址;如果传入字符值(如 arr[0]
、'a'
),会导致编译错误或运行崩溃。
char arr[] = "abcdef";
// err:arr[0] 是字符 'a'(值为 97),传入后会被当作内存地址 0x61(非法地址)
printf("%zu\n", strlen(arr[0]));
// err:'a' 是字符常量,同样会被当作非法地址
printf("%zu\n", strlen('a'));
运行时会触发 “非法内存访问”,导致程序崩溃。
- 返回值是
size_t
(无符号整数),注意运算逻辑
size_t
是无符号类型,与有符号整数运算时可能出现逻辑错误(因为无符号数不会为负数)。
#include <string.h>
#include <stdio.h>int main() {char arr1[] = "abc"; char arr2[] = "abcd"; // 错误逻辑:认为 3-4 = -1,判断为真,但实际是无符号运算if (strlen(arr1) - strlen(arr2) > 0) {printf("arr1 更长\n"); // 实际上是会执行的,因为 3-4 = 4294967295(无符号溢出),远大于 0}return 0;
}
- 不修改原字符串(参数是
const char *
)
strlen
的参数被 const
修饰,表示它仅读取字符串内容,不修改原内存
1.3 sizeof 和 strlen的对比
对比维度 | strlen | sizeof |
---|---|---|
本质 | 字符串长度计算函数(需要包含 <string.h> ) | 操作符(不是函数),计算 “数据 / 类型的内存大小” |
计算对象 | 从指定地址到 '\0' 的有效字符数 | 变量 / 数组 / 类型占用的总字节数(与 '\0' 无关) |
对字符串数组的处理 | 只认 '\0' ,忽略数组实际大小 | 计算整个数组的字节数(如 char arr[]="abc" 的 sizeof(arr)=4 ,含 '\0' ) |
对指针的处理 | 从指针指向的地址开始找 '\0' | 计算指针本身的大小(32 位系统 4 字节,64 位系统 8 字节) |
返回值类型 | size_t (无符号整数) | size_t (无符号整数) |
示例对比:
#include <string.h>
#include <stdio.h>int main() {char arr[] = "abcdef"; printf("strlen(arr) = %zu\n", strlen(arr)); // 6printf("sizeof(arr) = %zu\n", sizeof(arr)); // 7char *p = arr; printf("strlen(p) = %zu\n", strlen(p)); // 6printf("sizeof(p) = %zu\n", sizeof(p)); // 4/8return 0;
}
2.数组和指针笔试题解析
有了上述知识的铺垫,相信大家已经初步理解了sizeof和strlen的区别,接下来我们会面临更加头疼的问题:数组与指针的混合运算。
数组和指针密不可分,所以常常放在一起考查,同时又因为其在不同上下文中的微妙变化而布满了陷阱。
不过大家完全不用有压力!其实博主当初学习这块知识时,和现在的大家一样,也总会在代码上犯错 —— 这真的是学习过程中再正常不过的事了。所以针对每一段代码,我都会做非常细致的拆解和讲解,帮大家把知识点理解透、记扎实。
这里再多提一嘴:即便我当时学完了,过了一段时间回头看这些代码,还是会不小心出错。也正因为这样,想跟大家强调:对于学过的知识点,一定要定期复习!大家可以参考艾宾浩斯遗忘曲线来规划复习周期,这样能更高效地巩固记忆,避免学了又忘~
2.1 一维数组
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(a + 0));
printf("%d\n", sizeof(*a));
printf("%d\n", sizeof(a + 1));
printf("%d\n", sizeof(a[1]));
printf("%d\n", sizeof(&a));
printf("%d\n", sizeof(*&a));
printf("%d\n", sizeof(&a + 1));
printf("%d\n", sizeof(&a[0]));
printf("%d\n", sizeof(&a[0] + 1));
看上面的代码,大家先自己独立思考一下程序会输出什么?思考完毕后,再来看解析内容,看看自己的判断是否正确。
代码解析:
printf("%d\n", sizeof(a));
a
是数组名,sizeof(a)
中数组名表示整个数组- 数组有 4 个
int
元素,每个占 4 字节,总大小为4×4=16
字节
printf("%d\n", sizeof(a + 0));
a + 0
中,数组名a
参与运算,隐式转换为指向首元素的指针(int*
类型)a + 0
仍表示首元素地址,sizeof
计算指针大小,结果为 4 /8 字节
printf("%d\n", sizeof(*a));
a
转换为指向首元素的指针,*a
表示首元素(a[0]
),类型为int
- 计算
int
类型大小,结果为 4 字节
printf("%d\n", sizeof(a + 1));
a
转换为指针,a + 1
表示指向第二个元素的指针(int*
类型)- 指针大小为 4 / 8 字节
printf("%d\n", sizeof(a[1]));
a[1]
是数组第二个元素,类型为int
- 结果为
int
类型大小 4 字节
printf("%d\n", sizeof(&a));
&a
表示取整个数组的地址,类型为int(*)[4]
(指向包含 4 个int
的数组的指针)- 尽管指针类型不同,但指针大小仍为 4 / 8 字节
printf("%d\n", sizeof(*&a));
&a
是数组指针,*&a
等价于a
(表示整个数组)- 计算整个数组的大小,结果为 16 字节
printf("%d\n", sizeof(&a + 1));
&a + 1
表示跳过整个数组后的地址(仍为指针类型)- 指针大小为 4 / 8 字节
printf("%d\n", sizeof(&a[0]));
&a[0]
是首元素的地址(int*
类型指针)- 指针大小为 4 / 8 字节
printf("%d\n", sizeof(&a[0] + 1));
&a[0] + 1
是第二个元素的地址(int*
类型指针)- 指针大小为 4 / 8 字节
2.2 字符数组
代码1:
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr + 0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr + 1));
printf("%d\n", sizeof(&arr[0] + 1));
-
printf("%d\n", sizeof(arr));
-
arr
是数组名,在sizeof(arr)
中,数组名表示整个数组(sizeof
对数组名的特殊处理)。 -
数组
arr
初始化时包含 6 个char
类型元素('a'
到'f'
),每个char
占 1 字节。 -
总大小为:
6 × 1 = 6
字节。
-
-
printf("%d\n", sizeof(arr + 0));
-
arr + 0
中,数组名arr
参与运算,此时退化为指向首元素的指针(char*
类型)。 -
arr + 0
等价于首元素'a'
的地址,本质是一个指针。 -
指针大小取决于系统位数:32 位系统为 4 字节,64 位系统为 8 字节。
-
-
printf("%d\n", sizeof(*arr));
-
arr
退化为首元素指针(char*
),*arr
表示首元素'a'
(等价于arr[0]
),类型为char
。 -
char
类型的大小固定为 1 字节,因此结果为 1。
-
-
printf("%d\n", sizeof(arr[1]));
-
arr[1]
表示数组的第二个元素('b'
),类型为char
。 -
同样计算
char
类型的大小,结果为 1 字节。
-
-
printf("%d\n", sizeof(&arr));
-
&arr
表示取整个数组的地址,类型为char(*)[6]
(指向包含 6 个char
的数组的指针)。 -
尽管指针类型与
char*
不同,但所有指针在同一系统中的大小相同(32 位 4 字节,64 位 8 字节)。
-
-
printf("%d\n", sizeof(&arr + 1));
-
&arr + 1
表示从整个数组地址向后偏移一个数组长度的地址(跳过 6 个char
)。 -
结果仍为一个指针(类型还是
char(*)[6]
),因此大小为 4 或 8 字节。(&arr + 1
不会参与计算,不理解的看下sizeof
的核心特性)
-
-
printf("%d\n", sizeof(&arr[0] + 1));
-
&arr[0]
是首元素'a'
的地址(char*
类型),&arr[0] + 1
表示第二个元素'b'
的地址。 -
本质是
char*
类型的指针,大小为 4 或 8 字节。
-
代码2:
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
-
printf("%d\n", strlen(arr));
-
arr
作为数组名,退化为指向首元素'a'
的指针(char*
类型)。 -
strlen
从'a'
开始向后查找'\0'
,但数组arr
初始化时无'\0'
(仅包含'a'
到'f'
)。 -
会一直遍历到内存中随机出现
'\0'
为止,结果为随机值。
-
-
printf("%d\n", strlen(arr + 0));
-
arr + 0
等价于arr
,同样是指向'a'
的指针。 -
与上一条逻辑相同,因无
'\0'
,结果为随机值。
-
-
printf("%d\n", strlen(*arr));
-
*arr
是首元素'a'
(ASCII 值为 97),而非地址。 -
strlen
要求参数为指针(地址),此处将 97 当作地址访问,属于非法内存操作,可能导致程序崩溃。
-
-
printf("%d\n", strlen(arr[1]));
-
arr[1]
是元素'b'
(ASCII 值为 98),同样不是地址。 -
与上一条同理,将字符值当作地址传入,属于非法操作,行为未定义。
-
-
printf("%d\n", strlen(&arr));
-
&arr
是指向整个数组的指针(char(*)[6]
类型),但会被隐式转换为char*
类型。 -
实际指向的地址与
arr
相同(首元素地址),从'a'
开始查找'\0'
,因为无结束符,结果为随机值。
-
-
printf("%d\n", strlen(&arr + 1));
-
&arr + 1
是跳过整个数组后的地址(指向'f'
之后的位置)。 -
strlen
从该位置开始查找'\0'
,位置不确定,结果为随机值。(不检查越界,可以看之前铺垫的strlen
知识)
-
-
printf("%d\n", strlen(&arr[0] + 1));
-
&arr[0] + 1
是指向'b'
的指针(char*
类型)。 -
从
'b'
开始查找'\0'
,因无结束符,结果为随机值。
-
代码3:
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
-
printf("%d\n", sizeof(arr));
-
arr
是数组名,在sizeof(arr)
中表示整个数组。 -
字符串字面量
"abcdef"
包含 6 个可见字符,会自动添加'\0'
作为结束符,共 7 个char
元素。 -
每个
char
占 1 字节,总大小为7 × 1 = 7
字节。
-
-
printf("%d\n", sizeof(arr + 0));
-
arr + 0
中,数组名arr
参与运算,退化为指向首元素'a'
的指针(char*
类型)。 -
指针大小取决于系统位数:32 位系统为 4 字节,64 位系统为 8 字节。
-
-
printf("%d\n", sizeof(*arr));
-
arr
退化为首元素指针,*arr
等价于arr[0]
(字符'a'
),类型为char
。 -
char
类型大小为 1 字节,结果为 1。
-
-
printf("%d\n", sizeof(arr[1]));
-
arr[1]
是数组第二个元素(字符'b'
),类型为char
。 -
计算
char
类型大小,结果为 1 字节。
-
-
printf("%d\n", sizeof(&arr));
-
&arr
表示整个数组的地址,类型为char(*)[7]
(指向包含 7 个char
的数组的指针)。 -
所有指针在同一系统中大小相同,32 位为 4 字节,64 位为 8 字节。
-
-
printf("%d\n", sizeof(&arr + 1));
-
&arr + 1
是跳过整个数组后的地址,仍为指针类型(char(*)[7]
)。 -
指针大小为 4 或 8 字节(取决于系统位数)。
-
-
printf("%d\n", sizeof(&arr[0] + 1));
&arr[0] + 1
是指向第二个元素'b'
的指针(char*
类型)。- 指针大小为 4 或 8 字节(取决于系统位数)。
代码4:
char arr[] = "abcdef";
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
-
printf("%d\n", strlen(arr));
-
arr
退化为指向首元素'a'
的指针(char*
类型)。 -
字符串
"abcdef"
包含'\0'
结束符,strlen
从'a'
开始计数,到'\0'
停止(不包含'\0'
)。 -
有效字符为
'a'
到'f'
共 6 个,结果为 6。
-
-
printf("%d\n", strlen(arr + 0));
-
arr + 0
等价于arr
,同样指向'a'
的指针。 -
与上一条逻辑相同,从
'a'
开始到'\0'
结束,结果为 6。
-
-
printf("%d\n", strlen(*arr));
-
*arr
是首元素'a'
(ASCII 值为 97),而非地址。 -
strlen
要求参数为指针,此处将 97 当作地址访问,属于非法内存操作,可能导致程序崩溃。
-
-
printf("%d\n", strlen(arr[1]));
-
arr[1]
是元素'b'
(ASCII 值为 98),不是地址。 -
与上一条同理,将字符值当作地址传入,属于非法操作,行为未定义。
-
-
printf("%d\n", strlen(&arr));
-
&arr
是指向整个数组的指针(char(*)[7]
类型),会隐式转换为char*
类型。 -
实际指向地址与
arr
相同(首元素'a'
的地址),从'a'
到'\0'
共 6 个有效字符,结果为 6。
-
-
printf("%d\n", strlen(&arr + 1));
-
&arr + 1
是跳过整个数组后的地址(指向'\0'
之后的位置)。 -
strlen
从该位置开始查找'\0'
,因未知后续内存中'\0'
的位置,结果为随机值。
-
-
printf("%d\n", strlen(&arr[0] + 1));
-
&arr[0] + 1
是指向'b'
的指针(char*
类型)。 -
从
'b'
开始计数到'\0'
,有效字符为'b'
到'f'
共 5 个,结果为 5。
-
代码5:
char *p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p+1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p+1));
printf("%d\n", sizeof(&p[0]+1));
-
printf("%d\n", sizeof(p));
-
p
是字符指针变量,指向字符串常量"abcdef"
的首地址。 -
sizeof(p)
计算的是指针变量本身的大小,与指向的数据无关。 -
32 位系统中指针占 4 字节,64 位系统中占 8 字节。
-
-
printf("%d\n", sizeof(p + 1));
-
p + 1
表示指针向后偏移 1 个char
类型的地址(指向'b'
),仍为指针类型(char*
)。 -
指针大小取决于系统位数,结果为 4 或 8 字节。
-
-
printf("%d\n", sizeof(*p));
-
*p
表示指针p
指向的首元素('a'
),类型为char
。 -
char
类型的大小固定为 1 字节,结果为 1。
-
-
printf("%d\n", sizeof(p[0]));
-
p[0]
等价于*(p + 0)
,表示首元素'a'
,类型为char
。 -
结果为
char
类型的大小 1 字节。
-
-
printf("%d\n", sizeof(&p));
-
&p
表示取指针变量p
本身的地址,类型为char**
(指向指针的指针)。 -
无论指针层级如何,同一系统中所有指针大小相同,结果为 4 或 8 字节。
-
-
printf("%d\n", sizeof(&p + 1));
-
&p + 1
表示指针p
的地址向后偏移 1 个char*
类型的地址,仍为指针类型(char**
)。 -
指针大小为 4 或 8 字节(取决于系统位数)。
-
-
printf("%d\n", sizeof(&p[0] + 1));
-
&p[0]
等价于p
(指向'a'
的指针),&p[0] + 1
指向'b'
,类型为char*
。 -
指针大小为 4 或 8 字节(取决于系统位数)。
-
代码6:
char *p = "abcdef";
printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));
-
printf("%d\n", strlen(p));
-
p
是指向字符串"abcdef"
首元素'a'
的指针(char*
类型)。 -
strlen
从'a'
开始计数,直到遇到'\0'
停止,字符串包含'a'
到'f'
共 6 个有效字符,结果为 6。
-
-
printf("%d\n", strlen(p + 1));
-
p + 1
指向字符串中'b'
的地址(char*
类型)。 -
从
'b'
开始计数到'\0'
,有效字符为'b'
到'f'
共 5 个,结果为 5。
-
-
printf("%d\n", strlen(*p));
-
*p
是p
指向的首元素'a'
(ASCII 值为 97),并非地址。 -
strlen
要求参数为指针(地址),此处将 97 当作地址访问,属于非法内存操作,可能导致程序崩溃。
-
-
printf("%d\n", strlen(p[0]));
-
p[0]
等价于*p
,即字符'a'
(ASCII 值 97),不是地址。 -
与上一条同理,将字符值当作地址传入,属于非法操作,行为未定义。
-
-
printf("%d\n", strlen(&p));
-
&p
是指针变量p
本身的地址(char**
类型),会被隐式转换为char*
类型。 -
strlen
从该地址开始查找'\0'
,但此处内存中是否有'\0'
及位置未知,结果为随机值。
-
-
printf("%d\n", strlen(&p + 1));
-
&p + 1
是指针p
地址向后偏移一个char*
类型的地址,仍为指针(char**
类型)。 -
strlen
从该位置开始查找'\0'
,因内存内容未知,结果为随机值。
-
-
printf("%d\n", strlen(&p[0] + 1));
-
&p[0]
等价于p
(指向'a'
),&p[0] + 1
指向'b'
(char*
类型)。 -
从
'b'
开始计数到'\0'
,有效字符为 5 个,结果为 5。
-
2.3 二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));
printf("%d\n",sizeof(*(a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(*(a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));
-
printf("%d\n", sizeof(a));
-
a
是二维数组名,在sizeof(a)
中表示整个二维数组。 -
数组定义为
int a[3][4]
,包含 3 行 4 列共 12 个int
元素,每个int
占 4 字节。 -
总大小为:
3 × 4 × 4 = 48
字节。
-
-
printf("%d\n", sizeof(a[0][0]));
-
a[0][0]
是二维数组第 0 行第 0 列的元素,类型为int
。 -
int
类型大小为 4 字节,结果为 4。
-
-
printf("%d\n", sizeof(a[0]));
-
a[0]
表示二维数组的第 0 行(可看作一个一维数组int[4]
)。 -
每行包含 4 个
int
元素,大小为:4 × 4 = 16
字节。
-
-
printf("%d\n", sizeof(a[0] + 1));
-
a[0]
作为一维数组名参与运算,退化为指向第 0 行首元素的指针(int*
类型)。 -
a[0] + 1
指向第 0 行第 1 列元素,仍为指针类型,大小为 4 或 8 字节(取决于系统位数)。
-
-
printf("%d\n", sizeof(*(a[0] + 1)));
-
a[0] + 1
是指向第 0 行第 1 列元素的指针,*(a[0] + 1)
表示该元素(int
类型)。 -
结果为
int
类型的大小 4 字节。
-
-
printf("%d\n", sizeof(a + 1));
-
a
作为二维数组名参与运算,退化为指向第 0 行的指针(int(*)[4]
类型,数组指针)。 -
a + 1
指向第 1 行,仍为数组指针类型,大小为 4 或 8 字节(取决于系统位数)。
-
-
printf("%d\n", sizeof(*(a + 1)));
-
a + 1
是指向第 1 行的数组指针,*(a + 1)
表示第 1 行整个一维数组(int[4]
类型)。 -
每行大小为 16 字节,结果为 16。
-
-
printf("%d\n", sizeof(&a[0] + 1));
-
&a[0]
是指向第 0 行的数组指针(int(*)[4]
类型)。 -
&a[0] + 1
指向第 1 行,仍为数组指针类型,大小为 4 或 8 字节(取决于系统位数)。
-
-
printf("%d\n", sizeof(*(&a[0] + 1)));
-
&a[0] + 1
是指向第 1 行的数组指针,*(&a[0] + 1)
表示第 1 行整个一维数组。 -
每行大小为 16 字节,结果为 16。
-
-
printf("%d\n", sizeof(*a));
-
a
退化为指向第 0 行的数组指针,*a
表示第 0 行整个一维数组(int[4]
类型)。 -
大小为 16 字节。
-
-
printf("%d\n", sizeof(a[3]));
-
a[3]
看似越界访问(数组只有 3 行,索引 0~2),但sizeof
仅计算类型大小,不实际访问内存。 -
a[3]
的类型与其他行一致(int[4]
),大小为 16 字节。
-
2.4 学习建议
面对这些题目,切忌死记硬背答案。最好的方法是:
- 画内存布局图:将数组、指针、字符串在内存中的样子画出来。
- 写出每一步的类型:仔细分析每个表达式的类型是什么(
int*
?int(*)[4]
?),+1
操作会跳过多少字节。 - 结合编译器验证:自己编写代码并打印结果和地址(
%p
),观察地址的变化是否与你的分析一致。
那么现在通过这种抽丝剥茧式的分析,你会发现这些题目的规律性极强。掌握这些规律,是你理解sizeof
和strlen
的关键
3. 指针运算笔试题解析
在理解了上面的题目后我们会发现,对指针的深入掌握,绝不是靠死记硬背几条规则就能实现的。它更需要我们沉下心来,细致拆解每一层间接引用的逻辑,这样才能真正吃透指针的核心原理。
本讲中的指针运算笔试题,正是为此而设计的。它们看似复杂甚至刁钻,实则是对指针核心概念最凝练的考察:
- 指针的类型决定了指针算术运算的步长。
- 数组名在大多数情况下会“退化”为首元素的地址。
&
(取地址)和*
(解引用)操作符的深层含义。- 多级指针与指针数组的协同工作方式。
这些复杂的题目,你第一次看到时觉得无从下手完全没关系 —— 当初刚接触的我,也和现在的你们一样。接下来我会先带着大家一步步拆解、理解,不过最后还是希望大家能试着独立思考这些题目,尤其是要能清晰地画出每一块内存的布局,这才是真正掌握知识点的关键。
3.1 题目1
#include <stdio.h>
int main()
{int a[5] = { 1, 2, 3, 4, 5 };int *ptr = (int *)(&a + 1);printf( "%d,%d", *(a + 1), *(ptr - 1));return 0;
}
&a + 1
:数组指针&a
向后偏移 1 个数组长度(即跳过整个数组的 5 个元素),指向数组a
最后一个元素(5
)的下一个位置。int *ptr = (int *)(&a + 1)
:将&a + 1
的结果(也就是数组指针类型)强制转换为int*
类型,赋值给ptr
。此时ptr
指向数组a
末尾的下一个位置。*(a + 1)
:a
退化为指向首元素的指针(int*
),a + 1
指向第二个元素(2
),解引用后结果为2
。*(ptr - 1)
:ptr
是int*
类型指针,ptr - 1
向前偏移 1 个int
大小,指向数组最后一个元素(5
),解引用后结果为5
。
3.2 题目2
//在X86环境下
//假设结构体的⼤⼩是20个字节
//程序输出的结果是啥?
struct Test
{int Num;char* pcName;short sDate;char cha[2];short sBa[4];
}*p = (struct Test*)0x100000;
int main()
{printf("%p\n", p + 0x1);printf("%p\n", (unsigned long)p + 0x1);printf("%p\n", (unsigned int*)p + 0x1);return 0;
}
在 X86 环境下(32 位系统,指针大小为 4 字节),结合结构体大小为 20 字节的前提,我们来逐条分析:
printf("%p\n", p + 0x1); // 00100014
p
是struct Test*
类型的指针(指向结构体的指针)。- 指针加法运算时,偏移量 = 指针类型大小 × 增量。此处增量为
0x1
(即 1),结构体大小为 20 字节(0x14
)。 - 计算:
0x100000 + (20 × 1) = 0x100000 + 0x14 = 0x100014
,输出格式化为%p
即00100014
。
printf("%p\n", (unsigned long)p + 0x1); // 00100001
(unsigned long)p
将指针p
的地址值(0x100000
)强制转换为无符号长整型(数值)。- 数值加法直接累加:
0x100000 + 0x1 = 0x100001
,输出为00100001
。
printf("%p\n", (unsigned int*)p + 0x1); // 00100004
(unsigned int*)p
将p
强制转换为unsigned int*
类型(指向无符号整数的指针)。- X86 环境下
unsigned int
大小为 4 字节,指针加法偏移量 = 4 × 1 = 4 字节(0x4
)。 - 计算:
0x100000 + 0x4 = 0x100004
,输出为00100004
。
3.3 题目3
#include <stdio.h>
int main()
{int a[3][2] = { (0, 1), (2, 3), (4, 5) };int* p;p = a[0];printf("%d", p[0]);return 0;
}
-
数组初始化的关键问题
- 数组定义为
int a[3][2] = { (0, 1), (2, 3), (4, 5) }
,但这里使用的是逗号表达式而非花括号初始化。(肯定有很多人掉进这个坑) - 逗号表达式的结果是最后一个表达式的值,因此:
(0, 1)
的结果为1
(2, 3)
的结果为3
(4, 5)
的结果为5
- 数组实际初始化后的值为:
a[3][2] = { {1, 3}, {5, 0}, {0, 0} }
。
- 数组定义为
-
指针赋值与访问
p = a[0]
中,a[0]
是二维数组第 0 行的数组名,退化为指向第 0 行首元素的指针(int*
类型),因此p
指向a[0][0]
。p[0]
等价于*(p + 0)
,即访问p
指向的首元素,也就是a[0][0]
,其值为1
。
3.4 题目4
//假设环境是x86环境,程序输出的结果是啥?
#include <stdio.h>
int main()
{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]);return 0;
}
-
数组与指针类型
int a[5][5]
:二维数组,每行包含 5 个int
元素,每行大小为5×4=20
字节(int
占 4 字节)。int(*p)[4]
:p
是数组指针,指向包含 4 个int
的一维数组,每次指针移动的步长为4×4=16
字节。
-
指针赋值与偏移计算
-
p = a
:将二维数组a
的首地址赋值给p
(a
退化为指向首行的指针,此处强制转换为int(*)[4]
类型)。 -
p[4][2]
的地址计算:p
每次移动 1 步跳过 16 字节,p[4]
表示偏移 4 步:4×16=64
字节;再偏移 2 个
int
:2×4=8
字节;总偏移:
64+8=72
字节。 -
a[4][2]
的地址计算:a
每行 20 字节,a[4]
表示偏移 4 行:4×20=80
字节;再偏移 2 个
int
:2×4=8
字节;总偏移:
80+8=88
字节。
-
-
地址差值分析
- 地址差:
72 - 88 = -4
(以字节为单位,两个地址的实际差值)。 - 在 x86 的 32 位环境中,
%p
打印有符号整数的补码:-4
的 32 位补码为0xFFFFFFFC
; %d
直接打印有符号整数结果-4
。
- 地址差:
3.5 题目5
#include <stdio.h>
int main()
{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));return 0;
}
-
数组初始化与存储
int aa[2][5]
是一个 2 行 5 列的二维数组,初始化值为{{1,2,3,4,5}, {6,7,8,9,10}}
,在内存中连续存储。
-
指针
ptr1
分析&aa
表示取整个二维数组的地址,类型为int(*)[2][5]
(指向 2 行 5 列数组的指针)。&aa + 1
会跳过整个二维数组(共 10 个int
元素,总大小为10×4=40
字节),指向数组末尾的下一个位置。int* ptr1 = (int*)(&aa + 1)
将该地址强制转换为int*
类型,ptr1
指向数组最后一个元素(10
)的下一个位置。*(ptr1 - 1)
:ptr1 - 1
向前偏移 1 个int
大小,指向最后一个元素10
,解引用结果为10
。
-
指针
ptr2
分析aa
是二维数组名,退化为指向第 0 行的指针(int(*)[5]
类型)。aa + 1
指向第 1 行(跳过 5 个int
元素),*(aa + 1)
表示第 1 行的数组名(退化为指向第 1 行首元素6
的指针int*
)。int* ptr2 = (int*)(*(aa + 1))
中,ptr2
指向第 1 行首元素6
。*(ptr2 - 1)
:ptr2 - 1
向前偏移 1 个int
大小,指向第 0 行最后一个元素5
,解引用结果为5
。
3.6 题目6
#include <stdio.h>
int main()
{char* a[] = { "work","at","alibaba" };char** pa = a;pa++;printf("%s\n", *pa);return 0;
}
-
数组与指针定义
char* a[] = {"work","at","alibaba"}
定义了一个指针数组,数组a
的每个元素都是char*
类型(指向字符串的指针)。- 数组
a
存储了 3 个字符串的首地址:a[0]
指向"work"
,a[1]
指向"at"
,a[2]
指向"alibaba"
。
-
二级指针
pa
的操作char**pa = a
:pa
是二级指针(指向指针的指针),初始指向指针数组a
的首元素(即a[0]
的地址)。pa++
:二级指针pa
向后偏移 1 个位置(跳过一个char*
类型的大小),此时pa
指向a[1]
的地址。
-
打印内容分析
*pa
表示解引用pa
,得到a[1]
的值(即指向字符串"at"
的指针)。printf("%s\n", *pa)
以字符串格式打印该指针指向的内容,结果为"at"
。
3.7 题目7
#include <stdio.h>
int main()
{char* c[] = { "ENTER","NEW","POINT","FIRST" };char** cp[] = { c + 3,c + 2,c + 1,c };char*** cpp = cp;printf("%s\n", **++cpp);printf("%s\n", *-- * ++cpp + 3);printf("%s\n", *cpp[-2] + 3);printf("%s\n", cpp[-1][-1] + 1);return 0;
}
先明确各指针的初始指向关系:
c
是指针数组,存储 4 个字符串地址:c[0]="ENTER"
、c[1]="NEW"
、c[2]="POINT"
、c[3]="FIRST"
cp
是二级指针数组,存储 4 个指向c
的指针:cp[0]=c+3
、cp[1]=c+2
、cp[2]=c+1
、cp[3]=c
cpp
是三级指针,初始指向cp[0]
-
printf("%s\n", **++cpp); // POINT
-
++cpp
:cpp
从指向cp[0]
变为指向cp[1]
(cpp
现在指向cp[1]
) -
第一次解引用
*cpp
:得到cp[1]
的值c+2
(指向c[2]
) -
第二次解引用
**cpp
:得到c[2]
的值(指向字符串"POINT"
) -
输出字符串:
POINT
-
-
printf("%s\n", *-- * ++cpp + 3); // ER
-
++cpp
:cpp
从指向cp[1]
变为指向cp[2]
-
第一次解引用
*cpp
:得到cp[2]
的值c+1
-
--*cpp
:将c+1
减 1,变为c
(指向c[0]
) -
第二次解引用
*(--*cpp)
:得到c[0]
的值(指向"ENTER"
) -
+3
:指针向后偏移 3 个字符,指向"ENTER"
中第 3 个字符后(即"ER"
) -
输出字符串:
ER
-
-
printf("%s\n", *cpp[-2] + 3); // ST
-
cpp[-2]
等价于*(cpp-2)
:cpp
当前指向cp[2]
,减 2 后指向cp[0]
,解引用得到cp[0]
的值c+3
-
解引用
*cpp[-2]
:得到c[3]
的值(指向"FIRST"
) -
+3
:指针向后偏移 3 个字符,指向"FIRST"
中第 3 个字符后(即"ST"
) -
输出字符串:
ST
-
-
printf("%s\n", cpp[-1][-1] + 1); // EW
-
cpp[-1]
等价于*(cpp-1)
:cpp
当前指向cp[2]
,减 1 后指向cp[1]
,解引用得到cp[1]
的值c+2
-
cpp[-1][-1]
等价于*((c+2)-1) = *(c+1)
:得到c[1]
的值(指向"NEW"
) -
+1
:指针向后偏移 1 个字符,指向"NEW"
中第 1 个字符后(即"EW"
) -
输出字符串:
EW
-
结语
穿越这一系列指针笔试题的“魔鬼训练”,我们仿佛进行了一场深入内存腹地的探险。从最初的 sizeof
与 strlen
的对比,到一维、二维数组的层层剥茧,再到多级指针的曲折迂回,我们一次又一次地验证了那个核心法则:指针的类型,是理解其一切行为的钥匙。它决定了指针算术的步长,定义了解引用的深度,描绘了内存的访问边界。
这些题目或许曾让你感到困惑甚至挫败,但这正是深入理解指针的必经之路。它们迫使你跳出直觉,去思考编译器如何看待你的代码,数据在内存中如何实际排布。掌握这些知识,不仅能让你在面试中游刃有余,更能让你在日后的程序生涯中,写出更高效、更健壮、更接近机器本质的代码。
请记住,指针并不可怕,它只是需要一份细心和一份耐心。当你下次再遇到复杂的指针声明时,不妨从变量名开始,由内向外,结合运算符优先级逐步解析;当你对一段指针操作代码的行为不确定时,尝试在纸上画出内存布局图,一步一步追踪指针的移动和数据的流向——这是最有效的学习方法。
至此,我们的指针系列博客就告一段落了。希望这段学习经历,能让你不仅学会了指针的语法规则,更培养了一种从内存视角思考问题的能力。这种能力,将伴随你在编程的道路上不断前行,帮助你写出更优雅、更高效的代码。
如今,指针系列虽已结束,但这并不意味着学习的终止。C 语言的世界广阔无垠,指针只是其中一块重要的基石。掌握了指针,你在后续学习链表、树、图等数据结构时将会更加得心应手,在进行系统编程、嵌入式开发等领域的探索时也会更有底气。
感谢你一路的陪伴与坚持。愿你带着这份对指针的理解,在 C 语言的世界里继续探索,不断精进,开启新的编程征程!
思考与互动
在你学习指针的过程中,哪个概念或哪道题目曾让你有“顿悟”的感觉?欢迎在评论区分享你的经历与见解!