C++实现自定义的`new`/`delete`:在资源受限环境中的内存管理

好的,下面就以讲座的形式,详细介绍C++中自定义new/delete,以及其在资源受限环境下的内存管理应用。


C++自定义new/delete:资源受限环境下的内存管理

大家好!今天我们来深入探讨C++中的内存管理,特别是如何自定义newdelete操作符,以及它们在资源受限环境中发挥的作用。

1. 默认的new/delete及其局限性

首先,我们回顾一下C++中默认的newdelete。当我们使用new来分配内存时,实际上是调用了全局的operator new函数。同样,delete操作符会调用全局的operator delete函数来释放内存。

// 全局 operator new 的声明
void* operator new(std::size_t size) throw(std::bad_alloc);

// 全局 operator delete 的声明
void operator delete(void* ptr) throw();

这些全局的operator newoperator delete通常由C++标准库提供,它们底层依赖于操作系统提供的内存分配机制(比如mallocfree)。

然而,在资源受限的环境中,比如嵌入式系统、游戏引擎或者高性能服务器中,默认的new/delete可能存在以下局限性:

  • 性能开销大: 默认的内存分配器通常是通用的,为了处理各种情况,它们可能引入额外的锁、内存碎片管理等开销,这在性能敏感的应用中是不可接受的。
  • 内存碎片: 频繁地分配和释放不同大小的内存块会导致内存碎片,降低内存利用率,甚至导致分配失败。
  • 不可预测性: 默认的内存分配器可能无法提供确定性的分配时间,这在实时系统中是致命的。
  • 缺乏控制: 无法针对特定应用场景进行优化,比如预先分配内存池、使用特定的分配策略等。
  • 内存泄漏检测困难: 在复杂系统中,内存泄漏难以追踪,自定义内存管理可以方便地添加内存追踪功能。

因此,在资源受限的环境中,我们需要自定义newdelete来克服这些局限性。

2. 自定义new/delete的基本方法

自定义newdelete的核心在于重载operator newoperator delete函数。你可以在全局作用域、类作用域甚至命名空间作用域中重载它们。

2.1 全局作用域重载

在全局作用域重载operator newoperator delete会影响所有使用newdelete的代码。这通常不推荐,因为它可能会与其他库或代码产生冲突。

#include <iostream>
#include <cstdlib> // for malloc and free

void* operator new(std::size_t size) throw(std::bad_alloc) {
    std::cout << "Global new called, size: " << size << std::endl;
    void* p = std::malloc(size);
    if (!p) {
        throw std::bad_alloc();
    }
    return p;
}

void operator delete(void* ptr) throw() {
    std::cout << "Global delete called" << std::endl;
    std::free(ptr);
}

int main() {
    int* p = new int(10);
    delete p;
    return 0;
}

2.2 类作用域重载

在类作用域重载operator newoperator delete只会影响该类的对象的内存分配和释放。这是最常见的自定义new/delete的方式。

#include <iostream>
#include <cstdlib>

class MyClass {
public:
    void* operator new(std::size_t size) throw(std::bad_alloc) {
        std::cout << "MyClass new called, size: " << size << std::endl;
        void* p = std::malloc(size);
        if (!p) {
            throw std::bad_alloc();
        }
        return p;
    }

    void operator delete(void* ptr) throw() {
        std::cout << "MyClass delete called" << std::endl;
        std::free(ptr);
    }

private:
    int data;
};

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

2.3 placement new 和 delete

除了重载基本的 operator newoperator delete,C++ 还提供了 placement new,允许你在已分配的内存上构造对象。这在自定义内存管理中非常有用,例如在内存池中创建对象。

#include <iostream>
#include <cstdlib>

class MyClass {
public:
    MyClass(int value) : data(value) {}

    ~MyClass() {
        std::cout << "MyClass destructor called" << std::endl;
    }

private:
    int data;
};

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

    // 使用 placement new 在 buffer 上构造 MyClass 对象
    MyClass* obj = new (buffer) MyClass(42);

    // 显式调用析构函数
    obj->~MyClass();

    // 释放内存
    std::free(buffer);

    return 0;
}

Placement new 实际上并没有分配内存,它只是在已有的内存上调用构造函数。因此,你需要手动调用析构函数,并使用 std::free 或自定义的释放函数来释放内存。 Placement delete 并不存在,因为placement new 没有分配内存,所以不需要释放。 但是需要手动调用析构函数。

3. 资源受限环境下的内存管理策略

在资源受限的环境中,我们通常会采用以下内存管理策略:

3.1 静态内存分配

静态内存分配是在编译时就确定内存大小和位置的分配方式。它可以避免动态内存分配的开销和不确定性。

  • 全局变量/静态变量: 将对象声明为全局变量或静态变量,它们的生命周期贯穿整个程序,内存分配在程序启动时完成。
  • 预分配缓冲区: 预先分配一块大的缓冲区,然后在程序运行时从中分配小块内存。

