0

0

介绍有关C++中继承与多态的基础虚函数类

巴扎黑

巴扎黑

发布时间:2017-09-11 11:28:47

|

1753人浏览过

|

来源于php中文网

原创

这篇文章主要给大家介绍了关于c++中继承与多态的基础虚函数类的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。

前言

本文主要给大家介绍了关于C++中继承与多态的基础虚函数类的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。

虚函数类

继承中我们经常提到虚拟继承,现在我们来探究这种的虚函数,虚函数类的成员函数前面加virtual关键字,则这个成员函数称为虚函数,不要小看这个虚函数,他可以解决继承中许多棘手的问题,而对于多态那他更重要了,没有它就没有多态,所以这个知识点非常重要,以及后面介绍的虚函数表都极其重要,一定要认真的理解~ 现在开始概念虚函数就又引出一个概念,那就是重写(覆盖),当在子类的定义了一个与父类完全相同的虚函数时,则称子类的这个函数重写(也称覆盖)了父类的这个虚函数。这里先提一下虚函数表,后面会讲到的,重写就是将子类里面的虚函数表里的被重写父类的函数地址全都改成子类函数的地址。

立即学习C++免费学习笔记(深入)”;

纯虚函数

在成员函数的形参后面写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)

抽象类不能实例化出对象。纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。

看一个例子:


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

先总结一下概念:

1.派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外)

2.基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。

3.只有类的成员函数才能定义为虚函数。

4.静态成员函数不能定义为虚函数。

5.如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。

6.不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。

7.最好把基类的析构函数声明为虚函数。(why?另外析构函数比较特殊,因为派生类的析构函数跟基类的析构函数名称不一样,但是构成覆盖,这里是因为编译器做了特殊处理)

8.构造函数不能为虚函数,虽然可以将operator=定义为虚函数,但是最好不要将operator=定义为虚函数,因为容易使用时容易引起混淆.


上面概念大家可能都会问一句为什么要这样? 这些内容在接下来的知识里都能找到答案~ 好了那么我们今天的主角虚函数登场!!!!

何为虚函数表,我们写一个程序,调一个监视窗口就知道了。

下面是一个有虚函数的类:


#include 
#include 
using namespacestd; 
 
class Base 
{ 
public: 
   virtual void func1() 
   {} 
 
   virtual void func2() 
   {} 
 
private: 
   inta; 
}; 
 
void Test1() 
{ 
   Base b1; 
} 
 
int main() 
 
{ 
   Test1(); 
   system("pause"); 
   return0; 
}

我们现在点开b1的监视窗口

这里面有一个_vfptr,而这个_vfptr指向的东西就是我们的主角,虚函数表。一会大家就知道了,无论是单继承还是多继承甚至于我们的菱形继承虚函数表都会有不同的形态,虚函数表是一个很有趣的东西。

我们来研究一下单继承的内存格局

仔细看下面代码:


#include 
#include 
using namespace std; 
 
 
class Base 
{ 
public: 
   virtual void func1() 
   { 
     cout<< "Base::func1"<< endl; 
   } 
 
   virtual void func2() 
   { 
     cout<< "Base::func2"<< endl; 
   } 
 
private: 
   inta; 
}; 
 
class Derive:public Base 
{ 
public: 
   virtual void func1() 
   { 
     cout<< "Derive::func1"<< endl; 
   } 
 
   virtual void func3() 
   { 
     cout<< "Derive::func3"<< endl; 
   } 
 
   virtual void func4() 
   { 
     cout<< "Derive::func4"<< endl; 
   } 
 
private: 
   int b; 
};

对于Derive类来说,我们觉得它的虚表里会有什么?

首先子类的fun1()重写了父类的fun1() ,虚表里存的是子类的fun1() ,接下来父类的fun2() ,子类的fun3() , fun4()都是虚函数,所以虚表里会有4个元素,分别为子类的fun1() ,父类fun2() ,子类fun3() ,子类fun4() 。然后我们调出监视窗口看我们想的到底对不对呢?

 

我预计应该是看到fun1() ,fun2() ,fun3() ,fun4()的虚函数表,但是呢这里监视窗口只有两个fun1() , fun2() ,难道我们错了????

这里并不是这样的,只有自己靠得住,我觉得这里的编译器有问题,那我们就得自己探索一下了。 但是在探索之前我们必须来实现一个可以打印虚函数表的函数。


