1. 虚基类

1.1 虚基类作用

为了解决多继承时的命名冲突和冗余数据问题,使得派生类中只保留一份间接基类的成员。

其本质是是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class)

换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。

  • 建立对象时所指定的类称为最远派生类。
  • 虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
  • 在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中为虚基类的构造函数列出参数。如果未列出,则表示调用该虚基类的默认构造函数。
  • 在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,其他类对虚基类构造函数的调用被忽略。

1.2 虚基类作用机制

1.2.1 不使用虚继承和虚派生的问题

问题显而易见,会造成命名冲突和数据冗余,下面进行问题复现:

多继承很容易产生命名冲突,比如典型的菱形继承,类格如下图所示:

类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A-->B-->D 这条路径,另一份来自 A-->C-->D 这条路径。

在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B-->D 这条路径,还是来自 A-->C-->D 这条路径。下面是菱形继承的具体实现:

//间接基类A
class A{
protected:int m_a;
};//直接基类B
class B: public A{
protected:int m_b;
};//直接基类C
class C: public A{
protected:int m_c;
};//派生类D
class D: public B, public C{
public:void seta(int a){ m_a = a; }  //命名冲突void setb(int b){ m_b = b; }  //正确void setc(int c){ m_c = c; }  //正确void setd(int d){ m_d = d; }  //正确
private:int m_d;
};int main(){D d;return 0;
}

这段代码实现了上图所示的菱形继承,第 25 行代码试图直接访问成员变量 m_a,结果发生了错误,因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。

为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:

void seta(int a){ B::m_a = a; }

这样表示使用 B 类的 m_a。当然也可以使用 C 类的:

void seta(int a){ C::m_a = a; }

1.2.2虚继承和虚派生解决变量冗余

为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。

在继承方式前面加上 virtual 关键字就是虚继承,请看下面的例子:

//间接基类A
class A{
protected:int m_a;
};//直接基类B
class B: virtual public A{  //虚继承
protected:int m_b;
};//直接基类C
class C: virtual public A{  //虚继承
protected:int m_c;
};//派生类D
class D: public B, public C{
public:void seta(int a){ m_a = a; }  //正确void setb(int b){ m_b = b; }  //正确void setc(int c){ m_c = c; }  //正确void setd(int d){ m_d = d; }  //正确
private:int m_d;
};int main(){D d;return 0;
}

这段代码使用虚继承重新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

现在让我们重新梳理一下本例的继承关系,如下图所示:

观察这个新的继承体系,我们会发现虚继承的一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。在上图中,当定义 D 类时才出现了对虚派生的需求,但是如果 B 类和 C 类不是从 A 类虚派生得到的,那么 D 类还是会保留 A 类的两份成员。

换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。

1.3 虚基类成员的可见性

因为在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,那么仍然可以直接访问这个被覆盖的成员。但是如果该成员被两条或多条路径覆盖了,那就不能直接访问了,此时必须指明该成员属于哪个类。

以图2中的菱形继承为例,假设 A 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性:

  • 如果 B 和 C 中都没有 x 的定义,那么 x 将被解析为 A 的成员,此时不存在二义性。
  • 如果 B 或 C 其中的一个类定义了 x,也不会有二义性,派生类的 x 比虚基类的 x 优先级更高。
  • 如果 B 和 C 中都定义了 x,那么直接访问 x 将产生二义性问题。

可以看到,使用多继承经常会出现二义性问题,必须十分小心。上面的例子是简单的,如果继承的层次再多一些,关系更复杂一些,程序员就很容易陷人迷魂阵,程序的编写、调试和维护工作都会变得更加困难,因此我不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。也正是由于这个原因,C++ 之后的很多面向对象的编程语言,例如 Java、C#、PHP 等,都不支持多继承。

2. 虚函数

2.1 虚函数作用

虚函数的存在是为了实现动态联编(又称为动态绑定),其作用是:如果使用了virtual关键字,程序将根据引用或指针指向的 对 象 类 型 来选择方法,否则使用引用类型或指针类型来选择方法。

这是一种多态性。为实现多态,可分为静态联编和动态联编。函数重载就是一种静态联编,而虚函数就是动态联编,即函数调用发生在运行阶段,而不是发生在编译阶段,动态联编的效率较低。

2.2 虚函数动态绑定演示

class A{private:int i;public:A();A(int num) :i(num) {};virtual void fun1();virtual void fun2();};class B : public A{private:int j;public:B(int num) :j(num){};virtual void fun2();// 重写了基类的方法};// 为方便解释思想,省略很多代码A a(1);B b(2);A *a1_ptr = &a;A *a2_ptr = &b;// 当派生类“重写”了基类的虚方法,调用该方法时// 程序根据 指针或引用 指向的  “对象的类型”来选择使用哪个方法a1_ptr->fun2();// call A::fun2();a2_ptr->fun2();// call B::fun1();// 否则// 程序根据“指针或引用的类型”来选择使用哪个方法a1_ptr->fun1();// call A::fun1();a2_ptr->fun1();// call A::fun1();

2.3 虚函数底层实现机制

实现原理:虚函数表+虚表指针

关键字:虚函数底层实现机制;虚函数表;虚表指针

编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。

举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。看下面两种情况:

如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。

如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。

下面的图片体现了上述的底层实现机制:

2.4 虚函数的多继承

各种继承演示:https://blog.csdn.net/xp178171640/article/details/102670181#t3

2.5 虚函数注意事项

(1) 基类方法中声明了方法为虚后,该方法在基类派生类中是虚的。
(2) 若使用指向对象的引用或指针调用虚方法,程序将根据对象类型来调用方法,而不是指针的类型。
(3)如果定义的类被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚。
构造函数不能为虚函数。
基类的析构函数应该为虚函数。
友元函数不能为虚,因为友元函数不是类成员,只有类成员才能是虚函数。
如果派生类没有重定义函数,则会使用基类版本。
重新定义继承的方法若和基类的方法不同(协变除外),会将基类方法隐藏;如果基类声明方法被重载,则派生类也需要对重载的方法重新定义,否则调用的还是基类的方法。

3. 纯虚函数

3.1 纯虚函数作用

设置纯虚函数的目的是:

1,当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;

2,这个方法必须在派生类(derived class)中被实现;

因为:在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决这个问题,方便使用类的多态性,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。

c++中的虚特性(虚基类、虚函数、纯虚函数)相关推荐

