基础
指针
指针本质就是一个变量,只不过它存的不是普通的数值,而是另一个变量的内存地址。
1 2
| int a = 42; int* p = &a;
|
&a — 取地址,拿到 a 在内存中的位置*p — 解引用,通过地址找到那个位置上存的值
1 2 3 4
| #include <iostream>
std::cout << p; std::cout << *p;
|
修改 *p 就是在修改 a 本身:
1 2 3 4
| #include <iostream>
*p = 100; std::cout << a;
|
声明时注意初始化,未初始化的指针指向随机地址,解引用就是未定义行为:
1 2
| int* bad; int* safe = nullptr;
|
内存
- 内存可以看作一大片连续编号的字节,编号就是地址
- 每个变量都在内存里占一小段位置,
&a 拿到的是这段位置的起始地址 - 占多大由类型决定:
int 通常 4 字节、double 通常 8 字节。可以用 sizeof 查:
1 2 3 4
| #include <iostream>
std::cout << sizeof(int); std::cout << sizeof(double);
|
程序运行时的内存大致分几个区域,指针可以指向其中任意一块:
- 栈:局部变量,离开作用域自动销毁
- 堆:
new 分配出来的,需要手动 delete - 静态区:全局变量、
static 变量,程序运行期间一直存在 - 代码区:程序指令本身
堆 vs 栈
判断一个变量(普通数组、指针数组都一样)在堆还是栈,唯一的依据是它是怎么创建的,而不是它指向谁:
- 局部变量、局部数组 → 栈,离开作用域自动销毁
new / new[] 出来的 → 堆,必须手动 delete
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void demo() { int a[3] = {1, 2, 3};
int x = 1, y = 2, z = 3; int* ptrs[3] = {&x, &y, &z};
int* heap_ptrs[3]; for (int i = 0; i < 3; i++) heap_ptrs[i] = new int(i);
int* p = new int[3]; delete[] p; }
|
关键:指针自己放在哪,和被它指向的对象在哪,是两回事。
| 写法 | 变量/数组本身 | 它(们)指向的内容 |
|---|
int a[3]; | 栈 | —(元素是 int 值,直接存在数组里) |
int* ptrs[3]; | 栈 | 看每个指针怎么初始化,可能在栈也可能在堆 |
int* p = new int[3]; | 指针在栈 | 数组在堆 |
另外:栈空间有限(通常几 MB),大数组别直接放栈上,容易栈溢出;堆空间大得多,大块内存要用 new 去堆上分配。
指针与数组
用指针遍历数组
数组名本身就可以当作指向首元素的指针:
1 2 3 4 5 6 7 8
| #include <iostream>
int arr[] = {10, 20, 30, 40, 50}; int* p = arr;
for (int i = 0; i < 5; i++) { std::cout << *(p + i) << " "; }
|
注意:p + i 不是地址加 i 个字节,而是加 i 个元素的大小。如果 int 是 4 字节,p + 1 实际上地址偏移了 4 字节。
指针算术
1 2 3 4 5 6 7 8 9 10
| #include <iostream>
int arr[] = {10, 20, 30, 40, 50}; int* p = arr;
p++; p += 2;
int* q = &arr[4]; std::cout << q - p;
|
常见写法——用指针做循环:
1 2 3 4 5 6
| #include <iostream>
int arr[] = {1, 2, 3, 4, 5}; for (int* p = arr; p < arr + 5; p++) { std::cout << *p << " "; }
|
这和标准库迭代器的设计思路一致:[begin, end) 左闭右开区间。
函数传参:指针 vs 引用
用指针修改外部变量
C 语言时代只能用指针实现”在函数里改外面的变量”:
1 2 3 4 5 6 7 8
| void swap(int* a, int* b) { int tmp = *a; *a = *b; *b = tmp; }
int x = 1, y = 2; swap(&x, &y);
|
C++ 有了引用,同样的功能写起来更干净:
1 2 3 4 5 6 7 8
| void swap(int& a, int& b) { int tmp = a; a = b; b = tmp; }
int x = 1, y = 2; swap(x, y);
|
选择建议:
- 能用引用就用引用,语法更清晰
- 需要表达”可能为空”时用指针(引用必须绑定到有效对象)
- 和 C 接口交互时用指针
传数组给函数
数组传参时会退化为指针,必须额外传长度:
1 2 3 4 5 6 7 8 9 10
| #include <iostream>
void printArray(int* arr, int size) { for (int i = 0; i < size; i++) { std::cout << arr[i] << " "; } }
int data[] = {1, 2, 3, 4, 5}; printArray(data, 5);
|
现代 C++ 更推荐用 std::span(C++20)或传 std::vector&,避免指针加长度的手动管理。
用 std::span——数组和 vector 都能直接传,长度由 span 自己维护:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <iostream> #include <span> #include <vector>
void printArray(std::span<const int> s) { for (int x : s) { std::cout << x << " "; } }
int data[] = {1, 2, 3, 4, 5}; printArray(data);
std::vector<int> v{1, 2, 3}; printArray(v);
|
用 const std::vector<int>&——长度由 vector 自己维护:
1 2 3 4 5 6 7 8 9 10 11
| #include <iostream> #include <vector>
void printArray(const std::vector<int>& v) { for (int x : v) { std::cout << x << " "; } }
std::vector<int> data{1, 2, 3, 4, 5}; printArray(data);
|
如果拿到的本来就是 C 数组(例如和 C 库交互),std::span 是更合适的桥接方式。
动态内存:new / delete
分配与释放
栈上的变量出了作用域自动销毁,但有时候需要对象活得更久,或者大小在运行时才知道:
1 2 3 4 5 6 7 8 9 10 11
| #include <iostream>
int* p = new int(42); std::cout << *p; delete p;
int* arr = new int[100]; arr[0] = 1; delete[] arr;
|
规则:new 对应 delete,new[] 对应 delete[],混用是未定义行为。
内存泄漏的典型场景
1 2 3 4
| void leak() { int* p = new int(42); }
|
1 2 3 4 5 6 7
| void earlyReturn() { int* p = new int[100]; if (someCondition) { return; } delete[] p; }
|
1 2 3 4 5
| void exceptionLeak() { int* p = new int[100]; riskyFunction(); delete[] p; }
|
这些场景就是智能指针要解决的问题。
智能指针
智能指针是对裸指针的封装,利用 RAII(资源获取即初始化)机制在离开作用域时自动释放内存。需要 #include <memory>。
unique_ptr
独占所有权,同一时刻只有一个 unique_ptr 指向对象:
1 2 3 4 5 6
| #include <iostream> #include <memory>
auto p = std::make_unique<int>(42); std::cout << *p;
|
不能复制,但可以移动:
1 2 3 4 5 6
| #include <memory> #include <utility>
auto p1 = std::make_unique<int>(42);
auto p2 = std::move(p1);
|
shared_ptr
多个指针共享同一个对象,内部维护引用计数,最后一个 shared_ptr 销毁时释放内存:
1 2 3 4 5 6 7 8 9
| #include <iostream> #include <memory>
auto p1 = std::make_shared<int>(42); auto p2 = p1;
std::cout << p1.use_count(); p1.reset();
|
适用场景:对象的生命周期不确定由谁管理,比如缓存、观察者模式中的共享资源。
代价是引用计数带来的额外开销,别无脑用。
weak_ptr
weak_ptr 是 shared_ptr 的搭档,不增加引用计数,只是”观察”一个 shared_ptr 管理的对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <iostream> #include <memory>
auto sp = std::make_shared<int>(42); std::weak_ptr<int> wp = sp;
std::cout << sp.use_count();
if (auto p = wp.lock()) { std::cout << *p; } else { }
|
主要用途是打破 shared_ptr 的循环引用。两个对象互相持有对方的 shared_ptr 时,引用计数永远不为 0,谁都不会被释放,造成泄漏:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <memory>
struct Node { std::shared_ptr<Node> next; };
auto a = std::make_shared<Node>(); auto b = std::make_shared<Node>(); a->next = b; b->next = a;
struct Node2 { std::weak_ptr<Node2> next; };
auto a2 = std::make_shared<Node2>(); auto b2 = std::make_shared<Node2>(); a2->next = b2; b2->next = a2;
|
判断标准:只是”借用/观察”对象而不想延长它的生命,就用 weak_ptr。
现代 C++ 标准库的核心就是 unique_ptr / shared_ptr / weak_ptr 三个(auto_ptr 已被移除),日常开发记住它们的定位即可:独占、共享、观察。
常见问题
野指针 / 悬垂指针
1 2 3 4 5 6 7 8 9 10
| #include <iostream>
int* p = new int(42); delete p; std::cout << *p;
delete p; p = nullptr;
|
1 2 3 4 5
| int* bad() { int x = 42; return &x; }
|
double free
1 2 3 4
| int* p = new int(42); int* q = p; delete p; delete q;
|
解决方案:用 unique_ptr 明确所有权,从根本上避免”谁该 delete”的混乱。