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;
	}
	// 增加虚函数 Func4
	virtual 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.mfbz.cn/a/402887.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux网络编程(三-UDP协议)

目录 一、UDP概述 二、UDP的首部格式 三、UDP缓冲区 四、基于UDP的应用层协议 五、常见问题 一、UDP概述 UDP(User Datagram Protocol&#xff0c;用户数据协议报)是传输层协议&#xff0c;提供不可靠服务&#xff0c;其特点包括&#xff1a; 无连接&#xff1a;知道对端…

探针类型、方式及实验

目录 1、tcpSocket方式 2、就绪检测 3、就绪检测2 4、启动、退出动作 5、探针 5.1探针的三种类型 5.2探针的三种方式 1、tcpSocket方式 vim tcpsocket.yaml apiVersion: v1 kind: Pod metadata:name: probe-tcp spec:containers:- name: nginximage: soscscs/myapp:v1live…

300分钟吃透分布式缓存-10讲:MC是怎么定位key的?

我们在进行 Mc 架构剖析时&#xff0c;除了学习 Mc 的系统架构、网络模型、状态机外&#xff0c;还对 Mc 的 slab 分配、Hashtable、LRU 有了简单的了解。本节课&#xff0c;将进一步深入学习这些知识点。 接下来&#xff0c;进入 Memcached 进阶的学习。会讲解 Mc 是如何进行…

UIKit 在 UICollectionView 中拖放交换 Cell 视图的极简实现

概览 UIKit 中的 UICollectionView 视图是我们显示多列集合数据的不二选择&#xff0c;而丰富多彩的交互操作更是我们选择 UICollectionView 视图的另一个重要原因。 如上图所示&#xff1a;我们实现了在 UICollectionView 中拖放交换任意两个 Cell 子视图的功能&#xff0c;这…

YOLOv9来了! 使用可编程梯度信息学习你想学的内容, v7作者新作!【文献速读】

YOLOv9文献速读&#xff0c;本文章使用 GPT 4.0 和 Ai PDF 工具完成。 文章地址&#xff1a;https://arxiv.org/pdf/2402.13616.pdf 文章目录 文章简介有哪些相关研究&#xff1f;如何归类&#xff1f;谁是这一课题在领域内值得关注的研究员&#xff1f;论文试图解决什么问题&a…

实现律所高质量发展-Alpha法律智能操作系统

律师行业本质上属于服务行业&#xff0c;而律师团队作为一个独立的服务单位&#xff0c;应当包含研发、市场、销售、服务等单位发展的基础工作环节。但现实中&#xff0c;很多律师团队其实并没有区分这些工作。鉴于此&#xff0c;上海市锦天城律师事务所医药大健康行业资本市场…

2.22 day3、4 QT

完善对话框&#xff0c;点击登录对话框&#xff0c;如果账号和密码匹配&#xff0c;则弹出信息对话框&#xff0c;给出提示"登录成功”&#xff0c;提供一个Ok按钮&#xff0c;用户点击Ok后&#xff0c;关闭登录界面&#xff0c;跳转到其他界面 如果账号和密码不匹配&…

MIT-6.824-Lab2,Raft部分笔记|Use Go

文章目录 前记Paper6&#xff1a;RaftLEC5、6&#xff1a;RaftLAB22AtaskHintlockingstructureguide设计与编码 2BtaskHint设计与编码 2CtaskHint question后记 LEC5&#xff1a;GO, Threads, and Raftgo threads技巧raft实验易错点debug技巧 前记 趁着研一考完期末有点点空余…

十四、图像几何形状绘制

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

“最会写”的中文大模型Weaver来了,中文创意写作能力超GPT-4

分享&#xff5c; Weaver ChatGPT等通用大模型支持的功能成百上千&#xff0c;但是对于普通日常用户来说&#xff0c;智能写作一定是最常见的&#xff0c;也是大模型最能真正帮上忙的使用场景之一。尽管大模型经常能写出看起来像模像样的文字&#xff0c;但是大多数情况下内容…

详细·Kubeadm安装

目录 实验前准备部署K8S集群初始化kubeadm&#xff08;只需要master做&#xff09;部署网络插件flannel测试 pod 资源创建 测试访问部署Dashboard&#xff08;master01&#xff09;浏览器访问 实验前准备 master&#xff1a;192.168.188.11 node01&#xff1a;192.168.188.13 …

Code Composer Studio (CCS) - 全局搜索功能

Code Composer Studio [CCS] - 全局搜索功能 1. Ctrl H&#xff0c;全局搜索功能References 1. Ctrl H&#xff0c;全局搜索功能 References [1] Yongqiang Cheng, https://yongqiang.blog.csdn.net/

如何用代理IP防止被泄露真实IP地址?

随着互联网的普及&#xff0c;我们的网络行为越来越离不开IP地址。然而&#xff0c;由于一些不法分子利用IP地址进行网络攻击、窃取个人信息等行为&#xff0c;保护我们的真实IP地址变得尤为重要。代理IP地址是一种隐藏真实IP地址的方法&#xff0c;通过使用代理服务器来中转网…

Cartographer 栅格地图更新

栅格地图更新过程 首先来了一帧雷达数据&#xff0c;对应到每一个栅格点&#xff0c;即观测得到该栅格点是occupied或者是Free。 在cartographer中&#xff0c;使用CorrespondenceCostValue&#xff08;整数表示的空闲概率&#xff09;表示栅格状态&#xff0c;所以现在的目的就…

学习鸿蒙背后的价值?星河版开放如何学习?

现在是2024年&#xff0c;华为在1月18开展了鸿蒙千帆起仪式发布会。宣布了鸿蒙星河版&#xff0c;并对开发者开放申请&#xff0c;此次发布会主要是说明了&#xff0c;鸿蒙已经是全栈自研底座&#xff0c;鸿蒙星河版本的编程语言改为ArkTS/仓颉&#xff0c;内核改为鸿蒙原生内核…

5.网络游戏逆向分析与漏洞攻防-游戏网络架构逆向分析-测试需求与需求拆解

内容参考于&#xff1a;易道云信息技术研究院VIP课 上一个内容&#xff1a;模拟游戏登陆器启动游戏并且完成注入 首先正常分析软件程序有没有漏洞&#xff0c;需要通过它的操作侵入&#xff0c;比如买东西&#xff0c;就通过买东西的按钮它背后有源代码就看源代码&#xff0c…

开启MySQL远程访问权限,允许远程连接

1、登录mysql数据库 mysql -u root –p 如果端口不是默认的3306&#xff0c;此处端口为3308&#xff0c;使用该指令&#xff1a; mysql –u root –port3308 -p 2、输入密码&#xff1a; 3、使用mysql&#xff0c;查看user表 use mysql; 4、查询user表&#xff0c;root账…

SpringBoot启动报错:Failed to load property source from ‘file:/D:.....

SpringBoot启动报错&#xff1a;Failed to load property source from file:/D:… SpringBoot启动爆如图的错误 2024-02-22 20:57:42.865 ERROR 23024 --- [ restartedMain] o.s.boot.SpringApplication : Application run failedjava.lang.IllegalStateExce…

Centos7环境下安装Docker详细步骤

目录 0.前言 1.卸载旧版 2.配置Docker的yum库 3.安装Docker 4.启动和校验 5.配置镜像加速 5.1.注册阿里云账号 5.2.开通镜像服务 5.3.配置镜像加速 0.前言 环境&#xff1a;Centos7 推荐&#xff1a;买个Centos7阿里或者腾讯云服务&#xff0c;这样就可以不用安装虚…

智慧养老驿站健康监测系统场景需求和技术要求

场景建设需求 1.场景建设核心任务目标 搭建养老驿站的健康检测系统平台&#xff0c;以智慧化手段整合数据、视屏、物联设备&#xff0c;全方位提升对政府、老人、养老机构、服务机构、服务人员等对象的服务支撑能力&#xff0c;赋能居家养老、社区养老、机构养老等多种养老模…
最新文章