C语言基础回顾三:指针深度解析 一、指针基础什么是指针指针是 C 语言最核心的概念也是区分 C 与其他高级语言的关键特性。指针本质上是一个变量其存储的值是内存地址。理解指针的关键在于区分三个层次指针本身的地址p、指针存储的地址p和指针指向的值*p。这三者位于不同的内存层级混淆它们是绝大多数指针错误的根源。当你声明int *p;时p是一个指向int类型的指针变量。a取变量a的地址*p解引用指针p访问其指向的内存。指针的类型决定了指针运算时的步长——int *p执行p 1时地址增加的是sizeof(int)而不是 1。野指针与空指针指针声明后如果未初始化它指向的地址是随机的这种指针称为野指针。对野指针解引用会写入未知内存可能导致程序崩溃、数据损坏甚至安全漏洞。安全的做法是声明时立即初始化如果暂时没有明确的指向目标应赋值为NULL空指针。空指针解引用虽然也会导致运行时崩溃段错误 / Segmentation Fault但至少是可预测的、可调试的。相比之下野指针的行为完全不可预期可能在某些运行环境下恰好工作在另一些环境下崩溃给调试带来极大困难。#include stdio.h int main() { int a 100; int *p a; // 正确声明时初始化 printf(a %d\n, a); // 100 printf(*p %d\n, *p); // 100 printf(p %p\n, (void*)p); // a 的地址 int *q NULL; // *q 10; // 运行时崩溃但至少可预测 return 0; }二、指针与数组数组名的退化数组名在表达式中通常会退化为指向首元素的指针因此arr[i]和*(arr i)完全等价。这个等价关系还引出了一个有趣的结论arr[i]等价于i[arr]因为加法满足交换律*(arr i)就是*(i arr)。虽然后者在语法上合法但可读性极差不建议使用。但数组名和指针有一个关键区别数组名不是变量不能对其赋值。arr是非法的因为数组名代表的是一个常量地址。而指针p是合法的它使p指向下一个同类型元素。指针运算的自动缩放指针加减整数时偏移量不是字节数而是元素个数 × sizeof(元素类型)。例如int *p arr; p 1实际上地址增加了 4 字节假设int为 4 字节。这种自动缩放虽然方便但也容易让人忽视底层细节在需要精确控制字节级操作时产生困惑。#include stdio.h int main() { int arr[5] {10, 20, 30, 40, 50}; int *p arr; // 三种完全等价的访问方式 printf(arr[2] %d\n, arr[2]); printf(*(arr2) %d\n, *(arr 2)); printf(p[2] %d\n, p[2]); // 指针运算自动缩放 printf(p %p\n, (void*)p); printf(p 1 %p\n, (void*)(p 1)); // 差值是 sizeof(int) 4 字节 return 0; }三、指针与函数通过指针实现引用传递虽然 C 语言函数参数是值传递但通过传递指针可以实现间接修改外部变量。这是 C 语言中实现交换两个变量值的标准做法。初学者常犯的错误是在swap内部交换指针本身a b而非指针指向的值*a *b。前者修改的只是形参的拷贝对外部变量没有任何影响。正确的做法是使用临时变量保存*a的值然后交换*a和*b。#include stdio.h // 正确交换指针指向的值 void swap(int *a, int *b) { int temp *a; *a *b; *b temp; } int main() { int x 5, y 10; printf(交换前: x%d, y%d\n, x, y); swap(x, y); printf(交换后: x%d, y%d\n, x, y); return 0; }四、指针数组与数组指针两者的区别int *p[5]和int (*p)[5]看起来很像但意义完全不同。运算符优先级中[]高于*因此int *p[5]是一个数组包含 5 个int*类型的元素即指针数组。而int (*p)[5]由于括号改变了优先级是一个指针指向一个包含 5 个int的数组即数组指针。指针数组最常用的场景是存储字符串数组如命令行参数char *argv[]。数组指针则常用于二维数组传参或需要以行视角操作二维数据的场景。#include stdio.h int main() { // 指针数组元素是指针 char *weekdays[] {Mon, Tue, Wed}; for (int i 0; i 3; i) { printf(%s\n, weekdays[i]); } // 数组指针指向数组的指针 int arr[3][4] {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}}; int (*p)[4] arr; printf(p[1][2] %d\n, p[1][2]); // 7 return 0; }五、二级指针与函数指针二级指针二级指针int **pp是指向指针的指针常用于以下场景在函数内部修改调用者的指针变量如动态分配内存后让外部指针指向新分配的内存或表示指针数组的地址。当你需要在函数内为一个指针分配内存并让它在函数返回后仍然有效时必须传递该指针的地址也就是二级指针。如果只传递一级指针函数内部修改的只是形参拷贝外部指针仍然为NULL。函数指针函数指针存储函数的入口地址可用于实现回调函数、状态机或动态函数分发。函数指针的声明语法较为复杂typedef可以大大简化其使用。#include stdio.h int add(int a, int b) { return a b; } int sub(int a, int b) { return a - b; } int main() { // 函数指针 int (*op)(int, int) add; printf(3 5 %d\n, op(3, 5)); // 8 op sub; printf(3 - 5 %d\n, op(3, 5)); // -2 return 0; }六、指针常见错误总结指针相关的错误是C语言中最危险也最常见的bug来源内存泄漏malloc分配的内存没有free程序运行越久占用内存越大。在长时间运行的程序中这会导致系统内存耗尽。悬挂指针Dangling Pointerfree(p)后继续使用p。应在free后立即将指针置为NULL这样后续如果误用至少会触发可预测的段错误而不是随机行为。越界指针运算p指向数组第5个元素却访问*(p 100)。这种错误在编译期无法发现运行时可能潜伏很久才暴露。返回局部变量地址如前所述函数栈帧销毁后地址失效返回的指针成为野指针。内存重复释放对同一指针调用两次free可能导致堆管理器状态损坏引发难以调试的崩溃。七、小结指针赋予C语言直接操作内存的能力也带来了极大的风险。安全使用指针的黄金法则初始化时赋值解引用前检查NULL释放后置NULL绝不返回局部变量地址避免复杂表达式中混用多个副作用操作。掌握指针意味着真正理解了C语言接近硬件的设计哲学。