More Effective C++ 基礎議題(條款1-4)總結
- 2021 年 12 月 23 日
- 筆記
- C++, MoreEffective C++
More Effective C++ 基礎議題(條款1-4)總結
條款1:仔細區別pointers和references
- 如果有一個變數,其目的是用來指向(代表)另一個對象,但是也有可能它不指向(代表)這個變數,那麼應該使用
pointer
,因為可將pointer
設為null
,反之設計不允許變數為null
,那麼使用reference
- 以下這是有害的行為,其結果不可預期(C++對此沒有定義),編譯器可以產生任何可能的輸出
char *pc = 0; // 將 pointer 設定為null
char& rc = *pc; // 讓 refercence 代表 null pointer 的 解引值
- 沒有
null reference
, 使用reference
可能比pointers
更有效率,在使用reference
之前不需要測試其有效性
void printDouble(const double& rd)
{
cout < < rd; // 不需要測試rd,它
} // 肯定指向一個double值
//相反,指針則應該總是被測試,防止其為空:
void printDouble(const double *pd)
{
if (pd) // 檢查是否為NULL
{
cout < < *pd;
}
}
pointers
可以被重新賦值,指向另一個對象,reference
卻總是指向(代表)它最初獲得的哪個對象- 實現某些操作符。如operator[],操作符應返回某種「能夠被當作assignment賦值對象」
總結
當你知道你需要指向某個東西,而且絕不會改變指向其他東西,或是當你實現一個操作符而其語法需求無法由pointers達成,你就應該選擇reference。任何其他時候,請採用pointers
條款2:最好使用C++轉型操作符
- 舊式的C轉型方式,它幾乎允許你將任何類型轉換為任何其他類型,這是十分拙劣的
舊式轉型存在的問題:
- 例如將
pointer-to-const-object
轉型為一個pointer-to-non-const-object
(只改變對象的常量性),和將一個pointer-to-base-class-object
轉型為一個pointer-to-derived-class-object
(完全改變一個對象的類型),其間有很大的差異。但是傳統的C轉型動作對此並無區分 - 難以辨識,舊式轉型由一小對小括弧加上一個對象名稱(標識符)組成,而小括弧和對象名稱在C++的任何地方都有可能被使用
staic_cast:
static_cast
基本上擁有與 C 舊式轉型相同的威力與意義,以及相同的限制(如不能將struct轉型為int)。- 不能移除表達式的常量性,由
const_cast
專司其職 - 其他新式 C++ 轉型操作符適用於更集中(範圍更狹窄)的目的
(type) expression // 原先 C 的轉型寫碼形式
static_cast<type>(expression) // 使用 C++ 轉型操作符
const_cast:
const_cast
用來改變表達式的常量性(constness)或變易性(volatileness),使用const_cast
,便是對人類(編譯器)強調,通過這個轉型操作符,你唯一打算改變的是某物的常量性或變易性。這項意願將由編譯器貫徹執行。如果將const_cast
應用於上述以外的用途,那麼轉型動作會被拒絕
#include <iostream>
using namespace std;
class Widget {};
class SpecialWidget : public Widget {};
void update(SpecialWidget* psw);
SpecialWidget sw; // sw是個 non-const 對象
const SpecialWidget& csw = sw; // csw 確實一個代表sw的 reference
// 並視之為一個const對象
update(&csw); // 錯誤!不能及那個const SpecialWidget*
// 傳給一個需要SpecialWidget* 的函數
update(const_cast<SpecialWidget*>(&csw)); // 可!&csw的常量性被去除了
update((SpecialWidget*)&csw); // 可!但較難識別 C 舊式轉型語法
const_cast
最常見的用途就是將某個對象的常量性去除掉
dynamic_cast:
- 用來轉型繼承體系重「安全的向下轉型或跨系轉型動作」。也就是說你可以利用
dynamic_cast
,將「指向base ckass objects
的pointers
或references
」轉型為「指向derived(或sibling base)class objects
的pointers
或references
」,並得知轉型是否成功。如果轉型失敗,會以一個null
指針或一個exception
(當轉型對象是reference
)表現出來:
Widget *pw;
update(dynamic_cast<SpecialWidget*>(pw)); // 很好,傳給update()一個指針,指向pw所指的
// pw所指的SpecialWidget--如果pw
// 真的指向這樣的東西;否則傳過去的
// 將是一個 null 指針
void updateViaRef(SpecialWidegt& rsw);
updateViaRef(dynamic_cast<SpecialWidegt&>(*pw)); // 很好,傳給updateViaRef()的是
// pw所指的SpecialWidget--如果
// pw真的指向這樣的東西;否則
// 拋出一個exception
dynamic_cast
只能用來協助你巡航於繼承體系之中。它無法應用在缺乏虛函數(請看條款24)的類型身上,也不能改變類型的常量性(constness)- 如果不想為一個不涉及繼承機制的類型執行轉型動作,可使用
static_cast
;要改變常量性(constness),則必須使用const_cast
reinterpret_cast:
- 最後一個轉型操作符是
reinterpret_cast
。這個操作符的轉換結果幾乎總是與編譯平台息息相關。所以reinterpret_cast
不具移植性 reinterpret_cast
的最常用用途是轉換”函數指針”類型。
typedef void (*FuncPtr)(); // FuncPtr是個指針,指向某個函數
// 後者無須任何自變數,返回值為voids
FuncPtr funcPtrArray[10]; // funcPtrArray 是個數組
// 內有10個FuncPtrs
假設由於某種原因,希望將以下函數的一個指針放進funcPtrArray中
int doSomething();
如果沒有轉型,不可能辦到,因為doSomething
的類型與funcPtrArray
所能接受的不同。funcPtrArray
內各函數指針所指函數的返回值是void
,但doSomething
的返回值卻是int
funcPtrArray[0] = &doSomething; //錯誤!類型不符
funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething); //這樣便可通過編譯
某些情況下這樣的轉型可能會導致不正確的結果(如條款31),所以你應該盡量避免將函數指針轉型。
補充:
More Effective C++
沒有過多的對reinterpret_cast
操作符進行解釋,但我覺得應該對它進行更多說明,因為它實在是太強大了,也應該對使用規則做出足夠多的說明- reinterpret_cast通過重新解釋底層位模式在類型之間進行轉換。它將
expression
的二進位序列解釋成new_type
,函數指針可以轉成void*再轉回來。reinterpret_cast
很強大,強大到可以隨便轉型。因為他是編譯器面向二進位的轉型,但安全性需要考慮。當其他轉型操作符能滿足需求時,reinterpret_cast
最好別用。 - 更多了解可看cpp reference reinterpret_cast
總結:
在程式中使用新式轉型法,比較容易被解析(不論是對人類還是對工具而言),編譯器也因此得以診斷轉型錯誤(那是舊式轉型法偵測不到的)。這些都是促使我們捨棄C舊式轉型語法的重要因素
條款3:絕對不要以多態(polymorphically)方式處理數組
假設你有一個class BST
及一個繼承自BST的class BalancedBST
;
class BST {};
class BalancedBST : public BST {};
現在考慮有個函數,用來列印BSTs數組中的每一個BST的內容
void printBSTArray(ostream& s, const BST array[], int numElements)
{
for (int i = 0 ; i < numElements; ++i)
{
s << array[i]; // 假設BST objects 有一個
// operator<< 可用
}
}
當你將一個由BST對象組成的數組傳給此函數,沒問題:
BST BSTArray[10];
printBSTArray(cout, BSTArray, 10); // 運行良好
然而如果你將一個BalancedBST
對象所組成的數組交給printBSTArray
函數,會發生什麼事?
BalancedBST bBSTArray[10];
printBSTArrat(cout, bBSTArray, 10); // 可以正常運行嗎?
- 此時就會發生錯誤,因為array[i]代表的時
*(array+i)
,編譯器會認為數組中的每個元素時BST對象,所以array和array+i之間的距離一定是i*sizeof(BST) - 然後當傳入由
BalancedBST
對象組成的數組,編譯器會被誤導。它仍假設數組中每一元素的大小是BST的大小,但其實每一元素的大小是BalancedBST的大小。因此當BalancedBST
的大小不等於BST
的大小時,會產生未定義的行為 - 當嘗試通過一個·base class·指針,刪除一個由derived class objects組成的數組,上述的問題還會再次出現,下面是你可能做出的錯誤嘗試
void deleteArray(ostream& os,BST array[])
{
os << "Delete array,at address" <<
static_cast<void*>(array) << 'n';
delete []array;
}
編譯器看到這樣的句子
delete[] array;
會產生類似這樣的程式碼,問題也就跟之前一樣出現了
for(int i = the number of elements in the array-1; i >= 0; --i)
{
array[i].BST::~BST(); // 調用array[i]的 destructor
}
總結:
- 多態和指針算術不能混用,數組對象幾乎總是涉及指針的算術運算,數組和多態不要混用
條款4:非必要不提供default constructor
後續看過條款43,再回頭來補充
總結:
- 添加無意義的default constructors,也會影響classes的效率。如果class constructors可以確保對象的所有欄位都會被正確地初始化,為測試行為所付出的時間和空間代價都可以免除。如果default constructors無法提供這種保證,那麼最好避免讓default constructors出現。雖然這可能會對classes的使用方式帶來某種限制,但同時也帶啦一種保證:當你真的使用了這樣的classes,你可以預期它們所產生的對象會被完全地初始化,實現上亦富有效率