首页 > 编程语言 >【C++ STL分配器】详细介绍

【C++ STL分配器】详细介绍

时间:2024-08-13 15:52:16浏览次数:14  
标签:std 自定义 STL list C++ 内存 分配器 free


C++ 中的 分配器(Allocator) 是用于抽象和管理内存分配与释放的机制,主要用于标准模板库(STL)容器。分配器的设计允许开发者自定义内存管理策略,从而优化性能、满足特殊需求或实现特定功能(如内存池、共享内存等)。本文将详细介绍 C++ 分配器的概念、作用、自定义分配器的实现以及在 STL 中的应用。


1. 分配器的概念

分配器 是一个模板类,用于定义对象的内存分配和释放方式。在 STL 中,所有容器都接受一个分配器作为模板参数,默认使用 std::allocator。通过自定义分配器,开发者可以控制容器如何管理内存。

分配器的主要功能:

  • 内存分配:为对象分配原始的未构造的内存。
  • 对象构造:在已分配的内存上构造对象。
  • 对象销毁:调用对象的析构函数,销毁对象。
  • 内存释放:释放先前分配的内存。

2. 标准分配器 std::allocator

std::allocator 是 C++ 标准库提供的默认分配器,实现了最基本的内存分配和对象管理功能。其定义位于头文件 <memory> 中。

主要成员函数:

  • allocate:分配未构造的内存。

    pointer allocate(size_type n);
    
  • deallocate:释放先前分配的内存。

    void deallocate(pointer p, size_type n);
    
  • construct:在已分配的内存上构造对象。(C++17 之前)

    void construct(pointer p, const T& val);
    
  • destroy:调用对象的析构函数。(C++17 之前)

    void destroy(pointer p);
    

注意:从 C++17 开始,constructdestroy 被移除了,建议使用 std::allocator_traits 或者直接使用 std::uninitialized_fill 等算法。


3. 自定义分配器

自定义分配器允许开发者控制内存管理策略。例如,可以实现一个内存池分配器,以减少频繁的内存分配和释放带来的开销。

实现步骤:

  1. 继承或实现分配器接口:可以继承自 std::allocator,或者直接实现所需的成员函数。

  2. 定义类型别名:如 value_typepointersize_type 等。

  3. 实现必要的成员函数:如 allocatedeallocate 等。

示例:简单的内存池分配器

以下是一个基本的内存池分配器的示例,实现了固定大小的内存块的分配和释放。

#include <memory>
#include <cstddef>
#include <list>

template <typename T>
class PoolAllocator {
public:
    using value_type = T;

    PoolAllocator() = default;
    ~PoolAllocator() {
        for (auto& block : blocks_) {
            ::operator delete(block);
        }
    }

    T* allocate(std::size_t n) {
        if (n != 1) {
            throw std::bad_alloc();
        }

        if (!free_list_) {
            expandPool();
        }

        T* ptr = free_list_;
        free_list_ = free_list_->next;
        return reinterpret_cast<T*>(ptr);
    }

    void deallocate(T* p, std::size_t n) {
        if (p == nullptr || n != 1) return;

        auto node = reinterpret_cast<FreeNode*>(p);
        node->next = free_list_;
        free_list_ = node;
    }

    template <typename U, typename... Args>
    void construct(U* p, Args&&... args) {
        ::new ((void*)p) U(std::forward<Args>(args)...);
    }

    template <typename U>
    void destroy(U* p) {
        p->~U();
    }

private:
    struct FreeNode {
        FreeNode* next;
    };

    void expandPool() {
        std::size_t size = sizeof(T) > sizeof(FreeNode) ? sizeof(T) : sizeof(FreeNode);
        FreeNode* block = reinterpret_cast<FreeNode*>(::operator new(size));
        block->next = free_list_;
        free_list_ = block;
        blocks_.push_back(block);
    }

    FreeNode* free_list_ = nullptr;
    std::list<FreeNode*> blocks_;
};

解释

  • allocate:每次只允许分配一个对象的内存。如果空闲列表为空,则调用 expandPool 扩展内存池。

  • deallocate:将释放的内存块加入到空闲列表中,供后续分配使用。

  • construct / destroy:用于对象的构造和销毁。

使用示例

#include <vector>
#include <iostream>

int main() {
    std::vector<int, PoolAllocator<int>> vec;
    for (int i = 0; i < 10; ++i) {
        vec.push_back(i);
    }

    for (const auto& val : vec) {
        std::cout << val << " ";
    }

    return 0;
}

4. 分配器与容器

STL 中的容器都接受一个分配器作为模板参数,默认使用 std::allocator。通过提供自定义分配器,可以改变容器的内存管理方式。

示例:为 std::vector 提供自定义分配器

#include <vector>
#include <iostream>

// 假设已经定义了 PoolAllocator<T>

int main() {
    std::vector<int, PoolAllocator<int>> vec;
    vec.reserve(100); // 预留空间

    for (int i = 0; i < 100; ++i) {
        vec.push_back(i);
    }

    for (const auto& val : vec) {
        std::cout << val << " ";
    }

    return 0;
}

在上述示例中,std::vector 使用了自定义的 PoolAllocator,从而在插入元素时使用内存池进行内存管理。


5. std::allocator_traits

从 C++11 开始,引入了 std::allocator_traits,用于统一和简化分配器的实现。它为分配器提供了默认实现和辅助功能,建议在自定义分配器中使用。

使用示例

修改之前的 PoolAllocator,使其使用 std::allocator_traits

#include <memory>
#include <cstddef>
#include <list>

template <typename T>
class PoolAllocator {
public:
    using value_type = T;

    PoolAllocator() = default;
    ~PoolAllocator() {
        for (auto& block : blocks_) {
            ::operator delete(block);
        }
    }