typedef void(*FUNC)(void); 
void PrintVTable(int* VTable) 
{ 
   cout<< " 虚表地址"<", i,VTable[i]); 
     FUNC f = (FUNC)VTable[i]; 
     f(); 
   } 
 
   cout<< endl; 
} 
 
 
int main() 
{ 
   Derive d1; 
   PrintVTable((int*)(*(int*)(&d1))); 
   system("pause"); 
   return0; 
}

下图来说一下他的缘由:

唱鸭
唱鸭

音乐创作全流程的AI自动作曲工具,集 AI 辅助作词、AI 自动作曲、编曲、混音于一体

下载


我们来使用这个函数,该函数代码如下:


//单继承 
class Base 
{ 
public: 
 virtual void func1() 
 { 
  cout << "Base::func1" << endl; 
 } 
 
 virtual void func2() 
 { 
  cout << "Base::func2" << endl; 
 } 
 
private: 
 int a; 
}; 
 
class Derive :public Base 
{ 
public: 
 virtual void func1() 
 { 
  cout << "Derive::func1" << endl; 
 } 
 
 virtual void func3() 
 { 
  cout << "Derive::func3" << endl; 
 } 
 
 virtual void func4() 
 { 
  cout << "Derive::func4" << endl; 
 } 
 
private: 
 int b; 
}; 
typedef void(*FUNC)(void); 
void PrintVTable(int* VTable) 
{ 
   cout<< " 虚表地址"<", i,VTable[i]); 
     FUNC f = (FUNC)VTable[i]; 
     f(); 
   } 
  
   cout<< endl; 
} 
  
  
int main() 
{ 
   Derive d1; 
   PrintVTable((int*)(*(int*)(&d1))); //重点 
   system("pause"); 
   return0; 
}

这里我就要讲讲这个传参了,注意这里的传参不好理解,应当细细的"品味".


PrintVTable((int*)(*(int*)(&d1)));

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

最后我们来看看输出结果:



介绍有关C++中继承与多态的基础虚函数类到底打印的对不对呢? 我们验证一下: 

这里我们通过&d1的首地址找到虚表的地址,然后访问地址查看虚表的内容,验证我们自己写的这个函数是正确的。(这里VS还有一个bug,当你第一次打印虚表时程序可能会崩溃,不要担心你重新生成解决方案,再运行一次就可以了。因为当你第一次打印是你虚表最后一个地方可能没有放0,所以你就有可能停不下来然后崩溃。)我们可以看到d1的虚表并不是监视器里面打印的那个样子的,所以有时候VS也会有bug,不要太相信别人,还是自己靠得住。哈哈哈,臭美一下~

我们来研究一下多继承的内存格局

探究完了单继承,我们来看看多继承,我们还是通过代码调试的方法来探究对象模型

看如下代码:


class Base1 
{ 
public: 
 virtual void func1() 
 { 
  cout << "Base1::func1" << endl; 
 } 
 
 virtual void func2() 
 { 
  cout << "Base1::func2" << endl; 
 } 
 
private: 
 int b1; 
}; 
 
class Base2 
{ 
public: 
 virtual void func1() 
 { 
  cout << "Base2::func1" << endl; 
 } 
 
 virtual void func2() 
 { 
  cout << "Base2::func2" << endl; 
 } 
 
private: 
 int b2; 
}; 
 
 
class Derive : public Base1, public Base2 
{ 
public: 
 virtual void func1() 
 { 
  cout << "Derive::func1" << endl; 
 } 
 
 virtual void func3() 
 { 
  cout << "Derive::func3" << endl; 
 } 
 
private: 
 int d1; 
}; 
 
typedef void(*FUNC) (); 
void PrintVTable(int* VTable) 
{ 
 cout << " 虚表地址>" << VTable << endl; 
 
 for (int i = 0; VTable[i] != 0; ++i) 
 { 
  printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]); 
  FUNC f = (FUNC)VTable[i]; 
  f(); 
 } 
 cout << endl; 
} 
 
 
void Test1() 
{ 
 Derive d1; 
 //Base2虚函数表在对象Base1后面 
 int* VTable = (int*)(*(int*)&d1); 
 PrintVTable(VTable); 
 int* VTable2 = (int *)(*((int*)&d1 + sizeof (Base1) / 4)); 
 PrintVTable(VTable2); 
} 
int main() 
{ 
 Test1(); 
 system("pause"); 
 return 0; 
}

