一、多态定义
多态是函数调用的多种形态,使我们调用函数更加灵活。多态分为两种:静态多态和动态多态
1.静态多态
静态多态即函数重载,这里的静态是指编译时:
1. #include<iostream> 2. using namespace std; 3. 4. void Swap(int& s1, int& s2) 5. { 6. int temp = s1; 7. s1 = s2; 8. s2 = temp; 9. } 10. 11. void Swap(float& s1, float& s2) 12. { 13. float temp = s1; 14. s1 = s2; 15. s2 = temp; 16. } 17. 18. void Swap(char& s1, char& s2) 19. { 20. char temp = s1; 21. s1 = s2; 22. s2 = temp; 23. } 24. 25. int main() 26. { 27. int a = 1, b = 2; 28. float c = 3.0, d = 4.0; 29. char e = 'z', f = 'Z'; 30. 31. Swap(a, b); 32. Swap(c, d); 33. Swap(e, f); 34. 35. return 0; 36. }
看起来我们用的是一个函数,实际上这就是静态多态
2.动态多态
动态多态是指不同类型对象完成一件事的时候产生的动作不一样,那么结果也不一样。
在继承中要构成多态有两个条件,缺一不可:
(1)必须通过父类的指针或者引用调用虚函数
(2)被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写
动态多态父类指针或引用的指向:
(1)父类指针或引用指向父类,调用的就是父类虚函数
(2)父类指针或引用指向哪个子类,调用的就是哪个子类重写的虚函数
根据切片规则,父类的指针既可以指向父类,又可以指向子类,如果有多个子类,就可以指向不同类型。
(1)虚函数
被virtual修饰的成员函数叫做虚函数。
注意:
(1)只有类的非静态成员函数才可以被 virtual修饰,普通函数不可以。
(2)虽然虚函数的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有关系。虚函数的vitual是为了实现多态,虚继承中的virtual是为了解决菱形继承的数据冗余和二义性。
(2)虚函数的重写
虚函数重写也叫做覆盖,在子类中重写了一个和父类中的虚函数完全相同的虚函数:包括函数名、返回值、参数列表都相同,这时候子类就重写了父类的虚函数。
注意:
子类重写的虚函数的函数名、返回值、参数列表和父类一定要完全相同,否则就变成了函数重载,和继承无关。
如下,Bird类和Dog类就重写了父类的虚函数:
1. #include<iostream> 2. using namespace std; 3. 4. class Animal 5. { 6. public: 7. virtual void Speak()//父类虚函数 8. { 9. cout << "speak" << endl; 10. } 11. }; 12. 13. class Bird:public Animal 14. { 15. public: 16. virtual void Speak()//子类重写父类虚函数 17. { 18. cout << "chirp" << endl; 19. } 20. }; 21. 22. class Dog :public Animal 23. { 24. public: 25. virtual void Speak()//子类重写父类虚函数 26. { 27. cout << "bark" << endl; 28. } 29. }; 30. 31. //父类对象:会破坏多态条件,不构成多态 32. void fun(Animal a) 33. { 34. //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态 35. a.Speak(); 36. } 37. 38. //父类引用-构成多态 39. void fun1(Animal& a) 40. { 41. //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态 42. a.Speak(); 43. } 44. 45. //父类指针-构成多态 46. void fun2(Animal* pa) 47. { 48. //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态 49. pa->Speak(); 50. } 51. 52. int main() 53. { 54. Animal a; 55. Bird b; 56. Dog d; 57. 58. Animal *pa = &a; 59. Bird *pb = &b; 60. Dog *pd = &d; 61. 62. fun(a); 63. fun(b); 64. fun(d); 65. cout << endl; 66. 67. fun1(a); 68. fun1(b); 69. fun1(d); 70. cout << endl; 71. 72. fun2(pa); 73. fun2(pb); 74. fun2(pd); 75. 76. return 0; 77. } 78.
当父类对象调用虚函数时,不构成多态,当父类引用或指针调用虚函数时,构成多态:
如果去掉父类的虚函数关键字virtual,子类就没有重写父类的虚函数,那么就算传父类指针或父类引用也不会构成多态:
1. #include<iostream> 2. using namespace std; 3. 4. class Animal 5. { 6. public: 7. void Speak()//父类普通函数 8. { 9. cout << "speak" << endl; 10. } 11. }; 12. 13. class Bird:public Animal 14. { 15. public: 16. void Speak()//子类普通函数 17. { 18. cout << "chirp" << endl; 19. } 20. }; 21. 22. class Dog :public Animal 23. { 24. public: 25. void Speak()//子类普通函数 26. { 27. cout << "bark" << endl; 28. } 29. }; 30. 31. //父类对象:会破坏多态条件,不构成多态 32. void fun(Animal a) 33. { 34. //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态 35. a.Speak(); 36. } 37. 38. //父类引用-构成多态 39. void fun1(Animal& a) 40. { 41. //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态 42. a.Speak(); 43. } 44. 45. //父类指针-构成多态 46. void fun2(Animal* pa) 47. { 48. //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态 49. pa->Speak(); 50. } 51. 52. int main() 53. { 54. Animal a; 55. Bird b; 56. Dog d; 57. 58. Animal *pa = &a; 59. Bird *pb = &b; 60. Dog *pd = &d; 61. 62. fun(a); 63. fun(b); 64. fun(d); 65. cout << endl; 66. 67. fun1(a); 68. fun1(b); 69. fun1(d); 70. cout << endl; 71. 72. fun2(pa); 73. fun2(pb); 74. fun2(pd); 75. 76. return 0; 77. }
不构成多态:
(3)虚函数重写的两个例外
① 协变(返回值类型是父子关系)
子类重写父类虚函数时,与父类虚函数返回值类型不同。即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时,称为协变。
1. #include<iostream> 2. using namespace std; 3. 4. class Animal 5. { 6. public: 7. virtual Animal* Speak()//父类虚函数 8. { 9. cout << "Animal* Animal::Speak()" << endl; 10. return new Animal;//返回父类指针 11. } 12. }; 13. 14. class Bird :public Animal 15. { 16. public: 17. virtual Bird* Speak()//子类重写父类虚函数 18. { 19. cout << "Bird* Bird::Speak()" << endl; 20. return new Bird;//返回子类指针 21. } 22. }; 23. 24. int main() 25. { 26. Animal a; 27. Bird b; 28. 29. Animal* pa = &a; 30. pa->Speak(); 31. 32. Bird* pb = &b; 33. pb->Speak(); 34. 35. return 0; 36. } 37.
父类返回父类指针,子类返回子类指针:
所以虚函数重写后的返回值不一定相同 ,因为有协变的存在。
②析构函数的重写(子类与父类析构函数的名字不同)
如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字,都与父类的析构函数构成重写,虽然父类与子类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。可参考【C++】-- 继承第四节的第4小节。
把父类析构函数定义为虚函数,子类就可以重写父类的虚函数:
1. #include<iostream> 2. using namespace std; 3. 4. class Animal 5. { 6. public: 7. virtual ~Animal() 8. { 9. cout << "~Animal()" << endl; 10. } 11. }; 12. 13. class Bird :public Animal 14. { 15. public: 16. virtual ~Bird()//Bird类和Animal类的析构函数名看起来不同,但是他们构成虚构函数重写 17. { 18. cout << "~Bird()" << endl; 19. } 20. }; 21. 22. int main() 23. { 24. Animal a; 25. Bird b; 26. 27. return 0; 28. }
由于析构时,子类对象先调用自己的析构函数进行清理,清理完后再自动调用父类的析构函数,所以打印的前两行是Bird类对象调用的析构函数,第3行调用的是Animal类对象的析构函数。
但是发现把
virtual ~Animal()
和
virtual ~Bird()
中的virtual都去掉,运行结果还是一样。说明在普通场景下,父类和子类的析构函数是否是虚函数,是否构成重写,没什么影响。
那在什么场景下才有影响呢?
1. #include<iostream> 2. using namespace std; 3. 4. class Animal 5. { 6. public: 7. ~Animal() 8. { 9. cout << "~Animal()" << endl; 10. } 11. }; 12. 13. class Bird :public Animal 14. { 15. public: 16. ~Bird()//Bird类和Animal类的析构函数名看起来不同,但是他们构成虚构函数重写 17. { 18. cout << "~Bird()" << endl; 19. } 20. }; 21. 22. int main() 23. { 24. Animal* pa = new Animal; 25. Animal* pb = new Bird; 26. 27. //多态行为 28. delete pa;//pa->析构函数() + operator delete(pa) 29. delete pb;//pb->析构函数() + operator delete(pb) 30. 31. return 0; 32. }
delete在释放空间的同时要调用析构函数,需要做两步操作:
(1)先调用析构函数
(2)再释放空间
pa和pb的空间都会被释放,pa指向父类对象,期望调用父类的析构函数,pb指向子类对象,期望调用子类的析构函数,指向父类调父类,指向子类调子类,期望这里达到多态行为,虽然没有明显的函数调用,但是delete操作调了析构函数。
pb指向子类对象,但是发现没有调用子类析构函数,可能存在内存泄漏:
当子类析构函数不需要清理资源时也就没什么问题,但是当子类析构函数需要清理时,这样做会存在内存泄漏 。因此多态场景下子类和父类的析构函数最好加上virtual关键字完成虚函数重写就不会导致内存泄漏了。所以上面的代码最好不要删掉析构函数前面的virtual。
另外在继承一文中,说过子类和父类的析构函数构成隐藏。原因就是表面上子类的析构函数个父类的析构函数名不同,但是为了构成重写,编译器会对析构函数名调用时,统一将父类和子类的析构函数名改成destructor( )。统一改成destructor( )构成隐藏的目的就是在这里能够调用同一个函数,达到多态指向父类对象就调父类对象,指向子类对象就调子类对象的的目的。
因此,父类函数中的virtual不能省,否则子类继承不了父类的virtual属性,无法重写父类的虚函数,如果这个函数是析构函数,那么还会造成内存泄漏。为了保持统一,父类和子类虚函数前面的virtual都不要省。
(4)C++11的final和override
①final:如果一个虚函数不想被重写,可以在虚函数后面加final
1. #include<iostream> 2. using namespace std; 3. 4. class Animal 5. { 6. public: 7. virtual ~Animal() final//虚函数不想被重写 8. { 9. cout << "~Animal()" << endl; 10. } 11. }; 12. 13. class Bird :public Animal 14. { 15. public: 16. virtual ~Bird() 17. { 18. cout << "~Bird()" << endl; 19. } 20. };
一旦重写final修饰的虚函数,就会报错:
如果一个类不想被继承,可以在这个类后面加final
1. class Animal final//Animal类不想被继承 2. {}; 3. 4. class Bird :public Animal 5. {};
编译报错:final类无法被继承:
②override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
1. #include<iostream> 2. using namespace std; 3. 4. class Animal 5. { 6. public: 7. virtual void Speak() 8. { 9. cout << "speak" << endl; 10. } 11. }; 12. 13. class Bird :public Animal 14. { 15. public: 16. virtual void Speak(int i) override//用来检查子类是否完成父类虚函数的重写 17. { 18. cout << "chirp" << endl; 19. } 20. };
由于子类重写的虚函数的参数列表和父类虚函数的参数列表不同,导致子类没有成功完成重写父类的虚函数,override检查会报错:
(5)重载、重写与隐藏
重载、重写和隐藏的对比:
二、抽象类
1.纯虚函数
(1)纯虚函数
定义:在虚函数的后面写上 =0
(2)抽象类(接口类):
定义:包含纯虚函数的类
性质:抽象类不能实例化出对象。子类继承抽象类后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。
意义:
① 能够更好地去表示现实世界中没有实例对象是我抽象类型,如:植物、人、动物
② 体现接口继承,强制子类去重写虚函数(就算不重写,子类也是抽象类)
1. #include<iostream> 2. using namespace std; 3. 4. class Animal//抽象类 5. { 6. public: 7. virtual void Speak() = 0;//纯虚函数 8. }; 9. 10. class Bird :public Animal 11. { 12. public://没有重写纯虚函数 13. }; 14. 15. class Dog :public Animal 16. { 17. public: 18. virtual void Speak()//子类重写父类虚函数 19. { 20. cout << "bark" << endl; 21. } 22. };
抽象类不能实例化出对象:
1. int main() 2. { 3. Animal a; 4. return 0; 5. }
报错:
当子类没有重写父类的纯虚函数时,直接把父类的虚函数继承下来了,这个虚函数也是纯虚函数,那么这个子类就是抽象类,不能实例化出对象:
1. int main() 2. { 3. Bird b; 4. 5. return 0; 6. }
报错:
当子类重写了父类虚函数:
1. #include<iostream> 2. using namespace std; 3. 4. class Animal//抽象类 5. { 6. public: 7. virtual void Speak() = 0;//纯虚函数 8. }; 9. 10. class Bird :public Animal 11. { 12. public: 13. virtual void Speak()//子类重写父类纯虚函数 14. { 15. cout << "chirp" << endl; 16. } 17. }; 18. 19. class Dog :public Animal 20. { 21. public: 22. virtual void Speak()//子类重写父类纯虚函数 23. { 24. cout << "bark" << endl; 25. } 26. }; 27. 28. int main() 29. { 30. Animal* pBird = new Bird; 31. pBird->Speak(); 32. 33. Animal* pDog = new Dog; 34. pDog->Speak(); 35. 36. return 0; 37. }
pBird 和pDog是指向父类的指针,调用了子类虚函数,看起来像是调用了同一个虚函数Speak( )。
2.接口继承和实现继承
(1)实现继承
普通函数的继承是实现继承,不是接口继承,继承的是函数的实现,可以直接使用这个函数,也是一种复用。
(2)接口继承
虚函数包括纯虚函数的继承是接口继承,子类仅仅只继承了父类接口 ,父类没有实现这个接口函数,子类要对纯虚函数进行重写达到多态的目的。
注意:
如果为了达到多态目的,那可以把父类接口定义成虚函数,并且定义了后,子类必须要重写父类的虚函数,否则就不要把普通函数定义成虚函数。