【C++内存管理】分配器

1. allocator

分配器 (allocator) 是C++ STL库的基石之一,它是一种策略模式,允许用户将内存管理从容器中解耦出来,进行更具体化的操作。通过使用 allocator,我们可以自定义内存的分配和释放方式,从而可以更好地控制内存的使用。

1.1 为何使用allocator

在C++中,内存的申请和释放是一个昂贵的操作,频繁的申请和释放可能导致系统的内存碎片,使程序性能下降。通过使用allocator,我们可以自定义内存的申请和释放方式,减少系统的内存碎片,提高程序的性能。

此外,allocator还有一个重要的作用,那就是将对象的构造和内存的申请分开。在传统的内存申请方式中,我们在申请内存的同时就会调用对象的构造函数,但有时候,我们可能只是想申请内存,而不想立即构造对象,这时候,就可以使用allocator。

1.2 allocator的基本使用

在C++ STL中,allocator是一个模板类,我们可以通过为它提供一个类型参数来创建一个特定类型的allocator。以下是一个基本的例子:

#include <memory>

int main() {
    
    
    std::allocator<int> alloc; // 创建一个分配int的allocator
    int* p = alloc.allocate(10); // 分配10个int的空间

    // 使用未构造的内存
    for (int i = 0; i < 10; ++i) {
    
    
        alloc.construct(p + i, i); // 在分配的内存上构造对象
    }

    // 销毁对象并释放内存
    for (int i = 0; i < 10; ++i) {
    
    
        alloc.destroy(p + i); // 销毁对象
    }
    alloc.deallocate(p, 10); // 释放内存
    return 0;
}

1.3 自定义分配器

通过自定义分配器,我们可以更灵活地控制内存的申请和释放。例如,我们可以将vector的数据直接存储到数据库、共享内存或者文件中,实现了数据的持久化和共享。

一个自定义分配器需要提供以下几个接口:

  • typedefs:为使用的类型定义别名
  • allocate(n):分配能容纳n个对象的内存
  • deallocate(p, n):释放前面分配的内存
  • construct(p, val):在指针p所指向的内存上构造一个对象,其值为val
  • destroy(p):销毁指针p所指向的对象

以下是一个简单的自定义分配器的例子:

template <class T>
class MyAllocator {
    
    
public:
    typedef T value_type;

    MyAllocator() = default;
    template <class U> constexpr MyAllocator(const MyAllocator<U>&) noexcept {
    
    }

    T* allocate(std::size_t n) {
    
    
        return static_cast<T*>(::operator new(n*sizeof(T)));
    }

    void deallocate(T* p, std::size_t) noexcept {
    
    
        ::operator delete(p);
    }
};

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

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

在这个例子中,我们创建了一个自定义的分配器MyAllocator,这个分配器使用全局newdelete操作符来分配和释放内存。

2. 分配器概述

2.1 分配器的作用和重要性

分配器在C++中扮演着至关重要的角色,它们用于实现容器算法时,能够与存储细节隔离从而解耦合。使用分配器的优点在于,开发者可以专注于算法的实现,而无需关心内存的管理。不仅如此,分配器也为我们提供了存储分配与释放的标准方法,以及一些用于对象构造和销毁的函数。

2.2 STL中的标准分配器

C++ STL库中提供了一个标准的分配器:std::allocator,它实现了最基本的内存分配和释放策略。在大多数情况下,它的性能已经足够高,但是在某些特殊情况下(例如大量小对象的分配和销毁),使用自定义的分配器可能会获得更好的性能。

2.3 分配器的使用

下面的代码展示了如何使用std::allocator。其中,allocate用于分配内存,construct用于在已分配的内存上构造对象,destroy用于销毁对象,deallocate用于释放内存。需要注意的是,从C++17开始,constructdestroy函数已被废弃,我们需要使用std::allocator_traits来调用构造和析构。

#include <memory>

