C++学习(6) —— 多态
文章目录
多态
- 什么叫多态?
多态( polymorphism )是面向对象设计语言的基本特征之一。仅仅是将数据和函数捆绑在一起,进行类的封装,使用一些简单的继承,还不能算是真正应用了面向对象的设计思想。多态是面向对象的精髓。多态可以简单地概括为“一个接口,多种方法”。比如说:警车鸣笛,普通人反应一般,但逃犯听见会大惊失色,拔腿就跑。
通常是指对于同一个消息、同一种调用,在不同的场合,不同的情况下,执行不同的行为 。
- 为什么需要多态性?
我们知道,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类)。它们的目的都是为了代码重用。而多态除了代码的复用性外,还可以解决项目中紧耦合的问题,提高程序的可扩展性。
如果项目耦合度很高的情况下,维护代码时修改一个地方会牵连到很多地方,会无休止的增加开发成本。而降低耦合度,可以保证程序的扩展性。而多态对代码具有很好的可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。
C++支持两种多态性:编译时多态和运行时多态。
编译时多态:也称为静态多态,我们之前学习过的函数重载、运算符重载就是采用的静态多态,C++编译器根据传递给函数的参数和函数名决定具体要使用哪一个函数,又称为静态联编。
运行时多态:在一些场合下,编译器无法在编译过程中完成联编,必须在程序运行时完成选择,因此编译器必须提供这么一套称为“动态联编”(dynamic binding)的机制,也叫动态多态。C++通过虚函数来实现动态联编。接下来,我们提到的多态,不做特殊说明,指的就是动态多态。
虚函数
虚函数的定义在一个成员函数的前面加上virtual关键字,该函数就成为虚函数
看这样一个例子:
基类和派生类中定义了同名的display函数
运行结果如下:
——给Base中的display函数加上virtual关键字修饰,得到的结果
从运行结果中我们发现,virtual关键字加入后,发生了一件“奇怪”的事情 —— 用基类指针指向派生类对象后,通过这个基类对象竟然可以调用派生类的成员函数。
而且,基类和派生类对象所占空间的大小都改变了,说明其内存结构发生了变化。
内存结构如下所示:

虚函数的实现原理
虚函数指针
当Base的display函数加上了virtual关键字,变成了一个虚函数,Base对象的存储布局就改变了。在存储的开始位置会多加一个虚函数指针,该虚函数指针指向一张虚函数表(简称虚表),其中存放的是虚函数的入口地址
Derived继承了Base类,那么创建一个Derived对象,依然会创建出一个Base类的基类子对象

在Derived类中又定义了display函数,发生了覆盖的机制(override),覆盖的是虚函数表中虚函数的入口地址

