赞
踩
我们都知道,在C++中,不存在大小是零的类。即便是空类,也要占据一个字节,否则无法比较两个空类对象是否是同一个对象(在C/C++中,默认使用地址来判断两个变量是否是同一个)。
class BaseEmpty {
public:
BaseEmpty() { std::cout<<"Base address: "<< this << std::endl;}
};
int main(int argc, char const *argv[]) {
BaseEmpty empty_1{};
BaseEmpty empty_2{};
assert(&empty_1 != &empty_2); // 两个空类对象的地址肯定不同
std::cout<<sizeof(BaseEmpty{})<<std::endl; // 输出 1
}
子类继承父类,可以等效地看作子类将父类的成员变量复制到自己内存模型中。比如在下面的demo中,Dervied_1
继承了父类Base
,Derived_1
的内存模型等效于Derived_2
。
class Base { public: Base() = default; private: int num_{0}; bool state_{false}; }; class Derived_1 : public Base { public: Derived_1() = default; private: std::string name_{"CPP"}; }; class Derived_2 { public: Derived_2() = default; private: int num_{0}; bool state_{false}; // 子类的成员变量 std::string name_{"CPP"}; };
对于空类BaseEmpty
,假设有另一个空类DerivedEmpty
继承自BaseEmpty
,空类DerivedDeeperEmpty
继承自DerivedEmpty
,那么DerivedDeeperEmpty
对象的大小sizeof(DerivedDeeperEmpty{})
会是几个字节???
class DerivedEmpty: public BaseEmpty {
public:
DerivedEmpty() {
std::cout<<"Derived address: "<< this << std::endl;
}
};
class DerivedDeeperEmpty: public DerivedEmpty {
public:
DerivedDeeperEmpty() {
std::cout<<"deeper address: "<< this << std::endl;
}
};
如果仍然像之前说的那种内存等效模型,那么编译器会每个空基类对象都分配内存,因此即便DerivedDeeperEmpty
是个空类,也要占用两个字节,相当于内部分别包含了BaseEmpty
、DerivedEmpty
对象,即等效为类DerivedDeeperEmpty_eq:
class DerivedDeeperEmpty_eq {
public:
DerivedDeeperEmpty_eq() = default;
private:
BaseEmpty base_1_;
DerivedEmpty base_2_;
};
sizeof(DerivedDeeperEmpty_eq); // 2
如果编译器真的是这么实现,是否很不符合直觉。因为DerivedDeeperEmpty
明明只是个空类啊!!!大小却是2个字节???如果情况再极端点,DerivedDeeperEmpty
还有更深的空子类,那么空子类的大小会不断膨胀???
是的,这种情况太不符合直觉,严重浪费内存。因此,C++标准如下规定:在空类被用作基类时,如果不给它分配内存并不会导致它被存储到与同类型对象(包括子类对象)相同的地址上,那么就可以不给它分配内存。换句话说,BaseEmpty
作为空基类时,下面两种情况,编译器不会为Basement对象在子类中分配内存:
BaseEmpty
及BaseEmpty
的子类作为父类,那么BaseEmpty
是不会被分配内存的。什么意思呢?下面写个demo。
int main(int argc, char const *argv[]) {
auto deeperDerived = DerivedDeeperEmpty{};
std::cout<<"size: "<<sizeof(deeperDerived) << std::endl;
return 0;
}
输出如下:
$ g++ ecbo.cc -o ecbo && ./ecbo
Base address: 0x7ffff124aa07
Derived address: 0x7ffff124aa07
deeper address: 0x7ffff124aa07
size: 1
发现了什么?编译器并没有为deeperDerived
的基类对象分配内存,deeperDerived
中的两个基类对象和deeperDerived
指向了同一个地址!!!最终sizeof(deeperDerived)
输出的大小也是1,也验证了这一点。
再来看一个demo。现在,增加一个新的空类DerivedSameEmpty
,同时继承了BaseEmpty
、DerivedEmpty
,然后查看DerivedSameEmpty
对象的大小。
class DerivedSameEmpty : public BaseEmpty, DerivedEmpty {
public:
DerivedSameEmpty() {
std::cout<<"DerivedSameEmpty address: "<< this << std::endl;
}
};
int main(int argc, char const *argv[]) {
auto derivedSame = DerivedSameEmpty{};
std::cout<<"size: "<<sizeof(derivedSame)<<std::endl;
return 0;
}
编译输出:
$ g++ ecbo.cc -o ecbo && ./ecbo
ecbo.cc:32:7: warning: direct base ‘BaseEmpty’ inaccessible in ‘DerivedSameEmpty’ due to ambiguity
32 | class DerivedSameEmpty : public BaseEmpty, DerivedEmpty {
| ^~~~~~~~~~~~~~~~
Base address: 0x7fffc67ec656
Base address: 0x7fffc67ec657
Derived address: 0x7fffc67ec657
DerivedSame address: 0x7fffc67ec656
size: 2
好,问题出现了,分析就要开始了。
此时会发现DerivedSameEmpty
的大小是2,符合本期开篇提出的继承体系中的内存模型,可等效为DerviedSameEmpty
的内存布局:
class DerivedSameEmpty_eq {
public:
DerivedSameEmpty_eq() = default;
private:
BaseEmpty base_1_; // 1字节
DerivedEmpty base_2_; // 1 字节
};
这个从上面的编译输出可以验证:BaseEmpty
对象base_1_
的地址是0x7fffda4735b6
,而DerivedEmpty
对象base_2_
首地址是0x7fffda4735b7
,比base_1_
增加了一个字节,并且DerviedSameEmpty
对象的地址和base_1_
相同。
「BaseEmpty对象和DerivedEmpty对象的地址为何不同?」
问题不在于DerivedSameEmpty
继承了两个空基类,在于这空基类BaseEmpty
和DerivedEmpty
是同一个类型。
编译器可以不为BaseEmpty
对象分配内存,那么BaseEmpty
对象肯定会和DerivedSameEmpty
对象同一个地址。但是,如果编译器也不为DerivedEmpty
对象分配内存,那么DerivedEmpty
对象就会和DerviedSameEmpty
对象也是同一个地址。
最终,就差不多是这个意思:
&base_1_ == &base_2_ == &derivedSame; // 三个对象的地址相同
base_1_
、base_2_
两个不同的对象,却指向了同一个地址???
即便这两个对象有相同的基类,但毕竟是不同的对象,如果指向了同一个地址,那么就无法通过地址来区分对象base_1_
和base_2_
是否是同一个对象。因此,这个时候就导致编译器无法为DerviedSameEmpty
开启EBCO,需要为DerivedSameEmpty
继承的每个空基类对象都要分配1个字节,自然,DerivedSameEmpty{}
大小就是2了,此时的内存模型就是DerivedSameEmpty_eq。
因此,在空类A
作为基类时,空类B
继承A
,空类C
继承B
,如果要触发编译器的EBCO机制,那么空类C
不能再继承A
及其子类。
如下,有空类DerivedDiffEmpty继承了两个不同的空基类,其对象的大小还是1。
class BaseAnotherEmpty { };
class DerivedDiffEmpty : public BaseEmpty, BaseAnotherEmpty { };
int main(int argc, char const *argv[]) {
sizeof(DerivedDiffEmpty{}); // 1
return 0;
}
如下,模板类Foo
中有两个类对象,当T1
、T2
有可能是空类时,按照下面这种写法会导致类Foo
对象大小为2个字节,但是如果使用EBCO
来重新设计Foo
,则可以减少Foo
对象大小。
template<typename T1, typename T2>
class Foo {
public:
// ...
private:
T1 t1_obj_;
T2 t2_obj_;
};
怎么设计?将has-a改为私有继承T1
、T2
。
由于T1
、T2
是空基类,那么私有继承能达到和has-a
一样效果,同样的可以使用T1
、T2
的成员函数,同时借助于EBCO节省了内存。
template<typename T1, typename T2>
class Foo : private T1, T2 {
public:
// ...
};
当然,也不是所有的空基类都能这么设计,以下三种情况无法使用EBCO:
T1
、T2
是非class 类型或者是union
类型时,无法继承;T1
、T2
是同一种类型,或者具有继承关系,此时会发生DerivdedSameEmpty
中警告;T1
、T2
的类型可能是final
修饰时,此时T1
、T2
是不允许继承的类。其中,1和3是硬性限制,无法突破,但2问题是可以解决的。卖个关子,欲知详情,请见下期std::tuple
的设计。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。