C++笔记:OOP三大特性之多态

前言

本博客中的代码和解释都是在VS2019下的x86程序中进行的,涉及的指针都是 4 字节,如果要其他平台下测试,部分代码需要改动。比如:如果是x64程序,则需要考虑指针是8bytes问题等等。

文章目录

  • 前言
  • 一、多态的概念
  • 二、多态的定义及实现
    • 2.1 构成多态的两个必要条件
    • 2.2 什么虚函数?
    • 2.3 什么是虚函数重写?
    • 2.4 多态调用的例子
    • 2.5 虚函数重写的三个例外
      • 第一:派生类虚函数不加 virtual 关键字
      • 第二:协变(基类与派生类虚函数返回值类型不同)
      • 第三:析构函数的重写(基类与派生类析构函数的名字不同)
    • 2.6 重载、覆盖(重写)、隐藏(重定义)的对比
    • 2.7 C++11 override 和 final
  • 三、抽像类
    • 3.1 接口继承与实现继承
  • 四、探究多态下的对象模型及认识虚表
    • 4.1 虚函数指针与虚函数表
    • 4.2 虚函数与虚函数表的存储位置
    • 4.3 虚函数指针初始化和虚表生成时间
    • 4.4 动态多态的原理
  • 五、单继承和多继承关系的虚函数表
    • 5.1 单继承中的虚函数表
    • 5.2多继承中的虚函数表
  • 六、多态相关的一些问题

一、多态的概念

多态是面向对象编程中一个重要特性,它允许以一致的方式来使用不同的对象得到不同的结果,或者说,某一个动作被不同的对象完成会得到不同的结果,这两种说法都是一样的。

在C++中,多态性有两种主要形式:编译时多态性(静态多态性)和运行时多态性(动态多态性)。

  • 静态多态性:在程序编译阶段实现,表现为函数重载,通过传递不同的实参调用相应的同名函数获取不同的结果。
  • 动态多态性基于继承实现,指在程序运行阶段,根据具体拿到的类型确定程序的具体行为,调用具体的函数。

后面的内容都是关于动态多态,为了方便,接下来的内容的“多态”都默认指动态多态

二、多态的定义及实现

2.1 构成多态的两个必要条件

  1. 必须通过基类的指针或者引用调用虚函数。
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数完成重写
    (注意:只有虚函数才有重写这个概念)

2.2 什么虚函数?

虚函数:即被关键字 virtual 修饰的类成员函数称为虚函数。

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

2.3 什么是虚函数重写?

虚函数的重写,又叫做虚函数的覆盖,当派生类中实现一个跟基类完全相同的虚函数,这时候称 “派生类的虚函数重写了基类的虚函数”。

派生类虚函数与基类虚函数的完全相同要求满足以下三同:① 返回值类型相同、② 函数名相同、③ 参数列表相同

2.4 多态调用的例子

以下是一个多态调用的例子:

首先,左边 Func 函数中,people 是基类的引用,派生类 Student 完成了对基类 Person 的 BuyTicket() 的重写,满足多态调用。

其次,people 引用基类对象调用基类的 BuyTicket() ,引用派生类对象调用派生类重写的 BuyTicket() 。
在这里插入图片描述

2.5 虚函数重写的三个例外

C++中有三个形式上不满足函数重写的语法规定,但依旧是虚函数重写的特殊情况。

第一:派生类虚函数不加 virtual 关键字

上面那个例子中 ,Student 类中的虚函数像下面这样写也是可以编译通过的,因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性,但是该种写法不是很规范,建议基类和派生类都加上 virtual,以提高可读性

class Student : public Person {
public:void BuyTicket() {cout << "买票-半价" << endl; }
};

第二:协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,派生类虚函数与基类虚函数的返回值类型可以不同,但要求基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,即返回值构成继承关系,这种做法称之为 “ 协变 ”。

以下代码为一个协变的例子:

