C++多态

C++中的多态分为静态多态和动态多态两种,其中:

静态多态在编译阶段实现,其原理是由函数重载实现,通过不同的实参调用其相应的同名函数。

动态多态通过虚函数实现,以下着重介绍

动态多态的两个必要条件

  1. 必须通过基类的指针或者引用调用
  2. 被调用的必须是虚函数,且在派生类中实现了该虚函数的重写 (注意:只有虚函数才有重写这个概念)

此两个条件缺一不可

以下是一个动态多态的典型案例

class Person {
public:
	virtual void buyBucket(){
		std::cout << "全价" << std::endl;
	}
};
class student:public Person {
public:
	virtual void buyBucket() {
		std::cout << "半价" << std::endl;
	}
};
int main() {
	Person p;
	student s;
	Person &tmp1 = p;
	Person &tmp2 = s;//tmp2为基类Person对派生类student的引用
    Person *tmp3 = &s;//tmp3为基类Person指向派生类student的指针
	tmp1.buyBucket();
	tmp2.buyBucket();
    tmp3.buyBucket();
	return 0;
}

何为通过基类的指针或者引用调用?

使用基类型的指针或者引用

虚函数的重写:

首先明确什么是虚函数,virtual修饰的成员函数称为虚函数(友元函数不属于成员函数)

重写的定义:派生类中的虚函数与基类中虚函数满足函数名、参数、返回值均相同,叫做重写(覆盖)。

案例中派生类中的buyBucket()即完成了对基类同名虚函数的重写

重写的特殊情况--协变:当基类和派生类中该函数的返回值为父子关系的指针或者引用,返回值可以不同,这种情况称作协变

//协变 同理指针类型的返回值也能构成协变
class Person {
public:
	int a;
	virtual Person& buyBucket(){
		std::cout << "全价" << std::endl;
		return *this;
	}
};
class student:public Person {
public:
	int b;
	virtual student& buyBucket() {
		std::cout << "半价" << std::endl;
		return *this;
	}
};


与隐藏的区别:

隐藏:在子类的作用域子类的函数,根据就近原则会屏蔽父类中的同名函数,即子类中的同名函数即构成对父类中函数的隐藏。

只需满足函数名相同即可构成隐藏,可以认为重写是一种非常特殊的隐藏。

如果父类和子类都有相同的方法,参数个数不同, 将子类对象赋给父类对象后, 采用父类对象调用该同名方法时,实际调用的是父类的方法

重写的格式:

构成重写的函数都应是虚函数,即使用virtual修饰的成员函数,同时还要满足三同(函数名、返回值、参数)

建议使用标准格式编写代码,但仍有以下特殊情况:

1.协变:当基类和派生类中该函数的返回值为父子关系的指针或者引用,返回值可以不同,这种情况称作协变

2.当派生类的虚函数如果与基类的虚函数构成重写时,派生类的虚函数可以省略virtual关键字。

这是因为派生类在继承基类的虚函数时,首先继承了虚函数的属性,此时只要满足三同的条件,无论该函数是否有virtual关键字修饰,都会被编译器认为是虚函数,触发重写。

析构函数的重写:析构函数的函数名会被编译器默认处理为destruct()

所以如果将析构函数声明为虚函数,那么析构函数也可以构成重写。由于第二种特殊情况的情况,当基类的析构函数声明为虚函数后,后续所有的派生类的析构函数,无论是否使用virtual修饰,都会与基类的析构函数构成重写。

这样是为了解决一个特殊的应用场景,当使用基类的指针指向new申请的派生类对象时,delete时,如果析构函数未构成重写,delete将会调用基类的析构函数去析构派生类,这样会导致崩溃

多态的原理:

首先介绍虚函数表,在定义了虚函数的类中,都会有一个虚函数表指针,指向该类的虚函数表,虚函数的表指针会在构造函数初始化列表进行初始化,当派生类继承基类时,会继承基类的虚函数表(本质是函数指针数组);如果在派生类中实现了重写,那么派生类中的虚函数表会使用重写后函数的地址覆盖原地址。

总结虚函数表的生成过程,即:

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

注意:进行切片操作时,不会对虚函数表进行切片。这就导致,如果使用基类的对象调用函数,基类中的虚表仍是父类的虚表,无法实现多态。(基类的虚函数表指针会在初始化列表初始化,初始化后指向基类的虚表)

虚函数表同样存储在代码段上,因为一个类的不同对象共用一个虚函数表。如果存在栈上,那么每个对象都会维护一个自己的虚函数表,将会造成冗余。

C++多态_第1张图片

 多态的本质即基类的引用或指针指向谁,就去谁的的虚函数表中找到对应的虚函数调用调用

ps:即使将虚函数定义为私有,仍可通过函数指针找到虚函数的地址直接调用,这构成了一定的安全隐患

抽象类

抽象类即包含了纯虚函数的类,抽象类又称为接口类。抽象类无法实例化出对象,如果其子类未完成对父类纯虚函数的重写,那么子类也是抽象类,同样无法完成实例化对象。

抽象类实质上完成了强制使子类去完成父类虚函数的重写。

什么是纯虚函数?  纯虚函数格式为 :虚函数=0  如:virtual void A()=0;  纯虚函数一般只定义不实现,因为实现也没有意义。

如果一种事物,我们无法在现实世界中找到对应的实体,我们可以将它定义为抽象类。

通过抽象类,我们可以实现一切皆xx。如定义一个抽象文件类,在此抽象类中定义一个write()的纯虚函数,我们继承此抽象类并在在所有的具体类型中重写其相应write()函数,那么对于上层来说,无论哪种类型,我们都统一将它看作文件,且写入函数都是write()。

tips:在linux中,并不是此原理,而是使用C语言的函数指针实现了一切皆文件

PS:重载、重写和隐藏的区别

重载:两个函数在同一作用域下且函数名相同,就构成了函数重载,对参数、返回值没有要求。通过函数模板和类模板,在编译的时候重载适用于不同类型的函数,C++实现了泛型编程。

重写:两个函数分别在基类和派生类的作用域,且函数必须是虚函数,同时函数名、参数列表和返回值必须相同(协变情况除外)

隐藏(重定义):两个函数分别在基类和派生类的作用域,且函数名相同,同时不满足重写的条件的两个函数构成隐藏

你可能感兴趣的:(c++)