C++对象模型_第五章_C++函数语义学

作者:Mr_WangAndy日期:2025/11/20

本文介绍C++对象模型之函数语义学,揭露C++成员函数的神秘面纱,探究C++多态的底层原理,虚继承,类型转换原理。

文章目录

  • 第5章 函数语义学
    • 5.1 普通成员函数调用方式
    • 5.2虚成员函数、静态成员函数调用方式
      • 5.2.1 虚成员函数调用方式
        • 5.2.2 静态成员函数调用方式
    • 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.5单继承虚函数趣味性测试和回顾
      • 5.5.1 单个继承下的虚函数
          • 1 从虚函数表中继承虚函数
            * 2 派生类指针指向派生类对象
            * 3 基类指针指向派生类对象
        • 5.5.2 回顾虚函数地址
    • 5.6 多继承虚函数、第二基类,虚析构
      • 5.6.1 多继承下的虚函数,this指针偏移问题
        • 5.6.2 删除用第二基类指针new出来的继承类对象
    • 5.7多继承第二基类虚函数支持、虚继承带虚函数
      • 5.7.1 多重继承第二基类对虚函数支持的影响
          • 情况1:通过指向第2基类的指针,调用继承类的虚函数
            * 情况2:指向派生类的指针,调用第二基类中的虚函数
            * 情况3:允许虚函数的返回值类型有所变化
        • 5.7.2 虚继承下的虚函数
    • 5.8 RTTI运行时类型识别回顾与存储位置介绍
      • 5.8.1 RTTI回顾
        • 5.8.2 RTTI类型原理
        • 5.8.3 dynamic_cast<>原理
    • 5.9函数调用、继承关系性能说
      • 5.9.1 函数调用中编译器对循环代码的优化
        • 5.9.2 继承关系深度增加,开销增加
        • 5.9.3 虚函数导致的开销增加
    • 5.10指向成员函数的指针及vcall进一步谈
      • 5.10.1 指向成员函数的指针和指向成员变量的指针
        • 5.10.2 vcall
    • 5.11 内联函数
      • 5.11.1 内联回顾
        • 5.11.2 inline扩展细节
          • 5.11.2.1形参被实参取代
            * 5.11.2.2 内联函数中局部变量尽量少使用
            * 5.11.2.3 inline失败的情况

第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 的典型步骤(单继承)

  1. 取出对象首部的 vptr
  2. 按槽位从 vtable 读出函数入口地址
  3. 把 this 放到调用约定规定的位置(x86: ECX;x64: RCX)
  4. 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++函数语义学》 是转载文章,点击查看原文


相关推荐


Android多SDK合并为单个JAR包的完整指南
安卓蓝牙Vincent2025/11/19

痛点 多 SDK 分散:每个功能模块单独提供 JAR,用户需要逐一集成和管理 调用复杂:不同模块间存在依赖和包名冲突,用户在项目中使用不方便 升级维护困难:每次更新都要同步多个 JAR,容易出错 一、核心原理 1.1 最推荐的方案:源码合并 + 下层库作为“源码目录”加入 多 SDK 合并时,最终有效的构建环境只有顶层 SDK,因此最稳定的方式是: 源码合并(sourceSets) + 移除模块依赖 + 将下层 SDK 作为源码目录引入(而不是 module) Android St


Python 的内置函数 super
IMPYLH2025/11/17

