【8】C++进阶系列(重载)

1、重载规则

c++几乎可以重载全部的运算符,而且只能够重载c++已有的运算符。

其中,不能重载的运算符:"." 、 ".*" 、"::"、"?:"

重载之后运算符的优先级和结合性都不会改变。

运算符重载是针对新型数据的实际需要,对原有运算符进行适当的改造。例如:

使复数的对象可以用” + “运算符实现加法;

使时钟对象可以用”++“运算符实现时间增加1秒。 

运算符重载和函数重载是一样的。只是函数名的规则上有点特殊,需要使用operator后面加运算符来当作函数名。不仅可以在定义的类中重载运算符,也可以在类外定义全局函数来重载运算符。但不是所有的重载都可以放在类里作为成员函数来重载。有两种方式:1种是重载为类的非静态成员函数,第二种是重载为非成员函数(类外)。

2、双目运算符重载为成员函数

双目运算符:运算所需变量为两个的运算符叫做双目运算符,或者要求运算对象的个数是2的运算符称为双目运算符。

重载为类成员的运算符函数定义形式:

函数类型 operator 运算符(形参){……},参数个数=原操作数个数-1 (后置++,--除外),operator 运算符合起来作为函数名。见下图:

                                         

双目运算符重载规则:括号里的参数是右操作数

1、如果要重载B为类成员函数,使之能够实现表达式oprd1 B oprd2,其中oprd1为A类对象,即左操作数必须是类的对象,则B应被重载为A类的成员函数,形参类型应该是oprd2所属的类型。

2、经重载后,表达式oprd1 B oprd2相当于oprd1.operator B(oprd2)

                                            

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

要求将+,-运算重载为复数类的成员函数;规则是实部和虚部分别相加减。两个操作数都是复数类的对象。操作数:A+B,其中A,B叫做操作数,”+“叫做操作符.

                                            

这个时候,”+“和”-“其实都是函数,右键转到定义会直接来到”operator +“或者“opertor -”的位置。c1+c2相当于c1.operator +(c2),operator +是函数

#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 Complex &c2)const;
	void display() const;
	~Complex();

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;

}

Complex::~Complex()
{
}

int main() {
	Complex c1(1, 2), c2(4, 6), c3;
	cout << "c1="; c1.display();
	cout << "c2="; c2.display();
	cout << "重载加法:"<< endl;
	c3 = c1 + c2;
	cout << "c3=c1+c1="; c3.display();
	cout << "重载减法:" << endl;
	c3 = c1 - c2;
	cout << "c3=c1-c1="; c3.display();
	return 0;
}

                        

3、单目运算符重载为成员函数

前置单目运算符重载规则:如果要重载U为类成员函数,使之能够实现表达式U oprd,其中,oprd为A类对象,则U应被重载为A类的成员函数,无形参。经重载后,表达式U oprd相当于oprd.operator U(),oprd是操作数。

后置单目运算符++和--重载规则:此时operator U都是一样的,如何区分前置和后置呢?只能通过参数表来区分。后置运算符的参数表中,参数的个数会比前置的参数表多一个参数,多出来的参数仅仅用来区分前置和后置。如果要重载++或--为类成员函数,使之能够实现表达式oprd++或oprd--,其中oprd为A类对象,则++或--应被重载为A类的成员函数,且具有一个int类型的参数。经重载后,表达式oprd++相当于oprd.operator ++(0),注意,前置的话是没有形参的。

例子:重载前置++和后置++为始终类成员函数。

注意:前置单目运算符,重载函数没有形参。

后置++运算符,重载函数需要一个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);
	~Clock();

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;
	}
}

Clock::~Clock()
{
}
//前置自增运算符重载
Clock& Clock::operator ++() {//自增1,返回的是自增1之后的自己,所以用的引用
	second++;
	if (second>=60)
	{
		second -= 60; minute++;
		if (minute>=60)
		{
			minute -= 60; hour = (hour + 1) % 24;
		}
	}
	return *this;//返回值是当前对象的引用
}
//后置自增运算符重载
Clock Clock::operator ++(int) {//先使用后加1
	Clock old = *this;//当前对象目前的值暂时存在一个临时的局部变量里面
	++(*this);//为了达到同步改变的效果,调用了前置自增运算符
	return old;//触及不到对象本身
}
//前置自增返回的是左值,后置自增返回了右值