Base* p 去指向Derived对象,依然只能访问到基类的部分。用指针p去调用display函数,发现是一个虚函数,那么会通过vfptr找到虚表,此时虚表中存放的是Derived::display的入口地址,所以调用到Derived的display函数。
总结:
-
不加virtual关键字时,当Base * pbase = & derived时,此时发生向上转型。通过pbase->display()方法调用时,因为没有virtual关键字,属于静态绑定。编译器根据指针类型推断出应该调用Base类的display()方法。
-
加virtual关键字时,当Base * pbase = & derived时,此时同样发生向上转型。通过pbase->display()方法调用时,由于有virtual关键字,属于动态绑定。编译器会通过vfptr查找到虚表,根据虚表确定正确的函数地址。
虚函数的覆盖
如果一个基类的成员函数定义为虚函数,那么它在所有派生类中也保持为虚函数,即使在派生类中省略了virtual关键字,也仍然是虚函数。虚函数一般用于灵活拓展,所以需要派生类中对此虚函数进行覆盖。覆盖的格式有一定的要求:
- 与基类的虚函数有相同的函数名;
- 与基类的虚函数有相同的参数个数;
- 与基类的虚函数有相同的参数类型;
- 与基类的虚函数有相同的返回类型。
我们在派生类中对虚函数进行覆盖时,很有可能写错函数的形式(函数名、返回类型、参数个数),等到要使用时才发现没有完成覆盖。这种错误很难发现,所以C++提供了关键字override来解决这一问题。
关键字override的作用:
在虚函数的函数参数列表之后,函数体的大括号之前,加上override关键字,告诉编译器此处定义的函数是要对基类的虚函数进行覆盖。
class Base{
public:
virtual void display() const{
cout << "Base::display()" << endl;
}
private:
long _base;
};
class Derived
: public Base
{
public:
//想要在派生类中定义虚函数覆盖基类的虚函数
//很容易打错函数名字,同时又不会报错
//没有完成有效的覆盖
/* void dispaly() const{ //不会报错 */
/* void dispaly() const override //编译器会报错 */
void display() const override
{
cout << "Derived::display()" << endl;
}
private:
long _derived;
};
覆盖 总结:
(1)覆盖是在虚函数之间的概念,需要派生类中定义的虚函数与基类中定义的虚函数的形式完全相同;
(2)当基类中定义了虚函数时,派生类去进行覆盖,即使在派生类的同名的成员函数前不加virtual,依然是虚函数;
(3)发生在基类派生类之间,基类与派生类中同时定义形式相同的虚函数。覆盖的是虚函数表中的入口地址,并不是覆盖函数本身。
动态多态(虚函数机制)被激活的条件(重点*)
虚函数机制是如何被激活的呢,或者说动态多态是怎么表现出来的呢?其实激活条件还是比较严格的,需要满足以下全部要求:
- 基类定义虚函数
- **派生类中要覆盖虚函数 **(覆盖的是虚函数表中的地址信息)
- 创建派生类对象
- 基类的指针指向派生类对象(或基类引用绑定派生类对象)
- 通过基类指针(引用)调用虚函数
最终的效果:基类指针调用到了派生类实现的虚函数。(如果没有虚函数机制,基类指针只能调用到基类的成员函数)
虚函数表*
在虚函数机制中virtual关键字的含义
1、虚函数是存在的;(存在)
2、通过间接的方式去访问;(间接)
3、通过基类的指针访问到派生类的函数,基类的指针共享了派生类的方法**(共享)**
如果没有虚函数,当通过pbase指针去调用一个普通的成员函数,那么就不会通过虚函数指针和虚表,直接到程序代码区中找到该函数;
有了虚函数,去找这个虚函数的方式就成了间接的方式。
对虚函数和虚函数表有了基本认知后,我们可以思考这样几个问题(面试常考题)
1、虚表存放在哪里?
编译完成时,虚表应该已经存在;在使用的过程中,虚函数表不应该被修改掉(如果能修改,将会找不到对应的虚函数)——应该存在只读段——具体位置不同厂家有不同实现。
2、一个类中虚函数表有几张?
虚函数表(虚表)可以理解为是一个数组,存放的是一个个虚函数的地址
一个类可以没有虚函数表(没有虚函数就没有虚函数表);
可以有一张虚函数表(即使这个类有多个虚函数,将这些虚函数的地址都存在虚函数表中);
也可以有多张虚函数表(继承多个有虚函数的基类)
3、虚函数机制的底层实现是怎样的?
虚函数机制的底层是通过虚函数表实现的。当类中定义了虚函数之后,就会在对象的存储开始位置,多一个虚函数指针,该虚函数指针指向一张虚函数表,虚函数表中存储的是虚函数入口地址。
4. 三个概念的区分
重载 (overload) : 发生在同一作用域中, 当函数名称相同时 ,函数参数类型、顺序 、个数任一不同;
隐藏 (oversee) : 发生在基类派生类之间 ,函数名称相同时,就构成隐藏(参数不同也能构成隐藏);
覆盖(override): 发生在基类派生类之间,基类与派生类中同时定义返回类型、参数信息、名字都相同的虚函数,覆盖的是虚函数表中的入口地址,并不是覆盖函数本身
虚函数的限制
虚函数机制给C++提供了灵活的用法,但仍然受到了一些约束,以下几种函数不能设为虚函数:
1.构造函数不能设为虚函数
构造函数的作用是创建对象时完成数据的初始化,而虚函数机制被激活的条件之一就是要先创建对象,有了对象才能表现出动态多态。如果将构造函数设为虚函数,那此时构造未执行完,对象还没完成初始化,存在矛盾。
2.静态成员函数不能设为虚函数
虚函数的实际调用: this -> vfptr -> vtable -> virtual function,但是静态成员函数没有this指针,所以无法访问到vfptr
vfptr是属于一个特定对象的部分,虚函数机制起作用必然需要通过vfptr去间接调用虚函数。静态成员函数找不到这样特定的对象。
3.Inline函数不能设为虚函数
因为inline函数在编译期间完成替换,而在编译期间无法展现动态多态机制,所以起作用的时机是冲突的。如果同时存在,inline失效。
4.普通函数不能设为虚函数
虚函数要解决的是对象多态的问题,与普通函数无关,普通函数式非成员函数,不能设置为虚函数。
虚函数的各种访问情况分析
虚函数机制的触发条件中规定了要使用基类指针(或引用)来调用虚函数,那么其他的调用方式会是什么情况呢?
- 通过派生类对象直接调用虚函数
并没有满足动态多态触发机制的条件,此时只是Derived中定义display函数对Base中的display函数形成了隐藏。
- 在构造函数和析构函数中访问虚函数
#include<iostream>
using namespace std;
class Grandpa
{
public:
Grandpa(){ cout << "Grandpa()" << endl; }
~Grandpa(){ cout << "~Grandpa()" << endl; }
virtual void func1() {
cout << "Grandpa::func1()" << endl;
}
virtual void func2(){
cout << "Grandpa::func2()" << endl;
}
};
class Parent
: public Grandpa
{
public:
Parent(){
cout << "Parent()" << endl;
func1();//构造函数中调用虚函数
}
~Parent(){
cout << "~Parent()" << endl;
func2();//析构函数中调用虚函数
}
};
class Son
: public Parent
{
public:
Son() { cout << "Son()" << endl; }
~Son() { cout << "~Son()" << endl; }
virtual void func1() override {
cout << "Son::func1()" << endl;
}
virtual void func2() override{
cout << "Son::func2()" << endl;
}
};
void test0(){
Son ss;
Grandpa * p = &ss;
p->func1();
p->func2();
}
int main(){
test0();
return 0;
}
用Grandpa类指针p指向Son类对象,用这个指针p调用func1/func2.结果是指针p调用到的是Son类的func1和func2函数。
说明即使Parent中没有对func1和fucn2覆盖,在Son中也可以对func1和func2覆盖。
—— 如果在Parent类的构造和析构函数中调用虚函数
创建一个Son对象

