多态性学习(上)

什么是多态?

多态是指同样的消息被不同类型的对象接收时导致不同的行为。所谓消息是指对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。虽然这看上去好像很高级的样子,事实上我们普通的程序设计中经常用到多态的思想。最简单的例子就是运算符,使用同样的加号“+”,就可以实现整型数之间、浮点数之间、双精度浮点数之间的加法,以及这几种数据类型混合的加法运算。同样的消息--加法,被不同类型的对象—不同数据类型的变量接收后,采用不同的方法进行相加运算。这些就是多态现象。

多态的类型

面向对象的多态性可以分为4类:重载多态、强制多态、包含多态和参数多态。我们对于C++了解的函数的重载就是属于重载多态,上文讲到的运算符重载也是属于重载多态的范畴。包含多态是类族中定义于不同类中的同名成员函数的多态行为,主要是通过虚函数来实现的。这一次的总结中主要讲解重载多态和包含多态,剩下的两种多态我将在下文继续讲解。

运算符重载

运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为。运算符重载的实质就是函数重载。C++中预定义的运算符的操作对象只能是基本的数据类型,那么我们有时候需要对自定义的数据类型(比如类)也有类似的数据运算操作。所以,我们的运算符重载的这一多态形式就衍生出来了。

相信看到这里,应该有很多像我这样的大学生并不陌生了吧,在我们钟爱的ACM/ICPC中是不是经常遇到过的啊?没错,特别是在计算几何中我们定义完一个向量结构体之后,需要对“+”“-”实行运算符重载,这样我们就可以直接对向量进行加减乘除了。

运算符重载的规则

  • C++中的运算符除了少数几个之外,全部可以重载,而且只能重载C++中已经有的运算符。C++中类属关系运算符“.”、成员指针运算符“.*”、作用域分辨符“::”和三元运算符“?:”是不能重载的。
  • 重载之后运算符的优先级和结合性都不会改变。
  • 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。

运算符重载的实现

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<cstdlib>
 5 #include<cmath>
 6 #include<algorithm>
 7 #define inf 0x7fffffff
 8 using namespace std;
 9
10 class Complex {
11 public:
12     Complex (double r=0.0 , double i=0.0):real(r),imag(i){}
13     Complex operator + (const Complex &c2) const;
14     Complex operator - (const Complex &c2) const;
15     void display() const;
16 private:
17     double real;
18     double imag;
19 };
20
21 Complex Complex::operator + (const Complex &c2) const {
22     return Complex(real+c2.real , imag+c2.imag);
23 }
24 Complex Complex::operator - (const Complex &c2) const {
25     return Complex(real-c2.real , imag-c2.imag);
26 }
27 void Complex::display() const {
28     cout<<"("<<real<<", "<<imag<<")"<<endl;
29 }
30
31 int main()
32 {
33     Complex c1(5,4),c2(2,10),c3;
34     cout<<"c1= ";
35     c1.display();
36     cout<<"c2= ";
37     c2.display();
38     c3=c1+c2;
39     cout<<"c3=c1+c2 :";
40     c3.display();
41     c3=c1-c2;
42     cout<<"c3=c1-c2 :";
43     c3.display();
44     return 0;
45 }

在本例中,将复数的加减法这样的运算重载为复数类的成员函数,可以看出,除了在函数声明及实现的时候使用了关键字operator之外,运算符重载成员函数与类的普通成员函数没有什么区别。在使用的时候,可以直接通过运算符、操作数的方式来完成函数调用。这时,运算符“+”、“-”原有的功能都不改变,对整型数、浮点数等基本类型数据的运算仍然遵循C++预定义的规则,同时添加了新的针对复数运算的功能。

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<cstdlib>
 5 #include<cmath>
 6 #include<algorithm>
 7 #define inf 0x7fffffff
 8 using namespace std;
 9