void Clock::showTime() const{
	cout << hour << ":" << minute << ":" << second << endl;
}


int main() {
	Clock myClock(23, 59, 59);
	myClock.showTime();

	cout << "myclock ++: ";
	(myClock++).showTime();//(myClock++)的返回值是一个副本,old
	cout << "++ myclock: ";
	(++myClock).showTime();//(++myClock)返回的结果是(myClock++)操作后的自己。
	return 0;
}

4、运算符重载为类外的全局函数(非成员函数)

函数的形参代表依次从左到右次序排列的各操作数。

重载为非成员函数时,参数个数=原操作数个数(后置++,--除外);至少应该有一个自定义类型的参数。

后置单目运算符++和--的重载函数,形参列表中要增加一个int,单不必写形参名。

如果在运算符的重载函数中需要操作某类的私有成员,可以将此函数声明为该类的友元。

运算符重载为非成员函数的规则:

双目运算符B重载后,表达式oprd1 B oprd2等同于operator B(oprd1,oprd2)

前置单目运算符B重载后,表达式B oprd 等同于operator B(oprd)

后置单目运算符++和--重载后表达式oprd B等同于operator B(oprd,0)

例子:重载Complex的加减法和”<<“运算符为非成员函数。

将+、-(双目)重载为非成员函数,并将其声明为复数类的友元,两个操作数都是复数的常引用。

将<<(双目)重载为非成员函数,将其声明为负数类的友元,它的左操作数是std::ostream引用,右操作数为复数类的常引用,返回istd::ostream引用,用以支持下面形式的输出:

cout<<a<<b;

该输出调用的是:operator <<(operator<<(cout,a),b);

//test.h
#include<iostream>
#ifndef _TEST_H_
#define _TEST_H_


using namespace std;

class Complex
{
public:
	//Complex();
	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);//返回值是ostream对象的引用

private:
	double real, imag;
};

#endif // !_TEST_H_

//test.cpp
#include"test.h"

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)
{
	// TODO: 在此处插入 return 语句
	out << "(" << c.real << "," << c.imag << ")";
	return out;
}

//testApp.cpp
#include"test.h"
int main() {
	Complex c1(2, 3), c2(5, 6), 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;
}

5、虚函数

虚函数是实现动态绑定的函数。

前面在派生类型转换的地方有一个例子:如下(我们希望通过一个通用的方法调用不同的函数)

#include<iostream>

using namespace std;

class Base1
{
public:

	void display()const {
		cout << "Base1::display()" << endl;
	}

private:

};

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

};

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

};

void fun(Base1 *ptr) {
	ptr->display();
}

int main() {
	Base1 base1;
	Base2 base2;
	Derived derived;

	fun(&base1);
	fun(&base2);
	fun(&derived);
	return 0;
}

结果没有达到我们想要的效果。造成了代码的可读性有问题。

不成功的原因:在编译阶段,编译器根据指针无法判断它会指向一个什么类型的对象。所以只能说,指针是什么类型的,他就调用哪个类定义的display函数。这种情况下,我们希望告诉编译器。在编译器阶段没法正确的决定,则推迟这个决定,也就是说在编译的时候,先别确定display调用表达式到底哪个函数体和它相对应,把它留着到运行时再确定。那么在运行时就知道指针在某个时刻指向的实际对象是什么。

那么只需要如下修改:

                                                            

