1. 多态(polymorphism)
多态,就是 "多种形态" 的意思。
说具体点就是:去完成某个行为,不同的对象去做会产生不同的结果(状态)。
比如说地铁站买票这个行为,普通人、学生、军人买票是不同的。
普通人必须买全价票,学生就可能可以买半价票,而军人可以优先购买到预留票:
比如有一个 BuyTicket 买票的成员函数,创建普通人、学生和军人三个对象,
他们调用该函数形态结果我们就要设计成不一样的。
这种”不一样“的情况还有各种VIP等等。
所以由此可见,我们需要一种特性来做到这种 "分类" 的操作,这时我们就可以将其实现成多态。
1.1 构成多态的两个条件
构成多态的两个条件:
1、虚函数重写(覆盖) -> 虚函数 + 三同:函数名、参数和返回值相同,不符合重写就是隐藏。
2、父类指针或者引用去调用虚函数
特例1:子类虚函数不加virtual,依旧构成重写 (实际最好加上)
特例2:重写的协变。返回值可以不同,要求必须是父子关系的的指针或者引用
1.2 虚函数重写(覆盖)
我们先用代码实现一下我们刚才的购票场景。将 Student 和 Soldier 继承自 Person:
class Person {}; class Student : public Person {}; class Soldier : public Person {};
这里用 virtual 虚函数,并且做到函数名、参数和返回值相同,就能够达到 "重写" 的效果:
(不符合重写,就是隐藏关系)
class Person { public: virtual void BuyTicket() // virtual + 返回值 + 函数名+ 参数 相同 = 构成多态 { cout << "Person: 买票-全价" << endl; } }; class Student : public Person { public: // 这里也都相同 virtual void BuyTicket() { cout << "Student: 买票-半价" << endl; } }; class Soldier : public Person { public: // 这里也都相同 virtual void BuyTicket() { cout << "Soldier: 优先买票" << endl; } };
概念:重写也称为覆盖,重写即重新改写。
重写是为了将一个已有的事物进行某些改变以适应新的要求。
重写是子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。
最后我们再设计一个 Pay 函数去接收不同的身份,以调用对应的 BuyTicket 函数。
这里我们可以用指针和引用,这里我们用引用:
#include <iostream> using namespace std; class Person { public: virtual void BuyTicket() // virtual + 返回值 + 函数名+ 参数 相同 = 构成多态 { cout << "Person: 买票-全价" << endl; } }; class Student : public Person { public: // 这里也都相同 virtual void BuyTicket() { cout << "Student: 买票-半价" << endl; } }; class Soldier : public Person { public: // 这里也都相同 virtual void BuyTicket() { cout << "Soldier: 优先买票" << endl; } }; void Pay(Person& p) { p.BuyTicket(); } int main() { Person ps; Student st; Soldier sd; Pay(ps); Pay(st); Pay(sd); return 0; }
再看多态两个条件:
1、虚函数重写(覆盖)
2、父类指针或者引用去调用虚函数
特例1:子类虚函数不加virtual,依旧构成重写 (实际最好加上)
特例2:重写的协变。返回值可以不同,要求必须是父子关系的的指针或者引用
这里我们就构成了多态。(如果把Pay函数的引用去掉就不是多态了,调的三个都是全价)
1.3 协变构成多态
协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。
即基类虚函数返回基类对象的指针或者引用,
派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)
观察下面的代码,并没有达到 "三同" 的标准,它的返回值是不同的,但依旧构成多态:
#include <iostream> using namespace std; class A {}; class B : public A {}; class Person { public: virtual A* f() { cout << "virtual A* Person::f()" << endl; return nullptr; } }; class Student : public Person { public: virtual B* f() { cout << "virtual B* Student::f()" << endl; return nullptr; }; }; int main() { Person p; Student s; Person* ptr = &p; ptr->f(); ptr = &s; ptr->f(); return 0; }
但是协变也是有条件的,协变的类型必须是父子关系。
1.4 父虚子非虚构成多态
子类的虚函数没了却能构成多态:
#include <iostream> using namespace std; class A {}; class B : public A {}; class Person { public: virtual A* f() { cout << "virtual A* Person::f()" << endl; return nullptr; } }; class Student : public Person { public: B* f() { cout << "virtual B* Student:::f()" << endl; return nullptr; }; }; int main() { Person p; Student s; Person* ptr = &p; ptr->f(); ptr = &s; ptr->f(); return 0; }
这都不是虚函数了怎么也能构成多态呢?
解答:子类虚函数没有写 virtual,但 f 依旧是虚函数,是因为先继承了父类的函数接口声明。
子类继承父类的虚函数是一种接口继承,所以即使子类的 virtual 没写,它也是虚函数,
符合多态条件。这是重写父类虚函数的实现,也就是说父类有 virtual 的属性,子类也就有了。
最后,虽然子类虚函数可以不加 virtual,但是我们自己写的时候 子类虚函数建议加上 virtual。
总结:父类为虚函数,子类继承其父的情况下,即使不声明 virtual 也能构成多态。
1.5 析构函数的重写
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor。
#include <iostream> using namespace std; class Person { public: ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: ~Student() { cout << "~Student()" << endl; } }; int main() { Person p; Student s; return 0; }
第一行和第二行是 Student s 的,第三行是 Person p 的。我们来看看析构顺序,
Student s 是后定义的,析构顺序是后定义先析构。根据子类对象析构先子后父,
调用子类的析构函数结束后自动调用父类的析构函数,
所以第一行的 ~Student() 和第二行的 ~Person() 都是 Student 的,
随后第三行的 ~Person() 是 Person p 自己调的。
现在这两个析构函数默认是隐藏关系,
因为它们的函数名会被同一处理修改成 destructor 。
但是如果用 virtual 修饰 ~Person,我们知道,如果这加了不管 ~Student 加不加 virtual,
子类都会跟着父类变身成 virtual,那么现在这两个析构函数还是隐藏关系吗?
如果 Person 的析构函数加了 virtual,隐藏关系就变成了重写关系。
对普通对象(像上面的代码)来说,这里加 virtual 并不会带来什么改变,
再看另一段代码:
#include <iostream> using namespace std; class Person { public: virtual ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: ~Student() // 隐藏(重定义)关系 -> 重写(覆盖) 关系 { cout << "~Student()" << endl; } }; int main() { Person* ptr1 = new Person; // delete 调用 Person 的析构,对这个也没有影响 delete ptr1; Person* ptr2 = new Student; // 但是对这样的场景会产生影响 delete ptr2; return 0; }
把父类的virtual 去掉:
刚才我们看到了,如果这里不加 virtual,~Student 是没有调用析构的。
你可能会想这有啥,那是因为这里没场景,这其实是非常致命的,是不经意间会发生的内存泄露。
比如下面这个场景,我们是希望 delete 谁调用的就是谁的析构:
#include <iostream> using namespace std; class Person { public: ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: ~Student() // 隐藏(重定义)关系 -> 重写(覆盖) 关系 { cout << "~Student()" << endl; delete[] _name; cout << "delete: " << (void*)_name << endl; } private: char* _name = new char[10] { 'h', 'e', 'l', 'l', 'o' }; }; int main() { // 我们期望 delete ptr 调用析构函数是一个多态调用 Person* ptr = new Person; delete ptr; // ptr->destructor() + operator delete(ptr) ptr = new Student; delete ptr; // ptr->destructor() + operator delete(ptr) return 0; }
但是结果让我们很失望,Student 没析构。我们加上 virtual 再试试:
结论:如果设计的类可能会作为父类,析构函数最好设计成虚函数,即加上 virtual。
像刚才这种场景不加上 virtual 就会发生内存泄露,可怕的是还是悄无声息的!
报错不可怕,怕的是这种悄无声息的,像这种内存泄露找起来可是相当的恶心。
1.6 final 和 override 关键字(C++11)
final 关键字(C++11)
上一篇提到:C++11提供了关键字 final 写在类的后面,表明这个类不能被继承。
如果我有个虚函数,但我不想让它被人重写:
也可以关键字 final 写在函数的后面让虚函数不能被重写
#include <iostream> using namespace std; class Car { public: virtual void Drive() final {} }; class Benz : public Car { public: virtual void Drive() // 错误 C3248 "Car::Drive": 声明为 "final" 的函数不能由 "Benz::Drive" 重写 { cout << "Benz" << endl; } }; int main() { return 0; }
总结:final 的两个作用
写在虚函数后面让虚函数不能被重写
写在类后面让类不能被继承
override 关键字(C++11)
相信大家也体会到了 C++ 对函数重写的要求是非常严格的,
但是人难免会犯错,有些时候可能会导致函数名次序写反而无法构成重载,
而这种错误在编译期间是不会报的,因此往往只有在程序运行时你发现没有得到预期结果,
去 debug 找个半天才能将问题找出,这会让人感到非常的不爽:C++11 为了增加容错率,
推出了 final 和 override,find 是禁止重写,override 是必须重写。
override 关键字可以帮助你检查重写:
#include <iostream> using namespace std; class Car { public: virtual void Drive() {} }; // override 写在子类中:要求严格检查是否完成重写,如果没有重写就报错 class Benz : public Car { public: virtual void Drive() override { cout << "Benz" << endl; } }; int main() { return 0; }
把父类 virtual 去掉就报错:
#include <iostream> using namespace std; class Car { public: void Drive() {} }; // override 写在子类中:要求严格检查是否完成重写,如果没有重写就报错 class Benz : public Car { public: virtual void Drive() override//错误 C3668 “Benz::Drive”: 包含重写说明符“override”的方法没有重写任何基类方法 { cout << "Benz" << endl; } }; int main() { return 0; }
有了 override 修饰,像如果没有加 virtual 或参数不同就会报错。
当然,子类是可以省略 virtual 的,override 不会犯病报错放心使用,其在某些场景是非常有用的。
总结:override 写在子类中,会严格检查是否完成重写,如果没有就会报错提醒。
1.7 重载、覆盖、隐藏的对比
2. 抽象类(Abstract Class)
抽象在现实一般没有具体对应的实体,而不能实例化对象也就是没有实体,所以叫抽象类。
"抽象即不可名状,对应的是具象,具象即现实,抽象即虚拟。"
2.1 纯虚函数和抽象类
在虚函数的后面写上 =0,则我们称这个函数为 "纯虚函数"。
包含纯虚函数的类,就是 抽象类(abstract class),也叫接口类。
抽象类不能实例化出对象,子类即使在继承后也不能实例化出对象,只有重写纯虚函数,
子类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
#include <iostream> using namespace std; class Car { public: virtual void Drive() = 0; }; class BMW : public Car // 如果父类是抽象类,子类必须重写才能实例化 { public: virtual void Drive() // 重写 注释掉就会报错:错误 C2259 “BMW” : 无法实例化抽象类 { cout << "BMW" << endl; } }; int main() { BMW b; b.Drive(); return 0; }
如果 override 是直接要求你重写,那设计成抽象类就是间接要求你重写。
override 是放在子类虚函数,检查重写,它们的功能其实是有一些重叠和相似的。
纯虚函数规范了子类必须重写,另外虚函数更体现出了接口继承。
总结:抽象类不能实例化出对象,子类即使在继承后也不能实例化出对象,除非子类重写。
2.2 抽象类指针
虽然父类是抽象类不能定义对象,但是可以定义指针。
定义指针时如果 new 父类对象因为是纯虚函数,自然是 new 不出来的,
但是可以 new 子类对象:
#include <iostream> using namespace std; class Car { public: virtual void Drive() = 0; }; class BMW : public Car { public: virtual void Drive() { cout << "BMW" << endl; } }; int main() { Car* BMW1 = new BMW; BMW1->Drive(); BMW* BMW2 = new BMW; BMW2->Drive(); return 0; }
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题(中);https://developer.aliyun.com/article/1521916