C/C++ 入门:指针

指针难,不是因为语法多,而是因为它要求你开始从“变量的值”进一步看到“变量在内存里的位置”。

📚 基本概念速读

名称 定义 省流
地址 变量在内存中的位置编号 住址
指针 保存地址的变量 存地址
取地址 & 得到变量地址 问住址
解引用 * 通过地址访问目标值 按地址找值
自动管理的函数调用内存区域 自动生灭
手动申请、手动释放的内存区域 手动管理
空指针 不指向任何有效对象的指针 指向空
野指针 指向未知或无效位置的指针 危险地址
悬空指针 原对象已失效但指针还留着 地址过期

🧩 程序内存分区极简认识

程序运行时,内存不是一整块随便用,而是可以粗略分成几个区域。初学阶段不需要记所有细节,先建立心智模型。

1
2
3
4
5
6
7
8
9
10
11
+----------------------+
| 代码区 | 程序指令
+----------------------+
| 常量区 | 字符串字面量、只读常量
+----------------------+
| 全局区 | 全局变量、静态变量
+----------------------+
| 堆 | 手动申请、手动释放
+----------------------+
| 栈 | 局部变量、函数调用现场
+----------------------+

更简单地说:

区域 放什么 谁管理 常见特点
局部变量、函数参数 编译器和运行时自动管理 快,函数结束自动释放
手动申请的大块数据 程序员管理 灵活,但容易泄漏
全局区 全局变量、static 变量 程序启动到结束 生命周期长
常量区 字符串字面量、只读数据 编译器和系统管理 通常不允许修改

这只是入门模型。不同系统和编译器的真实内存布局会更复杂,但这个模型足够帮助理解指针。

📦 栈内存

局部变量生命周期

在函数里定义的普通局部变量,通常位于栈上。

1
2
3
4
5
int add(int a, int b)
{
int result = a + b;
return result;
}

这里的 abresult 都和这次函数调用绑定。函数开始时它们被创建,函数结束时它们被销毁。

可以这样理解:

1
2
3
4
5
6
7
8
9
10
调用 add(3, 5)
|
v
创建 a、b、result
|
v
return result
|
v
函数结束,局部变量销毁

函数结束自动销毁

看一个危险例子:

1
2
3
4
5
int* bad()
{
int x = 10;
return &x;
}

x 是局部变量,函数结束后就没了。return &x; 返回的是一个已经失效的地址。

初学阶段先记住:不要返回局部变量的地址

🧱 堆内存

手动分配

堆内存的生命周期不受函数作用域直接限制。C++ 里可以用 new 申请,用 delete 释放。

1
2
3
4
5
6
7
int* p = new int;
*p = 10;

cout << *p << endl;

delete p;
p = nullptr;

这段代码做了几件事:

代码 含义
new int 在堆上申请一个 int 空间
int* p 用指针 p 保存这块空间的地址
*p = 10; 通过地址把值写进去
delete p; 释放这块堆内存
p = nullptr; 避免指针继续保存旧地址

生命周期不受函数限制

下面这个函数返回堆内存地址是可以的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int* createNumber()
{
int* p = new int;
*p = 10;
return p;
}

int main()
{
int* value = createNumber();
cout << *value << endl;

delete value;
value = nullptr;

return 0;
}

因为 new int 申请的是堆内存,不会随着 createNumber 函数结束自动销毁。

但代价是:谁申请,谁负责释放。忘记 delete 会造成内存泄漏。

📍 地址、取地址与解引用

地址

每个变量都存放在内存中的某个位置,这个位置可以理解为地址。

1
2
int x = 10;
cout << &x << endl;

&x 表示取变量 x 的地址。

指针变量

指针变量专门用来保存地址。

1
2
int x = 10;
int* p = &x;

读法:

代码 含义
int x = 10; 创建一个整数变量
&x 得到 x 的地址
int* p 创建一个指向 int 的指针
p = &x p 保存 x 的地址

解引用

*p 表示通过指针 p 保存的地址,访问那个地址上的值。

1
2
3
4
5
6
7
int x = 10;
int* p = &x;

cout << *p << endl; // 10

*p = 20;
cout << x << endl; // 20

可以粗略理解为:

1
2
3
4
5
6
x: 10
^
|
p 保存 x 的地址

*p 表示沿着地址找到 x

* 的两个常见含义

* 在不同位置含义不同:

写法 含义
int* p 定义一个指针变量
*p = 20 解引用,通过指针访问目标值

这也是指针语法容易让新手混乱的地方。

🕳️ nullptr

空指针

如果一个指针暂时不指向任何有效对象,应该让它等于 nullptr

1
int* p = nullptr;

nullptr 的意思是:这个指针现在不保存有效地址。

判空

使用指针前,常见做法是先判断它是否为空:

1
2
3
4
5
int* p = nullptr;

if (p != nullptr) {
cout << *p << endl;
}

也可以简写:

1
2
3
if (p) {
cout << *p << endl;
}

初学阶段建议写清楚 p != nullptr,可读性更好。

避免和 NULL 混用

C 语言里常见 NULL,现代 C++ 更推荐 nullptr

1
2
int* p1 = nullptr;  // 推荐
int* p2 = NULL; // 旧写法,不推荐混用

原因是 nullptr 有明确的空指针类型,更适合 C++ 的类型检查。

🔧 指针做函数参数

修改外部变量

值传递只能修改副本:

1
2
3
4
void change(int x)
{
x = 100;
}

指针参数可以修改外部变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

void change(int* p)
{
if (p != nullptr) {
*p = 100;
}
}

int main()
{
int x = 10;
change(&x);
cout << x << endl; // 100
return 0;
}

