C++ 多态

news2024/12/19 11:06:33

目录

一、多态的定义和实现

1.1 多态的构成条件:

1.2 虚函数的重写(覆盖):

1.3 多态的两个特殊点:

1.4 析构函数的重写:

1.5 override和final

1.6 重载,重定义(隐藏),重写(覆盖)的区别

二、抽象类

2.1 纯虚函数

2.2 接口继承和实现继承

三、多态的原理

3.1 虚函数表

3.2 验证派生类自己新增的虚函数会不会存储在派生类虚表中:

3.3 多态的原理

3.4 动态绑定与静态绑定

四、多继承中的虚函数表

4.1 普通多继承下的虚函数表:

4.2 菱形继承下的多态

五、多态相关题


一、多态的定义和实现

1.1 多态的构成条件:

构成多态需要两个条件:

1. 必须通过基类的指针或者引用调用虚函数

2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数:被virtual修饰的类成员函数称为虚函数。

1.2 虚函数的重写(覆盖):

派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了父类的虚函数。 (其实就是和基类的虚函数声明完全相同,函数体不同,类似于重新定义函数的行为)

1.3 多态的两个特殊点:

1. 派生类中对基类虚函数进行重写时,可以省略virtual关键字,此时该函数仍然为虚函数且构成重写。建议加上virtual声明,更规范。(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性)

2. 协变:派生类对基类虚函数进行重写时,要求函数名,参数列表,返回值都相同。一个例外是返回值类型可以不同,基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

1.4 析构函数的重写:

回顾继承:

1. 在继承体系中,派生类的析构函数会自动调用基类析构函数去清理基类部分数据成员

2. 并且编译器会将继承体系中的析构函数函数名处理为destructor,所以若不将基类析构函数定义为虚函数,默认为隐藏关系!

那么,如果执行下面代码时,析构函数就必须重写!

class Base
{
public:
	Base()
		:p(new int[10])
	{
	}
	virtual ~Base()
	{
		cout << "~Base()" << endl;
		delete p;
	}
protected:
	int* p;
};

class Derived : public Base
{
public:
	Derived()  // 自动调用基类的默认构造函数
		:p2(new double[10])
	{
	}
	virtual ~Derived()
	{
		cout << "~Derived()" << endl;
		delete p2;
	}
protected:
	double* p2;
};

int main()
{
	Base* p = new Base;
	delete p;
	Base* p2 = new Derived;
	delete p2;
	return 0;
}

delete p2时,delete语句会执行 p2->destructor();   operator delete(ptr2); 调用p2所指向的析构函数,此时若不将析构函数定义为虚函数,则不会发生重写,而是隐藏。则第二个delete时,事实上,Derived类中会有两个析构函数,一个是基类的,一个是自己的。p2类型为Base*,且没有发生多态,只能调用基类的析构函数。

因此,诸如上方示例,建议将继承体系中的析构函数定义为虚函数,这样子类就可以对父类析构函数进行重写。

可以理解,编译后析构函数函数名被编译器处理为destructor(),也是为了便于设为虚函数,发生多态和动态绑定。这样delete基类指针时,才能调用正确的析构函数,即指向父类调用父类的析构函数,指向子类调用子类的析构函数。

1.5 override和final

1. final:修饰虚函数,表示该虚函数不能再被重写

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

1.6 重载,重定义(隐藏),重写(覆盖)的区别

重载:两个函数在同一定义域,函数名相同,参数列表不同。

重定义(隐藏):两个函数分别在基类和派生类的作用域中,函数名相同。

重写(覆盖):两个函数分别在基类和派生类的作用域中,函数名,参数,返回值相同(协变除外)

事实上,基类和派生类中,两个函数如果函数名相同,若没有构成重写,就是重定义(隐藏)

二、抽象类

2.1 纯虚函数

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象(因为继承了纯虚函数),只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Base
{
public:
    virtual void Drive() = 0;
};

2.2 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现,故,实际上调用基类的普通函数时,函数隐含的this指针类型为 Base* const。

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。(因为如果基类定义虚函数,派生类不重写,则Base*->func()调用时 仍然为运行时绑定,只是因为没有重写,执行的一定是基类的虚函数,还降低了效率) 

