第一章 设计EBO(空基类优化)

本文详细介绍了空基类优化(EBO)的概念及其在C++中的作用,包括为何空类的大小不为0的原因。通过对比不同类的内存占用,展示了EBO如何减少类的大小。接着,文章探讨了STL中EBO的应用,特别是在内存管理类中的实现,以及如何通过继承实现内存管理的优化。最后,通过实例演示了如何利用EBO设计一个简单的内存分配与释放系统,验证了EBO在减小容器大小方面的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、什么是EBO

1.空类

EBO简称Empty Base Optimization,空基类最优化。

要想了解EBO,让我们来看看这道题:

class Empty{
public:
    void print() {
        std::cout<<"I am an Empty class"<<std::endl;
    }
};

由于它是空的,所以这个sizeof是多少呢?

std::cout<<sizeof(Empty)<<std::endl; //1

结果是1,它是空的怎么不是0呢?

因为空类同样可以被实例化,每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址.所以上述大小为1.

大家看完这个问题应该就会问了,为什么两个不同对象的地址应该不同?

答案很简单,如果a的地址与b的地址一样,那么在同一地址具有两个对象将意味着在使用指针引用它们时将无法区分这两个对象。

2.空基类优化

现在对比一下下面两个用法,第一种,一个类中包含了两一个类作为成员,然后通过这个来获得被包含类的功能。

class notEbo  {
    int i;
    Empty e;
    // do other things
};

另一种直接采用继承的方式来获得基类的成员函数及其他功能等等。

class ebo:public Empty {
    int i;
    // do other things
};

接下来我们来做个测试:

std::cout<<sizeof(notEbo)<<std::endl;
std::cout<<sizeof(ebo)<<std::endl;

第一个结果是8,第二个结果是4;

第一种,会因为字节对齐,将其原来只占1字节,进行扩充到4的倍数,最后就是8字节。

对比这两个发现,第二种通过继承方式来获得基类的功能,并没有产生额外大小的优化称之为EBO(空基类优化)。

接下来,我们回到STL源码中,看看其中的使用!

二、STL中的EBO世界

不管是deque、rb_tree、list等容器,都离不开内存管理,在这几个容器中都包含了相应的内存管理,并通过_xx_impl来继承下面这几个类:

std::allocator<_Tp>
__gnu_cxx::bitmap_allocator<_Tp>
__gnu_cxx::bitmap_allocator<_Tp>
__gnu_cxx::__mt_alloc<_Tp>
__gnu_cxx::__pool_alloc<_Tp>
__gnu_cxx::malloc_allocator<_Tp>

那这和我们的EBO有啥关系呢?

实际上,上面所列出继承的基类都是内存管理的EBO(空基类)。

在每个容器中的使用都是调用每个内存管理的 rebind<_Tp>::other 。

例如红黑树源码结构:

typedef typename __gnu_cxx::__alloc_traits<_Alloc>::template
        rebind<_Rb_tree_node<_Val> >::other _Node_allocator;
struct _Rb_tree_impl : public _Node_allocator
{
// do somethings
};

接下来我们看上面列出的内存管理类里面的源码结构:这里拿allocator举例:

template<typename _Tp>
class allocator: public __allocator_base<_Tp>
{
	 template<typename _Tp1>
     struct rebind { typedef allocator<_Tp1> other; };
};

这里都是通过rebind<_Tp>::other来获得传递进来的内存分配器,也就是前面提到的这些。

std::allocator<_Tp>
__gnu_cxx::bitmap_allocator<_Tp>
__gnu_cxx::bitmap_allocator<_Tp>
__gnu_cxx::__mt_alloc<_Tp>
__gnu_cxx::__pool_alloc<_Tp>
__gnu_cxx::malloc_allocator<_Tp>

搞懂了这些,来测试一波:

void print() {
    cout<<sizeof(std::allocator<int>)<<" "<<sizeof(std::allocator<int>::rebind<int>::other)<<endl;
    cout<<sizeof(__gnu_cxx::bitmap_allocator<int>)<<" "<<sizeof(__gnu_cxx::bitmap_allocator<int>::rebind<int>::other)<<endl;
    cout<<sizeof(__gnu_cxx::new_allocator<int>)<<" "<<sizeof(__gnu_cxx::new_allocator<int>::rebind<int>::other)<<endl;
    cout<<sizeof(__gnu_cxx::__mt_alloc<int>)<<" "<<sizeof(__gnu_cxx::__mt_alloc<int>::rebind<int>::other)<<endl;
    cout<<sizeof(__gnu_cxx::__pool_alloc<int>)<<" "<<sizeof(__gnu_cxx::__pool_alloc<int>::rebind<int>::other)<<endl;
    cout<<sizeof(__gnu_cxx::malloc_allocator<int>)<<" "<<sizeof(__gnu_cxx::malloc_allocator<int>::rebind<int>::other)<<endl;
}

我们来测试这些sizeof是不是1,经过测试输出如下:

1 1
1 1
1 1
1 1
1 1
1 1

说明内存管理的实现就是通过采用继承的方式,使用空基类优化,来达到尽量降低容器所占的大小。

三、利用EBO,手动实现一个简单的内存分配与释放

首先定义一个sizeof(class)=1的类,同STL一样,里面使用allocate与deallocate来进行内存管理。

class MyAllocator {
public:
    void *allocate(std::size_t size) {
        return std::malloc(size);
    }

    void deallocate(void *ptr) {
        std::free(ptr);
    }
};

第一种方式的内存管理:嵌入一个内存管理类

template<class T, class Allocator>
class MyContainerNotEBO {
    T *data_ = nullptr;
    std::size_t capacity_;
    Allocator allocator_;   // 嵌入一个MyAllocator
public:
    MyContainerNotEBO(std::size_t capacity)
            : capacity_(capacity), allocator_(), data_(nullptr) {
        std::cout << "alloc malloc" << std::endl;
        data_ = reinterpret_cast<T *>(allocator_.allocate(capacity * sizeof(T))); // 分配内存
    }

    ~MyContainerNotEBO() {
        std::cout << "MyContainerNotEBO free malloc" << std::endl;
        allocator_.deallocate(data_);
    }
};

第二种方式:采用空基类优化,继承来获得内存管理功能

template<class T, class Allocator>
class MyContainerEBO
        : public Allocator {    // 继承一个EBO
    T *data_ = nullptr;
    std::size_t capacity_;
public:
    MyContainerEBO(std::size_t capacity)
            : capacity_(capacity), data_(nullptr) {
        std::cout << "alloc malloc" << std::endl;
        data_ = reinterpret_cast<T *>(this->allocate(capacity * sizeof(T)));
    }

    ~MyContainerEBO() {
        std::cout << "MyContainerEBO free malloc" << std::endl;
        this->deallocate(data_);
    }
};

开始测试:

int main() {
    MyContainerNotEBO<int, MyAllocator> notEbo = MyContainerNotEBO<int, MyAllocator>(0);
    std::cout << "Using Not EBO Test sizeof is " << sizeof(notEbo) << std::endl;
    MyContainerEBO<int, MyAllocator> ebo = MyContainerEBO<int, MyAllocator>(0);
    std::cout << "Using EBO Test sizeof is " << sizeof(ebo) << std::endl;

    return 0;
}

测试结果:

alloc malloc
Using Not EBO Test sizeof is 24
alloc malloc
Using EBO Test sizeof is 16
MyContainerEBO free malloc
MyContainerNotEBO free malloc

我们发现采用EBO的设计确实比嵌入设计好很多。至此,EBO设计完毕。

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小鱼的编程之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值