// A、B构成继承关系
class A {};
class B : public A {};class Person {
public:// Person 返回 基类A 的 指针virtual A* f() { cout << "A* f()" << endl;return new A; }
};
class Student : public Person {
public:// Student 返回 派生类B 的 指针virtual B* f() { cout << "B* f()" << endl;return new B; }
};int main()
{Person* p1 = new Person;Person* p2 = new Student;p1->f();p2->f();return 0;
}

在这里插入图片描述

假设A、B不构成继承关系,就会引发报错

// 去掉继承关系
class A {}
class B {}

在这里插入图片描述

在VS2019中,编译器对协变进行了强制检查,如果没有强制检查,会发生什么?

首先,基类和派生类的f()函数由于返回值类型不同不构成重写,不构成重写就满足多态调用,所以和普通的函数调用没有区别,普通函数调用取决于对象或者指针或者引用的类型

其次,由于Person和Student是继承关系,f()构成隐藏关系,由于编译器的赋值兼容转换机制且指针p1p2的类型都是Person*,两个指针会去调用Person的f(),而不会去调用Student类的f()

而下面讲的第三个例外不实现成重写也会导致这个问题。

第三:析构函数的重写(基类与派生类析构函数的名字不同)

一个继承体系中,派生类和基类的析构函数都会被编译器特殊处理成 destructor(),所以基类和派生类的析构函数会构成隐藏关系,在派生类调用基类析构函数需要指定类域显式调用,现在可以解释为什么要做这种特殊处理了,是为了重写。

学了动态多态之后,函数调用可以分成两种:

  • 普通调用,取决于指针或者引用或者对象的类型。
  • 多态调用,取决于指针或者引用指向的对象。

下面这份代码中由于两个析构函数没有满足虚函数重写,无法进行多态调用,指针p2仅对一个Student对象中的Person部分进行析构,Student对象内部的资源没有完全回收,这会导致内存泄漏问题

// 析构隐藏
class Person {
public:~Person() { cout << "~Person()" << endl; }
};class Student : public Person {
public:~Student() { cout << "~Student()" << endl; }
};// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,
// 才能构成多态,才能保证 p1 和 p2 指向的对象正确的调用析构函数。
int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

在这里插入图片描述

编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,只要基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写。

// 析构重写
class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,
// 才能构成多态,才能保证 p1 和 p2 指向的对象正确的调用析构函数。
int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

在这里插入图片描述

2.6 重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述

2.7 C++11 override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

  1. final,修饰虚函数时,表示该虚函数不能再被重写;修饰一个类时,表示该类不能被继承

在这里插入图片描述
在这里插入图片描述

  1. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

在这里插入图片描述

三、抽像类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。

class Car
{
public:// Drive是纯虚函数virtual void Drive() = 0;
};

包含纯虚函数的类被称之为抽象类(也叫接口类),抽象类不能被实例化对象。
在这里插入图片描述

抽象类定义了一个类可能发出的动作的原型,但既没有实现,也没有任何状态信息,引入抽象类的原因在于很多时候基类本身实例化不合情理的,例如车类作为一个基类可以派生出奔驰、宝马等子类,但是车类本身实例化是没有意义的。

这时候就可以将车类定义成抽象类,由于抽象类只能提供原型而无法被实例化,因此派生类必须提供接口的实现,派生类亦无法被实例化,纯虚函数规范了派生类必须重写。

class Car
{
public:virtual void Drive() = 0;
};// 奔驰类
class Benz :public Car
{
public:// 完成重写virtual void Drive(){cout << "Benz-舒适" << endl;}
};// 宝马类
class BMW :public Car
{
public:// 不完成重写
};int main()
{Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}

在这里插入图片描述

3.1 接口继承与实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

下面这道题就体现了接口继承

class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }virtual void test() { func(); }
};class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}

A:A->0 B:B->1 C:A->1 D:B->0 E:编译出错 F:以上都不正确

答案是B,解析如下:
A、B是继承关系,A中有两个虚函数(func 和 test),B中有一个虚函数(func),func接口构成重写。

在 main 函数中:B* 的指针变量 p 指向一个B对象,p-> 告诉编译器要到 B 的类域中找 test 的定义,同时把 p 传给 this,换言之,this 指向的B对象。