调用 change(&x) 时,传进去的是 x 的地址。函数内部用 *p 找到原来的变量并修改它。

传数组

数组作为函数参数时,通常会退化成指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
void printArray(int* arr, int length)
{
for (int i = 0; i < length; i++) {
cout << arr[i] << endl;
}
}

int main()
{
int nums[3] = {1, 2, 3};
printArray(nums, 3);
return 0;
}

这里的 arr 可以理解为指向数组第一个元素的指针。

传缓冲区

指针也常用来传一段可写缓冲区。

1
2
3
4
5
6
7
8
9
10
11
12
13
void fillZero(int* buffer, int length)
{
for (int i = 0; i < length; i++) {
buffer[i] = 0;
}
}

int main()
{
int data[5] = {1, 2, 3, 4, 5};
fillZero(data, 5);
return 0;
}

传缓冲区时,通常要同时传长度。只传指针,函数不知道后面有多少个元素。

🧮 指针与数组

数组名退化

很多表达式里,数组名会退化为指向首元素的指针。

1
2
3
4
int nums[3] = {10, 20, 30};

int* p = nums;
cout << *p << endl; // 10

nums 在这里相当于 &nums[0]

1
2
cout << nums << endl;
cout << &nums[0] << endl;

这两个输出通常会看到相同的地址。

指针遍历数组

可以通过指针移动遍历数组:

1
2
3
4
5
6
int nums[3] = {10, 20, 30};
int* p = nums;

for (int i = 0; i < 3; i++) {
cout << *(p + i) << endl;
}

p + i 不是简单加 i 个字节,而是向后移动 iint 的位置。

也可以写成更常见的数组下标:

1
2
3
for (int i = 0; i < 3; i++) {
cout << p[i] << endl;
}

p[i] 本质上可以理解为 *(p + i)

越界风险

指针不会自动知道数组长度。

1
2
3
4
int nums[3] = {10, 20, 30};
int* p = nums;

cout << p[3] << endl; // 错误示例:越界

p[3] 访问的是第 4 个元素,但数组只有 3 个元素。越界访问可能输出奇怪值,也可能直接崩溃。

初学阶段写数组函数时,尽量把指针和长度一起传:

1
2
3
4
5
6
void printArray(int* arr, int length)
{
for (int i = 0; i < length; i++) {
cout << arr[i] << endl;
}
}

🧷 指针数组、字符指针、数组指针

这三个名字很像,但含义不同。

指针数组

指针数组本质上是数组,数组里的每个元素都是指针。

1
2
3
4
5
6
7
8
9
int a = 10;
int b = 20;
int c = 30;

int* arr[3] = {&a, &b, &c};

cout << *arr[0] << endl; // 10
cout << *arr[1] << endl; // 20
cout << *arr[2] << endl; // 30

读法:arr 是一个数组,里面放了 3 个 int*

字符指针

字符指针常用来指向字符串字面量:

1
2
3
4
const char* text = "hello";

cout << text << endl;
cout << text[0] << endl; // h

这里建议写 const char*,因为字符串字面量通常位于只读区域,不应该被修改。

不要这样写:

1
char* text = "hello";  // 不推荐

如果需要可修改的字符数组,应写成数组:

1
2
char text[] = "hello";
text[0] = 'H';

数组指针

数组指针本质上是指针,它指向一个数组。

1
2
3
4
5
6
int nums[3] = {10, 20, 30};

int (*p)[3] = &nums;

cout << (*p)[0] << endl; // 10
cout << (*p)[1] << endl; // 20

int (*p)[3] 的读法是:p 是一个指针,指向一个包含 3 个 int 的数组。

注意括号很关键:

写法 含义
int* arr[3] 指针数组:数组里有 3 个 int*
int (*p)[3] 数组指针:指向含 3 个 int 的数组

⚠️ 野指针与悬空指针

未初始化指针

未初始化指针保存的是不确定地址。

1
2
int* p;
// cout << *p << endl; // 错误示例:p 没有初始化

正确做法:

1
int* p = nullptr;

指针定义时,如果暂时没有明确指向,就先设为 nullptr

释放后继续使用

释放堆内存后,指针里的旧地址还在,但那块内存已经不属于你。

1
2
3
4
5
6
int* p = new int;
*p = 10;

delete p;

// cout << *p << endl; // 错误示例:释放后继续使用

释放后建议立刻置空:

1
2
delete p;
p = nullptr;

这样后续可以通过判空避免继续使用旧地址。

栈内存销毁后指针残留

下面这个例子会产生悬空指针:

1
2
3
4
5
int* bad()
{
int x = 10;
return &x;
}

函数结束后,x 已经销毁,但外面拿到的指针还残留着原来的地址。

这种指针看起来像有值,实际上已经不能安全使用。

常见危险总结

问题 示例 结果
未初始化 int* p; 指向未知位置
空指针解引用 *nullptr 程序崩溃风险
释放后使用 delete p; *p 使用已释放内存
返回局部变量地址 return &x; 指向已销毁栈对象
数组越界 arr[100] 访问不属于自己的内存

✅ 指针使用最小准则

准则 原因
指针定义时初始化 避免野指针
不知道指向哪里就用 nullptr 明确表示空
解引用前先确认有效 避免崩溃
new 后要 delete 避免内存泄漏
delete 后置为 nullptr 避免悬空指针误用
数组指针参数同时传长度 避免越界
不返回局部变量地址 栈内存函数结束会失效

✅ 总结

指针的本质是地址;学指针,就是学会通过地址间接访问和管理内存。

先把三件事分清:& 取地址、指针保存地址、* 通过地址访问值。再逐步理解栈、堆、数组退化和悬空指针,指针就不会只是一堆符号。

Happy Hacking! 🎉