【C++】多态

本文详细介绍了多态的概念,包括其定义、构成条件、虚函数、虚函数重写、抽象类、虚函数表和运行时/编译时多态。通过实例阐述了多态的原理和常见问题,如虚函数与普通函数的区别、静态成员函数和构造函数的限制等。
摘要由CSDN通过智能技术生成

目录

一、多态的概念:

二、多态的定义及实现:

1、多态的构成条件:

2、虚函数:

3、虚函数的重写:

虚函数重写 / 覆盖的语法与原理层概念:

虚函数重写的例外情况:

4、override 与 final函数

5、重载、重写、重定义的对比:

6、抽象类:

接口继承与实现继承:

三、多态实现的原理:

虚函数表:

虚表指针:

底层如何实现多态:

运行时多态 / 编译时多态 :

四、关于多态的一些问题总结:


一、多态的概念:

        多态是面向对象编程的三大特性之一,它指的是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。再具体一点就是:去完成某一个行为时,不同的对象去完成会产生出不同的状态。

        比如买高铁票这个行为:学生买高铁票时可以半价买票,成人买票时就是全价买票,这时就可以看作是一个多态的一个表现,都是买票这一个行为,学生和成人分别去完成这个行为时,就会产生不同的状态。

二、多态的定义及实现:

1、多态的构成条件:

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。那么再继承中,要构成多态还有两个重要的条件:

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

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

例如下图:

还是买票这个例子,创建一个成人对象A,和一个学生对象B,分别传入Func函数中,Func函数参数为一个父类对象的引用(或者指针也可以),然后根据对象的不同就会分别的去调用各自的虚函数,学生调用学生的,输出半票,成人调用成人的,输出全票。(前提是学生类中有完成成人类中虚函数的重写)

2、虚函数:

概念:

        当一个基类中的成员函数被声明为虚函数时,它允许派生类重写该函数,并在运行时根据对象的实际类型来确定调用哪个版本的函数。(被virtual修饰的类成员函数成为虚函数

virtual关键字可以不在子类函数上添加,但必须在父类函数上添加,否则就不是虚函数无法构成虚函数重写而是构成重定义(隐藏)了。但是还是建议在子类函数上也添加上。

如上面图中的 Buy_Ticket() 函数就是一个虚函数,被virtual关键字修饰,并在子类中完成了重写。

3、虚函数的重写:

概念:

        虚函数的重写也叫做覆盖,派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同以下简称为三同),,称子类的虚函数重写了基类的虚函数。

        如上图中的子类的Buy_Ticket()函数就是对父类中的Buy_Ticket()函数完成了重写,以实现不同的功能。

虚函数重写 / 覆盖的语法与原理层概念:

语法层:

        从语法上来看是派生类对继承基类虚函数实现进行了重写。重写是语法层的叫法。   

原理层:

        从原理上来看是子类的虚表,拷贝父类虚表进行了修改,覆盖那个虚函数。覆盖是原理层的叫法(虚表在后面解释)

注意:

        对于虚函数的重写,本质是:在父类中声明,在子类中定义,比如如下一道题:问输出的是什么?

这里估计很多人都会选d选项,但是其实不是这样,按照我们上面的说法,子类函数和父类函数三同时只要父类加了virtual,子类可以不加virtual也构成重写,当虚函数构成重写时,本质是在父类中声明,在子类中定义,再来看这道题,虽然最后执行的是B类中的func()函数,但是声明是使用的A类中的声明,即使变量名改变也一样,所以答案是b。

虚函数重写的例外情况:

        从上面我们说过,实现虚函数的重写的前提条件得是三同(同名,同参,同返回值),但是在C++设计之初的时候总是会带点缺陷,但是因为要向前兼容的情况不能对例外的情况一刀切,所以就保留下来了这两个例外:

1、基类和派生类的析构函数构成重写:

        如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然函数名不相同, 看起来违背了重写的规则,其实不然,可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor()。

class Person
{
public:
	virtual ~Person() { cout << "~~Person()" << endl; }
};

class Student : public Person
{
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};

