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也会使数据不安全,所以也定义为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& 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:59

show myClock++:   23:59:59

show ++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版 郑莉 清华大学

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序小白Erike.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值