C++ 内存管理
内存管理是 C++ 最难也最强大的部分。和 Java/Python 不同,C++ 程序员直接控制内存——能极致优化性能,但稍不留神就是内存泄漏、段错误。好消息是,现代 C++(C++11 起)用智能指针解决了大部分痛点。这一章我们看 C++ 内存管理的全部要点。
一、内存的五个区
C++ 程序的内存大致分五个区:
- 栈(stack):局部变量、函数参数。自动管理,函数返回自动销毁。空间小(几 MB)、速度快。
- 堆(heap):
new分配的内存。手动管理,不delete就泄漏。空间大、速度慢。 - 全局/静态区:全局变量、static 变量。程序结束才销毁。
- 常量区:字符串字面量等只读数据。
- 代码区:编译后的机器指令。
#include <iostream>
using namespace std;
int globalVar = 1; // 全局区:程序整个生命周期
void demo() {
int localVar = 2; // 栈:函数返回时自动销毁
static int staticVar = 3; // 全局区(但只在此函数可见):程序结束销毁
int* heapVar = new int(4); // 堆:new 出来的,必须手动 delete
delete heapVar; // 不删就内存泄漏!
}
int main() {
demo();
// 栈的特点:快、自动管理、空间小(默认几 MB)
// 堆的特点:慢、手动管理、空间大(几乎全部可用内存)
// 栈上分配:直接定义变量
int a = 10; // 在栈上
int arr[100]; // 在栈上(数组)
// 堆上分配:用 new
int* p = new int(20); // 在堆上,*p = 20
int* bigArr = new int[1000000]; // 堆上分配 400 万字节(栈上放不下)
delete p;
delete[] bigArr; // 注意:数组用 delete[]
return 0;
}二、new 与 delete
new 在堆上分配内存并构造对象,delete 析构并释放:
#include <iostream>
using namespace std;
class Point {
public:
int x, y;
Point(int x = 0, int y = 0) : x(x), y(y) {
cout << "Point(" << x << "," << y << ") 构造" << endl;
}
~Point() {
cout << "Point 析构" << endl;
}
};
int main() {
// new:在堆上分配并构造对象,返回指针
Point* p1 = new Point; // 默认构造
Point* p2 = new Point(3, 4); // 带参构造
Point* arr = new Point[2]; // 数组:每个元素默认构造
cout << p2->x << ", " << p2->y << endl; // 3, 4
// delete:析构并释放内存
delete p1;
delete p2;
delete[] arr; // 数组用 delete[](用 delete 是未定义行为)
// new 失败默认抛 bad_alloc 异常(也可以用 nothrow 不抛)
int* huge = new(nothrow) int[10000000000LL];
if (huge == nullptr) {
cout << "分配失败" << endl;
}
return 0;
}关键规则:
- new 配 delete,new[] 配 delete[]:数组必须用
delete[],用delete是未定义行为。 - 每个 new 必须对应一个 delete:漏一个就泄漏。
- 不能重复 delete:同块内存 delete 两次程序崩溃。
- new 失败默认抛 bad_alloc 异常(
new(nothrow)不抛返回 nullptr)。
三、内存泄漏与各种坑
手动内存管理容易出 5 类典型 bug:
#include <iostream>
using namespace std;
void leak() {
int* p = new int(42);
// 函数返回,p 这个局部变量(指针本身)销毁
// 但它指向的堆内存还在,且再也没人能引用到它了
// —— 这就是"内存泄漏"
}
void alsoLeak() {
int* p = new int(42);
p = new int(100); // 把第一个 int "丢了",又没 delete
delete p; // 只删了第二个
}
void doubleFree() {
int* p = new int(42);
delete p;
// delete p; // 错误:重复释放,程序崩溃
}
void useAfterFree() {
int* p = new int(42);
delete p;
// cout << *p << endl; // 错误:使用已释放内存,未定义行为
}
void arrayMismatch() {
int* arr = new int[10];
// delete arr; // 错误:数组应该用 delete[]
delete[] arr; // 正确
}
int main() {
leak();
alsoLeak();
return 0;
}- 内存泄漏:分配了不释放。短期程序看不出,长期运行(如服务器)会内存耗尽崩溃。
- 重复释放:同块内存 delete 两次,程序崩溃(通常抛 Aborted)。
- use-after-free:用了已 delete 的指针。未定义行为,可能让黑客利用。
- new[] vs delete 不匹配:数组用 delete 不用 delete[]。
- 异常安全问题:new 完到 delete 之间抛异常,delete 永远不会执行,泄漏。
这些坑让 C++ 名声"难"。但现代 C++ 用下面的方法基本能根除它们。
四、RAII:资源管理的核心思想
RAII(Resource Acquisition Is Initialization)是 C++ 最重要的设计哲学。核心思想:
把资源(内存/文件/锁)绑定到对象的生命周期——构造时获取,析构时释放。
#include <iostream>
#include <fstream>
#include <mutex>
using namespace std;
// RAII:Resource Acquisition Is Initialization
// 核心思想:把资源(内存/文件/锁)绑定到对象的生命周期
// 构造时获取,析构时释放
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t n) : data(new int[n]), size(n) {}
~Buffer() { delete[] data; } // 析构自动释放!
int& operator[](size_t i) { return data[i]; }
};
void safeFunc() {
Buffer buf(100); // 构造时分配
buf[0] = 42;
// 即便这里抛异常、提前 return,析构函数也会被调用
// buf 自动释放内存——不会泄漏
}
// STL 已经按 RAII 实现:
// - vector、string:管理动态数组/字符缓冲
// - fstream:管理文件句柄
// - lock_guard / unique_lock:管理互斥锁
// - 智能指针:管理堆对象
void safeFile() {
ifstream file("data.txt"); // 构造时打开
if (!file) return;
// 读文件...
// 函数返回,file 析构自动关闭
}RAII 的优势:
- 异常安全:即便抛异常,栈展开(stack unwinding)也会调用析构函数,资源不会泄漏。
- 自动管理:不用手动 close/delete,代码更简洁。
- 局部性:资源的获取和释放在同一个类的构造/析构里,清晰可维护。
STL 容器(vector、string)、fstream、智能指针全部基于 RAII——这就是为什么现代 C++ 程序员几乎不写裸 new/delete。
五、unique_ptr:独占所有权(C++11)
unique_ptr 是智能指针的默认选择。它独占一个对象,不能拷贝,只能移动:
#include <iostream>
#include <memory>
using namespace std;
class Resource {
public:
Resource() { cout << "Resource 构造" << endl; }
~Resource() { cout << "Resource 析构" << endl; }
void use() { cout << "使用 Resource" << endl; }
};
int main() {
// unique_ptr:独占所有权,不能拷贝,只能移动
// 出作用域自动 delete
auto p = make_unique<Resource>(); // C++14 推荐
// 等价于 unique_ptr<Resource> p(new Resource);
p->use();
(*p).use();
// 转移所有权(只能移动)
unique_ptr<Resource> p2 = move(p);
// p 现在是 nullptr
if (!p) cout << "p 已被搬空" << endl;
p2->use();
// 出作用域时 p2 自动销毁 Resource
return 0;
}unique_ptr 的特点:
- 独占:同一时刻只有一个 unique_ptr 拥有对象。
- 零开销:和裸指针一样大、一样快(没有引用计数)。
- 出作用域自动 delete:RAII 自动管理。
- 不能拷贝,只能 move:转移所有权后原指针变 nullptr。
- 推荐 make_unique(C++14):比
unique_ptr<T>(new T)更安全(避免某些异常泄漏场景)。
六、shared_ptr:共享所有权
shared_ptr 用引用计数实现共享所有权——多个 shared_ptr 指向同一对象,最后一个销毁时对象才析构:
#include <iostream>
#include <memory>
using namespace std;
class Foo {
public:
Foo() { cout << "Foo 构造" << endl; }
~Foo() { cout << "Foo 析构" << endl; }
};
int main() {
// shared_ptr:共享所有权,引用计数
// 多个 shared_ptr 可以指向同一个对象
// 最后一个销毁时,对象才被 delete
auto p1 = make_shared<Foo>(); // 引用计数 = 1
cout << "count: " << p1.use_count() << endl; // 1
{
shared_ptr<Foo> p2 = p1; // 拷贝,引用计数 = 2
cout << "count: " << p1.use_count() << endl; // 2
} // p2 销毁,引用计数 = 1
cout << "count: " << p1.use_count() << endl; // 1
// 函数结束,p1 销毁,引用计数 = 0,Foo 被析构
// ⚠️ shared_ptr 的坑:循环引用
// 两个对象互相用 shared_ptr 指向对方,引用计数永远不为 0
// 导致内存泄漏。解决:用 weak_ptr 打破循环
return 0;
}shared_ptr 的注意点:
- 引用计数有线程开销:每次拷贝/析构都要原子操作,比 unique_ptr 慢。
- 循环引用:两个对象互相用 shared_ptr 指向对方,引用计数永远不为 0,内存泄漏。这是 shared_ptr 最大坑。
- 不是万能:能用 unique_ptr 就用 unique_ptr,需要共享才用 shared_ptr。
- 推荐 make_shared:比
shared_ptr<T>(new T)更快(一次分配,而不是两次)。
七、weak_ptr:打破循环引用
weak_ptr 是 shared_ptr 的"观察者",不增加引用计数,主要用于打破循环引用:
#include <iostream>
#include <memory>
using namespace std;
class Node {
public:
shared_ptr<Node> next;
weak_ptr<Node> prev; // 用 weak_ptr 避免循环引用
~Node() { cout << "Node 析构" << endl; }
};
int main() {
// weak_ptr:不增加引用计数,是 shared_ptr 的"观察者"
auto a = make_shared<Node>();
auto b = make_shared<Node>();
a->next = b; // b 的引用计数 = 2
b->prev = a; // a 的引用计数 = 2(weak 不增加)
// a 离开作用域:引用计数 2->1(因为 prev 是 weak,不影响)
// b 离开作用域:引用计数 2->1
// 但 a->next 还指向 b,所以 b 还活着
// 等 a 销毁,a->next 释放,b 才销毁
// weak_ptr 使用前要 lock() 提升为 shared_ptr
weak_ptr<Node> w = a;
if (auto sp = w.lock()) { // 提升成功,对象还活着
cout << "对象还在" << endl;
}
a->next = nullptr; // 显式断开,a 销毁后 b 也会销毁
return 0;
}
// 智能指针选型:
// - 独占资源:用 unique_ptr(默认选择)
// - 共享资源:用 shared_ptr
// - 观察者:用 weak_ptr(不影响生命周期)weak_ptr 的特点:
- 不影响对象生命周期:不增加引用计数。
- 不能直接访问对象:要先
lock()提升为 shared_ptr(对象可能已经销毁,提升返回空)。 - 典型用途:双向链表的 prev 指针、缓存、观察者模式。
八、智能指针选型
- unique_ptr:独占资源。默认选择,99% 的场景。
- shared_ptr:共享资源(多个所有者)。需要明确"共享所有权"时才用。
- weak_ptr:观察者,不影响生命周期。打破 shared_ptr 循环引用。
- 裸指针 T*:仅"借用",不拥有(如函数参数)。生命周期由别处管理。
九、现代 C++ 实践
现代 C++(C++11+)的内存管理原则:
// 现代 C++ 内存管理原则:
// 1. 优先用栈对象,不用堆对象
class Good {
std::string name; // 栈上的 string,自动管理
std::vector<int> data; // 栈上的 vector,自动管理
};
// 2. 必须用堆时,用智能指针
#include <memory>
auto p = std::make_unique<Good>();
// 3. 几乎不用裸 new/delete
// 反面教材:
// int* p = new int(42);
// ... 复杂逻辑 ...
// forget to delete -> 内存泄漏
// 4. STL 容器自带内存管理
// vector/map/string 自己管内存,你只管用
// 5. 函数参数:
// - 只读:const T& 或 T(小类型)
// - 可修改:T&
// - 转移所有权:unique_ptr<T>/shared_ptr<T>
// - 借用但不拥有:T*(原始指针,但调用者仍负责生命周期)总结口诀:
- 默认用栈对象:能用栈就用栈,自动管理。
- 必须用堆就用 unique_ptr:几乎覆盖所有场景。
- 需要共享再用 shared_ptr:有性能开销,谨慎用。
- 不要写裸 new/delete:封装到 RAII 类里。
- STL 容器自带内存管理:你只管用,内存它管。
小结
这一章你掌握了 C++ 内存管理:栈与堆的区别、new/delete 与各种坑、RAII 设计哲学、智能指针三件套(unique_ptr/shared_ptr/weak_ptr)、现代 C++ 几乎不用裸 new/delete 的实践。下一篇我们看异常处理——怎么处理程序运行时的错误。
← 上一篇 C++ STL
下一篇 C++ 异常 →