本文介绍C++对象模型之函数语义学,揭露C++成员函数的神秘面纱,探究C++多态的底层原理,虚继承,类型转换原理。
文章目录
- 第5章 函数语义学
-
- 5.1 普通成员函数调用方式
- 5.2虚成员函数、静态成员函数调用方式
-
- 5.2.1 虚成员函数调用方式
- 5.2.2 静态成员函数调用方式
- 5.2.1 虚成员函数调用方式
- 5.3虚函数地址转换---vcall引入
- 5.4 静动态类型、绑定,多态实现
-
- 5.4.1 静态类型和动态类型
- 5.4.2 静态绑定和动态绑定
- 5.4.3 继承的非虚函数坑
- 5.4.4 虚函数的动态绑定
- 5.4.5 重新定义虚函数的缺省参数
- 5.4.6 C++多态体现
- 5.4.1 静态类型和动态类型
- 5.5单继承虚函数趣味性测试和回顾
-
- 5.5.1 单个继承下的虚函数
-
- 1 从虚函数表中继承虚函数
* 2 派生类指针指向派生类对象
* 3 基类指针指向派生类对象
- 1 从虚函数表中继承虚函数
- 5.5.2 回顾虚函数地址
-
- 5.5.1 单个继承下的虚函数
- 5.6 多继承虚函数、第二基类,虚析构
-
- 5.6.1 多继承下的虚函数,this指针偏移问题
- 5.6.2 删除用第二基类指针new出来的继承类对象
- 5.6.1 多继承下的虚函数,this指针偏移问题
- 5.7多继承第二基类虚函数支持、虚继承带虚函数
-
- 5.7.1 多重继承第二基类对虚函数支持的影响
-
- 情况1:通过指向第2基类的指针,调用继承类的虚函数
* 情况2:指向派生类的指针,调用第二基类中的虚函数
* 情况3:允许虚函数的返回值类型有所变化
- 情况1:通过指向第2基类的指针,调用继承类的虚函数
- 5.7.2 虚继承下的虚函数
-
- 5.7.1 多重继承第二基类对虚函数支持的影响
- 5.8 RTTI运行时类型识别回顾与存储位置介绍
-
- 5.8.1 RTTI回顾
- 5.8.2 RTTI类型原理
- 5.8.3 dynamic_cast<>原理
- 5.8.1 RTTI回顾
- 5.9函数调用、继承关系性能说
-
- 5.9.1 函数调用中编译器对循环代码的优化
- 5.9.2 继承关系深度增加,开销增加
- 5.9.3 虚函数导致的开销增加
- 5.9.1 函数调用中编译器对循环代码的优化
- 5.10指向成员函数的指针及vcall进一步谈
-
- 5.10.1 指向成员函数的指针和指向成员变量的指针
- 5.10.2 vcall
- 5.10.1 指向成员函数的指针和指向成员变量的指针
- 5.11 内联函数
-
- 5.11.1 内联回顾
- 5.11.2 inline扩展细节
-
- 5.11.2.1形参被实参取代
* 5.11.2.2 内联函数中局部变量尽量少使用
* 5.11.2.3 inline失败的情况
- 5.11.2.1形参被实参取代
- 5.11.1 内联回顾
第5章 函数语义学
5.1 普通成员函数调用方式
编译器调用普通类的成员函数时候,默认传递了一个this指针。
C++语言设计的时候有一个要求:要求对这种普通成员函数的调用不应该比全局函数差。基于这种设计要求,编译器内部实际上是将对成员函数的调用转为了对全局函数的调用。
普通调用方式1:栈对象访问虚函数
下面代码中,直接不通过指针,而是使用栈上的对象调用类成员函数,就是普通调用,即使调用的是虚函数,也不会从虚函数表中访问,
1 MYACLS myc1; 2 myc1.myvirfunc(); // 这是普通函数调用 3
通过汇编代码可以看出,这是普通调用。

