欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

c++中的多态机制

程序员文章站 2022-04-09 14:54:14
静态联编与动态联编、赋值兼容性原则、多态原理详解、虚析构函数、纯虚函数、抽象类、接口 ......

目录

背景介绍

  虚函数重写:子类重新定义父类中有相同返回值、名称参数虚函数

  非虚函重写:子类重新定义父类中有相同名称参数非虚函数

  父子间的赋值兼容:子类对象可以当作父类对象使用(兼容性);具体表现为:

   1. 子类对象可以直接赋值给父类对象;

   2. 子类对象可以直接初始化父类对象;

   3. 父类指针可以直接指向子类对象;

   4. 父类引用可以直接引用子类对象;

  当发生赋值兼容时,子类对象退化为父类对象,只能访问父类中定义的成员,可以直接访问被子类覆盖的同名成员;

 1 // 在赋值兼容原则中,子类对象退化为父类对象,子类是特殊的父类;
 2 #include <iostream>
 3 #include <string>
 4 
 5 using namespace std;
 6 
 7 class parent
 8 {
 9 public:
10     int mi;
11 
12     void add(int i)
13     {
14         mi += i;
15     }
16     
17     void add(int a, int b)
18     {
19         mi += (a + b);
20     }
21 };
22 
23 class child : public parent
24 {
25 public:
26     int mi;
27     
28     void add(int x, int y, int z)
29     {
30         mi += (x + y + z);
31     }
32 };
33 
34 int main()
35 {
36     parent p;
37     child c;
38     
39     c.mi = 100;
40     p = c;             // p.mi = 0; 子类对象退化为父类对象
41     parent p1(c);   // p1.mi = 0; 同上
42     parent& rp = c;
43     parent* pp = &c;
44     
45     rp.add(5);             
46     pp->add(10, 20);        
47     
48     cout << "p.mi: " << p.mi <<endl;                           // p.mi: 0; 
49     cout << "p1.mi: " << p1.mi <<endl;                       // p1.mi: 0;  
50     cout << "c.parent::mi: " << c.parent::mi <<endl;    // c.parent::mi: 35
51     cout << "rp.mi: " << rp.mi <<endl;                        // rp.mi: 35
52     cout << "pp->mi: " << pp->mi <<endl;                 // pp->mi: 35
53     
54     return 0;
55 }

  在面向对象的继承关系中,我们了解到子类可以拥有父类中的所有属性与行为;但是,有时父类中提供的方法并不能满足现有的需求,所以,我们必须在子类中重写父类中已有的方法,来满足当前的需求。此时尽管我们已经实现了函数重写(这里是非虚函数重写),但是在类型兼容性原则中也不能出现我们期待的结果(不能根据指针/引用所指向的实际对象类型去调到对应的重写函数)。接下来我们用代码来复现这个情景:

 1 #include <iostream>
 2 #include <string>
 3 
 4 using namespace std;
 5 
 6 class parent
 7 {
 8 public:    
 9     void print()
10     {
11         cout << "i'm parent." << endl;
12     }
13 };
14 
15 class child : public parent
16 {
17 public:    
18     void print()
19     {
20         cout << "i'm child." << endl;
21     }
22 };
23 
24 void how_to_print(parent* p)
25 {
26     p->print();
27 }
28 
29 int main()
30 {
31     parent p;
32     child c;
33     
34     how_to_print(&p);   // i'm parent    // expected to print: i'm parent.
35     how_to_print(&c);   // i'm parent    // expected to print: i'm child.
36     
37     return 0;
38 }

  为什么会出现上述现象呢?(在赋值兼容中,父类指针/引用指向子类对象时为何不能调用子类重写函数?)

  问题分析:在编译期间,编译器只能根据指针的类型判断所指向的对象;根据赋值兼容,编译器认为父类指针指向的是父类对象;因此,编译结果只可能是调用父类中定义的同名函数。

       c++中的多态机制

  在编译这个函数的时候,编译器不可能知道指针p究竟指向了什么。但是编译器没有理由报错,于是,编译器认为最安全的做法是调用父类的print函数。因为父类和子类肯定都有相同的print函数。

  要想解决这个问题,就需要使用c++中的多态。那么如何实现c++中的多态呢?请看下面详解:

