C++ 自定义 `new`/`delete` 操作符重载:全局、类级别与数组形式

哈喽,各位好!今天咱们来聊聊 C++ 里面一个挺有意思,但又容易让人挠头的东西:自定义 new/delete 操作符重载。这玩意儿就像给你的内存管理动个大手术,能让你更精细地控制对象的创建和销毁。听起来有点吓人?别怕,咱们一步一步来,保证你听完之后能笑着说:“这玩意儿,so easy!”

啥是 new/delete 重载?

简单来说,newdelete 是 C++ 里负责动态内存分配和释放的两个操作符。默认情况下,它们会调用标准库的 mallocfree 函数来完成任务。但是,有时候我们可能对默认的行为不太满意,比如:

  • 性能优化: 默认的 malloc 可能不够快,或者不适合你的特定场景。
  • 内存泄漏检测: 想在分配和释放内存的时候做一些额外的检查,方便调试。
  • 自定义内存池: 想用自己的内存池来管理对象,避免频繁的系统调用。
  • 嵌入式系统: 在资源受限的环境中,需要更精细地控制内存分配。

这时候,new/delete 重载就派上用场了。我们可以定义自己的 newdelete 操作符,让它们按照我们想要的方式来分配和释放内存。

重载的几种姿势

C++ 允许我们在不同的作用域重载 newdelete

  1. 全局重载: 影响所有使用 newdelete 的地方。
  2. 类级别重载: 只影响特定类的对象。
  3. 数组形式重载: 针对数组的 new[]delete[] 操作符。

下面我们来逐一击破。

1. 全局重载:天下归一

全局重载就像给整个 C++ 世界换了一套新的内存管理系统。所有地方的 newdelete 都会使用你定义的版本。

代码示例:

#include <iostream>
#include <cstdlib> // For malloc, free
#include <new>     // For bad_alloc

// 自定义全局 new 操作符
void* operator new(size_t size) {
    std::cout << "全局 new 被调用,分配 " << size << " 字节" << std::endl;
    void* p = malloc(size);
    if (!p) {
        throw std::bad_alloc(); // 内存分配失败,抛出异常
    }
    return p;
}

// 自定义全局 delete 操作符
void operator delete(void* p) noexcept {
    std::cout << "全局 delete 被调用" << std::endl;
    free(p);
}

// 自定义全局 new[] 操作符
void* operator new[](size_t size) {
    std::cout << "全局 new[] 被调用,分配 " << size << " 字节" << std::endl;
    void* p = malloc(size);
    if (!p) {
        throw std::bad_alloc(); // 内存分配失败,抛出异常
    }
    return p;
}

// 自定义全局 delete[] 操作符
void operator delete[](void* p) noexcept {
    std::cout << "全局 delete[] 被调用" << std::endl;
    free(p);
}

int main() {
    int* arr = new int[5];
    delete[] arr;

    int* single = new int;
    delete single;

    return 0;
}

注意事项:

  • 全局 newdelete 必须定义在全局作用域。
  • new 操作符的返回值必须是 void*,参数是 size_t,表示要分配的字节数。
  • delete 操作符的参数是 void*,指向要释放的内存。
  • 全局 delete 应该声明为 noexcept,防止异常逃逸。
  • 必须处理内存分配失败的情况,通常是抛出 std::bad_alloc 异常。
  • 全局重载影响范围太广,要慎用,否则可能与其他库或代码产生冲突。

2. 类级别重载:精准打击

类级别重载只影响特定类的对象。这就像给某个类定制了一套专属的内存管理方案。

代码示例:

#include <iostream>
#include <cstdlib> // For malloc, free
#include <new>     // For bad_alloc

class MyClass {
public:
    // 自定义类级别的 new 操作符
    void* operator new(size_t size) {
        std::cout << "MyClass::new 被调用,分配 " << size << " 字节" << std::endl;
        void* p = malloc(size);
        if (!p) {
            throw std::bad_alloc();
        }
        return p;
    }

    // 自定义类级别的 delete 操作符
    void operator delete(void* p) noexcept {
        std::cout << "MyClass::delete 被调用" << std::endl;
        free(p);
    }

    // 自定义类级别的 new[] 操作符
    void* operator new[](size_t size) {
        std::cout << "MyClass::new[] 被调用,分配 " << size << " 字节" << std::endl;
        void* p = malloc(size);
        if (!p) {
            throw std::bad_alloc();
        }
        return p;
    }