  1. C++中为什么要引入抽象基类和纯虚函数?

    为什么要引入抽象基类和纯虚函数? 主要目的是为了实现一种接口的效果. 抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层. ⑴抽象类的定义:带有纯虚函数的类为抽象类. ...

  2. 转载:(C++)浅谈多态基类析构函数声明为虚函数

    原博文:https://www.cnblogs.com/AndyJee/p/4575810.html 主要内容: 1.C++类继承中的构造函数和析构函数 2.C++多态性中的静态绑定和动态绑定 3.C ...

  3. (C++)浅谈多态基类析构函数声明为虚函数

    主要内容: 1.C++类继承中的构造函数和析构函数 2.C++多态性中的静态绑定和动态绑定 3.C++多态性中析构函数声明为虚函数 1.C++类继承中的构造函数和析构函数 在C++的类继承中, 建立对 ...

  4. C++ day22 继承(二)基类指针数组通过虚方法实现智能的多态

    继承一共有三种: 公有继承 私有继承 保护继承 文章目录 公有继承 基类和派生类的关系 is-a(用公有继承表示"是一种"的关系) has-a uses-a is-like-a i ...

  5. C++的虚基类,抽象类,虚函数,纯虚函数,virtual

    虚基类 在说明其作用前先看一段代码 class A { public: int iValue; }; class B:public A { public: void bPrintf(){cout< ...

  6. 虚函数 纯虚函数 虚基类说明

    原文:http://www.cnblogs.com/ms-frank/archive/2008/01/16/1041310.html 虚基类 在说明其作用前先看一段代码 [cpp] view plai ...

  7. 38.【C++ 虚函数 纯虚函数 虚基类 (最全详解)】

    虚函数.虚基类.纯虚函数 (一).虚函数 1.什么是虚函数: 2.虚函数的格式: 3.关于虚函数的注意事项: 4.虚函数的作用: 5.虚函数访问格式 6.虚函数的各种疑难杂症 [当指针是基类.但虚函数 ...

  8. 为什么基类的析构函数是虚函数?

    1.第一段代码 #include<iostream> using namespace std; class ClxBase{ public:ClxBase() {};~ClxBase() ...

  9. 面试中常被问到(11)虚函数/纯虚函数

    虚函数 如何定义一个虚函数?在基类成员函数前加入virtual关键字,但并不代表此函数不被实现,只是说明允许基类指针调用派生类重写的此函数 一个类只要声明有虚函数或者从基类继承了虚函数,在编译过程中就 ...

  10. C#中所有对象共同的基类是System.Object

    C#中所有对象共同的基类是System.Object 转载于:https://www.cnblogs.com/boke1/p/11057080.html

最新文章

  1. 2013-12-2 学习笔记
  2. C++经典面试题(最全,面中率最高)
  3. 全球最大IXP为何选择与华为开展数据中心互联合作?
  4. spring cloud + nacos + feign调用
  5. PHP+Ajax手机移动端发红包实例
  6. 现代抽象UI素材背景3D流畅的造型(样条)|轻松地为Web创建3D体验
  7. 【测试】软件测试分类体系系统学习
  8. 不想remote的程序员跟咸鱼有什么区别?
  9. VS2012下基于Glut OpenGL GL_QUAD_STRIP示例程序:
  10. VS 2017 + EF6 + MySQL5.7 建立实体模型闪退问题
  11. Linux下Oracle的启动登陆命令、单实例启动、多实例启动
  12. the road to TCPIP(1)--TCPIP详解--数据链路层
  13. 163个人邮箱你了解多少?如何注册下载163邮箱呢?
  14. 四面体体积公式 hdu 1411
  15. 74hc165C语言程序,74hc165使用方法(74hc165功能_内部结构图_时序图)
  16. 【看表情包学Linux】Redirect 重定向 | 时间相关指令 | 文件查找 | 打包与压缩
  17. 暗月渗透实战靶场-项目六(上)
  18. 孙鑫vc++ 第六课 笔记 菜单的工作原理及编写应用
  19. es6之扩展运算符 Object.assign和 三个点(...)
  20. HTML的导航栏的写法

热门文章

  1. div的背景被body的背景遮蔽了。。。。。
  2. Android中RecyclerView的长按
  3. 先进的锂电池线性充电管理芯片BQ2057及其应用
  4. scn,headroom
  5. 「首席架构师推荐」2019年最佳云数据库
  6. 学习笔记之-51单片机特殊功能寄存器
  7. Cadence-元器件PCB封装绘制-Allegro PCB Designer使用方式
  8. 汽车机油压力传感器在发动机中发挥着怎样的重要作用?
  9. 「黑科技」无需越狱微信小程序实现iPhone通话录音!
  10. SAP那些事-理论篇-2-企业信息化的本质