C++中運算子的過載

ZhiboZhao 發表於 2021-07-05
C++

運算子過載相當於運算子的函式過載,用於對已有的運算子進行重新定義,賦予其另一種功能,以適應不同的資料型別。我們之前提到過C++中的函式過載,可以根據形參的不同呼叫不同的函式,那麼運算子過載跟函式過載的實現形式差不多,運算子過載的一般寫法為返回值 operator運算子(引數列表)

下面就根據幾個簡要的例子來簡單實現一下加法,左移以及自增運算子的過載,還有賦值,關係運算子等可以自己實現。首先自定義一個 person 類,通過運算子過載,實現 對person 類的物件中 age 屬性的一系列操作。

class person{
public:
	int age;
	string name;
	// 值傳遞建構函式
	person(string name,int age);
	// 拷貝建構函式
	person(person& p);
    void show();	// 回顯函式,顯示類內成員的數值
};
person::person(string name,int age){
	this->age = age; // this指標區分呼叫者
	this->name = name;
}
person::person(person& p){
	this->age = p.age;
	this->name = p.name;
}
void person::show(){
	cout << "name: " << name << "\tage: " << age << endl;
}

一、加法運算子過載

運算子過載可以表示為成員函式,也可以表示為全域性函式

1.1 成員函式
class person{
public:
	int age;
	string name;
	// 值傳遞建構函式
	person(string name,int age);
	// 拷貝建構函式
	person(person& p);
	void show();
	person operator+(person& p); // 加法過載函式
};
// 加法過載函式的實現,第一個`person`代表返回值,第二個`person`代表域
person person::operator+(person& p){
	person tmp(p); // 呼叫拷貝建構函式給物件 p 賦值
    tmp.age = this->age + p.age;
    tmp.name = this->name;	// 將第一個變數的 name 成員作為 name 的結果
	return tmp;
}
person p1("張三", 25);	// 呼叫值傳遞拷貝函式給物件賦值
person p2("李四", 27);	// 呼叫值傳遞靠別函式給物件賦值
person p3 = p1+p2;	// 加法運算子過載,實現兩個類中的 age 成員相加
p3.show();

上述過載的過程其實就是 p1呼叫 operator+ 函式,相當於 p1.operator+(p2)。在這個過程中,p2 作為引數傳遞給 operator,完成相加後,由於返回型別為值傳遞,因此 return *this其實就是返回 p1 拷貝出來的一個副本,必須在呼叫時重新賦值。輸出結果如下:

name: 張三      age: 52
1.2 全域性函式寫法

在成員函式的寫法中,由於在呼叫加法過載時已經指定了一個物件 ( this 指標指向的物件 ),所以過載函式內只需要再傳遞一個引數即可。但是全域性函式不屬於任何一個物件,因此在進行過載時需要傳入兩個引數。函式實現如下:

person operator+(person& p1, person& p2){
	person tmp(p1); // 呼叫拷貝建構函式給物件 p 賦值
    tmp.age = p1.age + p2.age;
    tmp.name = p1.name;	// 將第一個變數的 name 成員作為 name 的結果
	return tmp;
}
person p1("張三", 25);	// 呼叫值傳遞拷貝函式給物件賦值
person p2("李四", 27);	// 呼叫值傳遞靠別函式給物件賦值
person p3 = p1+p2;	// 加法運算子過載,實現兩個類中的 age 成員相加
p3.show();

輸出結果如下:

name: 張三      age: 52
1.3 鏈式程式設計

對於內建資料型別的加法運算子來說,可以實現 a+b+c 型別的操作,這種情況下先執行 a+b ,返回值再與 c 相加。這樣就必須確保返回值也是 person 資料型別。根據這個結論,上述兩種運算子過載的寫法返回值均為 person 型別,因此鏈式程式設計的實現為:

person p1("張三", 25);
person p2("李四", 27);
person p3("王五", 32);
person p4 = p1+p2+p3;
p4.show();

輸出結果為:

name: 張三      age: 84

二、左移運算子過載

左移運算子主要用於 cout << "abs" 等的輸出,也可以表示為成員函式或者全域性函式。根據對比發現,左移運算子需要兩個引數,coutperson,且已知 cout 屬於 ostream 類。

2.1 成員函式寫法

成員函式只能例項化一個 person 物件,然後 cout 作為函式引數,寫法如下:

class person{
public:
	int age;
	string name;
	// 值傳遞建構函式
	person(string name,int age);
	// 拷貝建構函式
	person(person& p);
	void show();
	person operator+(person& p);	// 加法運算子過載
	void operator<<(ostream& cout);	// 左移運算子過載
};
// 左移運算子過載函式實現,由於 cout 全域性只能有一個,若使用值傳遞的方式,則在傳遞過程中需要進行拷貝。因此只能使用引用傳遞的方式
void person::operator<<(ostream& cout){
	cout << "name: " << name << "\tage: " << age << endl;
}
person p1("張三", 25);
p1 << cout;	// 其實就是 p1.operator<<(cout) 的簡寫

輸出結果為:

name: 張三      age: 25

這種寫法與我麼通常使用的方式剛好相反,因此左移運算子一般不採取成員函式的形式來實現,通常使用全域性函式來實現。在這裡如果對引用不是很清楚的可以移步另一篇文章:C++中指標與引用詳解 - ZhiboZhao - 部落格園 (cnblogs.com)

2.2 全域性函式寫法
void operator<<(ostream& cout, person p){
	cout << "name: " <<p.name << "\tage: " << p.age << endl;
}
person p1("張三", 25);
cout << p1;

輸出結果為:

name: 張三      age: 25
2.3 鏈式程式設計