编译器在 B 类中找不到 test,然后由于继承关系存在,到 A 类中去找,找到并且继承到了使用权,所以,会调用到 A 类中的 test 接口。

A 类的 test 接口调用了 func 函数,函数是通过 this 指针来调用的(this->func();),此时在 A 的类域中,this 的类型显然是 A*。

类型为A* 的 this 指针指向一个 B 对象,且 func 满足虚函数重写,会去调用 B 中的 func()。

虚函数的重写是接口继承,virtual void func(int val = 1),这时候 val 的是 1,所以答案是 B->1。

四、探究多态下的对象模型及认识虚表

4.1 虚函数指针与虚函数表

下面代码中,sizeof(Base)是多少?

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};int main()
{cout << sizeof(Base) << endl;return 0;
}

在这里插入图片描述
按道理说,对象只存储成员变量,预期大小应该是 4 字节,可通过运行结果可以发现,Base对象的大小是 8 字节(看前言),因此,当一个类包含虚函数时,类对象模型肯定发生了改变。

接下来实例化出 Base 类的两个对象,然后通过监视窗口观察 Base 类的对象结构发现:

  • Base类对象中除了_b成员,还多一个__vfptr指针放在对象的最前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),__vfptr指向一个叫做 vftable 的数组,数组里有两个元素,但监视窗口只显示了第一个元素,它是 Base::Func 的函数指针。
  • Base 类实例化出的两个对象的 __vfptr 的内容都是一样的。
    在这里插入图片描述

当一个类中包含虚函数成员,类对象模型如下:

  • 对象内部除了自己定义的成员变量外,编译器自动添加了一个指针成员,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function),该指针指向的是一个数组,被称为虚函数表,虚函数表也简称虚表,虚表里面存放的是虚函数的地址。
  • 一个类的实例化出多个对象时,它们共享该类的虚表。

在这里插入图片描述


了解什么是虚表指针和虚表之后,Base的派生类对象模型又是怎样的呢?接着往下分析。

为了更好地测试,针对上面的代码改造成单继承但无虚函数重写的场景,查看派生类对象模型

  1. 我们增加一个派生类Derive去继承Base
  2. Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
class Derive : public Base
{
public:
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}

通过对监视窗口的观察可以看到:

  1. d 对象由两部分构成,一部分是基类继承下来的成员,另一部分是自己的成员。
  2. 派生类对象 d 中也有一个虚表指针,虚表指针存在基类部分的首个位置。
  3. 基类b对象和派生类d对象虚表指针是不一样的,可是虚表的内容是一样的,也就是说派生类对象会拷贝一份基类的虚表给自己。
  4. Func3 也继承下来了,但是不是它虚函数,所以不会放进虚表。

在这里插入图片描述


针对上面的代码的Derive中重写Func1改造成单继承且有虚函数重写的场景,再查看派生类对象模型

// Base 类不变class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};// main 函数不变

通过对监视窗口的观察可以看到:

  • 派生类对 Func1 完成重写之后,派生类对象 d 的虚表发生部分变换,原本 Base::Func1 地址被重写后的 Derive::Func1 的地址覆盖,这就是为什么虚函数的重写也叫作覆盖,重写是语法的叫法,覆盖是原理层的叫法。

在这里插入图片描述


针对上面的代码的Derive中增加虚函数 Func4再查看派生类对象模型

// Base 类不变class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}// 增加虚函数 Func4virtual void Func4(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};// main 函数不变

通过监视窗口 + 内存窗口的观察验证发现:

  • 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

在这里插入图片描述

总结一下派生类的虚表生成:

  1. 先将基类中的虚表内容拷贝一份到派生类虚表中。
  2. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  3. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

4.2 虚函数与虚函数表的存储位置

这里还有一个很容易混淆的问题:

虚函数存在哪的?虚表存在哪的?
答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。

上面的回答的错的。

首先,虚表存的是虚函数指针,不是虚函数本身,虚函数和普通函数虽然在语法上一样的,但在编译器看来它们都是函数,经过编译之后都会生成地址和指令,指令存储在代码段的,地址存到了虚表中。