int main()
{
	// Person指针既可以指向父类对象也可以指向子类对象(因为切片的原因)
	Person* p1 = new Person;
	Person* p2 = new Student;

	// 在这里要是析构不是虚函数的话调用的就都是父类的析构,如果子类有申请资源就得不到释放,造成内存泄漏
	delete p1;
	delete p2;
}

所以建议将析构函数写成虚函数,防止子类资源无法释放造成的内存泄漏。

2、协变(基类与派生类虚函数返回值类型不同,但必须是各自的指针或引用)

        派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

class A
{
public:
	virtual A* func(){ return nullptr; }
};

class B
{
public:     // 此时返回值类型为各自类型的指针(引用也行)也构成函数重写(称为协变)
	virtual B* func() { return nullptr; }
};

4、override 与 final函数

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

class A
{
public:
	virtual void func() final { } // 在父类函数中添加,表示该虚函数不能再被重写
};
class B : public A
{
	// ...
}

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

class A
{
public:
	virtual void func(){ } 
};
class B : public A
{
public:
	virtual void func() override { cout << "func override"; } // 在子类函数添加,检查是否重写
};

5、重载、重写、重定义的对比:

6、抽象类:

概念:

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

抽象类定义的一些抽象方法在抽象类中是没有具体的实现的,而是由子类去实现。这实际上是在定义一种接口,子类必须实现这些抽象方法才能被实例化。这种方式可以强制子类遵循某种规范或约定,保证程序的一致性和正确性。

接口继承与实现继承:

接口继承:

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

实现继承:

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

三、多态实现的原理:

虚函数表:

概念:

        这里的虚函数表与菱形虚拟继承的虚基表不一样,虚基表存放的是偏移量,表示该表的位置与基类那一部分内容的地址之间的距离。通过这个偏移量,子类可以在运行时准确地找到基类部分的地址,从而避免了数据冗余和二义性的问题,而虚函数表(简称虚表)则是:当一个类包含至少一个虚函数时,编译器会为这个类生成一个虚函数表。这个表是一个指针数组,其中每个指针指向类的虚函数。一个类的不同对象共用该类的虚表。

虚表指针:

比如:你觉得sizeof(A),也就是这个类的大小是多少?

class A
{
public:
	virtual void func() {}
private:
	int _a = 1;
};

正常一般都会觉得,这不就是4byte嘛,但是其实不是,让我们实例化一个A对象看一下:

从这里我们可以看到,a对象中不仅有变量_a,还多了一个指针_vfptr,这个就是虚函数表指针,简称虚表指针,只要类中有虚函数,就会生成这个虚表指针(占4/8byte)。

虚表指针指向一个指针数组,指针数组中存着各个虚函数的地址,也就是一个类只会有一个虚表,一个虚函数对应一个虚表指针,指针指向虚函数的地址。

底层如何实现多态:

就还是那文章开头买票的那个代码为例:

运行时多态 / 编译时多态 :

运行时多态:

        运行时多态是动态绑定,也叫晚期绑定;运行时的多态性实现依赖于虚函数和虚函数表。当程序运行时,通过基类指针或引用调用虚函数,程序会根据对象的实际类型查找虚函数表,并调用相应的函数从而实现多态。

编译时多态:

        编译时多态是静态绑定,也叫早期绑定,它主要发生在模板和函数重载中。在编译时多态中,编译器根据函数的参数列表或其他特性在编译期间就确定了应该调用哪个函数。由于这种确定是在编译时完成的,因此它不需要在运行时进行额外的类型检查或地址查找。

四、关于多态的一些问题总结:

1、虚函数和普通函数一样,是存在常量区(代码段)的,虚表中存储的是指针,指向虚函数的地址,虚表也是存在常量区的。

2、子类有虚函数、继承的父类有虚函数就有虚表,子类对象就不需要单独建立虚表。

3、在多继承中,如果继承有n个父类对象(都有各自的虚表),那么在子类中也会继承这n个虚表指针。

4、在单继承派生类的未重写的虚函数是放在基类的虚函数表中的,但是在多继承中,派生类的未重写的虚函数放在第一个继承的基类的虚函数表中。

5、静态成员函数不可以是虚函数。因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

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

7、析构函数可以是虚函数。且最好把基类的析构函数定义成虚函数。析构函数名统一会被处理成destructor()。

8、内联函数可以是虚函数。不过多态调用的时候编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值