普通调用方式2:直接使用用类名::虚函数名(),这种写法压制了虚拟机制,不再通过查询虚函数表来调用,等价于直接调用一个普通函数。
1 virtual void myvirfunc() 2 { 3 printf("myvirfunc()被调用,this = %p\n", this); 4 //myvirfunc2(); 居然走虚函数表指针调用 5 MYACLS::myvirfunc2(); //直接调用虚函数,效率更高。 6 // 这种写法压制了虚拟机制,不再通过查询虚函数表来调用 7 // 这种用类名::虚函数名()明确调用虚函数的方式等价于直接调用一个普通函数; 8 } 9 10
测试源码如下:
1 class MYACLS 2 { 3 public: 4 int m_i; 5 void myfunc(int abc) 6 { 7 mystfunc(); 8 } 9 virtual void myvirfunc() 10 { 11 printf("myvirfunc()被调用,this = %p\n", this); 12 //myvirfunc2(); 居然走虚函数表指针调用 13 MYACLS::myvirfunc2(); //直接调用虚函数,效率更高。 14 // 这种写法压制了虚拟机制,不再通过查询虚函数表来调用 15 // 这种用类名::虚函数名()明确调用虚函数的方式等价于直接调用一个普通函数; 16 } 17 virtual void myvirfunc2() 18 { 19 printf("myvirfunc2()被调用,this = %p\n", this); 20 } 21 22 //静态成员函数,不需要this参数 23 //static int m_si; 24 static void mystfunc() 25 { 26 printf("mystfunc()被调用\n"); 27 } 28 }; 29 30 void test() 31 { 32 // 1 虚成员函数调用方式 33 MYACLS myc1; 34 myc1.myvirfunc(); // 这是普通函数调用 35 36 MYACLS* mc = new MYACLS(); 37 mc->myvirfunc(); 38 // 编译器视角 39 // (*mc->vptr[0])(mc);从编译器 视角调用 虚函数 40 // a vptr 虚函数表指针 41 // b [0] 虚函数表中第一项,表示myvirtual 地址 42 // c 传递一个参数进去,就是this, 也是编译器给加的 43 // d * 就得到了虚函数的地址。 44 } 45 46
5.2虚成员函数、静态成员函数调用方式
5.2.1 虚成员函数调用方式
一个类中,如果有虚函数,直接new一个类对象,然后通过对象访问虚函数。
1class MyC 2{ 3public: 4 int m_i; 5 void myfunc(int abc) 6 { 7 8 } 9 virtual void myvirfunc() 10 { 11 printf("myvirtual()被调用 this = %p\n", this); 12 myvirfunc2(); 13 } 14 virtual void myvirfunc2() 15 { 16 printf("myvirfunc2()被调用,this = %p\n", this); 17 } 18 19 static void mystfunc() 20 { 21 printf("mystrfunc()被调用\n"); 22 } 23 24 25}; 26 void test() 27 { 28 MYACLS* mc = new MYACLS(); 29 mc->myvirfunc(); 30 // 编译器视角 31 // (*mc->vptr[0])(mc);从编译器 视角调用 虚函数 32 // a vptr 虚函数表指针 33 // b [0] 虚函数表中第一项,表示myvirtual 地址 34 // c 传递一个参数进去,就是this, 也是编译器给加的 35 // d * 就得到了虚函数的地址。 36 } 37 38
验证:从下面汇编代码可以看出,通过虚函数表来访问虚函数。

5.2.2 静态成员函数调用方式
静态函数特性:
1 静态成员函数没有this指针,这点很重要,并且静态成员函数存在代码段;
2 在静态函数中,无法直接使用类中普通非静态成员变量。
3 静态成员函数不能 加 const virtual 等关键字。
4 可以用类对象调用,但不一定要用类对象调用。
5 静态成员函数等同于非静态成员函数,有的需要提供回调这种场合,可以将静态成员函数作为回调函数。
静态函数调用方式如下:
1 class MyC 2 { 3 public: 4 int m_i; 5 void myfunc(int abc) 6 { 7 8 } 9 virtual void myvirfunc() 10 { 11 printf("myvirtual()被调用 this = %p\n", this); 12 myvirfunc2(); 13 } 14 virtual void myvirfunc2() 15 { 16 printf("myvirfunc2()被调用,this = %p\n", this); 17 } 18 static void mystfunc() 19 { 20 printf("mystrfunc()被调用\n"); 21 } 22 }; 23 24 void test() 25 { 26 // 静态成员函数调用方式 27 // 方式1: 28 MyC myc1; 29 myc1.mystfunc(); 30 // 方式2 31 MyC* mc = new MyC(); 32 mc->mystfunc(); 33 // 方式3: 34 MyC::mystfunc(); 35 // 以上三种方式相同,在编译器眼中都一样。 36 // 方式4: 37 ((MyC*)0)->mystfunc(); 38 // 但是这种方式调用带参数的普通成员函数会出错,因为没有对象空间,调用普通成员函数需要传递this指针,没有空间也就没有this指针 39 // ((MyC*)0)->myfunc(11); // 编译不会报错,但是在myfunc() 函数中报错 40 } 41 42
5.3虚函数地址转换—vcall引入
可以将vcall理解为一段代码,调用虚函数时,需要vcall这段代码协助,才能实现真正的调用。
vcall 的典型步骤(单继承)
- 取出对象首部的 vptr
- 按槽位从 vtable 读出函数入口地址
- 把 this 放到调用约定规定的位置(x86: ECX;x64: RCX)
- call 该地址
1class MC53 2{ 3public: 4 virtual void myvirfunc1() 5 { 6 } 7 virtual void myvirfunc2() 8 { 9 } 10}; 11 12int main() 13{ 14 printf("MYACLS::myvirfunc1()地址=%p\n", &MC53::myvirfunc1); // 15 printf("MYACLS::myvirfunc2()地址=%p\n", &MC53::myvirfunc2); 16 17 MC53 *pmyobj = new MC53(); 18 return 1; 19} 20 21

printf输出的02B1456h,vcall(一堆代码)经过转换之后,就可以调用真正的虚函数了。
5.4 静动态类型、绑定,多态实现
5.4.1 静态类型和动态类型
静态类型:对象定义时候,编译器就确定好的;动态类型:在运行时才确定的。下面代码演示了静态类型和动态类型。
1class Base 2{ 3public: 4 void func() 5 { 6 cout << "Base::myfunc()" << endl; 7 } 8 virtual void myvirtual(int value = 1) 9 { 10 cout << "Base::myvirtual() , value = " << value << endl; 11 } 12}; 13class Derive : public Base 14{ 15public: 16 void myfunc() // 普通成员函数 17 { 18 cout << "Derive::myfunc()" << endl; 19 } 20 virtual void myvirtual(int value = 2) 21 { 22 cout << "Derive::myvirtual() , value = " << value << endl; 23 } 24}; 25 26void test() 27{ 28// 静态类型:对象定义时候,编译器就确定好的 29 Base base; // 静态类型base,没有动态类型,因为没有指针也没有引用。 30 Derive derive; // pbase 静态类型依旧是Base * , 至少目前没有动态类型,因为它不是指针不是引用 31 Base* pbase; // pbase静态类型依旧是Base *, 没有动态类型,因为指针没有指向其他 32 Base* pbase2 = new Derive(); // pbase2 静态类型依旧是Base *, 动态类型是Derive 33 Base* pbase3 = new Derive(); // pbase3 静态类型,动态类型是Derive 34 // 动态类型:对象目前所指向的类型 35 // 一般只有指针或引用才有动态类型的说法,而且 一般都是指父类的指针或者引用。 36 // 动态类型在运行过程中可以改变,比如: 37 pbase = pbase2; // pbase2 动态类型是Derive 38 pbase = pbase3; // pbase 动态类型改变为Derive 39} 40 41
5.4.2 静态绑定和动态绑定
静态绑定:绑定的是静态类型,所对应的函数或者属性依赖于对象的静态类型,发生在编译期间。
动态绑定:绑定的动态类型,所对应的函数或者属性依赖于对象的动态类型,发生在运行期间。
普通成员函数是静态绑定,virtual 函数是动态绑定。
缺省参数一般都是静态绑定。
1 class Base 2 { 3 public: 4 void func() 5 { 6 cout << "Base::myfunc()" << endl; 7 } 8 virtual void myvirtual(int value = 1) 9 { 10 cout << "Base::myvirtual() , value = " << value << endl; 11 } 12 }; 13 class Derive : public Base 14 { 15 public: 16 void myfunc() // 普通成员函数 17 { 18 cout << "Derive::myfunc()" << endl; 19 } 20 virtual void myvirtual(int value = 2) 21 { 22 cout << "Derive::myvirtual() , value = " << value << endl; 23 } 24 }; 25 26 void test() 27 { 28 Base* pbase = new Derive(); 29 pbase->myvirtual(); // 这里输出值为1; 30 // Derive::myvirtual() , value = 1 31 } 32 33
5.4.3 继承的非虚函数坑
不能在子类中重新定义一个继承来的非虚函数,否则会覆盖父类的
即使在子类中重写了非虚函数,调用还是父类的非虚函数。
举例如下:Base中的myfunc()是非虚函数,在派生类中覆盖了myfunc(),也不会产生多态。
1 class Base 2 { 3 public: 4 void myfunc() //普通成员函数 5 { 6 cout << "Base::myfunc()" << endl; 7 } 8 virtual void myvirfunc(int value = 1) 9 { 10 cout << "Base::myvirfunc(),value = " << value << endl; 11 } 12 }; 13 14 class Derive :public Base 15 { 16 public: 17 void myfunc() //普通成员函数 18 { 19 cout << "Derive::myfunc()" << endl; 20 } 21 virtual void myvirfunc(int value = 2) 22 { 23 cout << "Derive::myvirfunc(),value = " << value << endl; 24 } 25 }; 26 27 void test() 28 { 29 Derive derive; 30 Derive* pderive = &derive; 31 pderive->myfunc(); 32 //Derive::myfunc() 33 34 Base* pbase = &derive; 35 pbase->myfunc(); 36 // 预期结果:"Derive::myfunc()" 37 //实际结果:Base::myfunc() 38 } 39 40
5.4.4 虚函数的动态绑定
基类的指针或者引用指向了派生类,然后基类指针调用虚函数,这是动态绑定,产生多态。
1 Base* pbase = new Derive(); 2 pbase->myvirtual(); 3 Derive der; 4 Base& pb = der; 5 pb.myvirtual(); // Derive::myvirtual() , value = 1 6 7
5.4.5 重新定义虚函数的缺省参数
不要重新定义虚函数的缺省参数的值。因为虚函数的缺省参数的值,还是父类中的值,见上面的例子。
5.4.6 C++多态体现
C++中多态性体现在两个方面:代码层面和表现形式。
代码层面:
有基类指针或者引用指向派生类,调用派生类中重写的方法。
5.5单继承虚函数趣味性测试和回顾
5.5.1 单个继承下的虚函数
1 从虚函数表中继承虚函数
基类指针指向基类,在编译时候就确定好了,访问基类的虚函数表。
1class Base 2{ 3public: 4 virtual void f() { cout << "Base::f()" << endl; } 5 virtual void g() { cout << "Base::g()" << endl; } 6 virtual void h() { cout << "Base::h()" << endl; } 7}; 8void test() 9{ 10 Base* bp = new Base(); 11 bp->f(); // 从虚函数表中调用虚函数 12 bp->g(); 13 bp->h(); 14} 15 16

从汇编角度,Base类指针指向base类对象,也会调用虚函数表中的虚函数。
2 派生类指针指向派生类对象
派生类指向派生类,在编译期间就确定好了,要访问派生类虚函数表中的虚函数。
1class Base 2{ 3public: 4 virtual void f() { cout << "Base::f()" << endl; } 5 virtual void g() { cout << "Base::g()" << endl; } 6 virtual void h() { cout << "Base::h()" << endl; } 7}; 8 9class Derive : public Base 10{ 11public: 12 virtual void i() { cout << "Derive::i()" << endl; } 13 virtual void g() { cout << "Derive::g()" << endl; } 14 void myfunc() {} 15}; 16 17 18void test() 19{ 20 Derive* myd = new Derive(); 21 myd->f(); 22 myd->g(); // 重写 23 myd->h(); 24 myd->i(); // 派生类自己的 25} 26 27

3 基类指针指向派生类对象
基类指针指向派生类对象;在运行期间确定访问的派生类的虚函数表,产生多态。
1class Base 2{ 3public: 4 virtual void f() { cout << "Base::f()" << endl; } 5 virtual void g() { cout << "Base::g()" << endl; } 6 virtual void h() { cout << "Base::h()" << endl; } 7}; 8class Derive : public Base 9{ 10public: 11 virtual void i() { cout << "Derive::i()" << endl; } 12 virtual void g() { cout << "Derive::g()" << endl; } 13 void myfunc() {} 14}; 15void test() 16{ 17 Base* bp = new Derive(); 18 bp->f(); 19 bp->g(); 20 bp->h(); 21} 22

5.5.2 回顾虚函数地址
通过上边实验,可以确定,编译期间就编译出了虚函数表和虚函数指针。并且,在编译阶段,编译器在构造函数中插入了vptr赋值的代码,当创建对象时候,因为要执行对象的构造函数,此时vptr就被赋值。
只有发生多态时候,才会在编译期间确定调用哪个虚函数表中的虚函数。
5.6 多继承虚函数、第二基类,虚析构
5.6.1 多继承下的虚函数,this指针偏移问题
下面的代码,在执行delete pb2时候出现异常,因为没有执行。
1 class Base1 2 { 3 public: 4 virtual void f() { cout << "Base1::f()" << endl; } 5 virtual void g() { cout << "Base1::f()" << endl; } 6 virtual void h() { cout << "Base1::f()" << endl; } 7 8 virtual ~Base1() 9 { 10 int abc = 1; 11 abc = 2; 12 } 13 }; 14 15 class Base2 16 { 17 public: 18 virtual void k() { cout << "Base2::k()" << endl; } 19 }; 20 21 class Derive : public Base1, public Base2 22 { 23 public: 24 // 重写 基类的 g() 25 virtual void j() { cout << "Derive::j()" << endl; } 26 virtual ~Derive() 27 { 28 } 29 }; 30 31 int main() 32 { 33 // 1 多继承下的虚析构函数 34 Base2* pb2 = new Derive(); 35 delete pb2; 36 return 0; 37 } 38 39 void test() 40 { 41 Base2* pb2 = new Derive(); 42 //编译器视角: 43 Derive* temp = new Derive(); 44 Base2* pb2 = (Base2*)((char*)temp + sizeof(Base2)); 45 } 46 47
delete pb2; 这条语句,当执行delete pb2时候,程序出现了异常。
异常原因:如下图所示,只调用了pb2中的析构函数,只删除了从pb2开始到结束的内存部分,而没有删除Base1类的内存,所以出现了异常。

解决方法,在Base2中增加虚析构函数。
5.6.2 删除用第二基类指针new出来的继承类对象
从上边的分析可以知道,当调用delete pb2时候,应该删除Derive中所有的内存部分,而不是只删除Base2对象及以下的部分内存。
如何同时删除Base1的内存呢?执行delete pb2时候,系统的动作应该是:
- 如果Base2里没有析构函数,编译器会直接删除以pb2开头到结尾的这段内存,一定报异常,因为相当于内存泄漏,只删除了内存的一部分。
- 如果Base2中有一个析构函数,是一个普通的析构函数,而不是virtual析构函数,出现的情况也是只删除base2到内存结尾的一段内存,还没没有删除Base1内存。
- 如果Base2中有一个析构函数,是虚析构函数,会进行如下调用:
先调用~Derive()
再调用~Base2()
最后调用~Base1()
修改后的代码,在Base2类中加入虚析构函数。
1class Base1 2{ 3public: 4 virtual void f() { cout << "Base1::f()" << endl; } 5 virtual void g() { cout << "Base1::f()" << endl; } 6 virtual void h() { cout << "Base1::f()" << endl; } 7 8 virtual ~Base1() 9 { 10 int abc = 1; 11 abc = 2; 12 } 13}; 14 15class Base2 16{ 17public: 18 virtual void k() { cout << "Base2::k()" << endl; } 19 virtual ~Base2() {} 20}; 21 22class Derive : public Base1,public Base2 23{ 24public: 25 virtual void j() { cout << "Derive::j()" << endl; } 26 virtual ~Derive() 27 { 28 29 } 30}; 31 32
上边类的虚函数表:
Base1:

Base2 :

Derive:

画出完整的派生类对象内存模型。

trunk使用:
Derive 类的第二个虚函数表发现了trunk字样,一般出现在多重继承中,trunk作用:
- 调整this指针
- 调用Derive 析构函数
5.7多继承第二基类虚函数支持、虚继承带虚函数
5.7.1 多重继承第二基类对虚函数支持的影响
子类多重继承了几个父类,子类就有几个虚函数表。多重继承下有几种情况,第二个或者后续的基类会对虚函数的支持产生影响。
this指针的作用是指向对象的首地址,这样就能通过偏移来调用对象的成员函数或成员属性。一个子类继承了多个父类,子类想要调用父类的方法就需要通过this指针的偏移来调用。下面的3种调整this指针的情况:
1 class Base 2 { 3 public: 4 virtual void f() { cout << "Base::f()" << endl; } 5 virtual void g() { cout << "Base::g()" << endl; } 6 virtual void h() { cout << "Base::h()" << endl; } 7 8 virtual ~Base() { 9 10 } 11 12 virtual Base *clone() const 13 { 14 return new Base(); 15 } 16 17 }; 18 19 class Base2 20 { 21 public: 22 virtual void hBase2() { 23 24 cout << "Base2::hBase2()" << endl; 25 } 26 27 virtual ~Base2() { 28 29 } 30 31 virtual Base2 *clone() const 32 { 33 return new Base2(); 34 } 35 }; 36 37 class Derive :public Base,public Base2 { 38 public: 39 virtual void i() { cout << "Derive::i()" << endl; } 40 virtual void g() { cout << "Derive::g()" << endl; } 41 void myselffunc() {} //只属于Derive的函数 42 43 virtual ~Derive() { 44 45 } 46 virtual Derive *clone() const 47 { 48 return new Derive(); 49 } 50 }; 51 52
情况1:通过指向第2基类的指针,调用继承类的虚函数
1 Base2 *pb2 = new Derive(); // this指针先调整到derive对象内存中的Base2位置, 2 delete pb2; // 调用继承类的虚析构函数。 3 4
情况2:指向派生类的指针,调用第二基类中的虚函数
1 Derive* pd2 = new Derive(); 2 pd2->k(); // 调用Base2中的虚函数,this指针调整到第二基类 3 4
情况3:允许虚函数的返回值类型有所变化
1 void test() 2 { 3 Base2* pbase1 = new Derive(); 4 Base2* pbase2 = pbase1->clone(); 5 // 执行clone时候,pb1首先会调整this指针, 6 // 指向Derive对象首地址,这样调用的是Derive版本的clone 7 } 8 9
5.7.2 虚继承下的虚函数
研究虚继承下的虚函数。
1class Base 2{ 3public: 4 int m_base; 5 virtual void f() 6 { 7 cout << "ff" << endl; 8 } 9 10 virtual ~Base() 11 { 12 cout << "this is a ~Base" << endl; 13 } 14}; 15 16class Derive : virtual public Base 17{ 18public: 19 virtual void k() 20 { 21 cout << "kk" << endl; 22 } 23 virtual ~Derive() 24 { 25 cout << "this is a ~Derive" << endl; 26 } 27 int m_derive; 28}; 29void test() 30{ 31 Derive* pderive = new Derive(); // 0x00beb550 // 虚基类指针 32 // 0x00beb554 // 派生类的虚函数表 33 pderive->m_derive = 2; // 0x00BEB558 // 派生类的成员属性 34 Base* pbase2 = (Base*)pderive; // 0x00beb55c// 基类的虚函数表 35 pbase2->m_base = 1; // 0x00BEB560,// 基类属性的位置 36} 37 38

上图是虚继承下派生类Derive的内存布局,
虚函数表和虚基类表中每个元素的作。首先介绍虚基类表( v b t a b l e @ )的作用,解决虚继承下的“菱形继承”问题,确保虚基类在派生类对象中只有一个实例。编译器通过这个表来定位虚基类子对象相对于 v b p t r 的偏移量。然后,虚函数表( vbtable@)的作用,解决虚继承下的“菱形继承”问题,确保虚基类在派生类对象中只有一个实例。编译器通过这个表来定位虚基类子对象相对于vbptr的偏移量。 然后,虚函数表( vbtable@)的作用,解决虚继承下的“菱形继承”问题,确保虚基类在派生类对象中只有一个实例。编译器通过这个表来定位虚基类子对象相对于vbptr的偏移量。然后,虚函数表(vftable@)用于实现C++的多态性。当通过基类指针或引用调用虚函数时,程序会通过对象的 vfptr 找到对应的虚函数表,并从中调用正确的函数版本(可能是基类的,也可能是派生类重写的)。
最后,虚函数表和虚基类表中每一项的作用如下:
1class sp2::Derive size(16): 2 +--- 3 0 | {vbptr} // 指向虚基类表的指针 4 4 | m_derivei // Derive 类的成员变量 5 +--- 6 +--- (virtual base sp2::Base) 7 8 | {vfptr} // 指向虚函数表的指针 812 | m_basei // Base类的成员变量 9 +--- 10 11sp2::Derive::$vbtable@: 12 0 | 0 // 值 0: 这个值表示从 vbptr 的地址到当前对象(Derive 对象)起始地址的偏移量。因为 vbptr 本身就位于 Derive 对象的起始处(偏移量为 0),所以这个值是 0。 13 1 | 8 (Derived(Derive+0)Base) // 这个值表示从vbptr的地址到虚基类Base子对象起始地址的偏移量。根据内存布局,Base子对象位于偏移量8的位置。当通过Derive对象的指针或引用访问Base成员时,编译器会查阅此表,计算出Base子对象的正确地址。 14 15sp2::Derive::$vftable@: 16 | -8 // 这是 “top offset”,它表示从vfptr的地址到整个对象起始地址的偏移量。它表示从 vfptr 的地址到整个对象(Derive 对象)起始地址的偏移量。在 Derive 对象中,vfptr 位于偏移量 8 的位置,而对象起始于偏移量 0。因此,从 vfptr 的位置需要回退 8 个字节才能到达对象的顶部,所以偏移量是 -8。 17 0 | &sp2::Base::f // 保存的基类的f地址,因为Derive没有override f . 18 1 | &sp2::Derive::{dtor} // 虚析构函数地址。 19 20
5.8 RTTI运行时类型识别回顾与存储位置介绍
5.8.1 RTTI回顾
C++运行时识别RTTI,要求父类中必须至少有一个虚函数;如果父类中没有虚函数,那么RTTI就不准确。RTTI靠typeid().name() 和 dynamic_cast运算符来体现。
1class Base 2{ 3public: 4 Base() 5 { 6 cout << "这是基类的Base()" << endl; 7 } 8 9 virtual void b() 10 { 11 cout << "this is base b" << endl; 12 } 13}; 14 15class Derive : public Base 16{ 17public: 18 Derive() 19 { 20 cout << "这是基类的Derive()" << endl; 21 } 22 virtual void b() 23 { 24 cout << "this is Derive b" << endl; 25 } 26}; 27 28 Base* pb = new Derive(); 29 pb->b(); 30 Derive pd; 31 Base& yb = pd; 32 cout << typeid(*pb).name() << endl; // class Derive 33 cout << typeid(yb).name() << endl;// class Derive 34 35 Derive* pderive = dynamic_cast<Derive*>(pb); 36 if (pderive != NULL) 37 { 38 cout << "pb 实际上一个Derive 类型" << endl; 39 } 40 41
5.8.2 RTTI类型原理
RTTI原理如下图所示。
vptr指向rtti相关地址;rtti指向了type_info表,该表偏移3个地址,指向type_info首地址,然后再通过type_info首地址访问。
1class Base 2{ 3public: 4 Base() 5 { 6 //cout << "这是的Base()" << endl; 7 } 8 9 virtual void b() 10 { 11 cout << "this is base b" << endl; 12 } 13}; 14 15class Derive : public Base 16{ 17public: 18 Derive() 19 { 20 //cout << "这是的Derive()" << endl; 21 } 22 virtual void f() { cout << "Base::f()" << endl; } 23 virtual void g() { cout << "Base::g()" << endl; } 24 virtual void h() { cout << "Base::h()" << endl; } 25}; 26 27void test() 28{ 29 Base* pb = new Derive(); 30 cout << sizeof(pb) << endl; 31 cout << sizeof(Derive) << endl; 32 printf("tp2地址为:%p\n", &pb); // pb指向 33 34 int* pvptr = (int*)pb; 35 int* vptr = (int*)(*pvptr); // 虚函数表中第一个虚函数地址 36 printf("虚函数表首地址为:%p\n", vptr); 37 printf("虚函数表首地址之前一个地址为:%p\n" ,vptr - 1); // 虚函数表中RTTI地址 38 int* prtinfo = (int*)(*(vptr - 1)); // 指向RTTI表中首地址 39 prtinfo += 3; 40 int* ptypeinfo = (int*)(*prtinfo); // 指向typeinfo对象首地址 41 const std::type_info* ptypeinfoaddr = (const std::type_info*)ptypeinfo; 42 printf("ptypeinfo地址为:%p\n", ptypeinfoaddr); 43 cout << ptypeinfoaddr->name() << endl; // class 44} 45

虚函数表解释:
1class sp4::Derive size(4): 2 +--- 3 0 | +--- (base class sp4::Base) 4 0 | | {vfptr} 5 | +--- 6 +--- 7 8sp4::Derive::$vftable@: 9 | &Derive_meta // 指向Derive类相关的运行时类型信息(RTTI)数据指针,位于-1的位置,这个位置保存了RTTICompleteObjectLocator结构体。 10 | 0 11 0 | &sp4::Base::b 12 1 | &sp4::Derive::f 13 2 | &sp4::Derive::g 14 3 | &sp4::Derive::h 15 RTTICompleteObjectLocator结构体内容如下: 16struct RTTICompleteObjectLocator { 17 unsigned long signature; // 签名,用于标识这是一个有效的 RTTI 结构。在32位下通常为0,64位下为1。 18 unsigned long offset; // 从当前 vfptr 到完整对象顶部的偏移量 (offset-to-top)。 19 unsigned long cdOffset; // 构造函数位移偏移量 (constructor displacement offset),用于虚基类。 20 TypeDescriptor* pTypeDescriptor; // TypeDescriptor 存储了关于类型信息,最主要的就是类型的名称(例如 "class sp4::Derive")。typeid 操作符主要就是通过这个指针找到 TypeDescriptor,然后返回其内部的 std::type_info 对象。 21 ClassHierarchyDescriptor* pClassDescriptor; // 这个指针作用,主要用于实现 dynamic_cast转换,它描述了完整的类继承链, 22}; 23 24
ClassHierarchyDescriptor 保存的信息:
1 当前类的“族谱”,即这个类所有的基类,本例中是Base.
2 每个基类相对于Derive对象起始地址的偏移量。
5.8.3 dynamic_cast<>原理
从上面了解到,RTTICompleteObjectLocator中保存了类型信息,typeid(*pb).name() 就是从这个结构体中的TypeDescriptor中获取的。同时,RTTICompleteObjectLocator中还保存了类型转换使用的信息。转换流程如下:
1Derive* pdy = dynamic_cast<Derive*>(pb); 2
1 dynamic_cast 首先要求Base多态,且至少有一个虚函数。
2 从虚函数表中的-1个位置找到RTTI结构体—RTTICompleteObjectLocator。
3 在 RTTI结构体 RTTICompleteObjectLocator中的pClassDescriptor指针,指向了ClassHierarchyDescriptor结构体,这个结构体中包含了两个信息:
1 当前类Derive的所有基类,上面是Base类。
2 每个基类相对于Derive对象起始地址偏移量。
4 执行类型检查和转换。检查pb 指向的真实类型,通过RTTI获取到是Derive类型,要转换的目标类型也是Derive类型,合法。
5 返回计算结果:这个例子中转换成功了,返回Derive地址。pb指向的就是Derive的首地址。
5.9函数调用、继承关系性能说
5.9.1 函数调用中编译器对循环代码的优化
在debug和release下面各不相同,release下编译器会对代码进行优化,比如for循环的优化。
在for中加入了一个printf函数,与没有加之前时间对比,加入printf之后,时间增加了几百毫秒。
1namespace _namesp1 2{ 3 __int64 mytest(int mv) 4 { 5 __int64 icout = 0; 6 for (int i = 1; i < 1000000; ++i) 7 { 8 icout += 1; 9 // printf("------"); // 加上这句话,整体时间增加了几百毫秒 10 } 11 // 在release下面,可能将上面三行for 优化为如下: 12 // icout += 循环多少次的和 13 return icout; 14 } 15 16 void func() 17 { 18 clock_t start, end; 19 __int64 mycount = 1; 20 start = clock(); 21 22 for (int i = 0; i <= 1000; i++) 23 { 24 mycount += mytest(i); 25 } 26 // 在release下面,编译器可能将for优化为如下: 27 // mycout += 循环1000次。 28 end = clock(); 29 30 cout << "用时:" << end - start << endl; 31 } 32} 33 34static void test01() 35{ 36 // 1 函数调用中编译器的循环代码优化 37 // debug release 38 // 优化循环,把循环优化成1条语句; 39 // 在编译器间,编译器也具有运算能力,有些运算编译器在编译期间能搞定。 40 _namesp1::func(); 41} 42 43
5.9.2 继承关系深度增加,开销增加
很多情况下,随着继承深度的增加,开销或者说执行时间也会增加;
下面例子中,C调用了B的构造函数,然后B再去调用A和A1的构造函数。
1class A 2 { 3 public: 4 A() 5 { 6 cout << "A::A()" << endl; 7 } 8 }; 9 class A1 10 { 11 public: 12 A1() 13 { 14 cout << "A1::A1()" << endl; 15 } 16 }; 17 18 class B :public A,public A1 19 { 20 public: // B的构造函数再去调用A的构造函数; 21 }; 22 class C :public B 23 { 24 public: 25 C() // 在C中,调用B的构造函数; 26 { 27 cout << "C::C()" << endl; 28 } 29 }; 30 void func() 31 { 32 C cobj; 33 34 } 35
5.9.3 虚函数导致的开销增加
一个类中有虚函数会产生虚函数表,虚函数表的增加也会导致内存的消耗。
1 class A 2 { 3 public: 4 /*A() 5 { 6 cout << "A::A()" << endl; 7 }*/ 8 virtual void myvirfunc() {} 9 }; 10 11 class B :public A 12 { 13 public: 14 }; 15 class C :public B 16 { 17 public: 18 C() 19 { 20 cout << "C::C()" << endl; 21 } 22 }; 23 24 void func() 25 { 26 C *pc = new C(); 27 } 28 29
5.10指向成员函数的指针及vcall进一步谈
5.10.1 指向成员函数的指针和指向成员变量的指针
下面分别演示了指向成员函数的指针和指向成员变量的指针。
1 class A 2 { 3 public: 4 void myfunc1(int tempvalue1) 5 { 6 cout << "tempvalue1 = " << tempvalue1 << endl; 7 } 8 9 void myfunc2(int tempvalue2) 10 { 11 cout << "tempvalue2 = " << tempvalue2 << endl; 12 } 13 14 static void mysfunc(int tempvalue) 15 { 16 cout << "A::mysfunc()静态成员函数--tempvalue = " << tempvalue << endl; 17 } 18 19 int m_i; 20 }; 21 22 void func() 23 { 24 A ma; 25 // 定义一个类的成员函数指针并给初值 26 void (A::*pmypoint)(int tmp) = &A::myfunc1; 27 // 通过类的成员函数指针来调用函数 28 (ma.*pmypoint)(12); 29 30 A* pmy = new A(); 31 (pmy->*pmypoint)(12); 32 33 // 上边的调用,从编译器视角: 34 // pmypoint(&pmy, 12); 35 36 // (2) 定义函数指针来调用类的静态函数 37 void (*pmypoint2)(int tmpvalue) = &A::mysfunc; 38 pmypoint2(22); 39 40 // 指向成员变量的指针 41 int A::*p = &A::m_i; 42 } 43 44
5.10.2 vcall
vcall = virtual call;
它代表一段要执行的代码的地址,这段代码引导咱们去执行正确的虚函数。可以把vcall看成是虚函数表,vcall{0} 表示虚表中的第一个虚函数,vcall{4}表示虚表中的第二个虚函数。
1class A 2 { 3 public: 4 void myfunc1(int tempvalue1) 5 { 6 cout << "tempvalue1 = " << tempvalue1 << endl; 7 } 8 void myfunc2(int tempvalue2) 9 { 10 cout << "tempvalue2 = " << tempvalue2 << endl; 11 } 12 13 static void mysfunc(int tempvalue) 14 { 15 cout << "A::mysfunc()静态成员函数--tempvalue = " << tempvalue << endl; 16 } 17 18 virtual void myvirfunc1(int tempvalue) 19 { 20 cout << "A::myvirfunc1()虚成员函数--tempvalue = " << tempvalue << endl; 21 } 22 23 virtual void myvirfunc2(int tempvalue) 24 { 25 cout << "A::myvirfunc2()虚成员函数--tempvalue = " << tempvalue << endl; 26 } 27 }; 28 29 void func() 30 { 31 void (A:: * myp)(int val) = &A::myvirfunc1; 32 A* ma = new A(); 33 (ma->*myp)(110); // mov dword ptr [myp],offset _np2::A::`vcall'{0}' (0961546h) 34 35 myp = &A::myvirfunc2; 36 (ma->*myp)(110); // mov dword ptr [myp],offset _np2::A::`vcall'{4}' (0961550h) 37 } 38 39
5.11 内联函数
5.11.1 内联回顾
使用inline之后,只是建议编译器使用Inline函数,同时编译器有一个比较复杂的测试算法来评估这个inline函数的复杂度。如果满足编译器的复杂度,就会用inline,如果inline函数复杂度过高,这个inline建议就会被编译器忽略。
5.11.2 inline扩展细节
5.11.2.1形参被实参取代
下面就是使用inline和未使用inline 汇编层面的区别:
1 int myfunc(int testc) 2 { 3 return testc * 3 * 3; 4 } 5 6

使用inline之后:
可以看到没有了函数调用call XXXX,而是直接进行替换并计算。
1inline int myfunc(int testc) 2{ 3 // (局部变量的使用) 4 int sum = testc * testc + 110; 5 return sum; 6} 7 8void func() 9{ 10 int a = 12; 11 int i = myfunc(12); 12 cout << i << endl; 13} 14 15
代码对应的汇编如下:

5.11.2.2 内联函数中局部变量尽量少使用
为了验证局部变量对程序的影响,现在修改inline函数,定义一个局部变量,然后返回局部变量,如下:
1inline int myfunc(int testc) 2{ 3 // (局部变量的使用) 4 // 修改之前的直接return 返回。 5 // return testc * testc + 110; 6 // 7 // 修改之后,加入sum局部变量,使用局部变量计算后,再返回。 8 int sum = testc * testc + 110; 9 return sum; 10} 11 12void func() 13{ 14 int a = 12; 15 int i = myfunc(12); 16 17 cout << i << endl; 18} 19 20

通过查看汇编发现,比之前多了两行汇编,这就是加入局部变量带来的性能开销增加。
5.11.2.3 inline失败的情况
至于编译器是否最终会调用inline,可以通过调试查看汇编代码,来确定编译器是否执行的真正的inline。
《C++对象模型_第五章_C++函数语义学》 是转载文章,点击查看原文。