现在我们现在知道会有两个虚函数表,分别是Base1和Base2的虚函数表,但是呢!我们的子类里的fun3()函数怎么办?它是放在Base1里还是Base2里还是自己开辟一个虚函数表呢?我们先调一下监视窗口:


监视窗口又不靠谱了。。。。完全没有找到fun3().那我们直接看打印出来的虚函数表。


现在很清楚了,fun3()在Base1的虚函数表中,而Base1是先继承的类,好了现在我们记住这个结论,当涉及多继承时,子类的虚函数会存在先继承的那个类的虚函数表里。记住了!

我们现在来看多继承的对象模型:


现在我们来结束一下上面我列的那么多概念现在我来逐一的解释为什么要这样.

1.为什么静态成员函数不能定义为虚函数?

因为静态成员函数它是一个大家共享的一个资源,但是这个静态成员函数没有this指针,而且虚函数变只有对象才能能调到,但是静态成员函数不需要对象就可以调用,所以这里是有冲突的.

2.为什么不要在构造函数和析构函数里面调用虚函数?

构造函数当中不适合用虚函数的原因是:在构造对象的过程中,还没有为“虚函数表”分配内存。所以,这个调用也是违背先实例化后调用的准则析构函数当中不适用虚函数的原因是:一般析构函数先析构子类的,当你在父类中调用一个重写的fun()函数,虚函数表里面就是子类的fun()函数,这时候已经子类已经析构了,当你调用的时候就会调用不到.

现在我在写最后一个知识点,为什么尽量最好把基类的析构函数声明为虚函数??

现在我们再来写一个例子,我们都知道平时正常的实例化对象然后再释放是没有一点问题的,但是现在我这里举一个特例:

我们都知道父类的指针可以指向子类,现在呢我们我们用一个父类的指针new一个子类的对象。


//多态 析构函数 
class Base 
{ 
public: 
 virtual void func1() 
 { 
  cout << "Base::func1" << endl; 
 } 
 
 virtual void func2() 
 { 
  cout << "Base::func2" << endl; 
 } 
 
 virtual ~Base() 
 { 
  cout << "~Base" << endl; 
 } 
 
private: 
 int a; 
}; 
 
class Derive :public Base 
{ 
public: 
 virtual void func1() 
 { 
  cout << "Derive::func1" << endl; 
 } 
 virtual ~Derive() 
 { 
  cout << "~Derive"<< endl; 
 } 
private: 
 int b; 
}; 
 
void Test1() 
{ 
 Base* q = new Derive; 
 delete q; 
} 
int main() 
{ 
 Test1(); 
 system("pause"); 
 return 0; 
}

这里面可能会有下一篇要说的多态,所以可能理解起来会费劲一点。

注意这里我先让父类的析构函数不为虚函数(去掉virtual),我们看看输出结果:


这里它没有调用子类的析构函数,因为他是一个父类类型指针,所以它只能调用父类的析构函数,无权访问子类的析构函数,这种调用方法会导致内存泄漏,所以这里就是有缺陷的,但是C++是不会允许自己有缺陷,他就会想办法解决这个问题,这里就运用到了我们下次要讲的多态。现在我们让加上为父类析构函数加上virtual,让它变回虚函数,我们再运行一次程序的:


诶! 子类的虚函数又被调用了,这里发生了什么呢??  来我们老方法打开监视窗口。


刚刚这种情况就是多态,多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。这个我们下一个博客专门会总结多态.

当然虚函数的知识点远远没有这么一点,这里可能只是冰山一角,比如说菱形继承的虚函数表是什么样?然后菱形虚拟继承又是什么样子呢? 这些等我总结一下会专门写一个博客来讨论菱形继承。虚函数表我们应该已经知道是什么东西了,也知道单继承和多继承中它的应用,这些应该就足够了,这些其实都是都是为你让你更好的理解继承和多态,当然你一定到分清楚重写,重定义,重载的他们分别的含义是什么. 这一块可能有点绕,但是我们必须要掌握.

相关文章

c++速学教程(入门到精通)
c++速学教程(入门到精通)

c++怎么学习?c++怎么入门?c++在哪学?c++怎么学才快?不用担心,这里为大家提供了c++速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
Python 自然语言处理(NLP)基础与实战
Python 自然语言处理(NLP)基础与实战

