探索C++对象模型

探索C++对象模型

前两篇博客主要了解了多态和继承的基础,可是当我们在学习多态和继承的时候,难免会碰到很多关于C++对象模型的问题,例如菱形继承中的数据冗余如何解决,虚基表是如何解决菱形继承中数据冗余问题等,这一篇博客我们以C++中多态与继承为基础,探索C++对象模型。

首先我们先来看看虚函数,虚函数就是在函数名前面加virtual的函数,虚函数可以解决继承的很多问题,而对于多态而言,没有虚函数就没有多态,虚函数存在虚函数表里供对象调用实现多态。

纯虚函数

纯虚函数就是在成员函数的形参后面加上=0,成员函数被声明为纯虚函数
包含纯虚函数的类被叫做**抽象类(接口类)**抽象类不能实例化出对象,只有在派生类中重新定义后,派生类才能实例化出对象。
我们用代码看看纯虚函数

class Person
{     
	virtual void Display () = 0;   
	// 纯虚函数
	protected :  
   	string _name ;        
 	 // 姓名
  }; 
  class Student : public Person{};

抽象类是强制重写的一种机制

虚函数与虚函数表

接下来我们来看看虚函数与虚函数表,虚函数是C++实现多态的方式,C++用在父类中实现虚函数,子类完成对虚函数的重写从而完成了类型的灵活调用。
下面是一个含有虚函数的类

class A
{
 virtual void fun1()
 {
  cout << "A::fun1()" << endl;
 }
 virtual void fun2()
 {
  cout << "A::fun2()" << endl;
 }
protected:
 int num;
};

在这里插入图片描述
我们可以看出当我们将一个类中的成员函数声明为虚函数后,会在编译的时候生成一张虚函数表,在内存的表现形式是在A中有一个vfptr指针,这个指针指向虚函数表,而虚函数表中存着这个类中的虚函数。
我们来总结下虚函数的使用规则

  1. 派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外)
  2. 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
  3. 只有类的成员函数才能定义为虚函数。
  4. 静态成员函数不能定义为虚函数。
  5. 如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。
  6. 不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。
  7. 最好把基类的析构函数声明为虚函数。(why?另外析构函数比较特殊,因为派生类的析构函数跟基类的析构函数名称不一样,但是构成覆盖,这里是因为编译器做了特殊处理)
  8. 构造函数不能为虚函数,虽然可以将operator=定义为虚函数,但是最好不要将operator=定义为虚函数,因为容易使用时容易引起混淆.

在看完虚函数后,我们了解虚函数可以在C++中完成重写,然而我们经常将重写,重载,重定义互相混淆,这里做出区分
在这里插入图片描述

单继承中的对象模型

接下来我们来看看单继承中的对象模型,在单继承中虚函数表在子类中是如何完成多态调用的
我们先来一段代码看看单继承中的对象模型

class A
{
 virtual void fun1()
 {
  cout << "A::fun1()" << endl;
 }
 virtual void fun2()
 {
  cout << "A::fun2()" << endl;
 }
protected:
 int num;
};

class C :public A
{
 virtual void fun1()
 {
  cout << "C::fun1()" << endl;
 }
 virtual void fun3()
 {
  cout << "C::fun3()" << endl;
 }
 virtual void fun4()
 {
  cout << "C::fun4()" << endl;
 }
};

这里我们可以预测下,A中实现了虚函数fun2与fun1,C中重写了A的fun1,自己实现了虚函数fun3,fun4我们调用监视窗口看看
在这里插入图片描述
可是我们发现在C的对象模型中虚表里面只有两个虚函数,这里其实是编译器的问题
那么我们尝试自己实现一个打印虚表的函数

void PrintVTable(int* VTable)
{
 cout << " 虚表地址" << VTable << endl;
 for (int i = 0;VTable[i] != 0; ++i)
 {
  printf(" 第%d个虚函数地址 :0X%x,->", i+1, VTable[i]);
  FUNC f = (FUNC)VTable[i];
  f();
 }
 cout << endl;

这里实现这个函数的原理是访问数组的方式去访问函数指针,取出虚表首地址后解引用进入虚表打印出每一个虚函数的地址

PrintVTable((int*)(*(int*)(&c)));

这个传参很值得解释下
首先我们拿到b的首地址,将他强转成int类型,让他读取到虚表的地址再然后对那个地址解引用,我们已经拿到虚表的首地址的内容(虚表里面存储的第一个函数的地址)了,但是此时这个变量的类型解引用后是int,不能够传入函数,所以我们再对他进行一个int的强制类型转换,这样我们就传入参数了,开始函数执行了,我们一切都是在可控的情况下使用强转,使用强转你必须要特别清楚的知道内存的分布结构。

多继承的对象模型

接下来我们剖析下多继承的对象模型,这个就没有单继承这个简单了
首先我们实现两个父类,两个父类中都有fun1与fun2,子类继承两个父类,看看子类的对象模型

#include<iostream>
#include<windows.h>
using namespace std;
class A
{
 virtual void fun1()
 {
  cout << "A::fun1()" << endl;
 }
 virtual void fun2()
 {
  cout << "A::fun2()" << endl;
 }
protected:
 int num;
};
class B
{
 virtual void fun1()
 {
  cout << "B::fun1()" << endl;
 }
 virtual void fun2()
 {
  cout << "B::fun2()" << endl;
 }
protected:
 int num;
};
class C :public A,public B
{
 virtual void fun1()
 {
  cout << "C::fun1()" << endl;
 }
 virtual void fun3()
 {
  cout << "C::fun3()" << endl;
 }
 virtual void fun4()
 {
  cout << "C::fun4()" << endl;
 }
};
typedef void(*FUNC)(void);
void PrintVTable(int* VTable)
{
 cout << " 虚表地址" << VTable << endl;
 for (int i = 0;VTable[i] != 0; ++i)
 {
  printf(" 第%d个虚函数地址 :0X%x,->", i+1, VTable[i]);
  FUNC f = (FUNC)VTable[i];
  f();
 }
 cout << endl;
}

int main()
{
 A a;
 B b;
 C c;
 PrintVTable((int*)(*(int*)(&c)));
 system("pause");
 return 0;
}

这里子类继承了A与B两张虚表,我们依旧是将虚表打印出来看看子类的对象模型
在这里插入图片描述
这里我们看到C中的fun1完成了函数的重写,虚表中继承的是A的fun2,这里很明显是继承了A的虚表,所以我们得出结论子类的虚表继承先继承的类的虚表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值