《C++ Primer Plus 6th.ed》讀書筆記之一:使用友元的重載運算符

概要

新年伊始,閒來無事,於故紙堆中翻到了《C++ Primer Plus》,正好有些許概念仍不清楚,所以在讀完書後想寫一點筆記幫助記憶。
關於運算符重載,這應該是一個很簡單的概念,而友元也算不上多麼複雜,但是當這兩者碰撞在一起的時候,就誕生一個不好選擇的十字路口:重載應該採用什麼樣的設計模式,是成員函數,非成員函數抑或友元函數?

基本的重載實現

假設現在有一個類的實現是這樣的(省去構造函數和析構函數等不必要的內容):

// code Fragment 0
class person
{
public:
	double cash;	// 表示個人持有的資金
};

現在將這個類實例化,得到TomJohn兩個類。若需要計算Tom和John二人手中共持有多少資金,最簡單的方法是total = Tom.cash + John.cash,這樣寫雖然直觀但是無形中加大了開發成本,這種情況下可以(但不是必須)使用重載+運算符的方式簡化代碼。

  1. 使用成員函數的重載
    // codeFragment 1
    class person
    {
    public:
    	double cash; // 表示個人持有的資金
    	double operator+(const person& s) // 重載之後的+運算符
    	{
    		return cash + s.cash;
    	}
    }
    
    如此只需調用total = Tom + John即可完成運算
  2. 非成員函數的重載
    // code Fragment 2
    class person
    {
    public:
    	double cash; // 表示個人持有的資金
    };
    
    // 重載的+運算符
    double operator+(const person& s1, const person& s2)
    {
    	return s1.cash + s2.cash;
    }
    
    這種寫法也可以實現total = Tom + John的效果,但是這樣的重載函數只能訪問類的公開成員,如果cashperson類的私有成員變量,則無法訪問,編譯不能通過

運算的次序問題

重載的本質依然是函數的調用,在C++中,函數的調用必須遵守參數的順序,那麼在重載函數中,可能會出現如下的一種尷尬情況:

// code Fragment 3
class time
{
public:
	int hours;		// 小時
	int minutes;	// 分鐘
	
	// 運算符*重載,對時間乘一個倍數
	time& operator*(double scaler)
	{
		time res;
		long total_min = (minutes + hours * 60ul) * scaler;
		res.hours = total_min / 60ul;
		res.minutes = total_min % 60ul;
		return res;
	}
}

這段代碼重定義了*運算符,應該這樣調用重載後的運算符:

A = B * 2.75; // 1

事實上,它等價於:

A = B.operator*(2.75); // 2

所以這樣寫是錯誤的:

A = 2.75 * B // 3

因爲2.75的類型是float/double,而不是我們期望的time類型,這個數字本身沒有重載乘法運算符,所以編譯器無法提供這樣的適配
那麼,如果我一定要按照3的方式去調用怎麼辦?
實際上這裏牽扯到軟工中一個比較有爭議的設計理念:“永遠不要相信用戶的輸入”——不知道有沒有人記得測試工程師與酒吧的笑話——“一位顧客點了一份炒飯,酒吧炸了”^_^
回到剛纔的問題,按照3的方式進行調用當然是可以的,不過需要額外提供一份非成員函數形式的乘法運算符重載:

// 非成員函數形式的乘法運算符重載
time& operator*(double scaler, const time& t)
{
	// 此處代碼省略
}

但是這樣做依然有一定的問題,如果要訪問類的私有成員變量,這個函數無論如何都是做不到的,這個時候可以考慮使用友元進行運算符重載
注意: 有些運算符只能使用成員函數的形式進行重載,它們分別是=[]()->

基本的友元重載

// code Fragment 4
class time
{
private:
	int hours;		// 小時
	int minutes;	// 分鐘

public:
	// 使用友元的重載聲明
	friend time& operator*(const time& s, double scaler);
}

// 重載的具體實現
time& operator*(const time& s, double scaler)
{
	// 具體實現省略
}

