跳到主要内容

C++ new和delete重载

什么是new和delete运算符重载?

在C++中,newdelete是用于动态内存分配和释放的运算符。当我们使用new创建对象时,它会调用两个操作:分配内存调用构造函数。类似地,delete也执行两个操作:调用析构函数释放内存

重载这些运算符可以让我们自定义对象的内存分配和释放策略,实现更加灵活高效的内存管理。

备注

重载newdelete运算符允许我们:

  • 跟踪内存分配
  • 实现内存池
  • 处理特定的内存分配需求
  • 提高程序的性能

为什么需要重载new和delete?

重载newdelete的常见原因包括:

  1. 性能优化:标准的newdelete可能不够高效,特别是对于频繁分配和释放小对象的情况。
  2. 内存泄漏检测:通过添加跟踪代码,可以检测内存泄漏。
  3. 特殊内存需求:某些应用可能需要对象在特定的内存区域分配。
  4. 内存对齐:为了性能或硬件要求,可能需要特定的内存对齐方式。
  5. 实现内存池:减少内存碎片,提高分配和释放速度。

new和delete运算符重载的形式

全局重载

重载全局的newdelete会影响所有使用这些运算符的代码:

cpp
// 全局重载new
void* operator new(size_t size) {
std::cout << "全局重载new: 分配 " << size << " 字节" << std::endl;
void* ptr = malloc(size);
if (!ptr) throw std::bad_alloc();
return ptr;
}

// 全局重载delete
void operator delete(void* ptr) noexcept {
std::cout << "全局重载delete: 释放内存" << std::endl;
free(ptr);
}

类级别重载

在类内部重载newdelete只会影响该类及其派生类的对象:

cpp
class MyClass {
public:
MyClass() {
std::cout << "MyClass构造函数调用" << std::endl;
}

~MyClass() {
std::cout << "MyClass析构函数调用" << std::endl;
}

// 重载类的new运算符
void* operator new(size_t size) {
std::cout << "MyClass::operator new: 分配 " << size << " 字节" << std::endl;
void* ptr = malloc(size);
if (!ptr) throw std::bad_alloc();
return ptr;
}

// 重载类的delete运算符
void operator delete(void* ptr) noexcept {
std::cout << "MyClass::operator delete: 释放内存" << std::endl;
free(ptr);
}
};

重载new和delete运算符的完整示例

下面是一个完整的示例,展示如何重载类的newdelete运算符:

cpp
#include <iostream>
#include <cstdlib>
#include <new>

class MemoryTracker {
public:
static size_t allocatedMemory;

// 重载类的new运算符
void* operator new(size_t size) {
allocatedMemory += size;
std::cout << "正在分配 " << size << " 字节内存, 当前总内存: "
<< allocatedMemory << " 字节" << std::endl;

void* ptr = malloc(size);
if (!ptr) throw std::bad_alloc();
return ptr;
}

// 重载类的delete运算符
void operator delete(void* ptr, size_t size) noexcept {
allocatedMemory -= size;
std::cout << "正在释放 " << size << " 字节内存, 剩余总内存: "
<< allocatedMemory << " 字节" << std::endl;

free(ptr);
}

// 构造函数
MemoryTracker() {
std::cout << "MemoryTracker对象已创建" << std::endl;
}

// 析构函数
~MemoryTracker() {
std::cout << "MemoryTracker对象已销毁" << std::endl;
}
};

// 初始化静态成员变量
size_t MemoryTracker::allocatedMemory = 0;

int main() {
std::cout << "程序开始运行" << std::endl;

// 使用重载的new创建对象
MemoryTracker* tracker1 = new MemoryTracker();
MemoryTracker* tracker2 = new MemoryTracker();

std::cout << "创建了两个对象" << std::endl;

// 使用重载的delete删除对象
delete tracker1;
delete tracker2;

std::cout << "程序结束运行" << std::endl;
return 0;
}

输出结果

程序开始运行
正在分配 1 字节内存, 当前总内存: 1 字节
MemoryTracker对象已创建
正在分配 1 字节内存, 当前总内存: 2 字节
MemoryTracker对象已创建
创建了两个对象
MemoryTracker对象已销毁
正在释放 1 字节内存, 剩余总内存: 1 字节
MemoryTracker对象已销毁
正在释放 1 字节内存, 剩余总内存: 0 字节
程序结束运行
警告

在上面的例子中,size参数通常会大于1字节,具体大小取决于编译器和对象的实际大小。简化后显示为1字节仅用于演示目的。

重载数组版本的new和delete

除了单个对象版本的newdelete,C++还提供了数组版本的new[]delete[]运算符:

