多态(C++)下

简介: 多态(C++)

多态的原理


虚函数表


class Base
{
public:
  virtual void Test()
  {
  cout << "Test()" << endl;
  }
private:
  int _a = 0;
};
int main()
{
  Base b;
  cout << sizeof(b) << endl;
  return 0;
}


按照以往的方式计算 Base类的大小:类中成员变量只有 int类型大小四个字节,成员函数 Test不占空间,所以类的大小应该是四个字节


运行结果如下


4d34aae9215b86db4b48564b01800f89_cdcf4b2888da4c3db72d92a5bf873038.png


运行结果和预算的结果不一样,这是怎么回事呢?难道是成员函数也要计算大小吗?接下来,通过监视一探究竟


71d7f8412caa257822f053b7fbd79b64_b42e5ac296514af998f3ed27f412510f.png


原来对象类中除了成员变量之外,还有_vfptr,也就是接下来要学习的虚函数表指针,想要学习指针,就先来了解虚函数表


虚函数表:用来存放虚函数,就如上面[0]中存放的就是Test的地址;本质就是函数指针数组


27b5491fb8421a53a7a745771d682749_7e7fceaf4d394ad28443f65b8c5a56e2.png


class Car
{
public:
  virtual void Test1()
  {
  cout << "Car:Test1()" << endl;
  }
  virtual void Test2()
  {
  cout << "Car:Test2()" << endl;
  }
private:
  int _a = 0;
};
class NIO : public Car
{
public:
  virtual void Test1()
  {
  cout << "NIO:Test1()" << endl;
  }
private:
  int _b = 0;
};
int main()
{
  Car c;
  NIO et7;
  return 0;
}


a7553d0945b6d301fd371277bd746c7b_beacfe8c15d3479e843c7c1ea7b97049.png


完成重写的虚函数,表函数表对应位置覆盖成重写的虚函数


多态的原理


int main()
{
  Car c;
  NIO et7;
    //多态调用
  Car* ptr = &c;
  ptr->Test1();
  ptr = &et7;
  ptr->Test1();
    //普通调用
  ptr = &c;
  ptr->Test2();
  ptr = &et7;
  ptr->Test2();
  return 0;
}


运行结果如下


a0c369372085a5e51687370354a88dff_e1413c1ea1ef4b6d8e7ef4419694be8e.png


普通调用由调用对象类型决定:Test2()函数不是虚函数,调用类型是Car;多态调用由指针/引用指向的对象决定:Test1()函数是虚函数,两次调用指向的对象都不同


再通过汇编进行观察


97c3e7b5ea6330746528943e337b3bc6_05dc8fba0ce1493cbf792dc543446d44.png


普通调用在编译时就确定好的(静态);多态调用在编译时是不确定的(动态)运行之后在,根据调用对象指向的类型,在表函数表中找到对应的函数进行调用


20a090f759cb17ffc81f0083d319f0c4_9d3063f2b603444fb651893117ab2eb0.png


动态绑定与静态绑定


静态绑定,在程序编译期间就已经确定程序的行为,也称静态多态

动态绑定,在程序运行期间,根据具体的类型确定程序具体的行为,调用具体的函数,也称动态多态


单继承和多继承关系中的虚函数表


单继承中的虚函数表


观察下列代码


class Car
{
public:
  virtual void test1()
  {
  cout << "Car test1()" << endl;
  }
  virtual void test2()
  {
  cout << "Car test2()" << endl;
  }
private:
  int _a;
};
class NIO:public Car
{
public:
  virtual void test1()
  {
  cout << "NIO test1()" << endl;
  }
  virtual void test3()
  {
  cout << "NIO test3()" << endl;
  }
private:
  int _b;
};
int main()
{
  Car c;
  NIO et7;
  return 0;
}


834378530dbb8baaab80cfd634005d8f_68e3d50faf6246bf971283b5b6d125d1.png


通过监视窗口能够发现:在派生类对象et7中重写了test1,继承了test2,但是本身的test3却没有,在内存窗口看到一个未知的地址,可以猜测是test3函数,现在就是想办法将其打印出来进行验证


上面了解到虚函数表本质是函数指针数组,接下来通过函数指针数组将虚函数表打印出来


typedef void(*_vfptr)();
void Print_vfptr(_vfptr vft[])
{
  for (int i = 0; vft[i] != nullptr; i++)
  {
  printf("[%d]:%p->", i, vft[i]);
  vft[i]();
  }
  cout << endl;
}

通过将对象进行取址再强转 int*获取前四个字节,在类型转换 _vfptr*传递给函数进行打印


Print_vfptr((_vfptr*)*(int*)&c);
  Print_vfptr((_vfptr*)*(int*)&et7);


完整代码如下


class Car
{
public:
  virtual void test1()
  {
  cout << "Car test1()" << endl;
  }
  virtual void test2()
  {
  cout << "Car test2()" << endl;
  }
private:
  int _a;
};
class NIO:public Car
{
public:
  virtual void test1()
  {
  cout << "NIO test1()" << endl;
  }
  virtual void test3()
  {
  cout << "NIO test3()" << endl;
  }
private:
  int _b;
};
typedef void(*_vfptr)();
void Print_vfptr(_vfptr vft[])
{
  for (int i = 0; vft[i] != nullptr; i++)
  {
  printf("[%d]:%p->", i, vft[i]);
  vft[i]();
  }
  cout << endl;
}
int main()
{
  Car c;
  Print_vfptr((_vfptr*)*(int*)&c);
  NIO et7;
  Print_vfptr((_vfptr*)*(int*)&et7);
  return 0;
}


12d6953a911ce3a81abdd7dc87ee4cfa_75727010afe84054b441ddf1f78dd884.png


打印结果和预期一致


