讀effetive c++筆記之對象傳遞和對象返回

先寫出關於對象傳遞和對象返回的總結:相對函數來說,如果是傳遞對象請使用pass-by-reference 而對象返回請使用pass-by-value.


  爲什麼對象傳遞的時候要用pass-by-reference,而不用pass-by-value呢,舉個例子,如下:

#include<iostream>
#include<string>
using  namespace std;

class Person {
public:
	Person(string _name, string _address):name(_name), address(_address) {
	}
	~Person() {}
private:
	string name;
	string address;
};

class Student:public Person {
public:
	Student(string _name, string _address, string _schoolName, string _schoolAddress)
		:Person(_name, _address), schoolName(_schoolName), schoolAddress(_schoolAddress){
	}
	~Student() {}
private:
	string schoolName;
	string schoolAddress;
};

Student student("tom", "yilu", "mit", "America");

bool isValidateStudent(Student s) {
	// to do
}


當把上面的student對象作爲參數傳遞給下面的:isValidateStudent函數的時候會發生什麼?Student的默認拷貝構造函數會構造一個student對象的副本s,並將student中存儲的內容複製到副本對象s中,當函數結束的時候,會對這個副本調用其析構函數,之後我們在分析一下Student內部的結構,算上從Person繼承的成員變量,它一共內涵4個string成員變量,所以在把student對象內容複製給s的時候,要調用4次string對象的構造函數,在再有析構s的時候,要調用4次的string的析構函數,所以加上對s的Person部分的構造,一共要調用6次構造函數和6次析構函數。這樣看來,進行值傳遞的代價是不是很大了。但是如果我們使用pass-by-reference進行傳遞的話,就不用有pass-by-value那樣的開銷,但是調用函數isValidateStudent的人也許不放心,因爲他怕函數的內部改變了student原有的值,所以在引用傳遞的時候,要加上const,防止傳遞的對象被意外更改。

pass-by-value會引起的另一個問題就是對象切割,還是以上面那個例子做基礎,做稍微的更改,如下:

#include<iostream>
#include<string>
using  namespace std;

class Person {
public:
	Person(string _name, string _address):name(_name), address(_address) {
	}
	~Person() {}
	void liveWhere() {
		cout << address << endl;
	}
	virtual void likingWhat();
private:
	string name;
	string address;
};

class Student:public Person {
public:
	Student(string _name, string _address, string _schoolName, string _schoolAddress)
		:Person(_name, _address), schoolName(_schoolName), schoolAddress(_schoolAddress){
	}
	~Student() {}
	virtual void likingWhat() {
		// to do 
	}
private:
	string schoolName;
	string schoolAddress;
};

Student student("tom", "yilu", "mit", "America");

void isLikingWhat(Person p) {
	cout << p.likingWhat() << endl;
}

上面的例子假設社會上的每一個羣體都有自己獨特的喜好,那麼函數isLikingWhat將接受一個基類Person的對象,並打印出每個對象所喜歡的東西,可以看出,這是對應多臺的應用。然而當傳遞student對象給isLikingWhat函數的時候,他不知道應該怎麼來構造這個對象,因爲他的參數類型是Person,它不能確定,繼承於Person的子類對象是什麼,所以在參數傳遞的時候,只有student對象的Person部分被複制,而屬於student專有的內容被拋棄了,則在isLikingWhat函數進行打印的時候他調用的是Person的函數,而不是Student實現的函數。這就是pass-by-value造成的對象切割。如果我們換成引用傳遞的話,我們應該還記得,子類的對象是可以用來給父類對象的引用賦值的。所以當在引用傳遞的時候,會根據虛函數列表找到相應的函數進行調用。

總結一下:pass-by-reference解決了兩個pass-by-value不能解決的問題,一個是構造函數和析構函數調用引起的資源浪費,另一個是對象切割。

既然能看到pass-by-reference這麼多好處,那麼我們在函數返回值的時候也考慮一下使用引用返回,怎麼樣呢,因爲在函數返回值的時候要進行對象拷貝,把返回的值拷貝到接收對象之中,如果讓接收對象只是一個引用,那麼就省去了構造函數和析構函數調用所帶來的代價。

考慮下面這樣一個例子:

#include<iostream>
#include<string>
using  namespace std;

class Rational {
public:
	Rational(int _n, int _d) {
		n = _n;
		d = _d;
	}
	~Rational() {
	}
	const Rational& operator*(const Rational& lhs, const Rational &rhs);
private:
	int n;
	int d;
};

const Rational& Rational::operator *(const Rational &lhs, const Rational &rhs) {
	Rational result = Rational(lhs.n * rhs.n, lhs.d * rhs.d);
	return result;
}

可以看看這個operator*函數,首先它建立了一個Rational對象,因爲是函數的內部對象,所以應該放在棧中,之後函數返回對對象result的引用。想想會出現什麼問題,當oper*函數結束的時候,result對象也被析構了,它在內存中不在存在,但是函數之外還是會對它進行引用,這就會像懸垂指針一樣,亂指內存,當有其他函數應用它的時候,就會出現問題,或者造成整個程序的崩潰,(我寫過這樣的程序,因爲野指針造成運行在手機上的程序崩潰,手機死機)。既然這樣,那我們就會馬上想到另外一種方法,就是用堆,這樣,函數結束的時候,對象就會存在,引用它的時候就不會出問題了,看看下面的實現:

const Rational& Rational::operator *(const Rational &lhs, const Rational &rhs) {
	Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
	return *result;
}

把對象存儲在堆上我們就放心了,但是還要考慮的問題就是,誰來釋放我們分配在對象的對象呢,釋放對象一個很好的時機就是沒有人引用它的時候進行釋放。那麼請看下面這個例子:

Rational a, b ,c , d; 

d = a * b * c;

按照上面operator*的實現方法,new要被調用兩次,但是隻有一個引用來指向兩次中最後一次分配的Rational,第一次分配的就沒有人最管它,想釋放的時候都不知道怎麼釋放了。

總結上面兩點:如果將對象放在heap或者stack上都會造成問題,那麼我們可以考慮另外一個方法,就是把對象放在可以長期保存又有人去管理的地方怎樣呢-靜態存儲區。

實現如下:

const Rational& Rational::operator *(const Rational &lhs, const Rational &rhs) {
	static Rational result =Rational(lhs.n * rhs.n, lhs.d * rhs.d);
	return result;
}

這樣實現,我們既不用考慮分配在stack上的問題,也不用考慮分配在heap上的問題,但是考慮下面這個調用:

bool operator==(const Rational &lhs, const Rational &rhs), 有Rational a, b, c, d; 
if (a * b == c  * d) {
} else {
}

大家能猜到結果麼,結果就是不管a,b,c,d 是什麼值,條件判斷的結果總是真,來分析一下吧,operator返回的是靜態變量的引用,那麼不管怎麼改變它的值,引用都是不知道的, 上面的條件判斷中,一共調用了兩次operator*, 第一次是a*b,結果存儲的是a*b的值,而第二次是c*d的值,仔細想一想,operator*返回的引用總是指向最新的值,而不管它被調用了多少次,因爲他是靜態變量。


通過以上的分析,總結出一點:當向函數內部傳遞參數的時候,請儘量使用pass-by-reference,當從函數向外部傳遞的時候,請儘量使用pass-by-value,也就是說,請不要在接收函數返回值的時候吝嗇調用構造函數和析構函數。




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