一 VS2010命令行查看虚函数表和类内存布局
以下内容引自<http://blog.csdn.net/daydreamingboy/article/details/8982563>
VS2010命令行下查看虚函数表和类内存布局
——《深度探索C++对象模型》读书札记系列
在学习多重继承下的Virtual functions时,需要分析派生类的虚函数表(vtable),但是在网上找了好几种Hack vtable方法,结果都不尽如人意。没想到MS Compiler(以VS2010为例)有打印vtable的编译选项,真是太好了!
1. 打开“Visual Studio Command Prompt (2010)”,如下
该CMD下具有VS2010命令行下的一些编译、链接等工具,例如cl.exe。
2. 编写一个cpp文件
以《深度探索C++对象模型》的160页的代码(160.cpp)为例,如下
class Base1 {
public:
Base1();
virtual ~Base1();
virtual void speackClearly();
virtual Base1* clone() const;
protected:
float data_Base1;
};
class Base2 {
public:
Base2();
virtual ~Base2();
virtual void mumble();
virtual Base2* clone() const;
protected:
float data_Base2;
};
class Derived : public Base1, public Base2 {
public:
Derived();
virtual ~Derived();
virtual Derived* clone() const;
protected:
float data_Derived;
};
int main(void)
{
return 0;
}
3、使用cl命令的/d1 reportAllClassLayout或reportSingleClassLayoutXXX选项。这里的reportAllClassLayout选项会打印大量相关类的信息,一般用处不大。而reportSingleClassLayoutXXX选项的XXX代表要编译的代码中类的名字(这里XXX类),打印XXX类的内存布局和虚函数表(如果代码中没有对应的类,则选项无效)。
举例如下
cl /d1 reportSingleClassLayoutBase1 160.cpp
运行结果下
可以看出Base1的大小为8个字节,共有3个虚函数,分别是~Base1、speackClearly和clone,对于学习上述的示例代码绰绰有余咯~~
二 具有虚函数的类的大小
以下内容引自<http://www.cnblogs.com/kanego/articles/2390238.html>
一般的书上都说,虚函数是在运行时根据对象的实际类型“动态决定”函数入口。但什么是“动态决定”呢?实际上C++编译器在实现这个功能的时候,并非真的 等到虚函数被调用时才去判断这个对象是什么类型的。下面我用一个简单的图表来说明C++编译器到底干了些什么。假设有两个类
struct Base {
virtual void f();
virtual void g();
};
struct Derived : public Base {
virtual void f();
virtual void g();
};
Base 和 Derived 各有一个虚表,分别是 VTable_B 和 VTable_D ,那么编译器是怎么通过只用一个虚表指针来实现下面的“动态”调用呢?
Base *pB = new Derived();
pB->f();
先让我们看Base和Derived对象是怎么存储的,以及两个类的虚表结构
Base: VTable_B:
------------ -------------
| vptr | | f() 入口 |
+---------+ +----------+
| Base的 | | g() 入口 |
| 数据 | -------------
------------
Derived: VTable_D:
------------ --------------
| vptr | | f() 入口 |
+---------+ +----------+
| Base的 | | g() 入口 |
| 数据 | -------------
+---------+
| Derived的|
| 数据 |
------------
pB 指针既可以指向 Base 对象,也可以指向 Derived 对象,所以 pB 本身是不确定的。但是,任何对象本身却从被 new 出来开始就是确定的,所以 Base 对象在构造时,编译器会往 vptr 中填上 VTable_B 的地址,而 Derived 对象在构造时,编译器会往 vptr 中填上 VTable_D 的地址。
等到虚函数被调用的时候,也就是 pB->f() 这行语句被执行的时候,编译器并不需要知道 pB 到底是指向 Base 还是 Derived ,它只要直接用 vptr 就能找到正确的虚表和虚函数入口了,父类和子类的虚表结构是相似的,同一个虚函数入口在父表和子表的偏移量都是一样的。
通过上面这些介绍,我想你应该能理解,为什么在单一继承的条件下,不管有多少层继承,每个对象只需一个 vptr 就行了。
多重继承的条件下,一个 vptr 行不行呢?
不行。多重继承的时候,虚函数既有来自父类1的,也有来自父类2的,所以这些虚函数入口是不能放在同一个虚表当中的。假设 Derived 除了 Base外,还继承 Base2,并且 Base2 中有两个虚函数 x() 和 y (),那么 Derived 对象的存储结构也许是这样的(只是大概,和具体编译器相关)。
Derived: VTable_D:
------------ --------------
| vptr | | f() 入口 |
+---------+ +----------+
| Base的 | | g() 入口 |
| 数据 | -------------
+---------+
| vptr2 | VTable_D2:
+---------+ -------------
| Base2的 | | x() 入口 |
| 数据 | +-----------+
+---------+ | y() 入口 |
| Derived的| -------------
| 数据 |
以下内容引自<http://www.cnblogs.com/kanego/articles/2390238.html>
本文版权归作者 kanego 和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
三 案例分析1:
如下程序:
#include <iostream>
using namespace std;
class A
{
int a;
public:
virtual void af(){}
virtual ~A(){}//如果注释,那么class D的大小变为12,这点想不通
};
class B : public A
{
int b;
public:
virtual void bf(){}
virtual ~B(){ /*cout << "B::~B()" << endl;*/ }
};
class C : public A
{
int c;
public:
virtual void cf(){}
virtual ~C(){ /*cout << "B::~B()" << endl;*/ }
};
class D :public B, public C
{
int d;
public:
virtual void df(){}
virtual ~D() { /*cout << "D::~D()" << endl;*/ }
};
int main(void)
{
cout << "A=" << sizeof(A) << endl; //result=1
cout << "B=" << sizeof(B) << endl; //result=8
cout << "C=" << sizeof(C) << endl; //result=8
cout << "D=" << sizeof(D) << endl; //result=12
//cout << "E=" << sizeof(E) << endl; //result=20
system("pause");
return 0;
}
输出结果:
A=8
B=12
C=12
D=28
请按任意键继续. . .
案例分析: 类A有一个虚函数表占4个字节,有自己int类型占4个字节,所以sizeof(A) = 8; 类B继承类A,属于单继承,所以类B只有一个虚函数表指针,占4个字节,继承自A的int变量,还有自身的int变量,所以sizeof(B) = 12; 类C与类B相似; 类D继承自B和C,有两个虚函数表指针,占据8个字节,继承自B的成员变量占8个字节,继承自C的成员变量占8个字节,自身有一个int变量占4字节,所以sizeof(D) = 28。
四 案例分析二:
空类所占内存大小:
class CBase
{
};
sizeof(CBase)=1;
为什么空的什么都没有是1呢?
c++要求每个实例在内存中都有独一无二的地址。//注意这句话!!!!!!!!!!
空类也会被实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化之后就有了独一无二的地址了。所以空类的sizeof为1。
程序:
class A
{
};
class A2
{
};
class B :public A
{
};
class D :public A, public A2
{
};
class C :public virtual B
{
};
int main()
{
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
cout << sizeof(C) << endl;
cout << sizeof(D) << endl;
system("pause");
return 0;
}
运行结果:
1
1
4
1
请按任意键继续. . .
注:B继承A存在空白基类优化现象,在空基类被继承后(单继承),由于空基类没有任何数据成员,所以让其在子类的对象布局中优化掉空基类所占用的一个字节。
说明:空类、单一继承的空类、多重继承的空类空间为1,但是虚继承涉及到虚表(虚指针),所以sizeof(C)的大小为4。
五 案例分析三:
class A
{
};
class B :public A
{
public:
virtual void f1(){}
};
class C :public A
{
public:
virtual void f1(){}
};
class D :public virtual B,C
{
};
int main()
{
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
cout << sizeof(C) << endl;
cout << sizeof(D) << endl;
system("pause");
return 0;
}
为什么sizeof(D) =12呢?
六 案例分析四:
class A
{
};
class B
{
};
class D
{
};
class E
{
};
class F
{
};
class C:public A,public B,public D,public E,public F
{
};
class M :public A, public B
{
//大小1
};
int main()
{
cout << sizeof(C) << endl;
cout << sizeof(M) << endl;
system("pause");
return 0;
}
输出结果4,1为什么呢? 多继承的情况下,为了区分各个不同的基类子对象,这些基类子对象必须具有不同的地址,所以这时候不能使用空基类优化,但单继承就可以,因为对于单继承,基类子对象与最终派生类对象地址相同的情形是允许的。
同时要注意,空基类优化只能存在于基类子对象,当空类对象作为完整对象时,是不能优化的,因为C++规定,每个完整对象都必须具有唯一的地址。空类完整对象的大小并不是只能为1,而是至少为1,有些编译器会加入内存对齐的因素,所以有些编译器的空类完整对象的大小会是4或者8等等。
注:若A含有静态数据成员,但是在c++里,静态成员被单独存储在全局区(静态区),所以它同样不影响A的大小。
总结:
class A//sizeof(A)=1
{};
class S:public A//单继承,空基类优化,sizeof(S)=4
{
int a;
};
class B//B是空类
{
//没有任何数据成员的类成为空类,这里的数据成员不仅仅包括类的成员变量
//同时还包括编译器为了某种目的引入的数据成员
//比如:为了支持多态而引入的虚指针vptr,为了支持虚继承而引入的必要的虚基类指针,而且还包括从
//基类直接或间接继承而来的上述的数据成员。
void fun(){}
};
class C
{
};
class D:public A, B//双继承,空基类优化,但是只能优化一个,sizeof(D)=8
{
int a;
};
class E:public A, B, C//三重继承,空基类优化一个,sizeof(E)=2
{
};
class F:public E//单继承且E为空,空基类优化, sizeof(F)=1;
{
void fun(){}
};
class G:public F//单继承,空基类优化,sizeof(G)=1
{
};
class M{ int a; };
class N :public M{};
class O :public N{};
class P :public N, A{};
class Q :public N, A, B{};
int main()
{
cout << "A" << sizeof(A) << endl;
cout << "S" << sizeof(S) << endl;
cout << "B" << sizeof(B) << endl;
cout << "C" << sizeof(C) << endl;
cout << "D" << sizeof(D) << endl;
cout << "E" << sizeof(E) << endl;
cout << "F" << sizeof(C) << endl;
cout << "M" << sizeof(M) << endl;
cout << "N" << sizeof(N) << endl;
cout << "O" << sizeof(O) << endl;
cout << "P" << sizeof(P) << endl;
cout << "Q" << sizeof(Q) << endl;
system("pause");
return 0;
}
//书上说:通常C++默默安插一个char到空对象内
运行结果:
A1
S4
B1
C1
D8
E2
F1
M4
N4
O4
P4
Q8
请按任意键继续. . .