目录
今天学习了《面试系列之C++的对象布局》一文,收获很多。记录下自己的实验和心得体会。
原文链接:面试系列之C++的对象布局 - 知乎
1.linux查看内存对象模型的方法
查看内存模型的方法:
clang -Xclang -fdump-record-layouts -stdlib=libc++ -std=gnu++11 xxx.cpp
为了测试方便,写了个简单的脚本./xxx.sh:
clang -Xclang -fdump-record-layouts -stdlib=libc++ -std=gnu++11 -c $1
./xxx.sh xxx.cpp
使用方法:./xxx.sh xxx.cpp
查看虚函数表的方法
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -std=gnu++11 -c xxx.cpp
2.没有虚函数的内存布局
示例代码:
struct Base {
Base() = default;
~Base() = default;
void FuncA() {}
int a;
int b;
};
int main() {
Base base1;
return 0;
}
内存布局:
Base有两个int型的成员变量,共计4+4=8个字节
3.有虚函数的内存布局
示例代码:
struct BaseA {
BaseA() = default;
virtual ~BaseA() = default;
void FuncA() {}
virtual void FuncB() {}
int a;
int b;
};
int main() {
BaseA base2;
return 0;
}
内存布局:
BaseA vtable pointer是一个虚函数表指针vtable pointer,占用8个字节
BaseA共计占用8+4+4=16个字节。
vtable:
共有3个虚函数,其中,虚析构函数有两个版本,没有深入研究,网上的一种说法是:
BaseA::~BaseA() [complete],当在栈上构造该对象时,析构时用这个版本;
BaseA::~BaseA() [deleting],当在堆上构造该对象时,析构时用这个版本。
1)子类不重写父类的虚函数
示例代码:
struct BaseA {
BaseA() = default;
virtual ~BaseA() = default;
void FuncA() {}
virtual void FuncB() {}
int a;
int b;
};
struct Derived1: public BaseA {
};
int main() {
Derived1 derived1;
return 0;
}
内部布局:
Derived1没有新增成员变量或者成员函数,所以,它的内存模型和其父类的模型一致。派生类从其基类拷贝一份内存中的数据。
vtable:
pic1表示派生类所有的虚函数,从中可知,派生类继承了基类的虚函数void BaseA::FuncB()。
pic2表示派生类自己的虚函数。
2)子类重写父类的虚函数
示例代码:
struct BaseA {
BaseA() = default;
virtual ~BaseA() = default;
void FuncA() {}
virtual void FuncB() {}
int a;
int b;
};
struct Derived2: public BaseA {
virtual void FuncB() override {}
};
int main() {
Derived2 derived2;
return 0;
}
vtable:
从图中可以看出来,
派生类重写了虚函数FuncB,此时,派生类实现的版本void Derived2::FuncB()会覆盖基类的版本void BaseA::FuncB()。
4.多重继承的内存布局
1)子类不重写父类的虚函数
示例代码:
struct BaseA {
BaseA() = default;
virtual ~BaseA() = default;
void FuncA() {}
virtual void FuncB() {}
int a;
int b;
};
struct BaseB {
BaseB() = default;
virtual ~BaseB() = default;
void FuncA() {}
virtual void FuncC() {}
int a;
int b;
};
struct Derived3: public BaseA, BaseB {
};
int main() {
Derived3 derived3;
return 0;
}
内部布局:
从图中可以看出,派生类分别从其两个父类拷贝一份内存中的数据,派生类占用的字节数为16+16=32个字节。内存的排布按照声明时的顺序排列。struct Derived4: public BaseA, BaseB {};BaseA在前,BaseB在后。
vtable:
派生类的vtable也由两个父类的vtable组成。由于Derived3和BaseA的起始地址相同,所以offset_to_top为0。BaseB的起始地址和Derived3的起始地址相差16个字节,所以offset_to_top为16。
2)子类重写父类的虚函数
示例代码:
struct BaseA {
BaseA() = default;
virtual ~BaseA() = default;
void FuncA() {}
virtual void FuncB() {}
int a;
int b;
};
struct BaseB {
BaseB() = default;
virtual ~BaseB() = default;
void FuncA() {}
virtual void FuncC() {}
int a;
int b;
};
struct Derived4: public BaseA, BaseB {
virtual void FuncB() override {}
virtual void FuncC() override {}
virtual void FuncD() {}
};
struct Derived5: public BaseB, BaseA {
virtual void FuncB() override {}
virtual void FuncC() override {}
};
int main() {
Derived4 derived4;
Derived5 derived5;
return 0;
}
vtable:
结论1:多重继承时,派生类新增的虚函数,会被添加到排在最前面的vtable中,且只添加一份。从图中可以看出,派生类新增的虚函数FuncD,被添加到了BaseA的vtable中,没有被添加到BaseB的vtable中。
Derived4重写了FuncB和FuncC,为什么其vtable中BaseA部分有FuncB和FuncC,而BaseB部分只有FuncC?
对于这个问题,没有找到确切的答案,下面是我的理解,不正确的话不吝指教。
step1:struct Derived4: public BaseA, BaseB {};,声明Derived4时,BaseA排在前面。先构造(这么说可能不是很准确)BaseA,发现FuncB被重写了,所以用void Derived4::FuncB()去覆盖void BaseA::FuncB()。
step2:对于BaseA来说,void Derived4::FuncC()是新增的虚函数,根据前文的“结论1”,把void Derived4::FuncC()添加到BaseA的vtable中。
step3:然后构造BaseB部分,发现FuncC被重写了,所以用void Derived4::FuncC()覆盖之前的版本。void Derived4::FuncB()对于BaseB来说是新增的函数,根据“结论1”,只需要添加到排在前面的对象的vtable中,由于BaseA部分已经有一个void Derived4::FuncB()了,所以,不需要再添加到BaseB部分了。
为了验证上述结论,在声明Derived4时交换继承的顺序,改为struct Derived5: public BaseB, BaseA {};那么按照step3中的分析,先构造BaseB部分,则会用void Derived5::FuncB()覆盖之前的版本,把void Derived5::FuncC()作为新增的函数添加进来。然后构造BaseA部分,只需要用void Derived5::FuncB()覆盖之前的版本就可以了。下图中的vtable布局,和上述分析吻合。