MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ std::shared_ptr 的自定义分配器

2022-02-134.0k 阅读

理解 std::shared_ptr

在C++中,std::shared_ptr是智能指针家族的一员,它提供了一种自动管理动态分配对象生命周期的机制。std::shared_ptr通过引用计数来跟踪有多少个std::shared_ptr实例指向同一个对象。当引用计数降为0时,对象会被自动释放,从而避免了内存泄漏。

例如,下面是一个简单使用std::shared_ptr的例子:

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> ptr = std::make_shared<int>(42);
    std::cout << "Value: " << *ptr << std::endl;
    return 0;
}

在上述代码中,std::make_shared<int>(42)创建了一个指向int类型对象(值为42)的std::shared_ptrstd::cout通过解引用ptr来输出对象的值。

std::shared_ptr 的默认分配器

std::shared_ptr在默认情况下使用std::allocator来分配和释放内存。std::allocator是C++标准库提供的默认内存分配器,它使用operator newoperator delete来进行内存的分配与释放。

例如,当我们使用std::make_shared创建std::shared_ptr时,std::make_shared会使用默认的分配器来分配对象的内存:

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};

int main() {
    std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
    return 0;
}

在这个例子中,std::make_shared<MyClass>()使用默认分配器分配MyClass对象的内存。当ptr超出作用域时,MyClass对象会被释放,调用其析构函数。

为什么需要自定义分配器

虽然默认分配器在大多数情况下工作得很好,但在某些特定场景下,我们可能需要使用自定义分配器:

  1. 内存池:在需要频繁分配和释放小块内存的场景中,使用内存池可以减少内存碎片,提高内存分配效率。自定义分配器可以从内存池中分配和释放内存。
  2. 特定的内存需求:例如,需要在特定的内存区域(如共享内存、显存等)分配内存,默认分配器无法满足这种需求,需要自定义分配器。
  3. 性能优化:在某些对性能要求极高的应用中,自定义分配器可以针对应用的特点进行优化,提高内存分配和释放的速度。

实现自定义分配器

自定义分配器的基本结构

自定义分配器需要满足一定的接口要求,它是一个类模板,至少需要实现以下成员类型和成员函数:

  1. 成员类型
    • pointer:指向分配对象类型的指针。
    • const_pointer:指向常量分配对象类型的指针。
    • value_type:分配对象的类型。
    • size_type:用于表示内存大小的无符号整数类型。
    • difference_type:用于表示两个指针之间距离的有符号整数类型。
    • rebind<U>:用于将分配器适配到不同类型的模板。
  2. 成员函数
    • pointer allocate(size_type n):分配nvalue_type对象所需的内存。
    • void deallocate(pointer p, size_type n):释放由allocate分配的内存,p是指向要释放内存的指针,n是对象的数量。
    • template <class... Args> void construct(pointer p, Args&&... args):在p指向的内存位置构造对象。
    • void destroy(pointer p):销毁p指向的对象。

下面是一个简单的自定义分配器示例,它只是简单地包装了std::mallocstd::free

#include <iostream>
#include <memory>

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

    template <typename U>
    struct rebind {
        using other = MyAllocator<U>;
    };

    pointer allocate(size_type n) {
        return static_cast<pointer>(std::malloc(n * sizeof(T)));
    }

    void deallocate(pointer p, size_type n) {
        std::free(p);
    }

    template <class... Args>
    void construct(pointer p, Args&&... args) {
        new (p) T(std::forward<Args>(args)...);
    }

    void destroy(pointer p) {
        p->~T();
    }
};

使用自定义分配器与 std::shared_ptr

要使用自定义分配器与std::shared_ptr,我们可以使用std::shared_ptr的构造函数,它接受一个分配器对象作为参数。

#include <iostream>
#include <memory>

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

    template <typename U>
    struct rebind {
        using other = MyAllocator<U>;
    };

    pointer allocate(size_type n) {
        return static_cast<pointer>(std::malloc(n * sizeof(T)));
    }

    void deallocate(pointer p, size_type n) {
        std::free(p);
    }

    template <class... Args>
    void construct(pointer p, Args&&... args) {
        new (p) T(std::forward<Args>(args)...);
    }

    void destroy(pointer p) {
        p->~T();
    }
};

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};

int main() {
    MyAllocator<MyClass> alloc;
    std::shared_ptr<MyClass> ptr(alloc.allocate(1), [&alloc](MyClass* p) {
        alloc.destroy(p);
        alloc.deallocate(p, 1);
    });
    alloc.construct(ptr.get());
    return 0;
}

在上述代码中,我们首先定义了MyAllocator。然后在main函数中,我们创建了一个MyAllocator<MyClass>对象alloc。接着,我们使用std::shared_ptr的构造函数,第一个参数是通过alloc.allocate(1)分配的内存,第二个参数是一个自定义的删除器,它调用allocdestroydeallocate函数来释放资源。最后,我们使用alloc.construct在分配的内存上构造MyClass对象。

内存池分配器示例

内存池的概念

内存池是一种内存管理技术,它预先分配一块较大的内存块,然后将其划分成多个小块供程序使用。当程序需要分配内存时,直接从内存池中获取小块内存,而不是调用系统的内存分配函数(如malloc)。当程序释放内存时,将小块内存返回给内存池,而不是释放回系统。这样可以减少内存碎片,提高内存分配效率。

实现内存池分配器

下面是一个简单的内存池分配器的实现:

#include <iostream>
#include <memory>
#include <vector>