多态介绍

1、 什么是多态

  在现实生活中,多态是同一个事物在不同场景下的多种形态。

  在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。与之相对应的编译时绑定函数称为静态绑定。

  多态是设计模式的基础,多态是框架的基础。

2、 多态的分类

  c++中的多态机制

  静态多态编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数就调用,没有的话就会发出警告或者报错;

  动态多态在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。

3、动态多态成立的条件

    由之前出现的问题可知,编译器的做法并不符合我们的期望(因为编译器是根据父类指针的类型去父类中调用被重写的函数);但是,在面向对象的多态中,我们期望的行为是 根据实际的对象类型来判断如何调用重写函数(虚函数)

  1. 即当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;

  2. 即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数;

       c++中的多态机制

  这种多态行为的表现效果为:同样的调用语句在实际运行时有多种不同的表现形态。

  那么在c++中,如何实现这种表现效果呢?(实现多态的条件)

  1.  要有继承

  2.  要有虚函数重写(被 virtual 声明的函数叫虚函数)

  3.  要有父类指针(父类引用)指向子类对象

4、静态联编和动态联编

  静态联编:在程序的编译期间就能确定具体的函数调用;如函数重载,非虚函数重写;

  动态联编:在程序实际运行后才能确定具体的函数调用;如虚函数重写,switch 语句和 if 语句;

 1 #include <iostream>
 2 #include <string>
 3 
 4 using namespace std;
 5 
 6 class parent
 7 {
 8 public:
 9     virtual void func()
10     {
11         cout << "parent::void func()" << endl;
12     }
13     
14     virtual void func(int i)
15     {
16         cout << "parent::void func(int i) : " << i << endl;
17     }
18     
19     virtual void func(int i, int j)
20     {
21         cout << "parent::void func(int i, int j) : " << "(" << i << ", " << j << ")" << endl;
22     }
23 };
24 
25 class child : public parent
26 {
27 public:
28     void func(int i, int j)
29     {
30         cout << "child::void func(int i, int j) : " << i + j << endl;
31     }
32     
33     void func(int i, int j, int k)
34     {
35         cout << "child::void func(int i, int j, int k) : " << i + j + k << endl;
36     }
37 };
38 
39 void run(parent* p)
40 {
41     p->func(1, 2);     // 展现多态的特性
42                        // 动态联编
43 }
44 
45 
46 int main()
47 {
48     parent p;
49     
50     p.func();         // 静态联编
51     p.func(1);        // 静态联编
52     p.func(1, 2);     // 静态联编
53     
54     cout << endl;
55     
56     child c;
57     
58     c.func(1, 2);     // 静态联编
59     
60     cout << endl;
61     
62     run(&p);
63     run(&c);
64     
65     return 0;
66 }
67 /*
68     parent::void func()
69     parent::void func(int i) : 1
70     parent::void func(int i, int j) : (1, 2)
71 
72     child::void func(int i, int j) : 3
73 
74     parent::void func(int i, int j) : (1, 2)
75     child::void func(int i, int j) : 3
76 */