10 class Clock {
11 public:
12     Clock(int hour=0,int minute=0,int second=0);
13     void showTime() const;
14     Clock& operator ++ ();
15     Clock operator ++ (int);
16 private:
17     int hour,minute,second;
18 };
19
20 Clock::Clock(int hour,int minute,int second) {
21     if (hour>=0&&hour<24 && minute>=0&&minute<60 && second>=0&&second<60) {
22         this->hour = hour;
23         this->minute = minute;
24         this->second = second;
25     }
26     else {
27         cout<<"Time error!"<<endl;
28     }
29 }
30 void Clock::showTime() const {
31     cout<<hour<<":"<<minute<<":"<<second<<endl;
32 }
33 Clock & Clock::operator ++ () {
34     second ++ ;
35     if (second >= 60) {
36         second -= 60;
37         minute ++ ;
38         if (minute >= 60) {
39             minute -= 60;
40             hour = (hour+1)%24;
41         }
42     }
43     return *this;
44 }
45 Clock Clock::operator ++ (int) {
46     Clock old= *this;
47     ++(*this);
48     return old;
49 }
50
51 int main()
52 {
53     Clock myClock(23,59,59);
54     cout<<"First time output: ";
55     myClock.showTime();
56     cout<<"show myClock++: ";
57     (myClock++).showTime();
58     cout<<"show ++myClock: ";
59     (++myClock).showTime();
60     return 0;
61 }

这个例子中,我们把时间自增前置++和后置++运算重载为时钟类的成员函数,前置单目运算符和后置单目运算符的重载最主要的区别就在于重载函数的形参。

语法规定:前置单目运算符重载为成员函数时没有形参,后置单目运算符重载为成员函数时需要有一个int型形参。

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<cstdlib>
 5 #include<cmath>
 6 #include<algorithm>
 7 #define inf 0x7fffffff
 8 using namespace std;
 9
10 class Complex {
11 public:
12     Complex (double r=0.0,double i=0.0):real(r),imag(i){}
13     friend Complex operator + (const Complex &c1,const Complex &c2);
14     friend Complex operator - (const Complex &c1,const Complex &c2);
15     friend ostream & operator << (ostream &out,const Complex &c);
16 private:
17     double real;
18     double imag;
19 };
20
21 Complex operator + (const Complex &c1,const Complex &c2) {
22     return Complex(c1.real+c2.real , c1.imag+c2.imag);
23 }
24 Complex operator - (const Complex &c1,const Complex &c2) {
25     return Complex(c1.real-c2.real , c1.imag-c2.imag);
26 }
27 ostream & operator << (ostream &out,const Complex &c) {
28     cout<<"("<<c.real<<", "<<c.imag<<")"<<endl;
29     return out;
30 }
31
32 int main()
33 {
34     Complex c1(5,4),c2(2,10),c3;
35     cout<<"c1= "<<c1<<endl;
36     cout<<"c2= "<<c2<<endl;
37     c3=c1+c2;
38     cout<<"c3=c1+c2 :"<<c3<<endl;
39     c3=c1-c2;
40     cout<<"c3=c1-c2 :"<<c3<<endl;
41     return 0;
42 }

这一次我们将运算符重载为类的非成员函数,就必须把操作数全部通过形参的方式传递给运算符重载函数,“<<”操作符的左操作数为ostream类型的引用,ostream是cout类型的一个基类,右操作数是Complex类型的引用,这样在执行cout<<c1时,就会调用operator<<(cout,c1)。

包含多态

刚才就有说到,虚函数是包含多态的主要内容。那么,我们就来看看什么是虚函数。

虚函数是动态绑定的基础。虚函数经过派生之后,在类族中就可以实现运行过程中的多态。

根据赋值兼容规则,可以使用派生类的对象来代替基类对象。如果用基类类型的指针指向派生类对象,就可以通过这个指针来访问该对象,但是我们访问到的只是从基类继承来的同名成员。解决这一问题的方法是:如果需要通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,那么首先在基类中将这个同名函数说明为虚函数。这样,通过基类类型的指针,就可以使属于不同派生类的不同对象产生不同的行为,从而实现运行过程的多态。

上面这一段文字初次读来有点生拗,希望读者多读两遍,因为这是很重要也是很核心的思想。接下来,我们看看两段代码,体会一下基类中虚函数的作用。

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<cstdlib>
 5 #include<cmath>
 6 #include<algorithm>
 7 #define inf 0x7fffffff
 8 using namespace std;
 9
