C++“多态性”的实现与详细解说(学习来源于---清华大学 郑莉老师){附源码}
多态(清华大学 郑莉老师)意义:
指的是操作接口具有表现多种形态的能力,即能根据操作系统的不同采用不同的处理方式
是面向对象系统的主要特征之一(抽象、继承、封装、多态)
在这样的系统中,一组具有相用基本语义的方法在同一个接口下为不同的对象服务
C++语言支持的多态性可以按其实现的时机分为:
编译时多态 运行时多态 (快慢)
多态的类型:重载多态、强制多态、包含多态、参数多态、
C++不但提供了固有的多态性,还提供了实现自定义多态性的手段
(绑定是指把一个标识符名和一个存储地址联系在一起的过程)
编译时多态(静态多态)
函数重载的理念、运算符重载、绑定工作在编译连接阶段完成的情况称为静态绑定。编译时多态是发生在模板和函数重载中(泛型编程)。
运行时多态(动态多态)
虚函数实现的多态,绑定工作在程序运行阶段完成的情况称为动态绑定。运行时多态发生在继承体系中,是指通过基类的指针或引用访问派生类中的虚函数。
问题引入:用“+”“-”能够实现复数的加减运算吗?
实现复数的加减运算的方法——重载:“+”、“-”运算符
运算符重载是对已有的运算符赋予多重含义,使得同一个运算符作用于不同类型的数据时导致产生不同的行为。从而实现多态
C++几乎可以重载全部的运算符,而且只允许重载C++中固有的运算符。
特殊情况: “.”和“.*"和 "::"和"?:"这四个不能进行重载运算符
重载后的运算符的优先级和结合性都不会改变,它是针对新的类型数据的实际需要,对原有的运算符进行适当的变化。
两种重载方式:
重载为类的非静态成员函数和重载为非成员函数(设计为友元函数)
声明形式
函数类型 operator运算符(形参)
{
......}
重载为类成员函数时:参数个数 = 原操作数个数-1(后置++、--除外)
重载为非成员函数时:参数个数 = 原操作数个数,且至少应该有一个自定义类型的形参。
例如:二元运算符B(双目运算符)
如果要重载B为类成员函数,使之能够实现表达式op1 B op2,
在其中op1为A类对象,则B应被重载为A类的成员函数,形参类型应该是op2所属的类型
经过重载以后,表达式op1 B op2 相当于 op1.operatorB(op2)
(表达式的返回结果就是这个函数的返回值)
例:复数类加减法运算重载-成员函数形式
将 “+”、“-”运算重载为复数类的成员函数。
规则:
将实部和虚部分别相加减
操作数:
两个操作数都是复数类的对象
#include <iostream>
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;//特殊:传引用比传指针速度快,相对数据不安全,加入const常量可以确保数据的安全。//运算符+重载成员函数Complex operator - (const Complex& c2) const;运算符-重载成员函数void display() const;//输出复数。display也会使数据不安全,所以也定义为constprivate: //私有数据成员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(5, 4), c2(2, 10), 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 0;
}
//后面可以尝试练习矩阵加减法
前置单目运算符U :如++ --
如果要重载U为类成员函数,使之可以实现表达式U opd,其中opd为A类对象,则U应被重载为A类的成语函数,无形参。
经重载后:
表达式U opd 相当于opd.operator U()
后置单目运算符++ --
如果要重载++或--为类成员函数,使其能够实现表达式opd++ 或者 opd-- ,其中opd为A类对象,则++ 或 -- 应被重载为A类的成员函数,且具有一个int类型形参
经过重载以后,表达式opd++ 相当于 opd.operator++(o)
例:
运算符前置++ 和后置++重载为时钟类的成员函数。
前置单目运算符,重载函数没有形参,对于后置单目运算符,重载函数需要有一个整型形参。
操作数是时钟类的对象,实现时间增加1秒钟。
#include <iostream>
using namespace std;
class Clock //时钟类定义
{
public: //外部接口Clock(int hour = 0,int minute = 0,int second = 0);void showTime() const;Clock& operator ++ (); //前置单目运算符重载Clock operator ++ (int);//后置单目运算符重载//注意:后置++不是&引用,前置++因为是先自增再使用参数,后置++是先传参,在自增。//前置可以使用&引用的值,后置则是直接使用的是Clock本身private://私有数据成员int hour, minute, second;
};Clock::Clock(int hour/*=0*/, int minute /*= 0*/, int second /*= 0*/)
{if (0 <= hour && hour < 24 && 0 < minute && minute < 60 && 0 <= second && second < 60){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 >= 60) {second -= 60;minute++;if (minute >= 60) {minute -= 60;hour = (hour + 1) % 24;}}return *this;
}Clock Clock::operator++ (int) { //后置单目运算符重载//注意形参表中的整型参数Clock old = *this;++(*this); //调用前置“++”运算符return old;
}int main() {Clock myClock(23, 59, 59);cout << "First time output: ";myClock.showTime();cout << "show myClock++:";(myClock++).showTime();cout << "show ++myClock:";(++myClock).showTime();return 0;
}
运行结果:
First time output:23:59:59show myClock++: 23:59:59show ++myClock: 0: 0:1
运算符重载为非成员函数(友元函数)参数个数和操作数个数一样,同时后置多一个0作为参数,用于区分。
解决什么问题?例如刚刚 不写两个复数相加,写实数+复数相加时怎么办?
双目运算符B重载后,
表达式opd1 B opd2 等同于 operator B(opd1,opd2)
前置单目运算符B重载以后,
表达式:B opd 等同于: operator B(opd)
后置单目运算符++和--重载后,
表达式:opd B 等同于: operator B(opd,0)
加减法运算 和 “<<” 运算符
将+、-(双目)重载为非成员函数,并将其声明为复数类的友元函数,两个操作数都是复数类的常引用。
将<<(双目)重载为非成员函数,并将其声明为复数类的友元,它的左操作数为复数类的常引用,返回std::ostream引用
用以支持下面形式的输出:
cout << a << b;
//该输出调用的是:operator << (operator << (cout,a0),b)
#include <iostream>
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) {//重载运算符实现out << "(" << c.real << "," << c.imag << ")";return out;
}int main() {Complex c1(5, 4), c2(2,10), 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 0;
}
---------------------------------------------------------------------------------------------------------------------------------
下面是实现动态多态性(虚函数形式)
C++中引入了虚函数的机制在派生类中可以对基类中的成员函数进行覆盖(重定义)
虚函数的声明
Virtual 函数类型 函数名(形参表)
{
函数体;
}
class Base1 //基类Base1定义
{
public:virtual void display() const;//虚函数
};
void Base1::display() const {cout << "Base1::display()" << endl;
} class Base2:public Base1 { //公有派生类Base2
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 << "Base2::display()" << endl;
}void fun(Base1* ptr) { //参数为指向基类对象的指针ptr->display();//"对象指针->成员名"
}
int main() {Base1 base1; //定义Base1类对象Base2 base2; //定义Base2类对象Derived derived; //定义Derived类对象fun(&base1); //用Base1对象的指针调用fun函数fun(&base2); //用Base2对象的指针调用fun函数fun(&derived);//用Derived对象的指针调用fun函数return 0;
}
//运行结果
/*Base1::display()Base2::display()Base2::display()*/
这里讲一下虚函数表、虚表指针的虚函数实现机制的底层原理。
虚函数通过虚函数表来实现,虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中保存了指向虚函数表的指针(虚表指针,无法显示调用),通过虚表指针可以找到对应类所对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这个虚函数表就指明了实际需要调用的函数。
虚函数表:
虚函数表内存放的是类的虚函数的地址。
建立时间是在程序编译阶段,编译过程中会把虚函数的地址存放到一张虚函数表中。
指向虚函数表的指针存放在定义对象的类的内存空间地址的最前面位置,也是为了保证正确的取到虚函数的偏移量。
接着引入 “虚析构函数” 的概念
虚析构函数(借用老师的一些话,析构函数,当你为你的函数定义时增加了析构函数,就表明你在你的程序结束时,在你这个函数调用结束的时候,一定有重要的事情要做。)
为什么需要虚析构函数?在析构函数前加virtual
1.可能通过基类指针删除派生类对象;
2.如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),就需要让基类的析构函数成为虚函数,否则制行delete的结果是不确定的。
#include <iostream>
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(0);
}
Derived::~Derived() {cout << "Derived destructor" << endl;delete p;
}
void fun(Base* b) {delete b;
}
int main() {Base* b = new Derived();fun(b);return 0;
}
纯虚函数是一个在基类中声明的虚函数,它在该类基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:
virtual 函数类型 函数名(参数表)= 0;
带有纯虚函数的类称为抽象类:
class 类名
{
virtual 类型 函数名(参数表) = 0; //纯虚函数
......
}
例:抽象类举例
如例题,Base1就成为了抽象类不能生成实例的,只能作为基类来使用。
这个没有实现的纯虚函数,要由其派生类实现,如果派生类不实现这个函数,那么派生类继续变成抽象类。
#include <iostream>
using namespace std;
class Base1 {//基类
public:virtual void display() const = 0;//纯虚函数
};
class Base2: public Base1 {//公有派生类Base2
public:void display() const; //覆盖基类的虚函数
};
void Base2::display() const {cout << "Base2::display()" << endl;
}
//这里Base2就实现了派生类的函数class Derived :public Base2 { //公有派生类Derived定义
public:void display() const; //覆盖基类的虚函数
};
void Derived::display() const {cout << "Derived::display()" << endl;
}void fun(Base1* ptr) { //参数为指向基类对象的指针ptr->display(); //对象指针->成员名
}int main() {Base2 base2; //定义Base2类对象Derived derived; //定义Derived对象fun(&base2); //用Base2对象的指针调用fun函数fun(&derived); //用Derived对象的指针调用fun函数return 0;
}
运行结果:
Base2::display()
Derived::display()
这里我们看到实现出来一个多态的结果
好啦!以上就是我们分享的一个关于C++多态的知识点,感兴趣的小伙伴欢迎评论区留言,给予我一些意见建议、或者有什么问题可以一起讨论。
声明:例题来源于《C++程序设计》第4版 郑莉 清华大学
C++“多态性”的实现与详细解说(学习来源于---清华大学 郑莉老师){附源码}相关推荐
- 手把手!基于领域预训练和对比学习SimCSE的语义检索(附源码)
之前看到有同学问,希望看一些偏实践,特别是带源码的那种,安排!今天就手把手带大家完成一个基于领域预训练和对比学习SimCSE的语义检索小系统. 所谓语义检索(也称基于向量的检索),是指检索系统不再拘泥 ...
- 基于 Android NDK 的学习之旅-----Java 调用C(附源码)
基于 Android NDK 的学习之旅-----Java 调用C 随便谈谈为什么要Java调用C 吧: 我认为: 1. 有些公司开发Android项目的时候, 许多组件功能可能是C中已经实现了,所 ...
- (微信小程序毕业设计)小学生语文学习打卡项目开发实例(附源码+论文)
大家好!我是岛上程序猿,感谢您阅读本文,欢迎一键三连哦.
- 郑莉老师c++第五版+b站视频 学习笔记
本文是系统学习 郑莉c++第五版+b站视频的学习笔记. 郑莉老师b站内容分为 导学 文章目录 第0章:学习怎么学习--论道开篇+怎样学c++ 论道开篇 怎样学c++ 第1章绪论 导学笔记(学堂在线+b ...
- ios开发学习-手势交互(Gesture)效果源码分享
qianqianlianmeng ios开发学习-手势交互(Gesture)效果源码分享 All Around Pull View 介绍:实现视图四个方向(上下左右)都能够拖动更新(pull to r ...
- java中batch基础_详解Spring batch 入门学习教程(附源码)
详解Spring batch 入门学习教程(附源码) 发布时间:2020-09-08 00:28:40 来源:脚本之家 阅读:99 作者:achuo Spring batch 是一个开源的批处理框架. ...
- glibc-2.23学习笔记(二)—— free部分源码分析
glibc-2.23学习笔记(二)-- free部分源码分析 _libc_free _int_free 函数定义 局部变量 start fast bins部分 unsorted bins部分 mmap ...
- glibc-2.23学习笔记(一)—— malloc部分源码分析
glibc-2.23学习笔记(一)-- malloc部分源码分析 搭建Glibc源码调试环境 1.下载并解压glibc源码 2.配置gdb 3.编译测试程序 第一次调用 源码分析 __libc_mal ...
- 【附源码】计算机毕业设计JAVA智友少儿编程学习平台
[附源码]计算机毕业设计JAVA智友少儿编程学习平台 目运行 环境项配置: Jdk1.8 + Tomcat8.5 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(In ...
最新文章
- 聊聊SwitchUserFilter的使用
- C# 断点续传原理与实现
- Codeforces Round #766 (Div. 2) D. Not Adding 数学gcd
- python 创建类_python 用type()创建类
- js整体缩小网页_SEO网页优化的原则是什么?
- javaWeb RSA加密使用
- fishhook源码分析
- python读音-Python怎么读
- 从零开始搭二维激光SLAM --- 基于gtsam的后端优化的代码实现
- 关于坑爹的编解码问题
- 苹果手机上编辑html文件夹,苹果手机可以编辑EXCEL文件吗
- supermap idesktop 许可更新方案
- UOS 操作蓝牙、wifi开关
- JavaScript 简单 登录验证 固定账号密码
- 指令周期,机器周期(CPU周期),时钟周期 关系
- 数据可视化~matplotlib阶梯图,直方图
- Python-文件夹的拷贝操作
- 微软裁员新招:积极离职者奖诺基亚手机
- openflow交换机 ryu_ryu(示例代码)
- 西安临潼秦始皇陵、地宫、八大奇迹馆、骊山一日游