可以看到,友元重載有這樣的特點

  1. operator*()的聲明位於類聲明中,但是其具體實現不加::限定符,也不是類成員函數
  2. operator*()的聲明前加了friend關鍵字,告訴編譯器這是一個友元函數
  3. operator*()具有和類成員函數一樣的訪問權限,即可以訪問由private等修飾的域

事實上,友元函數作爲類聲明中接口的一部分早已得到廣泛應用

最常用的友元重載——重載<<運算符

偶爾我們可能希望使用過標準流輸出打印一些信息到終端或者文件中,例如對code Fragment 4中的代碼,希望它可以打印出諸如xx hours, yy minutes這樣格式的時間;但是注意到這個操作涉及到的兩個成員變量都是私有成員變量,所以我們可能需要一些特殊的函數來讀取它們,例如:

// code Fragment 5
class time
{
private: 
	int hours;		// 小時
	int minutes;	// 分鐘

public:
	int get_hours(void) { return hours; }			// 獲取小時數
	int get_minutes(void) { return minutes; }		// 獲取分鐘數
}

int main(void)
{
	// 實例化
	time A;
	
	// 省去賦值操作......

	// 按照既定格式輸出時間
	std::cout << A.get_hours() << " hours, " << A.get_minutes() << " minutes" << std::endl;

	return 0;
}

可見這樣對於私有成員變量的操作是較爲繁瑣的,能不能使用cout << A這樣的方式進行輸出呢?答案是可以
下面討論如何適當的重載<<運算符
首先,<<運算符最早的語義是向左移位計算,有過C語言基礎的讀者應該對這個符號並不陌生;但是在STL中,該符號被多次重載,作爲流運算符而廣泛應用
現在,先嚐試基本的友元重載:

// code Fragment 6
#include <iostream>

class time
{
private:
	int hours;		// 小時
	int minutes;	// 分鐘

public:
	time(int h, int m) { hours = h; minutes = m; }				// 構造函數
	friend void operator<<(std::ostream& os, const time& s);	// 重載<<的聲明
};

// 重載<<的實現
void operator<<(std::ostream& os, const time& s)
{
	os << s.hours << " hours, " << s.minutes << " minutes" << std::endl;
}

int main(void)
{
	time A(15, 23);
	std::cout << A;
	return 0u;
}

這樣構造的operator<<()可以達到我們預期的目的,但是它並不能實現連續輸出的功能,例如cout << A << B這樣的寫法,這是爲什麼呢?解決這個問題就要了解在STL中該運算符的原理

插話:標準<<原理和實現

有時候,經常可以看到這樣的實現:

int x = 0, y = x;
cout << x << y;

這個操作的本質應該是:

cout = cout << x;
cout << y;
// 或者是這樣:
(cout << x) << y;

所以我們可以得出這樣的結論,即operator<<()針對int類型的函數原型應該是:

ostream& operator<<(const int& num);
// 或者
ostream& operator<<(ostream& os, const int& num);

當然實際的實現中,該函數的原型應該是第一種,因爲它是basic_ostream的類成員函數,可以在C++標準頭文件<ostream>中查到
瞭解到這一點之後,就可以對剛纔的重載進行一點點修正,使之變成通用的<<運算符

修正的實現
// code Fragment 7
#include <iostream>

class time
{
private:
	int hours;		// 小時
	int minutes;	// 分鐘

public:
	time(int h, int m) { hours = h; minutes = m; }				// 構造函數
	friend std::ostream& operator<<(std::ostream& os, const time& s);	// 重載<<的聲明
};

// 重載<<的實現
std::ostream& operator<<(std::ostream& os, const time& s)
{
	os << s.hours << " hours, " << s.minutes << " minutes";
	return os;
}

int main(void)
{
	time A(15, 23), B(12, 21);
	std::cout << A << std::endl << B;
	return 0u;
}

比較code Fragment 6code Fragment 7,對於operator<<()的重載,其區別僅在於增加了返回值,這樣就可以把自定義的類嵌入到標準流當中,而且由於輸出流的繼承關係,被重載的運算符甚至可以把類的內容輸出到文件流中,可以說是非常方便的操作了

發佈了17 篇原創文章 · 獲贊 20 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章