class MemoryPool {
private:
    std::vector<char> pool;
    std::vector<bool> used;
    size_t blockSize;
    size_t numBlocks;

public:
    MemoryPool(size_t blockSize, size_t numBlocks)
        : blockSize(blockSize), numBlocks(numBlocks) {
        pool.resize(blockSize * numBlocks);
        used.resize(numBlocks, false);
    }

    void* allocate() {
        for (size_t i = 0; i < numBlocks; ++i) {
            if (!used[i]) {
                used[i] = true;
                return &pool[i * blockSize];
            }
        }
        return nullptr;
    }

    void deallocate(void* p) {
        size_t index = static_cast<char*>(p) - &pool[0];
        index /= blockSize;
        if (index < numBlocks) {
            used[index] = false;
        }
    }
};

template <typename T>
class PoolAllocator {
private:
    MemoryPool& pool;

public:
    using pointer = T*;
    using const_pointer = const T*;
    using value_type = T;
    using size_type = std::size_t;
    using difference_type = std::ptrdiff_t;

    template <typename U>
    struct rebind {
        using other = PoolAllocator<U>;
    };

    PoolAllocator(MemoryPool& pool) : pool(pool) {}

    PoolAllocator(const PoolAllocator& other) : pool(other.pool) {}

    template <typename U>
    PoolAllocator(const PoolAllocator<U>& other) : pool(other.pool) {}

    pointer allocate(size_type n) {
        if (n != 1) {
            throw std::bad_alloc();
        }
        return static_cast<pointer>(pool.allocate());
    }

    void deallocate(pointer p, size_type n) {
        if (n != 1) {
            throw std::logic_error("Deallocation of multiple blocks not supported");
        }
        pool.deallocate(p);
    }

    template <class... Args>
    void construct(pointer p, Args&&... args) {
        new (p) T(std::forward<Args>(args)...);
    }

    void destroy(pointer p) {
        p->~T();
    }
};

使用内存池分配器与 std::shared_ptr

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};

int main() {
    MemoryPool pool(sizeof(MyClass), 10);
    PoolAllocator<MyClass> alloc(pool);
    std::shared_ptr<MyClass> ptr(alloc.allocate(1), [&alloc](MyClass* p) {
        alloc.destroy(p);
        alloc.deallocate(p, 1);
    });
    alloc.construct(ptr.get());
    return 0;
}

在上述代码中,我们首先定义了MemoryPool类来管理内存池。然后定义了PoolAllocator,它使用MemoryPool进行内存分配和释放。在main函数中,我们创建了一个MemoryPool对象pool,并基于此创建了PoolAllocator<MyClass>对象alloc。最后,我们使用std::shared_ptr结合alloc来管理MyClass对象的生命周期。

自定义分配器与线程安全

多线程环境下的问题

在多线程环境中,使用自定义分配器需要特别注意线程安全问题。如果多个线程同时访问自定义分配器的共享资源(如内存池),可能会导致数据竞争和未定义行为。

例如,在前面的内存池分配器中,如果多个线程同时调用allocatedeallocate函数,used向量和pool向量可能会被同时修改,导致数据不一致。

解决线程安全问题

  1. 互斥锁:可以使用std::mutex来保护共享资源。在访问共享资源前,先锁定互斥锁,访问完成后解锁互斥锁。
#include <iostream>
#include <memory>
#include <vector>
#include <mutex>

class MemoryPool {
private:
    std::vector<char> pool;
    std::vector<bool> used;
    size_t blockSize;
    size_t numBlocks;
    std::mutex mtx;

public:
    MemoryPool(size_t blockSize, size_t numBlocks)
        : blockSize(blockSize), numBlocks(numBlocks) {
        pool.resize(blockSize * numBlocks);
        used.resize(numBlocks, false);
    }

    void* allocate() {
        std::lock_guard<std::mutex> lock(mtx);
        for (size_t i = 0; i < numBlocks; ++i) {
            if (!used[i]) {
                used[i] = true;
                return &pool[i * blockSize];
            }
        }
        return nullptr;
    }

    void deallocate(void* p) {
        std::lock_guard<std::mutex> lock(mtx);
        size_t index = static_cast<char*>(p) - &pool[0];
        index /= blockSize;
        if (index < numBlocks) {
            used[index] = false;
        }
    }
};
  1. 无锁数据结构:使用无锁数据结构来避免锁的开销。例如,使用无锁队列或无锁链表来管理内存池中的空闲块。这需要更复杂的实现,但可以提高多线程环境下的性能。

自定义分配器的局限性

  1. 可移植性:某些自定义分配器可能依赖于特定的操作系统或硬件平台,导致代码的可移植性较差。例如,在特定平台上使用特殊的内存映射技术实现的分配器,在其他平台上可能无法工作。
  2. 兼容性:自定义分配器需要与标准库容器和其他使用分配器的组件兼容。如果自定义分配器没有正确实现分配器接口,可能会导致与标准库组件的不兼容,从而出现难以调试的错误。
  3. 维护成本:实现和维护自定义分配器需要更多的精力。由于自定义分配器通常针对特定的应用场景进行优化,当应用场景发生变化时,可能需要对分配器进行大量的修改。

总结

自定义分配器为C++开发者提供了在内存管理方面的强大灵活性。通过理解std::shared_ptr的默认分配器机制,以及掌握自定义分配器的实现方法,开发者可以根据应用的具体需求优化内存管理,提高程序的性能和效率。在多线程环境下,要特别注意自定义分配器的线程安全问题,选择合适的同步机制或无锁数据结构来保证数据的一致性。同时,也要清楚自定义分配器的局限性,权衡其带来的好处与可能增加的维护成本和兼容性问题。在实际应用中,应根据具体情况谨慎选择是否使用自定义分配器以及如何实现它。