1 什么是多态

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

C++ 多态意味着由继承而产生的相关的不同的类,调用重写函数时,会根据实际的对象类型来执行不同的函数。

有了多态,可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性。可以减轻系统升级、维护、调试的工作量和复杂度。

1.2 静态链接、动态链接

链接是指一个程序模块、代码之间相互关联的过程。

静态链接 是程序链接在编译阶段实现,也称为早期匹配。

例如:由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象,从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态链接。

示例代码如下所示:

// test1.cpp,用于验证静态链接

#include <iostream>

using namespace std;

class Parent
{
public: void print()
{
cout << "this is ParentClass" << endl;
}
}; class Child : public Parent
{
public: void print()
{
cout << "this is ChildClass" << endl;
}
}; void printByPoint(Parent *parent)
{
parent->print();
} void printByReference(Parent& parent)
{
parent.print();
} int main()
{
Child childTest_4;
Parent *parentTest_4 = NULL;
parentTest_4 = &childTest_4; // 父类指针指向子类对象
printByPoint(parentTest_4); Child childTest_5;
Parent &parentTest_5 =childTest_5; // 父类引用引用子类对象
printByReference(parentTest_5); return 0;
}

运行结果:

从以上运行结果发现,程序并没有如我们所想实现多态。我们想要的是在程序中任意点可以根据实际所调用的对象类型来选择调用的函数,这种操作被称为 动态链接,或后期绑定。

多态的发生是动态链接,是在程序执行的时候判断具体父类指针应该调用的方法。

C++ 通过 virtual 关键字对多态进行支持。

1.3 虚函数

虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

示例代码如下:

// test2.cpp,用于验证虚函数的动态链接

#include <iostream>

using namespace std;

class Parent
{
public: virtual void print()
{
cout << "this is ParentClass" << endl;
}
}; class Child : public Parent
{
public: virtual void print()
{
cout << "this is ChildClass" << endl;
}
}; void printByPoint(Parent *parent)
{
parent->print();
} void printByReference(Parent& parent)
{
parent.print();
} int main()
{
Child childTest_4;
Parent *parentTest_4 = NULL;
parentTest_4 = &childTest_4; // 父类指针指向子类对象
printByPoint(parentTest_4); Child childTest_5;
Parent &parentTest_5 =childTest_5; // 父类引用引用子类对象
printByReference(parentTest_5); return 0;
}

运行结果:

从以上运行结果发现,使用 virtual 声明的函数被重写后即可展现多态特性。

1.4 多态成立的条件

  1. 要有继承

  2. 要有虚函数重写

  3. 要有父类指针(或父类引用)指向子类对象

2 虚析构函数

2.1 为什么需要虚析构函数

当一个类有子类时,该类的析构函数必须是虚函数,原因:可能存在有资源释放不完全的情况。

先举例,资源释放完全的情况:

2.1.1 直接通过子类对象释放资源,不需要在父类写虚析构函数

示例代码如下所示:

// test3.cpp,用于验证为什么需要虚析构函数

#include <iostream>

using namespace std;

class Parent
{
public: Parent()
{
str = new char[20]; strcpy(str,"this is parent"); cout << "父类构造函数运行" << endl;
} ~Parent()
{
delete str; cout << "父类析构函数运行" << endl;
} private: char *str;
}; class Child : public Parent
{
public: Child()
{
str = new char[20]; strcpy(str,"this is Child"); cout << "子类构造函数运行" << endl;
} ~Child()
{
delete str; cout << "子类析构函数运行" << endl;
} private: char *str;
}; // 通过 父类指针 释放子类的资源
void delelteChildByParentPoint(Parent * parent)
{
delete parent;
} int main()
{ Child *childTest = new Child(); //delete childTest; // 直接通过子类对象释放资源,不需要在父类写虚析构函数 return 0;
}

运行结果:

可以看到,子类和父类的对象都析构了。

2.1.2 通过父类的指针调用释放子类的资源,需要在父类写虚析构函数

示例代码如下所示:

// test4.cpp,用于验证为什么需要虚析构函数

#include <iostream>

using namespace std;

class Parent
{
public: Parent()
{
str = new char[20]; strcpy(str,"this is parent"); cout << "父类构造函数运行" << endl;
} ~Parent()
{
delete str; cout << "父类析构函数运行" << endl;
} private: char *str;
}; class Child : public Parent
{
public: Child()
{
str = new char[20]; strcpy(str,"this is Child"); cout << "子类构造函数运行" << endl;
} ~Child()
{
delete str; cout << "子类析构函数运行" << endl;
} private: char *str;
}; // 通过 父类指针 释放子类的资源
void delelteChildByParentPoint(Parent * parent)
{
delete parent;
} int main()
{ Child *childTest = new Child(); // 通过 父类指针 释放子类的资源
delelteChildByParentPoint(childTest); return 0;
}

