【C++】-- 多态(一)

简介: 【C++】-- 多态

一、多态定义

多态是函数调用的多种形态,使我们调用函数更加灵活。多态分为两种:静态多态和动态多态

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)接口继承

虚函数包括纯虚函数的继承是接口继承,子类仅仅只继承了父类接口 ,父类没有实现这个接口函数,子类要对纯虚函数进行重写达到多态的目的。

注意:

如果为了达到多态目的,那可以把父类接口定义成虚函数,并且定义了后,子类必须要重写父类的虚函数,否则就不要把普通函数定义成虚函数。


相关文章
|
1月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
38 2
C++入门12——详解多态1
|
6月前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
149 1
|
1月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
79 1
|
3月前
|
存储 编译器 C++
|
4月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
53 1
【C++】深度解剖多态(下)
|
4月前
|
存储 编译器 C++
|
3月前
|
存储 编译器 C++
C++多态实现的原理:深入探索与实战应用
【8月更文挑战第21天】在C++的浩瀚宇宙中,多态性(Polymorphism)无疑是一颗璀璨的星辰,它赋予了程序高度的灵活性和可扩展性。多态允许我们通过基类指针或引用来调用派生类的成员函数,而具体调用哪个函数则取决于指针或引用所指向的对象的实际类型。本文将深入探讨C++多态实现的原理,并结合工作学习中的实际案例,分享其技术干货。
74 0
|
4月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
4月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
54 2
|
4月前
|
程序员 C++
【C++】揭开C++多态的神秘面纱
【C++】揭开C++多态的神秘面纱