C++ 基础 5:多态
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 多态成立的条件
要有继承
要有虚函数重写
要有父类指针(或父类引用)指向子类对象
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 传统形式 函数指针定义方法
直接定义一个函数指针:
函数返回值类型 (*函数指针变量名)(指向函数的参数类型列表);
(传统形式)通过函数类型定义函数指针:
函数类型 *函数指针变量名;
(这种方法与 typedef 联用,请参看 7.1.2 使用 typedef 定义 函数指针)
// 方法一:
// 直接定义一个函数指针
void (*fp)(int,int);
7.1.2 使用 typedef 定义函数指针
通过 typedef 定义一个函数类型,定义一个函数指针:
typedef 函数返回值类型 (函数名)(函数的参数类型列表);
通过 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:多态的更多相关文章
- Java基础十一--多态
Java基础十一--多态 一.多态定义 简单说:就是一个对象对应着不同类型. 多态在代码中的体现: 父类或者接口的引用指向其子类的对象. /* 对象的多态性. class 动物 {} class 猫 ...
- 五.OC基础--1.多态,2.类对象,3.点语法,4.@property&@synthesize,5.动态类型,内省(判断对象是否遵循特定的协议,以及是否可以响应特定的消息)
五.OC基础--1.多态, 1. 多态概念,定义:多态就是某一类事物的多种形态: 表现形式: Animal *ani = [Dog new]; 多态条件:1.有继承关系 2.有方法的重写 2.多态代码 ...
- Java基础之多态和泛型浅析
Java基础之多态和泛型浅析 一.前言: 楼主看了许多资料后,算是对多态和泛型有了一些浅显的理解,这里做一简单总结 二.什么是多态? 多态(Polymorphism)按字面的意思就是“多种状态”.在面 ...
- C#非常重要基础之多态
前几天看了一位同志的博客,写的是关于他自己去支付宝面试的经历.过程大体是这样的:问答的时候,前面部分,作者都应答如流,说起自己经验如何之丰富,最后面试官问了作者一个问题:请简述多态的概念和作用.结果这 ...
- 30天C#基础巩固-----多态,工厂模式
自己要有自信,相信自己可以找到好的工作.面对校招,企业更加看重自己的基础,这30天就把C#的基础好好的复习,学习下.笔记一定要认真的记录,这样自己复习回顾的时候就有了可以参考的东西了. 一: ...
- Objective-C基础之──多态
Objective-C语言是面向对象的高级编程语言,因此,它具有面向对象编程所具有的一些特性,即:封装性.继承性和多态性. 今天介绍一下Objective-C中的多态性. 一.什么是多态 多态:不同对 ...
- Python基础之多态与多态性
切记:不要将多态与多态性这二者混为一谈,只要分开,就会很明朗了. 一.多态 多态指的是一类事物有多种形态,(一个抽象类有多个子类,因而多态的概念依赖于继承). 比如:动物分为人类.狗类.猪类(在定义角 ...
- iOS开发Objective-C基础之──多态
Objective-C语言是面向对象的高级编程语言,因此,它具有面向对象编程所具有的一些特性,即:封装性.继承性和多态性. 今天介绍一下Objective-C中的多态性. 一.什么是多态 多态:不同对 ...
- 【Java基础】多态
首先先来个总结: 什么是多态 面向对象的三大特性:封装.继承.多态.从一定角度来看,封装和继承几乎都是为多态而准备的.这是我们最后一个概念,也是最重要的知识点. 多态的定义:指允许不同类的对象对同一消 ...
- java基础之 多态
在面向对象编程(Object-Oriented Programming, OOP)中,多态机制无疑是其最具特色的功能,甚至可以说,不运用多态的编程不能称之为OOP.这也是为什么有人说,使用面向对象语言 ...
随机推荐
- redis 开启AOF
找到redis 安装目录 例如 cd /usr/local/redis 打开 redis.conf 修改以下参数: # vi /usr/local/redis/etc/redis.conf appe ...
- Verilog基础入门——简单的语句块编写(二)
[题干] [代码] module top_module( input a, input b, output out ); assign out = a&b; endmodule [与或非门等] ...
- 可变数据类型不能作为python函数的参数
可变数据类型:列表.字典 不可变数据类型:整型.浮点型.字符串.元组 为什么可变数据类型不能作为python函数的参数?请看以下例子: def foo(a=[]): a.append(1) retur ...
- selenium分布式启动(deepin)
1.deepin安装jdk: 下载地址:链接:https://pan.baidu.com/s/19-pU8G6RzMW92uBCxBH7sA 密码:1c7n 解压:tar -zxvf jdk-8u20 ...
- Linux用户和组管理命令-用户删除userdel
删除用户 userdel 可删除Linux 用户 格式: userdel [OPTION]... Login 常见选项: -f, --force 强制 -r, --remove 删除用户家目录和邮箱 ...
- pytest参数化代码笔记
#!/usr/local/bin/python3 # -*- coding: utf-8 -*- import pytest __author__ = "Carp-Li" __da ...
- Azure Kay Vault(一).NET Core Console App 获取密钥保管库中的机密信息
一,引言 Azure 密钥保管库用于存储敏感信息,例如链接字符串,密码,API 密钥等.我们无法直接从Azure 密钥库中访问机密!那么我们如何才能访问应用程序中的机密信息?比如,在我们的实际项目中, ...
- 蓝桥杯2020 E:七段码
题解 正规解法是 dfs + 并查集,首先用 dfs 将其所有的情况枚举出来,再用并查集来判断是否在一个连通块上. 许多小伙伴计算的答案为76,主要是判断连通块这方面有问题,倘若不用并查集,直接枚举一 ...
- Python ( 学习基础篇 第二部 )
目录 运算符 算数运算符 比较运算符 赋值运算符 位运算符 逻辑运算符 成员运算符 身份运算符 Python 中运算符的优先级 运算符总结基础语法 判断类型 isinstence 代码块 流程控制 w ...
- 「IDEA插件精选」安利一个IDEA骚操作:一键生成方法的序列图
在平时的学习/工作中,我们会经常面临如下场景: 阅读别人的代码 阅读框架源码 阅读自己很久之前写的代码. 千万不要觉得工作就是单纯写代码,实际工作中,你会发现你的大部分时间实际都花在了阅读和理解已有代 ...