【从 C 向 C++ 进阶】- 类 - 20. C++ 对象模型

从 C 向 C++ 进阶系列导航


1. C++ 对象模型

class 类对象的内存分布与 struct 结构体对象的内存分布相同,即遵循内存对齐原则。同时,类对象的内存中只保存成员变量,而成员函数保存在代码段。

那么程序中是怎么通过成员对象来调用对应的成员函数呢?实际上,在调用成员函数时,编译器会把对象的地址隐式传递给成员函数,成员函数中用隐式形参指针 this 来接收对象地址,这个传递过程是隐式完成的,这也是 this 指针指向调用函数对象的原因。

当发生类继承时,对象模型实际为子类的成员变量在父类的成员变量上进行叠加。即使子类继承后发生成员变量冲突,也仅是逻辑上的成员变量覆盖而非内存上的变量覆盖,通过作用域限定符可以访问父类的成员变量也说明了这一点。

  • 实验:
class Parent
{
private:
	int miVar;
	char mcVar;
	
public:
	void Print()
	{
		cout << "in Parent" << endl;
	}
};

class Child : public Parent
{
private:
	int miVar;
	float mfVar;
	
public:
	void Print()
	{
		cout << "in Child" << endl;
	}
};

int main(int argc, char *argv[])
{	
	cout << "size(Parent) = " << sizeof(Parent) << endl;	// size(Parent) = 8
	cout << "size(Child) = " << sizeof(Child) << endl;		// sizeof(Child) = 16
}

以上子类 Child 对象的内存分布如下:

miVar
mcVar
miVar
mfVar

2. 多态对象模型

当定义了虚函数时,对象模型也发生了变化。当类中存在虚函数时,编译器会为类自动添加一个指针,该指针指向一个名为虚函数表的结构,虚函数表中记录的是类中所有虚函数的函数地址。因此,含有虚函数的类比无虚函数的类大了 4 字节(由指针所占的内存大小决定)。

  • 实验:
class Parent
{
public:
	int miVar;
	virtual void Print()
	{
		cout << "in Parent" << endl;
	}
};

class Child : public Parent
{
public:
	int miVar;
	virtual void Print()
	{
		cout << "in Child" << endl;
	}
};


int main(int argc, char *argv[])
{	
	cout << "size(Parent) = " << sizeof(Parent) << endl;	        // size(Parent) = 8
	cout << "size(Child) = " << sizeof(Child) << endl;	        	// sizeof(Child) = 12
	Parent Parent_obj;
	cout << "&Parent_obj = " << &Parent_obj << endl;				// &Parent_obj = 0x7ffdd8c0
	cout << "&Parent_obj.miVar = " << &Parent_obj.miVar << endl;	// &Parent_obj.miVar = 0x7ffdd8c4
	void **p = (void**)&Parent_obj;
	cout << "*p = " << *p << endl;									// *p = 0x400c78,虚函数表地址
}

虚函数表指针在类对象模型的最前面,且每个含虚函数的类都有一个与之一一对应的虚函数表。虚函数表存放在只读存储区(.rodata 段),在编译期形成,由编译器创建与维护。

虚函数表的创建过程:编译器编译到类的实现时,编译发现类中存在虚函数,于是把在类模型前添加一个虚函数表指针,然后在只读存储区创造一个虚函数表结构,每编译到一个虚函数时,就把虚函数的函数地址记录到表中。

虚函数表的访问过程:创建类对象时,在构造函数执行完后,虚函数表指针指向该类对应的虚函数表。当使用父类指针(或引用)访问子类对象的成员函数时,编译器会检查该成员函数是否被记录在虚函数表中。如果不在,则直接调用成员函数,这里的函数调用在编译期已确定;如果在,则再访问虚函数表中对应的虚函数地址,这里的函数调用在程序运行期确定。因此,调用虚函数时会比调用普通的成员函数多一次地址访问,使用虚函数程序效率会有所下降,所以不能盲目定义虚函数。

怎么理解函数调用在程序运行期确定?
一般地,在程序编译时,通过函数调用表达式可以确定具体调用的函数地址。但虚函数是特殊的函数调用,需要通过虚函数表再确定具体调用的函数地址。通过汇编指令可以看到,在编译期间,编译器只是对“调用虚函数表中的函数”进行编译,但具体会调用到哪个函数地址编译器是不知情的,因为不同的类有着各自的虚函数表,表中记录不同的虚函数地址。只有在程序运行时,CPU 才可以真正地通过类对象对应的虚函数表访问其中的虚函数地址。这就是为什么同一条函数调用表达式得到的调用结果不同,也就是为什么虚函数的调用需要二次寻址。这一行为被称为动态联编或动态绑定,是多态的核心思想!

发布了60 篇原创文章 · 获赞 36 · 访问量 5939

猜你喜欢

转载自blog.csdn.net/qq_35692077/article/details/97690621
今日推荐