5、动态多态的实现原理

  虚函数表与vptr指针

  1. 当类中声明虚函数时,编译器会在类中生成一个虚函数表;

  2. 虚函数表是一个存储类成员函数指针的数据结构;

  3. 虚函数表是由编译器自动生成与维护的;

  4. virtual成员函数会被编译器放入虚函数表中;

  5. 存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)。

  多态执行过程:

  1.  在类中,用 virtual 声明一个函数时,就会在这个类中对应产生一张 虚函数表,将虚函数存放到该表中;

       2.  用这个类创建对象时,就会产生一个 vptr指针,这个vptr指针会指向对应的虚函数表;

       3.  在多态调用时, vptr指针 就会根据这个对象 在对应类的虚函数表中 查找被调用的函数,从而找到函数的入口地址;

            》 如果这个对象是 子类的对象,那么vptr指针就会在 子类的 虚函数表中查找被调用的函数

            》 如果这个对象是 父类的对象,那么vptr指针就会在 父类的 虚函数表中查找被调用的函数

  c++中的多态机制

  c++中的多态机制

 c++中的多态机制

 c++中的多态机制

 

  注:出于效率考虑,没有必要将所有成员函数都声明为虚函数。

  如何证明vptr指针的存在?

 1 #include <iostream>
 2 #include <string>
 3 
 4 using namespace std;
 5 
 6 class demo1
 7 {
 8 private:
 9     int mi; // 4 bytes
10     int mj; // 4 bytes
11 public:
12     virtual void print(){}  // 由于虚函数的存在,在实例化类对象时,就会产生1个 vptr指针
13 };
14 
15 class demo2
16 {
17 private:
18     int mi; // 4 bytes
19     int mj; // 4 bytes
20 public:
21     void print(){}
22 };
23 
24 int main()
25 {
26     cout << "sizeof(demo1) = " << sizeof(demo1) << " bytes" << endl; // sizeof(demo1) = 16 bytes
27     cout << "sizeof(demo2) = " << sizeof(demo2) << " bytes" << endl; // sizeof(demo2) = 8 bytes
28     
29     return 0;
30 }
31 
32 // 64bit(os) 指针占 8 bytes
33 // 32bit(os) 指针占 4 bytes

  显然,在普通的类中,类的大小 == 成员变量的大小;在有虚函数的类中,类的大小 == 成员变量的大小 + vptr指针大小。

6、 虚析构函数

  定义:用 virtual 关键字修饰析构函数,称为虚析构函数;

  格式:virtual ~classname(){ ... }

  意义:虚析构函数用于指引 delete 运算符正确析构动态对象;(当父类指针指向子类对象时,通过父类指针去释放所有子类的内存空间)

  应用场景:在赋值兼容性原则中(父类指针指向子类对象),通过  delete 父类指针   去释放所有子类的内存空间。(动态多态调用:通过父类指针所指向的实际对象去判断如何调用 delete 运算符)

  !!建议在设计基类时将析构函数声明为虚函数,为的是避免内存泄漏,否则有可能会造成派生类内存泄漏问题。
  案列分析
 1 #include <iostream>
 2 #include <cstring>
 3 
 4 using namespace std;
 5 
 6 class base
 7 {
 8 protected:
 9     char *name;
10 public:
11     base()
12     {
13         name = new char[20];
14         strcpy(name, "base()");
15         cout <<this << "  " << name << endl;
16     }
17     
18     ~base()
19     {
20         cout << this << "  ~base()" << endl;
21         delete[] name;
22     }
23 };
24 
25 
26 class derived : public base
27 {
28 private:
29     int *value;
30 public:
31     derived()
32     {
33         strcpy(name, "derived()");
34         value = new int(strlen(name));
35         cout << this << "  " << name << "  " << *value <<  endl;
36     }
37     
38     ~derived()
39     {
40         cout << this << "  ~derived()" << endl;
41         delete value;
42     }
43 };
44 
45 
46 int main()
47 {    
48     cout << "在赋值兼容中,关于 子类对象存在内存泄漏的测试" << endl;
49     
50     base* bp = new derived();
51     cout << bp << endl;
52     // ...
53     delete bp;  // 虽然是父类指针,但析构的是子类资源
54 
55     return 0;
56 }
57 
58 /**
59  * 在赋值兼容中,关于 子类对象存在内存泄漏的测试
60  * 0x7a1030  base()
61  * 0x7a1030  derived()  9
62  * 0x7a1030
63  * 0x7a1030  ~base()
64  */
 1 #include <iostream>
 2 #include <cstring>
 3 
 4 using namespace std;
 5 
 6 class base
 7 {
 8 protected:
 9     char *name;
10 public:
11     base()
12     {
13         name = new char[20];
14         strcpy(name, "base()");
15         cout <<this << "  " << name << endl;
16     }
17     
18     virtual ~base()
19     {
20         cout << this << "  ~base()" << endl;
21         delete[] name;
22     }
23 };
24 
25 
26 class derived : public base
27 {
28 private:
29     int *value;
30 public:
31     derived()
32     {
33         strcpy(name, "derived()");
34         value = new int(strlen(name));
35         cout << this << "  " << name << "  " << *value <<  endl;
36     }
37     
38     virtual ~derived()
39     {
40         cout << this << "  ~derived()" << endl;
41         delete value;
42     }
43 };
44 
45 
46 int main()
47 {
48     //derived *dp = new derived();
49     //delete dp; // 直接通过子类对象释放资源不需要 virtual 关键字
50     
51     cout << "在赋值兼容中,虚析构函数的测试" << endl;
52     
53     base* bp = new derived();
54     cout << bp << endl;
55     // ...
56     delete bp;  // 动态多态发生
57 
58     return 0;
59 }
60 
61 /**
62  * 在赋值兼容中,虚析构函数的测试
63  * 0x19b1030  base()
64  * 0x19b1030  derived()  9
65  * 0x19b1030
66  * 0x19b1030  ~derived()
67  * 0x19b1030  ~base()
68  */

  两个案列的区别:第1个案列只是普通的析构函数;第2个案列是虚析构函数。