int main() {
    
    
    std::allocator<int> alloc; // 创建一个分配int的allocator
    int* p = alloc.allocate(10); // 分配10个int的空间

    // 使用未构造的内存
    for (int i = 0; i < 10; ++i) {
    
    
        std::allocator_traits<std::allocator<int>>::construct(alloc, p+i, i);
    }

    // 销毁对象并释放内存
    for (int i = 0; i < 10; ++i) {
    
    
        std::allocator_traits<std::allocator<int>>::destroy(alloc, p+i);
    }
    alloc.deallocate(p, 10); // 释放内存

    return 0;
}

上述代码中,首先我们创建了一个分配int的allocator,并分配了10个int的空间。然后,我们使用std::allocator_traitsconstruct方法在分配的内存上构造对象。最后,我们使用std::allocator_traitsdestroy方法销毁对象,并使用deallocate方法释放内存。

3. 自定义分配器

C++ STL库的灵活性主要源于其策略模式的设计,分配器就是这种设计的一个重要应用。通过自定义分配器,我们可以实现一些特殊的内存管理策略,比如内存共享、内存泄漏探测,预分配对象存储、内存池等。

3.1 自定义分配器的应用场景

以下列出了一些自定义分配器的应用场景:

  1. 内存共享:对于多进程或者多线程应用,我们可能需要共享内存空间。自定义分配器可以使我们将对象存储在共享内存中。

  2. 内存泄漏探测:在复杂的应用中,内存泄漏可能是一个难以定位的问题。自定义分配器可以帮助我们追踪内存的分配和释放,从而检测内存泄漏。

  3. 预分配对象存储:对于一些知道内存需求的应用,预先分配内存可以避免频繁的内存分配和释放,提高性能。

  4. 内存池:对于频繁分配和释放小块内存的应用,使用内存池可以减少内存碎片,提高性能。

3.2 自定义分配器的实现

一个自定义分配器需要实现以下几个接口:

  • typedefs:为使用的类型定义别名
  • allocate(n):分配能容纳n个对象的内存
  • deallocate(p, n):释放前面分配的内存
  • construct(p, val):在指针p所指向的内存上构造一个对象,其值为val
  • destroy(p):销毁指针p所指向的对象

下面的代码演示了如何实现一个自定义的分配器:

template <class T>
class MyAllocator {
    
    
public:
    typedef T value_type;

    MyAllocator() = default;
    template <class U> constexpr MyAllocator(const MyAllocator<U>&) noexcept {
    
    }

    T* allocate(std::size_t n) {
    
    
        // 你的内存分配策略
    }

    void deallocate(T* p, std::size_t) noexcept {
    
    
        // 你的内存释放策略
    }

    template<typename... Args>
    void construct(T* p, Args&&... args) {
    
    
        // 你的对象构造策略
    }

    void destroy(T* p) {
    
    
        // 你的对象销毁策略
    }
};

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

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

3.3 自定义分配器的使用

自定义分配器可以用于STL中的任何容器,包括vector、list等。以下是一个使用自定义分配器的vector的例子:

#include <vector>
#include "MyAllocator.h" // 包含你的自定义分配器的头文件

int main() {
    
    
    std::vector<int, MyAllocator<int>> vec; // 使用自定义分配器的vector
    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(3);
    return 0;
}

在这个例子中,我们创建了一个使用MyAllocatorstd::vector。因此,这个vector的内存管理策略将由我们的MyAllocator来决定。同样的方法也可以应用于std::list或其他STL容器。

4. 未初始化内存算法

在 C++ STL 中,有一系列的未初始化内存算法,这些算法用于在未初始化的内存上直接构造对象,可以提高程序的效率。这些算法的名称通常以 uninitialized_ 开头,其中 uninitialized_copy 是最常用的一种。

4.1 uninitialized_copy 算法

uninitialized_copy 是一种用于在未初始化内存上复制序列的算法。它接受两个输入迭代器(定义了要复制的序列)和一个输出迭代器(定义了未初始化内存的起始位置),并尝试在输出范围内构造与输入序列相同的元素。