在parent的构造函数执行时,并不知道是在构造Son的对象,在此过程中,只能看到本层及以上的部分(因为Grandpa类的基类子对象已经创建完毕,虚表中记录了Grandpa::func1和func2的地址)
在Parent的析构函数执行时,此时Son的析构函数已经执行完了,可以理解为Son需要进行的回收工作都已经结束了。所以Parent的析构函数也只能看到本层及以上的部分。
——如果Parent类中也覆盖了func1和func2,那么会调用Parent本层的虚函数。
总结:
C++标准规定,在构造函数和析构函数中,通过this指针(隐式或显式)调用的虚函数会被解析为当前类(即构造函数或析构函数所属的类)的版本,而不是通过虚函数表来动态解析。
抽象类
1 . 声明了纯虚函数的类,称为抽象类
纯虚函数
纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。纯虚函数的格式如下:
class 类名 {
public:
virtual 返回类型 函数名(参数 ...) = 0;
};
在基类中声明纯虚函数就是在告诉派生类的设计者 —— 你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它。
多个派生类可以对纯虚函数进行多种不同的实现,但是都需要遵循基类给出的接口(纯虚函数的声明)。
声明了纯虚函数的类成为抽象类,抽象类不能实例化对象。
看一个简单例子:
class A
{
public:
virtual void print() = 0;
virtual void display() = 0;
};
class B
: public A
{
public:
virtual void print() override{
cout << "B::print()" << endl;
}
};
class C
: public B
{
public:
virtual void display() override{
cout << "C::display()" << endl;
}
};
void test0(){
//A类定义了纯虚函数,A类是抽象类
//抽象类无法创建对象
//A a;//error
//B b;//error
C c;
A * pa2 = &c;
pa2->print();
pa2->display();
}
在A类中声明纯虚函数,A类就是抽象类,无法创建对象;
在B类中去覆盖A类的纯虚函数,如果把所有的纯虚函数都覆盖了(都实现了),B类可以创建对象;只要还有一个纯虚函数没有实现,B类也会是抽象类,也无法创建对象;
再往下派生C类,完成所有的纯虚函数的实现,C类才能够创建对象。
最顶层的基类(声明纯虚函数的类)虽然无法创建对象,但是可以定义此类型的指针,指向派生类对象,去调用实现好的纯虚函数。
—— 这种使用方式也归类为动态多态,尽管不符合第一个条件(基类中声明纯虚函数,而非定义),最终的效果仍然是基类指针调用到了派生类实现的虚函数,属于动态多态的特殊情况。
纯虚函数使用案例:
实现一个图形库,获取图形名称,获取图形之后计算它的面积
#include<iostream>
#include<string>
#include<math.h>
using namespace std;
#define PI 3.14
class Figure{
public:
Figure(){
cout << "Figure()" << endl;
}
~Figure(){
cout << "~Figure()" << endl;
}
virtual string getname() const = 0;
virtual float getarea() const = 0;
void show(){
cout << this->getname() << ":" << this->getarea() << endl;
}
};
class Circle:public Figure{
private:
float _r;
string _name;
public:
Circle(float r):_r(r), _name("circle"){
cout << "Circle()" << endl;
}
~Circle(){
cout << "~Circle()" << endl;
}
string getname() const override{
return _name;
}
float getarea() const override{
return _r * _r * PI;
}
};
class Square:public Figure{
private:
float _a;
string _name;
public:
Square(int a):_a(a),_name("square"){
cout << "Square()" << endl;
}
~Square(){
cout << "~Square()" << endl;
}
string getname()const override{
return _name;
}
float getarea()const override{
return _a * _a;
}
};
void show(Figure & fg){
cout << fg.getname() << ":" << fg.getarea() << endl;
}
void test(){
Circle cle(3);
show(cle);
Figure *fig = new Square(5);
fig->show();
delete fig;
}
int main(){
test();
return 0;
}
基类Figure中声明了纯虚函数,交给多个派生类去实现,最后可以使用基类的指针(引用)指向(绑定)不同类型的派生类对象,再去调用已经被实现的纯虚函数。
纯虚函数就是为了后续扩展而预留的接口。
析构函数设为虚函数(重点)
虽然构造函数不能被定义成虚函数,但析构函数可以定义为虚函数,一般来说,如果类中定义了虚函数,析构函数也应被定义为虚析构函数,尤其是类内有申请的动态内存,需要清理和释放的时候。
比如上述的Figure案例,如果我们创建了以下代码,那么我们会发现仅执行了Figure的析构函数,但是并没有执行Square的析构函数,那么便会存在内存泄漏的风险。