其次,对象中存的不是虚表,存的是虚表指针,虚表指针是对象的成员,如果对象在栈上的,虚表指针就在栈上,如果对象是new出来的,虚表指针就在堆上。

既然不确定虚表的存储位置,那样可以对比法来验证一下。

int main()
{Base b;Derive d;int i = 0;static int j = 0;int* p1 = new int;const char* p2 = "xxxxxxxxxxxxxxxxx";Base* p3 = &b;Derive* p4 = &d;printf("栈:%p\n", &i);printf("堆:%p\n", p1);printf("静态区:%p\n", &j);printf("常量区:%p\n", p2);// vfptr在对象的第一个位置,x86下指针是4字节,类型强转(int*)p3获得vfptr// 对vfptr解引用能够找到虚表第一个虚函数的地址// 对比分析虚函数地址和哪个区的地址接近就在哪个区printf("Base虚表首元素:%p\n", *(int*)p3);printf("Derive虚表首元素:%p\n", *(int*)p4);return 0;
}

测试结果发现,虚表上的函数指针和常量区(代码段)的地址是最接近,由此可以认为在VS下虚表是存储在常量区(代码段)
在这里插入图片描述

Linux 发行版 CentOS 7.6 下的g++编译器的测试结果如下:
测试结果同样是发现虚表实在代码端上的在这里插入图片描述

4.3 虚函数指针初始化和虚表生成时间

先来一波猜测:

  1. 对象内部的虚函数指针成员是编译器自己加上去的,虚函数指针的初始化应当交由编译器在对象构造时进行的。
  2. 类与对象的语法部分规定:对象的成员变量的初始化必须经过初始化列表,如果虚函数指针是在调用构造函数期间初始化的,就能够说明虚函数指针在初始化列表完成初始化的
  3. 在VS平台下,虚函数指针在对象模型的首位,假如虚函数指针的初始化时间比一个对象中任意一个成员还早就说明它是第一个被初始化
  4. 在 C++ 中,虚函数转换成地址和指令是程序在编译期间完成的,对象的构造函数是在运行时期间被调用的,如果虚函数指针在初始化列表被初始化,说明虚表在虚函数指针被初始化之前就已经生成好了

为 Base 类添加构造函数后验证结果如下:

  1. 虚表在编译阶段生成。
  2. 虚函数指针在运行阶段由编译器调用构造函数通过初始化列表初始化。
  3. 虚函数指针在VS的类对像模型中是第一个被初始化的。

在这里插入图片描述

4.4 动态多态的原理

多态调用通过基类的指针或者引用,指向基类调用基类的虚函数,指向派生类调用派生类的虚函数,通过对上面虚表的了解之后,不用说肯定是通过虚表来完成的,但具体的过程是怎么样的呢?

下面就用这份代码例子来做一个深入的研究:

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

在这里插入图片描述

  1. 观察下图的红色箭头我们看到,p 是指向 mike 对象时,p->BuyTicketmike 的虚表中找到虚函数是 Person::BuyTicket
  2. 观察下图的蓝色箭头我们看到,p 是指向 johnson 对象时,p->BuyTicketjohson 的虚表中找到虚函数是 Student::BuyTicket
  3. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是基类的指针或引用调用虚函数,这是为什么?

第一:基类的指针或者引用指向派生类对象时,编译器会发生赋值兼容转换操作,将派生类对象中基类部分切割给基类的指针或者引用,然后基类的指针和引用可以把这些派生类对象当成基类对象来使用。
第二:由于继承的缘故,派生类的虚表指针是在基类部分的成员中的,切割之后基类的指针或者引用依旧能够使用派生类的虚表。
第三:如果不完成虚函数覆盖,派生类的虚表和基类的虚表是一样的,只有派生类完成了虚函数覆盖,虚表上的函数指针才会发生改变,基类指针或者引用才能调用到派生类重写的虚函数,否则只能调用到基类的虚函数。

  1. 为什么说动态多态是在运行时阶段实现的?

