相信很多关于C++的笔试面试题里都有这样的题目:C++中一个空对象为什么还要占用一个字节空间?(或者C++的一个空对象占多少字节空间)
这篇文章我们来分析下为什么是这样的,继承空基类,组合空基类,空基类优化和使用场景。
sizeof空基类
示例1
#include<iostream>
using namespace std;
class Base {};
int main()
{
cout << sizeof(Base) << endl;
return 0;
}
先看一个实例输出结果:
1
为什么sizeof得到的的结果是1,而不是0或者4,8呢?
其实C++标准里规定对象的大小必须大于0,“An object is a region of storage. ”。
“a most derived object shall have a non-zero size and shall occupy one or more bytes of storage. Base class sub-objects may have zero size. ”,什么意思呢?意思是说最终派生对象大小是非0值,其大小可以是1或多个字节,基类子对象可以为0。
sizeof操作符中也有相关规定“The size of a most derived class shall be greater than zero ”,可见规定的确规定了空对象大小不能为0的现实,但却不强制其大小一定为1(这为编译器为不同的操作系统进行优化留有余地,比如说在不同字长[8,16,32,64]位CPU下,最有效率的操作数类型都是CPU的字长,那么编译器可以选择机器字长来作为不为0时的最小长度)。
那么我们尝试看看继承关系下,继承空对象的子类又是怎么样的呢?
空基类优化
示例2:
#include<iostream>
using namespace std;
class Base {};
class Derived : Base {
};
int main()
{
cout << sizeof(Base) << endl;
cout << sizeof(Derived) << endl;
}
刚才我们都知道当一个空类用作基类时,不需要为它分配空间,前提是它不会导致它被分配到与另一个相同类型的对象或子对象相同的地址。
这里将输出
1
1
因此我们这里引入了一个空基类优化的概念,如果编译器实现了空基优化,它将为每个类打印相同的大小,但这些类都没有大小为零。这意味着在Derived类中,Base类没有任何空间。还要注意,具有优化的空基(没有其他基)的空类也是空的。这就解释了为什么类Base也可以具有与类Derived相同的大小。所以他们的内存布局应该是这样的:
如果编译器没有实现空基优化,它将打印不同的大小,而且内存布局因该是这样的:
刚才我们说的是继承空基类关系的情况,那么如果基类是空基类,同时他又作为被派生类的成员的时候,空基类占的内存是没法被优化掉的,
示例3
#include<iostream>
using namespace std;
class Base {};
class Derived1 {
Base c;
};
class Derived2 {
int i;
Base c;
};
class Derived3 : Base {
int i;
};
int main()
{
std::cout << "sizeof(Derived1): " << sizeof(Derived1) << endl;
std::cout << "sizeof(Derived2): " << sizeof(Derived2) << endl;
std::cout << "sizeof(Derived3): " << sizeof(Derived3) << endl;
}
这段代码,你觉得输出结果是什么?3个sizeof得到的结果相同吗?为什么?
实际上空基类优化对数据成员没有起作用,所以这里Derived1仍然是占用1字节,Derived2如果按照4字节对齐,那么应该是占8字节。
如果一个类或者他的基类中包含虚函数,那么该类就不是空类,因为通常一个含有虚函数的类,都有一个虚函数指针,所以就有了数据成员,虽然是隐藏的,因此,你看下这段代码,看看sizeof会是多少?
示例4:
#include<iostream>
using namespace std;
class Base {
public:
virtual void fun() {
}
};
int main()
{
std::cout << "sizeof(Base): " << sizeof(Base) << endl;
return 0;
}
关于虚函数和虚函数表,虚函数指针的相关内容,我将在专栏后续分享。
通过这几个简单的例子我们就发现了
以组合方式包含的空类A,导致整个类对象的大小有着近翻倍的增长。反而是以继承的方式会减少很多,
其实我们的很多STL容器都是通过继承空间配置器类别以分配空间,你会发现这些容器都不会在容器类中内含一个allocator,一般都是用继承的方式,这样通过空基类可以省下几个字节的空间。
空基类使用场景和优化
因此空基类优化对于模板库而言是一个重要的优化方案,STL中很多时候引入基类的时候都只是为了引入一些新的类型别名或者额外的函数功能,而不会增加新的数据成员。
今天从一个符合类型STL的元组谈起,浅谈c++中空基类优化的使用。
STL中的元组允许有不同类型的元素在一起,因此我们可以想象下,假设有这么几种元素:
class EmptyA {
public:
EmptyA() {}
};
class EmptyB {
public:
EmptyB () {}
};
需要放到元组中,我们通过模板(如果你对模板还不是很熟悉,不要紧,后边我还会给大家分享模板一些有趣的用法)定义一个元组:
template <typename ...Types>
class MyTuple;
template <class _This, class... _Rest>
class MyTuple<_This, _Rest...>
{
_This val;
MyTuple<_Rest...> tail;
public:
MyTuple(_This const& v, _Rest const& ... rest)
: val(v), tail(rest...) {}
};
template <>
class MyTuple<> {
};
我们来sizeof求下MyTuple<A,B>的大小
#include <iostream>
class EmptyA {
public:
EmptyA() { std::cout << "EmptyA\n"; }
};
class EmptyB {
public:
EmptyB() { std::cout << "EmptyB\n"; }
};
template <typename ...Types>
class MyTuple;
template <class _This, class... _Rest>
class MyTuple<_This, _Rest...>
{
_This val;
MyTuple<_Rest...> tail;
public:
MyTuple(_This const& v, _Rest const& ... rest)
: val(v), tail(rest...) {}
};
template <>
class MyTuple<> {
};
int main() {
MyTuple<EmptyA, EmptyB> t(EmptyA{}, EmptyB{});
std::cout << sizeof(t) << std::endl;
}
得到的结果是3,我们其实可以看下内存布局应该是这样的
那么我们还能节省空间吗?
有人想到了私有继承(别急,私有继承的入坑用法同样的我会在专栏里计划更新),因为私有继承通常意味着 is implemented in terms of 的一种关系,而不是 is-a 。因此私有继承能够实现空基类的最优化。
那么我们试下:
#include <iostream>
class EmptyA {
public:
EmptyA() { std::cout << "EmptyA\n"; }
};
class EmptyB {
public:
EmptyB() { std::cout << "EmptyB\n"; }
};
template <typename ...Types>
class MyTuple;
template <>
class MyTuple<> {
public:
MyTuple() {
}
};
template <class _This, class... _Rest>
class MyTuple<_This, _Rest...> : private MyTuple<_Rest...>
{
_This val;
public:
MyTuple() : val() {}
};
int main() {
MyTuple<EmptyA, EmptyB> t;
std::cout << sizeof(t) << "\n";
}
这次果然如你所愿,sizeof得到的结果比刚才还小了一个字节,那么他对应的内存布局是下面这样的。
当然,我相信很多技术开发都有一种精益求精的态度,我们还能进一步优化MyTuple吗?使得sizeof是1??
可以的,因为我们刚才使用私有继承的方式,减少了一次MyTuple<_Rest...> tail;所带来的额外空间,那么我们是不是可以继续利用私有继承,将所有的元组元素都通过私有继承的方式来压缩空间?所以我们需要多重继承,为了防止多个元组可能存在类型相同的情况,我们增加一个MyTupleElement类, 只要MyTupleElement可以安全的从T进行继承的话,就让MyTupleElement私有继承T ,这样当 T是空类时,EBO就发挥效果了。
#include <iostream>
class EmptyA {
public:
EmptyA() { std::cout << "EmptyA\n"; }
};
class EmptyB {
public:
EmptyB() { std::cout << "EmptyB\n"; }
};
template <typename... Types>
class MyTuple;
template <size_t Index, typename T, bool = std::is_class_v<T> && !std::is_final_v<T>>
class MyTupleElement;
template <>
class MyTuple<> {
};
template <class _This, class... _Rest>
class MyTuple<_This, _Rest...> :
private MyTuple<_Rest...>,
private MyTupleElement<sizeof...(_Rest), _This>
{
};
template <size_t Index, typename T>
class MyTupleElement<Index, T, true> : private T {
};
template <size_t Index, typename T>
class MyTupleElement<Index, T, false> {
T val;
};
int main() {
MyTuple<EmptyA, EmptyB> t;
std::cout << sizeof(t) << "\n";
}
运行得到的结果是1
最后的这段代码也就是STL标准模板库里的tuple的一个简要的原型。
C++20引入no_unique_address
从c++20起,若空成员子对象使用属性 [[no_unique_address]],则允许像空基类一样优化掉它们。取这种成员的地址会产生可能等于同一个对象的某个其他成员的地址。[[no_unique_address]]指示此数据成员不需要具有不同于其类的所有其他非静态数据成员的地址。这表示若该成员拥有空类型(例如无状态分配器),则编译器可将它优化为不占空间,正如同假如它是空基类一样。若该成员非空,则其中的任何尾随填充空间亦可复用于存储其他数据成员。
来看示例
示例5
#include<iostream>
using namespace std;
class Base {};
class Derived1 {
int i;
Base c;
};
//空基类优化
class Derived2 :private Base {
int i;
};
//使用no_unique_address进行空基类优化
class Derived3 {
int i;
[[no_unique_address]]Base b;
};
//使用no_unique_address进行空基类优化
class Derived4 {
int i;
[[no_unique_address]]Base b1,b2;//这里b1 与 i 共享同一地址,因为b1标记有 [[no_unique_address]],但b2不能共享,所以b2不能被优化。
// 然而,其中一者可以与 c 共享地址。
};
int main()
{
std::cout << "sizeof(Derived1): " << sizeof(Derived1) << endl;
std::cout << "sizeof(Derived2): " << sizeof(Derived2) << endl;
std::cout << "sizeof(Derived3): " << sizeof(Derived3) << endl;
std::cout << "sizeof(Derived4): " << sizeof(Derived4) << endl;
}
这里我们Derived2 是使用空基类优化的情况,Derived3则使用 [[no_unique_address]]进行空基类优化,所以得到的结果应该是
文章来源:https://www.toymoban.com/news/detail-437340.html
这里,给各位读者一个思考题,如下输出的结果是多少?为什么?文章来源地址https://www.toymoban.com/news/detail-437340.html
示例6
#include<iostream>
using namespace std;
class Base {};
class Base2 {};
class Derived1 {
int i;
[[no_unique_address]]Base b1;
[[no_unique_address]]Base2 b2;
};
class Derived2 {
int i;
[[no_unique_address]]Base b1,b2;
[[no_unique_address]]Base2 b3,b4;
};
int main()
{
std::cout << "sizeof(Derived1): " << sizeof(Derived1) << endl;
std::cout << "sizeof(Derived2): " << sizeof(Derived2) << endl;
}
到了这里,关于换个花样玩C++(5)玩转空类,空类不是一个sizeof=1就这么简单就能讲完的的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!