class Base
{
public:
Base()
: _base(new int(10))
{ cout << "Base()" << endl; }
virtual void display() const{
cout << "*_base:" << *_base << endl;
}
~Base(){
if(_base){
delete _base;
_base = nullptr;
}
cout << "~Base()" << endl;
}
private:
int * _base;
};
class Derived
: public Base
{
public:
Derived()
: Base()
, _derived(new int(20))
{
cout << "Derived()" << endl;
}
virtual void display() const override{
cout << "*_derived:" << *_derived << endl;
}
~Derived(){
if(_derived){
delete _derived;
_derived = nullptr;
}
cout << "~Derived()" << endl;
}
private:
int * _derived;
};
void test0(){
Base * pbase = new Derived();
pbase->display();
delete pbase;
//编译器会进行类型检查,pbase指向的空间是一个Derived对象
//所以会调用Derived的析构函数 —— 需要让析构函数设为虚函数,Derived析构函数会在虚表中覆盖Base析构函数的地址
//这样通过pbase才能调用到Derived析构函数
//Derived析构函数执行完,会自动调用Base的析构函数(没有走虚表这个途径) —— 析构函数本身的机制
}
在执行delete pbase时的步骤:
首先会去调用Derived的析构函数,但是此时是通过一个Base类指针去调用,无法访问到,只能跳过,再去调用Base的析构函数,回收掉存放10这个数据的这片空间,最后调用operator delete回收掉堆对象本身所占的整片空间
为了让基类指针能够调用派生类的析构函数,需要将Base的析构函数也设为虚函数。
Derived类中发生虚函数的覆盖,将Derived的虚函数表中记录的虚函数地址改变了。析构函数尽管不重名,也认为发生了覆盖。
在派生类析构函数执行完毕后,会自动调用基类析构函数。这是由编译器在析构函数调用序列中隐式安排的,这个过程不依赖于虚函数表,属于C++的语言规则。

