條款26:盡可能延后變量定義式的出現(xiàn)時間
- 考察下面的示例代碼:
void Foo()
{
std::string myStr;
if (條件) {
// 沒用用到myStr
return;
}
// 使用myStr變量的代碼
}
很顯然,這里的myStr提前定義了,并且會帶來額外的默認(rèn)構(gòu)造函數(shù)的開銷,雖然在這個例子中微乎其微。
在定義一個類對象時,盡可能使用其帶參數(shù)的構(gòu)造函數(shù),而不是先使用默認(rèn)構(gòu)造函數(shù)然后再使用賦值語句操作。
在for循環(huán)中用到的臨時變量最好是在循環(huán)體中定義,這樣做的好處是臨時變量的作用域只在循環(huán)體中,避免了與外部變量的沖突。
for (int i = 0; i < N; i++) {
MyClass obj; // 在循環(huán)體內(nèi)定義變量,這是建議的做法。
// 循環(huán)體代碼
}
條款27:盡量少做轉(zhuǎn)型動作
C++中新型的類型轉(zhuǎn)換有:
- const_cast:用于去掉const變量的const屬性,但是如果通過轉(zhuǎn)換后的非const指針去修改一個const變量是造成未定義的后果。
const int val = 100;
int *p = const_cast<int*>(&val);
*p = 101; // undefined behavior
詳見:https://en.cppreference.com/w/cpp/language/const_cast
PS:個人覺得const_cast有點讓人摸不著頭腦,不知道為啥要設(shè)計這個轉(zhuǎn)換。
- dynamic_cast:“安全向下”轉(zhuǎn)型,用于將一個基類型指針轉(zhuǎn)換為子類型指針,存在額外開銷,并且需要判斷轉(zhuǎn)換后的結(jié)果是否為空。
class B {
public:
B() {}
virtual ~B() {}
};
class D : public B {
};
int main()
{
B* pb1 = new D();
B* pb2 = new B();
D* pd1 = dynamic_cast<D*>(pb1);
if (pd1 != nullptr) {
std::cout << "pd1 is ref to D obj" << std::endl;
} else {
std::cout << "pd1 is not ref to D obj" << std::endl;
}
D* pd2 = dynamic_cast<D*>(pb2);
if (pd2 != nullptr) {
std::cout << "pd2 is ref to D obj" << std::endl;
} else {
std::cout << "pd2 is not ref to D obj" << std::endl;
}
return 0;
}
- reinterpret_cast:常用于重新解釋一個數(shù)據(jù)結(jié)構(gòu),存在比較大的安全風(fēng)險。
- statitc_cast:對應(yīng)于C語言的中強制轉(zhuǎn)換。
使用建議:
- 盡可能使用C++新的轉(zhuǎn)型關(guān)鍵字
- 如無必要,盡量減少轉(zhuǎn)型,特別是dynamic_cast這種轉(zhuǎn)換,影響性能。
條款28:避免返回handles指向?qū)ο髢?nèi)部成分
- 一個類中如果定義了private成員變量,但同時又通過public成員函數(shù)返回了其指針或者引用,這樣就會間接的破壞了類的封裝性,原本private成員變量會被外部客戶直接使用。
- 返回類中成員變量的引用或者指針時,會增加“懸垂指針”問題的產(chǎn)生的風(fēng)險。因為,當(dāng)外部代碼獲取到這些引用或者指針后,其對應(yīng)的類實例對象可能在其它地方已經(jīng)釋放了。
條款29:為“異常安全”而努力是值得的
異常安全的函數(shù)需要實現(xiàn)以下要求:
- 不泄露任何資源:當(dāng)異常發(fā)生后,之前分配的資源不應(yīng)該沒有釋放。
- 不允許數(shù)據(jù)敗壞,主要是異常發(fā)生后要保證數(shù)據(jù)的一致性。
異常安全有三個級別的承諾:
- 基本承諾:異常發(fā)生后,系統(tǒng)的狀態(tài)不會出現(xiàn)不一致的情況,但是具體是什么狀態(tài)無法保證。
- 強烈保證:要么完全成功,要么退回到函數(shù)調(diào)用前的狀態(tài)。
- 不拋異常保證:函數(shù)執(zhí)行過程中保證不產(chǎn)生異常。
常用的異常安全實現(xiàn)方法是“copy and swap”方法,該方法步驟如下:
1、為你將要修改的對象,先建立一個副本;
2、在副本上進行需要的修改;
3、將原對象與副本對象進行置換操作,并且保證調(diào)用的是一個不會拋出異常的swap操作,通常是兩個指針變量之間的交換;
條款30:透徹了解inlining的里里外外
- inline函數(shù)的好處是可以像函數(shù)那樣調(diào)用,但是卻沒有函數(shù)調(diào)用的開銷;
- 帶來的問題是會增加程序代碼的大小,因為會在調(diào)用的地方展開。所以一般是將常用的并且短小的函數(shù)設(shè)置成inline。
- class類定義體中的實現(xiàn)函數(shù)默認(rèn)設(shè)置成inline方式。
- 函數(shù)模板不要設(shè)置成inline方式,雖然一般是在頭文件中定義。
條款31:將文件間的編譯依存關(guān)系降至最低
我們在聲明一個類時,如果成員變量中包含了其它類時,則需要引用(include)其它類的頭文件,因為編譯器在編譯這個類時需要知道每個成員變量占用多大的空間。在這里,該類的聲明就在編譯層面與其它類產(chǎn)生了依存關(guān)系。
為了減少這種依存關(guān)系,在類的定義中需要避免在成員變量中定義其它類的對象,如果一定要定義,最好采用引用或者指針(包括智能指針)形式。這時候,可以在類的頭文件中采用前置聲明的方式來引用其它類,而不是直接inlcude其它頭文件。
舉例:存在一個Date類表示日期,Address類表示地址,以及Person類表示一個人,并且提供獲取此人生日和地址的方法。
- Date.h
#ifndef UNTITLED6_DATE_H
#define UNTITLED6_DATE_H
class Date {
};
#endif //UNTITLED6_DATE_H
- Address.h
#ifndef UNTITLED6_ADDRESS_H
#define UNTITLED6_ADDRESS_H
class Address {
};
#endif //UNTITLED6_ADDRESS_H
- Person.h
#ifndef UNTITLED6_PERSON_H
#define UNTITLED6_PERSON_H
class Address; // 前置聲明
class Date; // 前置聲明
class Person {
public:
Address GetAddr();
Date GetDate();
};
#endif //UNTITLED6_PERSON_H
- Person.cpp
#include "Person.h"
#include "Address.h"
#include "Date.h"
Address Person::GetAddr()
{
Address tmp;
return tmp;
}
Date Person::GetDate()
{
Date tmp;
return tmp;
}
可以看到Person.h中并沒有include Date.h以及Address.h,但是在cpp實現(xiàn)文件中需要引用涉及到的類頭文件。
通過這種方式,將類的編譯從相依于定義式轉(zhuǎn)變?yōu)橄嘁烙诼暶魇健?/p>
在接口類的設(shè)計中,為了盡可能降低接口類編譯的依存關(guān)系,也采用了這種思路,有兩種做法:
- handle class:在接口類中定義一個實現(xiàn)類的handle(引用或者指針),由實現(xiàn)類完成與其他類的編譯依存關(guān)系。
- interface class:接口類只包含虛函數(shù)功能接口,由具體的實現(xiàn)類繼承接口類進行實現(xiàn),采用簡單工廠模式創(chuàng)建具體的實現(xiàn)類對象。