C++中多态性学习(上)

多态性学习(上)

什么是多态?

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

多态的类型

面向对象的多态性可以分为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 };

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

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

时间: 2024-10-14 00:37:19

C++中多态性学习(上)的相关文章

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

这一篇博文主要来总结下SpringMVC中实现文件上传的步骤.但这里我只讲单个文件的上传. 环境准备 SpringMVC上传文件的功能需要两个jar包的支持,如下: 工程中肯定要导入以上两个jar包,主要是CommonsMultipartResolver解析器依赖commons-fileupload和commons-io这两个jar包. 单个文件的上传 前台页面 我们要改造editItem.jsp页面,主要是在form表单中添加商品图片一栏,效果我截图如下: 注意一点的是form表单中别忘了写e

struts2中的文件上传和下载

天下大事,必做于细.天下难事,必作于易. 曾经见过某些人,基础的知识还不扎实就去学习更难的事,这样必然在学习新的知识会很迷惑结果 再回来重新学习一下没有搞懂的知识,这必然会导致学习效率的下降!我写的这篇上传和下载都很基础. 十分适合初学者! jsp:页面 <!--在进行文件上传时,表单提交方式一定要是post的方式,因为文件上传时二进制文件可能会很大,还有就是enctype属性,这个属性一定要写成multipart/form-data, 不然就会以二进制文本上传到服务器端--> <for

切记ajax中要带上AntiForgeryToken防止CSRF攻击

在程序项目中经常看到ajax post数据到服务器没有加上防伪标记,导致CSRF被攻击,下面小编通过本篇文章给大家介绍ajax中要带上AntiForgeryToken防止CSRF攻击,感兴趣的朋友一起学习吧 经常看到在项目中ajax post数据到服务器不加防伪标记,造成CSRF攻击 在Asp.net Mvc里加入防伪标记很简单在表单中加入Html.AntiForgeryToken()即可. Html.AntiForgeryToken()会生成一对加密的字符串,分别存放在Cookies 和 in

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

英文原文:Lessons From A Lifetime Of Being A Programmer 在我 30 多年的程序员生涯里,我学到了不少有用的东西.下面是我这些年积累的经验精华.我常常想,如果以前能有人在这些经验上指点一二,我相信我现在会站得更高. 1. 客户在接触到产品之后,才会真正明白自己的需求. 这是我在我的第一份工作上面学来的.只有当我们给客户展示产品的时候,他们才会意识到哪些是必须的.给出一个功能性原型设计远远比一张长长的文字表格要好. 2. 只要有充足的时间,所有安全防御系

PHP中,文件上传实例

PHP中,文件上传一般是通过move_uploaded_file()来实现的.  bool move_uploaded_file ( string filename, string destination ) 本函数检查并确保由 filename 指定的文件是合法的上传文件(即通过 PHP 的 HTTP POST 上传机制所上传的).如果文件合法,则将 其移动为由 destination 指定的文件. 如果 filename 不是合法的上传文件,不会出现任何操作,move_uploaded_fi

MVC3.0 中Razor 学习

C# 的主要 Razor 语法规则 Razor 代码封装于 @{ ... } 中 行内表达式(变量和函数)以 @ 开头 代码语句以分号结尾 字符串由引号包围 C# 代码对大小写敏感 C# 文件的扩展名是 .cshtml MVC3.0 中Razor 学习 随着MVC3.0RTM版本的发布,最近将公司的项目从MVC2.0升级到MVC3.0.同时打算在MVC3中全面使用Razor模板引擎.现将Razor学习拿出来和大家分享,如果存在不足的地方欢迎您指出. 其实在使用<%= %>在html中调用C#代

设计模式(九): 从醋溜土豆丝和清炒苦瓜中来学习&quot;模板方法模式&quot;(Template Method Pattern)

今天是五.四青年节,祝大家节日快乐.看着今天这标题就有食欲,夏天到了,醋溜土豆丝和清炒苦瓜适合夏天吃,好吃不上火.这两道菜大部分人都应该吃过,特别是醋溜土豆丝,作为“鲁菜”的代表作之一更是为大众所熟知,醋溜土豆丝,好吃不上火.清炒苦瓜这道菜好啊,更是夏天必备之良菜,其功效在此就不做过多赘述了.言归正传,上篇博客我们从“小弟”中学习了“外观模式”,我们也把“外观模式”戏称为“小弟模式”.今天我们要从醋溜土豆丝和清炒苦瓜的制作过程中来学习一下我们今天博客的主题“模板方法模式”(Template Me

详细阐述Web开发中的图片上传问题

Web开发中,图片上传是一种极其常见的功能.但是呢,每次做上传,都花费了不少时间. 一个"小功能"花费我这么多时间,真心不愉快. So,要得认真分析下原因. 1.在最初学习Java Web开发的时候,经验不足,属于能力问题,比如对技术认识不到位. 2.图片上传是一类问题,而不是一个问题.   比如,大家都会做饭,但每个人自己做饭是有不同的.做了一个人吃.一家人吃.喜事待客做好几桌,是不同的问题.   同样的,图片上传,是上传一张还是多张,前端的用户体验如何,后端逻辑处理是否正确,图片存

Python中subprocess学习

生命不息奋斗不止! subprocess的目的就是启动一个新的进程并且与之通信. subprocess模块中只定义了一个类: Popen.可以使用Popen来创建进程,并与进程进行复杂的交互.它的构造函数如下: subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None