virtual的意思就是告诉编译器,当遇到对这样原型的调用,都不要马上做决定,决定它该去调用哪个函数的函数体,要将它延后。也就是不要在编译阶段做静态绑定,要为运行阶段做动态绑定做好准备。现在就不能把display的实现写在类体中作为内联函数了,因为内联函数会在编译阶段就做处理,把函数体嵌带代码中去,所以不能将函数的实现写为内联函数。既然要他在运行阶段才决定去执行对应类的函数体,所以加了virtual的函数都要把函数的实现写在类的外面,而不能写在类里面。各个类中的display声明都要写成virtual的形式并且实现都要写在类外。(修改后的代码如下)

#include<iostream>

using namespace std;

class Base1
{
public:

	virtual void display()const;
private:

};
void Base1::display()const {
	cout << "Base1::display()" << endl;
}

class  Base2:public Base1
{
public:
	virtual void display() const;
private:

};

void Base2::display() const {
	cout << "Base2::display()" << endl;
}

class  Derived :public Base2
{
public:
	virtual void display() const;
private:
};

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;
}

可以看到,实现了我们想要的效果。虽然是基类指针来调用display,但是却能找到每个对象自己的display函数,因为是在运行时确定的具体调用那个函数,实现动态绑定。

虚函数:必须是非静态的成员函数,也就是说虚函数应该是属于对象的,而不是属于类的。需要在运行时通过指针确定对象,在决定执行哪个函数体。那么虚函数经过派生之后就能实现运行中的多态。

虚函数是用virtual关键字说明的函数

虚函数是实现运行时多态性的基础

c++中的虚函数是动态绑定的函数

虚函数必须是非静态的成员函数

什么函数可以是虚函数?——一般成员函数可以是虚函数,构造函数不能是虚函数,析构函数可以是虚函数。

一般的虚成员函数:

虚函数的声明:virtual 函数类型 函数名 (参数表);

虚函数声明之恩那个出现在类定义中的函数原型声明中,而不能出现在成员函数实现的时候,也就是说virtual关键字只能出现在类体中函数原型声明的时候,不能出现在类外。

在派生类中可以对基类中的成员函数进行覆盖

虚函数一般不声明为内联函数,因为虚函数的调用需要动态绑定,实在函数运行的时候处理的,而内联函数的处理是静态的,是在编译阶段实现的。

virtual关键字小结:

派生类可以不显示的用virtual声明虚函数,这时系统会用以下的规则来判断派生类的一个函数成员是不是虚函数:1、该函数是不是与基类的额虚函数有相同名称、参数个数及对应参数类型?2、该函数是否与基类的虚函数有相同的返回值或者满足类型兼容规则的指针、引用型的返回值。

如果从名称、参数及返回值三个方面检查之后,派生类的函数满足上面三个条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数。

派生类中的虚函数还会隐藏积累中同名函数的所有其他重载形式。

一般习惯于在派生类的函数中也使用virtual关键字,以增加程序的可读性

6、虚析构函数

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

                     

如上图,定义fun函数时,参数是Base类型的指针,由于没有声明虚函数,所以编译时是静态绑定的,只能通过声明的指针来判断调用哪个函数,也就是说只会调用~Base()。Derived的析构函数根本就没有运行只有Base类的析构函数被运行了,那么p得不到释放,造成内存泄漏。

将析构函数声明为虚析构函数,这样就会等到运行时根据指针b指向实际的对象,就会调用Derived函数,才会析构释放p指针。如下:

                        

所以,很多情况下我们都需要写虚析构函数的。

7、虚表与动态绑定。

为什么运行的时候可以实现动态绑定?当运行时,没有编译环境了,只有一个操作系统,我们把可执行程序放在操作系统上运行,谁来帮我们确定该执行哪个函数体呢?——其实编译器早就为我么预先做好了准备。——虚表

虚表:每个多态类有一个虚表(virtual table),虚表中有当前类的各个虚函数的入口地址,每个对象有一个指向当前类的虚表的指针(虚指针vptr),这是一个隐含的指针

动态绑定的实现:

构造函数中为对象的虚指针赋值。

通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址

通过该入口地址调用虚函数。

虚表的示意图:

                 

执行时,先通过虚表的指针找到虚表,在通过虚表找到各个成员函数的指针,通过这些指针找到对应的成员函数。

8、抽象类

