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

news2024/10/6 20:32:08

前言

本博客中的代码和解释都是在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.coloradmin.cn/o/1462776.html

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

相关文章

前端基础自学整理|HTML + JavaScript + DOM事件

目录 一、HTML 1、Html标签 2、Html元素 3、基本的HTML标签 二、CSS 样式 层叠样式表 三、JavaScript 使用示例 四、HTML DOM 通过可编程的对象模型&#xff0c;javaScript可以&#xff1a; window document 1、查找HTML元素 2、操作HTML元素 获取元素的属性 四…

现货白银投资指南 主打一个真实

一说到现货白银投资指南&#xff0c;投资者可能就想到那些怎么教投资者赚钱&#xff0c;而且是赚大钱的技巧。老实说&#xff0c;老看这些大路货很没意思&#xff0c;下面我们就来讨论一些真实的现货白银投资指南。 首先在这个现货白银投资指南开篇我们就需要知道&#xff0c;作…

Retrofit2原理分析

Retrofit官网 GitHub上的Retrofit 使用Retrofit进行网络请求的主要步骤 创建一个接口 用于描述HTTP请求。接口里的方法使用注解来标记请求方式、API路径、请求参数等信息。使用Retrofit.Builder().build();配置和创建一个Retrofit实例&#xff1b;调用retrofit.create()方法获…

四、分类算法 - 随机森林

目录 1、集成学习方法 2、随机森林 3、随机森林原理 4、API 5、总结 sklearn转换器和估算器KNN算法模型选择和调优朴素贝叶斯算法决策树随机森林 1、集成学习方法 2、随机森林 3、随机森林原理 4、API 5、总结

开源 - 一款可自定义的在线免杀平台|过x60、wd等

免责声明&#xff1a;本工具仅供安全研究和教学目的使用&#xff0c;用户须自行承担因使用该工具而引起的一切法律及相关责任。作者概不对任何法律责任承担责任&#xff0c;且保留随时中止、修改或终止本工具的权利。使用者应当遵循当地法律法规&#xff0c;并理解并同意本声明…

智慧项目管理平台安全系统开发,实现智慧化、精细化、智能化管理

场景建设需求 1.建设内容&#xff1a;智慧项目管理平台以工程项目为载体&#xff0c;着眼交通运输铁路施工、道路施工、建筑施工相关行业&#xff0c;以标准化、统一化、动态管理为抓手&#xff0c;以互联网、大数据云计算、5G应用、数字孪生、趋势分析、安全预警、视频监控等…

MKS薄膜规622/626/627/628/629说明接口定义等说明

MKS薄膜规622/626/627/628/629说明接口定义等说明

Python 进阶语法:JSON

1 什么是 JSON&#xff1f; 1.1 JSON 的定义 JSON 是 JavaScript Object Notation 的简写&#xff0c;字面上的意思是 JavaScript 对象标记。本质上&#xff0c;JSON 是轻量级的文本数据交换格式。轻量级&#xff0c;是拿它与另一种数据交换格式XML进行比较&#xff0c;相当轻…

突破亚马逊智能检测,全自动化运营的新利器:亚马逊鲲鹏系统

在亚马逊运营的道路上一般最为棘手的问题之一就是账号关联和安全性。而亚马逊鲲鹏系统它不仅拥有最新的防指纹技术&#xff0c;还能够完全模拟真实的人类行为&#xff0c;让每个账号都拥有独立环境运行&#xff0c;从而保证账号的安全性&#xff0c;让用户摆脱了账号关联的困扰…

使用贪婪算法解决作业调度问题

对于贪婪算法的基本思想是,在给定判断条件下,如果每次选择当下能够得到的最佳回报的选项,在很多情况下,这么做使无法实现最优解的,但是贪婪算法要能产生最优解,那他所对应的问题必须是具有特定的递归结构的。 而在某种条件的判断下选取出来最优方案之后,问题的规模就会…

IP地址定位能精确到哪里?——技术限制与定位精度