编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用哪个类的虚函数。
只有在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。

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

5.1 单继承中的虚函数表

在前面 4.1 探究派生类对象模型中,通过下面三种情况基本了解清楚了:

  1. 单继承,派生类无虚函数覆盖
  2. 单继承,派生类有虚函数覆盖,但无自己的虚函数
  3. 单继承,派生类有虚函数覆盖,有自己的虚函数

这里不进行过多的赘述,不过可以将基类和派生类的虚表打印出来进行一个验证:
取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
指针的指针数组,这个数组最后面放了一个nullptr

  1. 先取b的地址,强转成一个int*的指针
  2. 再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
  3. 再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
  4. 虚表指针传递给PrintVTable进行打印虚表
  5. 需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
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(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Base b;Derive d;VFPTR* vTableb = (VFPTR*)(*(int*)&b);PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*(int*)&d);PrintVTable(vTabled);return 0;
}

在这里插入图片描述

5.2多继承中的虚函数表

用下面这份代码来探究一下,多继承中派生类对象模型以及虚表结构:

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(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}int main()
{Derive d;cout << " 对象空间的大小: " << sizeof(d) << endl << endl;Base1* ptr1 = &d;Base2* ptr2 = &d;VFPTR* vTableb1 = (VFPTR*)(*(int*)ptr1);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)ptr1 + sizeof(Base1)));PrintVTable(vTableb2);return 0;
}

第一:sizeof(d) 的大小是多少?

对象 d 由三部分成员构成,① Base1 部分的虚表指针及其成员,这里有 8 字节;② Base2 部分的虚表指针及其成员,这里有 8 字节;③ Derive 自己的成员变量,这里有 4 字节,结果应该是 20 字节。
在这里插入图片描述

第二:赋值兼容转换的过程是怎样,或者说,ptr1ptr2 是否相等?

答案是不相等。

  1. 监视窗口中,&d 和 ptr1 的值是一样的,但是意义不一样,虽然 &d 和 ptr1 都是指向 对象 d 这块空间的起始位置,但是指针的类型限制了指针解引用能够访问多大的空间,&d 的类型是 Derive* 解引用可以访问 20 个字节,ptr1 的类型是 Base1* 解引用只能够访问 8 个字节。
  2. ptr2 在切片过程中会发生偏移,编译器会找到 Base2 部分的开始,然后将地址交给 ptr2。
    在这里插入图片描述

第三:对象 d 中有两张虚表,Base1 的虚函数指针放在 Base1 部分的虚表,Base2 的虚函数指针放在 Base3 部分的虚表,但是 Derive 中有一个 Func3() 既不属于 Base1 也不属于 Base2,它该放到哪张虚表里?

有两种可能性:①两张虚表都有 Func3 的函数指针,② Base1部分的虚表里有 Func3 的函数指针
经过测试验证:在VS平台下,多继承体系总派生类的虚函数放在第一个声明的基类当中。
在这里插入图片描述

六、多态相关的一些问题

  1. inline 函数能否是虚函数?

inline 函数会在编译阶段原地展开,直接转换为指令,剩下的建立栈帧带来的消耗,但是这样的做法导致 inline 函数没有函数指针,按道理来说,inline 函数无法称为虚函数。
但是 inline 只是对编译器的一个建议,加不加 inline 是否生效取决于编译器。
如果 inline 虚函数 满足多态调用,编译器就会忽略 inline 属性;
如果 inline 虚函数不满足多态调用, inline 虚函数依旧可以在原地展开。

class Base
{
public:inline virtual void Func1()	{ cout << "Base::Func1()" << endl; }virtual void Func2() { cout << "Base::Func2()" << endl; }void Func3() { cout << "Base::Func3()" << endl; }
private:int _b = 1;
};
class Derive : public Base
{
public:virtual void Func1() { cout << "Derive::Func1()" << endl; }
private:int _d = 2;
};
int main()
{// inline 虚函数满足多态调用Base* p = new Derive;p->Func1();// inline 虚函数不满足多态调用Base b;b.Func1();return 0;
}

