Inside The C++ Object Model 学习笔记(III)

这篇将总结C++的函数语义学相关的内容( Inside The C++ Object Model, Chapter 4 )。

Nonstatic Member Function

非静态成员函数和普通函数的等价形式:

1
2
float compute(const Strategy* sp);
float Strategy::compute() const;

一般来说,非静态成员函数在经过编译时会被转化为非成员函数,转化步骤如下:

  1. 改写函数原型,参数变为this指针。
  2. 将所有成员变量的存取操作都转化为经this指针的操作。
  3. 将此函数重写为一个外部函数并导出,名称进行 Name Mangling。最后函数的调用形式也随之改变:obj.magnitude()转化为类似于magnitude_7Point3dFv(&obj)这种名称

Deep in Virtual Function

单继承下的虚函数

对于普通的类,每个类只含一个虚函数表(vtbl),其中虚函数表记录了基本信息(type_info)及各函数的地址。每个对象在编译时都会被安插虚函数指针(vptr)指向虚函数表。

对于以下类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Scala {
protected:
int type;
public:
explicit Scala(int type): type(type) {}
virtual ~Scala() {}
virtual void f() = 0; // pure virtual function
virtual int g() {
return this->type;
}
virtual void h() {
std::cout << "FUCK 0" << std::endl;
}
};
class Scalaz : public Scala {
protected:
int s2;
public:
explicit Scalaz(int s2, int type): s2(s2), Scala(type) {}
virtual ~Scalaz() {}
void f() {
std::cout << "Scalaz" << std::endl;
}
void h() {
std::cout << "FUCK => 1" << std::endl;
}
};

其中的Scala类和Scalaz类的模型如下所示:

Object Model

注意:不同的C++编译器对vtable的实现不同,vtbl的初始偏移量可能是0,也可能是-8之类的。如果编译器开启了RTTI,则vtbl里会包含type_info

现在如果调用ptr->g(),我们并不知道ptr所指对象的具体类型,但是有两点很清楚:

  • 无论ptr对应哪种对象,我们总是可以通过ptr找到对应对象的vtable
  • 无论ptr对应哪种对象,g函数的地址总是在slot 3位置

因此此调用可以转化为:(*ptr->vptr[3])(ptr)

用gdb查看运行时的vtbl(命令:i vtbl 对象名):

1
2
3
4
5
6
vtable for 'Scala' @ 0x400d70 (subobject @ 0x603010):
[0]: 0x400bf0 <Scalaz::~Scalaz()>
[1]: 0x400c2a <Scalaz::~Scalaz()>
[2]: 0x400c50 <Scalaz::f()>
[3]: 0x400b7a <Scala::g()>
[4]: 0x400c7a <Scalaz::h()>

纯虚函数为什么等于0

在C++标准中,通过使虚函数=0来定义纯虚函数,其含义是在vtbl对应的地方填上0。关于为什么设计纯虚函数,以及纯虚函数为什么为0, The Design and Evolution of C++ 中的描述是:

The curious =0 syntax was chosen over the obvious alternative of introducing a new keyword pure or abstract because at the time I saw no chance of getting a new keyword accepted. Had I suggested pure, Release 2.0 would have shipped without abstract classes. Given a choice between a nicer syntax and abstract classes, I chose abstract classes. Rather than risking delay and incurring the certain fights over pure, I used the tradition C and C++ convention of using 0 to represent “not there.” The =0 syntax fits with my view that a function body is the initializer for a function also with the (simplistic, but usually adequate) view of the set of virtual functions being implemented as a vector of function pointers.

另外一点:在MSVC中,NULL = 0;而在GCC的实现中,NULL的内部实现是__null而不是0。因此在定义纯虚函数的时候不要用NULL代替0,也不能用C++ 11的nullptr

普通多继承下的虚函数

假设派生类直接继承了n个类,则派生类中就会有n个vptr。多继承下的派生类拥有一个主要的vptr和 n-1 个次要的vptr。多重继承最左端的基类,在派生类中作为主要实体,其对应的vtbl为主要的vtbl

假设现在有下面的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Base1 {
public:
explicit Base1() {}
virtual ~Base1() {}
virtual void f() {}
virtual Base1 *clone() const {return ...}
protected:
float b1;
};
class Base2 {
public:
explicit Base2() {}
virtual ~Base2() {}
virtual void g() {}
virtual Base2 *clone() const {return ...}
protected:
float b2;
};
class Derived : public Base1, public Base2 {
public:
explicit Derived() {}
virtual ~Derived() {}
virtual Derived *clone() const {return ...}
protected:
float d;
};

在上面的继承关系中,Base1就作为主要实体。

例如,有以下调用:

1
2
3
4
5
Base1 *b1 = new Derived();
Base2 *b2 = new Derived();
...
delete b1;
delete b2;

这两个指针所指对象对应的vtbl是不同的,里面涉及多继承指针转换的问题:

  • b1不需要调整this指针(最左边的类)
  • b2需要调整this指针

b2来说,构造函数必须调整对象地址,使其指向Base2 sub-object(当然析构函数也是):

1
2
Derived *__temp = new Derived();
Base2 *b2 = __temp ? __temp + sizeof(Base1) : 0;
文章目录
  1. 1. Nonstatic Member Function
  2. 2. Deep in Virtual Function
    1. 2.1. 单继承下的虚函数
    2. 2.2. 纯虚函数为什么等于0
    3. 2.3. 普通多继承下的虚函数