第十三章 高級引用和指針 13.1按引用傳遞以提高效率 每次將值按對象傳入函數是,都將創建該對象的一個備份。每次按值從函數返回一個對象時,也將創建其備份。 對於用戶創建的大型對象,備份的代價很高。這將增加程式占用的記憶體量,而程式的運行速度將更慢。 在棧中,用戶創建的對象的大小為其成員變數 ...
第十三章 高級引用和指針
13.1按引用傳遞以提高效率
每次將值按對象傳入函數是,都將創建該對象的一個備份。每次按值從函數返回一個對象時,也將創建其備份。
對於用戶創建的大型對象,備份的代價很高。這將增加程式占用的記憶體量,而程式的運行速度將更慢。
在棧中,用戶創建的對象的大小為其成員變數的大小之和,而每個成員變數本身也可能是用戶創建的對象。從性能和記憶體耗用方面說,將如此大的結構傳入棧中的代價非常高。
當然,還有其它代價。對於您創建的類,每次創建備份時,編譯器都將調用一個特殊的構造函數:複製構造函數(拷貝構造函數)。
對於大型對象,調用這些構造函數和析構函數在速度和記憶體耗用方面的代價非常高。
程式清單13.1 ObjectRef.cpp
#include <iostream>
class SimpleCat {
public:
SimpleCat(); //預設構造函數
SimpleCat(SimpleCat &); //拷貝構造函數
~SimpleCat();
};
SimpleCat::SimpleCat() {
std::cout << "Simple Cat Constructor ..." << std::endl;
}
SimpleCat::SimpleCat(SimpleCat &) {
std::cout << "Simple Cat Copy Constructor ..." << std::endl;
}
SimpleCat::~SimpleCat() {
std::cout << "Simple Cat Destructor ..." << std::endl;
}
SimpleCat FunctionOne(SimpleCat theCat);
SimpleCat *FunctionTwo(SimpleCat *theCat);
int main() {
std::cout << "Making a Cat ..." << std::endl;
SimpleCat simpleCat;
std::cout << "Calling FunctionOne..." << std::endl;
FunctionOne(simpleCat);
std::cout << "Calling FunctionTwo..." << std::endl;
FunctionTwo(&simpleCat);
return 0;
}
SimpleCat FunctionOne(SimpleCat theCat) {
std::cout << "Function One. Returning ..." << std::endl;
return theCat;
}
SimpleCat *FunctionTwo(SimpleCat *theCat) {
std::cout << "Function Two. Returning ..." << std::endl;
return theCat;
}
可以看到程式中FunctionOne按值傳遞,FunctionTwo按址傳遞,FunctionOne的調用會產生一次拷貝構造,返回值是SimpleCat類型,所以也會產生一次拷貝構造,然後FunctionOne結束時,兩個拷貝出來的對象(一個是調用時產生,一個是返回時產生)就會被析構函數進行析構。
調用FunctionTwo,因為參數是按引用傳遞,所以不會進行複製備份,也就不會調用拷貝構造函數,所以也就不會有輸出。所以結果中,FunctionTwo所觸發的輸出語句僅有Function Two. Returning ...一句,且並未調用拷貝構造和析構函數。
13.2傳遞const指針
這部分其實前面十一章的筆記有提到。
雖然將指針傳遞給函數效率更高(比如上面的程式),但這也使得對象有被修改的風險。按值傳遞雖然效率較低,但是只是將複製品傳遞,所以並不會影響到原品,也就較按址傳遞多了一層保護。
要同時獲得按值傳遞的安全性和按址傳遞的效率,解決的辦法是傳遞一個指向該類常量的指針(也可為常量的常量指針,即const 類型 *const 指針變數),這樣就不能修改所指向對象的值(如果是常量的常量指針,不僅不能修改所指向對象的值,也不能修改自身的值,即不能指向其它對象)
程式清單13.2 ConstPasser.cpp
#include <iostream>
class SimpleCat {
private:
int itsAge;
public:
SimpleCat();
SimpleCat(SimpleCat &);
~SimpleCat();
int getAge() const { return itsAge; }
void setAge(int age) { itsAge = age; }
};
SimpleCat::SimpleCat() {
std::cout << "Simple Cat Constructor ..." << std::endl;
itsAge = 1;
}
SimpleCat::SimpleCat(SimpleCat &) {
std::cout << "Simple Cat Copy Constructor ..." << std::endl;
}
SimpleCat::~SimpleCat() {
std::cout << "Simple Cat Destructor ..." << std::endl;
}
const SimpleCat *const
FunctionTwo(const SimpleCat *const simpleCat); //指向常量的常量指針
int main() {
std::cout << "Making a cat ..." << std::endl;
SimpleCat simpleCat;
std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
<< std::endl;
int age = 5;
simpleCat.setAge(age);
std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
<< std::endl;
std::cout << "Calling FunctionTwo..." << std::endl;
FunctionTwo(&simpleCat);
std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
<< std::endl;
return 0;
}
const SimpleCat *const FunctionTwo(const SimpleCat *const simpleCat) {
std::cout << "Function Two. Returning ..." << std::endl;
std::cout << "simpleCat is now " << simpleCat->getAge() << " years old"
<< std::endl;
// simpleCat->setAge(9);//報錯,無法對const類型對象進行修改
return simpleCat;
}
如果將註釋掉的代碼去掉註釋,程式則無法通過,所以可見將參數設置為常量的指針或者是常量的常量指針這種方式兼顧了址傳遞的高效與值傳遞的安全。
13.3作為指針替代品的引用
將上一個程式重寫,使用引用而非指針。
程式清單13.3 RefPasser.cpp
#include <iostream>
class SimpleCat {
private:
int itsAge;
public:
SimpleCat();
SimpleCat(SimpleCat &);
~SimpleCat();
int getAge() const { return itsAge; }
void setAge(int age) { itsAge = age; }
};
SimpleCat::SimpleCat() {
std::cout << "Simple Cat Constructor ..." << std::endl;
itsAge = 1;
}
SimpleCat::SimpleCat(SimpleCat &) {
std::cout << "Simple Cat Copy Constructor ..." << std::endl;
}
SimpleCat::~SimpleCat() {
std::cout << "Simple Cat Destructor ..." << std::endl;
}
const SimpleCat &FunctionTwo(const SimpleCat &simpleCat); //指向常量的常量指針
int main() {
std::cout << "Making a cat ..." << std::endl;
SimpleCat simpleCat;
std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
<< std::endl;
int age = 5;
simpleCat.setAge(age);
std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
<< std::endl;
std::cout << "Calling FunctionTwo..." << std::endl;
FunctionTwo(simpleCat);
std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
<< std::endl;
return 0;
}
const SimpleCat &FunctionTwo(const SimpleCat &simpleCat) {
std::cout << "Function Two. Returning ..." << std::endl;
std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
<< std::endl;
// simpleCat.setAge(9);//報錯,無法對const類型對象進行修改
return simpleCat;
}
可見,改用引用效率和代價較指針沒變,但是使用起來較指針簡單。
13.4什麼情況下使用引用以及什麼情況下使用指針
一般而言,c++程式員更喜歡使用引用而不是指針,因為他們更清晰,使用起來更容易。然而,引用不能重新賦值,如果需要依次指向不同的對象,就必須使用指針。引用不能為NULL,因此如果要指向的對象可能為NULL,就必須使用指針,而不能使用引用。如果要從堆中分配動態記憶體,就必須使用指針。
13.5指向對象的引用不在作用域內
c++程式員學會按引用傳遞後,常常會瘋狂地使用這種方式。然而,過猶不及,別忘了,引用始終是另一個對象的別名,如果將引用傳入或傳出函數,務必自問:它是哪個對象的別名?當我使用引用時,這個對象還存在嗎?
程式清單13.4 ReturnRef.cpp
#include <iostream>
class SimpleCat {
private:
int itsAge;
int itsWeight;
public:
SimpleCat(int age, int weight);
~SimpleCat(){};
int getAge() { return itsAge; }
int getWeight() { return itsWeight; }
};
SimpleCat::SimpleCat(int age, int weight) : itsAge(age), itsWeight(weight) {}
SimpleCat &TheFunction();
int main() {
SimpleCat &rCat = TheFunction();
int age = rCat.getAge();
std::cout << "rCat is " << age << " years old!" << std::endl;
return 0;
}
SimpleCat &TheFunction() {
SimpleCat simpleCat(5, 9);
return simpleCat;
}
這個程式一般編譯器是會報錯的,因為TheFunction()函數返回的對象引用是在TheFunction()函數內創建的,但是TheFunction()返回時,創建的這個對象是已經被銷毀了的,也就是返回的引用指向了一個不存在的對象(空引用被禁止),從而被編譯器禁止該程式運行。
13.6返回指向堆中對象的引用
你是否認為:如果TheFunction()函數在堆中創建對象,這樣,返回的時候這個對象就依然存在,也就自然解決了上面的空引用問題。
這種方法的問題在於,使用完該對象後,如何釋放為它分配的記憶體?
程式清單13.5 Leak.cpp
#include <iostream>
class SimpleCat {
private:
int itsAge;
int itsWeight;
public:
SimpleCat(int age, int weight);
~SimpleCat(){};
int getAge() { return itsAge; }
int getWeight() { return itsWeight; }
};
SimpleCat::SimpleCat(int age, int weight) : itsAge(age), itsWeight(weight) {}
SimpleCat &TheFunction();
int main() {
SimpleCat &rCat = TheFunction();
int age = rCat.getAge();
std::cout << "rCat is " << age << " years old!" << std::endl;
std::cout << "&rCat is " << &rCat << std::endl;
SimpleCat *pCat = &rCat;
delete pCat; //原對象被釋放了,這時候rCat不就成了空引用了嗎?
return 0;
}
SimpleCat &TheFunction() {
SimpleCat *simpleCat = new SimpleCat(5, 9);
std::cout << "SimpleCat: " << simpleCat << std::endl;
return *simpleCat;
}
不能對引用調用delete,一種聰明的解決方案是創建一個指針並將其指向該引用的對象的地址,這個時候調用delete也就能釋放分配的記憶體,但是往後引用名不就成了空引用嗎?雖然編譯器檢測不到且能正常運行,所以這就埋了一個雷,指不定什麼時候炸了。(正如之前指出,引用必須始終是一個實際存在的對象的別名。如果它指向的是空對象,那麼程式仍是非法的)
對於這種問題,實際上有兩種解決方案。一種是返回一個指針,這樣可以在使用完該指針之後將其刪除。為此,需要將返回值類型聲明為指針而非引用,並返回該指針。
另一種更好的解決方案是:在發出調用的函數中聲明對象,然後將其按引用傳遞給TheFunction()。這種方法的優點是,分配記憶體的函數(發出調用的函數)也負責釋放記憶體。
13.7誰擁有指針
程式在堆中分配的記憶體時將返回一個指針。必須一直讓某個指針指向這塊記憶體,因為如果指針丟失(也就是沒有指針指向它了),便無法釋放該記憶體,進而導致記憶體泄露。
在函數之間傳遞記憶體塊時,其中一個函數“擁有”指針。通常,使用引用傳遞記憶體塊中的值,而分配記憶體塊的函數將負責釋放它,但這是一個大致規則,並非不可打破。
然而,讓一個函數分配記憶體,而另一個函數釋放很危險。在誰擁有指針方面不明確可能會導致兩個問題:忘記刪除指針或重覆刪除。無論哪種情況,都會給程式帶來嚴重的問題。編寫函數時,讓其負責釋放自己分配的記憶體是更安全的做法。