描述抽象的概念。有些功能,有些函数就无法实现。比如,定义一个二维图像,求面积。不同图像的面积求取是不一样的。在这个抽象的图形面积里面怎样去确定面积公式呢?确定不了

纯虚函数:是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际定义自己的版本,纯虚函数的声明格式为:virtual 函数类型 函数名(参数表)= 0;=0表示没有函数体。抽象类不能定义对象。

”无法实现”是指在基类中定义的信息不够具体,于是这个函数没办法规定具体的算法;但是为了规定整个类家族统一的行为和对外接口又需要在比较高层次的基类中定义这么一个函数,这时就可以在函数头之后加“=0”,表示没有函数体,而不是表示函数的固定结果为0.

带有纯虚函数的类就叫做抽象类,因为这样的类还有些东西没有实现,所以,它不能产生实例。也就是说,抽象类是不能定义对象的。——那么不能定义对象有什么用呢?——作为基类来使用(baseclass):用来规范整个类家族的统一对外接口。

抽象类的语法:只要有纯虚函数,那么这个类就是抽象类

带有纯虚函数的类:class 类名{

virtual 类型 函数名(参数表)=0;

//其他成员……}

虽然不能实例化,但是它可以规定对外接口的统一形式。有什么好处呢?——使得将基类对象和各级不同派生类对象都按照统一的方式进行处理,因为他们都有同样的接口。我们通过基类指针可以接受不同派生类对象的地址,然后去调用在基类中定义过的函数名(虽然在基类中没有实现,但是在派生类中实现了),这样的方法配合着虚函数的动态绑定机制去利用多态性的一种很好的方式,有了这种规范的统一的行为,就能够保证派生类具有这种统一要求的行为。

抽象类的作用:

将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。

对于暂时无法实现的函数,可以申明为纯虚函数,留给派生类去实现。

注意:

抽象类只能作为基类来使用,不能定义抽象类的对象。

在某一类中实现纯虚函数以前,前面的类都是抽象类,只有当某一级实现了纯虚函数的具体内容使得不再纯虚了(有了函数体),那么这一类才不是抽象类了,才可以用来定义对象。将上面的代码略作修改,将Base1中的display函数改为纯虚函数。

                                                   

#include<iostream>

using namespace std;

class Base1
{
public:

	virtual void display()const = 0;//纯虚函数
private:

};

class  Base2 :public Base1
{
public:
	virtual void display() const;
private:

};

void Base2::display() const {
	cout << "Base2::display()" << endl;
}

class  Derived :public Base2
{
public:
	virtual void display() const;
private:
};

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;
}

9、override与final(c++标准提供的新功能)

当派生类中想要实现一个基类中同样的函数,对基类函数进行覆盖,但是由于某种原因可能丢失了某些信息,使得声明的函数和基类中的函数原型有差异。这时,编译器便不会报错,但是运行时却达不到我们预期的多态性结果,而往往这种错误又非常难调试。如:virtual在派生类中是可以缺省的,但是const关键字有无对于函数来说是完全不一样的,漏掉const之后达不到覆盖基类中原函数的效果。

                               

c++11引入了一种显式的函数覆盖功能。在编译期间而非运行期间捕获此类错误。在虚函数显式重载中应用,编译器会检查基类是否存在一类虚拟韩式,与派生类中带有声明override的虚拟函数,有相同的函数标签(signature);若不存在,则会报错。

有时,自己定义的类已经很完善,不希望被别人继承和发展,只是希望别人拿来用就好了(比如有的功能在系统中很关键,我们不希望他被修改或是覆盖,不希望接口被屏蔽)。有时不希望某函数被修改,希望所有使用该函数都是一致的算法。可以通过final来处理。override和final都不是语言的关键字,它只在特定的地方有特定的含义。

例子:

                                      

                                          

想要看更加详细的关于override和final的信息,请移步:使用C++11继承控制关键词来防止在类层次结构上的不一致

猜你喜欢

转载自blog.csdn.net/qq_21210467/article/details/82897379