Python 内建函数列表 > Python 的内置函数 super Python 的内置函数 super() 是一个非常重要的内置函数,主要用于在子类中调用父类(超类)的方法。这个函数在面向对象编程中扮演着关键角色,特别是在处理继承关系时。 基本用法 super() 最常见的用法是在子类的初始化方法中调用父类的初始化方法: class Parent: def __init__(self, name): self.name = name class Child(


Python 的内置函数 pow
IMPYLH2025/11/16

Python 内建函数列表 > Python 的内置函数 pow Python 的内置函数 pow() 是一个用于计算幂运算的强大工具。它有两种基本用法,可以计算数值的幂次方,也支持进行模运算。 基本语法 pow(base, exp) 参数说明 base:底数,可以是整数或浮点数exp:指数,可以是整数或浮点数 使用示例 基本幂运算: pow(2, 3) # 返回8 (2的3次方) pow(2.5, 2) # 返回6.25 (2.5的平方) 带模运算: pow(2,


🔥 “Solo Coding”的近期热度解析(截至 2025 年末)
LeonGao2025/11/15

🧠 一、概念回顾 Solo Coding 并不是新词,但在过去一年随着 AIGC 编程辅助工具(如 Copilot、Cursor、TabNine、ChatGPT Code Interpreter) 的普及,它被重新定义为: 一个人独立开发完整系统,但具备团队级效率。 这与传统意义的“独立开发者(Indie Developer)”不同,核心在于借助 AI 的合作力量,实现准团队式的个人生产力爆发。 📈 二、热度增长趋势 时间区间关键词趋势


Python 的内置函数 iter
IMPYLH2025/11/14

Python 内建函数列表 > Python 的内置函数 iter Python 的内置函数 iter() 用于创建一个迭代器对象,它可以将可迭代对象(如列表、元组、字典、集合等)转换为迭代器,从而支持逐个访问元素的操作。 基本语法 iter(iterable, sentinel) iterable:必需参数,表示要转换为迭代器的可迭代对象(如列表、字符串等)。sentinel:可选参数,用于指定迭代停止的条件值(主要用于自定义迭代行为)。 示例说明 基本用法(无 sentinel


python+uniapp基于微信小程序的垃圾分类信息系统
Q_Q5110082852025/11/13

目录 项目介绍本项目具体实现截图开发技术大数据类设计开发的基本流程是:论文大纲结论源码lw获取/同行可拿货,招校园代理 :文章底部获取博主联系方式! 项目介绍 本文介绍了一款基于微信小程序的垃圾分类信息系统。该系统旨在帮助用户更便捷地了解垃圾分类知识,提高垃圾分类的准确性和效率。通过微信小程序平台,用户可以随时随地查询各类垃圾的归属类别,并获取详细的分类指导。 本研究首先进行了用户需求分析,明确了平台应具备的功能和特点。然后,利用微信小程序开发技术,设计并实现了该平台。课题主要分为


HTML 的 <svg> 标签
hubenchang05152025/11/11

#HTML 的 <svg> 标签 请查看 HTML 元素帮助手册 了解更多 HTML 元素。 如果 svg 不是根元素,svg 元素可以用于在当前文档(比如说,一个 HTML 文档)内嵌套一个独立的 svg 片段。这个独立片段拥有独立的视口和坐标系统。 #属性 请查看 HTML 元素的全局属性 了解 HTML 元素的全局属性。 #示例 <svg width="300" height="300" viewBox="0 0 300 300" xmlns="http://www.w3.org/


LRU 缓存的设计与实现
前似锦2025/11/9

目录 一、LRU 缓存的核心诉求 二、数据结构选型与设计思路 1. 双向链表:维护访问顺序的 “时间轴” 2. 哈希表:实现 key 的 O (1) 寻址 3. 组合设计:“哈希表 + 双向链表” 的协同工作 三、代码实现 1. 类结构定义 2. get 方法实现:查询并更新访问顺序 3. put 方法实现:插入、更新与容量控制 四、复杂度与边界场景分析 1. 时间复杂度 2. 边界场景处理 五、测试验证与工程价值 六、总结 在高并发与大数据场景中,缓存是提


Less-8 GET-Blind-Boolean Based-Single Quotes
泷羽Sec-静安2025/11/7

GET-盲注-基于布尔值-单引号 Less-8 代码分析 关键特征对比 特征Less-5Less-8SQL结构id='$id'id='$id'成功时“You are in”“You are in”失败时显示错误 mysql_error()什么都不显示注入类型报错注入/布尔盲注纯布尔盲注核心区别(关键!) // Less-5 else { echo 'You have an error in your SQL syntax'; print_r(mysql_error()); /


Python 的内置函数 format
IMPYLH2025/11/2

Python 内建函数列表 > Python 的内置函数 format Python 的内置函数 format() 是一个功能强大的字符串格式化工具,它提供了灵活且可读性强的格式化方式。该函数主要通过两种形式使用: 作为字符串对象的方法: "格式化字符串".format(参数) 这是最常见的用法,在字符串内部使用 {} 作为占位符,然后通过 format() 方法传入参数进行替换。 作为独立的内置函数: format(value, format_spec) 这种形式主要用于对单个值进

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2025 聚合阅读