C++ 多态

news/2024/11/25 0:29:56/

目录

一.   概念

二.   定义与实现

1.构成条件

2.虚函数

3.虚函数的重写

4.协变

5.override 和 final

三.   抽象类

四.   虚函数表

1.单继承虚函数表

2.多继承的虚函数表


一.   概念

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

而不同的对象,我们可以使用函数重载来实现,也可以使用继承中的基类和派生类来表示

静态,主要是包括静态的和动态的,函数重载是静态的,是在编译的过程中实现的,而继承是动态的多态,是在运行的时候实现的,我们主要来看的是动态的多态


二.   定义与实现

1.构成条件

在继承中要构成多态有两个条件:

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

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

2.虚函数

虚函数是指被virtual修饰的非静态的类成员函数

class A {
public:virtual void Name() { cout << 'A' << endl; }
};

3.虚函数的重写

想要实现虚函数的重写,需要让基类与派生类的函数名、返回值类型、参数(个数、类型)都相同

class A {
public:virtual void Name() { cout << 'A' << endl; }
};class B : public A {
public:virtual void Name() { cout << 'B' << endl; }
};

而在虚函数重写时,派生类的虚函数也可以不加virtual关键字,依然可以实现虚函数的重写,这是因为,B类的Name函数继承了A类Name函数的属性(包括virtual关键字、访问限定符)

因此,也可以写作这样

class A {
public:virtual void Name() { cout << 'A' << endl; }
};class B : public A {
private:void Name() { cout << 'B' << endl; }
};

实践一下 

class A {
public:virtual void Name() { cout << 'A' << endl; }
};class B : public A {
private:void Name() { cout << 'B' << endl; }
};void Func(A& n)
{n.Name();
}
int main()
{A a;B b;Func(a);Func(b);return 0;
}

可以看到,依旧能够完成虚函数的重写,而同样,即使B类的Name函数的访问限定符为private,依旧可以在类外的Func函数中被调用。

而对于构造函数和析构函数来说,首先构造函数不能是虚函数,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的,至于什么是虚函数表后面再说。

而对于析构函数来说,首先我们在继承中提到过,析构函数会被处理成destrutor(),因此基类和派生类的析构函数函数名是相同的,因此析构函数可以是虚函数,而我们也尽量将析构函数写作虚函数。

class A {
public:~A(){ cout << "~A()" << endl; }
};class B : public A {
public:~B() { cout << "~B()" << endl; }
};int main()
{A* a =new A;delete a;B* b = new B;delete b;return 0;
}

首先如果我们正常使用new和delete,可以看到,在delete对象b时调用了派生类和基类的析构函数

但如果我们这样去使用的话

class A {
public:~A(){ cout << "~A()" << endl; }
};class B : public A {
public:~B() { cout << "~B()" << endl; }
};int main()
{A* a =new A;delete a;A* b = new B;delete b;return 0;
}

会发现delete对象b只会去调用基类的析构函数,引起内存泄漏

因此我们可以将析构函数写作虚函数来解决这个问题

class A {
public:virtual ~A(){ cout << "~A()" << endl; }
};class B : public A {
public:virtual ~B() { cout << "~B()" << endl; }
};int main()
{A* a =new A;delete a;A* b = new B;delete b;return 0;
}

 

4.协变

我们上面说到,想要实现重写,其中一个条件是返回值相同,但当返回值为该类的指针或引用时,也可以实现重写

class A {
public:virtual A* Name() {cout << 'A' << endl; return this;}
private:int _a = 10;
};class B : public A {
public:virtual B* Name(){ cout << 'B' << endl; return this;}
private:int _b = 20;
};

5.override 和 final

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

class A {
public:virtual void Name() final { cout << 'A' << endl; }
private:int _a = 10;
};class B : public A {
public:virtual void Name() { cout << 'B' << endl; }
private:int _b = 20;
};

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

