1. 继承体系中的内存模型
我们都知道,在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"};
};
2. 空基类优化
对于空类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对象在子类中分配内存:
-
子类单继承,比如类DerivedDeeperEmpty。
-
子类在多继承、选择第二个基类时,没有继续选择
BaseEmpty
及BaseEmpty
的子类作为父类,那么BaseEmpty
是不会被分配内存的。