【从 C 向 C++ 进阶】- 类 - 20. 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 字节(由指针所占的内存大小决定)。

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

class Parent_novir
{
public:
	int miVar;
    void fun () {}
};

class Parent
{
public:
	int miVar;
	virtual void fun () {}
};

class Child : public Parent
{
public:
	int miVar;
	virtual void fun () {}
};


int main(int argc, char *argv[])
{	
    /* 虚函数表指针占用类对象大小 */
	cout << "size(Parent_novir) = " << sizeof(Parent_novir) << endl;	// size(Parent_novir) = 4
    cout << "size(Parent) = " << sizeof(Parent) << endl;	            // size(Parent) = 8
	cout << "size(Child) = " << sizeof(Child) << endl;	        	    // sizeof(Child) = 12

    /* 虚函数表位于类对象模型头部 */
	Parent Par_obj1;
	cout << "&Par_obj1 = " << &Par_obj1 << endl;				        // &Par_obj1 = 0x7ffdd8c0
	cout << "&Par_obj1.miVar = " << &Par_obj1.miVar << endl;	        // &Par_obj1.miVar = 0x7ffdd8c4

    /* 每个类对应唯一的虚函数表 */
	cout << "Par_obj1 virtual-table is " << *(void**)&Par_obj1 << endl;	    // Par_obj1 virtual-table is 0x400df8
    Parent Par_obj2;
	cout << "Par_obj2 virtual-table is " << *(void**)&Par_obj2 << endl;	    // Par_obj2 virtual-table is 0x400df8
    Child Ch_obj;
	cout << "Ch_obj virtual-table is " << *(void**)&Ch_obj << endl;	        // Ch_obj virtual-table is 0x400df0
}

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

假设有如下类定义:

class Parent
{
public:
	virtual void fun1 () {}
    virtual void fun2 () {}
};

class Child : public Parent
{
public:
	virtual void fun1 () {}
    virtual void fun3 () {}
};

编译后,类 Parent 和类 Child 对应的虚函数表分别如下所示:

Parent
Parent::fun1()
Parent::fun2()
Child
Child::fun1()
Child::fun3()

虚函数表的访问过程:创建类对象时,在构造函数执行完后,虚函数表指针指向该类对应的虚函数表。当使用父类指针(或引用)访问父类或子类对象的成员函数时,编译器会检查该成员函数是否被记录在虚函数表中。如果不在,则直接调用成员函数,这里的函数调用在编译期已确定;如果在,则再访问类对象对应的虚函数表中记录的虚函数地址,这里的函数调用在程序运行期确定。

因此,调用虚函数时会比调用普通的成员函数多一次地址访问,使用虚函数程序效率会有所下降,所以不能盲目定义虚函数。

怎么理解函数调用在程序运行期确定?

一般地,在程序编译时,通过函数调用表达式可以确定具体调用的函数地址。但虚函数是特殊的函数调用,需要通过虚函数表再确定具体调用的函数地址。通过汇编指令可以看到,在编译期间,虚函数的调用的编译结果为:调用实际对象对应的虚函数表中的函数,但具体会访问哪个函数地址编译器是不知情的,因为不同的类有着各自的虚函数表。只有在程序运行时,CPU 才可以真正地通过类对象对应的虚函数表访问到真正的虚函数地址。

这就是为什么同一条函数调用表达式得到的调用结果不同,也就是为什么虚函数的调用需要二次寻址。这一行为被称为动态联编或动态绑定,是多态的核心思想!


更多从 C 向 C++ 进阶系列博文

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值