class A {
public:virtual void Name() { cout << 'A' << endl; }
private:int _a = 10;
};class B : public A {
public:virtual void Name(int i) override{ cout << 'B' << endl; }
private:int _b = 20;
};


三.   抽象类

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

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


四.   虚函数表

1.单继承虚函数表

class A {
public:virtual void Func1(){}
private:int _a = 10;
};class B : public A {
public:virtual void Func1(){}
private:int _b = 20;
};int main()
{A a;B b;return 0;
}

 在调试的过程中,我们可以看到在对象中除了类成员变量,还存在一个名为_vfptr的指针,对应的地址中存放的是一个数组,而这个数组就被称作是虚函数表,而这个数组,实质上是一个函数指针数组。

而我们也可以通过内存来观察一下,其中虚函数表最后会放一个nullptr

 而若是我们多添加几个虚函数

class A {
public:virtual void Func1(){}virtual void Func2(){}
private:int _a = 10;
};class B : public A {
public:virtual void Func1() {}virtual void Func3() {}
private:int _b = 20;
};int main()
{A a;B b;return 0;
}

而我们可以看到,虚函数表中并没有显示有Func3,但其实是存在的,我们可以通过内存来看 

 

可以看到,在nullptr之前,有三个地址,最后一个代表的就是Func3

但当我们观察Func3的地址时发现和虚基表中的地址并不相同,就连Func1和Func2的也不相同,这是因为,在我使用的vs中,进行了一些处理,我们可以调用一下Func1来通过反汇编观察

class A {
public:virtual void Func1(){}virtual void Func2() {}
private:int _a = 10;
};class B : public A {
public:virtual void Func1(){}virtual void Func3() {};
private:int _b = 20;
};int main()
{A a;B b;b.Func1();return 0;
}

 

 

 

可以看到,通过call,我们调用到了jmp,它的地址就是存在虚函数表里的地址 

而通过jmp我们才找到Func1函数。

当然,我们也可以通过打印虚函数表来观察

首先整体思路就是通过类型转换来使对象的地址所指向的内容为单个指针大小(32位4字节,64位8字节),之后通过遍历知道结尾处的nullptr

首先我们可以先将函数的实现写出来

class A
{
public:virtual void Func1() { cout << "Func1" << endl; }virtual void Func2() { cout << "Func2" << endl; }
};class B:public A
{
public:virtual void Func1() { cout << "Func1" << endl; }virtual void Func3() { cout << "Func3" << endl; }
};typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :%p,->", i+1, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}

重点在于应该如何进行传参

首先我们需要强制类型转换为4字节(8字节),这里我们可以使用int*(long long*)来实现,不过这样需要进行条件编译来判断应该使用Int*还是long long*,还有一种方式,我们可以直接使用void**来完成。之后我们就需要进行解引用操作,最后由于类型不匹配,我们还需要进行一次强转为VFPTR*,这样我们就可以传参了

class A
{
public:virtual void Func1() { cout << "Func1" << endl; }virtual void Func2() { cout << "Func2" << endl; }
};class B:public A
{
public:virtual void Func1() { cout << "Func1" << endl; }virtual void Func3() { cout << "Func3" << endl; }
};typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :%p,->", i+1, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}int main()
{B b;PrintVTable((VFPTR*)(*(int*)&b));return 0;
}

 

总结一下单继承的虚函数表,我们可以得到以下结论

1.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。另外Func2继承下来后是虚函数,所以也放进了虚表

2.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

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

4.和普通函数相同,虚函数也是存在代码段的,只是需要通过对象中的虚函数表来进行调用

2.多继承的虚函数表

class A {
public:virtual void Func1() {  }virtual void Func2() {  }
private:int _a = 10;
};class B
{
public:virtual void Func1() {  }virtual void Func3() {  }
private:int _b = 20;
};class C : public A, public B {
public:virtual void Func1(){  }
private:int _c = 30;
};int main()
{A a;B b;C c;return 0;
}