三、多态的原理

3.1 虚函数表

class Base
{
public:
	Base()
		:p(new int[10])
	{
	}
	virtual ~Base()
	{
		delete p;
	}
	virtual void Func1()
	{
		cout << "virtual void Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "virtual void Base::Func2()" << endl;
	}
    void Func3()
    {
        cout << "void Base::func3()" << endl;
    }
protected:
	int* p;
};

class Derived : public Base
{
public:
	Derived()  // 自动调用基类的默认构造函数
		:p2(new double[10])
	{
	}
	virtual ~Derived()
	{
		delete p2;
	}
	virtual void Func1() // 重写基类Func1
	{
		cout << "virtual void Derived::Func1()" << endl;
	}
protected:
	double* p2;
};

void test1()
{
	Base b;
	Derived d;
}

1. 每一个含有虚函数的类的实例化对象中都有一个指针成员,称为虚函数表指针(虚表指针),即上图中的_vfptr(virtual function ptr),这个指针指向一个虚函数表(简称虚表,一种函数指针数组)这个虚函数表中会存储这个类定义的所有虚函数的地址。

2. 因为基类定义了虚函数,所以每一个虚函数的地址会存储在基类的虚表指针指向的虚表中,派生类继承基类,这个虚表指针和虚表作为基类的数据成员也被继承了下来,派生类内重写了的虚函数的地址会覆盖基类的虚函数的地址。故上图中,基类和派生类的虚表中 析构函数的地址不同,Func1的地址不同,而Func2的地址相同。

3. 虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数地址的覆盖。重写是语法的叫法,覆盖是原理层的叫法。 (派生类虚表中Func1和析构函数覆盖基类的Func1和析构函数)

4. 基类的非虚函数Func3不会放进虚表中。

5. 容易混淆的问题:虚函数存在哪的?虚表存在哪的? 虚函数和普通函数一样,存储在代码段。类对象中存储的是虚表指针,而不是虚表。虚表中存储的是虚函数的地址,而不是虚函数。虚表事实上也是存储在代码段的(只读的,vs下)

6. 派生类虚表生成大致过程:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

3.2 验证派生类自己新增的虚函数会不会存储在派生类虚表中:

class Base
{
public:
	Base()
		:p(new int[10])
	{
	}
	//virtual ~Base()
	//{
	//	delete p;
	//}
	virtual void Func1()
	{
		cout << "virtual void Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "virtual void Base::Func2()" << endl;
	}
protected:
	int* p;
};

class Derived : public Base
{
public:
	Derived()  // 自动调用基类的默认构造函数
		:p2(new double[10])
	{
	}
	//~Derived()
	//{
	//	delete p2;
	//}
	virtual void Func1() // 重写基类Func1
	{
		cout << "virtual void Derived::Func1()" << endl;
	}
	virtual void new_add()
	{
		cout << "virtual void Derived::new_add()" << endl;
	}
protected:
	double* p2;
};

typedef void(*VFPtr)();

void print_virtual_function_table(VFPtr* VFTable)  // 函数指针数组
{
	for (int i = 0; VFTable[i]; ++i)
	{
		printf("VFTable[%d] : %p\n", i, VFTable[i]);
		VFTable[i]();
	}
}
void test1()
{
	Base b;
	Derived d;
	print_virtual_function_table((VFPtr*)*(int*)&d);
}

事实证明,派生类自己新增的虚函数是会按照声明顺序添加到自己的虚函数表的后端的。只是VS下的监控窗口隐藏了。 

3.3 多态的原理

有了上面虚函数表和虚函数指针的基础,再来理解多态的原理。

如上,符合多态的条件:基类指针调用重写好的虚函数。

1. 多态调用:程序(进程)运行过程中,会去指针指向的对象的虚表指针指向的虚表中找到函数的地址,进行调用。所以,p2指向的是基类,调用基类的虚函数,指向派生类,调用派生类的虚函数。

 2. 不满足多态的普通函数调用,编译链接时已经确定函数的地址,运行时直接调用。

