动态内存管理是 C++ 编程中非常重要且容易出错的部分。
1. 动态内存与智能指针
为什么需要动态内存?
- 程序在运行时才知道需要多少对象
- 程序需要在多个对象间共享数据
- 生存期需要跨越函数调用
智能指针类型
C++11 引入了三种智能指针,都在 <memory>
头文件中:
std::shared_ptr<T>
- 共享所有权
cpp
#include <memory>
std::shared_ptr<std::string> p1 = std::make_shared<std::string>("hello");
std::shared_ptr<std::string> p2 = p1; // 拷贝,引用计数+1
std::cout << p1.use_count(); // 输出引用计数:2
特点:
- 多个
shared_ptr
可以指向同一个对象 - 使用引用计数管理内存
- 当最后一个
shared_ptr
被销毁时,对象自动释放
std::unique_ptr<T>
- 独占所有权
cpp
std::unique_ptr<int> p1(new int(42));
// std::unique_ptr<int> p2 = p1; // 错误!不能拷贝
std::unique_ptr<int> p3 = std::move(p1); // 可以移动
特点:
- "独占"所指向的对象
- 不支持普通的拷贝和赋值
- 支持移动语义
- 开销小,效率高
std::weak_ptr<T>
- 弱引用
cpp
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp; // 创建弱引用
if (std::shared_ptr<int> np = wp.lock()) { // 尝试提升为 shared_ptr
// 使用 np
std::cout << *np << std::endl;
}
用途:
- 解决
shared_ptr
的循环引用问题 - 不控制对象生存期
- 需要调用
lock()
来获取可用的shared_ptr
2. 直接内存管理(原始指针)
new
和 delete
操作符
cpp
// 动态分配单个对象
int *pi = new int{}; // 值初始化为0
int *pi2 = new int(1024); // 直接初始化
std::string *ps = new std::string(10, '9');
// 动态分配数组
int *pia = new int[10](); // 10个值初始化为0的int
// 一定要配对释放!
delete pi;
delete pi2;
delete ps;
delete[] pia;
// 释放后设为nullptr避免悬空指针
pi = nullptr;
pi2 = nullptr;
ps = nullptr;
pia = nullptr;
常见陷阱
cpp
// 1. 忘记delete导致内存泄漏
void leak() {
int* p = new int(42);
// 忘记 delete p;
}
// 2. 使用已经delete的内存
int* p = new int(42);
delete p;
*p = 10; // 未定义行为!
// 3. 对同一块内存delete两次
delete p;
delete p; // 未定义行为!
3. 智能指针的实现原理
引用计数机制
cpp
// shared_ptr 的简化实现概念
template<typename T>
class shared_ptr {
private:
T* ptr;
int* count; // 引用计数
public:
// 构造函数
shared_ptr(T* p) : ptr(p), count(new int(1)) {}
// 拷贝构造函数
shared_ptr(const shared_ptr& other)
: ptr(other.ptr), count(other.count) {
++(*count);
}
// 析构函数
~shared_ptr() {
if (--(*count) == 0) {
delete ptr;
delete count;
}
}
};
4. 动态数组
使用 new
和 delete[]
cpp
// 分配动态数组
int* arr = new int[size];
// 初始化动态数组
int* arr2 = new int[size]{1, 2, 3}; // 前三个元素初始化,其余值初始化
// 释放数组
delete[] arr;
delete[] arr2;
使用 std::unique_ptr
管理动态数组
cpp
#include <memory>
// unique_ptr 管理数组
std::unique_ptr<int[]> up(new int[10]);
// 可以直接使用下标访问
for (size_t i = 0; i < 10; ++i) {
up[i] = i;
}
// 自动调用 delete[],不需要手动释放
使用 std::vector
(通常更好)
cpp
#include <vector>
// 通常比动态数组更好
std::vector<int> vec(10); // 10个元素,值初始化为0
// 更安全,功能更丰富,自动管理内存
5. allocator
类
为什么需要 allocator
?
new
将内存分配和对象构造绑定在一起delete
将内存释放和对象析构绑定在一起- 有时我们需要分离这两个操作
使用 allocator
cpp
#include <memory>
std::allocator<std::string> alloc;
// 分配未构造的内存
auto const p = alloc.allocate(10); // 分配10个string的内存
// 在内存中构造对象
auto q = p;
alloc.construct(q++); // 构造空string
alloc.construct(q++, 10, 'c'); // 构造 "cccccccccc"
alloc.construct(q++, "hi"); // 构造 "hi"
// 使用对象
std::cout << *p << std::endl; // 输出第一个string
// 析构对象
while (q != p) {
alloc.destroy(--q);
}
// 释放内存
alloc.deallocate(p, 10);
6. 文本查询程序示例
这一章最后通过一个文本查询程序综合运用了动态内存管理的知识:
程序功能
- 读取文本文件
- 允许用户查询单词出现的行
- 使用
shared_ptr
共享数据
关键设计点
cpp
class QueryResult; // 前向声明
class TextQuery {
public:
using line_no = std::vector<std::string>::size_type;
TextQuery(std::ifstream&);
QueryResult query(const std::string&) const;
private:
// 使用 shared_ptr 让多个 QueryResult 共享数据
std::shared_ptr<std::vector<std::string>> file;
std::map<std::string, std::shared_ptr<std::set<line_no>>> wm;
};
7. 重点与难点
必须掌握的概念
- RAII(Resource Acquisition Is Initialization):资源获取即初始化
- 引用计数的原理和实现
- 所有权语义:独占 vs 共享
- 移动语义在智能指针中的应用
常见错误
cpp
// 错误:混合使用智能指针和原始指针
void process() {
int* x = new int(10);
std::shared_ptr<int> sp1(x);
std::shared_ptr<int> sp2(x); // 错误!两个独立的shared_ptr管理同一内存
// 正确做法:
std::shared_ptr<int> sp1 = std::make_shared<int>(10);
std::shared_ptr<int> sp2 = sp1; // 共享所有权
}
最佳实践
- 优先使用智能指针而不是原始指针
- 优先使用
std::make_shared
而不是直接new
- 使用
std::unique_ptr
默认情况下 - 只有需要共享所有权时才使用
std::shared_ptr
- 使用
std::weak_ptr
打破循环引用
这一章是 C++ 现代编程风格的基础,掌握好动态内存管理对于写出安全、高效的 C++ 程序至关重要!