10 class A {
11 public:
12     A() {}
13     virtual void foo() {
14         cout<<"This is A."<<endl;
15     }
16 };
17 class B:public A {
18 public:
19     B(){}
20     void foo() {
21         cout<<"This is B."<<endl;
22     }
23 };
24
25 int main()
26 {
27     A *a=new B();
28     a->foo();
29     if (a != NULL) delete a;
30     return 0;
31 }

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<cstdlib>
 5 #include<cmath>
 6 #include<algorithm>
 7 #define inf 0x7fffffff
 8 using namespace std;
 9
10 class Base1 {
11 public:
12     virtual void display() const;
13 };
14 void Base1::display() const {
15     cout<<"Base1::display()"<<endl;
16 }
17
18 class Base2:public Base1 {
19 public:
20     void display() const;
21 };
22 void Base2::display() const {
23     cout<<"Base2::display()"<<endl;
24 }
25
26 class Derived:public Base2 {
27 public:
28     void display() const;
29 };
30 void Derived::display() const {
31     cout<<"Derived::display()"<<endl;
32 }
33
34 void fun(Base1 *ptr) {
35     ptr->display();
36 }
37
38 int main()
39 {
40     Base1 base1;
41     Base2 base2;
42     Derived derived;
43     fun(&base1);
44     fun(&base2);
45     fun(&derived);
46     return 0;
47 }

在后面的一段程序中,派生类并没有显式的给出虚函数的声明,这时系统就会遵循以下规则来判断派生类的一个函数成员是否是虚函数:

  • 该函数是否与基类的虚函数有相同的名称
  • 该函数是否与基类的虚函数有相同的参数个数及相同的对应参数类型
  • 该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针、引用型的返回值。

虚析构函数

在C++中,不能声明虚构造函数,但是可以声明虚析构函数。如果一个类的析构函数是虚函数,那么由它派生而来的所有子类的析构函数也是虚函数。在析构函数设置为虚函数之后,在使用指针引用时可以动态绑定,实现运行时的多态,保证使用基类类型的指针就能够调用适当的析构函数针对不用的对象进行清理工作。

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<cstdlib>
 5 #include<cmath>
 6 #include<algorithm>
 7 #define inf 0x7fffffff
 8 using namespace std;
 9
10 class Base {
11 public:
12     ~Base();
13 };
14 Base::~Base() {
15     cout<<"Base destructor"<<endl;
16 }
17
18 class Derived:public Base {
19 public:
20     Derived();
21     ~Derived();
22 private:
23     int *p;
24 };
25 Derived::Derived() {
26     p=new int(0);
27 }
28 Derived::~Derived() {
29     cout<<"Derived destructor"<<endl;
30     delete p;
31 }
32
33 void fun(Base *b) {
34     delete b;
35 }
36
37 int main()
38 {
39     Base *b=new Derived();
40     fun(b);
41     return 0;
42 }

这说明,通过基类指针删除派生类对象时调用的是基类的析构函数,派生类的析构函数没有被执行,因此派生类对象中动态分配的内存空间没有得到释放,造成了内存泄露。

避免上述错误的有效方法就是将析构函数声明为虚函数:

1 class Base {
2 public:
3     virtual ~Base();
4 };

此时,我们再次运行这一份代码,得到的结果就如下图所示。

这说明派生类的析构函数被调用了,派生类对象中动态申请的内存空间被正确地释放了。这是由于使用了虚析构函数,实现了多态。

转载于:https://www.cnblogs.com/BaiYiShaoNian/p/4596407.html

