指针

指针是 C/C++ 里最核心也最容易混淆的概念,但它其实只有一句话:指针就是存地址的变量
抓住"地址"这个关键词,指针就通了。


一、:程序 = 数据结构 + 算法

1.1 程序的本质

一个程序要解决问题,离不开两样东西:

程序 = 数据结构 + 算法  
组成部分 回答的问题 例子
数据结构 数据怎么存 数组、结构体、链表、栈、队列
算法 数据怎么处理 查找、排序、递归、动态规划

一句话概括:

  • 数据结构解决"怎么把数据组织起来";
  • 算法解决"怎么用这些数据算出结果"。

1.2 为什么需要先学指针

指针是理解链表、树、图等复杂数据结构的基础——这些结构在内存里往往不是连续的一块,而是靠"地址"把一个个节点串起来。而记录地址、通过地址访问数据,正是指针的本职工作。


二、指针:记录变量内存地址

2.1 什么是地址

计算机的内存像一栋大楼,每个字节是一个"房间",每个房间有唯一的编号,这个编号就是地址(Address)

地址 0x1000  0x1001  0x1002  0x1003  ... 
┌───────┬───────┬───────┬───────┬────内存 
│       │       │       │       │  ... 
└───────┴───────┴───────┴───────┴──── ↑ 1 字节 = 8 bit

2.2 变量与地址

定义一个变量时,编译器做了两件事:

  1. 在内存里为它分配一块空间(大小由类型决定);
  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 空指针:NULLnullptr

空指针表示"不指向任何地方"。

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,避免野指针。