C++(多态性)

news/2024/9/17 7:11:29/ 标签: c++, 开发语言

多态

        多态是指同样的消息被不同类型的对象接收时导致不同的行为。所谓消息是指对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。

        最简单的例子就是使用同样的运算符'+',可以实现整数与整数之间,浮点数与浮点数之间的加法运算。

多态的类型

        面向对象的多态性可以分为4类:重载多态﹑强制多态、包含多态和参数多态。

        重载多态:

        包含多态:

        强制多态:

        参数多态:

多态的实现

        两类:编译时多态(早绑定,模板类,函数重载)与运行时多态(晚绑定,继承,虚函数)

        确定操作的具体对象的过程:绑定( binding):绑定是指计算机程序自身彼此关联的过程,也就是把一个标识符名和一个存储地址联系在一起的过程;用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。

        绑定是指计算机程序自身彼此关联的过程,也就是把一-个标识符名和一个存储地址联系在一起的过程;用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。

运算符重载

        运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为。

        运算符重载的实质就是函数重载。在实现过程中,首先把指定的运算表达式转化为对运算符函数的调用,将运算对象转化为运算符函数的实参,然后根据实参的类型来确定需要调用的函数,这个过程是在编译过程中完成的。

运算符重载规则:

        1.只能重载C++已有的运算符,且重载之后运算符的优先级和结合性都不会改变。

        2.不能改变原操作符的操作对象个数,且至少有一个操作对象是自定义类型

        3.不能重载的运算符,类属关系运算符“.”,成员指针运算符“.*”,作用域标识符“::”,与三木运算符“?:”,以及sizeof

语法形式:

返回类型 operator 运算符(形参表)
{函数体
)

       返回类型指定了重载运算符的返回值类型,也就是运算结果类型, operator是定义运算符重载函数的关键字,运算符即是要重载的运算符名称,必须是C++中可重载的运算符,比如要重载加法运算符,这里就写“+”,形参表中给出重载运算符所需要的参数和类型。

        注:当以非成员函数形式重载运算符时,有时需要访问运算符参数所涉及类的私有成员,这时可以把该函数声明为类的友元函数。       

运算符重载为成员函数:

        对于双目运算符B,如果要重载为类的成员函数,使之能够实现表达式opr1 B opr2,其中 opr1为A类的对象,则应当把B重载为A类的成员函数,该函数只有一个形参,形参的类型是oprd2所属的类型。经过重载之后﹐表达式oprdl B oprd2就相当于函数调用oprd1.operator B(oprd2)。

        对于后置运算符“++”和“- -”,如果要将它们重载为类的成员函数,用来实现表达式oprd++或oprd- -,其中oprd 为A类的对象,那么运算符就应当重载为A类的成员函数,这时函数要带有一个整型( int)形参。重载之后,表达式oprd++和 oprd- - 就相当于函数调用oprd.operator++(0)和oprd. operator- - (0)。这里的int类型参数在运算中不起任何作用,只是用于区别后置++,- -与前置++,- -。

#include <iostream>using namespace std;
class Cormpiex {
//复数类定义
//外部接口
public:Complex (double r=o.0,double i=0.0) : real(r), imag(i) f }//构造函数Complex operator+ (const Complex &c2)const;//运算符+重载成员函数Complex operator- (const Corplex &c2) const;//运算符-重载成员函数void display () const;//输出复数
private://复数实部double real;//复数虚部double imag;
}complex Complex :: operator+ (const Complex &c2) const {//重载运算符函数实现return Complex(real+c2.real,imag+ c2.imag);//创建一个临时无名对象作为返回值
}
Complex Complex :: operator- (const Complex sc2) 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=- "; cl.display () ;cout<< "C2="; c2.display {) ;c3=cl-c2;//使用重载运算符完成复数减法cout<< "c3=cl- c2="; c3.display();c3=c1+c2;//使用重载运算符完成复数加法cout<< "c3=cl+c2="; c3.display() ;return 0;
}

前置++与后置++运算符重载:

Clock operation++();   //前置++
Clock operation++(int);    //后置++

虚函数

        虚函数是动态绑定的基础。虚函数必须是非静态的成员函数。虚函数经过派生之后,在类族中就可以实现运行过程中的多态(如果基类指针指向派生类对象,那么此时指向的对象可以不再被认定为基类,而是派生类)。

一般虚函数成员