    // 自定义类级别的 delete[] 操作符
    void operator delete[](void* p) noexcept {
        std::cout << "MyClass::delete[] 被调用" << std::endl;
        free(p);
    }

private:
    int data;
};

int main() {
    MyClass* obj = new MyClass();
    delete obj;

    MyClass* arr = new MyClass[3];
    delete[] arr;

    int* other = new int; // 这里会调用全局 new
    delete other;       // 这里会调用全局 delete

    return 0;
}

注意事项:

  • 类级别的 newdelete 必须是类的静态成员函数(即使你没有显式声明 static,编译器也会把它当成 static)。
  • 类级别的 newdelete 只会影响该类的对象。
  • 如果类中定义了 new,但没有定义 delete,可能会导致内存泄漏。
  • 类级别的重载可以与全局重载同时存在,互不影响。
  • 可以为类定义多个重载的 new 操作符,通过不同的参数来区分。这种叫做 "Placement new"。

Placement new

Placement new 允许你将对象构造到已分配的内存中。它不会分配新的内存,而是直接在已有的内存上调用对象的构造函数。

代码示例:

#include <iostream>
#include <new> // For placement new

class MyClass {
public:
    MyClass(int value) : data(value) {
        std::cout << "MyClass 构造函数被调用,data = " << data << std::endl;
    }

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

    int data;
};

int main() {
    // 1. 分配一块原始内存
    void* buffer = malloc(sizeof(MyClass));

    // 2. 使用 placement new 在 buffer 上构造 MyClass 对象
    MyClass* obj = new (buffer) MyClass(42); // 注意这里的语法!

    // 3. 使用对象
    std::cout << "obj->data = " << obj->data << std::endl;

    // 4. 显式调用析构函数 (重要!)
    obj->~MyClass();

    // 5. 释放原始内存
    free(buffer);

    return 0;
}

Placement new 的用途:

  • 内存池: 从预先分配的内存池中分配对象。
  • 自定义内存管理: 更精细地控制对象的构造和析构。
  • 避免内存碎片: 通过预先分配大块内存,减少内存碎片。

重要提示:

  • Placement new 不会分配内存,你需要自己负责分配和释放内存。
  • 在使用 placement new 构造的对象之后,必须显式调用其析构函数,否则可能会导致资源泄漏。
  • Placement delete 不需要自己实现,因为placement new没有做内存分配,相应也不需要手动释放。

3. 数组形式重载:团队作战

数组形式的 new[]delete[] 操作符用于分配和释放对象数组。

代码示例:

#include <iostream>
#include <cstdlib> // For malloc, free
#include <new>     // For bad_alloc

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::new[] 被调用,分配 " << size << " 字节" << std::endl;
        void* p = malloc(size);
        if (!p) {
            throw std::bad_alloc();
        }
        return p;
    }

    // 自定义类级别的 delete[] 操作符
    void operator delete[](void* p) noexcept {
        std::cout << "MyClass::delete[] 被调用" << std::endl;
        free(p);
    }
};

int main() {
    MyClass* arr = new MyClass[3]; // 调用 MyClass::new[]
    delete[] arr;                 // 调用 MyClass::delete[]

    return 0;
}

注意事项:

  • new[]delete[] 必须成对出现,否则会导致内存泄漏或未定义行为。
  • delete[] 会依次调用数组中每个对象的析构函数。
  • 如果类中定义了 new[],但没有定义 delete[],可能会导致内存泄漏。

为什么要重载 new[]delete[]

主要目的是为了更精细地控制对象数组的分配和释放,例如:

  • 在分配内存时保存数组的大小: C++ 标准规定,delete[] 需要知道数组的大小才能正确调用每个对象的析构函数。如果自定义 new[],可以在分配的内存块前面保存数组的大小,delete[] 就可以读取这个大小。
  • 定制对象的构造和析构过程: 例如,可以在分配内存后,统一初始化数组中的每个对象。

完整示例:保存数组大小

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

class MyClass {
public:
    MyClass(int id) : id_(id) {
        std::cout << "MyClass 构造函数被调用,id = " << id_ << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass 析构函数被调用,id = " << id_ << std::endl;
    }

private:
    int id_;
};