优点:

  • 速度快,没有动态分配的开销。
  • 确定性,避免了分配失败的风险。

缺点:

  • 灵活性差,需要在编译时确定内存大小。
  • 可能造成内存浪费,如果预分配的内存没有被充分利用。

3.2 内存池

内存池是一种预先分配一块大的连续内存区域,然后将其划分为固定大小的块,用于快速分配和释放对象的内存管理策略。

#include <iostream>
#include <vector>
#include <cstddef>  // for std::size_t

class MemoryPool {
public:
    MemoryPool(std::size_t blockSize, std::size_t poolSize)
        : blockSize_(blockSize), poolSize_(poolSize), freeBlocks_(poolSize) {
        memory_ = std::malloc(blockSize_ * poolSize_);
        if (!memory_) {
            throw std::bad_alloc();
        }

        // 将内存块链接成链表
        char* block = static_cast<char*>(memory_);
        for (std::size_t i = 0; i < poolSize_ - 1; ++i) {
            *reinterpret_cast<void**>(block) = block + blockSize_;
            block += blockSize_;
        }
        *reinterpret_cast<void**>(block) = nullptr; // 最后一个块指向 nullptr
        freeList_ = memory_;
    }

    ~MemoryPool() {
        std::free(memory_);
    }

    void* allocate() {
        if (freeList_ == nullptr) {
            return nullptr; // 内存池已耗尽
        }

        void* block = freeList_;
        freeList_ = *reinterpret_cast<void**>(freeList_);
        --freeBlocks_;
        return block;
    }

    void deallocate(void* ptr) {
        if (!ptr) return;

        // 将释放的块添加到链表头部
        *reinterpret_cast<void**>(ptr) = freeList_;
        freeList_ = ptr;
        ++freeBlocks_;
    }

    std::size_t getFreeBlocks() const {
        return freeBlocks_;
    }

private:
    void* memory_;        // 指向内存池的起始地址
    void* freeList_;      // 指向空闲块链表的头部
    std::size_t blockSize_; // 每个块的大小
    std::size_t poolSize_;  // 内存池中块的数量
    std::size_t freeBlocks_; //空闲块数量
};

// 自定义 new 和 delete 使用内存池
class MyClass {
public:
    MyClass(int value) : data(value) {}

    static void* operator new(std::size_t size) {
        return pool.allocate();
    }

    static void operator delete(void* ptr) {
        pool.deallocate(ptr);
    }

private:
    int data;
    static MemoryPool pool;
};

// 初始化内存池,在程序开始前
MemoryPool MyClass::pool(sizeof(MyClass), 10); // 10个 MyClass 对象

int main() {
    MyClass* obj1 = new MyClass(1);
    MyClass* obj2 = new MyClass(2);

    std::cout << "Free blocks: " << MyClass::pool.getFreeBlocks() << std::endl; // 输出 8

    delete obj1;
    delete obj2;

    std::cout << "Free blocks: " << MyClass::pool.getFreeBlocks() << std::endl; // 输出 10

    return 0;
}

优点:

  • 快速分配和释放,避免了动态分配的开销。
  • 减少内存碎片,所有块大小相同。
  • 可以控制内存分配的行为,比如限制最大分配数量。

缺点:

  • 只适用于分配固定大小的对象。
  • 需要预先确定内存池的大小。

3.3 定制分配器 (Custom Allocators)

C++ 标准库允许你创建自定义的分配器,并将其与标准容器一起使用。这允许你控制容器的内存分配行为。

#include <iostream>
#include <vector>
#include <memory> // allocator

template <typename T>
class MyAllocator {
public:
    using value_type = T;
    using pointer = T*;
    using const_pointer = const T*;
    using reference = T&;
    using const_reference = const T&;
    using size_type = std::size_t;
    using difference_type = std::ptrdiff_t;

    MyAllocator() noexcept {}

    template <typename U>
    MyAllocator(const MyAllocator<U>&) noexcept {}

    pointer allocate(size_type n) {
        std::cout << "Allocating " << n << " elements" << std::endl;
        pointer p = static_cast<pointer>(std::malloc(n * sizeof(T)));
        if (!p) {
            throw std::bad_alloc();
        }
        return p;
    }

    void deallocate(pointer p, size_type n) {
        std::cout << "Deallocating " << n << " elements" << std::endl;
        std::free(p);
    }
};

template <typename T, typename U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) noexcept {
    return true;
}

template <typename T, typename U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) noexcept {
    return false;
}

int main() {
    std::vector<int, MyAllocator<int>> myVector; // 使用自定义分配器
    myVector.push_back(10);
    myVector.push_back(20);

    return 0;
}