cpp
class ArrayMemoryTracker {
public:
// 重载new[]运算符
void* operator new[](size_t size) {
std::cout << "数组分配: 正在分配 " << size << " 字节内存" << std::endl;
void* ptr = malloc(size);
if (!ptr) throw std::bad_alloc();
return ptr;
}

// 重载delete[]运算符
void operator delete[](void* ptr) noexcept {
std::cout << "数组释放: 正在释放内存" << std::endl;
free(ptr);
}

ArrayMemoryTracker() {
std::cout << "ArrayMemoryTracker对象已创建" << std::endl;
}

~ArrayMemoryTracker() {
std::cout << "ArrayMemoryTracker对象已销毁" << std::endl;
}
};

使用示例:

cpp
// 创建对象数组
ArrayMemoryTracker* array = new ArrayMemoryTracker[5];

// 删除对象数组
delete[] array;

输出结果

数组分配: 正在分配 xx 字节内存
ArrayMemoryTracker对象已创建
ArrayMemoryTracker对象已创建
ArrayMemoryTracker对象已创建
ArrayMemoryTracker对象已创建
ArrayMemoryTracker对象已创建
ArrayMemoryTracker对象已销毁
ArrayMemoryTracker对象已销毁
ArrayMemoryTracker对象已销毁
ArrayMemoryTracker对象已销毁
ArrayMemoryTracker对象已销毁
数组释放: 正在释放内存

重载new和delete的高级参数

C++还允许重载带有额外参数的newdelete运算符,称为"定位new"(placement new):

cpp
class PlacementExample {
public:
// 标准的new
void* operator new(size_t size) {
std::cout << "普通new被调用" << std::endl;
return ::operator new(size);
}

// 定位new,在指定内存位置创建对象
void* operator new(size_t size, void* ptr) {
std::cout << "定位new被调用" << std::endl;
return ptr;
}

// 带自定义参数的new
void* operator new(size_t size, const char* file, int line) {
std::cout << "调试信息: 在文件 " << file << ", 行 "
<< line << " 分配了 " << size << " 字节" << std::endl;
return ::operator new(size);
}

// 定位delete
void operator delete(void* ptr, void*) noexcept {
std::cout << "定位delete被调用" << std::endl;
// 不需要释放内存,因为内存不是由定位new分配的
}

// 带自定义参数的delete
void operator delete(void* ptr, const char* file, int line) noexcept {
std::cout << "调试信息: 在文件 " << file << ", 行 "
<< line << " 释放了内存" << std::endl;
::operator delete(ptr);
}

// 标准的delete
void operator delete(void* ptr) noexcept {
std::cout << "普通delete被调用" << std::endl;
::operator delete(ptr);
}
};

使用定位new示例:

cpp
// 分配一块内存用于定位new
char buffer[sizeof(PlacementExample)];

// 使用定位new在buffer中创建对象
PlacementExample* obj1 = new(buffer) PlacementExample();

// 使用自定义参数的new
PlacementExample* obj2 = new("main.cpp", 150) PlacementExample();

// 显式调用析构函数,但不释放内存(定位new的情况)
obj1->~PlacementExample();

// 使用delete释放obj2的内存
delete obj2;

实际应用案例:自定义内存池

下面是一个简化的内存池实现,展示如何使用重载的newdelete提高小对象的内存分配效率:

cpp
#include <iostream>
#include <vector>
#include <cstddef>
#include <new>

class MemoryPool {
private:
static constexpr size_t BLOCK_SIZE = 4096; // 每块内存大小
static constexpr size_t OBJECT_SIZE = 32; // 单个对象大小
static constexpr size_t OBJECTS_PER_BLOCK = BLOCK_SIZE / OBJECT_SIZE;

struct Block {
char memory[BLOCK_SIZE];
bool used[OBJECTS_PER_BLOCK] = {false};
};

std::vector<Block*> blocks;

public:
MemoryPool() {
std::cout << "内存池已创建" << std::endl;
}

~MemoryPool() {
for (Block* block : blocks) {
delete block;
}
std::cout << "内存池已销毁,释放了 " << blocks.size() << " 块内存" << std::endl;
}

void* allocate(size_t size) {
if (size > OBJECT_SIZE) {
// 过大的对象使用标准分配
return ::operator new(size);
}

// 在现有块中查找可用空间
for (Block* block : blocks) {
for (size_t i = 0; i < OBJECTS_PER_BLOCK; ++i) {
if (!block->used[i]) {
block->used[i] = true;
return &(block->memory[i * OBJECT_SIZE]);
}
}
}

// 所有块都已满,分配新块
Block* newBlock = new Block();
blocks.push_back(newBlock);

// 使用新块的第一个位置
newBlock->used[0] = true;
std::cout << "分配了新内存块,当前总块数: " << blocks.size() << std::endl;
return &(newBlock->memory[0]);
}

void deallocate(void* ptr) {
// 检查是否是我们分配的内存
for (Block* block : blocks) {
char* blockStart = block->memory;
char* blockEnd = blockStart + BLOCK_SIZE;

if (ptr >= blockStart && ptr < blockEnd) {
// 计算对象索引
size_t index = ((char*)ptr - blockStart) / OBJECT_SIZE;
block->used[index] = false;
return;
}
}

// 如果不是从内存池分配的,使用标准释放
::operator delete(ptr);
}
};