class MyArray {
public:
    // 自定义 new[],保存数组大小
    static void* operator new[](size_t size) {
        std::cout << "MyArray::new[] 被调用,分配 " << size << " 字节" << std::endl;

        // 计算实际需要分配的内存大小 (存储数组大小 + 对象数组)
        size_t actualSize = size + sizeof(size_t);

        void* p = malloc(actualSize);
        if (!p) {
            throw std::bad_alloc();
        }

        // 在分配的内存块前面保存数组大小
        *(size_t*)p = size; // 存储原始大小,不包含size_t的大小

        // 返回对象数组的起始地址
        return (char*)p + sizeof(size_t);
    }

    // 自定义 delete[],读取数组大小并释放内存
    static void operator delete[](void* p) noexcept {
        if (p == nullptr) return; // 避免空指针

        // 获取存储的数组大小
        void* realPtr = (char*)p - sizeof(size_t);
        size_t size = *(size_t*)realPtr;

        std::cout << "MyArray::delete[] 被调用,释放 " << size << " 字节" << std::endl;

        // 释放实际分配的内存
        free(realPtr);
    }
};

int main() {
    MyArray* arr = new (std::nothrow) MyArray[5]; //分配5个MyArray对象,实际分配内存空间会多出sizeof(size_t)的空间用于存储5这个数字
    if(arr == nullptr){
        std::cout << "内存分配失败" << std::endl;
        return 1;
    }
    delete[] arr;

    return 0;
}

代码解释:

  1. new[]:

    • 计算实际需要分配的内存大小,包括存储数组大小的空间。
    • 使用 malloc 分配内存。
    • 在分配的内存块前面保存数组的大小。
    • 返回对象数组的起始地址(跳过存储数组大小的空间)。
  2. delete[]:

    • 获取对象数组的起始地址。
    • 计算实际分配的内存地址(减去存储数组大小的空间)。
    • 读取存储的数组大小。
    • 使用 free 释放实际分配的内存。

表格总结:

特性 全局重载 类级别重载 数组形式重载 Placement new
作用域 所有使用 new/delete 的地方 特定类的对象 对象数组 已分配的内存
语法 定义在全局作用域 类的静态成员函数 类的静态成员函数 new (address) Type(arguments)
影响范围 广,可能与其他代码冲突 窄,只影响特定类的对象 窄,只影响对象数组 非常局部,只影响指定地址上的对象
用途 系统级别的内存管理 类的专属内存管理 对象数组的专属内存管理 在已分配的内存上构造对象
是否分配内存
析构函数调用 delete 自动调用 delete 自动调用 delete[] 自动调用 需要手动调用
内存释放 delete 自动释放 delete 自动释放 delete[] 自动释放 需要手动释放
是否需要自定义 通常需要 视情况而定,例如自定义内存池 视情况而定,例如保存数组大小 不需要,但需要手动管理内存

什么时候用?

  • 全局重载: 当你需要彻底改变整个程序的内存管理方式时。谨慎使用。
  • 类级别重载: 当你需要为某个特定的类定制内存管理方案时,例如,使用内存池来优化该类对象的分配和释放。
  • 数组形式重载: 当你需要更精细地控制对象数组的分配和释放时,例如,在分配内存时保存数组的大小。
  • Placement new: 当你需要在已分配的内存上构造对象时,例如,从预先分配的内存池中分配对象。

注意事项 (再强调一遍!)

  • 成对出现: 如果重载了 new,一定要重载 delete,反之亦然。new[]delete[] 也要成对出现。
  • 处理异常:new 操作符中,必须处理内存分配失败的情况,通常是抛出 std::bad_alloc 异常。
  • 避免内存泄漏: 确保所有分配的内存最终都被释放。
  • 考虑线程安全: 如果你的程序是多线程的,需要考虑 newdelete 操作符的线程安全问题。可以使用互斥锁来保护内存分配和释放。
  • Placement new: 必须手动调用析构函数。

总结

自定义 new/delete 操作符重载是一个强大的工具,可以让你更精细地控制 C++ 程序的内存管理。但是,它也需要谨慎使用,否则可能会导致内存泄漏、未定义行为或其他问题。 掌握了这些,相信你已经对 C++ 的 new/delete 重载有了更深刻的理解。 记住,实践是检验真理的唯一标准。 多写代码,多尝试,你就能真正掌握这门技术。

希望今天的讲座对你有所帮助!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注