在这里插入图片描述

  1. 静态成员可以是虚函数吗?

不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
在这里插入图片描述

  1. 构造函数可以是虚函数吗?

不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

  1. 析构函数可以是虚函数吗?

可以,并且建议虚构函数都定义成虚函数,具体看虚函数重写的第三个例外。

  1. 对象访问普通函数快还是虚函数更快?

首先如果是普通调用,结果是一样快的。
如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/696254.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C++初阶】系统实现日期类

目录 一.运算符重载实现各个接口 1.小于 (d1)<> 2.等于 (d1d2) 3.小于等于&#xff08;d1<d2&#xff09; 4.大于&#xff08;d1>d2&#xff09; 5.大于等于&#xff08;d1>d2&#xff09; 6.不等于&#xff08;d1!d2&#xff09; 7.日期天数 (1) 算…

mac图片怎么转换格式jpg?四种高效方法助你轻松搞定JPG格式

mac图片怎么转换格式jpg&#xff1f;在数字时代&#xff0c;图片格式的转换成为了我们日常操作中的一项基本技能。特别是在使用Mac操作系统的用户中&#xff0c;如何将图片转换为JPG格式成为了一个热门话题。本文将为你详细介绍四种简单实用的方法&#xff0c;帮助你在Mac上轻松…

测试基础1:伟大航路哟呼(Linux基础、mysql基础)

1 测试流程和方法 软件测试定义&#xff1a; 从方式上看&#xff1a;包含人工测试、自动化测试 从方法上看&#xff1a;运行程序或系统和测定程序或系统的过程 从目的上看&#xff1a;包括找bug和找bug出现的原因 软件测试的原则&#xff1a;功能性、可靠性、易用性、效率性…

一、网络基础知识

1、IP地址和端口号 1.1、IP地址 定义&#xff1a;用于在网络中唯一标识设备的地址。格式&#xff1a;通常由四个数字组成&#xff0c;以点分十进制表示&#xff0c;例如&#xff1a;192.168.0.1。(IPv4)作用&#xff1a;允许网络中的设备相互通信&#xff0c;通过IP地址可以定…

Python 数据可视化之密度散点图 Density Scatter Plot

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 密度散点图&#xff08;Density Scatter Plot&#xff09;&#xff0c;也称为密度点图或核密度估计散点图&#xff0c;是一种数据可视化技术&#xff0c;主要用于展示大量数据点在二维平面上的分布情况…

一样的代码不同项目跳转页面报404的解决办法

今天收到实施反馈的一个问题&#xff0c;点项目名称跳转项目详情页面时&#xff0c;有的页面跳转显示正常&#xff0c;有的页面跳转报404错误。错误如下&#xff1a; 发现报错的项目都有一个共性就是有特殊字符“[ ]” , 解决的办法就是把带有特殊字符的字段 用 encodeURI()…

【Django】Django自定义后台表单——对一个关联外键对象同时添加多个内容

以官方文档为例&#xff1a; 一个投票问题包含多个选项&#xff0c;基本的表单设计只能一个选项一个选项添加&#xff0c;效率较低&#xff0c;如何在表单设计中一次性添加多个关联选项&#xff1f; 示例代码&#xff1a; from django.contrib import adminfrom .models impo…

【软件架构】02-复杂度来源

1、性能 1&#xff09;单机 受限于主机的CPU、网络、磁盘读写速度等影响 在多线程的互斥性、并发中的同步数据状态等&#xff1b; 扩展&#xff1a;硬件资源、增大线程池 2&#xff09;集群 微服务化拆分&#xff0c;导致调用链过长&#xff0c;网络传输的消耗过多。 集…

嵌入式Qt 计算器核心算法_3

一.后缀表达式实现算数运算思路 二.算法实现 #include "QCalculatorDec.h"QCalculatorDec::QCalculatorDec() {m_exp "";m_result ""; }QCalculatorDec::~QCalculatorDec() {}bool QCalculatorDec::isDigitOrDot(QChar c) {return ((0 < c)…

基于SpringBoot的景区旅游管理系统

