类的内存布局
看下面的程序中,输出的结果会是what?
class Base
{
public:
//Base();
~Base();
virtual void f() {}
static Base origin;
int a, b, c;
};
int main(int argc, char const *argv[])
{
printf("%p\n", &Base::a);
printf("%p\n", &Base::b);
printf("%p\n", &Base::c);
Base* base = new Base;
printf("%p\n", &base->a);
printf("%p\n", &base->b);
printf("%p\n", &base->c);
return 0;
}
运行结果
0x8
0xc
0x10
0x1f4a038
0x1f4a03c
0x1f4a040
分析
Inside C++ Object Model第三章中有这样的介绍:
- 取一个类的非静态数据成员的地址,将会得到它在class中的offset。
- 取一个对象的非静态数据成员的地址,将会得到它在内存中的真正地址。
&Base::a = 0x8,在64bit系统下正好是一个指针的大小,Base包含虚函数,可以vptr是在类的起始位置。
虚拟继承
虚基类的两种实现方式
-
Microsoft编译器引入所谓的virtual base class table,每一个类如果有一个或多个虚基类,就会有编译器安插一个指针,指向虚基类表,而真正的虚基类指针放在表格中。
clipboard.png -
第二种方法是在virtual function table中放置virtual base class的offset
clipboard1.png
虚函数表到底在哪
因为在程序里每个类只需要一个vtbl拷贝,所以编译器肯定会遇到一个棘手的问题:
把它放在哪里。大多数程序和程序库由多个object(目标)文件连接而成,但是每个object
文件之间是独立的。哪个object文件应该包含给定类的vtbl呢?你可能会认为放在包含
main函数的object文件里,但是程序库没有main,而且无论如何包含main的源文件不会
涉及很多需要vtbl的类。编译器如何知道它们被要求建立那一个vtbl呢?
- 必须采取一种不同的方法,编译器厂商为此分成两个阵营。对于提供集成开发环境(包含编译程序和连接程序)的厂商,一种干脆的方法是为每一个可能需要vtbl的object文件生成一个vtbl拷贝。连接程序然后去除重复的拷贝,在最后的可执行文件或程序库里就为每个vtbl保留一个实例。
- 更普通的设计方法是采用启发式算法来决定哪一个object文件应该包含类的vtbl。通常启发式算法是这样的:要在一个object文件中生成一个类的vtbl,要求该object文件包含该类的第一个非内联、非纯虚拟函数(non-inline non-pure virual function)定义(也就是类的实现体)。因此上述C1类的vtbl将被放置到包含C1::~C1定义的object文件里(不是内联的函数),C2类的vtbl被放置到包含C1::~C2定义的object文件里(不是内联函数)。
实际当中,这种启发式算法效果很好。但是如果你过分喜欢声明虚函数为内联函数(参见Effective C++条款33) ,如果在类中的所有虚函数都内声明为内联函数,启发式算法就会失败,大多数基于启发式算法的编译器会在每个使用它的object 文件中生成一个类的vtbl。在大型系统里,这会导致程序包含同一个类的成百上千个vtbl拷贝!大多数遵循这种启发式算法的编译器会给你一些方法来人工控制vtbl的生成,但是一种更好的解决此问题的方法是避免把虚函数声明为内联函数。下面我们将看到,有一些原因导致现在的编译器一般总是忽略虚函数的的inline指令。
网友评论