指针
指针是 C/C++ 里最核心也最容易混淆的概念,但它其实只有一句话:指针就是存地址的变量。
抓住"地址"这个关键词,指针就通了。
一、:程序 = 数据结构 + 算法
1.1 程序的本质
一个程序要解决问题,离不开两样东西:
程序 = 数据结构 + 算法
| 组成部分 | 回答的问题 | 例子 |
|---|---|---|
| 数据结构 | 数据怎么存 | 数组、结构体、链表、栈、队列 |
| 算法 | 数据怎么处理 | 查找、排序、递归、动态规划 |
一句话概括:
- 数据结构解决"怎么把数据组织起来";
- 算法解决"怎么用这些数据算出结果"。
1.2 为什么需要先学指针
指针是理解链表、树、图等复杂数据结构的基础——这些结构在内存里往往不是连续的一块,而是靠"地址"把一个个节点串起来。而记录地址、通过地址访问数据,正是指针的本职工作。
二、指针:记录变量内存地址
2.1 什么是地址
计算机的内存像一栋大楼,每个字节是一个"房间",每个房间有唯一的编号,这个编号就是地址(Address)。
地址 0x1000 0x1001 0x1002 0x1003 ...
┌───────┬───────┬───────┬───────┬────内存
│ │ │ │ │ ...
└───────┴───────┴───────┴───────┴──── ↑ 1 字节 = 8 bit
2.2 变量与地址
定义一个变量时,编译器做了两件事:
- 在内存里为它分配一块空间(大小由类型决定);
- 用变量名作为这块空间的"别名",方便我们读写。
int a = 10;
| 项 | 说明 |
|---|---|
| 变量名 | a(方便人用) |
| 值 | 10(空间里存的内容) |
| 地址 | &a(空间的编号) |
| 大小 | sizeof(a) = 4 字节 |
2.3 指针的定义
指针就是专门用来存"地址"的变量。
int *p; // p 是一个指针,存的是"某个 int 的地址"
对比普通变量和指针变量:
int a = 10; // a 存的是"值"
int *p = &a; // p 存的是"a 的地址"
💡 记住这个类比:
- 普通变量 = 直接存钱;
- 指针变量 = 存"保险箱的号码",通过号码去找钱。
三、指针语法:取址符 & 与解引用 *
3.1 两个核心运算符
| 运算符 | 作用 | 读法 |
|---|---|---|
&a |
取变量 a 的地址 |
“a 的地址” |
*p |
取指针 p 指向内存的值 |
“p 指向的内容” |
& 和 * 是互逆的一对:
&a ─────────► 地址 ─────────► *p
取地址 解引用(取值)
3.2 三步走:定义 → 指向 → 使用
int a = 10;
int *p; // ① 定义指针
p = &a; // ② 让 p 指向 a(把 a 的地址存进 p)
cout << *p; // ③ 通过 p 间接访问 a,输出 10
也可以一行搞定:
int a = 10;
int *p = &a; // 定义并初始化
3.3 通过指针修改原变量
int a = 10;
int *p = &a;
*p = 20; // 相当于 a = 20cout << a; // 输出 20
这是指针最重要的能力:通过地址间接修改另一个变量的值。
3.4 类型必须匹配
指针的类型必须和它指向的变量类型一致:
int *p; // 只能指向 int
char *q; // 只能指向 char
double *r; // 只能指向 double
double d = 3.14;
int *p = &d; // ❌ 类型不匹配,编译器会警告/报错
3.5 区分 * 的两个身份
* 在不同位置含义不同,这是初学最容易混的地方:
| 位置 | 含义 |
|---|---|
定义时 int *p; |
声明 p 是指针类型 |
使用时 *p |
解引用,取 p 指向的值 |
int *p; // 这里的 * 是"声明指针"
*p = 10; // 这里的 * 是"解引用"
四、指针与数组
4.1 数组名就是指针
数组名是数组首元素的地址,本质上就是一个指向首元素的指针。
int a[5] = {10, 20, 30, 40, 50};
int *p = a; // 等价于 int *p = &a[0];cout << (a == p); // 输出 1,地址相同
4.2 指针的加减:移动一个元素
指针 +1 跳过的不是 1 字节,而是 sizeof(类型) 字节——正好到下一个元素。
int a[5] = {10, 20, 30, 40, 50};
int *p = a; // 指向 a[0]
cout << *p; // 10
p++; // 指向 a[1]cout << *p; // 20
p += 2; // 指向 a[3]cout << *p; // 40
4.3 用指针遍历数组
int a[5] = {10, 20, 30, 40, 50};
// 方法一:下标
for (int i = 0; i < 5; i++)
cout << a[i] << ' ';
// 方法二:指针
for (int *p = a; p < a + 5; p++)
cout << *p << ' ';
4.4 指针与下标等价
a[i] 本质就是 *(a + i):
a[i] ≡ *(a + i)
&a[i] ≡ a + i
所以数组的下标访问,底层就是指针运算。
五、指针与结构体
5.1 结构体与指针
结构体也是一种类型,同样可以定义指向它的指针:
struct Student {
string name; int age;
};
Student s = {"小明", 18};
Student *p = &s; // p 指向结构体 s
5.2 两种访问属性的方式
通过指针访问结构体的属性,有两种写法,完全等价:
方式 a:箭头运算符 p->属性
cout << p->name; // 小明
cout << p->age; // 18
方式 b:先解引用再加点 (*p).属性
cout << (*p).name; // 小明
cout << (*p).age; // 18
5.3 为什么 (*p).属性 要加括号
因为 . 的优先级高于 *,如果不加括号:
*p.name // ❌ 等价于 *(p.name),但 p 是指针,没有 .name,会报错
(*p).name // ✔ 先解引用得到结构体,再取 .name
💡 口诀:指针访问结构体,箭头最省事
p->x;不用箭头就加括号(*p).x。
5.4 通过指针修改结构体属性
Student s = {"小明", 18};
Student *p = &s;
p->age = 19; // 相当于 s.age = 19(*p).age = 20; // 也相当于 s.age = 20
cout << s.age; // 20
5.5 结构体指针的典型用途
链表就是"结构体 + 指针"的经典组合——每个节点存一个指针,指向下一个节点:
struct Node {
int data;
Node *next; // 指向下一个节点的指针
};
六、指针拓展
6.1 三种函数传参方式
C++ 里给函数传参有三种方式,核心区别是函数内修改形参,会不会影响实参。
(1)值传参(默认)
void f(int x) {
x = 100; // 只改了自己的副本
}
int main() {
int a = 10; f(a);
cout << a; // 仍是 10,没变
}
特点:把实参复制一份给形参,函数内改的是副本,不影响实参。
(2)引用传参 &
void f(int &x) {
x = 100; // x 就是 a 的别名,直接改 a}
int main() {
int a = 10; f(a);
cout << a; // 变成 100}
特点:形参是实参的别名,函数内修改会直接影响实参。
(3)指针传参
void f(int *p) {
*p = 100; // 通过地址间接修改 a}
int main() {
int a = 10; f(&a);
cout << a; // 变成 100}
特点:把实参的地址传进去,函数内通过解引用间接修改实参。
6.2 三种方式对比
| 传参方式 | 写法 | 函数内能否改实参 | 本质 |
|---|---|---|---|
| 值传参 | void f(int x) |
❌ 不能 | 复制一份 |
| 引用传参 | void f(int &x) |
✅ 能 | 起别名 |
| 指针传参 | void f(int *p) |
✅ 能 | 传地址 |
💡 引用传参和指针传参都能改实参,但引用更简洁安全(不会出现空指针、野指针),所以 C++ 中更推荐引用;
指针传参是 C 语言的老方法,理解它能帮你看懂"地址到底怎么流动"。
6.3 经典例子:三种方式写 swap
// 值传参:❌ 交换不了
void swap1(int x, int y) {
int t = x;
x = y;
y = t;
}
// 引用传参:✔ 能交换
void swap2(int &x, int &y) {
int t = x;
x = y;
y = t;
}
// 指针传参:✔ 能交换
void swap3(int *x, int *y) {
int t = *x;
*x = *y;
*y = t;
}
6.4 多级指针
指针本身也有地址,所以可以定义"指向指针的指针"——二级指针;二级指针又有地址,于是还有三级指针……以此类推,可以有更多级指针。
核心规则:每一级指针只能记录"比它低一级"的指针的地址,层级不能乱。
| 指针级别 | 存的是什么 |
|---|---|
| 一级指针 | 普通变量的地址 |
| 二级指针 | 一级指针的地址 |
| 三级指针 | 二级指针的地址 |
| N 级指针 | (N-1) 级指针的地址 |
正确的层级关系
int a = 10;
int *p1 = &a; // 一级指针,存普通变量 a 的地址
int **p2 = &p1; // 二级指针,存一级指针 p1 的地址
int ***p3 = &p2; // 三级指针,存二级指针 p2 的地址
cout << a; // 10
cout << *p1; // 10(解引用一级,得到 a)
cout << **p2; // 10(解引用二级,得到 a)
cout << ***p3; // 10(解引用三级,得到 a)
关系图:
p3 p2 p1 a
┌────┐ ┌────┐ ┌────┐ ┌────┐
│地址 │ ──► │地址 │ ──► │地址 │ ──► │ 10 │
│ p2 │ │ p1 │ │ a │ └────┘
└────┘ └────┘ └────┘
三级 二级 一级 普通变量
💡 规律:每解引用一次
*,就"降一级",向最终的值靠近一步:
p3是三级指针*p3得到二级指针p2**p3得到一级指针p1***p3得到普通变量a的值10
层级不能乱(错误示范)
int a = 10;
int *p1 = &a; // 一级指针
int ***p3 = &p1; // ❌ 三级指针直接存一级指针的地址,跳级了
int **p2 = &a; // ❌ 二级指针直接存普通变量的地址,跳级了
💡 口诀:一级存变量,二级存一级,三级存二级;每级只降一级,不能跳级。
实际编程里很少用到三级及以上的指针(一般二级就够,例如动态二维数组),这里主要是帮你理解"层级"这个概念。
6.5 空指针:NULL 与 nullptr
空指针表示"不指向任何地方"。
int *p = NULL; // 传统写法,NULL 本质是 0
int *q = nullptr; // C++11 推荐写法,类型安全
| 写法 | 说明 |
|---|---|
NULL |
老写法,其实是个宏,等于 0,C/C++ 通用 |
nullptr |
C++11 引入,专用于空指针,类型安全,推荐使用 |
两者的区别:
void f(int x) { cout << "int\n"; }
void f(int *p) { cout << "pointer\n"; }
f(NULL); // 输出 int,因为 NULL 是 0,被当成整数
f(nullptr); // 输出 pointer,nullptr 明确是指针类型
⚠️ 空指针不能解引用,否则程序崩溃(段错误)。
int *p = nullptr;
cout << *p; // ❌ 解引用空指针,未定义行为
if (p != nullptr) {
cout << *p; // 先判断再使用,安全
}
6.6 野指针(补充警示)
int *p; // 未初始化,p 的值是随机的 → 野指针
cout << *p; // ❌ 危险,可能访问到非法内存
💡 养成习惯:指针要么定义时就初始化,要么先赋
nullptr,避免野指针。