项目介绍 本期给大家介绍一个 景区旅游管理 系统.。主要模块有首页&#xff0c;旅游路线&#xff0c;旅行攻略&#xff0c;在线预定。管理员可以登录管理后台对用户进行管理&#xff0c;可以添加酒店&#xff0c;景区&#xff0c;攻略&#xff0c;路线等信息。整体完成度比较高…

一文搞懂match、match_phrase与match_phrase_prefix的检索过程

一、在开始之前&#xff0c;完成数据准备&#xff1a; # 创建映射 PUT /tehero_index {"settings": {"index": {"number_of_shards": 1,"number_of_replicas": 1}},"mappings": {"_doc": {"dynamic": …

探索气膜球幕影院:未来的电影体验

气膜球幕影院作为一种新兴的电影放映方式&#xff0c;正逐渐成为人们关注的焦点。它采用了充气式膜结构&#xff0c;可以为观众带来 360 度全景的观影体验&#xff0c;让人仿佛置身于电影之中。本文将介绍气膜球幕影院的特点、技术原理以及未来的发展前景。 传说在古代&#x…

十四、图像几何形状绘制

项目功能实现&#xff1a;矩形、圆形、椭圆等几何形状绘制&#xff0c;并与原图进行相应比例融合 按照之前的博文结构来&#xff0c;这里就不在赘述了 一、头文件 drawing.h #pragma once#include<opencv2/opencv.hpp>using namespace cv;class DRAWING { public:void…

QPaint绘制自定义仪表盘组件01

网上抄别人的&#xff0c;只是放这里自己看一下&#xff0c;看完就删掉 ui Dashboard.pro QT core guigreaterThan(QT_MAJOR_VERSION, 4): QT widgetsCONFIG c11# You can make your code fail to compile if it uses deprecated APIs. # In order to do so, uncomm…

week04day02(爬虫02)

<span>: 通常用于对文本的一部分进行样式设置或脚本操作。<a>: 定义超链接&#xff0c;用于创建链接到其他页面或资源的文本。<img>: 用于插入图像。<br>: 用于插入换行。 姓名&#xff1a;<input type"text" value"lisi">…

【析】考虑同时取送和时间窗的车辆路径及求解算法

期刊&#xff1a;computer engineering and applications 计算机工程与应用![c 引言 1. 问题分析 1.1 问题描述 问题描述为&#xff1a; 若干运输车辆从配送中心出发为客户取送货并最终返回配送中心&#xff0c;每位客户仅由一辆车服务一次&#xff0c;车辆在配送过程中任…

ChatGPT丨成像光谱遥感技术中的AI革命:ChatGPT应用指南

遥感技术主要通过卫星和飞机从远处观察和测量我们的环境&#xff0c;是理解和监测地球物理、化学和生物系统的基石。ChatGPT是由OpenAI开发的最先进的语言模型&#xff0c;在理解和生成人类语言方面表现出了非凡的能力。本课程重点介绍ChatGPT在遥感中的应用&#xff0c;人工智…

尾矿库排洪系统结构仿真软件WKStruc(可试用)

1、背景介绍 尾矿库作为重大危险源之一&#xff0c;在国际灾害事故排名中位列第18位&#xff0c;根据中国钼业2019年8月刊《中国尾矿库溃坝与泄漏事故统计及成因分析》的统计&#xff0c;在46起尾矿库泄漏事故中&#xff0c;由于排洪设施导致的尾矿泄漏事故占比高达1/3&#x…

c#创建安装windows服务

背景:最近在做设备数据对接采集时,遇到一些设备不是标准的Service-Client接口,导致采集的数据不够准确;比如设备如果中途开关机后,加工的数量就会从0开始重新计数,因此需要实时监控设备的数据,进行叠加处理;考略到工厂设备比较多,实时监听接口的数据为每秒3次,因此将…

外包干了三年,技术算是废了。。。

先说一下自己的个人情况&#xff0c;大专生&#xff0c;17年通过校招进入湖南某软件公司&#xff0c;干了接近5年的手工测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了五年的手工…