2 初始化和類型轉換
2.1 聲明、定義與初始化
規則2.1 禁止用memcpy、memset初始化非POD對象
說明:POD 全稱是“Plain Old Data”,是C++ 98標準(ISO/IEC 14882, first edition, 1998-09-01)
中引入的一個概念, POD類型主要包括int, char, float,double,enumeration,void,指標等原始
類型及其集合類型,不能使用封裝和面對對象特性(如使用者定義的構造/賦值/解構函式、基類、虛函
數等)。
由於非POD類型比如非集合類型的class對象,可能存在虛函數,記憶體布局不確定,跟編譯器有關,濫
用記憶體拷貝可能會導致嚴重的問題。
即使對集合類型的class,使用直接的記憶體拷貝和比較,破壞了資訊隱蔽和資料保護的作用,也不提倡
memcpy、memset操作。
樣本:×××產品程式異常退出(core dump)。
經過現場環境的模似,程式產生COREDUMP,其原因是:在初始化函數內使用memset(this, 0,
sizeof(*this))進行了類的初始化,將類的虛函數表指標被清空,從而導致使用null 指標。
解決方案:使用C++建構函式初始化,不要使用memset函數初始化類對象。
建議2.1 變數使用時才聲明並初始化
說明:變數在使用前未賦初值,是常見的低級編程錯誤。使用前才聲明變數並同時初始化,非常方便
地避免了此類低級錯誤。
在函數開始位置聲明所有變數,後面才使用變數,範圍覆蓋整個函數實現,容易導致如下問題:
程式難以理解和維護:變數的定義與使用分離。
變數難以合理初始化:在函數開始時,經常沒有足夠的資訊進行變數初始化,往往用某個預設的
空值(比如零)來初始化,這通常是一種浪費,如果變數在被賦於有效值以前使用,還會導致錯誤。
遵循變數範圍最小化原則與就近聲明原則, 使得代碼更容易閱讀,方便瞭解變數的類型和初始值。
特別是,應使用初始化的方式替代聲明再賦值。
樣本:
//不好的例子:聲明與初始化分離
string name; //聲明時未初始化:調用預設建構函式
//…….
name=”zhangsan”; //再次調用賦值操作符函數;聲明與定義在不同的地方,理解相對困難
//好的例子:聲明與初始化一體,理解相對容易
string name(”zhangsan”); //調用一次建構函式
建議2.2 避免建構函式做複雜的初始化,可以使用“init”函數
說明:正如函數的變數都在函數內部初始化一樣,類資料成員最好的初始化場所就是建構函式,資料
成員都應該盡量在建構函式中初始化。
以下情況可以使用init()函數來初始化:
需要提供初始化返回資訊。
資料成員初始化可能拋異常。
資料成員初始化失敗會造成該類對象初始化失敗,引起不確定狀態。
資料成員初始化依賴this指標:建構函式沒結束,對象就沒有構造出來,建構函式內不能使用this
成員;
資料成員初始化需要調用虛函數。在建構函式和解構函式中調用虛函數,會導致未定義的行為。
樣本:資料成員初始化可能拋異常:
class CPPRule
{
public:
CPPRule():size_(0), res (null) {}; //僅進行值初始化
long init(int size)
{
//根據傳入的參數初始化size_, 分配資源res
}
private:
int size_;
ResourcePtr* res;
};
//使用方法:
CPPRule a;
a.init(100);
建議2.3 初始化列表要嚴格按照成員聲明順序來初始化它們
說明:編譯器會按照資料成員在類定義中聲明的順序進行初始化,而不是按照初始化列表中的順序,
如果打亂初始化列表的順序實際上不起作用,但會造成閱讀和理解上的混淆;特別是成員變數之間存
在依賴關係時可能導致BUG。
樣本:
//不好的例子:初始化順序與聲明順序不一致
class Employee
{
public:
Employee(const char* firstName, const char* lastName)
: firstName_(firstName), lastName_(lastName)
, email_(firstName_ + "." + lastName_ + "@huawei.com") {};
private:
string email_, firstName_, lastName_;
};
類定義email_是在firstName_, lastName_之前聲明,它將首先初始化,但使用了未初始化的
firstName_和lastName_,導致錯誤。
在成員聲明時,應按照成員相互依賴關係按順序聲明。
建議2.4 明確有外部依賴關係的全域與靜態對象的初始化順序
說明:如果全域對象A的成員變數有外部依賴,比如依賴另外一個全域變數B,在A的建構函式中訪問B,
隱含的規則就是B先於A初始化,然而全域與靜態對象的初始化與析構順序未有嚴格定義,無法確保B
已經完成初始化,而每次產生可執行程式都可能發生變化,這類BUG難以定位。
通常採用單件(Singleton)模式或者把有依賴關係的全域對象放在一個檔案中定義來明確初始化順序。
同一個檔案中,若全域對象a在全域對象b之前定義,則a一定會在b之前初始化;但是不同檔案中的全
局對象就沒有固定的初始化順序。可以在main()或 pthread_once() 內初始化一個運行期間不回收的
指標。
2.2 類型轉換
避免使用類型分支來定製行為:類型分支來定製行為容易出錯,是企圖用C++編寫C代碼的明顯標誌。
這是一種很不靈活的技術,要添加新類型時,如果忘記修改所有分支,編譯器也不會告知。使用模板
和虛函數,讓類型自己而不是調用它們的代碼來決定行為。
規則2.2 使用C++風格的類型轉換,不要使用C風格的類型轉換
說明:C++的類型轉換由於採用關鍵字,更醒目,更容易尋找,編程中強迫程式員多停留思考片刻,謹
慎使用強制轉換。
C++使用const_cast, dynamic_cast, static_cast, reinterpret_cast等新的類型轉換,它們允許用
戶選擇適當層級的轉換符,而不是像C那樣全用一個轉換符。
dynamic_cast:主要用於下行轉換,dynamic_cast具有類型檢查的功能。dynamic_cast有一定的開銷,
建議在調測代碼中使用。
static_cast:和C風格轉換相似可做值的強制轉換,或上行轉換(把衍生類別的指標或引用轉換成基類的
指標或引用)。該轉換經常用於消除多重繼承帶來的類型歧義,是相對安全的。下行轉換(把基類的指
針或引用轉換成衍生類別的指標或引用)時,由於沒有動態類型檢查,所以不安全的,不提倡下行轉換。
reinterpret_cast:用於轉換不相關的類型。reinterpret_cast強制編譯器將某個類型對象的記憶體重
新解釋成另一種類型,相關代碼可移植不好。建議對reinterpret_cast<> 的用法進行注釋,有助於減
少維護者在看到這種轉換時的顧慮。
const_cast:用於移除對象的 const屬性,使對象變得可修改。
樣本:
extern void Fun(DerivedClass* pd);
void Gun(BaseClass* pb)
{
//不好的例子: C風格強制轉換,轉換會導致物件版面配置不一致,編譯不報錯,運行時可能會崩潰
DerivedClass* pd = (DerivedClass *)pb;
//好的例子: C++風格強制轉換,明確知道pb實際指向DerivedClass
DerivedClass* pd = dynamic_cast< DerivedClass *>(pb);
if(pd)
Fun(pd);
}
建議2.5 避免使用reinterpret_cast
說明:reinterpret_cast用於轉換不相互關聯類型。嘗試用reinterpret_cast將一種類型強制轉換另一種
類型,這破壞了類型的安全性與可靠性,是一種不安全的轉換。不同類型之間盡量避免轉換。
建議2.6 避免使用const_cast
說明:const_cast用於移除對象的 const性質。
const屬性提供一種安全感,讓程式員知道這個定義是固定不變的,從而不需要擔心後面的變化。如果
const屬性在程式員不知道的地方被消除,會帶來很多嚴重的後果。
樣本:不好的例子
unsigned const int arraySize = 1024;
int &newArraySize = const_cast<int&>(arraySize);
newArraySize = 2048;
這裡如果不通過引用或者指標訪問arraySize,那麼arraySize的值始終是1024。可是如果被作為一個
指標或者引用傳給其他函數進行取值的話,會發現值變成了2048。
樣本:不好的例子:強制去掉入參的const屬性,導致函數可以對入參進行修改。
void setObj(TBase const *obj)
{
//m_pObj的定義為:
TBase *m_pObj;
m_pObj = const_cast<TBase*>(obj);
m_pObj->value = 123;
}
建議2.7 使用虛函數替換dynamic_cast
說明:很多剛從C語言轉過了的程式員習慣這樣的思路:若對象的類型是T1,則做某種處理;若對象的
類型是T2,則做另外的處理等等。但C++提供了更好的解決方案:虛函數。
虛函數與dynamic_cast類型轉換相比:
虛函數更安全,不會出現強制轉換錯的情況;
虛函數效率更高:用函數指標,避免條件判斷;
虛函數不需要在編碼時確定對象的真實類型,而dynamic_cast必須告知要轉成的類型,運行時若
類型不當返回null 指標或者拋異常;
虛函數適用性更強:虛函數是真正動態綁定;類型轉換當增加或刪除一個衍生類別時,dynamic_cast
必須增減相應的代碼。