c++之多态性入门

C++ — 多态性入门

运算符的重载

1、c++中几乎可以重载所有运算符,而且只能够重载c++中已经有的。
不能重载的运算符:“ . ”、“ .* ”、“::”、“?:”
2、重载之后的运算符的优先级和结合性都不会改变。
3、运算符的重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。

运算符重载为成员函数:

定义形式:
函数类型 operator 运算符(形参)
{
… …
}
参数个数=原操作数个数-1(后置++、- -除外)
其左操作数必须是该类的对象
双目运算符重载规则:
如果重载双目运算符B为类的成员函数,使之能够实现表达式oprd1 B oprd2,其中oprd1为A类的对象,则B应被重载为A类的成员函数,形参类型应该是oprd2所属的类型。
经重载后,表达式oprd1 B oprd2相当于oprd1 . operator B(oprd2)。

例:复数类加减法运算重载为成员函数

#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;
	Complex operator +(const int x)const;//重写一个“复数加实数”的函数 
	Complex operator -(const Complex &c2)const;
	Complex operator -(const int x)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 int x)const{
	return Complex(real+x,imag);//重写一个“复数加实数”的函数 
	}
	
Complex Complex::operator-(const Complex &c2)const{
	return Complex(real-c2.real,imag-c2.imag);
	}
	
Complex Complex::operator-(const int x)const{
	return Complex(real-x,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<<"c1+c2=";//两个复数相加 
	c3.display();
	
	c3=c1-c2;
	cout<<"c1-c2=";//两个复数相减 
	c3.display();
	
	c3=c1+1; 
	cout<<"c1+1=";//复数加实数 
	c3.display();
	 
	c3=c1-1;
	cout<<"c1-1=";// 复数减实数 
	c3.display();
	return 0;
}

结果:
在这里插入图片描述
前置单目运算符重载规则:
如果要重载单目运算符U为类成员函数,使之能够实现表达式U oprd,其中oprd为A类的对象,则U应被重载为A类的成员函数,无形参。
经重载后,表达式U oprd相当于oprd . operator U()
后置单目运算符重载规则:
如果要重载单目运算符U为类成员函数,使之能够实现表达式oprd U,其中oprd为A类的对象,则U应被重载为A类的成员函数,且具有一个int类型的形参。
经重载后,表达式oprd U相当于oprd . operator U(int)

例:
重载前置++和后置++为时钟类成员函数(操作类是时钟类对象,实现时间增加一秒钟)

#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);//后置单目运算符重载
	
private:
	int hour,minute,second; 
};

Clock::Clock(int hour,int minute,int second){
	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;//将本身的值赋值给新变量old 
	++(*this);//调用前置++运算符 ,将本身加1 
	return old;//返回的时没有加1后的值 ,但此时本身已经加1 
}
int main()
{	
	Clock myClock(23,59,59);
	cout<<"First time output:";
	myClock.showTime();
	cout<<"Show myClock++:";
	(myClock++).showTime();
	//显示的是没有加1时的时间 
	cout<<"Show ++myClock:";
	(++myClock).showTime(); 
	//显示加1后的时间,但由于之前已经加1,则此时的时间为最原始的时间加2 
	return 0;
}

结果:
在这里插入图片描述

运算符重载为非成员函数

运算符重载为非成员函数的规则:
1、函数的形参代表依自左至右次序排列的各操作数。
2、参数个数=原操作数个数(后置++、- -除外)
至少应该有一个自定义类型的参数。
3、后置单目运算符++和- -的重载函数,形参列表中要增加一个int,但不必写形参名。
4、如果在运算符的重载函数中需要操作某类对象的私有成员,可以将此函数声明为该类的友元。
5、双目运算符B重载后表达式oprd1 B oprd2相当于operator B(oprd1,oprd2)
6、前置单目运算符B重载后表达式B oprd相当于operator B(oprd)
7、后置单目运算符B重载后表达式oprd B相当于operator B(oprd,int)
例:
在这里插入图片描述

