Effective c++(筆記) 之 類與函數的設計聲明中常遇到的問題

1.當我們開始去敲代碼的時候,想過這個問題麼?怎麼去設計一個類?

或者對於程序員來說,寫代碼真的就如同搬磚一樣,每天都乾的事情,但是我們是否曾想過,在c++的代碼中怎麼樣去設計一個類?我覺得這個問題可比我們“搬磚”重要的多,大家說不是麼?

這個答案在本博客中會細細道來,當我們設計一個類時,其實會出現很多問題,例如:我們是否應該在類中編寫copy constructor 和assignment運算符(這個上篇博客中已說明),另外,我們是讓編寫的函數成爲類的成員函數還是友元還是非成員函數,函數的參數使用傳引用的方式還是傳值的方式,這個函數該不該聲明爲const,函數的返回值是該設計成const麼?等一系列的問題,我會在下文分成各個小問題來解釋。

首先,我覺得應該考慮的一個重要問題,我們設計的類該有多大,每當有新的需求時,我們是否應該隨意的添加。

請在設計類的時候遵循下面的原則:

讓你的class類接口即完美又最小化

原因------設計的類就相當於定義了一個新的類型,如果不能滿足我們的需求,那麼就不用談其他的了,所以首先我們應該讓設計的類滿足我們的需求,在滿足我們的需求時,儘可能的使類最小化,類的最小化是指,當類有新的需求時,我們看這個需求是否跟已經編寫的函數衝突,是否可以和以前的整合,也就是說看這個成員函數是否是必要寫到類裏面的,因爲大型class接口的缺點可維護性差。


2.類中的數據成員是設計成public、protected還是private?

答案:儘量使自己的data members設計成私有,不讓外部訪問,使其封裝性更好,如果類的數據成員設計成public的話,外界隨便訪問,這對於c++的封裝性而言就不是很好。

我們通常設計爲pirvate,如果需要得到或者改變這些值,我們會編寫專門的成員函數來操作,如下所示

int GetX() const { return x;}
void SetX(int value) { x = value;}

如果所設計的類爲基類,同時希望基類的數據成員被派生類繼承,那麼一個很好的方法常常將數據成員設計爲保護類型protected

這樣,當繼承方式爲公有繼承時,基類的公有成員和保護成員均以原有的狀態繼承下來,派生類中的成員函數和友元可以訪問到基類的數據成員。


3.類class與結構體struct 的區別在哪裏?

答:定義類等於定義了一種新的類型,結構體其實也可以達到這樣的結果,有兩個非常明顯的不同點

類中如果不註明數據成員的訪問級別默認的數據成員是私有的private,當繼承的時候,如果不註明繼承方式,則是私有private繼承

結構體正好相反,它定義是默認的數據成員是公有的public,同時它的默認繼承方式也是公有public的


4.設計類的函數時,應該將其設計爲成員函數、非成員函數還是友元函數?

答:首先簡單說一下它們之間的區別

成員函數與非成員函數最大的區別是---------------成員函數可以爲虛函數,而其他的函數不可以爲虛函數(最大最明顯的區別)。

友元函數相當於該類的一個朋友友元,它可以訪問該類的所有數據成員,但有時候朋友多並不是什麼好事,所以,在設計類的時候,如果這個函數不能爲成員函數但同時它又必須訪問到該類的數據成員(輸入>> 輸出<< 操作符重載)此時再設計成友元,如果可能不設計爲友元那就儘量這樣。

用一個例子來教我們怎麼判斷這個函數是設計成成員函數還是非成員函數!

下面是一個分數的類,其中有個實現分數乘法的函數,我們暫且先將它設計爲類的成員函數來討論

class Rational{
public:
	Rational( int numerator = 0 , int denominator = 1);
	int numerator() const;
	int denominator() const;
	//分數的乘法,開始設計成類的成員函數
	const Rational operator*(const Rational &rhs) const;
private:
	int n , d;//分別表示分子和分母
};
int Rational::numerator() const
{
	return n;
}
int Rational::denominator() const
{
	return d;
}
const Rational Rational::operator *(const Rational &rhs) const
{
	Rational result;
	result.n = n * rhs.n;
	result.d = d * rhs.d;
	return result;
}

看上述代碼,將Rational類中的分數乘法設計爲類的成員函數看似沒什麼錯誤,看下面的實例就知道了

Rational oneHalf(1,2);
Rational oneEight(1,8);
Rational res;
res = oneHalf * oneEight;
res = oneHalf *2;
res = 2 * oneHalf;//報錯