总结:
在实际的使用中,如果有通过基类指针回收派生类对象的需求,都要将基类的析构函数设为虚函数。
建议:一个类定义了虚函数,而且需要显示定义析构函数,就将它的析构函数设为虚函数。
验证虚表的存在(重点)
从前面的知识讲解,我们已经知道虚表的存在,但之前都是理论的说法,我们是否可以通过程序来验证呢?——当然可以
#include <iostream>
using std::cout;
using std::endl;
class Base{
virtual
void print(){
cout << "Base::print()" << endl;
}
virtual
void display(){
cout << "Base::display()" << endl;
}
virtual
void show(){
cout << "Base::show()" << endl;
}
private:
long _base = 10;
};
class Derived
: public Base
{
void print(){
cout << "Derived::print()" << endl;
}
void display(){
cout << "Derived::display()" << endl;
}
void show(){
cout << "Derived::show()" << endl;
}
private:
long _derived = 100;
};
void test(){
Base base;
Derived derived;
cout << "sizeof(base)=" << sizeof(base) << endl; //16
cout << "sizeof(derived)=" << sizeof(derived) << endl; //24
long * pbase = (long *)&base;
long * pderived = (long *)&derived;
//pbase可以看做是当base对象的首地址,偏移一个指针大小便是base变量
long * base_vfptr = (long *)*pbase;
long base_number = *(pbase + 1);
cout << "base_vfptr=" << base_vfptr << endl;
cout << "base_number=" << base_number << endl;
//pderived可以看做是当derived对象的首地址,偏移一个指针大小便是base和derived变量
long * derived_vfptr = (long *)*pderived;
long derived_base_number = *(pderived + 1);
long derived_number = *(pderived + 2);
cout << "derived_vfptr=" << derived_vfptr << endl;
cout << "derived_base_number=" << derived_base_number << endl;
cout << "derived_number=" << derived_number << endl;
//虚函数表的第一二三个函数地址
cout << "base第一个虚函数地址=" << base_vfptr << endl;
cout << "base第二个虚函数地址=" << base_vfptr + 1 << endl;
cout << "base第三个虚函数地址=" << base_vfptr + 2 << endl;
cout << "derived第一个虚函数地址=" << derived_vfptr << endl;
cout << "derived第二个虚函数地址=" << derived_vfptr + 1 << endl;
cout << "derived第三个虚函数地址=" << derived_vfptr + 2 << endl;
//再次对虚函数地址进行解引用,然后定义一个函数指针,调用函数
typedef void (*pFunc)();
pFunc base_print = (pFunc)*base_vfptr;
pFunc base_display = (pFunc)*(base_vfptr + 1);
pFunc base_show = (pFunc)*(base_vfptr + 2);
base_print();
base_display();
base_show();
pFunc derived_print = (pFunc)*derived_vfptr;
pFunc derived_display = (pFunc)*(derived_vfptr + 1);
pFunc derived_show = (pFunc)*(derived_vfptr + 2);
derived_print();
derived_display();
derived_show();
}
int main()
{
test();
return 0;
}
创建一个Derived类对象d,这个对象的内存结构是由三个内容构成的,开始位置是虚函数指针,第二个位置是long型数据_base,
第三个位置是long型数据_derived.
第一次强转将这个Derived类对象视为了存放三个long型元素的数组,打印这个数组中的三个元素,后两个本身就是long型数据,输出其值,第一个本身是指针(地址),打印出来的结果是编译器以long型数据来看待这个地址的值。
这个虚函数指针指向虚表,虚表中存放三个虚函数的入口地址(3 * 8字节),那么再将虚表视为存放三个long型元素的数组,第二次强转,直接输出数组的三个元素,得到的结果是编译器以long型数据来看待这三个函数地址的值。
虚表中的三个元素本身是函数指针,那么再将这个三个元素强转成相应类型的函数指针,就可以通过函数指针进行调用了。
——验证了虚表中存放虚函数的顺序,是按照基类中虚函数的声明顺序去存放的。
带虚函数的多继承
描述:先是Base1、Base2、Base3都拥有虚函数f、g、h,Derived公有继承以上三个类,在Derived中覆盖了虚函数f,还有一个普通的成员函数g1,四个类各有一个double成员。
class Base1{
public:
Base1()
:_iBase1(10)
{
cout << "Base1()" << endl;
}
virtual void f(){
cout << "Base1::f()" << endl;
}
virtual void g(){
cout << "Base1::g()" << endl;
}
virtual void h(){
cout << "Base1::h()" << endl;
}
virtual ~Base1(){
cout << "~Base1()" << endl;
}
private:
double _iBase1;
};
class Base2{
public:
Base2()
:_iBase2(20)
{
cout << "Base2()" << endl;
}
virtual void f(){
cout << "Base2::f()" << endl;
}
virtual void g(){
cout << "Base2::g()" << endl;
}
virtual void h(){
cout << "Base2::h()" << endl;
}
virtual ~Base2(){
cout << "~Base2()" << endl;
}
private:
double _iBase2;
};
class Base3{
public:
Base3()
:_iBase3(30)
{
cout << "Base3()" << endl;
}
virtual void f(){
cout << "Base3::f()" << endl;
}
virtual void g(){
cout << "Base3::g()" << endl;
}
virtual void h(){
cout << "Base3::h()" << endl;
}
virtual ~Base3(){
cout << "~Base3()" << endl;
}
private:
double _iBase3;
};
class Derived
: public Base1
, public Base2
, public Base3
{
public:
Derived()
: _iDerived(10000)
{ cout << "Derived()" << endl; }
void f()
{
cout << "Derived::f()" << endl;
}
void g1()
{
cout << "Derived::g1()" << endl;
}
private:
double _iDerived;
};
int main(void)
{
cout << sizeof(Derived) << endl;
Derived d;
Base1* pBase1 = &d;
Base2* pBase2 = &d;
Base3* pBase3 = &d;
cout << "&Derived = " << &d << endl;
cout << "pBase1 = " << pBase1 << endl;
cout << "pBase2 = " << pBase2 << endl;
cout << "pBase3 = " << pBase3 << endl;
return 0;
}