virtual 函数类型 函数名(形参表);

        这实际上就是在类的定义中使用virtual关键字来限定成员函数,虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
        运行过程中的多态需要满足3个条件,第一是类之间满足赋值兼容规则(如果一个类型的每个数据成员都可以赋值给另一个类型的一个数据成员,那么这两个类型是赋值兼容的。,第二是要声明虚函数,第三是要由成员函数来调用或者是通过指针、引用来访问虚函数。如果是使用对象名来访问虚函数﹐则绑定在编译过程中就可以进行(静态绑定),而无须在运行过程中进行。
        注:虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的,所以虚函数一般不能以内联函数处理。但将虚函数声明为内联函数也不会引起错误。

class Basel {
//基类Base1定义
public:virtual void display() const;//虚函数
};void Base1 :: display () const {cout<<"Base1 ::display()"<<endl; 
}class Base2 : public Basel {
//公有派生类Base2定义
public:void display() const;//覆盖基类的虚函数
};void Base2 :: display () const {cout<<"Base2 : :display i)"<<endl;
}

        如果从名称,参数及返回值3个方面检查之后,派生类的函数满足了上述条件,就会自动确定为虚函数(与一般的重载不同,一般的重载只检查名称与参数,不检查返回值)。这时,派生类的虚函数便覆盖了基类的虚函数。不仅如此,派生类中的虚函数还会隐藏基类中同名函数的所有其他重载形式。

        注:派生类指针使用::的方式仍能够访问基类的虚函数,ptr->Base1.display()

        习惯:在派生类中重载虚函数时,虽然使不使用virtual关键字它都是一个虚函数,但是为了使代码的可读性提高,一般加上virtual关键字

虚析构函数

        虽然不能声明虚构造函数,但是可以声明虚析构函数

        一般声明形式:

        virtual ~ 类名();

        析构函数设置为虚函数之后,在使用指针引用时可以动态绑定,实现运行时的多态,保证使用基类类型的指针就能够调用适当的析构函数针对不同的对象进行清理工作。
        简单来说,如果有可能通过基类指针调用对象的析构函数(通过delete,相当于C语言的free),就需要让基类的析构函数成为虚函数,否则会产生不确定的后果。

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(O) ;
)
Derived :: ~ Derived () {cout << "Derived destructor" << endl ;delete p;
}void fun(Base *b){delete b;
}int main(){Base *b = new Derived();fun(b);return 0;
}

输出信息:

Base destructor

        此时基类指针删除派生类对象调用的是基类的析构函数,造成了内存泄漏,此时,应该把析构函数声明为虚函数

class Base {
public:virtual ~Base ( );
};

此时输出信息

Base destructor
Derived destructor

派生类析构函数执行顺序:基类-->派生类

派生类构造函数执行顺序:派生类-->基类

纯虚函数和抽象类

抽象类:

        抽象类是一种特殊的类,抽象类是带有纯虚函数的类,它为一个类族提供统一的操作界面。可以说,建立抽象类,就是为了通过它多态地使用其中的成员函数。抽象类处于类层次的上层,一个抽象类自身无法实例化,也就是说我们无法定义一个抽象类的对象﹐只能通过继承机制,生成抽象类的非抽象派生类,然后再实例化

纯虚函数:

        纯虚函数声明形式:virtual 函数类型 函数名(参数表)= 0;

        纯虚函数与一般虚函数成员的原型在书写格式上的不同就在于后面加了“= 0”。声明为纯虚函数之后,基类中就可以不再给出函数的实现部分。但是此时派生类必须写出纯虚函数的函数体

注:

        1.基类中仍然允许对纯虚函数给出实现,但即使给出实现,也必须由派生类覆盖,否则无法实例化。

        在基类中对纯虚函数定义的函数体的调用,必须通过“基类名∵函数名(参数表)"的形式。如果将析构函数声明为纯虚函数,必须给出它的实现,因为派生类的析构函数体执行完后需要调用基类的纯虚函数。

        2.纯虚函数不同于函数体为空的虚函数,纯虚函数根本就没有函数体,而空的虚函数的函数体为空;前者所在的类是抽象类,不能直接进行实例化,而后者所在的类是可以实例化的。它们共同的特点是都可以派生出新的类﹐然后在新类中给出虚函数新的实现,而且这种新的实现可以具有多态特征。

抽象类

        抽象类就是带有纯虚函数的类。抽象类的主要作用是通过它为一个类族建立一个公共的接口,使它们能够更有效地发挥多态特性。抽象类声明了一个类族派生类的共同接口,而接口的完整实现,即纯虚函数的函数体,需要由派生类自己定义。

        抽象类派生出新的类之后,如果派生类给出所有纯虚函数的函数实现,这个派生类就可以定义自己的对象,因而不再是抽象类;反之,如果派生类没有给出全部纯虚函数的实现,这时的派生类仍然是-个抽象类。也就是说,派生类需要给出所有纯虚函数的实现,否则无法实例化

        虽然抽象类不能实例化,但是可以定义一个抽象类的指针,来访问派生类对象,进而访问派生类成员