运行结果:

这里可以看到,对象销毁时只调用了父类的析构函数。如果这时子类的析构函数中有关于内存释放的操作,将会造成内存泄露。所以需要给父类的析构函数加上 virtual。

2.2 虚析构函数的实现与例子

给父类的析构函数加上关键字 virtual,即可实现虚析构函数。

示例代码如下:


// test5.cpp,用于验证为什么需要虚析构函数 #include <iostream> using namespace std; class Parent
{
public: Parent()
{
str = new char[20]; strcpy(str,"this is parent"); cout << "父类构造函数运行" << endl;
} virtual ~Parent()
{
delete str; cout << "父类析构函数运行" << endl;
} private: char *str;
}; class Child : public Parent
{
public: Child()
{
str = new char[20]; strcpy(str,"this is Child"); cout << "子类构造函数运行" << endl;
} ~Child()
{
delete str; cout << "子类析构函数运行" << endl;
} private: char *str;
}; // 通过 父类指针 释放子类的资源
void delelteChildByParentPoint(Parent * parent)
{
delete parent;
} int main()
{ Child *childTest = new Child(); // 通过 父类指针 释放子类的资源
delelteChildByParentPoint(childTest); return 0;
}

运行结果:

可以看到,子类和父类的对象都析构了。

3 重载、重写、重定义

3.1 重载(添加)

重载(添加),特征如下:

  • 相同的范围(在同一个类中)

  • 函数名字相同

  • 参数不同

  • virtual 关键字可有可无

3.2 重写(覆盖)

重写(覆盖),是指派生类覆盖基类函数,特征如下:

  • 不同的范围,分别位于基类和派生类中

  • 函数的名字相同

  • 参数相同

  • 基类函数必须有 virtual 关键字

3.3 重定义(隐藏)

重定义(隐藏),是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

  • 如果派生类的函数和基类的函数同名,但是参数不同,此时不管基类有没有 virtual 关键字,基类的函数都会被隐藏

  • 如果派生类的函数和基类的函数同名,并且参数也相同,但是基类没有 virtual 关键字,基类的函数还是被隐藏。

6 纯虚函数和抽象类

6.1 基本概念

您想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到 纯虚函数

纯虚函数的语法:

virtual 类型 函数名(参数表) = 0;

一个具有纯虚函数的基类称为 抽象类。抽象类不能被实例化,其存在的意义就是被继承,提供子类的公共接口。

6.2 编程实践

代码如下所示:

#include <iostream>
#include <string> using namespace std; // 图形类
// 拥有纯虚函数的类, 就叫抽象类
class Shape
{
public:
// 是一个抽象的接口,说明图形是有一个得到面积方法
virtual double getArea() = 0; // 定义一个个打印面积的接口
virtual void print() = 0;
}; // 圆类
// 如果 一个子类继承了抽象类, 那么一定要重写这个纯虚函数。
class Circle :public Shape
{
public:
Circle(double r)
{
this->r = r;
} // 重写父类抽象类的纯虚函数
virtual double getArea()
{
return 3.14 * r * r;
} virtual void print() {
cout << "圆的面积是" << endl;
cout << this->getArea() << endl;
} private:
double r;// 半径
}; // 实现一个正方形
class Rect :public Shape
{
public:
Rect(double a)
{
this->a = a;
}
// 是一个抽象的接口,说明图形是有一个得到面积方法
virtual double getArea()
{
return a*a;
} // 一个打印面积的接口
virtual void print() {
cout << "正方形的面积是" << endl;
cout << this->getArea() << endl;
}
private:
double a;// 边长
}; // 三角形
class Tri :public Shape
{
public:
Tri(double a, double h)
{
this->a = a;
this->h = h;
}
virtual double getArea() {
return 0.5 * h * a;
} virtual void print() {
cout << "三角形的面积是" << endl;
cout << this->getArea() << endl;
} private:
double a;// 底
double h;// 高
}; // 一个传递抽象类 指针的函数
void printArea(Shape *p)
{
p->print();
} int main(void)
{
//Shape p;// 抽象类不能实例化 Shape *sp = new Circle(10.0);
printArea(sp);
delete sp; // 创建一个正方形的对象。用抽象类指针(父类指针)指向子类对象
sp = new Rect(10.0);
printArea(sp);
delete sp; Shape *sp2 = new Tri(10, 20);
sp2->print();
delete sp2; cout << " ------ " << endl; return 0;
}

