C++ 多态(一) : 多态的构成条件、final、override、协变、析构函数的重写、抽象类

x33g5p2x  于2022-06-06 转载在 其他  
字(4.0k)|赞(0)|评价(0)|浏览(464)

⏰1.多态的定义和实现

🌕多态的浅层理解

多态,即多种形态,也就是说,不同的对象在完成某个行为时会产生不同的状态。
举个例子,买火车票时,普通人正常买票,学生半价买票,军人优先买票。

在C++中,多态就是对于同一个函数,当调用的对象不同,他的操作也不同。

🌕多态的构成条件

多态是继承体系中的一个行为,如果要在继承体系中构成多态,需要满足两个条件:

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

2. 被调用的函数必须是虚函数,并且派生类必须要对继承的基类的虚函数进行重写
解释1:因为子类和父类的虚表各自一份,倘若能够通过对象传递的方式同时传递虚表的话,那么父类就可能拿到子类的虚表,不合理。
解释2:有虚函数就有虚函数表,对象当中就会存放一个虚基表指针,通过虚基表指针指向的内容来访问对应的函数。若子类没有重写父类的虚函数内容,则子类也会调用父类的函数。

⏰2.虚函数

虚函数就是被virtual修饰的类成员函数(这里的virtual和虚继承的virtual虽然是同一个关键字,但是作用不一样)

  1. class Person
  2. {
  3. public:
  4. virtual void func()
  5. {
  6. cout << "普通人->正常买票" << endl;
  7. }
  8. };

📕虚函数的重写规则

虚函数,即被virtual关键字重写的类成员函数
重写(覆盖):派生类中有一个跟基类中完全相同的虚函数(三同:即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同,也有例外!),这样则称子类重写了父类的虚函数。

示例代码如下:

  1. class Person
  2. {
  3. public:
  4. virtual void func()
  5. {
  6. cout << "普通人->正常买票" << endl;
  7. }
  8. };
  9. class Student : public Person
  10. {
  11. public:
  12. //子类必须重写父类的虚函数
  13. virtual void func()
  14. {
  15. cout << "学生->半价买票" << endl;
  16. }
  17. };
  18. //必须是父类的指针或引用去调用虚函数
  19. //这里的参数类型不能是对象,否则是一份临时拷贝,则无法构成多态
  20. void F(Person& ps)
  21. {
  22. ps.func();
  23. }
  24. int main()
  25. {
  26. Person ps;
  27. Student st;
  28. F(ps);
  29. F(st);
  30. return 0;
  31. }

笔试选择题常考点(选自Effective C++):

如果不满足虚函数重写的条件,例如参数不同则会变成重定义。

思考如下代码输出结果:

  1. #include <iostream>
  2. class Base{
  3. public:
  4. virtual void Show(int n = 10)const{ //提供缺省参数值
  5. std::cout << "Base:" << n << std::endl;
  6. }
  7. };
  8. class Base1 : public Base{
  9. public:
  10. virtual void Show(int n = 20)const{ //重新定义继承而来的缺省参数值
  11. std::cout << "Base1:" << n << std::endl;
  12. }
  13. };
  14. int main(){
  15. Base* p1 = new Base1;
  16. p1->Show();
  17. return 0;
  18. }

输出的是Base1:10,

因为如果子类重写了缺省值,此时的子类的缺省值是无效的,使用的还是父类的缺省值。原因是因为多态是动态绑定,而缺省值是静态绑定。对于P1,他的静态类型也就是这个指针的类型是Base,所以这里的缺省值是Base的缺省值,而动态类型也就是指向的对象是Base1,所以这里调用的虚函数则是Base1中的虚函数,所以这里就是Base1中的虚函数,Base中的缺省值,也就是Base1:10。

简单概括就是:虚函数的重写只重写函数实现,不重写缺省值。

📕虚函数重写条件的两个例外

🍁1.协变(返回值不同)

当基类和派生类的返回值类型不同时,如果基类对象返回基类对象的引用或者指针,派生类对象也返回的是派生类对象的引用或者指针时,就会引起协变。协变也能完成虚函数的重写

例1:指针

  1. class A {};
  2. class B :public A {};
  3. class Person
  4. {
  5. public:
  6. virtual A* func()
  7. {
  8. cout << "virtual A* func()" << endl;
  9. return new A;
  10. }
  11. };
  12. class Student : public Person
  13. {
  14. public:
  15. virtual B* func()
  16. {
  17. cout << "virtual B* func()" << endl;
  18. return new B;
  19. }
  20. };