#include <iostream>
using namespace std;class Base1  //基类Base定义
{public:virtual void display() const = 0;  //纯虚函数
};class Base2: public Base1  //共有派生类Base2定义
{public:void display() const;  //覆盖基类的虚函数
};
void Base2::display() const
{cout << "Base2::display()" << endl;
}class Derived: public Base2  //公有派生类Derived定义
{public:void display() const;  //覆盖基类的虚函数
}
void Derived::display() const
{cout << "Derived::display()" << endl;
}viod fun(Base1 *ptr) //参数为指向基类对象的指针
{ptr->display();  //对象指针 -> 成员名
}int main()
{Base2 base2;  Derived derived;fun(&base2);fun(&derived);return 0;
}

运行结果:

Base2::display()
Derived::display()

        在fun函数中通过基类Basel的指针 ptr就可以访问到ptr指向的派生类Base2和Derived类对象的成员。这样就实现了同一类组的对象进行统一的多态处理

函数模板与类模板

        模板:通过它可以实现参数化多态性。所谓参数化多态性,就是将程序所处理的对象的类型参数化,使得一段程序可以用于处理多种不同类型的对象。

函数模版

函数模板的定义形式:

template <模板参数表>
类型名 函数名(参数表)
{函数体的定义
}

        所有函数模板的定义都是用关键字template开始的,该关键字之后是使用尖括号<>括起来的“模板参数表”。模板参数表由用逗号分隔的模板参数构成,可以包括以下内容。

        1.class(或typename)标识符,指明可以接收一个类型参数,这些类型参数代表的是类型,可以是内部类型或自定义类型

        2.“类型说明符”标识符,指明可以接收一个由“类型说明符”所规定类型的常量作为参数。

        3.template<参数表>class标识符,指明可以接收一个类模板名作为参数。

        类型参数可以用来指定函数模板本身的形参类型、返回值类型,以及声明函数中的局部变量。函数模板中函数体的定义方式与定义普通函数类似。

#include <iostream>
using namespace std;
template <typename T>
T abs(T t)
{return x<0 ? -x : x;
}int main()
{int n = -5;double d = -5.5;cout << abs(n) << endl;cout << abs(d) << endl;return 0;
}

此时输出:

5
5.5

类模板

        使用类模板使用户可以为类定义一种模式,使得类中的某些数据成员、某些成员函数的参数、返回值或局部变量能取任意类型(包括系统预定义的和用户自定义的)。

        类是对一组对象的公共性质的抽象,而类模板则是对不同类的公共性质的抽象,因此类模板是属于更高层次的抽象。由于类模板需要一种或多种类型参数,所以类模板也常常称为参数化类。

类模板声明的一般形式:

template<模板参数表>
class 类名
{类成员声明
}

类模板外定义:

template <模板参数表>
类型名 类名<模板参数标识符列表>::函数名(参数列表)

使用模板类创建对象:

模板名<模板参数表>对象名1,...,对象名n

模板类应用:

分析:在本实例中,声明一个实现任意类型数据存取的类模板Store,然后通过具体数据类型参数对类模板进行实例化,生成类,然后类再被实例化生成对象s1,s2,s3和d.不过类模板的实例化过程(如图9-5所示)在程序中是隐藏的。