以下是 uninitialized_copy 的基本用法:

#include <memory>
#include <vector>

int main() {
    
    
    std::vector<int> vec {
    
    1, 2, 3, 4, 5};
    std::allocator<int> alloc;

    // 使用 allocator 分配未初始化内存
    int* p = alloc.allocate(vec.size());

    // 使用 uninitialized_copy 将 vec 中的元素复制到未初始化的内存中
    std::uninitialized_copy(vec.begin(), vec.end(), p);

    // 使用完成后,需要手动调用 destructor 和 deallocate 释放资源
    for (std::size_t i = 0; i < vec.size(); ++i) {
    
    
        alloc.destroy(p + i);
    }
    alloc.deallocate(p, vec.size());

    return 0;
}

在上述代码中,我们首先创建了一个包含五个整数的 vector。然后,我们使用 allocator 分配了一块足以存储 vector 中所有元素的未初始化内存。接着,我们使用 uninitialized_copyvector 中的元素复制到这块未初始化的内存中。最后,我们遍历这块内存,对每个元素调用 destroy,然后调用 deallocate 释放整块内存。

需要注意的是,由于 uninitialized_copy 不会自动调用 destructor 和 deallocate,所以我们需要手动调用它们以防止内存泄露。

4.2 uninitialized_copy_n 算法

uninitialized_copy_nuninitialized_copy 的一个变体,它接受一个输入迭代器(定义了要复制的序列的起始位置)、一个大小值n(定义了要复制的元素数量)和一个输出迭代器(定义了未初始化内存的起始位置),并尝试在输出范围内构造与输入序列前n个相同的元素。

以下是 uninitialized_copy_n 的基本用法:

#include <memory>
#include <vector>

int main() {
    
    
    std::vector<int> vec {
    
    1, 2, 3, 4, 5};
    std::allocator<int> alloc;

    // 使用 allocator 分配未初始化内存
    int* p = alloc.allocate(vec.size());

    // 使用 uninitialized_copy_n 将 vec 中的前3个元素复制到未初始化的内存中
    std::uninitialized_copy_n(vec.begin(), 3, p);

    // 使用完成后,需要手动调用 destructor 和 deallocate 释放资源
    for (std::size_t i = 0; i < 3; ++i) {
    
    
        alloc.destroy(p + i);
    }
    alloc.deallocate(p, vec.size());

    return 0;
}

在上述代码中,我们首先创建了一个包含五个整数的 vector。然后,我们使用 allocator 分配了一块足以存储 vector 中所有元素的未初始化内存。接着,我们使用 uninitialized_copy_nvector 中的前3个元素复制到这块未初始化的内存中。最后,我们遍历这块内存,对复制的每个元素调用 destroy,然后调用 deallocate 释放整块内存。

需要注意的是,由于 uninitialized_copy_n 不会自动调用 destructor 和 deallocate,所以我们需要手动调用它们以防止内存泄露。这点与 uninitialized_copy 是一样的。

4.3 uninitialized_fill 算法

uninitialized_fill 是一种在未初始化内存上填充值的算法。它接受两个迭代器(定义了未初始化内存的范围)和一个值,然后尝试在指定范围内构造这个值。

以下是 uninitialized_fill 的基本用法:

#include <memory>

int main() {
    
    
    std::allocator<int> alloc;

    // 使用 allocator 分配未初始化内存
    int* p = alloc.allocate(5);

    // 使用 uninitialized_fill 将值42填充到未初始化的内存中
    std::uninitialized_fill(p, p + 5, 42);

    // 使用完成后,需要手动调用 destructor 和 deallocate 释放资源
    for (std::size_t i = 0; i < 5; ++i) {
    
    
        alloc.destroy(p + i);
    }
    alloc.deallocate(p, 5);

    return 0;
}

