指针基础 — 地址与解引用
C语言学习教程
指针基础 — 地址与解引用
指针是 C 语言最强大的特性,也是最难的概念。这很正常——你的大脑需要时间适应”间接访问”这种思维方式。别着急,我们从零开始。
为什么要有指针?
指针不是 C 语言发明者故意为难你的,它解决了三个实际问题:
问题一:如何在函数里修改外部的变量? 普通传参只能传值(复印件),函数改不了原件。传地址就能改——函数拿到门牌号,直接找到变量。
问题二:如何高效传递大块数据? 10MB 的图像数据按值传递每次都要复制 10MB。传一个 8 字节的指针就够了。
问题三:如何在运行时动态分配内存?
程序运行前不知道要存多少数据。malloc 返回一个指针指向新分配的内存。
三个问题的答案一样:不直接拿数据,而是拿数据的地址。 这就是指针。
核心比喻:房子与门牌号
变量 = 一栋房子,里面住着数据
地址 = 房子的门牌号(如"解放路 128 号")
指针 = 一张纸条,纸条上写着门牌号
- 你去房子 = 直接访问:
printf("%d", x) - 看纸条上的门牌号 = 取指针值:
printf("%p", p) - 按门牌号找过去 = 解引用:
printf("%d", *p) - 把纸条改成另一家的门牌号 = 让指针改指向:
p = &y
关键洞察:纸条不是房子,纸条只是写着房子在哪。 改纸条(换门牌号)不等于改房子里的东西。
把内存想象成一栋大公寓
计算机内存是一栋巨大的公寓楼:
┌───────┬───────┬───────┬───────┬───────┬───────┐
│ 1000 │ 1004 │ 1008 │ 100C │ 1010 │ 1014 │ ... 每个房间4㎡(4字节)
├───────┼───────┼───────┼───────┼───────┼───────┤
│ │ │ │ │ │ │
└───────┴───────┴───────┴───────┴───────┴───────┘
- 每个房间有唯一编号(地址)
- int = 4 字节 = 需要连续 4 个房间
- 指针 = 8 字节 = 连续 8 个房间(64 位系统)
声明 int x = 42; :编译器找 4 个连续空房间,放 42,以后写 x 就自动替换成”那些房间里的内容”。
声明 int *p = &x; :编译器再找 8 个房间,写上 x 的门牌号,以后写 *p 就分两步:先读 p 里的门牌号,再按门牌号找过去读内容。
两个关键运算符:& 和 *
| 运算符 | 名字 | 含义 | 读法 |
|---|---|---|---|
& | 取地址 | 问”这个变量住哪?" | "……的地址” |
* | 解引用 | 按门牌号找过去 | ”地址……里的内容” |
注意:* 有两个身份!
int *p = &x; // 声明中的 * :"p 是指向 int 的指针"
*p = 100; // 执行中的 * :"p 指向的那个变量"
同一个符号,声明中和执行中含义完全不同。这是初学者第一个大坑。
示例一:最基础的指针操作
#include <stdio.h>
int main() {
int x = 42;
int *p = &x; // p 里写下 x 的门牌号
printf("x 的值: %d\n", x); // 42
printf("x 的地址: %p\n", &x); // 0x7fff1a3b (举例)
printf("纸条上的字: %p\n", p); // 同上
printf("按纸条找去: %d\n", *p); // 42
return 0;
}
内存图解:
假设 x 在 0x1000, p 在 0x1008
地址: 0x1000 0x1008
┌────────────┐ ┌────────────┐
│ 42 │ │ 0x1000 │
│ (x) │ │ (p) │
└────────────┘ └────────────┘
↑ │
└──────p 指向 x┘
指针值(p) 点向的值(*p)
┌──────────┐ ┌──────────┐
│ 0x1000 │ ───→ │ 42 │
│(一张纸条)│ │(真正的房)│
└──────────┘ └──────────┘
画重点:p 的值是 0x1000(门牌号),*p 是 42(房间里的东西),&x 也是 0x1000。注意 p 自己也有地址 &p = 0x1008。
示例二:通过指针修改原变量——“遥控器”效果
#include <stdio.h>
int main() {
int score = 60;
printf("初始分数: %d\n", score); // 60
int *remote = &score; // 拿到 score 的"遥控器"
*remote = 90; // 按遥控器改分数
printf("修改后分数: %d\n", score); // 90
// *remote 和 score 是同一个东西!改了一个另一个跟着变
return 0;
}
内存变化:
步骤1: int score = 60;
┌──────────┐
│ 60 │ ← score
└──────────┘
步骤2: int *remote = &score;
┌──────────┐ ┌──────────┐
│ 60 │ ←┐ │0x??score │ ← remote
└──────────┘ └──└──────────┘
步骤3: *remote = 90; (按门牌号找过去改)
┌──────────┐ ┌──────────┐
│ 90 │ ←┐ │0x??score │ remote 没变
└──────────┘ └──└──────────┘
示例三:swap 函数——指针最经典的用途
为什么普通传参不能交换两个变量?
#include <stdio.h>
// ✘ 错误:改的是复印件
void swap_wrong(int a, int b) {
int temp = a;
a = b;
b = temp;
}
// ✔ 正确:传入地址,间接改原件
void swap_right(int *a, int *b) {
int temp = *a; // 按 a 纸条找过去,暂存
*a = *b; // 把 b 指向的值复制到 a 指向的房间
*b = temp; // 暂存的值放进 b 指向的房间
}
int main() {
int x = 10, y = 20;
printf("交换前: x=%d, y=%d\n", x, y); // 10, 20
swap_wrong(x, y);
printf("swap_wrong 后: x=%d, y=%d\n", x, y); // 10, 20(没变!)
swap_right(&x, &y);
printf("swap_right 后: x=%d, y=%d\n", x, y); // 20, 10(成功!)
return 0;
}
swap_right 内存图解:
调用前:
┌──────┐ ┌──────┐
│ 10 │ │ 20 │
│ x │ │ y │
└──────┘ └──────┘
进入 swap_right(&x, &y):
a 纸条写 x 的门牌号,b 纸条写 y 的门牌号
temp = *a: 读 a 纸条→x→得到10,暂存 temp=10
*a = *b: 读 b 纸条→y→得到20,写入 a 纸条→x→x 变成 20
*b = temp: 把 10 写入 b 纸条→y→y 变成 10
函数结束,a 和 b 消失,但 x 和 y 已经被改!
传值 vs 传地址——一表区分:
| swap_wrong(x, y) | swap_right(&x, &y) | |
|---|---|---|
| 传了什么 | 10 和 20(值) | x 的地址 和 y 的地址 |
| 函数拿到 | a=10, b=20 | a=纸条(x), b=纸条(y) |
| 修改什么 | 局部变量 a, b | *a 即 x, *b 即 y |
| 结果 | x,y 不变(复印件改了) | x,y 交换(直接改原件) |
示例四:指针与数组——一个秘密
数组名就是指向第一个元素的指针(常量指针):
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
// arr 就是 &arr[0]
printf("arr = %p\n", arr); // 0x1000
printf("&arr[0] = %p\n", &arr[0]); // 0x1000(一样!)
int *p = arr;
// 五种等价方式访问 arr[2]
printf("%d\n", arr[2]); // 30
printf("%d\n", *(arr+2)); // 30
printf("%d\n", p[2]); // 30(指针也能用下标)
printf("%d\n", *(p+2)); // 30
printf("%d\n", 2[arr]); // 30(a[b] == *(a+b))
return 0;
}
数组在内存中连续存放:
地址: 0x1000 0x1004 0x1008 0x100C 0x1010
┌───────┬───────┬───────┬───────┬───────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │
│arr[0] │arr[1] │arr[2] │arr[3] │arr[4] │
└───────┴───────┴───────┴───────┴───────┘
↑ p ↑ p+4
p+1 不是"地址加 1 字节",而是"跳到下一个 int"
p+1 = 0x1000 + sizeof(int) = 0x1004
指针遍历数组(两种写法):
int arr[] = {10, 20, 30, 40, 50};
// 写法一:下标
int *p = arr;
for (int i = 0; i < 5; i++) printf("%d ", p[i]);
// 写法二:指针滑动
for (int *p = arr; p < arr + 5; p++) printf("%d ", *p);
数组名 vs 指针——关键区别:
| 数组名 arr | 指针 p | |
|---|---|---|
| 能改值吗 | arr = ??? 报错 | p = &x 可以 |
| sizeof | 整个数组 20 字节 | 指针本身 8 字节 |
arr++; | 编译错误 | 可以 |
示例五:通过指针遍历字符串
字符串就是字符数组,指针遍历是经典 C 风格:
#include <stdio.h>
int main() {
char str[] = "Hello";
// 内存: 'H' 'e' 'l' 'l' 'o' '\0'
// 经典指针遍历
char *p = str;
while (*p != '\0') {
printf("%c ", *p);
p++;
}
printf("\n");
// 更简洁
for (char *q = str; *q != '\0'; q++) {
printf("%c ", *q);
}
printf("\n");
return 0;
}
字符数组内存布局:
地址: 0x1000 0x1001 0x1002 0x1003 0x1004 0x1005
┌──────┬──────┬──────┬──────┬──────┬──────┐
│ 'H' │ 'e' │ 'l' │ 'l' │ 'o' │ '\0' │
└──────┴──────┴──────┴──────┴──────┴──────┘
↑ p '\0' 停止
示例六:NULL 指针与安全范式
NULL 是特殊值,表示”空白纸条,没有写任何有效门牌号”。解引用前必须检查:
#include <stdio.h>
int main() {
int *p = NULL; // 明确:暂不指向任何东西
int x = 100;
// 安全范式:先检查再用
if (p != NULL) {
*p = 42; // 不会执行,p 是 NULL
} else {
printf("p 是空指针,不能解引用\n");
}
p = &x; // 赋有效地址后才安全
if (p != NULL) printf("*p = %d\n", *p); // 100
return 0;
}
五大常见错误
错误一:野指针——没初始化就解引用
✘ int *p; ✔ int x;
*p = 100; // p 指向哪? int *p = &x;
// 不知道!崩溃/乱写 *p = 100; // 安全
// 或至少 int *p = NULL;
比喻:拿着一张开白纸条就说”按纸条找过去放 100”——纸条上什么都没写,你要去哪?
错误二:把数值当地址
✘ int *p = 1000; // 1000 号房间里有啥?不知道!
✔ int x = 1000; int *p = &x;
比喻:编了一个不存在的门牌号写在纸条上,然后按这个门牌号去找房子——撞上墙或别人家。
错误三:混淆”换指向”和”改值”
int x = 10, y = 20;
int *p = &x;
p = &y; // 改了纸条!p 现在指向 y(*p 变成 20)
*p = y; // 改了房间!x 变成 20(p 还指向 x)
一句话:p = 新地址 是改纸条内容,*p = 新值 是按纸条找到房间改内容。
错误四:返回局部变量的地址(致命!)
✘ int* bad() { ✔ int* good() {
int local = 42; int *p = malloc(sizeof(int));
return &local; *p = 42;
} // 函数结束 local 就没了! return p; // 调用者用完要 free
} }
比喻:函数里临时搭了一个帐篷,你记下位置。函数结束帐篷拆了,你把位置告诉别人——什么都没有。
错误五:声明时 int *p, q; 陷阱
int *p, q; // 只有 p 是指针!q 是普通 int
int *p, *q; // 两个都是指针——每人要有自己的 *
指针运算速查
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // p 指向 arr[0]
p // arr[0] 的地址
*p // arr[0] 的值 = 10
*(p+1) // arr[1] 的值 = 20
p[2] // *(p+2) = 30
p++ // p 移到下一个元素
重点回顾
| 概念 | 读法 | 例子 |
|---|---|---|
&x | x 的地址 | 问”x 住哪?“ |
*p | p 指向的值 | 按纸条找过去 |
int *p = &x | p 是指向 int 的指针 | 纸条写 x 的门牌号 |
*p = 100 | 改 p 指向的房间 | 按纸条找过去换内容 |
p = &y | 改纸条内容 | 不再指向 x,改指 y |
NULL | 空指针 | 空白纸条 |
三条黄金法则:
- 永远初始化指针——
= &某变量或= NULL - 解引用前检查是否为 NULL
- 想清楚改的是指针本身(
p = ...)还是指向的东西(*p = ...)