#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);
	//声明非成员函数为类的友元,则可以更方便地调用类内的私有成员 
	friend Complex operator +(const int c1,const Complex &c2);
	friend Complex operator -(const int c1,const Complex &c2);
	friend Complex operator +(const Complex &c1,const int c2);
	friend Complex operator -(const Complex &c1,const int c2);
	//实现实数加减复数,复数加减实数 
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);
}

Complex operator +(const int c1,const Complex &c2){
	return Complex(c2.real+c1,c2.imag);
}

Complex operator -(const int c1,const Complex &c2){
	return Complex(c1-c2.real,c2.imag);
}

Complex operator +(const Complex &c1,const int c2){
	return Complex(c1.real+c2,c1.imag);
}

Complex operator -(const Complex &c1,const int c2){
	return Complex(c1.real-c2,c1.imag);
}

//自定义输出函数 
ostream & operator <<(ostream &out,const Complex &c){
	out<<"("<<c.real<<","<<c.imag<<")";
//此处"<<"为系统内定义的,其左边out是ostream类的对象,则可以给显示器输出右边的东西,同时返回值也为ostream类的对象 
	return out;
}
//返回值为ostream类的对象,则可以做到级联输出 
int main()
{	
	Complex c1(5,6),c2(7,8),c3;
	cout<<"c1="<<c1<<endl;
	cout<<"c2="<<c2<<endl;
	c3=c1+c2;
	cout<<"c1+c2="<<c3<<endl;
	c3=c1-c2;
	cout<<"c1-c2="<<c3<<endl;
	c3=1+c1;
	cout<<"1+c1="<<c3<<endl;
	c3=1-c1;
	cout<<"1-c1="<<c3<<endl;
	return 0;
}

在这里插入图片描述

虚函数

在这里插入图片描述
在这里插入图片描述
此时不管给fun函数的参数是哪一类的对象,其调用的都是Base1的显示函数,因为在编译阶段,编译器根据指针无法去判断在运行时会指向什么类型的对象,故指针时什么类型的,它就调用那个类的display函数。
解决方法:在编译时先不确定指向什么,在运行时再确定指向的对象。
例:
通过虚函数实现运行时多态

#include <iostream>
using namespace std;

class Base1{
public:
	virtual void display() const;
//virtual关键字能使编译器凡是遇到对这种原型的函数的调用,都不要在编译阶段马上做决定它该调用哪个函数的函数体,即不要在编译阶段做静态绑定,而为运行阶段做动态绑定做好准备 
};
//此时不能把display写成内联函数,因为内联函数都是要在编译阶段进行处理的 
void Base1::display() const{
	cout<<"Base1::display"<<endl;
}

class Base2:public Base1{
public:
	virtual void display() const;
};
void Base2::display() const{
	cout<<"Base2::diaplay"<<endl;
}

class Derived:public Base2{
public:
	virtual void display() const;
};
void Derived::display() const{
	cout<<"Derived::display"<<endl;
}

void fun(Base1 *ptr){
	ptr->display();
}
int main()
{	
	Base1 base1;
	Base2 base2;
	Derived derived;
	fun(&base1);
	fun(&base2);
	fun(&derived);
	return 0;
}

在这里插入图片描述
在这里插入图片描述
一般虚函数成员:
1、virtual 函数类型 函数名(形参表);
2、虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
3、在派生类中可以对基类中的成员函数进行覆盖。
4、虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的。
在这里插入图片描述
上面的例程中,Base2和Derived类体中的虚函数声明中的virtual均可省略。

虚析构函数

如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),就需要让基类的析构函数称为虚函数,否则执行delete的结果是不确定的。

扫描二维码关注公众号,回复: 11028012 查看本文章
#include <iostream>
using namespace std;

class Base{
public:
	~Base();
};
Base::~Base(){
	cout<<"Base delete"<<endl;
}

class Derived:public Base{
public:
	Derived();
	~Derived();
private:
	int *p;
};
Derived::Derived(){
	p=new int(0);
}
Derived::~Derived(){
	cout<<"Derived delete"<<endl;
	delete p;
}

void fun(Base *b){
	delete b;
}

int main()
{	
	Base *b=new Derived();
	fun(b);
	return 0;
}