运行结果:

7 面向接口编程

7.1 函数指针

函数指针 用于指向一个函数,函数名是函数体的入口地址。

7.1.1 传统形式 函数指针定义方法

  1. 直接定义一个函数指针: 函数返回值类型 (*函数指针变量名)(指向函数的参数类型列表);传统形式

  2. 通过函数类型定义函数指针: 函数类型 *函数指针变量名;这种方法与 typedef 联用,请参看 7.1.2 使用 typedef 定义 函数指针)

// 方法一:
// 直接定义一个函数指针
void (*fp)(int,int);

7.1.2 使用 typedef 定义函数指针

  1. 通过 typedef 定义一个函数类型,定义一个函数指针: typedef 函数返回值类型 (函数名)(函数的参数类型列表);

  2. 通过 typedef 定义一个函数指针类型,直接定义一个函数指针: typedef 函数返回值类型 (*函数指针变量名)(指向函数的参数类型列表);

示例如下:

// 方法一:
// 定义一个函数类型
typedef void (myfunctype)(int);
// 定义一个函数指针
myfunctype* fp1= NULL; // 7.1.1 中的方法二 通过函数类型定义函数指针 // 方法二:
// 定义一个函数指针类型
typedef void (*myfunctype_pointer)(int a,int b)
// 定义一个函数指针
myfunctype_pointer fp2 = NULL; // 7.1.1 中的方法二 通过函数类型定义函数指针

7.1.3 函数指针调用示例

// functionPointTest1.cpp,函数指针调用示例

#include <iostream>

using namespace std;

int (*fp_add)(int, int);		      // 1. 直接定义一个 函数指针(传统形式)
typedef int (myfunctype)(int, int); // 2. 使用 typedef 定义一个 函数类型
typedef int (*myfunctype_pointer)(int, int); // 3. 使用 typedef 定义一个 函数指针类型 int add(int a, int b)
{
return a + b;
} int main()
{
fp_add = add;
myfunctype *myfunctype_fp = add;
myfunctype_pointer myfunctype_pointer_fp = add; cout << fp_add(1, 1) << endl;
cout << myfunctype_fp(1, 1) << endl;
cout << myfunctype_pointer_fp(1, 1) << endl; return 0;
}

运行结果:

7.2 回调函数

当函数指针作为函数参数时,这就是回调函数。

示例代码如下:

// callbackTest.cpp,回调函数

#include <iostream>

using namespace std;

typedef int (*myfunctype_pointer)(int, int);  // 使用 typedef 定义一个 函数指针类型

int function(int a, int b)
{
cout << "function 函数被触发" << endl; return a + b;
} // 回调函数
void callback(myfunctype_pointer fp, int param1, int param2)
{
cout << "callback 回调函数被触发 " << endl; int total = -1; total = fp(param1, param2); cout << "total = " << total << endl;
} int main()
{
callback(function, 1, 1); return 0;
}

运行结果:

7.2.1 回调函数的本质

回调函数的本质:提前做了一个协议的规定,把函数的参数,函数返回值提前约定好。

如 7.2 的示例代码,我们可以不关心 function函数的实现 是怎样的,只要 function 函数的参数,函数返回值 与 回调函数的函数指针参数 一致就可以。函数 function 的代码做了修改,也可以不用改动 main 函数和回调函数代码,这样便于程序的维护和升级。