436b3350f4ba29109e03f1ba1347c8de_a952c9421a82439a9cc3a34c46528815.png


多继承中的虚函数表


class NIO
{
public:
  virtual void test1()
  {
  cout << "NIO test1()" << endl;
  }
  virtual void test2()
  {
  cout << "NIO test2()" << endl;
  }
private:
  int _a;
};
class XPENG
{
public:
  virtual void test1()
  {
  cout << "XPENG test1()" << endl;
  }
  virtual void test2()
  {
  cout << "XPENG test2()" << endl;
  }
private:
  int _b;
};
class NEA :public NIO, public XPENG
{
public:
  virtual void test1()
  {
  cout << "NEA test1()" << endl;
  }
  virtual void test3()
  {
  cout << "NEA test3()" << endl;
  }
private:
  int _c;
};
int main()
{
  NIO et7;
  XPENG p7;
  NEA car;
  return 0;
}


b63c644aa25c31fb2400d170619ddf16_cb146da990d947ca8079d58442eb0e7b.png


通过监视会发现与上面类似的疑问,派生类 NEA对象本身的虚函数存放在第一个基类 NIO中还是第二个基类 XPENG中呢?


接下来再次通过函数指针数组进行验证


typedef void(*_vfptr)();
void Print_vfptr(_vfptr vft[])
{
  for (int i = 0; vft[i] != nullptr; i++)
  {
  printf("[%d]:%p->", i, vft[i]);
  vft[i]();
  }
  cout << endl;
}
class NIO
{
public:
  virtual void test1()
  {
  cout << "NIO test1()" << endl;
  }
  virtual void test2()
  {
  cout << "NIO test2()" << endl;
  }
private:
  int _a;
};
class XPENG
{
public:
  virtual void test1()
  {
  cout << "XPENG test1()" << endl;
  }
  virtual void test2()
  {
  cout << "XPENG test2()" << endl;
  }
private:
  int _b;
};
class NEA :public NIO, public XPENG
{
public:
  virtual void test1()
  {
  cout << "NEA test1()" << endl;
  }
  virtual void test3()
  {
  cout << "NEA test3()" << endl;
  }
private:
  int _c;
};
int main()
{
  NIO et7;
  Print_vfptr((_vfptr*)*(int*)&et7);
  XPENG p7;
  Print_vfptr((_vfptr*)*(int*)&p7);
  NEA car;
  //NIO虚函数表
  Print_vfptr((_vfptr*)*(int*)&car);
  XPENG* ptr = &p7;
  //XPENG虚函数表
  Print_vfptr((_vfptr*)*(int*)ptr);
  return 0;
}

b662e4d05615c61e8ca484c7be427f54_f67f3f339bbe44f6841ede95f90cad48.png


打印结果显示:派生类本身的虚函数是存放在第一个基类的虚函数表中的

8ee14e30e1db95be82ea6c6fb9a7c5a3_51d1174ffc904b77b1a1d4cb6ec05dbc.png


目录
相关文章
|
文件存储 Docker 智能硬件
DIY nas之casaOS云系统&Home Assistant
CasaOS是一个基于Docker生态系统的开源家庭云系统,专为家庭场景而设计,它可以让您创建自己的云服务器
3126 1
|
人工智能 测试技术 程序员
3天功能开发→3小时:通义灵码2.0+DEEPSEEK实测报告,单元测试生成准确率92%的秘密
通义灵码2.0是阿里巴巴推出的一款智能编程辅助工具,借助AI的强大能力,为开发者提供高效的代码生成、单元测试自动生成及跨语言编程支持。其核心亮点包括: 通义灵码2.0不仅提升了开发效率,还降低了编程门槛,帮助初学者快速上手。强烈推荐给所有开发者! [了解更多](https://hyk52syx.blog.csdn.net/article/details/145642464?spm=1001.2014.3001.5502)
1527 23
|
存储 C++ 内存技术
解码mp4文件分别存储为pcm,yuv文件
使用FFmpeg库在C++中解码MP4文件,并将音频数据存储为PCM格式,视频数据存储为YUV格式。
333 3
解码mp4文件分别存储为pcm,yuv文件
|
存储 网络协议 网络安全
IP路由详解:网络互联的心脏
【4月更文挑战第22天】
1115 7
IP路由详解:网络互联的心脏
|
存储 JavaScript 安全
|
JavaScript Linux 应用服务中间件
Docker部署Node应用简单实践
本文将从零至一,介绍如何在云服务器上通过 Docker 容器运行一个简单的Node应用。
3116 0
|
SQL 安全 数据安全/隐私保护
DVWA Open HTTP Redirect 通关解析
DVWA Open HTTP Redirect 通关解析
|
数据安全/隐私保护
okcc呼叫中心隐藏号码如何设置?
要在 OKCC中隐藏号码,可以按照以下步骤进行设置: 1. 登录 OKCC:使用你的帐号和密码登录 OKCC 系统。 2. 进入设置页面:在 OKCC 界面上,找到设置选项或个人配置(可能标记为个人设置、个人信息或用户设置等),点击进入个人设置页面。 3. 寻找“隐藏号码”选项:在个人设置页面中,寻找与号码隐藏相关的选项。可能会显示为“隐藏呼叫者ID”、“匿名呼叫”或类似的选项。 4. 启用号码隐藏:选中“隐藏号码”选项,并确认启用该功能。 5. 应用设置:保存设置并退出个人设置页面,确保设置生效。 请注意,具体的设置流程可能会因 OKCC 版本、界面设计和提供商的设置差异而有所不
okcc呼叫中心隐藏号码如何设置?
|
Android开发
android studio找不到aar问题解决
android studio找不到aar问题解决
936 0
|
Linux
centos 查看服务器信息 版本cpu
centos 查看服务器信息 版本cpu
513 0