结果为:
在这里插入图片描述
此时Derived中的p指针空间未被释放。
当用虚析构函数改造后:

#include <iostream>
using namespace std;

class Base{
public:
	virtual ~Base();
};
Base::~Base(){
	cout<<"Base delete"<<endl;
}

class Derived:public Base{
public:
	Derived();
	virtual ~Derived();
private:
	int *p;
};
Derived::Derived(){
	p=new int(0);
}
Derived::~Derived(){
	cout<<"Derived delete"<<endl;
	delete p;
}

void fun(Base *b){
	delete b;
}

int main()
{	
	Base *b=new Derived();
	fun(b);
//此时会在运行时根据指针b所指向的实际对象来调用相应的析构函数 
	return 0;
}

结果为:
在这里插入图片描述

虚表与动态绑定

虚表:
1、每个多态类有一个虚表。
2、虚表中有当前类的各个虚函数的入口地址。
3、每个对象有一个指向当前类的虚表的指针(虚指针vptr)。
动态绑定的实现:
1、构造函数中为对象的虚指针赋值。
2、通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址。
3、通过该入口地址调用虚函数。
简单的动态绑定的虚表示意图:
在这里插入图片描述
每个对象所包含的内容除了自己定义或继承的 i , j 之外,还有一个指向虚表的指针vptr。
当用一个指向该对象的指针去调用其函数时,会首先通过其找到对象中的虚表指针vptr,然后找到虚表,再从虚表里找到指向相应函数的指针调用函数体,所以才能在运行时根据指针所指向的对象,去正确调用这个对象他自己的功能函数。

抽象类

纯虚函数:
1、是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据需要定义自己的版本。
2、声明格式为:
virtual 函数类型 函数名(参数表)= 0;
3、带有纯虚函数的类就为抽象类,因为这样的类还有东西没有实现,所以这个类时不能定义对象的,只做基类用,因为它可以规范整个类家族的统一对外接口。
抽象类作用:
1、将有关的数据和行为组织在一个继承层次结构中,保证派生类具有统一要求的行为。
2、对于暂时无法实现的函数,可以声明为虚函数,留给派生类去实现。
3、当派生类没有实现基类中未实现的纯虚函数时,那么这个派生类仍继续作为抽象类,仍无法实例化定义对象,直到某一级派生类实现了纯虚函数。
例:

#include <iostream>
using namespace std;

//抽象基类,不能产生对象 
class Base1{
public:
	virtual void display() const = 0;//纯虚函数 
};

class Base2:public Base1{
public:
	virtual void display() const;//纯虚函数的实现 
};
void Base2::display() const{
	cout<<"Base2::display"<<endl;
}

class Derived:public Base2{
public:
	virtual void display() const;
};
void Derived::display() const{
	cout<<"Derived::display"<<endl;
}

void fun(Base1 *ptr){
	ptr->display();
}
int main()
{	
	Base2 *base2=new Base2();
	Derived derived;
	fun(base2);
	fun(&derived);
	return 0;
}

结果:
在这里插入图片描述

override与final

override:
1、多态行为的基础:基类声明虚函数,派生类声明一个函数覆盖该虚函数。
2、覆盖要求:函数名完全一致。
3、函数签名包括:函数名,参数列表,const。
4、当出现失误时,即派生类中想覆盖基类虚函数的函数没有与这个虚函数完全一致,则在编译阶段编译器是不会报错的,但是结果就会出问题。(一般问题会出现在const上)
5、在c++11中引入了显式函数覆盖,即override。如果在派生类中用override说明了一个函数,但编译器无法在基类中找到相同原型的虚函数,则就会报错。
final:
当一个类定义后加上final,则这个类无法被继承;当一个函数后加上final,则这个函数不能再被覆盖。
在这里插入图片描述
**注:**override和final都不是关键字,但是一般不要把自定义的变量或函数名起名为这两个,虽然语法没错,但却影响程序的可读性。

发布了30 篇原创文章 · 获赞 33 · 访问量 1273

猜你喜欢

转载自blog.csdn.net/weixin_45949075/article/details/104188542