感想:这里其实还存在切片的现象,比如基类指针指向派生类对象,可以理解为指针指向的是派生类中基类的这一部分, 虚表指针也属于这一部分。因此,当使用基类指针或者引用(引用同理)调用虚函数时,会去指向对象(基类对象or派生类对象)的虚表指针指向的虚表中找函数地址。发生多态现象,称为动态绑定(运行时绑定)

3.4 动态绑定与静态绑定

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载

2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。

四、多继承中的虚函数表

4.1 普通多继承下的虚函数表:

class Base1
{
public:
	virtual void func1() {
		printf("Base1::func\n");
	}
	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 print_virtual_function_table(VFPtr* VFTable)  // 函数指针数组
{
	for (int i = 0; VFTable[i]; ++i)
	{
		printf("VFTable[%d] : %p\n", i, VFTable[i]);
		VFTable[i]();
	}
}

int main()
{
	Base1 b;
	Derive d;
	print_virtual_function_table((VFPtr*)*(int*)&d);
	cout << endl;
	print_virtual_function_table((VFPtr*)*(int*)((char*)&d + sizeof(Base1)));
	return 0;
}

1. 派生类有两个基类,每个基类都有定义虚函数,则每个基类都有虚函数表指针,派生类继承就会有两个虚函数表指针,func1进行了重写,因此两个虚表中func1的地址都进行了覆盖。func2没有覆盖。而派生类自己新增的虚函数会存储在第一个继承基类部分的虚函数表中

注:这里存在很多编译器的行为,比如这里的地址事实上并不是函数的真实地址(函数的第一条指令的地址),而是某跳转指令的地址,跳转指令会跳转至函数真正的执行语句。但是我们依然理解为虚表中存储的是虚函数的地址!

第二个点:上图中打印的派生类重写了的func1函数,函数地址不同,这里是因为Base2* 去调用这个func1时,要进行一些额外操作:Base2*指向的是基类Base2部分的虚表指针处,调用func1需要事先将此指针减一些偏移量,使其指向派生类对象的开端,即派生类对象的地址,因此函数地址不同。下图所示,执行eax - 8

下图为派生类对象直接调用func1和Base1*调用func1(指向派生类),Base2*调用func1(指向派生类)的汇编代码。

4.2 菱形继承下的多态

class A
{
public:
	virtual void func1()
	{
		cout << "A::func1()" << endl;
	}
public:
	int _a;
};

 //class B : public A
class B : virtual public A
{
public:
	virtual void func1()
	{
		cout << "B::func1()" << endl;
	}

	virtual void func2()
	{
		cout << "B::func2()" << endl;
	}
public:
	int _b;
};

 //class C : public A
class C : virtual public A
{
public:
	virtual void func1()
	{
		cout << "C::func1()" << endl;
	}

	virtual void func2()
	{
		cout << "C::func2()" << endl;
	}
public:
	int _c;
};

class D : public B, public C
{
public:
	virtual void func1()
	{
		cout << "D::func1()" << endl;
	}
public:
	int _d;
};

int main()
{
	D d;
	cout << sizeof(d) << endl;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	//print_virtual_function_table((VFPtr*)*(int*)&d);
	//cout << endl;
	//print_virtual_function_table((VFPtr*)*(int*)((char*)&d + sizeof(B)));
	return 0;
}

 

 D中有BC基类数据,还有_d,BC内都是除了自己的_b _c 还有虚表指针(因为虚函数),虚基表指针(因为虚拟继承)。虚基表中存储着到本类的虚表指针的偏移量,以及基类A部分数据的偏移量。基类A中有一个虚表指针(图中的0x00349b90)和自己的_a

五、多态相关题

   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;
   }

B* p 指向派生类对象。test是一个虚函数,是属于基类的,此时隐含的this指针为A* const this,将p赋值过去(注意这里调用test是一个常规的函数调用),使得this指针为基类类型,指向派生类对象,调用func,发生动态绑定和多态现象。此时调用的是派生类的虚表中存储的func,但是因为虚函数的继承是一种接口继承,目的是为了重写函数体,所以,此时函数的参数列表为int val = 1,因此结果为B->1。

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

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