    T* allocate(std::size_t n) {
        if (n != 1) {
            throw std::bad_alloc();
        }

        if (!free_list_) {
            expandPool();
        }

        T* ptr = reinterpret_cast<T*>(free_list_);
        free_list_ = free_list_->next;
        return ptr;
    }

    void deallocate(T* p, std::size_t n) {
        if (p == nullptr || n != 1) return;

        auto node = reinterpret_cast<FreeNode*>(p);
        node->next = free_list_;
        free_list_ = node;
    }

private:
    struct FreeNode {
        FreeNode* next;
    };

    void expandPool() {
        std::size_t size = sizeof(T) > sizeof(FreeNode) ? sizeof(T) : sizeof(FreeNode);
        FreeNode* block = reinterpret_cast<FreeNode*>(::operator new(size));
        block->next = free_list_;
        free_list_ = block;
        blocks_.push_back(block);
    }

    FreeNode* free_list_ = nullptr;
    std::list<FreeNode*> blocks_;
};

在使用时,容器会通过 std::allocator_traits 来调用分配器的相应方法,如构造和销毁对象。


6. 分配器的应用场景

  • 性能优化:通过自定义分配器,可以减少内存碎片,提高分配和释放的效率,尤其是在频繁进行小对象分配的场景下。

  • 内存池:预先分配一大块内存,按需分配给对象,避免频繁的系统调用。

  • 共享内存:在多进程场景下,通过分配器将对象放置在共享内存中,实现跨进程的数据共享。

  • 自定义内存策略:如实时系统中,需要严格控制内存分配的时间和方式。


7. 注意事项

  • 兼容性:确保自定义分配器满足分配器的要求,尤其是在不同的容器和算法中正确工作。

  • 异常安全:在分配和释放内存时,要考虑异常安全,避免内存泄漏。

  • 线程安全:如果在多线程环境中使用,需要确保分配器的线程安全性。


8. 总结

分配器是 C++ 中强大的内存管理工具,通过自定义分配器,开发者可以针对特定的应用场景优化内存分配策略。理解分配器的工作原理和正确使用方法,对于编写高性能和高可靠性的代码至关重要。

标签:std,自定义,STL,list,C++,内存,分配器,free
From: https://blog.csdn.net/YRB0127/article/details/141166216

相关文章

  • /lib64/libstdc++.so.6: version GLIBCXX_3.4.20 not found
    java应用运行出现了2个错误,error1:/lib64/libstdc++.so.6:versionGLIBCXX_3.4.20notfounderror2:/lib64/libstdc++.so.6:versionCXXABI_1.3.8notfound查阅了网上的解决方法,都说要更新libstdc++.so.6,按照教程操作,一直没有成功,最后参考了好几篇文章,综合了大家的方法,成功更新......
  • c++通讯录管理系统
    1、系统需求(1)添加。添加联系人信息(主要包括姓名、年龄、号码和备注)。(2)遍历。显示出所有联系人的信息。(3)删除。删除指定的联系人信息,通过电话号码筛选。(4)修改。修改指定的联系人信息,通过电话号码查找。(5)查找。查找指定的联系人信息,通过电话号码查找。(6)清空。清空所有联系......
  • 高性能的 C++ Web 开发框架 CPPCMS + WebSocket 模拟实现聊天与文件传输案例。
    1.项目结构2.config.json{"service":{"api":"http","port":8080,"ip":"0.0.0.0"},"http":{"script":"",&q......
  • C++浅拷贝和深拷贝
    在C++编程中,对象的拷贝是一项常见的操作。深拷贝和浅拷贝是两种常用的拷贝方式,对于理解对象拷贝的内部机制和避免潜在的问题至关重要。本文将深入解析C++中的深拷贝和浅拷贝的概念、原理以及使用场景,帮助读者更好地掌握和运用这两种拷贝方式。浅拷贝(ShallowCopy)是指在拷贝对象时......
  • C++——构造函数和析构函数
    一、初识构造函数和析构函数简单来说,有对象生成必然会调用构造函数,有对象销毁必然会调用析构函数。构造函数的作用是初始化成员变量,是由编译器去调用的,而析构函数同理也是由编译器调用,不过他的作用则是清理。可以由下面的代码体验两个函数的使用。注意:相同点:两个函数都没有......
  • 基于Dango+微信小程序的广西东盟旅游资源信息管理系统+80003(免费领源码)可做计算机毕业
    django广西-东盟旅游资源信息管理系统小程序摘 要在社会快速发展和人们生活水平提高的影响下,旅游产业蓬勃发展,旅游形式也变得多样化,使旅游资源信息的管理变得比过去更加困难。依照这一现实为基础,设计一个快捷而又方便的基于小程序的旅游资源信息管理系统是一项十分重要并且......
  • c++ 字符串转 整形
    目前有两种string 转整形的方式std::atoi(constchar*);std::stoi(conststd::string);atoi()是c语言风格,而stoi()是c++11标准库中新增的函数两者的区别在atoi()的参数是constchar*,所以我们必须将字符串的类型从string转换为constchar类型才能够转换为int。str......
  • C++:内存管理
    C++内存管理的概念        C语言内存管理方式(malloc/free)在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。new/delete                    new/d......
  • C++:类与对象(下)
    再探构造函数        构造函数体赋值与初始化列表其实之前我们实现构造函数时,初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种⽅式,就是初始化列表,C++规定初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后⾯跟......
  • C++:类与对象(中)
    类的默认成员函数:        在C++中,如果你没有显式地定义某些特定的成员函数(如构造函数、析构函数、拷贝构造函数、拷贝赋值运算符和移动构造函数),那么编译器会自动生成这些函数。这些由编译器自动生成的函数被称为默认成员函数。        构造函数   ......