17、指针(三):指针数组与数组指针、多级指针、void指针
指针这东西,学的时候觉得绕,用起来是真爽。今天咱们把指针数组、数组指针、多级指针和void指针这几个“硬骨头”啃下来。我当年刚接触C时,也被这些概念绕晕过,后来在项目里调试过一个野指针导致的死机,才真正把它们刻在脑子里。
17.1 指针数组 vs 数组指针
这两个名字长得像双胞胎,但性格完全不同。说白了:
- 指针数组:是一个数组,里面每个元素都是指针。
- 数组指针:是一个指针,它指向一个数组。
怎么记?看最后两个字。指针数组最后是“数组”,所以它是数组;数组指针最后是“指针”,所以它是指针。
17.1.1 指针数组
我在项目中常用指针数组来处理字符串列表。比如命令行参数解析,用指针数组存各个选项的字符串,遍历起来特别方便。
#include <stdio.h>
int main() {
// 指针数组:每个元素是 char*
char *fruits[3] = {"apple", "banana", "cherry"};
for (int i = 0; i < 3; i++) {
printf("%s\n", fruits[i]); // fruits[i] 就是字符串首地址
}
return 0;
}
17.1.2 数组指针
数组指针指向的是整个数组,不是数组的第一个元素。你想想看,如果有一个二维数组,用数组指针来遍历,代码会清晰很多。
#include <stdio.h>
int main() {
int arr[2][3] = {{1,2,3}, {4,5,6}};
int (*p)[3]; // 数组指针:指向含有3个int的数组
p = arr; // p 指向第一行(整个一维数组)
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", p[i][j]); // 像二维数组一样用
}
printf("\n");
}
return 0;
}
int *p[3] 和 int (*p)[3] 时,括号位置搞反了。前者是指针数组,后者是数组指针。少一个括号,意思天差地别。
17.2 多级指针(指向指针的指针)
多级指针,说白了就是指针的指针。为什么需要它?最常见的场景是:你想在函数里修改传入的指针本身(比如分配内存),就得传指针的地址,也就是二级指针。
17.2.1 二级指针的基本用法
#include <stdio.h>
#include <stdlib.h>
void allocate_memory(int **p, int size) {
*p = (int*)malloc(size * sizeof(int));
if (*p == NULL) {
printf("内存分配失败\n");
exit(1);
}
// 初始化
for (int i = 0; i < size; i++) {
(*p)[i] = i * 10;
}
}
int main() {
int *arr = NULL;
allocate_memory(&arr, 5); // 传指针的地址
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
free(arr);
return 0;
}
嗯,这里要注意:如果只传一级指针,函数内部修改的是指针的副本,外面的指针还是NULL。我早期写链表时就在这里栽过跟头。
17.2.2 三级指针?
三级指针理论上可以用,但实际项目中很少见。我见过最深的也就是二级指针。如果你发现需要三级指针,多半是设计上可以简化。比如用结构体封装一下,比裸指针嵌套更安全。
17.3 void指针
void指针(void*)是C语言里的“万能容器”。它可以指向任何类型的数据,但你不能直接解引用它——因为编译器不知道它指向什么类型。
17.3.1 基本用法
#include <stdio.h>
int main() {
int a = 100;
double b = 3.14;
char c = 'X';
void *ptr;
ptr = &a;
printf("int: %d\n", *(int*)ptr); // 必须强制转换
ptr = &b;
printf("double: %.2f\n", *(double*)ptr);
ptr = &c;
printf("char: %c\n", *(char*)ptr);
return 0;
}
void* 数据,这样一套代码可以支持int、float、甚至结构体。但记得在释放时,要清楚实际类型,否则内存泄漏。
17.3.2 void指针与函数指针
标准库的 qsort 就用 void* 实现通用排序。你只需要提供一个比较函数,qsort内部通过void指针操作数据。
#include <stdio.h>
#include <stdlib.h>
int compare(const void *a, const void *b) {
return (*(int*)a - *(int*)b);
}
int main() {
int arr[] = {5, 2, 9, 1, 5, 6};
int n = sizeof(arr)/sizeof(arr[0]);
qsort(arr, n, sizeof(int), compare);
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
ptr++ 是不允许的,因为编译器不知道步长。必须先转换成具体类型再运算。
17.4 知识体系图
下面这张图帮你理清今天这几个概念的关系。我画图时习惯把容易混淆的放在一起对比,这样记忆更深刻。
17.5 避坑指南与总结
最后,我把自己这些年踩过的坑总结一下:
- 指针数组初始化:如果声明
char *arr[3],记得给每个元素赋初值,否则是野指针。我曾经在嵌入式项目里忘了初始化,结果指针乱指,系统直接挂掉。 - 数组指针的括号:
int (*p)[3]的括号不能省。省了就成了指针数组。我建议你写的时候先写括号,再写星号,最后写变量名。 - 多级指针的传参:如果函数要修改指针本身(比如分配内存),必须传二级指针。一级指针只能修改指针指向的内容,不能修改指针的地址。
- void指针的转换:解引用void指针前,一定要强制转换成具体类型。而且转换后的类型要和实际存储的类型一致,否则数据解释会出错。
好了,这一章的内容就到这里。指针这部分,多写代码、多调试,慢慢就熟练了。下一章咱们继续聊指针和函数的关系,到时候见。