例2:引用

  1. class Human
  2. {
  3. public:
  4. virtual Human& print()
  5. {
  6. cout << "i am a human" << endl;
  7. return *this;
  8. }
  9. };
  10. class Student : public Human
  11. {
  12. public:
  13. virtual Student& print()
  14. {
  15. cout << "i am a student" << endl;
  16. return *this;
  17. }
  18. };
🍁2.析构函数的重写(函数名不同)

析构函数虽然函数名不同,但是也能构成重写,因为站在编译器的视角,他所调用的析构函数名称都叫做destructor

为什么编译器要通过这种方式让析构函数也能构成重写呢?
假设我用一个基类指针或者引用指向派生类对象,如果不构成多态会怎样?

  1. class Human
  2. {
  3. public:
  4. ~Human()
  5. {
  6. cout << "~Human()" << endl;
  7. }
  8. };
  9. class Student : public Human
  10. {
  11. public:
  12. ~Student()
  13. {
  14. cout << "~Student()" << endl;
  15. }
  16. };
  17. int main()
  18. {
  19. Human* h = new Student;
  20. delete h;
  21. return 0;
  22. }

上述代码只会调用Human的析构函数,即如果不构成多态,那么指针是什么类型的,就会调用什么类型的析构函数,这也就导致了一种情况,如果派生类的析构函数中有资源释放,而这里却没有释放掉那些资源,就会导致内存泄漏的问题。

所以为了防止这种情况,必须要将析构函数定义为虚函数。这也就是编译器将析构函数重命名为destructor的原因

  1. class Human
  2. {
  3. public:
  4. virtual ~Human()
  5. {
  6. cout << "~Human()" << endl;
  7. }
  8. };
  9. class Student : public Human
  10. {
  11. public:
  12. virtual ~Student()
  13. {
  14. cout << "~Student()" << endl;
  15. }
  16. };
  17. int main()
  18. {
  19. Human* h = new Student;
  20. delete h;
  21. return 0;
  22. }

⏰3.C++11 override 和 final

🍁override

override关键字是用来检测派生类虚函数是否构成重写的关键字。
如基类虚函数没有virtual或者派生类虚函数名拼错等问题,这些问题不会被编译器检查出来,发生错误时也很难一下子锁定,所以C++增添了override这一层保险,当修饰的虚函数不构成重写时就会编译错误。

  1. class A
  2. {
  3. public:
  4. virtual void func() {}
  5. };
  6. class B : public A
  7. {
  8. public:
  9. //未重写则报错
  10. virtual void func() override {};
  11. };

🍁final

使用final修饰的虚函数不能被重写。
如果某一个虚函数不想被派生类重写,就可以用final来修饰这个虚函数

  1. class Human
  2. {
  3. public:
  4. virtual void print() final
  5. {
  6. cout << "i am a human" << endl;
  7. }
  8. };
  9. class Student : public Human
  10. {
  11. public:
  12. virtual void print()
  13. {
  14. cout << "i am a student" << endl;
  15. }
  16. };

🍁重载、重定义(隐藏)、重写(覆盖)的对比:

⏰4.抽象类

虚函数后面加上=0就是纯虚函数,包含纯虚函数的类即为抽象类(接口类)抽象类不能实例化出对象,派生类继承抽象类后若没有重写纯虚函数那么仍为抽象类,亦不能实例化出对象。纯虚函数规范了派生类必须重写虚函数,并且更加体现出了接口继承。

抽象类就像是一个蓝图,为派生类描述好一个大概的架构,派生类必须实现完这些架构,至于要在这些架构上面做些什么,增加什么,就属于派生类自己的问题。

  1. class Human
  2. {
  3. public:
  4. virtual void print() = 0;
  5. };
  6. class Student : public Human
  7. {
  8. public:
  9. virtual void print()
  10. {
  11. cout << "i am a student" << endl;
  12. }
  13. };
  14. class Teacher : public Human
  15. {
  16. public:
  17. virtual void print()
  18. {
  19. cout << "i am a teacher" << endl;
  20. }
  21. };

🍁接口继承与实现继承

普通函数的继承就是接口继承,派生类可以使用基类的函数;而虚函数的重写则是实现继承,派生类继承的仅仅是基类的函数接口,目的是为了重写基类虚函数的函数体,达成多态。因此如果不实现多态,则不要将函数定义为虚函数。

相关文章