对于C++OBP的理解
前言:
因为自己查阅文章时经常遇到,这些问题:因为环境不同而导致结果不一致、因为文章跳跃度太大而导致无法理解、因为文章代码运行结果不同而苦恼。
所以为了看我文章的人不会遭遇同样的问题,
以后我的文章将遵循以下原则:
1.会在文章开头标明相关配置与环境
2.尽可能循序渐进(还是看不懂可能是我没有这样的天赋),标出阅读的前提知识
3.列出的代码自己先跑一遍
////////////////////////////////////////
语言:c++
前提知识:
1.类
2.继承
3.静态成员
集成开发环境:Visual Studio 2017
////////////////////////////////////////
文章:
所谓ebo,就是Empty Base Class Optimization,空基类最优化。
空类为没有非静态成员(此处的成员不包含函数)的类,也就是说空类中不包含占空间的成员,因为函数是不会影响类的大小的。
然后稍微解释一下,非静态成员,因为静态成员并不是存储在类对象的空间中,否则对象销毁时静态成员也会被销毁,所以静态成员是不会影响类的大小的。
空类的例子:
class Empty {};
class FuntionOnly
{
public:
void a() {}
int b() { return 1; }
int c(int n) { int m = n; return m; }
};
class StaticMemberOnly{ public:static int a; };
但是说是空类,其实sizeof(空类)=1,这表示其存在(根据其他博客所说,这也与大小的计算方法有关),但是这点空间是毫无意义的,因为它不存储数据,所以就有了ebo,空基类最优化。
空基类最优化:只有空类对象不与同一类型的其他对象或子对象分配在同一地址,就不需要为其分配空间。
可能有人说空类这只有1字节的大小,对程序影响不大,先不说积少成多,c++有一个特性字节对齐,比如说:
class Empty{}
class Test :public Empty
{
int a;
}
sizeof(Test)为多少呢,其为4,但是在没有ebo的情况下呢,其为8,这是因为c++的字节补齐,默认类的大小必须为类中大小最大的成员的倍数。
所以Empty大小为1,int大小为4,一共为5,所以补齐为8(4的倍数),当然你可以选择自定义对齐方式,但这不在这篇文章的讨论范围内。
1的补齐浪费空间是巨大的,当类中其他成员大小越大,补齐的空间就可能越大,这就是ebo的意义所在。
比如说:
class Empty {};
class Empty2:public Empty{};
class Empty3:public Empty2{};
class Empty4:public Empty, public Empty2{};//特殊案例
sizeof(Empty)=1
sizeof(Empty2)=1
sizeof(Empty3)=1
sizeof(Empty4)=1 //这个1的意义不一样,这个也是我写这篇东西的原因
前三个都好理解,他们就是ebo的体现(),那个1的大小只是为了标识类的存在,但是Empty4呢,其实它继承了两次Empty,不符合ebo的规则,父类Empty所以并没有进行最优化,但是实际上Empty4的大小仍为1,我上网查阅了其他博客,但是几乎没有提到这个问题,甚至有出现这个例子,其中还有将其大小写为2的,也许是不同编译器的处理问题,在这里我为跟我一样得到结果为1的同志们解释一下为何为1。
首先,类的继承,继承的原理为何,简单而言,其实就是往类中添加一个隐藏的被继承类的对象,并且编译器将被继承类的中被继承的成员当作继承类的成员,但是ebo只继承中会被使用,被包含时也不会使用。
比如
class Empty {};
class IncludeEmpty
{
public:
int a;
Empty empty;
};
class ExtendEmpty :Empty
{
public:
int a;
};
sizeof(IncludeEmpty)=8
sizeof(ExtendEmpty)=4
那么回来看Empty4,
class Empty4: public Empty,public Empty2{}
Empty4同时继承了Empty和Empty2,而Empty2又继承了Empty,所以实际上Empty4继承了两次Empty,为了查看父类地址,我往类中添加了几个函数(函数是不占类的大小的)
class Empty
{
public:
void pEmpty() { std::cout << "&Empty =" << this << std::endl; }
};
class Empty2 :public Empty
{
public:
void pEmpty2() { std::cout << "&Empty2=" << this << std::endl; }
};
class Empty4 :public Empty2, public Empty
{
public:
void pEmpty4() { std::cout << "&Empty4=" << this << std::endl; }
void pEmpty2_Empty() { std::cout << "Empty2::pEmpty() "; Empty2::pEmpty(); }
void pEmpty() { std::cout << "Empty::pEmpty() "; Empty::pEmpty(); }
};
然后产生对象:empty4
empty4.pEmpty4() 输出:&Empty4=00B9FB00
empty4.pEmpty2() 输出:&Empty2=00B9FB00
empty4.pEmpty2_Empty() 输出:&Empty2::pEmpty() &Empty=00B9FB00
empty4.pempty() 输出:Empty::pEmpty() &Empty=00B9FB01
发现父类Empty2中的Empty与父类Empty的地址不一致,那么将会产生1的大小(sizeof的结果其实是尾地址-首地址),其实这里已经没有在使用ebo,那1的大小是为了区分两个不同的Empty而存在的和前面的三个例子不同,
如果将Empty4改成如下就一目了然了:
class Empty4 :public Empty2, public Empty
{
public:
int a;
void pEmpty4() { std::cout << "&Empty4=" << this << std::endl; }
void pEmpty2_Empty() { std::cout << "Empty2::pEmpty() "; Empty2::pEmpty(); }
void pEmpty() { std::cout << "Empty::pEmpty() "; Empty::pEmpty(); }
};
此时sizeof(Empty4)=8
很明显,空间超出了4而因为补齐而变为8。这就验证了ebo的准则:只有空类对象不与同一类型的其他对象或子对象分配在同一地址,就不需要为其分配空间。
接下来再谈谈同一地址的问题,即使是同一类型的对象只有分配地址不同,也不需要为其分配空间
再来两个类:
class Integer
{
public:
int a;
}
class Empty5:public Empty,public Integer,public Empty2{}
那么sizeof(Empty5)等于?
其实sizeof(Empty5)=4,仅仅只有Integer类的大小,也就是使用了ebo,因为Empty与Empty2中Empty分配的地址不同。这里Integer必须继承,而且位置必须在Empty与Empty2之间,只有这样才会影响它们的地址,
因为对象的初始化是从父对象开始的,而且初始化顺序与继承顺序一致。
结尾:
那么这篇文章的探讨到此为止,这是本人第一篇文章,只是一时兴起还请大家多多包含,指出错误。