如果我們寫成上述的成員函數,那麼乘號*左邊一定得是Rational對象,因爲成員函數的形式決定了這樣,

在此你可能會說不對,兩邊都必須是Rational對象,乘號*右邊可以爲int對象,原因如下:

當我們在類中的構造函數設計爲

Rational( int numerator = 0 , int denominator = 1);

而不是

explicit Rational( int numerator = 0 , int denominator = 1);

這二者的區別就是,當函數的參數是類類型的時候,當你傳入函數的參數並不是類類型,恰該類類型的構造函數沒有申明explicit,那麼便會產生隱式轉換,使int ------->   Rational,其內部的轉換過程大致如下:

//運用構造函數的隱式轉換產生一個臨時的類類型對象
const Rational temp(2);
//用該臨時對象替換int的值
res = oneHalf * temp;

再次說明:

只有當類類型的構造函數沒有聲明explicit時纔會產生類型轉換

這裏的類型轉換隻針對於出現在參數表上的類類型形參有效,而對於(*this)是無效的,所以最後一個實例會報錯,因爲左邊不會進行轉換。

所以這樣看來將分數乘法的這個函數設計爲成員函數顯然不合理,因爲沒辦法乘號*左邊是int類型的數據,這跟現實不符合。

有人說了再寫一個類似能實現左邊是int值的函數就可以了,別忘了我們設計類最初要遵循的:儘量接口最小化,我們完全可以通過一個函數實現所有的類型的分數相乘,爲什麼要再寫一個函數呢,如果再寫一個函數就違背了接口最小化的原則。

我們可以這樣改變operator*函數 , 將其設計爲非成員函數,如下所示

const Rational operator *(const Rational &lhs , const Rational &rhs) 
{
	return Rational(lhs.numerator() *rhs.numerator() , lhs.denominator() * rhs.denominator());
}

這樣參數爲兩個均爲Rational類的引用,都爲參數,在構造函數不爲explicit時,均可以進行隱式轉換,實現了左右兩邊都可以是int型數據的可能。同時類中的成員設計爲私有,通過numerator() 和denominator()來訪問,提高了類的封裝性,參數的形式採用了引用的方式,當調用該函數時,不用複製實參,提高了效率,返回值採用了const的形式,避免了分數乘積的結果被寫的危險,即有效率又有安全性的寫法。

很多情況下,都需要重載輸入輸出操作符,常常爲了我們的編程習慣,把輸入輸出操作符重載的函數設計成了類的非成員友元函數。

istream& operator>>(istream &in , T &object)
{//因爲輸入改變了對象的狀態,參數不能爲const
	//輸出類的成員操作
	return in;
}

ostream& operator<<(ostream &out , const T &object)
{//輸出沒有改變對象的狀態,設置爲const引用的方式

	return out;
}

結論

虛擬函數必須爲類的成員函數

類的加減乘除運算符重載常常設計爲類的非成員函數

輸入輸出操作符重載的函數一定不能爲類的成員函數,常常爲類的友元函數

只有非成員函數才能在其左端對象(形參)身上使用類型轉換


5.函數的參數儘量使用傳址方式,而少使用傳值方式

這條幾乎是c++領域中公認的規則,在編寫函數中儘量使用引用(傳址)方式,而少用c語言中的傳值方式

原因有下面兩點

傳址方式效率比較高,不用在調用函數的時候複製實參調用拷貝構造函數,通常將對象以傳值的方式進行時,將實參複製給形參需要調用copy constructor 當返回的時候將返回值傳回對象又調用copy constructor 完了之後對局部對象會析構掉,還要調用析構函數,這樣的效率可知。

另外,當使用傳址的方式時,可以避免派生類對象傳入參數爲基類對象的函數時發生的切割現象,當用傳址的方式,該基類對象的引用綁定的是派生類對象,所以在執行這個函數裏中如果該對象調用了虛函數,那麼就會根據其綁定的動態對象來決定執行基類還是派生類的對象,這樣很容易達到我們的目的。


6. 當返回值是對象時,儘量不要採用傳回引用的方式

其實這點我感覺Effective c++沒有說清楚,我認爲,當函數是類的成員函數,通常返回的應該是該類的引用,爲什麼呢?因爲Effective c++上說這條的原因是,如果返回的是對象,採用引用的方式,通常該引用指向不存在的對象,但是在類的成員函數中,常以T&作爲返回值,是因爲調用該成員函數的對象肯定存在我們常常返回時*this,所以對於成員函數而言返回*this不可能指向不存在的對象。

