深入理解C语言指针:从一级指针到函数指针

张开发
2026/4/17 22:40:13 15 分钟阅读

分享文章

深入理解C语言指针:从一级指针到函数指针
前言指针是C语言的灵魂也是让无数初学者“从入门到放弃”的罪魁祸首。但说实话指针本身并不复杂——复杂的是它背后所代表的内存地址这一抽象概念。一旦你理解了“指针就是一个存放地址的变量”你会发现指针不仅不恐怖反而是C语言最优雅的设计之一。今天我们从最基础的一级指针开始一路走到函数指针和复杂声明解析帮你彻底拿下指针。一、指针的本质地址的容器cint a 42;int *p a;在内存中变量 a 占据4个字节假设32位系统这4个字节的起始地址就是 a。而指针变量 p 存储的就是这个地址。关键理解· p 存的是地址如 0x7ffd1234· *p 是“解引用”意思是“去 p 存储的地址那里取出该地址上的值”画个图就清晰了内存地址: 0x7ffd1230 0x7ffd1234 0x7ffd1238------------------------------| ? | 42 | ? |------------------------------↑a 在这里p 0x7ffd1234p自己也有自己的地址二、一级指针最基础的指针常见用法c// 1. 修改外部变量函数内修改函数外的值void swap(int *a, int *b) {int tmp *a;*a *b;*b tmp;}// 2. 返回多个结果int divide(int a, int b, int *remainder) {if (b 0) return 0;*remainder a % b;return a / b;}// 3. 避免大结构体拷贝传指针比传值高效void process_struct(struct BigData *data) {// 只传8字节的指针而非整个结构体}空指针与野指针类型 定义 后果空指针 int *p NULL; 解引用会段错误至少能发现野指针 int *p; 未初始化 解引用可能修改随机内存极难调试建议声明指针时立即初始化无法确定值时初始化为 NULL。三、二级指针指向指针的指针二级指针常用于需要在函数内修改指针本身的场景。c// 场景动态分配内存后返回给调用者void allocate_memory(int **ptr, size_t size) {*ptr (int*)malloc(size * sizeof(int));// 调用者传的是 p所以 *ptr 就是调用者的 p}int main() {int *p NULL;allocate_memory(p, 10); // 传入指针的地址p[0] 100;free(p);return 0;}为什么要用二级指针 C语言是值传递。在函数内修改 p 本身而非 *p必须传递 p。四、指针与数组暧昧不清的关系数组名就是指针吗不是。数组名在大多数表达式中会被隐式转换为指向首元素的指针但有例外cint arr[5] {1,2,3,4,5};int *p arr; // arr 转换为 arr[0]printf(%zu\n, sizeof(arr)); // 20 5*4数组名没转换printf(%zu\n, sizeof(p)); // 864位系统指针的大小指针运算cint *p arr;p; // 移动 sizeof(int) 4 字节*(p2); // 等价于 arr[3]公式arr[i] 完全等价于 *(arr i)数组作为函数参数秘密就是指针cvoid func(int arr[]) { // 编译器自动改写为 int *arrprintf(%zu, sizeof(arr)); // 输出 8不是20}记住数组形参本质上就是指针所以无法在函数内获取数组长度需要额外传入 size。五、指针数组 vs 数组指针这是面试常考题区分关键在于优先级[] 优先级高于 *。写法 含义 记忆技巧int *p[5] 指针数组5个元素每个都是 int* p 先和 [] 结合int (*p)[5] 数组指针指向一个长度为5的int数组 (*p) 表示 p 是指针c// 指针数组常用于字符串数组char *strs[] {hello, world, c};// 数组指针常用于二维数组int arr[3][5];int (*p)[5] arr; // p 指向第一行六、函数指针把函数当作数据函数也有地址可以存储在指针中实现回调、策略模式等。c// 声明一个函数指针指向 返回int、参数(int,int) 的函数int (*p)(int, int);// 赋值int add(int a, int b) { return a b; }p add;// 调用int result p(3, 5); // 等价于 add(3,5)实用场景回调函数c// 通用的排序函数让用户决定比较规则void sort(int *arr, int size, int (*cmp)(int, int)) {for (int i 0; i size; i) {for (int j i1; j size; j) {if (cmp(arr[i], arr[j]) 0) {int tmp arr[i];arr[i] arr[j];arr[j] tmp;}}}}int asc(int a, int b) { return a - b; }int desc(int a, int b) { return b - a; }// 使用sort(myarr, 10, asc); // 升序sort(myarr, 10, desc); // 降序函数指针的复杂声明读懂即可c// 信号处理函数void (*signal(int sig, void (*handler)(int)))(int)// 用 typedef 简化typedef void (*sighandler_t)(int);sighandler_t signal(int sig, sighandler_t handler);七、const 与指针四种组合这是初学者最容易混淆的地方记住一条规则const 修饰谁谁就不能改。写法 含义 能否改指向 能否改值const int *p 指向常量的指针 ✅ 可以 ❌ 不行int const *p 同上 ✅ 可以 ❌ 不行int * const p 常量指针 ❌ 不行 ✅ 可以const int * const p 两者都不可改 ❌ 不行 ❌ 不行记忆口诀const 在 * 左边修饰值在 * 右边修饰指针本身。八、常见陷阱与调试技巧1. 解引用未初始化的指针cint *p;*p 10; // 错误p 指向哪里2. 返回局部变量的地址cint* bug() {int a 10;return a; // 错误函数返回后 a 被销毁}3. 释放后继续使用cfree(p);*p 10; // 未定义行为p NULL; // 释放后立即置空是个好习惯4. 调试技巧· 打印指针值printf(p %p\n, p);· 使用 Valgrind 检测非法内存访问· 地址消毒剂gcc -fsanitizeaddress -g结语指针确实需要时间去消化但一旦掌握了它你就真正拥有了C语言。记住三个核心概念1. 指针就是地址2. 解引用就是去那个地址取值3. 传指针是为了让函数能够修改外部变量剩下的无非是反复练习、反复调试。下一篇文章我们将进入 C语言内存布局看看程序在运行时的代码、数据、堆、栈是如何组织的。下一篇预告《C语言内存全景图从代码到运行的完整旅程》---有任何指针方面的问题欢迎在评论区留言讨论

更多文章