三种不同的基类类型指针指向派生类对象时,实际指向的位置是相应类型的基类子对象的位置
VS上验证布局和虚函数表存放的内容
布局规则
通过VS平台展示类对象内存布局的功能,我们可以总结出以下规则:
1 . 每个基类都有自己的虚函数表(前提是基类定义了虚函数)—— 派生类拥有三张虚表
2 . 派生类如果有自己的虚函数,会被加入到第一个虚函数表之中 —— 希望尽快访问到虚函数

3 . 内存布局中,其基类的布局按照基类被声明时的顺序进行排列(有虚函数的基类会往上放——希望尽快访问到虚函数)
如果继承顺序为Base1/Base2/Base3,在Derived对象的内存布局中就会先是Base1类的基类子对象,然后是Base2、Base3基类子对象
此时,如果Base1中没有定义虚函数,那么内存排布上会将Base1基类子对象排在Base2、Base3基类子对象之后。

4 . 派生类会覆盖基类的虚函数,只有第一个虚函数表中存放的是真实的被覆盖的函数的地址;其它的虚函数表中对应位置存放的并不是真实的对应的虚函数的地址,而是一条跳转指令 —— 指示到哪里去寻找被覆盖的虚函数的地址

带虚函数的多重继承的二义性
例子:
class A{
public:
virtual void a(){ cout << "A::a()" << endl; }
virtual void b(){ cout << "A::b()" << endl; }
virtual void c(){ cout << "A::c()" << endl; }
};
class B{
public:
virtual void a(){ cout << "B::a()" << endl; }
virtual void b(){ cout << "B::b()" << endl; }
void c(){ cout << "B::c()" << endl; }
void d(){ cout << "B::d()" << endl; }
};
class C
: public A
, public B
{
public:
virtual void a(){ cout << "C::a()" << endl; }
void c(){ cout << "C::c()" << endl; }
void d(){ cout << "C::d()" << endl; }
};
//先不看D类
class D
: public C
{
public:
void c(){ cout << "D::c()" << endl; }
};
内存结构的示意图:
请分析以下各种调用情况的结果
void test0(){
C c;
c.a();//C::a() 没有通过虚表,隐藏,相当于把a()看成普通成员函数
c.b();//error,成员名访问冲突二义性
c.c();//C::c(),本质是虚函数,没有通过虚表,隐藏
c.d();//C::d(),不是虚函数,隐藏
cout << endl;
A* pa = &c;
pa->a();//C::a(),动态多态被触发
pa->b();//A::b(),调用A类定义的虚函数b(),通过虚表,没有覆盖
pa->c();//C::c(),动态多态被触发
pa->d();//error,只能调用A基类子对象的内容,无法调用
cout << endl;
B* pb = &c;
pb->a();//C::a(), 动态多态触发
pb->b();//B::b(),通过虚表,但是没有覆盖
pb->c();//B::c(),调用B定义的普通成员函数c()
pb->d();//B::d(),同上
cout << endl;
C * pc = &c;
pc->a();//C::a() 通过虚表,但是没有触发动态多态
pc->b();//error 成员名访问冲突二义性
pc->c();//C::c(),本质是虚函数,通过虚表,不算动态多态
pc->d();//C::d(),不是虚函数,隐藏
}
——思考:pc->c() 这里的c函数是不是虚函数
从内存的角度分析,C::c()已经在第一张虚函数表中了,所以应该当成是虚函数处理。能否验证一下呢?
D类继承C类,重新定义c()函数,用C类指针指向D类对象,并调用c()函数