7、 关于虚函数的思考题

  1. 构造函数可以成为虚函数吗?--- 不可以

      不可以。因为在构造函数执行结束后,虚函数表指针才会被正确的初始化。

   在c++的多态中,虚函数表是由编译器自动生成与维护的,虚函数表指针是由构造函数初始化完成的,即构造函数相当于是虚函数的入口点,负责调用虚函数的前期工作;在构造函数执行的过程中,虚函数表指针有可能未被正确的初始化;由于在不同的c++编译器中,虚函数表 与 虚函数表指针的实现有所不同,所以禁止将构造函数声明为虚函数。

          c++中的多态机制

  2. 析造函数可以成为虚函数吗?--- 虚函数,且发生多态

      可以,并且产生动态多态。因为析构函数是在对象销毁之前被调用,即在对象销毁前  虚函数表指针是正确指向对应的虚函数表。

  3. 构造函数中可以调用虚函数发生多态吗?--- 不能发生多态

   构造函数中可以调用虚函数,但是不可能发生多态行为,因为在构造函数执行时,虚函数表指针未被正确初始化。

  4. 析构函数中可以调用虚函数发生多态吗?--- 不能发生多态

      析构函数中可以调用虚函数,但是不可能发生多态行为,因为在析构函数执行时,虚函数表指针已经被销毁。   

 1 #include <iostream>
 2 #include <string>
 3 
 4 using namespace std;
 5 
 6 class base
 7 {
 8 public:
 9     base()
10     {
11         cout << "base()" << endl;
12         
13         func();
14     }
15     
16     virtual void func() 
17     {
18         cout << "base::func()" << endl;
19     }
20     
21     virtual ~base()
22     {
23         func();
24         
25         cout << "~base()" << endl;
26     }
27 };
28 
29 
30 class derived : public base
31 {
32 public:
33     derived()
34     {
35         cout << "derived()" << endl;
36         
37         func();
38     }
39     
40     virtual void func()
41     {
42         cout << "derived::func()" << endl;
43     }
44     
45     virtual ~derived()
46     {
47         func();
48         
49         cout << "~derived()" << endl;
50     }
51 };
52 
53 void test()
54 {
55     derived d;
56 }
57 
58 int main()
59 {   
60     //栈空间
61     test();
62 
63     // 堆空间
64     //base* p = new derived();    
65     //delete p; // 多态发生(指针p指向子类对象,并且又有虚函数重写)
66     
67     return 0;
68 }
69 /*
70 base()
71 base::func()
72 derived()
73 derived::func()
74 derived::func()
75 ~derived()
76 base::func()
77 ~base()
78 */

  结论:在构造函数与析构函数中调用虚函数不能发生多态行为,只调用当前类中定义的函数版本! !

8、纯虚函数、抽象类、接口

  1.  定义 --- 以案例的方式说明

  想必大家很熟悉,对于任何一个普通类来说都可以实例化出多个对象,也就是每个对象都可以用对应的类来描述,并且这些对象在现实生活中都能找到各自的原型;比如现在有一个“狗类

(0)
打赏 c++中的多态机制 微信扫一扫

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

c++中的多态机制
验证码: c++中的多态机制