C++ 动态内存
在 C++ 中,“动态内存”通常指在运行时从堆(heap)上分配和释放的内存空间,它与栈上分配的自动变量相对。合理使用动态内存可以让程序在运行时根据需要灵活调整资源,但不当使用也极易导致内存泄漏、悬空指针和资源竞用等问题。下面从基本机制到高级管理分章介绍。
1. 原始操作:new
/ delete
1.1 单对象分配与释放
// 分配一个 int,初始值为 42
int* p = new int(42);
// 使用
std::cout << *p << "\n"; // 输出 42
// 释放
delete p;
p = nullptr; // 避免成为悬空指针
1.2 数组分配与释放
// 分配一个长度为 N 的 double 数组
size_t N = 100;
double* arr = new double[N]; // 元素未初始化
// 使用 arr[i]
// 释放时必须用 delete[]
delete[] arr;
arr = nullptr;
- 注意:
new[]
必须配对delete[]
,否则可能只调用首元素析构,或产生未定义行为。
2. C 风格接口:malloc
/ free
#include <cstdlib>
int* q = static_cast<int*>(std::malloc(sizeof(int) * N));
if (!q) throw std::bad_alloc(); // malloc 失败返回 nullptr
// 使用 q[i]
// 释放
std::free(q);
q = nullptr;
malloc
/free
属于 C 标准库,不调用构造/析构函数;在 C++ 中一般不推荐直接使用,除非与 C 代码互操作。
3. 智能指针(Smart Pointer)
为了避免手动 new
/delete
带来的管理负担,C++11 起引入智能指针,通过 RAII 保证资源自动释放。
3.1 std::unique_ptr
- 独占所有权:一个
unique_ptr
拥有某块内存,离开作用域或被重置时自动delete
。 - 禁止拷贝,可 移动。
#include <memory>
auto up = std::make_unique<MyClass>(args…);
// 等价于: unique_ptr<MyClass> up(new MyClass(args…));
up->doSomething();
// 转移所有权
auto up2 = std::move(up);
if (!up) {
// up 现在为空
}
// 离开作用域时自动 delete up2 所指对象
3.2 std::shared_ptr
/ std::weak_ptr
- 共享所有权:多个
shared_ptr
可以指向同一对象,通过引用计数管理生命周期;最后一个shared_ptr
销毁时释放资源。 - 配合
weak_ptr
:解决循环引用导致的泄漏问题。
#include <memory>
auto sp1 = std::make_shared<MyClass>(args…);
{
auto sp2 = sp1; // 引用计数 +1
// …
} // sp2 离开作用域,计数 -1
// 若要观察对象但不增加计数,用 weak_ptr
std::weak_ptr<MyClass> wp = sp1;
if (auto sp3 = wp.lock()) {
sp3->doSomething();
} else {
// 对象已被销毁
}
shared_ptr
对象大小稍大(内部含控制块指针),并有原子操作开销。
4. RAII 与容器封装
- 尽量使用标准容器(
std::vector
,std::string
,std::map
…)来管理动态分配的数据,容器析构时会自动释放内部资源。 - 对于非内存资源(文件句柄、锁等),也可封装成对象,在构造时获取资源、析构时释放(RAII 原则)。
// 例:将 FILE* 封装
class File {
FILE* f_;
public:
File(const char* path, const char* mode)
: f_(std::fopen(path, mode)) {
if (!f_) throw std::runtime_error("open failed");
}
~File() {
if (f_) std::fclose(f_);
}
// 禁止拷贝,允许移动
File(const File&) = delete;
File(File&& o) noexcept : f_(o.f_) { o.f_ = nullptr; }
FILE* get() const { return f_; }
};
5. 常见错误与防范
问题类型 | 原因 | 防范措施 |
---|---|---|
内存泄漏 | new 后未 delete | 用智能指针或容器管理;代码审计 |
悬空指针 | delete 后仍使用指针 | delete 后置空指针;尽量减少裸指针 |
重复释放 | 同一指针多次 delete | 统一所有权;使用智能指针 |
越界访问 | 对数组或缓冲区下标检查不足 | 使用容器的 .at() ;手动检查边界 |
对象部分析构 | new[] vs delete 、new vs delete[] 匹配错误 | 严格配对;尽量避免裸数组 |
循环引用 | 两个或以上 shared_ptr 互相引用,计数永不为零 | 用 weak_ptr 打破循环 |
6. 自定义分配器
在性能敏感或需要特殊对齐、内存池策略时,可为容器或对象提供自定义分配器。
template<typename T>
struct MyAllocator {
using value_type = T;
T* allocate(std::size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t) noexcept {
::operator delete(p);
}
};
std::vector<int, MyAllocator<int>> v;
- 自定义分配器需符合 Allocator 概念,包含
allocate
、deallocate
、value_type
等。
7. 调试与工具
- Valgrind / AddressSanitizer:检测泄漏、越界、未初始化读写等。
- Static Analyzer:Clang-Tidy、Visual Studio Code Analysis 可静态检测潜在内存错误。
- 智能指针覆盖率:在 code review 中优先考虑智能指针模式。
小结
- 原始方式:
new
/delete
、malloc
/free
,但易错需谨慎配对。 - 智能指针:
unique_ptr
、shared_ptr
、weak_ptr
,结合 RAII 自动管控生命周期。 - 容器优先:优先使用 STL 容器管理动态内存,减少手动操作。
- 资源封装:将所有资源(内存、文件、锁)封装在对象内,析构时自动释放。
- 检测工具:借助动态工具和静态分析,及时发现内存相关错误。
掌握以上策略,便能在 C++ 项目中安全、高效地使用动态内存。