所以我感覺書上這點應該指的是類的非成員函數,當類的非成員函數返回對象時,我們的確不要返回該對象的引用。

傳引用無非目標就是避免調用構造函數使效率提高,但是因爲傳回的引用必然要綁定對象(因爲引用其實就是別名,爲某個對象某個變量起了另外一個名字,改變這個別名也就等於改變了本身,操作這個引用也就等於操作了本身) ,所以我們必然要產生一個對象,這樣返回時,引用才能指向一個對象,棧空間和堆空間中產生

凡是局部變量都是在棧空間中產生的,

凡是通過new出來的都是在堆空間中產生的

還是拿上面的那個分數乘法的例子繼續討論

//傳回非引用的對象
const Rational operator *(const Rational &lhs , const Rational &rhs) 
{
	return Rational(lhs.numerator() *rhs.numerator() , lhs.denominator() * rhs.denominator());
}
//傳回引用的對象
const Rational& operator*(const Rational &lhs , const Rational &rhs)
{
	//在棧空間產生result
	Rational result(lhs.numerator() *rhs.numerator() , lhs.denominator() * rhs.denominator());
	return result;
}

const Rational& operator*(const Rational &lhs , const Rational &rhs)
{
	//在堆空間中產生result
	Rational *result = new Rational(lhs.numerator() *rhs.numerator() , lhs.denominator() * rhs.denominator());
	return *result;
}

首先,在棧空間中產生的result,仍然調用了構造函數,效率完全沒有提高,另外一個最大的bug是你傳回了局部對象,這樣當函數執行完後局部對象就析構不存在了,這是非常大的錯誤。

如果在堆空間中產生result,new產生的還是需要調用構造函數,同時也存在一個bug,那就是你new了什麼時候delete哈!new和delete必須要配對產生哈!

所以,在非成員函數中,如果返回的是對象時,儘量返回值儘量爲對象而不應該爲對象的引用

7.什麼時候應該使用const?

其實,在初學c++的時候感覺最鬱悶的就是這個const關鍵字了,它無時不刻存在所有的c++代碼中,讓我看得頭暈眼花。

const表示常量,在定義全局變量時我們常用到,如下所示

const int M = 1000000007;
const double ASPECT_RATIO = 1.653;

這是在全局作用域定義一些常量,別告訴我你還在用#define,可以改改舊的c語言習慣了哈!

另外在類的成員函數的末尾也經常見到這個關鍵字const,如下所示

int numerator() const;
int denominator() const;

這表示調用該成員函數的對象的數據成員不可更改,說的簡單點就是這個函數中隱藏的this指針所指向的對象時const對象(注意,指針並不是const,而是指向的對象時const對象)

在函數的返回值時有時也能見到const

這裏表示返回的對象時const,不能被寫,只能讀,函數傳回的是一個常量值。如下所示

const Rational operator *(const Rational &lhs , const Rational &rhs) 
{
	return Rational(lhs.numerator() *rhs.numerator() , lhs.denominator() * rhs.denominator());
}
Rational a , b , c;
(a*b) = c //報錯,這樣是錯誤的,返回的是const常量,不能賦值

常常函數的參數我們也使用const 引用的方式

此處,如果在函數中不打算改變參數的數據成員,就儘量設置成const,這樣當你不注意在函數體內試圖改變參數時編譯器便會給提醒。

怎麼去區別const關鍵字是限制指針還是變量的,下面是一種最簡單的方法

const char *p = "Hello"; //非const指針指向const變量
char * const p = "Hello";//const指針指向非const變量
const char * const p = "Hello";//const指針指向const變量

以*號爲分界線,const在左邊就是限制變量,即指向的是const的變量,const在*號右邊指的是指針不能修改,const指針。


8.如果不想用編譯器產生的默認函數,儘量顯示的拒絕這個函數

怎麼去拒絕編譯器產生的默認函數,將它定義爲類的私有方式即可,這樣實例化的對象也可以是客戶就沒法調用這個函數,或者嘗試去調用這個函數時,編譯器會提示錯誤。

爲什麼要去拒絕編譯器爲我們產生的默認函數,比如默認的賦值操作符

當我們定義數據類的時候,數組是不能賦值的,需要循環纔可以,所以在設計類的時候,便不想允許這個函數存在,雖然自己沒有編寫,但是編譯器依然還會給我們合成一個,所以此時我們就必須顯示指出這個函數不能調用,如下所示

template<class T>
class Array{
private:
	//顯示的指出不定義這個函數
	Array& operator=(const Array &rhs);
};


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章