如果将A类中c函数的virtual关键字去掉,毫无疑问C中c函数是一个普通函数(发生的是隐藏)
总结:
- 如果通过对象来调用虚函数,那么不会通过虚表来找虚函数,因为编译器从一开始就确定调用函数的对象是什么类型,直接到程序代码区中找到对应函数的实现;
- 如果基类指针指向派生类对象,通过基类指针调用虚函数,若派生类中对这个虚函数进行了覆盖(重写-override),那么符合动态多态的触发机制,最终的效果是基类指针调用到了派生类定义的虚函数;如果派生类对这个虚函数没有进行覆盖,也会通过虚表访问,访问到的是基类自己定义的虚函数的入口地址;
- 如果是派生类指针指向本类对象,调用虚函数时,也会通过虚表去访问虚函数。若本类中对基类的虚函数进行覆盖,那么调用到的就是本类的虚函数实现,如果没有覆盖,那么会调用到基类实现的虚函数。
虚拟继承
虚拟继承是一种特殊的继承方式,用于解决菱形继承中的冗余性和二义性问题。类D继承自类C1和类C2,而C1类和C2类都继承自类B,类D中会两次继承B,为了节省空间,可以将C1、C2对B的继承定义为虚拟继承,而B就成了虚拟基类。虚拟基类的成员变量和成员函数在子类中只会存在一份,避免了冗余性问题。
普通继承 vs 虚拟继承
1.虚拟继承相较于普通继承会多出一个虚基指针
2.普通继承内存模型是基类成员在前,派生类成员在后;而虚拟继承是派生类成员在前,基类成员在后
内存模型:
(1)虚拟继承的内存结构(无虚函数)
class A{
public:
/*virtual*/ void f() { cout << "A::f()" << endl; }
private:
int _ia = 10;
};
class B : virtual public A{
public:
void fb(){ cout << "B::fb()" << endl; }
void f() { cout << "B::f()" << endl; }
/*virtual*/ void fb2() { cout << "B::fb2()" << endl; }
/*virtual*/ void fb3(){cout << "B::fb3()" << endl; }
private:
int _ib = 20;
};

(2)如果虚基类中包含了虚函数
此时会多出一个虚函数指针,指向虚函数的入口位置
class A{
public:
virtual void f() { cout << "A::f()" << endl; }
private:
int _ia = 10;
};
class B : virtual public A{
public:
void fb(){ cout << "B::fb()" << endl; }
void f() { cout << "B::f()" << endl; }
/*virtual*/ void fb2() { cout << "B::fb2()" << endl; }
/*virtual*/ void fb3(){cout << "B::fb3()" << endl; }
private:
int _ib = 20;
};