本专题系统讲解 Python 在自然语言处理(NLP)领域的基础方法与实战应用,涵盖文本预处理(分词、去停用词)、词性标注、命名实体识别、关键词提取、情感分析,以及常用 NLP 库(NLTK、spaCy)的核心用法。通过真实文本案例,帮助学习者掌握 使用 Python 进行文本分析与语言数据处理的完整流程,适用于内容分析、舆情监测与智能文本应用场景。

9

2026.01.27

拼多多赚钱的5种方法 拼多多赚钱的5种方法
拼多多赚钱的5种方法 拼多多赚钱的5种方法

在拼多多上赚钱主要可以通过无货源模式一件代发、精细化运营特色店铺、参与官方高流量活动、利用拼团机制社交裂变,以及成为多多进宝推广员这5种方法实现。核心策略在于通过低成本、高效率的供应链管理与营销,利用平台社交电商红利实现盈利。

107

2026.01.26

edge浏览器怎样设置主页 edge浏览器自定义设置教程
edge浏览器怎样设置主页 edge浏览器自定义设置教程

在Edge浏览器中设置主页,请依次点击右上角“...”图标 > 设置 > 开始、主页和新建标签页。在“Microsoft Edge 启动时”选择“打开以下页面”,点击“添加新页面”并输入网址。若要使用主页按钮,需在“外观”设置中开启“显示主页按钮”并设定网址。

13

2026.01.26

苹果官方查询网站 苹果手机正品激活查询入口
苹果官方查询网站 苹果手机正品激活查询入口

苹果官方查询网站主要通过 checkcoverage.apple.com/cn/zh/ 进行,可用于查询序列号(SN)对应的保修状态、激活日期及技术支持服务。此外,查找丢失设备请使用 iCloud.com/find,购买信息与物流可访问 Apple (中国大陆) 订单状态页面。

119

2026.01.26

npd人格什么意思 npd人格有什么特征
npd人格什么意思 npd人格有什么特征

NPD(Narcissistic Personality Disorder)即自恋型人格障碍,是一种心理健康问题,特点是极度夸大自我重要性、需要过度赞美与关注,同时极度缺乏共情能力,背后常掩藏着低自尊和不安全感,影响人际关系、工作和生活,通常在青少年时期开始显现,需由专业人士诊断。

6

2026.01.26

windows安全中心怎么关闭 windows安全中心怎么执行操作
windows安全中心怎么关闭 windows安全中心怎么执行操作

关闭Windows安全中心(Windows Defender)可通过系统设置暂时关闭,或使用组策略/注册表永久关闭。最简单的方法是:进入设置 > 隐私和安全性 > Windows安全中心 > 病毒和威胁防护 > 管理设置,将实时保护等选项关闭。

6

2026.01.26

2026年春运抢票攻略大全 春运抢票攻略教你三招手【技巧】
2026年春运抢票攻略大全 春运抢票攻略教你三招手【技巧】

铁路12306提供起售时间查询、起售提醒、购票预填、候补购票及误购限时免费退票五项服务,并强调官方渠道唯一性与信息安全。

112

2026.01.26

个人所得税税率表2026 个人所得税率最新税率表
个人所得税税率表2026 个人所得税率最新税率表

以工资薪金所得为例,应纳税额 = 应纳税所得额 × 税率 - 速算扣除数。应纳税所得额 = 月度收入 - 5000 元 - 专项扣除 - 专项附加扣除 - 依法确定的其他扣除。假设某员工月工资 10000 元,专项扣除 1000 元,专项附加扣除 2000 元,当月应纳税所得额为 10000 - 5000 - 1000 - 2000 = 2000 元,对应税率为 3%,速算扣除数为 0,则当月应纳税额为 2000×3% = 60 元。

32

2026.01.26

oppo云服务官网登录入口 oppo云服务登录手机版
oppo云服务官网登录入口 oppo云服务登录手机版

oppo云服务https://cloud.oppo.com/可以在云端安全存储您的照片、视频、联系人、便签等重要数据。当您的手机数据意外丢失或者需要更换手机时,可以随时将这些存储在云端的数据快速恢复到手机中。

100

2026.01.26

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 7.9万人学习

Django 教程
Django 教程

共28课时 | 3.6万人学习

Excel 教程
Excel 教程

共162课时 | 13.7万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号