指针是 C 语言的灵魂,也是初学者最大的拦路虎。理解了指针,你就掌握了 C 语言直接操作内存的能力——这是它高效、强大的根源。本文从内存模型讲起,覆盖指针的本质、运算、函数指针与动态内存管理,并指出最常见的几个陷阱。
一、指针的本质:存储地址的变量
内存是一块连续的字节空间,每个字节有一个地址编号。指针变量本身也是一个变量,只不过它存储的是另一个变量的地址,而非数据本身。
#include <stdio.h>
int main(void) {
int a = 42; // 在栈上分配一个 int,存 42
int *p = &a; // p 是指针,存 a 的地址
printf("a 的值 = %d\n", a); // 42
printf("a 的地址 = %p\n", &a); // 0x7ffee...
printf("p 的值 = %p\n", p); // 同上
printf("p 指向的值 = %d\n", *p); // 42 —— 解引用
*p = 100; // 通过指针修改 a
printf("a 现在 = %d\n", a); // 100
return 0;
}
一句话:变量存数据,指针存地址。
&取地址,*解引用(取地址里的内容)。
二、指针类型与步长
指针有类型(int*、char*、double*),类型决定解引用时读几个字节,也决定指针加减时的步长。
int arr[] = {10, 20, 30};
int *p = arr; // 数组名即首元素地址
printf("%d\n", *p); // 10
printf("%d\n", *(p+1)); // 20 —— p+1 跨过 1 个 int(4 字节)
printf("%d\n", *(p+2)); // 30
// 不同类型步长不同
char *cp = (char*)arr;
printf("%d\n", *(cp+4)); // 跨过 4 个 char 才到 arr[1]
p+1 实际加的是 sizeof(指针所指类型) 字节,而不是 1。这就是指针"带类型"的意义。
三、指针与数组:一脉相承
数组名在大多数表达式中会"退化"为指向首元素的指针,所以指针和数组访问可以互换。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
// 这四种写法等价
printf("%d", arr[2]);
printf("%d", *(arr + 2));
printf("%d", p[2]);
printf("%d", *(p + 2));
四、函数指针:回调的基石
函数指针指向函数,可以动态调用不同函数,是回调、策略模式、事件驱动的底层支撑。
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
// 函数指针类型:int (*)(int, int)
int calc(int (*op)(int, int), int a, int b) {
return op(a, b); // 通过指针调用函数
}
int main(void) {
printf("%d\n", calc(add, 5, 3)); // 8
printf("%d\n", calc(sub, 5, 3)); // 2
return 0;
}
五、动态内存管理:堆上的指针
栈内存随函数返回自动释放,生命周期长的数据需用 malloc 在堆上分配,用完必须 free,否则内存泄漏。
#include <stdlib.h>
#include <stdio.h>
int main(void) {
int n = 5;
int *arr = malloc(n * sizeof(int)); // 申请 5 个 int
if (arr == NULL) return 1; // 必须检查返回值
for (int i = 0; i < n; i++) arr[i] = i * 10;
for (int i = 0; i < n; i++) printf("%d ", arr[i]); // 0 10 20 30 40
free(arr); // 释放内存
arr = NULL; // 置空,避免悬空指针
return 0;
}
六、五大常见指针陷阱
- 野指针:未初始化就解引用。声明时立即赋值或置 NULL。
- 悬空指针:free 后仍使用。free 后立即置 NULL。
- 内存泄漏:malloc 了没 free。配对使用,借助工具(valgrind)检查。
- 越界访问:指针加减超出数组边界。务必校验索引范围。
- 返回栈地址:函数内返回局部变量地址。改用 static、堆分配或参数传出。
// 反例:返回栈上局部变量地址 —— 经典错误
int* bad() {
int x = 10;
return &x; // 函数返回后栈帧销毁,地址失效
}
// 正例:通过参数传出或使用堆
void good(int *out) { *out = 10; }
指针的价值在于"直接、高效"——它让你以字节级粒度操作内存,这是 C 语言能写操作系统、驱动、嵌入式底层的根本原因。掌握指针的关键不是背语法,而是建立清晰的内存模型:在脑中画出每个变量在内存中的位置、地址如何流转。多画图、多调试,指针终会成为你手中的利器。