通常使用左移運算子時,能夠實現 cout << a << b <<...<< endl 的效果,此過程中先執行 cout << a,返回值再執行 下一個左移運算子。因此返回值必須是 ostream 型別。

// 返回值為 ostream 型別,又因為 cout 只有一個,因此以引用的方式返回
ostream& operator<<(ostream& cout, person p){
	cout << "name: " <<p.name << "\tage: " << p.age << endl;
    return cout;
}
person p1("張三", 25);
person p2("李四", 27);
cout << p1 << p2;

輸出結果為:

name: 張三      age: 25
name: 李四      age: 27

三、遞增運算子過載

遞增運算子++有兩種表現形式,分為前置和後置。

  1. 當為前置方式時"++p",p的值先自增1,然後再返回增1後的p值
  2. 當為後置方式時"p++",先返回p的值,p的值再自增1

比如:p=5, 那麼 a = p++ 執行結束後 a=5, p=6, 如果 a=++p 執行結束後,a=6,p=6。所以我們過載運算子需要分別實現這兩種形式。總的來說,前置運算子和後置運算子如果在不使用返回值的情況下,二者的作用一樣,都是使引數自增;當使用返回值時,前置運算子返回自增後的引數,而後置運算子返回自增之前的引數。

3.1 成員函式寫法
3.1.1 前置運算子實現

前置運算子的作用:1)自增 2)返回自增之後的引數,因此實現程式碼為:

class person{
public:
	int age;
	string name;
	// 值傳遞建構函式
	person(string name,int age);
	// 拷貝建構函式
	person(person& p);
	void show();
	person operator++();	// 前置自增運算子
	void operator<<(ostream& cout);	// 左移運算子過載
};
person person::operator++(){
	age++;	// age自增
	return *this;	// 返回自增之後的物件
}

person p1("張三", 25);
person p3 = ++p1;	// 呼叫自增運算子過載函式
cout << "p3: " << p3;	
cout << "p1: " << p1;

輸出結果如下:

p3: name: 張三  age: 26	// 前置自增運算子的返回值
p1: name: 張三  age: 26	// 自增之後

根據輸出結果可以知道,過載的前置自增運算子能夠實現期望的功能。

3.1.2 後置運算子實現

後置運算子的作用:1)自增 2)返回自增之前的引數,在函式內定義 int 佔位符作為形參,來實現與前置自增運算子的區分。因此實現程式碼為:

class person{
public:
	int age;
	string name;
	// 值傳遞建構函式
	person(string name,int age);
	// 拷貝建構函式
	person(person& p);
	void show();
	person operator+(person& p);
	person operator++(); // 前置自增運算子
	person operator++(int);  // 後置自增運算子
	void operator<<(ostream& cout); // 左移運算子過載
};
person p2("李四", 27);
person p4 = p2++;	// 呼叫後置運算子
cout << "p4: " << p4;
cout << "p2: " << p2;

輸出結果如下:

p4: name: 李四  age: 27	// 後置自增運算子的返回值
p2: name: 李四  age: 28	// 自增之後
3.2 全域性函式寫法

需要注意的是,由於全域性函式不屬於任何一個物件,因此形參為引用或者指標傳遞時才能修改原資料。程式碼實現如下:

// 前置自增運算子實現
person operator++(person& p){
	p.age++;
	return p;
}
// 後置自增運算子實現
person operator++(person& p, int){
	person tmp = p;
	p.age++;
	return tmp;
}
person p1("張三", 25);
person p2("李四", 27);
// 前置自增運算子測試
person p3 = ++p1;
cout << "p3: " << p3;
cout << "p1: " << p1;
// 後置自增運算子測試
person p4 = p2++;
cout << "p4: " << p4;
cout << "p2: " << p2;

輸出結果:

p3: name: 張三  age: 26	// 前置自增運算子的返回值
p1: name: 張三  age: 26	// 自增之後
p4: name: 李四  age: 27	// 後置自增運算子的返回值
p2: name: 李四  age: 28	// 自增之後
3.3 鏈式程式設計

由於後置自增運算子沒有鏈式實現,因此我們僅僅以前置自增運算子進行說明。首先我們先使用上述全域性函式實現的自增過載函式進行鏈式測試,程式碼如下:

person p1("張三", 25);

person p3 = ++(++p1);
cout << "p3: " << p3;
cout << "p1: " << p1;

輸出如下:

p3: name: 張三  age: 27	// 兩次前置自增之後的返回值
p1: name: 張三  age: 26	// 兩次自增之後

這個輸出結果是不對的,我們期望經過兩次前置遞增之後,p1.age 同樣也遞增兩次變為 27p2.age 也遞增兩次變為 29。通過分析發現:我們每次通過值傳遞的方式進行返回,所以在作為第一輪遞增之後,++p1 返回了一個臨時建立的副本,繼續進行下一次的遞增,導致第二次的遞增沒有發生在 p1 身上。解決這種問題的辦法就是將值返回改為返回引用。

以成員函式實現改寫後的程式碼為例:

class person{
public:
	int age;
	string name;
	// 值傳遞建構函式
	person(string name,int age);
	// 拷貝建構函式
	person(person& p);
	void show();
	person operator+(person& p);
	person& operator++(); // 前置自增運算子
	person operator++(int);  // 後置自增運算子
	void operator<<(ostream& cout); // 左移運算子過載
};
person& person::operator++(){
	age++;
	return *this;
}
person person::operator++(int){
	person tmp(*this);
	age++;
	return tmp;
}
person p1("張三", 25);
person p3 = ++(++p1);
cout << "p3: " << p3;
cout << "p1: " << p1;

輸出結果如下:

p3: name: 張三  age: 27	// 兩次前置自增之後的返回值
p1: name: 張三  age: 27	// 兩次自增之後