随着互联网的发展&#xff0c;IP地址定位技术被广泛运用于网络管理、个性化服务等领域。然而&#xff0c;IP地址定位的精确度受到技术限制&#xff0c;无法达到完全精准的地理位置定位。IP数据云将探讨IP地址定位能精确到哪里的技术限制&#xff0c;以及如何在实际应用中克服这…

The Captainz NFT 概览与数据分析

作者&#xff1a;stellafootprint.network 编译&#xff1a;cicifootprint.network 数据源&#xff1a;The Captainz NFT Collection Dashboard The Captainz 是 Memeland 的旗舰系列&#xff0c;由 9,999 个实用性极强的 PFP 组成。持有者在 Memeland 宇宙中展开了一场神…

Python:Keyboard Interrupt - 当代码遇到“Ctrl+C“时发生了什么?

Python&#xff1a;Keyboard Interrupt - 当代码遇到"CtrlC"时发生了什么&#xff1f; &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;【Matplotlib之旅&#xff1a;零基础精通数据可视化】 &#x1f4a1; 创作高质量博文&#x…

@echo off是什么意思

echo off 命令用于关闭命令回显。这意味着在执行批处理文件中的命令时&#xff0c;这些命令本身不会显示在命令行窗口上。 echo off执行以后&#xff0c;后面所有的命令均不显示&#xff0c;包括本条命令。 echo off执行以后&#xff0c;后面所有的命令均不显示&#xff0c;但本…

【PX4学习笔记】13.飞行安全与炸机处理

目录 文章目录 目录使用QGC地面站的安全设置、安全绳安全参数在具体参数中的体现安全绳 无人机炸机处理A&#xff1a;无人机异常时控操作B&#xff1a;无人机炸机现场处理C&#xff1a;无人机炸机后期维护和数据处理D&#xff1a;无人机再次正常飞行测试 无人机飞行法律宣传 使…

nginx优化配置

一 全局配置的六个模块简介 全局块&#xff1a;全局配置&#xff0c;对全局生效 events块&#xff1a;配置影响 Nginx 服务器与用户的网络连接 http块&#xff1a;配置代理&#xff0c;缓存&#xff0c;日志定义等绝大多数功能和第三方模块的配置 server块&#xff1a;配置…

npm/nodejs安装、切换源

前言 发现自己电脑上没有npm也没有node很震惊&#xff0c;难道我没写过代码么&#xff1f;不扯了&#xff0c;进入正题哈哈…… 安装 一般没有npm的话会报错&#xff1a; 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称而且报这个错&#xff0c;我们执行…

idea 启动java 项目时,日志卡住不动,项目重新启动失败,前端页面访问失败

项目场景&#xff1a; 背景&#xff1a; IDEA-启动SpringBoot 项目时&#xff0c;日志卡住不动&#xff0c;项目启动失败 问题描述 问题&#xff1a; IDEA-启动SpringBoot 项目时&#xff0c;日志卡住不动&#xff0c;启动失败&#xff0c;前端页面刷新后访问失败 idea 的左…

12 Autosar_SWS_MemoryMapping.pdf解读

AUTOSAR中MemMap_autosar memmap-CSDN博客 1、Memory Map的作用 1.1 避免RAM的浪费&#xff1a;不同类型的变量&#xff0c;为了对齐造成的空间两份&#xff1b; 1.2 特殊RAM的用途&#xff1a;比如一些变量通过位掩码来获取&#xff0c;如果map到特定RAM可以通过编译器的位掩码…

Qt应用软件【协议篇】MQTT官方源码编译安装

文章目录 QT官方代码选择对应的版本Qt Creator编译代码代码下载与编译安装mqtt命令行方式编译与安装代码示例QT官方代码 https://github.com/qt/qtmqtt/tree/5.15.2 选择对应的版本 我们可以在github上切换分支,切换到我们需要的版本上 Qt Creator编译代码 代码下载与编译…