// 全局内存池
MemoryPool globalPool;

class PoolUser {
public:
PoolUser() {
std::cout << "PoolUser对象已创建" << std::endl;
}

~PoolUser() {
std::cout << "PoolUser对象已销毁" << std::endl;
}

// 使用内存池分配内存
void* operator new(size_t size) {
std::cout << "PoolUser::new 分配 " << size << " 字节" << std::endl;
return globalPool.allocate(size);
}

// 使用内存池释放内存
void operator delete(void* ptr) noexcept {
std::cout << "PoolUser::delete 释放内存" << std::endl;
globalPool.deallocate(ptr);
}

// 一些数据使对象大小合适
char data[20];
};

int main() {
std::cout << "测试内存池..." << std::endl;

// 创建一些对象
PoolUser* users[10];
for (int i = 0; i < 10; ++i) {
users[i] = new PoolUser();
}

std::cout << "已创建10个对象" << std::endl;

// 释放部分对象
for (int i = 0; i < 5; ++i) {
delete users[i];
users[i] = nullptr;
}

// 再创建一些对象
for (int i = 0; i < 5; ++i) {
users[i] = new PoolUser();
}

// 释放所有对象
for (int i = 0; i < 10; ++i) {
if (users[i]) {
delete users[i];
}
}

std::cout << "测试完成" << std::endl;
return 0;
}

输出将类似于

内存池已创建
测试内存池...
PoolUser::new 分配 24 字节
分配了新内存块,当前总块数: 1
PoolUser对象已创建
...
已创建10个对象
PoolUser对象已销毁
PoolUser::delete 释放内存
...
PoolUser::new 分配 24 字节
PoolUser对象已创建
...
PoolUser对象已销毁
PoolUser::delete 释放内存
...
测试完成
内存池已销毁,释放了 1 块内存

重载new和delete的注意事项

在重载newdelete时,需要注意以下几点:

  1. 异常处理:重载的new必须在内存分配失败时抛出std::bad_alloc异常,或者返回nullptr(如果是nothrow版本)。

  2. 声明为static:类的newdelete运算符通常声明为static(虽然不需要显式指定,因为默认就是static)。

  3. 一致性:如果重载了new,通常应该同时重载相应的delete,反之亦然。

  4. 数组版本:重载单个对象的newdelete不会影响数组版本,需要单独重载new[]delete[]

  5. 内存对齐:重载的new需要考虑内存对齐要求,特别是在某些硬件平台上。

注意

不正确的重载newdelete可能导致内存泄漏、崩溃或未定义行为。在生产环境中使用前,确保彻底测试您的实现。

总结

重载newdelete运算符是C++内存管理的高级特性,它允许我们自定义对象的内存分配和释放策略。通过重载这些运算符,我们可以实现内存跟踪、内存池、特殊内存分配需求等功能,从而提高程序的性能和可靠性。

关键要点:

  1. 可以全局重载newdelete,影响所有动态内存分配。
  2. 可以在类级别重载newdelete,只影响特定类的对象。
  3. 数组版本需要单独重载new[]delete[]
  4. 定位new允许在预先分配的内存区域创建对象。
  5. 重载时需要遵循C++对这些运算符的约定和要求。

练习

  1. 编写一个类,重载newdelete运算符以记录程序中所有的内存分配和释放。

  2. 实现一个简单的内存池,为固定大小的对象提高分配效率。

  3. 编写一个程序,使用定位new在栈上创建对象,而不是在堆上。

  4. 修改内存池实现,添加统计功能,跟踪内存使用情况,如内存碎片、最大分配量等。

  5. 尝试重载带有自定义参数的new运算符,将文件名和行号作为参数,以便于调试内存问题。

附加资源

  • 《Effective C++》和《More Effective C++》中关于内存管理的章节
  • C++标准库文档中关于<new>头文件的部分
  • 《C++设计与演化》中Bjarne Stroustrup对内存管理的讨论