我们可以看到,派生类中的两个虚函数表中,代表Func1的指针并不相同,我们在上面说到过,虚函数表中存的其实是jmp指令,而这两个jmp指令会指向同一个Func1。

class A {
public:virtual void Func1() {  }virtual void Func2() {  }
private:int _a = 10;
};class B
{
public:virtual void Func1() {  }virtual void Func3() {  }
private:int _b = 20;
};class C : public A, public B {
public:virtual void Func1() {  }
private:int _c = 30;
};int main()
{C c;A& a = c;B& b = c;a.Func1();b.Func1();return 0;
}

我们可以用上述的代码通过汇编来看,这里就不多做演示


 

 


http://www.ppmy.cn/news/21573.html

相关文章

Python | 数据类型之集合 | 函数

知识目录一、集合简介1.1 集合的定义1.2 实例二、集合的基本操作三、函数3.1 函数的定义3.2 函数的调用3.3 全局变量和局部变量一、集合简介 1.1 集合的定义 集合&#xff08;set&#xff09;是一个无序的不重复元素序列。 可以使用大括号 { } 或者 set() 函数创建集合&…

4.6 QR分解二:Householder变换

1 Householder reflector Householder反射是这样子的(图片来自瑞典皇家理工学院)&#xff1a;   图中u是长度为1的向量。x是任意向量&#xff0c;H是u的Householder reflector。可见无论x是什么向量&#xff0c;HxHxHx始终除于和u正交的平面上。H和u的关系是&#xff1a; HI…

入职一年,那个准的下班的人,比我先升职了...

最近心态崩了。 和我同期一道进公司的人又升了一级&#xff0c;可是明明大家在进公司时&#xff0c;他不论是学历还是工作经验&#xff0c;样样都不如自己&#xff0c;眼下不过短短的两年时间便一跃在自己的职级之上&#xff0c;这着实让我有几分不甘心。 我想不明白&#xff…

明细打印重影方案

一、问题描述生产上出现明细查询打印业务&#xff0c;部分客户打印数据时出现数据重叠现象&#xff0c;不利于客户使用&#xff0c;影响客户体验。二、问题原因对方户名公司名称字段目前没有限制&#xff0c;按照现有的分页处理机制&#xff0c;如果一页纸出现多个公司名称较长…

什么是单体应用?什么是微服务?

Monolith&#xff08;单体应用&#xff09;&#xff0c; 也称之为单体系统或者是 单体架构 。就是一种把系统中所有的功能、模块、组件等耦合在一个应用中应用最终打成一个(war,jar)包使用一个容器(Tomcat)进行部署&#xff0c;通常一个应用享用一个数据库。 也就是将所有的代码…

C语言进阶——字符函数和字符串函数(上)

目录 一、前言 二、正文 1.求字符串长度 ♥strlen 2.长度不受限制的字符串函数 ♥strcpy ♥strcat ♥strcmp 三、结语 一、前言 一日不见&#xff0c;如隔三秋&#xff1b;几日不见&#xff0c;甚是想念。猜想小伙伴们在平常进行有关字符的练习时遇到有关字符的操作却无从下手…

蓝桥杯STM32G431RBT6学习——M24C02

蓝桥杯STM32G431RBT6学习——M24C02 前言 IIC是单片机的通用协议&#xff0c;在蓝桥杯单片机、嵌入式中都是考点。国信长天开发板板载M24C02&#xff08;IIC驱动&#xff09;作为调电存储模块&#xff0c;可以通过IIC对其写入数据后&#xff0c;掉电进行保存以供读取。其硬件…

想成为数据分析师,看这里,数据分析必备的43个Excel函数

目录 前言 函数分类&#xff1a; 关联匹配类清洗处理类逻辑运算类计算统计类时间序列类 前言 Excel是我们工作中经常使用的一种工具&#xff0c;对于数据分析来说&#xff0c;这也是处理数据最基础的工具。 很多传统行业的数据分析师甚至只要掌握Excel和SQL即可。 对于初学者…