前面十四篇文章,我们盖好了地基:变量存数据,函数拆模块,数组管批量。但你有没有觉得还缺一块关键拼图——为什么我们可以在函数里修改数组,却改不了普通变量?为什么 scanf 要用 & 取地址?为什么说 C 语言“贴近硬件”?
答案就在指针。
指针是 C 语言最强大、也最让初学者头疼的特性。但不要怕——你已经有变量、内存、作用域的扎实基础,理解指针就只剩一层窗户纸。今天,我们就来捅破它。
一、为什么需要指针?
先看三个你或许已经遇到的困惑:
困惑一:为什么 scanf 要加 &?
int age;
scanf("%d", &age); // 这个 & 是什么?
困惑二:为什么在函数里修改数组,外面也能看到变化?
void fill_zero(int arr[], int n) {
for (int i = 0; i < n; i++) arr[i] = 0; // 外面的数组也变了!
}
困惑三:为什么我们写的 swap 函数没用?
void swap(int a, int b) {
int temp = a; a = b; b = temp; // 外面纹丝不动
}
这三个困惑都指向同一个核心概念:如何直接访问和操作内存中的数据。普通变量通过名字访问,但有时候我们需要通过地址来访问——这就引出了指针。
二、指针是什么?
简单说:指针就是一个变量,但它里面存的不是普通数值,而是另一个变量的内存地址。
int a = 10;
int *p = &a; // p 里存的是 a 的地址
这里:
a是一个普通的int变量,里面存的是10。&a是取地址运算符,得到a在内存里的地址(比如0x7ffd1c)。p是一个指针变量,它里面存的就是那个地址。*p是间接访问运算符(解引用),沿着p存的地址找到a,读写它的值。
关系图:
变量 a: [10] <-- 地址 0x100
^
|
指针 p: [0x100] <-- p 自己的地址是 0x200
所以 *p 就是 a 本人。你写 *p = 20;,a 就变成了 20。
三、声明指针变量
声明指针时,在类型后面加一个 *:
int *p; // p 是指向 int 的指针
char *ch; // ch 是指向 char 的指针
double *dp; // dp 是指向 double 的指针
声明时可以初始化:
int a = 5;
int *p = &a; // p 指向 a
也可以分开写:
int *p;
p = &a; // 让 p 指向 a
注意:int *p, q; 中,只有 p 是指针,q 是普通 int。如果想声明两个指针,写 int *p, *q;。这是 C 语法的一个小陷阱。
四、& 取地址与 * 解引用
这两个运算符是指针的基本功,必须熟练掌握。
1. & 取地址
把任意变量的地址取出来,返回指向该类型的指针。
int x = 42;
printf("%p\n", (void*)&x); // 打印 x 的地址
%p 专门用来打印地址(指针值),需要强制转换为 void*。
2. * 解引用(间接访问)
通过指针访问它所指向的变量。
int a = 10;
int *p = &a;
printf("%d\n", *p); // 输出 10,等价于 printf("%d\n", a);
*p = 20; // 把 a 改成 20
printf("%d\n", a); // 输出 20
*p 可以出现在赋值号的左边(左值),用来修改指向的值。
五、指针作为函数参数:实现真正的“交换”
还记得第十二篇那个失败的 swap 吗?现在用指针来拯救它。
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a; // 取 a 指向的值
*a = *b; // 把 b 指向的值赋给 a 指向的位置
*b = temp;
}
int main(void) {
int x = 5, y = 10;
printf("交换前: x=%d, y=%d\n", x, y);
swap(&x, &y); // 传 x 和 y 的地址
printf("交换后: x=%d, y=%d\n", x, y);
return 0;
}
输出:
交换前: x=5, y=10
交换后: x=10, y=5
成功!因为 swap 接收的是 x 和 y 的地址,它通过 *a 和 *b 直接修改了 main 里那两个变量的内存。这就是指针的核心威力:让函数有能力修改外部的变量。
六、指针与数组:一对亲兄弟
1. 数组名就是首元素的地址
int arr[5] = {10, 20, 30, 40, 50};
printf("%p\n", (void*)arr); // 数组名直接当指针用
printf("%p\n", (void*)&arr[0]); // 和上面一样
数组名 arr 在表达式中会被自动转换成指向首元素的指针。所以:
int *p = arr; // p 指向 arr[0]
现在你可以用指针来访问数组元素:
printf("%d\n", *p); // arr[0] = 10
printf("%d\n", *(p+1)); // arr[1] = 20
printf("%d\n", *(p+2)); // arr[2] = 30
p + 1 不是地址值加 1 个字节,而是加 1 * sizeof(int) 个字节——也就是跳过整个元素,指向下一个 int。这称为指针算术。
2. 指针算术
指针加整数 n,地址值增加 n * sizeof(指向的类型)。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // 指向 arr[0]
p = p + 1; // 指向 arr[1]
p++; // 指向 arr[2]
p += 2; // 指向 arr[4]
也可以用下标访问指针:
int *p = arr;
printf("%d\n", p[2]); // 等价于 arr[2],输出 30
为什么数组和指针这么亲?因为 arr[i] 在底层被编译器翻译成 *(arr + i)。这两者完全等价。甚至你可以写 i[arr],会被翻译成 *(i + arr),也是一样的(但千万别真这么写)。
3. 用指针遍历数组
#include <stdio.h>
int main(void) {
int arr[] = {2, 4, 6, 8, 10};
int *p;
for (p = arr; p < arr + 5; p++) {
printf("%d ", *p);
}
printf("\n");
return 0;
}
p < arr + 5 判断指针是否越过了数组末尾(arr + 5 指向最后一个元素之后的位置,不能解引用,但可以做比较)。
七、数组作为函数参数的本质
现在我们可以解释那个困惑了:为什么函数里修改数组,外面也会变?
void modify(int arr[], int n) {
arr[0] = 999;
}
实际上,编译器看到 int arr[] 时,会把它当成 int *arr。函数调用时,传进来的是数组首地址的副本,而不是整个数组的副本。通过这个地址,函数可以直接修改原数组。
modify(my_array, 5); // 传的是 &my_array[0]
所以 arr[0] = 999; 等价于 *(arr + 0) = 999;,直接写到了原数组的内存上。这就是为什么数组“按引用传递”的真相——它传的是地址值。
八、void* 指针初识
有一种特殊的指针类型:void*(无类型指针)。它可以指向任何类型的数据,但不能直接解引用,因为编译器不知道它指向的数据类型大小。
int a = 10;
void *vp = &a; // 可以指向 int
// printf("%d\n", *vp); // 错误!不能解引用 void*
printf("%d\n", *(int*)vp); // 先强制转换回 int*,再解引用
void* 常用于通用内存操作函数,比如 malloc、memcpy,后面讲动态内存时会遇到。
九、常见错误与陷阱
1. 使用未初始化的指针
int *p;
*p = 10; // 危险!p 指向哪里?可能是随机地址,导致崩溃
指针必须指向合法的内存(已声明的变量、数组、动态分配的内存)才能解引用。
2. 返回局部变量的地址
int* bad_func(void) {
int x = 100;
return &x; // 函数返回后 x 已销毁,返回的地址无效
}
这是经典的“悬空指针”错误。要返回指针,可以返回静态局部变量、全局变量或动态分配的内存的地址。
3. 解引用空指针
int *p = NULL;
printf("%d\n", *p); // 段错误!NULL 是空地址,不允许访问
NULL 是一个宏,表示空指针。在解引用前,一定要确保指针非空。
4. 指针类型不匹配
int a = 10;
double *dp = &a; // 编译器警告,类型不兼容
printf("%f\n", *dp); // 未定义行为
不同类型的指针不要随便互指,除非你很明白自己在做什么(并且用强制转换)。
5. 野指针
指针指向的内存已经释放(free 之后),但指针还在,指向的地址无效。后面动态内存部分会细讲。
十、小结
今天你第一次触碰了 C 语言的灵魂——指针。它们不是什么神秘魔法,只是存地址的变量。但它们解锁了:
- 直接修改外部变量的能力(
swap终于能用了) - 高效操作数组的方式(指针算术)
- 理解了数组作为函数参数的本质
你现在知道了:
&取地址,*解引用。- 指针变量声明用
int *p; - 数组名就是首元素地址,
arr[i]就是*(arr + i) - 函数传数组本质是传地址,所以能在函数内修改原数组。
指针的旅途才刚刚开始。下一篇我们会深入指针与数组更复杂的关系——指针数组、数组指针、多级指针,以及字符串与指针的紧密联系。当你能轻松玩弄指针时,你就真正拥有了 C 语言。
课后小练习
- 写一个函数
void increment(int *p),让传入的整数加 1。在main中测试。 - 使用指针遍历一个
double数组,打印所有元素,观察指针加 1 时地址增加了多少字节。 - 写一个函数
int array_sum(int *arr, int n),用指针算术(而不是下标)计算数组元素的和。 - 分析以下代码错在哪里:
为什么输出可能不是 5?用静态局部变量怎么修正?int* get_pointer(void) { int val = 5; return &val; } int main(void) { int *p = get_pointer(); printf("%d\n", *p); return 0; }
我们下期见!
643

被折叠的 条评论
为什么被折叠?



