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 ...
随机推荐
- Android TextView 阴影效果(投影)
Android TextView 阴影效果(投影) 四个参数: 1 2 3 4 android:shadowColor="@color/white" android:shadowD ...
- vs2012 ultimate 密钥
Visual Studio Ultimate 2012 静态激活密钥,可以试一下. RBCXF-CVBGR-382MK-DFHJ4-C69G8
- tiny4412 串口驱动分析五 --- LDD3上TTY驱动程序源码
关于tty这部分请参考: <Linux设备驱动开发详解 第二版>第14章 Linux终端设备驱动 <精通Linux设备驱动程序开发>第6章 串行设备驱动程序 <Linux ...
- 发掘StateListAnimator的全部潜能
原文地址:https://blog.stylingandroid.com/statelistanimator/ 原文作者:Leave a reply 译文出自:安卓巴士 译者: MrlLee 校对者: ...
- android 代码覆盖率
背景 项目使用的是small插件.一个app分为main和多个插件,为了统计插件的代码覆盖率. 1 修改插件 修改插件build.gradle buildTypes { release { ... } ...
- [置顶]
kubernetes资源类型--pod和job
pod Pod是K8S的最小操作单元,一个Pod可以由一个或多个容器组成:整个K8S系统都是围绕着Pod展开的,比如如何部署运行Pod.如何保证Pod的数量.如何访问Pod等. 特点 Pod是能够被创 ...
- mac如何挂载移动硬盘、存储设备、U盘
默认情况下Mac OSX对NTFS磁盘的挂载方式是只读(read-only)的,如何实现读写: 1.借助第三方软件:比如免费版的Mounty 2.因为OSX原生就是支持NTFS的,但是后来由于微软的限 ...
- Jsp中如何在<c:forEach>标签内获取集合的长度
利用jstl标签functions的prefix属性的length属性值 1.首先在jsp页面导入jstl function标签 <%@ taglib prefix="fn" ...
- Java 数字签名原理及产生
数字签名与数字证书 关于数字签名的介绍可以参考以上这篇. 这里稍微说一下不对称加密的方式: 用公钥加密的内容只能用私钥解密,用私钥加密的内容只能用公钥解密. 这样比对称加密更安全 Java 中数字签名 ...
- ElasticSearch 结构化搜索全文
1.介绍 上篇介绍了搜索结构化数据的简单应用示例,现在来探寻 全文搜索(full-text search) :怎样在全文字段中搜索到最相关的文档. 全文搜索两个最重要的方面是: 相关性(Relevan ...