在上述代码中,我们使用 allocator 分配了一块可以存储5个整数的未初始化内存。然后,我们使用 uninitialized_fill 将值42填充到这块未初始化的内存中。最后,我们遍历这块内存,对每个元素调用 destroy,然后调用 deallocate 释放整块内存。

需要注意的是,由于 uninitialized_fill 不会自动调用 destructor 和 deallocate,所以我们需要手动调用它们以防止内存泄露。

其他的未初始化内存算法(uninitialized_fill_nuninitialized_default_constructuninitialized_value_construct)用法与 uninitialized_copy 类似,也需要注意手动调用 destructor 和 deallocate 以防止内存泄露。

5. construct_at、destroy_at 对象构造和销毁

在 C++17 和 C++20 中,有两个非常重要的函数:std::construct_atstd::destroy_at。这两个函数可以分别在给定的内存位置上构造和销毁对象。

5.1 std::construct_at

std::construct_at 是一种在指定内存位置上构造对象的方法。它接受一个指针和一系列构造函数参数,然后在指针指向的内存位置上构造一个对象。

以下是 std::construct_at 的基本用法:

#include <memory>

struct MyStruct {
    
    
    int x;
    float y;
    MyStruct(int x, float y) : x(x), y(y) {
    
    }
};

int main() {
    
    
    std::allocator<MyStruct> alloc;

    // 使用 allocator 分配未初始化内存
    MyStruct* p = alloc.allocate(1);

    // 使用 construct_at 在未初始化的内存中构造一个 MyStruct 对象
    std::construct_at(p, 42, 3.14f);

    // 使用完成后,需要手动调用 destroy_at 和 deallocate 释放资源
    std::destroy_at(p);
    alloc.deallocate(p, 1);

    return 0;
}

在上述代码中,我们首先定义了一个名为 MyStruct 的结构。然后,我们使用 allocator 分配了一块足以存储一个 MyStruct 对象的未初始化内存。接着,我们使用 construct_at 在这块未初始化的内存上构造一个 MyStruct 对象。最后,我们调用 destroy_at 销毁这个对象,然后调用 deallocate 释放整块内存。

5.2 std::destroy_at

std::destroy_at 是一种在指定内存位置上销毁对象的方法。它接受一个指针,然后调用该指针指向的对象的析构函数。

在上述代码的 std::construct_at 部分,我们已经展示了 std::destroy_at 的基本用法。这里再给出一个独立的例子:

#include <memory>

struct MyStruct {
    
    
    int x;
    float y;
    MyStruct(int x, float y) : x(x), y(y) {
    
    }
    ~MyStruct() {
    
    
        // 自定义析构函数
        std::cout << "MyStruct object is being destroyed.\n";
    }
};

int main() {
    
    
    std::allocator<MyStruct> alloc;

    // 使用 allocator 分配未初始化内存
    MyStruct* p = alloc.allocate(1);

    // 使用 construct_at 在未初始化的内存中构造一个 MyStruct 对象
    std::construct_at(p, 42, 3.14f);

    // 使用 destroy_at 销毁这个对象
    std::destroy_at(p);

    // 使用完成后,需要手动调用 deallocate 释放资源
    alloc.deallocate(p, 1);

    return 0;
}

在上述代码中,我们首先定义了一个名为 MyStruct 的结构,它具有一个自定义的析构函数。然后,我们使用 allocator 分配了一块足以存储一个 MyStruct 对象的未初始化内存。接着,我们使用 construct_at 在这块未初始化的内存上构造一个 MyStruct 对象。接下来,我们调用 destroy_at 销毁这个对象,可以看到自定义析构函数的输出信息。最后,我们调用 deallocate 释放整块内存。

总的来说,std::construct_atstd::destroy_at 提供了一种方便、安全的方式在指定的内存位置上构造和销毁对象,与直接使用 newdelete 相比,它们提供了更好的控制,尤其是在处理未初始化的内存时。

猜你喜欢

转载自blog.csdn.net/weixin_52665939/article/details/131620359