1. 程式人生 > >【8】C++進階系列(過載)

【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繼承控制關鍵詞來防止在類層次結構上的不一致