Effective C++ 条款16:成对使用 new 和 delete 时要采取相同形式
🎯 核心观点 :如果你在 new 表达式中使用了
[],必须在对应的 delete 表达式中也使用[];如果在 new 中未使用[],delete 中也一定不要使用[]。
一、问题的引入
在 C++ 中,动态内存管理是最基础也最危险的操作之一。很多开发者都知道 new 和 delete 要成对使用,但却容易忽视一个关键细节------形式必须严格匹配。
来看一段看似无害的代码:
cpp
#include <iostream>
class ResourceHolder {
public:
ResourceHolder() { std::cout << "构造函数\n"; }
~ResourceHolder() { std::cout << "析构函数\n"; }
};
int main() {
// 分配单个对象
ResourceHolder* pSingle = new ResourceHolder();
// 分配对象数组
ResourceHolder* pArray = new ResourceHolder[3];
// 错误示范 1:用 delete[] 释放单个对象
delete[] pSingle; // ❌ 未定义行为!
// 错误示范 2:用 delete 释放数组
delete pArray; // ❌ 内存泄漏 + 未定义行为!
return 0;
}
上面的代码能编译通过,但运行结果却是灾难性的。为什么会出现这种情况?我们需要从 new 和 delete 的底层机制说起。
二、原理深度剖析
2.1 new 的背后发生了什么?
当你写下 new ResourceHolder() 时,实际上发生了两件事:
- 内存分配 :调用
operator new分配足够的原始内存。 - 构造函数调用:在该内存上调用对象的构造函数。
而当你写下 new ResourceHolder[3] 时:
- 内存分配 :调用
operator new[]分配内存。注意,这里分配的内存通常比 3 个对象的大小还要多一些------额外的空间用于存储数组元素的个数。 - 循环调用构造函数:依次调用 3 次构造函数。
2.2 delete 的背后又发生了什么?
delete 同样分为两步:
- 调用析构函数:对对象调用析构函数。
- 释放内存 :调用
operator delete将内存归还给系统。
关键区别在这里:
| 操作 | 内存布局特点 | delete 行为 |
|---|---|---|
new(单个对象) |
仅分配对象本身大小的内存 | delete 直接释放该内存 |
new[](数组) |
额外存储数组长度信息 | delete[] 先读取长度,再循环调用析构函数 |
2.3 不匹配使用的后果
场景一:new 后用 delete[]
cpp
ResourceHolder* p = new ResourceHolder(); // 分配单个对象
delete[] p; // 灾难!
运行时看到 delete[],会尝试从内存块的前面几个字节读取"数组长度"。但这段内存是用 new 分配的,前面并没有存储长度信息------读取到的是垃圾值。结果可能是:
- 调用析构函数的次数完全错误
- 内存释放的地址偏移错误
- 未定义行为(Undefined Behavior)
场景二:new[] 后用 delete
cpp
ResourceHolder* p = new ResourceHolder[3]; // 分配数组
delete p; // 严重错误!
这里的问题更加隐蔽且危险:
delete只会调用一次析构函数 ,而不是 3 次。其余 2 个对象的析构函数永远不会执行,如果它们持有资源(如文件句柄、网络连接、锁),就会造成资源泄漏。delete释放的内存地址是错误的------它没有考虑到数组长度信息占用的偏移,可能导致堆损坏。
⚠️ 注意 :对于内置类型(如
int、double),不匹配使用可能不会立即崩溃,因为内置类型没有析构函数。但这仍然是未定义行为,在不同编译器或运行环境下可能表现完全不同!
三、代码示例与验证
3.1 正确用法示范
cpp
#include <iostream>
#include <string>
class FileHandler {
private:
std::string filename_;
bool isOpen_;
public:
explicit FileHandler(const std::string& name)
: filename_(name), isOpen_(true) {
std::cout << "[构造] 打开文件: " << filename_ << "\n";
}
~FileHandler() {
if (isOpen_) {
std::cout << "[析构] 关闭文件: " << filename_ << "\n";
isOpen_ = false;
}
}
// 禁用拷贝,允许移动(现代 C++ 实践)
FileHandler(const FileHandler&) = delete;
FileHandler& operator=(const FileHandler&) = delete;
};
int main() {
std::cout << "=== 单个对象 ===\n";
FileHandler* single = new FileHandler("config.txt");
delete single; // ✅ 正确:new 对应 delete
std::cout << "\n=== 对象数组 ===\n";
FileHandler* array = new FileHandler[3] {
FileHandler("log1.txt"),
FileHandler("log2.txt"),
FileHandler("log3.txt")
};
delete[] array; // ✅ 正确:new[] 对应 delete[]
return 0;
}
3.2 typedef 陷阱
这是实际开发中非常容易踩的坑:
cpp
typedef ResourceHolder ResourceArray[4]; // ResourceArray 是一个包含 4 个元素的数组类型
// 现在 new 返回的是数组!
ResourceArray* p = new ResourceArray; // 等价于 new ResourceHolder[4]
// ❌ 错误!看起来像单个对象
delete p;
// ✅ 正确!虽然类型名里没有 [],但实际是数组
delete[] p;
💡 最佳实践 :尽量避免对数组类型使用 typedef。如果必须使用,务必在代码注释中明确说明,并考虑使用
std::array或std::vector替代。
3.3 现代 C++ 的解决方案
在 C++11 及以后,强烈建议使用智能指针和容器来避免手动管理内存:
cpp
#include <memory>
#include <vector>
// ✅ 使用 unique_ptr 管理单个对象
std::unique_ptr<ResourceHolder> safeSingle = std::make_unique<ResourceHolder>();
// ✅ 使用 vector 管理对象数组(最推荐)
std::vector<ResourceHolder> safeArray;
safeArray.emplace_back();
safeArray.emplace_back();
safeArray.emplace_back();
// ✅ 如果确实需要动态数组,使用 unique_ptr 的数组特化
std::unique_ptr<ResourceHolder[]> safeArrayPtr(new ResourceHolder[3]);
// 自动调用 delete[],无需手动管理
四、实际应用场景
4.1 场景:游戏引擎中的资源管理
在游戏开发中,经常需要动态创建大量游戏对象:
cpp
class GameEntity {
public:
virtual ~GameEntity() = default;
virtual void update() = 0;
};
class Enemy : public GameEntity { /* ... */ };
class Player : public GameEntity { /* ... */ };
// 危险的传统做法
void spawnEnemies(int count) {
GameEntity** enemies = new GameEntity*[count]; // 指针数组
for (int i = 0; i < count; ++i) {
enemies[i] = new Enemy();
}
// ... 使用 enemies ...
// ❌ 极易出错:需要先 delete 每个元素,再 delete[] 数组
for (int i = 0; i < count; ++i) {
delete enemies[i]; // 释放每个 Enemy 对象
}
delete[] enemies; // 释放指针数组
}
// ✅ 现代 C++ 做法
void spawnEnemiesSafe(int count) {
std::vector<std::unique_ptr<GameEntity>> enemies;
for (int i = 0; i < count; ++i) {
enemies.push_back(std::make_unique<Enemy>());
}
// ... 使用 enemies ...
// 完全自动管理,无需手动 delete
}
4.2 场景:网络服务器中的缓冲区管理
cpp
class NetworkBuffer {
private:
char* data_;
size_t size_;
public:
explicit NetworkBuffer(size_t size) : size_(size) {
data_ = new char[size]; // 分配原始字节数组
}
~NetworkBuffer() {
delete[] data_; // ✅ 必须用 delete[]!
}
// 禁用拷贝,实现移动语义
NetworkBuffer(const NetworkBuffer&) = delete;
NetworkBuffer& operator=(const NetworkBuffer&) = delete;
NetworkBuffer(NetworkBuffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
char* data() { return data_; }
size_t size() const { return size_; }
};
五、常见误区与排查
| 误区 | 真相 |
|---|---|
| "内置类型不需要匹配" | 仍然是未定义行为,只是可能不立即崩溃 |
| "编译器会帮我检查" | 编译器通常不会报错,这是运行时问题 |
| "delete nullptr 是安全的" | 是的,但前提是匹配形式 |
| "智能指针完全不需要关心" | unique_ptr<T> 和 unique_ptr<T[]> 是不同的类型! |
调试技巧
如果你怀疑存在 new/delete 不匹配的问题,可以使用以下工具:
- AddressSanitizer (ASan) :编译时加上
-fsanitize=address,可以检测大部分内存错误。 - Valgrind (Linux):
valgrind --tool=memcheck ./your_program - Visual Studio 调试器:启用"页堆"(Page Heap)检测。
六、总结
| 规则 | 说明 |
|---|---|
new → delete |
单个对象的标准配对 |
new[] → delete[] |
数组对象的标准配对 |
| 不匹配 = 未定义行为 | 可能导致内存泄漏、堆损坏、程序崩溃 |
| 优先使用现代 C++ | std::unique_ptr、std::vector、std::make_unique |
📌 记住:C++ 不会在你犯错时温柔地提醒你。new 和 delete 的形式匹配是程序员的责任,也是专业 C++ 开发的基本素养。
七、延伸阅读
- Effective C++ 条款13:以对象管理资源
- Effective C++ 条款17:以独立语句将 newed 对象置入智能指针
- C++ Core Guidelines:优先使用 RAII,避免显式 new/delete
如果这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬!你的支持是我持续创作的动力!