C++中多态性学习(上)相关推荐

  1. (转)SpringMVC学习(九)——SpringMVC中实现文件上传

    http://blog.csdn.net/yerenyuan_pku/article/details/72511975 这一篇博文主要来总结下SpringMVC中实现文件上传的步骤.但这里我只讲单个文 ...

  2. 奥卡姆剃刀是什么?机器学习实践中那些学习模型或者那些评估指标践行了这一理论?

    奥卡姆剃刀是什么?机器学习实践中那些学习模型或者那些评估指标践行了这一理论? 奥卡姆剃刀:无无必要,勿增实体. 奥卡姆剃刀原理应用于模型选择时变为以下想法:在所有可能选择的模型中,能够很好地解释已知数 ...

  3. DeepMind发布最新《神经网络中持续学习》综述论文!

    来源:AI科技评论 本文约3200字,建议阅读6分钟 本文重点介绍一些神经网络中最有前途和最有影响力的方向. 在过去的几十年中,人工智能研究取得了巨大进展,但是它主要依赖于固定的数据集和固定的环境. ...

  4. 运动想象脑机接口中迁移学习的完整流程

    点击上面"脑机接口社区"关注我们 更多技术干货第一时间送达 脑机接口(Brain-Computer Interface, BCI)可以让用户使用脑电信号直接与计算机或其他外部设备进 ...

  5. 伍冬睿教授:脑机接口中迁移学习的完整流程

    大家好,今天Rose分享一篇关于脑机接口中的迁移学习的完整流程.本文由华中科技大学伍冬睿教授授权分享. 关于脑机接口中迁移学习方面的研究,伍教授团队做过大量的工作.之前社区分享过<脑机接口中的流 ...

  6. 如何用java创建超链接_Java如何在PPT中的幻灯片上创建超链接?

    在Java编程中,如何在PPT中的幻灯片上创建超链接? 注意:需要访问网址:http://poi.apache.org/download.html , 下载一个Apache POI软件包.这里下载最新 ...

  7. android webview 多文件上传,Android中的webview支持页面中的文件上传实例代码

    Android webview在默认情况下是不支持网页中的文件上传功能的: 如果在网页中有,在android webview中访问时也会出现浏览文件的按钮 但是点击按钮之后没有反应... 那么如何能够 ...

  8. python中subprocess_Python中subprocess学习

    subprocess的目的就是启动一个新的进程并且与之通信. subprocess模块中只定义了一个类: Popen.可以使用Popen来创建进程,并与进程进行复杂的交互.它的构造函数如下: subp ...

  9. 30多年程序员生涯经验总结(成功源自于失败中的学习;失败则是因为容忍错误的横行)...

    英文原文:Lessons From A Lifetime Of Being A Programmer 在我 30 多年的程序员生涯里,我学到了不少有用的东西.下面是我这些年积累的经验精华.我常常想,如 ...

最新文章

  1. arduino 呼吸灯_如何改善您的Arduino呼吸机:用于临时COVID-19呼吸机设计的RTS和SCS简介...
  2. 阿里老员工论坛炫耀:每年税前260万,还有三千万期权在握
  3. HP-UX下查询函数是否为线程安全的方法
  4. 对 Excel 工作簿中的数字签名和代码签名的说明
  5. PySide 简易教程三-------动手写起来
  6. 计算机二级web题目(9.1)--综合选择题3
  7. php去掉 部分字符,输出,php如何去除某个字符
  8. 读取P12格式证书的密钥
  9. RK平台LCD调试说明
  10. select回显异常
  11. linux 桥,Linux网桥
  12. springMVC中415错误
  13. 计算机专业 论文检索,精选】计算机专业文献检索论文参考选题
  14. 【久远讲算法】栈——后进先出的数据结构
  15. HL7数据的转换传输
  16. [面试题]100层楼丢玻璃球,一旦超过某层就会破,你只有两个球。
  17. 电影《不夜城》的主题曲:金城武、山本未来主演
  18. 【认知计算】认知风险管理
  19. 全球与中国磨料水射流切割机市场深度研究分析报告
  20. fiddler实现手游封包逆向测试:Fiddler手机代理一步到位(fiddler安装+手机代理+封包详解)

热门文章

  1. 【Linux】一步一步学Linux——nisdomainname命令(171)
  2. 【Linux】一步一步学Linux——nohup命令(136)
  3. 【Linux】一步一步学Linux——groups命令(93)
  4. java 洗牌_java数组之完美洗牌算法
  5. 内存中的调用别的软件程序加密解密函数_公司加密软件哪个最好用?
  6. 每天一道LeetCode-----平面上n个点,计算最多有多少个点在一条直线上
  7. 分割数组的最大值—leetcode410
  8. 第一章 PX4-Pixhawk-程序编译过程解析
  9. 自己在windows下第一次安装pygame成功的经历
  10. 3.3线性回归简洁实现(API的使用)