C++中多态性学习(上)
多态性学习(上)
什么是多态?
多态是指同样的消息被不同类型的对象接收时导致不同的行为。所谓消息是指对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。虽然这看上去好像很高级的样子,事实上我们普通的程序设计中经常用到多态的思想。最简单的例子就是运算符,使用同样的加号“+”,就可以实现整型数之间、浮点数之间、双精度浮点数之间的加法,以及这几种数据类型混合的加法运算。同样的消息--加法,被不同类型的对象—不同数据类型的变量接收后,采用不同的方法进行相加运算。这些就是多态现象。
多态的类型
面向对象的多态性可以分为4类:重载多态、强制多态、包含多态和参数多态。我们对于C++了解的函数的重载就是属于重载多态,上文讲到的运算符重载也是属于重载多态的范畴。包含多态是类族中定义于不同类中的同名成员函数的多态行为,主要是通过虚函数来实现的。这一次的总结中主要讲解重载多态和包含多态,剩下的两种多态我将在下文继续讲解。
运算符重载
运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为。运算符重载的实质就是函数重载。C++中预定义的运算符的操作对象只能是基本的数据类型,那么我们有时候需要对自定义的数据类型(比如类)也有类似的数据运算操作。所以,我们的运算符重载的这一多态形式就衍生出来了。
相信看到这里,应该有很多像我这样的大学生并不陌生了吧,在我们钟爱的ACM/ICPC中是不是经常遇到过的啊?没错,特别是在计算几何中我们定义完一个向量结构体之后,需要对“+”“-”实行运算符重载,这样我们就可以直接对向量进行加减乘除了。
运算符重载的规则
- C++中的运算符除了少数几个之外,全部可以重载,而且只能重载C++中已经有的运算符。C++中类属关系运算符“.”、成员指针运算符“.*”、作用域分辨符“::”和三元运算符“?:”是不能重载的。
- 重载之后运算符的优先级和结合性都不会改变。
- 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。
运算符重载的实现
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#define inf 0x7fffffff
using namespace std; class Complex {
public:
Complex (double r=0.0 , double i=0.0):real(r),imag(i){}
Complex operator + (const Complex &c2) const;
Complex operator - (const Complex &c2) const;
void display() const;
private:
double real;
double imag;
}; Complex Complex::operator + (const Complex &c2) const {
return Complex(real+c2.real , imag+c2.imag);
}
Complex Complex::operator - (const Complex &c2) const {
return Complex(real-c2.real , imag-c2.imag);
}
void Complex::display() const {
cout<<"("<<real<<", "<<imag<<")"<<endl;
} int main()
{
Complex c1(,),c2(,),c3;
cout<<"c1= ";
c1.display();
cout<<"c2= ";
c2.display();
c3=c1+c2;
cout<<"c3=c1+c2 :";
c3.display();
c3=c1-c2;
cout<<"c3=c1-c2 :";
c3.display();
return ;
}
在本例中,将复数的加减法这样的运算重载为复数类的成员函数,可以看出,除了在函数声明及实现的时候使用了关键字operator之外,运算符重载成员函数与类的普通成员函数没有什么区别。在使用的时候,可以直接通过运算符、操作数的方式来完成函数调用。这时,运算符“+”、“-”原有的功能都不改变,对整型数、浮点数等基本类型数据的运算仍然遵循C++预定义的规则,同时添加了新的针对复数运算的功能。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#define inf 0x7fffffff
using namespace std; class Clock {
public:
Clock(int hour=,int minute=,int second=);
void showTime() const;
Clock& operator ++ ();
Clock operator ++ (int);
private:
int hour,minute,second;
}; Clock::Clock(int hour,int minute,int second) {
if (hour>=&&hour< && minute>=&&minute< && second>=&&second<) {
this->hour = hour;
this->minute = minute;
this->second = second;
}
else {
cout<<"Time error!"<<endl;
}
}
void Clock::showTime() const {
cout<<hour<<":"<<minute<<":"<<second<<endl;
}
Clock & Clock::operator ++ () {
second ++ ;
if (second >= ) {
second -= ;
minute ++ ;
if (minute >= ) {
minute -= ;
hour = (hour+)%;
}
}
return *this;
}
Clock Clock::operator ++ (int) {
Clock old= *this;
++(*this);
return old;
} int main()
{
Clock myClock(,,);
cout<<"First time output: ";
myClock.showTime();
cout<<"show myClock++: ";
(myClock++).showTime();
cout<<"show ++myClock: ";
(++myClock).showTime();
return ;
}
这个例子中,我们把时间自增前置++和后置++运算重载为时钟类的成员函数,前置单目运算符和后置单目运算符的重载最主要的区别就在于重载函数的形参。
语法规定:前置单目运算符重载为成员函数时没有形参,后置单目运算符重载为成员函数时需要有一个int型形参。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#define inf 0x7fffffff
using namespace std; class Complex {
public:
Complex (double r=0.0,double i=0.0):real(r),imag(i){}
friend Complex operator + (const Complex &c1,const Complex &c2);
friend Complex operator - (const Complex &c1,const Complex &c2);
friend ostream & operator << (ostream &out,const Complex &c);
private:
double real;
double imag;
}; Complex operator + (const Complex &c1,const Complex &c2) {
return Complex(c1.real+c2.real , c1.imag+c2.imag);
}
Complex operator - (const Complex &c1,const Complex &c2) {
return Complex(c1.real-c2.real , c1.imag-c2.imag);
}
ostream & operator << (ostream &out,const Complex &c) {
cout<<"("<<c.real<<", "<<c.imag<<")"<<endl;
return out;
} int main()
{
Complex c1(,),c2(,),c3;
cout<<"c1= "<<c1<<endl;
cout<<"c2= "<<c2<<endl;
c3=c1+c2;
cout<<"c3=c1+c2 :"<<c3<<endl;
c3=c1-c2;
cout<<"c3=c1-c2 :"<<c3<<endl;
return ;
}
这一次我们将运算符重载为类的非成员函数,就必须把操作数全部通过形参的方式传递给运算符重载函数,“<<”操作符的左操作数为ostream类型的引用,ostream是cout类型的一个基类,右操作数是Complex类型的引用,这样在执行cout<<c1时,就会调用operator<<(cout,c1)。
包含多态
刚才就有说到,虚函数是包含多态的主要内容。那么,我们就来看看什么是虚函数。
虚函数是动态绑定的基础。虚函数经过派生之后,在类族中就可以实现运行过程中的多态。
根据赋值兼容规则,可以使用派生类的对象来代替基类对象。如果用基类类型的指针指向派生类对象,就可以通过这个指针来访问该对象,但是我们访问到的只是从基类继承来的同名成员。解决这一问题的方法是:如果需要通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,那么首先在基类中将这个同名函数说明为虚函数。这样,通过基类类型的指针,就可以使属于不同派生类的不同对象产生不同的行为,从而实现运行过程的多态。
上面这一段文字初次读来有点生拗,希望读者多读两遍,因为这是很重要也是很核心的思想。接下来,我们看看两段代码,体会一下基类中虚函数的作用。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#define inf 0x7fffffff
using namespace std; class A {
public:
A() {}
virtual void foo() {
cout<<"This is A."<<endl;
}
};
class B:public A {
public:
B(){}
void foo() {
cout<<"This is B."<<endl;
}
}; int main()
{
A *a=new B();
a->foo();
if (a != NULL) delete a;
return ;
}
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#define inf 0x7fffffff
using namespace std; class Base1 {
public:
virtual void display() const;
};
void Base1::display() const {
cout<<"Base1::display()"<<endl;
} class Base2:public Base1 {
public:
void display() const;
};
void Base2::display() const {
cout<<"Base2::display()"<<endl;
} class Derived:public Base2 {
public:
void display() const;
};
void Derived::display() const {
cout<<"Derived::display()"<<endl;
} void fun(Base1 *ptr) {
ptr->display();
} int main()
{
Base1 base1;
Base2 base2;
Derived derived;
fun(&base1);
fun(&base2);
fun(&derived);
return ;
}
在后面的一段程序中,派生类并没有显式的给出虚函数的声明,这时系统就会遵循以下规则来判断派生类的一个函数成员是否是虚函数:
- 该函数是否与基类的虚函数有相同的名称
- 该函数是否与基类的虚函数有相同的参数个数及相同的对应参数类型
- 该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针、引用型的返回值。
虚析构函数
在C++中,不能声明虚构造函数,但是可以声明虚析构函数。如果一个类的析构函数是虚函数,那么由它派生而来的所有子类的析构函数也是虚函数。在析构函数设置为虚函数之后,在使用指针引用时可以动态绑定,实现运行时的多态,保证使用基类类型的指针就能够调用适当的析构函数针对不用的对象进行清理工作。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#define inf 0x7fffffff
using namespace std; class Base {
public:
~Base();
};
Base::~Base() {
cout<<"Base destructor"<<endl;
} class Derived:public Base {
public:
Derived();
~Derived();
private:
int *p;
};
Derived::Derived() {
p=new int();
}
Derived::~Derived() {
cout<<"Derived destructor"<<endl;
delete p;
} void fun(Base *b) {
delete b;
} int main()
{
Base *b=new Derived();
fun(b);
return ;
}
这说明,通过基类指针删除派生类对象时调用的是基类的析构函数,派生类的析构函数没有被执行,因此派生类对象中动态分配的内存空间没有得到释放,造成了内存泄露。
避免上述错误的有效方法就是将析构函数声明为虚函数:
class Base {
public:
virtual ~Base();
};
此时,我们再次运行这一份代码,得到的结果就如下图所示。
这说明派生类的析构函数被调用了,派生类对象中动态申请的内存空间被正确地释放了。这是由于使用了虚析构函数,实现了多态。
C++中多态性学习(上)的更多相关文章
- (转)SpringMVC学习(九)——SpringMVC中实现文件上传
http://blog.csdn.net/yerenyuan_pku/article/details/72511975 这一篇博文主要来总结下SpringMVC中实现文件上传的步骤.但这里我只讲单个文 ...
- 设计模式(九): 从醋溜土豆丝和清炒苦瓜中来学习"模板方法模式"(Template Method Pattern)
今天是五.四青年节,祝大家节日快乐.看着今天这标题就有食欲,夏天到了,醋溜土豆丝和清炒苦瓜适合夏天吃,好吃不上火.这两道菜大部分人都应该吃过,特别是醋溜土豆丝,作为“鲁菜”的代表作之一更是为大众所熟知 ...
- 转:openwrt中luci学习笔记
原文地址:openwrt中luci学习笔记 最近在学习OpenWrt,需要在OpenWrt的WEB界面增加内容,本文将讲述修改OpenWrt的过程和其中遇到的问题. 一.WEB界面开发 ...
- PHP中,文件上传实例
PHP中,文件上传一般是通过move_uploaded_file()来实现的. bool move_uploaded_file ( string filename, string destinati ...
- c语言学习上的思考与心得
由于这段时间在c语言的学习中,表现的很努力并且完成作业态度认真,所以得到了老师奖励的小黄衫. 以下是我对于c语言的学习感受与心得. 学习感受与心得 我选择计算机的这个专业,是因为我对计算机的学习很有兴 ...
- Python中subprocess学习
subprocess的目的就是启动一个新的进程并且与之通信. subprocess模块中只定义了一个类: Popen.可以使用Popen来创建进程,并与进程进行复杂的交互.它的构造函数如下: subp ...
- asp.net中遍历界面上所有控件进行属性设置
* 使用方法: * 前台页面调用方法,重置: protected void Reset_Click(object sender, EventArgs e) { ...
- 切记ajax中要带上AntiForgeryToken防止CSRF攻击
在程序项目中经常看到ajax post数据到服务器没有加上防伪标记,导致CSRF被攻击,下面小编通过本篇文章给大家介绍ajax中要带上AntiForgeryToken防止CSRF攻击,感兴趣的朋友一起 ...
- 在NLP中深度学习模型何时需要树形结构?
在NLP中深度学习模型何时需要树形结构? 前段时间阅读了Jiwei Li等人[1]在EMNLP2015上发表的论文<When Are Tree Structures Necessary for ...
随机推荐
- ife2015-task2-javascript-util.js
util.js/** * Created by Administrator on 2016/12/14. *///判断是否为数组function isArray(arr){ return (arr i ...
- 死磕 Fragment 的生命周期
死磕 Fragment 的生命周期 本文原创,转载请注明出处.欢迎关注我的 简书 ,关注我的专题 Android Class 我会长期坚持为大家收录简书上高质量的 Android 相关博文.本篇文章已 ...
- Winform 遍历 ListBox中的所有项
foreach(DataRowView row in listBox.Items ) { MessageBox.Show(row["displayMember"].ToString ...
- 网易云音乐PC客户端加密API逆向解析
1.前言 网上已经有大量的web端接口解析的方法了,但是对客户端的接口解析基本上找不到什么资料,本文主要分析网易云音乐PC客户端的API接口交互方式. 通过内部的代理设置,使用fiddler作为代理工 ...
- Bean的作用域scope
Bean的作用域scope 1.singleton 单例,指一个bean容器中只存在一份 2.prototype 每次请求(每次使用)创建新的实例,destroy方式不生效 3.request 每次h ...
- 3D数学读书笔记——矩阵基础番外篇之线性变换
本系列文章由birdlove1987编写.转载请注明出处. 文章链接:http://blog.csdn.net/zhurui_idea/article/details/25102425 前面有一篇文章 ...
- mysql 存储过程 演示样例代码
drop procedure if exists P_SEQUENCE; /** 暂省略包 @AUTO LIANGRUI 2014/6/27 T_PRO_PRODUCT 表 排序 对整个表进行按序号排 ...
- PS 如何制作球面化文字效果
球面化文字效果图.... 00newopen-a 00newopen-b 01mew+channel 02ctrl+L 03ctrl+I 04new+wenzi 05R ...
- 状态机工作流,顺序工作流和Flowchart
什么是工作流,工作流可以说是对业务处理过程的建模,当我们设计工作流的时候,我们首先要分析业务处理过程中要经历的步骤.然后,我们就可以利用WF创建工作流模型来模拟业务的处理过程. WF工作流包含两种类型 ...
- 从程序员到asp.net架构师转变[转]
微软的DotNet开发绝对是属于那种入门容易提高难的技术.而要能够成为DotNet架构师没有三年或更长时间的编码积累基本上是不可能的.特别是在大型软件项目中,架构师是项目核心成员,承上启下,因此 RU ...