优点:

  • 可以与标准容器无缝集成。
  • 灵活性高,可以根据需要定制分配行为。

缺点:

  • 需要实现分配器接口,比较复杂。
  • 可能需要考虑线程安全问题。

3.4 双缓冲/循环缓冲

在某些应用中,比如音视频处理,可以使用双缓冲或循环缓冲来避免频繁的内存分配和释放。

  • 双缓冲: 使用两个缓冲区,一个用于写入数据,另一个用于读取数据。当一个缓冲区写满时,切换到另一个缓冲区。
  • 循环缓冲: 使用一个固定大小的缓冲区,当写入数据到达缓冲区末尾时,从头开始覆盖。

优点:

  • 避免了频繁的内存分配和释放。
  • 可以实现实时数据处理。

缺点:

  • 需要预先确定缓冲区的大小。
  • 可能存在数据覆盖的风险。

4. 内存泄漏检测

自定义new/delete可以方便地添加内存泄漏检测功能。例如,可以维护一个已分配内存块的列表,在程序退出时检查是否有未释放的内存块。

#include <iostream>
#include <cstdlib>
#include <vector>
#include <algorithm>

struct MemoryBlock {
    void* address;
    std::size_t size;
};

std::vector<MemoryBlock> allocatedBlocks;

void* operator new(std::size_t size) throw(std::bad_alloc) {
    void* p = std::malloc(size);
    if (!p) {
        throw std::bad_alloc();
    }
    allocatedBlocks.push_back({p, size});
    return p;
}

void operator delete(void* ptr) throw() {
    auto it = std::find_if(allocatedBlocks.begin(), allocatedBlocks.end(),
                            [ptr](const MemoryBlock& block) { return block.address == ptr; });
    if (it != allocatedBlocks.end()) {
        allocatedBlocks.erase(it);
        std::free(ptr);
    } else {
        std::cerr << "Error: Attempting to delete unallocated memory" << std::endl;
    }
}

void DumpMemoryLeaks() {
    if (!allocatedBlocks.empty()) {
        std::cerr << "Memory Leaks Detected:" << std::endl;
        for (const auto& block : allocatedBlocks) {
            std::cerr << "  Address: " << block.address << ", Size: " << block.size << std::endl;
        }
    } else {
        std::cout << "No memory leaks detected." << std::endl;
    }
}

// 示例类
class MyClass {
public:
    MyClass(int value) : data(value) {}
private:
    int data;
};

int main() {
    MyClass* obj1 = new MyClass(10);
    // 故意不释放 obj1,造成内存泄漏
    //delete obj1;

    DumpMemoryLeaks(); // 在程序结束时检测内存泄漏
    return 0;
}

5. 选择合适的内存管理策略

选择合适的内存管理策略取决于具体的应用场景和资源限制。

策略 优点 缺点 适用场景
静态内存分配 速度快,确定性 灵活性差,可能造成内存浪费 内存需求固定,对性能要求高的场景
内存池 快速分配和释放,减少内存碎片,可控 只适用于固定大小的对象,需要预先确定大小 大量分配和释放固定大小对象的场景,比如游戏引擎
定制分配器 与标准容器集成,灵活性高 实现复杂,可能需要考虑线程安全 需要定制容器内存分配行为的场景
双缓冲/循环缓冲 避免频繁分配和释放,实现实时数据处理 需要预先确定大小,可能存在数据覆盖的风险 音视频处理,实时数据采集等需要避免频繁分配的场景

6. 其他注意事项

  • 对齐: 自定义内存分配器需要保证分配的内存块满足对齐要求,以避免性能问题。可以使用std::align函数来对齐内存。
  • 异常安全: 确保自定义newdelete是异常安全的,即在构造函数抛出异常时,能够正确地释放已分配的内存。
  • 线程安全: 在多线程环境下,需要考虑线程安全问题,可以使用锁或其他同步机制来保护内存分配器。
  • 调试: 自定义内存管理可能会使调试更加困难,需要使用合适的调试工具和技术来定位问题。

总结:

自定义newdelete是C++中强大的内存管理工具,在资源受限环境中尤为重要。通过选择合适的内存管理策略,可以提高程序的性能、可靠性和可控性。请记住,选择合适的策略需要仔细评估应用场景和资源限制,并进行充分的测试和验证。


定制内存管理是关键

掌握自定义 newdelete 的方法,并了解各种内存管理策略的优缺点,可以帮助你在资源受限环境中构建更高效、更稳定的应用程序。

根据实际情况选择策略

静态内存分配、内存池、定制分配器和双缓冲/循环缓冲等策略各有千秋,选择哪一种取决于具体的应用场景和资源限制。

代码示例的价值

通过代码示例加深理解,能够更好地掌握自定义内存管理的技术,并在实际项目中灵活应用。

更多IT精英技术系列讲座,到智猿学院

发表回复

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