好玩的指针笔试题|你来试试?
指针作为C语言的核心概念,一直是面试和笔试中的高频考点。很多开发者在面对指针相关的题目时,常常因为对内存模型理解不够深入而出现错误。本文将通过一系列经典的指针笔试题,带你深入理解指针的工作原理,掌握常见题型的解题思路。
无论你是正在准备面试的求职者,还是想要巩固C语言基础的开发者,这些题目都能帮助你检验对指针的掌握程度。每道题目都配有详细解析,从内存布局到指针运算,逐一拆解背后的原理。
1. 指针基础概念回顾
在开始做题之前,我们先快速回顾一下指针的核心概念,为后续的题目解析打下基础。
1.1 什么是指针
指针本质上是一个变量,其存储的值是另一个变量的内存地址。通过指针,我们可以间接访问和操作内存中的数据。理解指针的关键在于分清"指针本身"和"指针指向的内容"。
C
1
int a = 10; // 定义一个整型变量a
2
int *p = &a; // 定义指针p,指向变量a的地址
4
printf("a的值: %d\n", a); // 直接访问:输出10
5
printf("p指向的值: %d\n", *p); // 间接访问:输出10
6
printf("a的地址: %p\n", &a); // 输出a的地址
7
printf("p的值: %p\n", p); // 输出p存储的地址(即a的地址)
1.2 指针的常见操作
指针支持多种运算操作,这些操作是笔试题中经常考察的重点:
- 取地址操作(&):获取变量的内存地址
- 解引用操作(*):通过指针访问指向的内存内容
- 指针算术运算:对指针进行加减操作,移动指针位置
- 指针比较:比较两个指针是否指向同一内存位置
C
1
int arr[5] = {1, 2, 3, 4, 5};
2
int *ptr = arr; // ptr指向数组首元素
4
printf("第一个元素: %d\n", *ptr); // 输出1
5
printf("第二个元素: %d\n", *(ptr + 1)); // 输出2
6
printf("第三个元素: %d\n", ptr[2]); // 输出3
2. 基础指针笔试题解析
现在让我们开始解答一些经典的指针笔试题,从简单到复杂逐步深入。
2.1 题目一:指针的基本运算
C
9
printf("*p1 = %d, *p2 = %d\n", *p1, *p2);
12
printf("执行 *p1 = *p2 + 5 后:\n");
13
printf("a = %d, b = %d\n", a, b);
14
printf("*p1 = %d, *p2 = %d\n", *p1, *p2);
解析:
这道题考察指针解引用和赋值操作。初始时,p1指向a,p2指向b。执行*p1 = *p2 + 5时,相当于a = b + 5,即a = 20 + 5 = 25。b的值保持不变。
运行结果:
2.2 题目二:指针与数组的关系
C
4
int arr[] = {1, 2, 3, 4, 5};
8
printf("arr[2] = %d\n", arr[2]);
9
printf("*(arr + 2) = %d\n", *(arr + 2));
10
printf("*(p + 2) = %d\n", *(p + 2));
11
printf("p[2] = %d\n", p[2]);
14
printf("&arr[2] = %p\n", &arr[2]);
15
printf("arr + 2 = %p\n", arr + 2);
16
printf("p + 2 = %p\n", p + 2);
解析:
这道题演示了数组名和指针的等价性。在C语言中,数组名在大多数情况下可以看作指向数组首元素的指针。arr[2]、*(arr + 2)、*(p + 2)、p[2]这四种写法是等价的,都访问数组的第三个元素。
运行结果:
TEXT
7
&arr[2] = 0x7ffd5a3b4c08
8
arr + 2 = 0x7ffd5a3b4c08
3. 进阶指针笔试题
现在我们来挑战一些更有难度的指针题目,这些题目往往在面试中出现。
3.1 题目三:指针的指针
C
10
printf("*p = %d\n", *p);
11
printf("**pp = %d\n", **pp);
14
printf("&a = %p\n", &a);
15
printf("p = %p\n", p);
16
printf("&p = %p\n", &p);
17
printf("pp = %p\n", pp);
18
printf("*pp = %p\n", *pp);
22
printf("执行 **pp = 200 后:\n");
23
printf("a = %d\n", a);
解析:
二级指针是指向指针的指针。pp存储的是p的地址,p存储的是a的地址。因此*pp得到的是p的值(即a的地址),**pp得到的是a的值。通过二级指针可以间接修改变量的值。
运行结果:
3.2 题目四:指针运算与类型大小
C
4
char char_arr[] = {'A', 'B', 'C', 'D'};
5
int int_arr[] = {10, 20, 30, 40};
7
char *char_ptr = char_arr;
8
int *int_ptr = int_arr;
11
printf("sizeof(char) = %zu\n", sizeof(char));
12
printf("sizeof(int) = %zu\n", sizeof(int));
15
printf("char_ptr = %p, 指向: %c\n", char_ptr, *char_ptr);
16
printf("char_ptr + 1 = %p, 指向: %c\n", char_ptr + 1, *(char_ptr + 1));
19
printf("int_ptr = %p, 指向: %d\n", int_ptr, *int_ptr);
20
printf("int_ptr + 1 = %p, 指向: %d\n", int_ptr + 1, *(int_ptr + 1));
23
printf("(char_ptr + 1) - char_ptr = %ld\n", (char_ptr + 1) - char_ptr);
24
printf("(int_ptr + 1) - int_ptr = %ld\n", (int_ptr + 1) - int_ptr);
解析:
这道题考察指针运算的类型相关性。指针加1操作的实际偏移量取决于指针所指向类型的大小。char类型占1字节,所以char指针加1移动1字节;int类型通常占4字节,所以int指针加1移动4字节。
运行结果:
TEXT
5
char_ptr = 0x7ffd5a3b4c10, 指向: A
6
char_ptr + 1 = 0x7ffd5a3b4c11, 指向: B
8
int_ptr = 0x7ffd5a3b4c20, 指向: 10
9
int_ptr + 1 = 0x7ffd5a3b4c24, 指向: 20
11
(char_ptr + 1) - char_ptr = 1
12
(int_ptr + 1) - int_ptr = 1
4. 复杂指针表达式解析
有些指针表达式看起来复杂,但只要掌握运算优先级和结合性,就能轻松理解。
4.1 题目五:指针与自增运算符
C
4
int arr[] = {1, 2, 3, 4, 5};
8
printf("初始: *p = %d\n", *p);
11
printf("*p++ = %d\n", *p++); // 先取*p,然后p++
12
printf("现在 *p = %d\n", *p);
15
printf("*(p++) = %d\n", *(p++)); // 等价于*p++
16
printf("现在 *p = %d\n", *p);
19
printf("(*p)++ = %d\n", (*p)++); // 先取*p,然后(*p)加1
20
printf("现在 arr[0] = %d, *p = %d\n", arr[0], *p);
22
p = arr; // 重置指针,但arr[0]已被修改为2
24
printf("++*p = %d\n", ++*p); // 先*p加1,然后取*p
25
printf("现在 arr[0] = %d\n", arr[0]);
解析:
这道题考察运算符优先级和结合性。*p++等价于*(p++),先取p指向的值,然后p自增。(*p)++先取p指向的值,然后该值自增。++*p等价于++(*p),先对p指向的值加1,然后取该值。
运行结果:
4.2 题目六:指针数组与数组指针
这是指针概念中最容易混淆的一对概念,通过题目来彻底理解它们。
C
4
int a = 1, b = 2, c = 3;
7
int *ptr_arr[3] = {&a, &b, &c};
10
int arr[3] = {10, 20, 30};
11
int (*arr_ptr)[3] = &arr;
15
for(int i = 0; i < 3; i++) {
16
printf("ptr_arr[%d] = %p, *ptr_arr[%d] = %d\n",
17
i, ptr_arr[i], i, *ptr_arr[i]);
20
printf("\n数组指针演示:\n");
21
printf("arr_ptr = %p\n", arr_ptr);
22
printf("*arr_ptr = %p (指向数组首元素)\n", *arr_ptr);
23
printf("(*arr_ptr)[0] = %d\n", (*arr_ptr)[0]);
24
printf("(*arr_ptr)[1] = %d\n", (*arr_ptr)[1]);
25
printf("(*arr_ptr)[2] = %d\n", (*arr_ptr)[2]);
27
printf("\n类型大小验证:\n");
28
printf("sizeof(ptr_arr) = %zu (指针数组大小)\n", sizeof(ptr_arr));
29
printf("sizeof(arr_ptr) = %zu (指针大小)\n", sizeof(arr_ptr));
30
printf("sizeof(*arr_ptr) = %zu (整个数组大小)\n", sizeof(*arr_ptr));
解析:
- 指针数组:
int *ptr_arr[3]是一个包含3个int指针的数组
- 数组指针:
int (*arr_ptr)[3]是一个指向包含3个int元素的数组的指针
关键区别在于运算符优先级:[]的优先级高于*,所以int *p[3]是指针数组,而int (*p)[3]是数组指针。
运行结果:
TEXT
3
ptr_arr[0] = 0x7ffd5a3b4c04, *ptr_arr[0] = 1
4
ptr_arr[1] = 0x7ffd5a3b4c08, *ptr_arr[1] = 2
5
ptr_arr[2] = 0x7ffd5a3b4c0c, *ptr_arr[2] = 3
8
arr_ptr = 0x7ffd5a3b4c10
9
*arr_ptr = 0x7ffd5a3b4c10 (指向数组首元素)
15
sizeof(ptr_arr) = 24 (指针数组大小)
16
sizeof(arr_ptr) = 8 (指针大小)
17
sizeof(*arr_ptr) = 12 (整个数组大小)
5. 函数指针实战题目
函数指针是C语言的高级特性,在回调函数、函数表等场景中广泛应用。
5.1 题目七:基础函数指针
C
4
int add(int a, int b) {
8
int multiply(int a, int b) {
14
int (*operation)(int, int);
20
printf("10 + 20 = %d\n", operation(10, 20));
24
printf("10 * 20 = %d\n", operation(10, 20));
27
int (*operations[2])(int, int) = {add, multiply};
28
printf("通过函数指针数组调用:\n");
29
printf("add(5, 6) = %d\n", operations[0](5, 6));
30
printf("multiply(5, 6) = %d\n", operations[1](5, 6));
解析:
函数指针的声明格式为:返回类型 (*指针变量名)(参数列表)。函数指针可以指向具有相同签名的不同函数,实现了运行时多态的效果。
运行结果:
5.2 题目八:回调函数实战
C
4
typedef void (*Callback)(int, int);
7
void process_numbers(int a, int b, Callback callback) {
8
printf("处理数字 %d 和 %d:\n", a, b);
13
void print_sum(int a, int b) {
14
printf(" 和: %d + %d = %d\n", a, b, a + b);
17
void print_product(int a, int b) {
18
printf(" 积: %d × %d = %d\n", a, b, a * b);
21
void print_difference(int a, int b) {
22
printf(" 差: %d - %d = %d\n", a, b, a - b);
26
printf("题目8: 回调函数演示\n");
28
process_numbers(15, 3, print_sum);
29
process_numbers(15, 3, print_product);
30
process_numbers(15, 3, print_difference);
解析:
回调函数是通过函数指针实现的经典设计模式。process_numbers函数接受一个函数指针参数,在适当的时候调用这个回调函数。这种模式提高了代码的灵活性和可复用性。
运行结果:
6. 内存布局与指针分析
理解内存布局对于深入掌握指针至关重要,下面通过题目分析各种数据结构的内存分布。
6.1 题目九:结构体与指针
C
2
# include <stddef.h> // 用于offsetof宏
11
Student s = {101, "Alice", 95.5};
14
printf("题目9: 结构体指针分析\n");
15
printf("结构体大小: %zu bytes\n", sizeof(Student));
17
printf(" id偏移: %zu\n", offsetof(Student, id));
18
printf(" name偏移: %zu\n", offsetof(Student, name));
19
printf(" score偏移: %zu\n", offsetof(Student, score));
21
printf("\n通过指针访问成员:\n");
22
printf("直接访问: s.id = %d\n", s.id);
23
printf("指针访问: sp->id = %d\n", sp->id);
24
printf("等价写法: (*sp).id = %d\n", (*sp).id);
27
printf("sp = %p\n", sp);
28
printf("sp + 1 = %p\n", sp + 1);
29
printf("偏移量: %td bytes\n", (char*)(sp + 1) - (char*)sp);
解析:
结构体指针使用->运算符访问成员,这等价于(*sp).成员名。结构体指针的运算以整个结构体大小为步长,sp + 1会跳过整个Student结构体的大小。
运行结果:
TEXT
15
sp + 1 = 0x7ffd5a3b4c0c
6.2 题目十:多级指针与内存访问
C
9
printf("题目10: 多级指针深度解析\n");
11
printf("value = %d\n", value);
12
printf("*p1 = %d\n", *p1);
13
printf("**p2 = %d\n", **p2);
14
printf("***p3 = %d\n", ***p3);
17
printf("&value = %p\n", &value);
18
printf("p1 = %p\n", p1);
19
printf("&p1 = %p\n", &p1);
20
printf("p2 = %p\n", p2);
21
printf("&p2 = %p\n", &p2);
22
printf("p3 = %p\n", p3);
24
printf("\n通过多级指针修改变量:\n");
26
printf("执行 ***p3 = 100 后:\n");
27
printf("value = %d\n", value);
30
printf("\n指针运算演示:\n");
31
int arr[3] = {10, 20, 30};
33
int **arr_pptr = &arr_ptr;
35
printf("arr[0] = %d\n", arr[0]);
36
printf("*(*arr_pptr) = %d\n", *(*arr_pptr));
37
printf("*(*arr_pptr + 1) = %d\n", *(*arr_pptr + 1));
38
printf("*(*arr_pptr + 2) = %d\n", *(*arr_pptr + 2));
解析:
多级指针提供了多层次的间接访问。p3指向p2,p2指向p1,p1指向value。通过***p3可以访问到最终的value变量。多级指针在动态多维数组、函数参数传递等场景中很有用。
运行结果:
TEXT
9
&value = 0x7ffd5a3b4bfc
7. 综合挑战题目
现在我们来解决一些综合性的指针题目,这些题目融合了多个指针概念。
7.1 题目十一:复杂指针声明解析
C
4
int a = 1, b = 2, c = 3;
5
int *p1 = &a, *p2 = &b, *p3 = &c;
8
int *arr1[3] = {p1, p2, p3}; // 指针数组
9
int (*arr2)[3] = (int(*)[3])&a; // 数组指针(强制类型转换,仅演示)
10
int **pp = &p1; // 指向指针的指针
12
printf("题目11: 复杂指针声明分析\n");
15
for(int i = 0; i < 3; i++) {
16
printf("arr1[%d] = %p, *arr1[%d] = %d\n",
17
i, arr1[i], i, *arr1[i]);
21
printf("arr1的类型: int *[3] (指针数组)\n");
22
printf("arr2的类型: int (*)[3] (数组指针)\n");
23
printf("pp的类型: int ** (二级指针)\n");
25
printf("\nsizeof验证:\n");
26
printf("sizeof(arr1) = %zu\n", sizeof(arr1)); // 3个指针的大小
27
printf("sizeof(arr2) = %zu\n", sizeof(arr2)); // 指针的大小
28
printf("sizeof(pp) = %zu\n", sizeof(pp)); // 指针的大小
31
printf("\n复杂表达式解析:\n");
32
printf("**pp = %d\n", **pp); // 通过二级指针访问a
33
printf("*arr1[0] = %d\n", *arr1[0]); // 通过指针数组访问a
34
printf("*(*arr1 + 1) = %d\n", *(*arr1 + 1)); // 等价于arr1[0][1],但类型不匹配
解析:
这道题综合了各种指针类型。关键要理解:
int *arr1[3]:数组,元素是指向int的指针
int (*arr2)[3]:指针,指向包含3个int的数组
int **pp:指针,指向指向int的指针
复杂的指针表达式需要根据运算符优先级和类型来解析。
7.2 题目十二:实战内存操作
C
4
void reverse_string(char *str) {
5
if (str == NULL) return;
8
char *end = str + strlen(str) - 1;
23
printf("题目12: 指针实战 - 字符串反转\n");
25
char text[] = "Hello, Pointer!";
26
printf("原始字符串: %s\n", text);
29
printf("反转后字符串: %s\n", text);
32
printf("\n子字符串查找演示:\n");
33
char *main_str = "This is a test string";
34
char *sub_str = "test";
36
char *found = strstr(main_str, sub_str);
38
printf("找到子串 '%s' 在位置: %ld\n", sub_str, found - main_str);
39
printf("从该位置开始的字符串: %s\n", found);
解析:
这个实战题目展示了指针在字符串操作中的强大能力。通过双指针技巧(start和end),我们可以高效地反转字符串。指针的算术运算(如found - main_str)可以计算位置偏移量。
运行结果:
TEXT
3
反转后字符串: !retnioP ,olleH
7
从该位置开始的字符串: test string
8. 常见指针错误与避坑指南
在指针使用过程中,开发者经常会遇到各种错误。了解这些常见错误及其解决方法至关重要。
8.1 空指针解引用
C
7
// printf("%d\n", *p); // 会导致段错误
13
printf("指针为空,不能解引用\n");
避坑建议:
- 在解引用指针前总是检查是否为NULL
- 函数返回指针时,文档化说明是否可能返回NULL
- 使用断言在调试阶段捕获空指针问题
8.2 野指针问题
C
8
// printf("%d\n", *p); // 未定义行为
11
p = (int*)malloc(sizeof(int));
14
printf("分配的内存: %d\n", *p);
20
// printf("%d\n", *p); // 使用悬空指针
避坑建议:
- 指针声明后立即初始化
- 释放内存后将指针设为NULL
- 避免返回局部变量的地址
8.3 数组越界访问
C
4
int arr[3] = {1, 2, 3};
8
// printf("%d\n", p[5]); // 未定义行为
12
if (index >= 0 && index < 3) {
13
printf("arr[%d] = %d\n", index, p[index]);
15
printf("索引 %d 越界\n", index);
避坑建议:
- 始终检查数组索引边界
- 使用sizeof计算数组元素个数
- 避免硬编码数组大小
9. 指针最佳实践总结
通过以上题目的练习,我们总结出一些指针使用的最佳实践:
9.1 代码规范建议
- 命名规范:指针变量名以p、ptr结尾,如
int *data_ptr
- 初始化原则:声明指针时立即初始化,或设为NULL
- 空指针检查:在解引用前检查指针是否为NULL
- 内存管理:谁分配谁释放,配对的malloc/free要写在一起
9.2 调试技巧
- 打印指针值:使用
%p格式打印指针地址
- 使用调试器:在IDE中查看指针指向的内存内容
- 添加断言:使用assert检查指针有效性
- 内存检测工具:使用valgrind等工具检测内存问题
9.3 性能考虑
- 减少指针间接访问:过多的指针解引用会影响性能
- 局部性原理:连续内存访问比随机访问更高效
- 避免不必要的指针运算:复杂的指针表达式可能影响可读性和性能
指针是C语言的精髓所在,掌握指针不仅有助于通过技术面试,更能写出高效、灵活的代码。建议读者亲自动手运行这些示例,并尝试修改参数来观察不同结果,这样才能真正理解指针的工作原理。