#include <iostream>
#include <cstdlib>
using namespace std;struct Student
{int id;   //学号float gpa;   //平均分
};template<class T>
class Store
{
public:Store();    //无参构造函数T &getElem();    //提取数据函数void putElem();    //存入数据函数
private:T item;    //用于存放各种类型的数据bool haveValue;  //用于标记item是否存入内容
};//以下实现各成员函数
template<classT>    //默认构造函数的实现
Store<T>::Store():haveValue(false){}template<class T>    //提取数据函数的实现
T &Store<T>::getElem()
{if(!haveValue){cout<<"No item present!"<<endl;    //如果试图提取未初始化的数据,则终止程序exit(1);    //使程序完全退出,返回到操作系统//参数可用来表示程序终止的原因,可以被操作系统接收}return item;    返回item中存放的数据
}template<class T>    //存人数据函数的实现
void store<T>::putElem(const T &x)
{haveValue=true;    //将haveValue置为true,表示item中已存人数值item=x;    //将x的值存入item
}int main()
{Store<int>s1,s2;    //定义两个Store<int>类对象,其中数据成员item为int类型s1.putElem(3);    //向对象s1中存入数据(初始化对象s1)s2.putElem(-7);    //向对象s2中存人数据(初始化对象s2)cout<<sl.getElem()<<""<<s2.getElem()<<endl;    //输出对象s1和s2的数据成员Student g={1000,23};    //定义student类型结构体变量的同时赋予初值Store<Student>s3;     //定义 Store<student>类对象s3,其中数据成员item为Student类型   s3.putElem(g);    //向对象s3中存人数据(初始化对象s3)cout<<"The student id is "<<s3.getElem().id<<endl;    //输出对象s3的数据成员Store<double>d;//定义 store<doubie>类对象 d,其中数据成员 item为 double类型   cout<<"Retrieving obiectd...";cout<<d.getElem()<<endl;//输出对象d的数据成员    //由于d未经初始化,在执行函数d.getElement()过程中导致程序终止return 0;
}

运行结果:

3 -7
The student idis 1000
Retrieving object d… No item present!


http://www.ppmy.cn/news/1522471.html

相关文章

前端基础面试题·第一篇——HTML

1 .HTML标签头部< !DOCTYPE html> 的作用 DOCTYPE 使 document type的缩写&#xff0c;是html文档的类型声明&#xff0c;告诉浏览器文档的类型&#xff0c;便于解析文档。 这里会涉及到浏览器渲染页面的两种形式&#xff1a; CSS1 Compatible Mode(标准模式): 浏览器使…

Nginx反向代理功能及动静分离实现

一&#xff1a;Nginx支持正向代理和反向代理 1.正向代理 正向代理&#xff0c;指的是通过代理服务器 代理浏览器/客户端去重定向请求访问到目标服务器 的一种代理服务。 正向代理服务的特点是代理服务器 代理的对象是浏览器/客户端&#xff0c;也就是对于目标服务器 来说浏览…

共享内存和信号量

共享内存和信号量可以配合起来一起使用。 什么是共享内存&#xff1f;&#xff1a; 共享内存就是映射一段能被其他进程所访问的内存&#xff0c;这段共享内存由一个进程创建&#xff0c;但多个进程都可以访问。共享内存是最快的IPC方式&#xff0c;它是针对其他进程间通信方式…

漫谈设计模式 [8]:装饰器模式

引导性开场 菜鸟&#xff1a;老鸟&#xff0c;我最近在项目中遇到一个问题。有些功能&#xff0c;比如日志记录和权限校验&#xff0c;我需要在多个地方使用。代码很冗余&#xff0c;不知道有没有更好的解决办法&#xff1f; 老鸟&#xff1a;菜鸟&#xff0c;这个问题很常见…

FaskAPI Web学习

FaskAPI Web学习 个人笔记使用&#xff0c;感谢阅读&#xff01; # -*- ecoding: utf-8 -*- # Author: SuperLong # Email: miu_zxl163.com # Time: 2024/9/7 11:37 from enum import Enum from typing import Optionalfrom fastapi import FastAPI import uvicorn app FastA…

log4j 多classloader重复加载配置问题解决

最近OneCoder在开发隔离任务运行的沙箱&#xff0c;用于隔离用户不同任务间以及任务和 框架本身运行代码的隔离和解决潜在的jar包冲突问题。 运行发现&#xff0c;隔离的任务正常运行&#xff0c;但是却没有任何日志记录。从控制台可看到如下错误信息&#xff1a; 全文详见个人…

从零开始学习JVM(七)- StringTable字符串常量池

1 概述 String应该是Java使用最多的类吧&#xff0c;很少有Java程序没有使用到String的。在Java中创建对象是一件挺耗费性能的事&#xff0c;而且我们又经常使用相同的String对象&#xff0c;那么创建这些相同的对象不是白白浪费性能吗。所以就有了StringTable这一特殊的存在&…

Spark2.x 入门:逻辑回归分类器

方法简介 逻辑斯蒂回归&#xff08;logistic regression&#xff09;是统计学习中的经典分类方法&#xff0c;属于对数线性模型。logistic回归的因变量可以是二分类的&#xff0c;也可以是多分类的。 示例代码 我们以iris数据集&#xff08;iris&#xff09;为例进行分析。i…

Spring AOP的注解式开发实现

目录 AOP常用注解注解开发实现步骤1. 导入Maven项目依赖2. 准备一个实体类&#xff08;先定义接口再实现&#xff09;3. 定义切面类4. 准备配置文件5. 编写测试类 注意事项 AOP的配置开发方式见&#xff1a;Spring入门之AOP&#xff08;包含实例代码&#xff09;。其他纯注解开…

记忆化搜索【下】

375. 猜数字大小II 题目分析 题目链接&#xff1a;375. 猜数字大小 II - 力扣&#xff08;LeetCode&#xff09; 题目比较长&#xff0c;大致意思就是给一个数&#xff0c;比如说10&#xff0c;定的数字是7&#xff0c;让我们在[1, 10]这个区间猜。 如果猜大或猜小都会说明…

Spring Boot之DevTools介绍

Spring Boot DevTools 是 Spring Boot 提供的一组易于使用的工具&#xff0c;旨在加速开发和测试过程。它通过提供一系列实用的功能&#xff0c;如自动重启、实时属性更新、依赖项的热替换等&#xff0c;极大地提高了开发者的开发效率。本文将详细介绍 Spring Boot DevTools 的…

RLVF:避免过度泛化地从口头反馈中学习

人工智能咨询培训老师叶梓 转载标明出处 大模型在不同行业和个人中的广泛应用要求模型能够根据具体的用户反馈进行调整或定制&#xff0c;以满足细微的要求和偏好。虽然通过高层次的口头反馈来指定模型调整非常方便&#xff0c;例如“在给老板起草电子邮件时不要使用表情符号”…

【A题第二套完整论文已出】2024数模国赛A题第二套完整论文+可运行代码参考(无偿分享)

“板凳龙” 闹元宵路径速度问题 摘要 本文针对传统舞龙进行了轨迹分析&#xff0c;并针对一系列问题提出了解决方案&#xff0c;将这一运动进行了模型可视化。 针对问题一&#xff0c;我们首先对舞龙的螺线轨迹进行了建模&#xff0c;将直角坐标系转换为极坐标系&#xff0…

缓存和数据库缓存有什么区别

缓存通常是在应用层面进行管理的&#xff0c;它就像是应用的一个临时数据仓库&#xff0c;可以存储一些常用的数据&#xff0c;这样应用就可以直接从缓存中获取数据&#xff0c;而不用每次都去数据库里查询。而数据库缓存则是在数据库层面进行管理的&#xff0c;它主要存储的是…

一键云迁移:利用VMware PowerCLI将OVA虚拟机顺利迁移到AWS

哈喽大家好&#xff0c;欢迎来到虚拟化时代君&#xff08;XNHCYL&#xff09;。 “ 大家好&#xff0c;我是虚拟化时代君&#xff0c;一位潜心于互联网的技术宅男。这里每天为你分享各种你感兴趣的技术、教程、软件、资源、福利…&#xff08;每天更新不间断&#xff0c;福利…

天气预报爬虫

一、获取天气接口 主要通过nowapi注册用户之后&#xff0c;进入相应的接口&#xff0c;进行抓取报文。 二、wireshark抓取报文&#xff0c;解析cjson格式 Http的交互过程 1.建立TCP连接 2.发送HTTP请求报文 3.回复HTTP响应报文 4.断开TCP连接 CJSON的使用办法 1. JSON…

Python爬虫-Amazon亚马逊oData参数

前言 本文是该专栏的第37篇,后面会持续分享python爬虫干货知识,记得关注。 本文以“亚马逊Amazon”为例,主要获取亚马逊商品详情页的oData参数规律。 具体实现思路和详细逻辑,笔者将在正文结合完整代码进行详细介绍。接下来,跟着笔者直接往下看正文详细内容。(附带完整…

axure之变量

一、设置我们的第一个变量 1、点击axure上方设置一个全局变量a 3 2、加入按钮、文本框元件点击按钮文档框展示变量值。 交互选择【单击时】【设置文本】再点击函数。 点击插入变量和函数直接选择刚刚定义的全局变量&#xff0c;也可以直接手动写入函数(注意写入格式。) 这…

RISC-V最先进CPU微架构分析

简介 近几年热门的RISC-V架构发展迅猛&#xff0c;尽管因为生问题&#xff0c;RISC-V应用方向主要是单片机级的&#xff0c;高端应用方向发展发展速度缓慢&#xff0c;依然有不少公司推出了基于RISC-V指令集的高端应用场景的处理器。 本文汇总具有代表性的RISC-V公司推出的先…

『功能项目』武器的切换实例【34】

本章项目成果展示 我们打开上一篇33战士的A键连击的项目&#xff0c; 本章要做的事情是按键盘E键切换职业时切换手中的武器 首先在资源商店下载免费的武器模型 创建一个空物体 命名为WeaponPos 将武器预制体拖拽至WeaponPos &#xff08;注意调整空物体位置就可以后续文章会更…