相关文章

Linux【进程地址空间】

进程地址空间&#x1f4d6;1. 地址空间概念&#x1f4d6;2. 写时拷贝&#x1f4d6;3. 虚拟地址空间的优点&#x1f4d6;1. 地址空间概念 在学习C/C内存管理时&#xff0c;我们可能见过这样一幅图&#xff1a; 但是我们可能不是很理解它&#xff0c;首先有一个问题&#xff1a;…

OpenTCS客户端开发之Web客户端(一)

越来越多人私信我关于OpenTCS的问题。可以感觉到很多人对OpenTCS的研究的人多了很多&#xff0c;很好。这些问题很多是关于算法方面的&#xff0c;也有一部分是关于UI方面的&#xff0c;毕竟OpenTCS本质上是一个算法项目&#xff0c;但是如果希望把它进行商业化&#xff0c;那免…

【微服务】服务拆分和远程调用

2.1 服务拆分原则 这里总结了微服务拆分时的几个原则&#xff1a; 不同微服务&#xff0c;不要重复开发相同业务微服务数据独立&#xff0c;不要访问其它微服务的数据库微服务可以将自己的业务暴露为接口&#xff0c;供其它微服务调用 2.2 服务拆分示例 以微服务cloud-demo为…

第三节:运算符【java】

目录 &#x1f392;运算符 &#x1f4c3;1. 什么是运算符 &#x1f4d7;2. 算术运算符 2.1 基本四则运算符&#xff1a;加减乘除模( - * / %) 2.2 增量运算符 - * % 2.3 自增/自减运算符 -- &#x1f4d9;3. 关系运算符 &#x1f4d5;4.逻辑运算符(重点) 4.1 逻辑与…

隔离出来的“陋室铭”

被隔离了 日常锻炼身体就是去公司旁边的酒店游泳&#xff0c;结果酒店里除了小阳人&#xff0c;我就喜提次密称号&#xff0c;7天隔离走起&#xff1b;又因为不想耽误家里孩子上学&#xff0c;老人外出&#xff0c;就选择了单独隔离&#xff0c;结果就拉到了单独的隔离点&…

精通Git(三)——Git分支机制

文章目录前言分支机制简述创建分支切换分支基本的分支与合并操作基本的分支操作基本的合并操作基本的合并冲突处理分支管理与分支有关的工作流长期分支主题分支远程分支推送跟踪分支拉取删除远程分支变基基本的变基操作变基操作的潜在危害只在需要的时候执行变基操作变基操作与…

C++——vector容器的基本使用和模拟实现

1、vector的介绍 vector是表示可变大小数组的序列容器。 就像数组一样&#xff0c;vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素 进行访问&#xff0c;和数组一样高效。但是又不像数组&#xff0c;它的大小是可以动态改变的&#xff0c;而且…

【动手学深度学习PyTorch版】18 使用块的网络 VGG

上一篇请移步【动手学深度学习PyTorch版】17 深度卷积神经网络 AlexNet_水w的博客-CSDN博客 目录 一、使用块的网络 VGG 1.1 AlexNet--->VGG ◼ VGG网络简介 1.2 VGG架构 1.3 总结 二、VGG网络的代码实现 2.1 VGG网络&#xff08;使用自定义&#xff09; 一、使用块的…

软件测试基本概念

目录本章要点什么是软件测试?软件测试的特定?软件测试和开发的区别?软件测试和软件开发中的调试有什么区别?软件测试在不同公司的定位?一个优秀的测试人员应该具备的素质(你为啥要选择测试开发)需求是衡量软件测试的依据从软件测试人员角度看需求为啥需求对软件测试人员如…

SpringBoot 面试题总结 (JavaGuide)

SpringBoot 面试题总结 &#xff08;JavaGuide&#xff09; 用 JavaGuide 复习 SpringBoot 时&#xff0c;找到一些面试题&#xff0c;没有答案&#xff0c;自己花了一天时间在网上找资料总结了一些&#xff0c;有些答案的来源比较杂忘了没有标注&#xff0c;望见谅。 1. 简单…