C++ 基础 5:多态的更多相关文章

  1. Java基础十一--多态

    Java基础十一--多态 一.多态定义 简单说:就是一个对象对应着不同类型. 多态在代码中的体现: 父类或者接口的引用指向其子类的对象. /* 对象的多态性. class 动物 {} class 猫 ...

  2. 五.OC基础--1.多态,2.类对象,3.点语法,4.@property&@synthesize,5.动态类型,内省(判断对象是否遵循特定的协议,以及是否可以响应特定的消息)

    五.OC基础--1.多态, 1. 多态概念,定义:多态就是某一类事物的多种形态: 表现形式: Animal *ani = [Dog new]; 多态条件:1.有继承关系 2.有方法的重写 2.多态代码 ...

  3. Java基础之多态和泛型浅析

    Java基础之多态和泛型浅析 一.前言: 楼主看了许多资料后,算是对多态和泛型有了一些浅显的理解,这里做一简单总结 二.什么是多态? 多态(Polymorphism)按字面的意思就是“多种状态”.在面 ...

  4. C#非常重要基础之多态

    前几天看了一位同志的博客,写的是关于他自己去支付宝面试的经历.过程大体是这样的:问答的时候,前面部分,作者都应答如流,说起自己经验如何之丰富,最后面试官问了作者一个问题:请简述多态的概念和作用.结果这 ...

  5. 30天C#基础巩固-----多态,工厂模式

         自己要有自信,相信自己可以找到好的工作.面对校招,企业更加看重自己的基础,这30天就把C#的基础好好的复习,学习下.笔记一定要认真的记录,这样自己复习回顾的时候就有了可以参考的东西了. 一: ...

  6. Objective-C基础之──多态

    Objective-C语言是面向对象的高级编程语言,因此,它具有面向对象编程所具有的一些特性,即:封装性.继承性和多态性. 今天介绍一下Objective-C中的多态性. 一.什么是多态 多态:不同对 ...

  7. Python基础之多态与多态性

    切记:不要将多态与多态性这二者混为一谈,只要分开,就会很明朗了. 一.多态 多态指的是一类事物有多种形态,(一个抽象类有多个子类,因而多态的概念依赖于继承). 比如:动物分为人类.狗类.猪类(在定义角 ...

  8. iOS开发Objective-C基础之──多态

    Objective-C语言是面向对象的高级编程语言,因此,它具有面向对象编程所具有的一些特性,即:封装性.继承性和多态性. 今天介绍一下Objective-C中的多态性. 一.什么是多态 多态:不同对 ...

  9. 【Java基础】多态

    首先先来个总结: 什么是多态 面向对象的三大特性:封装.继承.多态.从一定角度来看,封装和继承几乎都是为多态而准备的.这是我们最后一个概念,也是最重要的知识点. 多态的定义:指允许不同类的对象对同一消 ...

  10. java基础之 多态

    在面向对象编程(Object-Oriented Programming, OOP)中,多态机制无疑是其最具特色的功能,甚至可以说,不运用多态的编程不能称之为OOP.这也是为什么有人说,使用面向对象语言 ...

随机推荐

  1. 萌新学python

    python python安装 进入官网http://www.python.org/download/ 下载 我下的是3.6.6大家可以根据需要下载(3.x和2.x不兼容请小心) 之后安装就可以了 p ...

  2. android的adb命令整理

    adb.exe的路径在Android\Sdk\platform-tools 把这个路径加入到系统的path环境下. 先用usb连接设备,比如一台android手机 adb tcpip 5555 adb ...

  3. 2014年 实验二 B2C网上购物

    实验二 B2C网上购物 [实验目的] ⑴.熟悉虚拟银行和网上支付的应用 ⑵.熟悉并掌握消费者B2C网上购物和商家的销售处理 [实验条件] ⑴.个人计算机一台 ⑵.计算机通过局域网形式接入互联网 (3) ...

  4. python程序整理(2)

    # 写一个函数完成三次登陆功能: # 用户的用户名密码从一个文件register中取出. # register文件包含多个用户名,密码,用户名密码通过|隔开,每个人的用户名密码占用文件中一行. # 完 ...

  5. CentOS7克隆多个虚拟机

    VMware+centos7克隆虚拟机 步骤一:打开虚拟机,右键选中已经配置好的虚拟机,选择manage下面的clone选项.这里有一个需要注意的地方,就是虚拟机在启动或者挂起的状态下是不能clone ...

  6. HDU-1051 Wooden Sticks--线性动归(LIS)

    题目大意:有n根木棍(n<5000),每根木棍有一个长度l和重量w(l,w<10000),现在要对这些木头进行加工,加工有以下规则: 1.你需要1分钟来准备第一根木头. 2.如果下一根木头 ...

  7. linux时间校准 设置时间为上海时区

      [root@localhost log]# rm -f /etc/localtime [root@localhost log]# cp /usr/share/zoneinfo/Asia/Shang ...

  8. .Net Core单元测试规范

    .Net Core单元测试规范 一. 前言 为了有效提升代码质量,保证DevOps的顺利进行.将全面开始采用单元测试进行覆盖,届时单元测试将完全纳入 到完整的持续构建生命周期中做为第一道质量把控的门槛 ...

  9. Linux命令行bash的快捷键

    提升效率 锁屏 Ctrl + s 敲什么命令没反应,但是敲上去了,屏幕上不做任何反应 Ctrl + q 再解锁 例如: 先Ctrl + s 锁屏 然后在命令行敲入 [root@C8-1 ~]# rm ...

  10. spring-boot-route(二十一)quartz实现动态定时任务

    Quartz是一个定时任务的调度框架,涉及到的主要概念有以下几个: Scheduler:调度器,所有的调度都由它控制,所有的任务都由它管理. Job:任务,定义业务逻辑. JobDetail:基于Jo ...