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. [BJWC 2011]元素

    题目大意: 你有n个二元组(x,y),要求从中任取几个,使得x的值亦或起来不为0,且y之和最大. 题解: 显然是以x来构造线性基的,然而加入元素的个数是有限制的,那当然就是大的先来喽,排个序就OK啦! ...

  2. dockerfile关键字

    DockerFile关键字(保留字指令) FORM:基础镜像,表明当前镜像是基于那么镜像的 MAINTAINER :镜像维护者的名字和邮箱地址 RUN:容器构建时需要用到的命令 EXPOSE:当前容器 ...

  3. 微信小程序实时将less编译为wxss

    1.npm或者yarn全局安装wxss-cli npm install -g wxss-cli 2.运行waxes-cli命令(mp_wx为小程序目录) wxss ./mp_wx 实时监听mp_wx目 ...

  4. 解决VMware无法共享ubuntu虚拟机文件

    1.错误信息:无法更新运行时文件夹共享状态:在客户机操作系统内装载共享文件夹文件系统时出错 2.检查vmware tool是否正确安装 lsmod | grep vmhgfs modprobe vmh ...

  5. 不要以为Bug写的好就是好程序员,其实这只占不到15%!

      最近和一位从事多年架构工作的技术哥们见面,聊到了近期面试程序员的一些经历,谈到了"如何判断程序员水平高低"这个话题,颇有些感触,觉得有价值,因此花了些时间整理.分享给大家. 正 ...

  6. spring boot: 通过filter过滤器实现中文的简体繁体字符集转换(spring boot 2.3.1)

    一,为什么要使用filter来实现简繁体转换? 项目中有时会有同时支持简体和繁体两种字符集的要求, 或者搜索引擎有支持繁体输入字符的需求. 针对繁体字符的显示, 我们通常会在数据库和模板.文案配置中默 ...

  7. docker19.03限制容器使用的cpu资源

    一,用--cpus限制可用的cpu个数 例子: [root@localhost liuhongdi]# docker run -idt --name kafka1 --hostname kafka1 ...

  8. FDDB人脸检测数据集 生成ROC曲线

    看了好多博客,踩了很多坑,终于把FDDB数据集的ROC曲线绘制出来了.记录一下. 环境:ubuntu18.04 1.数据集准备 去FDDB官网:http://vis-www.cs.umass.edu/ ...

  9. filezilla pureftpd 读取目录列表失败

    放行   21, 39000 - 40000端口

  10. rsync 守护进程模式搭建 与常见报错

    守护进程模式搭建 1.环境准备 2.安装rsync(做备份的服务器都安装) [root@backup ~]# yum install -y rsync 3.服务端配置 [root@backup ~]# ...