第十三章 指针(二):指针与字符串、指针数组、多级指针、void指针
指针这东西,学完基础用法后,真正的挑战才刚刚开始。我个人觉得,指针的进阶用法才是C语言的灵魂所在。今天咱们聊聊指针与字符串的纠缠、指针数组的妙用、多级指针的层层递进,还有那个神秘的void指针。
13.1 指针与字符串:字符指针的真相
字符串在C语言里,本质上就是字符数组。但用指针操作字符串,效率会高很多。我刚开始学的时候,总觉得字符数组和字符指针差不多,后来踩了坑才明白——它们有本质区别。
// 方式一:字符数组
char str1[] = "Hello";
// 方式二:字符指针
char *str2 = "Hello";
这两种写法,看起来都是存了个"Hello"。但实际内存布局完全不同。
关键区别:
str1[]是在栈上分配了6个字节(含'\0'),内容可以修改str2指向的是字符串常量区,内容不能修改
我曾经在项目中写过这样的代码:char *p = "hello"; p[0] = 'H'; 结果程序直接崩溃。后来排查了半天,才发现是试图修改字符串常量。嗯,这个教训挺深刻的。
为什么不能修改?因为字符串常量存储在只读数据段。你想想看,如果多个指针指向同一个字符串常量,其中一个改了,其他的全受影响——这显然不合理。
13.2 字符串操作:指针的拿手好戏
用指针遍历字符串,代码可以写得很优雅。我习惯用这种方式:
char *p = str;
while (*p) {
// 处理 *p
p++;
}
这个循环,说白了就是:只要当前字符不是'\0',就继续。简洁、高效、不易出错。
写一个自己的字符串拷贝函数,你就能体会到指针的威力:
void my_strcpy(char *dest, const char *src) {
while (*dest++ = *src++)
;
}
一行代码搞定。赋值表达式的值就是被赋的字符,当遇到'\0'时,值为0,循环结束。我个人觉得,这种写法是C语言指针魅力的集中体现。
小技巧:参数用const char *src,表明源字符串不会被修改。这是好习惯,能避免很多意外。
13.3 指针数组:数组里存的是指针
指针数组,就是数组的每个元素都是一个指针。最常见的应用场景是处理多个字符串。
const char *weekdays[] = {
"Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", "Sunday"
};
for (int i = 0; i < 7; i++) {
printf("%s\n", weekdays[i]);
}
你看,每个元素都是指向字符串常量的指针。这样做的好处是:
- 节省内存:每个字符串不必连续存储
- 方便交换:交换两个指针比交换整个字符串快得多
- 灵活管理:可以动态改变指向
我在项目中遇到过需要按字母序排序星期几的情况。如果用二维字符数组,交换两个字符串得用strcpy,效率低。用指针数组,直接交换指针就行——快得不是一星半点。
13.4 多级指针:指针的指针
多级指针,说白了就是指向指针的指针。二级指针最常见,三级以上就很少见了。
int value = 42;
int *ptr = &value;
int **pptr = &ptr;
内存关系是这样的:
pptr指向ptrptr指向valuevalue的值是42
二级指针最常见的用途是:在函数内部修改指针本身的值。你想想看,如果要在函数里给指针分配内存,只传一级指针是不够的。
void allocate_memory(int **p, int size) {
*p = (int *)malloc(size * sizeof(int));
if (*p == NULL) {
// 处理错误
}
}
int main() {
int *arr = NULL;
allocate_memory(&arr, 10);
// 现在 arr 指向了分配的内存
free(arr);
return 0;
}
为什么必须传二级指针?因为C语言是值传递。如果传一级指针,函数内部修改的是指针的副本,外面的指针还是NULL。传二级指针,才能修改外面那个指针的值。
我曾经在写链表操作时,忘了用二级指针来修改头指针,结果链表永远只有第一个节点。排查了半小时才发现问题——嗯,这种错误犯过一次就不会再犯了。
13.5 void指针:万能指针
void * 是一种特殊指针,它可以指向任何类型的数据。说白了,就是"无类型指针"。
int a = 10;
double b = 3.14;
char c = 'X';
void *vp;
vp = &a; // 可以
vp = &b; // 也可以
vp = &c; // 都可以
但void指针有两个限制:
- 不能直接解引用:
*vp是错的,因为不知道类型 - 不能做指针运算:
vp++是错的,因为步长未知
使用前必须强制类型转换:
int *ip = (int *)vp;
printf("%d\n", *ip);
void指针最常见的用途是实现通用函数。qsort、memset、memcpy 这些库函数,都用void指针来接受任意类型的数据。
void *memcpy(void *dest, const void *src, size_t n);
这个函数不管你要拷贝的是int数组、double数组还是结构体数组,都能处理。因为void指针不在乎类型,它只关心字节数。
我的习惯:写通用接口时用void指针,但内部使用前一定做类型转换。不要偷懒,类型安全很重要。
13.6 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以对照着梳理一下思路。
13.7 避坑指南
指针用多了,踩坑是难免的。我把自己踩过的坑整理一下,你遇到了可以少走弯路。
| 常见错误 | 错误示例 | 正确做法 |
|---|---|---|
| 修改字符串常量 | char *p = "abc"; p[0]='x'; |
用字符数组 char p[] = "abc"; |
| 忘记二级指针 | void func(int *p) { p=malloc(...); } |
传指针的地址 void func(int **p) |
| void指针直接运算 | void *p; p++; |
先转成具体类型再运算 |
| 指针数组越界 | char *arr[3]; arr[3] = ...; |
注意数组下标从0开始 |
核心总结:指针就是地址,类型决定了怎么解释这个地址里的内容。多级指针就是地址的地址,void指针就是放弃类型信息。理解了这个,指针就不再神秘。
好了,这一章的内容就到这儿。指针的进阶用法需要多练,光看是学不会的。打开你的编译器,把上面的代码跑一遍,改一改,看看会发生什么——这才是学习指针最好的方式。