C++中的多态分为静态多态和动态多态两种,其中:
静态多态在编译阶段实现,其原理是由函数重载实现,通过不同的实参调用其相应的同名函数。
动态多态通过虚函数实现,以下着重介绍
动态多态的两个必要条件:
- 必须通过基类的指针或者引用调用
- 被调用的必须是虚函数,且在派生类中实现了该虚函数的重写 (注意:只有虚函数才有重写这个概念)
此两个条件缺一不可
以下是一个动态多态的典型案例
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.派生类自己新增加的虚函数按其在 派生类中的声明次序增加到派生类虚表的最后
注意:进行切片操作时,不会对虚函数表进行切片。这就导致,如果使用基类的对象调用函数,基类中的虚表仍是父类的虚表,无法实现多态。(基类的虚函数表指针会在初始化列表初始化,初始化后指向基类的虚表)
虚函数表同样存储在代码段上,因为一个类的不同对象共用一个虚函数表。如果存在栈上,那么每个对象都会维护一个自己的虚函数表,将会造成冗余。
多态的本质即基类的引用或指针指向谁,就去谁的的虚函数表中找到对应的虚函数调用调用
ps:即使将虚函数定义为私有,仍可通过函数指针找到虚函数的地址直接调用,这构成了一定的安全隐患
抽象类
抽象类即包含了纯虚函数的类,抽象类又称为接口类。抽象类无法实例化出对象,如果其子类未完成对父类纯虚函数的重写,那么子类也是抽象类,同样无法完成实例化对象。
抽象类实质上完成了强制使子类去完成父类虚函数的重写。
什么是纯虚函数? 纯虚函数格式为 :虚函数=0 如:virtual void A()=0; 纯虚函数一般只定义不实现,因为实现也没有意义。
如果一种事物,我们无法在现实世界中找到对应的实体,我们可以将它定义为抽象类。
通过抽象类,我们可以实现一切皆xx。如定义一个抽象文件类,在此抽象类中定义一个write()的纯虚函数,我们继承此抽象类并在在所有的具体类型中重写其相应write()函数,那么对于上层来说,无论哪种类型,我们都统一将它看作文件,且写入函数都是write()。
tips:在linux中,并不是此原理,而是使用C语言的函数指针实现了一切皆文件
PS:重载、重写和隐藏的区别
重载:两个函数在同一作用域下且函数名相同,就构成了函数重载,对参数、返回值没有要求。通过函数模板和类模板,在编译的时候重载适用于不同类型的函数,C++实现了泛型编程。
重写:两个函数分别在基类和派生类的作用域,且函数必须是虚函数,同时函数名、参数列表和返回值必须相同(协变情况除外)
隐藏(重定义):两个函数分别在基类和派生类的作用域,且函数名相同,同时不满足重写的条件的两个函数构成隐藏