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. 万万没想到!ModelArts与AppCube组CP了

    摘要:嘘,华为云内部都不知道的秘密玩法,我悄悄告诉您! 双"魔"合璧庆双节 ↑开局一张图,故事全靠编 华为云的一站式开发平台ModelArts和应用魔方AppCube居然能玩到一起 ...

  2. Docker笔记7:Docker 命令自动补齐

    经常大家会碰到这种现象,Docker 已经安装好了,但是使用 docker 命令时 不能自动补齐,即输入 docker 命令后,按 Tab 键无法列出子命令(或参数)的候选项. [机制] Linux ...

  3. 2440启动流程 <转载>

    韦东山 博客园 首页 订阅 管理 2440启动过程分析   2440启动过程分析 2440启动过程算是一个难点,不太容易理解,而对于2440启动过程的理解,影响了后面裸机代码执行流程的分析,从而看出2 ...

  4. 用ip xfrm搭ipsec隧道

    拓扑如下 基本的IP配置就不说了,直接写重点,在LS上配置 #配置SA ip xfrm state add src 194.168.10.4 dst 194.168.10.5 proto esp sp ...

  5. 【折半枚举+二分】POJ 3977 Subset

    题目内容 Vjudge链接 给你\(n\)个数,求出这\(n\)个数的一个非空子集,使子集中的数加和的绝对值最小,在此基础上子集中元素的个数应最小. 输入格式 输入含多组数据,每组数据有两行,第一行是 ...

  6. swoft运行流程

    启动命令 php bin/swoft http:start 或者  swoftctl run -c http:start 1 入口文件 bin/swoft.php #!/usr/bin/env php ...

  7. centos8上安装ffmpeg4.2.2并做视频截图

    一,ffmpeg的作用: FFmpeg是一套可以用来记录.转换数字音频.视频,并能将其转化为流的开源计算机程序. 它提供了录制.转换以及流化音视频的完整解决方案.它包含了非常先进的音频/视频编解码库l ...

  8. 【荐】JavaScript图片放大技术(放大镜)示例代码

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  9. 往with as中写入数据的方法

    方法1:直接写入,使用union all,简单直观,但程序运行效率低,几百条就很慢了 with dw_wms_outbound_info_v100 as( select '10700001' as o ...

  10. 4.QOpenGLWidget-对三角形进行纹理贴图、纹理叠加

    在上章3.QOpenGLWidget-通过着色器来渲染渐变三角形,我们为每个顶点添加颜色来增加图形的细节,从而创建出有趣的图像.但是,如果想让图形看起来更真实,我们就必须有足够多的顶点,从而指定足够多 ...