先寫出關於對象傳遞和對象返回的總結:相對函數來說,如果是傳遞對象請使用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
}
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,也就是說,請不要在接收函數返回值的時候吝嗇調用構造函數和析構函數。