Visual Studio 2022开发Arduino详述

目录&#xff1a; 一、概述 二、软件的下载与安装 1、前言 2、Visual Studio 2022的下载与安装 3、Visual Micro扩展插件的导入 4、Visual Micro的使用 1&#xff09;安装修改插件 2&#xff09;搜索 : Visual.Micro.Processing.Sketch.dll 3&#xff09;打开Visual.…

【Linux学习】基础IO

目录前言一、C语言文件IO1. C语言文件接口以及打开方式2. 对当前路径的理解3. 默认打开的三个流二、 系统文件IO1. 系统接口openwritereadclose系统接口和库函数2. 文件描述符及其分配规则文件描述符文件描述符分配原则3. 重定向及dup2系统调用重定向标准输出和标准错误的区别d…

Linux XWindow的安装和配置

1.开始安装XWindow必须需要的组件 输入指令&#xff1a;yum groupinstall "X Window System" yum groupinstall "X Window System" 选择y继续安装。 当看到complete表示已经安装成功了。 输入startx测试一下 看到如上界面就证明你的XWindow安装成功了。 2…

Python数据分析(3):pandas

文章目录二. pandas入门2.1 数据结构2.1.1 Series对象2.1.2 DataFrame对象2.2 读取数据2.2.1 读取Excel&#xff1a;read_excel()1. 读取特定工作簿&#xff1a;sheet_name2. 指定列标签&#xff1a;header3. 指定行标签&#xff1a;index_col4. 读取指定列&#xff1a;usecols…

TypeScript接口——interface

目录 一、接口概述&#xff1a; 二、接口类型介绍&#xff1a; 1、属性接口&#xff1a; 2、 函数接口&#xff1a; 3、可索引接口&#xff1a; &#xff08;1&#xff09;可索引接口约束数组示例&#xff1a; &#xff08;2&#xff09; 可索引接口约束对象示例&#xf…

【Python】numpy矩阵运算大全

文章目录前言0 遇事不决&#xff0c;先查官网&#xff0c;查着查着就查熟了1 矩阵运算及其必要性2 矩阵的创建2.1 普通矩阵2.2 特殊矩阵3 矩阵的索引3.1 str, list, tupple的索引3.2 numpy索引4 矩阵的运算4.1 通用函数与广播机制4.3 矩阵乘法4.4 矩阵求逆4.5 矩阵转置4.6 向量…

SpringBoot整合mybatis-plus 实现增删改查和分页查询

SpringBoot整合mybatis-plus 实现增删改查和分页查询整体的运行图片&#xff1a;一、环境搭建&#xff1a;1、依赖2、application.yml文件3、数据库二、实体类&#xff1a;三、数据层开发——基础CRUD四、业务层开发——分页功能制作4.1分页配置类 configuration4.2service接口…

【Node.js】模块化学习

Node.js教学 专栏 从头开始学习 目录 模块化的基本概念 什么是模块化 现实中的模块化 编程领域中的模块化 模块化规范 Node.js中的模块化 Node.js中模块的分类 加载模块 Node.js中的模块作用域 什么是模块作用域 模块作用域好处 向外共享模块作用域中的成员 module对象 modu…

第二站:分支与循环(终幕)一些经典的题目

目录 一、计算n的阶乘 1.一般解法 2.优化不能表示出较大数的阶乘 二、 计算 1!2!3!……10! 1.循环嵌套解法 2.一次循环解法(优化计算时间) 三、在一个有序数组中查找具体的某个数字n 1.遍历查找 2.二分查找算法&#xff08;优化了查找时间&#xff09; 四、编写代码&am…

IDEA Out of memory 问题

文章目录1. 前提2. 问题记录与解决方案1. 前提 阅读本文之前&#xff0c;读者要首先把 Out of memory 这个问题的解决方案多搜几个帖子&#xff0c;先按照其他帖子的解决方案&#xff08;修改配置文件Xmx属性等&#xff09;尝试一遍&#xff0c;不能解决再参考本文。 本文所描…