c++中的多态机制
目录
背景介绍
虚函数重写:子类重新定义父类中有相同返回值、名称和参数的虚函数;
非虚函重写:子类重新定义父类中有相同名称和参数的非虚函数;
父子间的赋值兼容:子类对象可以当作父类对象使用(兼容性);具体表现为:
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 }
为什么会出现上述现象呢?(在赋值兼容中,父类指针/引用指向子类对象时为何不能调用子类重写函数?)
问题分析:在编译期间,编译器只能根据指针的类型判断所指向的对象;根据赋值兼容,编译器认为父类指针指向的是父类对象;因此,编译结果只可能是调用父类中定义的同名函数。
在编译这个函数的时候,编译器不可能知道指针p究竟指向了什么。但是编译器没有理由报错,于是,编译器认为最安全的做法是调用父类的print函数。因为父类和子类肯定都有相同的print函数。
要想解决这个问题,就需要使用c++中的多态。那么如何实现c++中的多态呢?请看下面详解:
多态介绍
1、 什么是多态
在现实生活中,多态是同一个事物在不同场景下的多种形态。
在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。与之相对应的编译时绑定函数称为静态绑定。
2、 多态的分类
静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数就调用,没有的话就会发出警告或者报错;
动态多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。
3、动态多态成立的条件
由之前出现的问题可知,编译器的做法并不符合我们的期望(因为编译器是根据父类指针的类型去父类中调用被重写的函数);但是,在面向对象的多态中,我们期望的行为是 根据实际的对象类型来判断如何调用重写函数(虚函数);
1. 即当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;
2. 即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数;
这种多态行为的表现效果为:同样的调用语句在实际运行时有多种不同的表现形态。
那么在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指针就会在 父类的 虚函数表中查找被调用的函数
注:出于效率考虑,没有必要将所有成员函数都声明为虚函数。
如何证明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++编译器中,虚函数表 与 虚函数表指针的实现有所不同,所以禁止将构造函数声明为虚函数。
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. 定义 --- 以案例的方式说明
想必大家很熟悉,对于任何一个普通类来说都可以实例化出多个对象,也就是每个对象都可以用对应的类来描述,并且这些对象在现实生活中都能找到各自的原型;比如现在有一个“狗类
相关文章:
-
-
我的LeetCode:https://leetcode cn.com/u/ituring/ 我的LeetCode刷题源码[GitHub]:https:/... [阅读全文]
-
################################################## 集合的作用是:# 1、获得两个集合之间某种关系的集... [阅读全文]
-
我们先来看一下什么是构造器: 1、构造器也叫构造方法或构造函数,分为有参构造器和无参构造器; 2、构造器也是一种方法,只不过是一种特殊的方法,它会在对象... [阅读全文]
-
YII 中使用 Expression解决查询中带有常量报错的问题
Yii 官方手册关于 Expression 的解释:https://www.yiichina.com/doc/api/2.0/yii-db-expres... [阅读全文] -
题目描述 在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的... [阅读全文]
-
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
上一篇: 一些经典的计算机书籍
发表评论