(3)如果派生类中又定义了新的虚函数,会在内存中多出一个属于派生类的虚函数指针,指向一张新的虚表(VS的实现)
为什么要进行这种设计呢?为什么不将两个虚函数表进行合并呢?
主要的原因在于虚拟继承时,派生类成员在前,基类成员在后。如果和从基类吸收过来的虚函数表进行合并,那么如果想要查找B:fb2或者B:fb3入口地址时,需要先借助于虚基指针,查找到虚基类所在的位置,然后再利用虚函数指针得到对应的虚函数位置。但是如果新增一个虚函数指针,此时只需要查找一次,便可以得到fb2或者fb3的入口位置。

class A{
public:
virtual void f() { cout << "A::f()" << endl; }
private:
int _ia = 10;
};
class B : virtual public A{
public:
void fb(){ cout << "B::fb()" << endl; }
void f() { cout << "B::f()" << endl; }
virtual void fb2() { cout << "B::fb2()" << endl; }
virtual void fb3(){cout << "B::fb3()" << endl; }
private:
int _ib = 20;
};
(4)带虚函数的菱形继承——虚拟继承方式(拔高,不要求一定掌握)
class B{
public:
virtual void f(){
cout << "B::f()" << endl;
}
virtual void Bf(){
cout << "B::Bf()" << endl;
}
private:
int _ib;
char _cb;
};
class B1 : virtual public B{
public:
virtual void f(){cout << "B1::f()" << endl;}
virtual void f1(){cout << "B1::f1()" << endl;}
virtual void Bf1(){cout << "B1::Bf1()" << endl;}
private:
int _ib1;
char _cb1;
};
class B2 : virtual public B{
public:
virtual void f(){cout << "B2::f()" << endl;}
virtual void f2(){cout << "B2::f2()" << endl;}
virtual void Bf2(){cout << "B1::Bf2()" << endl;}
private:
int _ib2;
char _cb2;
};
class D
: public B1
, public B2
{
public:
virtual void f(){cout << "D::f()" << endl;}
virtual void f1(){cout << "D::f1()" << endl;}
virtual void f2(){cout << "D::f2()" << endl;}
virtual void Df(){cout << "D::Df()" << endl;}
private:
int _id;
char _cd;
};
虚函数表和虚基类表中,前面有下标的才是表中真实记录的信息。

虚拟继承时派生类对象的构造和析构
如下菱形继承的结构中,中间层基类虚拟继承了顶层基类,注意底层派生类的构造函数
class A
{
public:
A(double a)
: _a(a)
{
cout << "A(double)" << endl;
}
~A(){cout << "~A()" << endl;}
private:
double _a = 10;
};
class B
: virtual public A
{
public:
B(double a, double b)
: A(a)
, _b(b)
{
cout << "B(double,double)" << endl;
}
~B(){ cout << "~B()" << endl; }
private:
double _b;
};
class C
: virtual public A
{
public:
C(double a, double c)
: A(a)
, _c(c)
{
cout << "C(double,double)" << endl;
}
~C(){ cout << "~C()" << endl; }
private:
double _c;
};
class D
: public B
, public C
{
public:
D(double a,double b,double c,double d)
: A(a)
, B(a,b)
, C(a,c)
, _d(d)
{
cout << "D(double * 4)" << endl;
}
~D(){ cout << "~D()" << endl; }
private:
double _d;
};

在虚拟继承的结构中,最底层的派生类不仅需要显式调用中间层基类的构造函数,还要在初始化列表最开始调用顶层基类的构造函数。

——那么A类构造岂不是会调用3次?
并不会,有了A类的构造之后会压抑B、C构造时调用A类构造,A类构造只会调用一次。可以对照菱形继承的内存模型理解,D类对象中只有一份A类对象的内容。
对于析构函数,同样存在这样的压抑效果,D类析构执行完后,根据继承声明顺序的反序调用C类的析构函数,C的析构函数执行完后并没有自动调用A的析构函数,而是接下来调用B的析构函数,最后调用A的析构函数。
文章大纲

更多推荐




所有评论(0)