5.10 前置自增和自減 Preincrement and Predecrement
Tip 對於迭代器iterator和其他模板對象template object使用前綴形式(++i)的自增, 自減運算符;
定義:
對於變量在自增(++i 或 i++)或自減(--i 或 i--)後, 表達式的值沒有被用到的情況下, 需要確定到底是使用前置還是後置的自增(自減);
優點:
不考慮返回值的話, 前置pre自增(++i)通常要比後置post自增(i++)效率更高; 因爲後置自增(自減)需要對錶達式的值 i 進行一次拷貝; 如果i是迭代器或其他非數值 non-scalar類型, 拷貝的代價是比較大的; 既然兩種自增方式實現的功能一樣, 爲什麼不總是使用前置自增呢?
缺點:
在C開發中, 當表達式的值未被使用時, 傳統的做法還是使用後置自增, 特別是在 for循環中; 有些人覺得後置自增更加易懂, 因爲這很像自然語言, 主語subject(i)在謂語動詞precede(++)前;
[C語言中沒有class類型, 基本上POD不必在意前置或後置的效率區別]
結論:
對簡單數值scalar(非對象non-object), 兩種都無所謂; 對迭代器和模板類型, 使用前置自增(自減);
5.11 const的使用 Use of const
Tip 強烈建議在任何可能的情況下都要使用 const; [Add] c++11中, constexpr對於某些const使用情況是更好的選擇; [http://en.cppreference.com/w/cpp/language/constexpr]
<<<
定義:
在聲明的變量或參數前preceded加上關鍵字 const用於指明變量值不可被篡改(如 const int foo); 爲類中的函數加上 const限定符qualifier表明該函數不會修改類成員變量的狀態(如 class Foo { int Bar(char c) cosnt; }; )
優點:
大家更容易理解如何使用變量; 編譯器可以更好地進行類型檢測; 相應地, 也能生成更好的代碼; 人們對編寫正確的代碼更加自信, 因爲他們知道所調用的函數被限定了能或不能修改變量值; 即使是在無鎖的多線程編程中without locks in multi-threaded, 人們也知道什麼樣的函數是安全的;
缺點:
const是入侵性viral的: 如果你向一個函數傳入 const變量, 函數原型聲明中也必須對應 const參數(否則變量需要 const_cast類型轉換), 在調用庫函數是顯得尤其麻煩;
結論:
const變量, 數據成員, 函數和參數爲編譯時類型檢測增加了一層保障: 便於儘早發現錯誤; 因此, 我們強烈建議在任何可能的情況下使用 const;
- 如果函數不會修改傳入的引用或指針類型參數, 該參數應聲明爲const;
- 儘可能將函數聲明爲const; 訪問函數幾乎總是const; 其他不會修改任何數據成員, 沒有調用非const函數, 不會返回數據成員非const指針或引用的函數也應聲明成const;
- 如果數據成員在對象構造之後不再發生變化, 可將其定義爲const;
[Remove] 然而, 也不要發瘋似的使用const; 像 const int* const* const x; 就有些過了, 雖然它非常精確地描述了常量 x; 關注真正有幫助一樣的信息: 前面的例子寫成 const int** x就夠了; [內容不可變] <<<
關鍵字 mutable可以使用, 但是在多線程中是不安全的, 使用時首先要考慮線程安全性;
const的位置 Where to put the const:
有人喜歡 int const *foo形式, 不喜歡 const int* foo; 他們認爲前者更一致因此可讀性也更好: 遵循了const總位於其描述的對象之後的原則; 但是一致性原則不適用於此, 由於多數const表達式只有一個const, 而且應用的是一個值, 很少有深層嵌套的指針表達式few deeply-nested pointer expressions; "不要過度使用"的聲明可以取消大部分你原本想保持的一致性;
將const放在前面才更易讀, 因爲在自然語言中形容詞adjective(const)是在名詞noun(int)之前;
這是說, 我們提倡但不強制const在前; 但要保持代碼的一致性; (譯註, 就是不要在一些地方把const寫在類型前面, 在其他地方又寫在後面, 要確定一種寫法, 然後保持一致);
[Add]
使用constexpr Use of constexpr
C++11中, 使用constexpr來定義true的常量或確保常量初始化constant initialization;
定義:
一些變量可以被聲明爲constexpr, 表明變量是true的常量, e.g. 在編譯/鏈接時是固定的; 一些函數和cotr可以被聲明爲constexpr, 讓它們可以在定義一個constexpr變量時被使用;
優點:
使用constexpr定義浮點數floating-point表達式常量, 而不是字面量literal定義, 用戶定義的類型的定義和函數調用function call的常量的定義;
缺點:
把某些東西定義爲constexpr可能會在之後導致一些遷移migration問題, 或許不得不回退回去downgraded; 目前的對於constexpr函數和ctor的限制規定restriction可能會在這些定義中產生些隱晦的替代方案workaround;
結論:
constexpr定義可以在一些接口的const部分給出健壯的規格robust specification; 使用constexpr來指定真實常量true constants以及支持函數的定義; 使用constexpr來防止複雜的函數定義; 不要使用constexpr來強制內聯 inline; [http://stackoverflow.com/questions/14391272/does-constexpr-imply-inline ]
<<<
5.12 整型 Integer Types
Tip C++內建整型中, 僅使用 int; 如果程序中需要不同大小的變量, 可以使用 <stdint.h>中長度精確precise-width的整型, 如 int16_t; [http://www.cplusplus.com/reference/cstdint/ ]
[Add] 如果你的變量表示了一個可以變大或者等於2^31(2GiB)的值, 使用一個64-bit類型, 比如int64_t; 記住即使你的值對於int來說不會過大, 它還是可能在一些中間計算中需要一個更大的類型; 如果不確定, 就選用更大的類型; <<<
定義:
C++沒有指定整型的大小; 通常人們假定 short是16位, int是32位, long是32位, long long是64位; (bits)
優點:
保持聲明統一性Uniformity;
缺點:
C++中整型大小因編譯器和體系結構architecture的不同而不同;
結論:
<stdint.h>定義了 int16_t, uint32_t, int64_t等整型, 在需要確保guarantee 整型大小時可以優先preference使用它們代替 short, unsigned long long等; 在C整型中, 只使用 int; 在合適的情況下, 推薦使用標準類型如 size_t和ptrdiff_t;
如果已知整數不會太大, 我們常常會使用 int, 如循環計數loop counter; 在類似的情況下使用原生類型plain old int; 你可以認爲int至少爲32位, 但不要認爲它會多於32位; 如果需要64位整型, 用 int64_t或 uint64_t;
對於大整數, 使用 int64_t;
不要使用 uint32_t等無符號整型, 除非有正當valid 理由, 比如在表示一個位組bit pattern而不是一個數值, 或是需要定義二進制補碼溢出overflow modulo 2^N; 尤其是不要爲了指出數值永遠不會爲負, 而使用無符號類型; 相反, 你應該用斷言來保護數據;
<<<
[Add] 如果你的代碼是一個返回大小的容器, 確保使用一個可適應accommodate任何使用的可能性的數據類型; 不確定的時候使用一個更大的類型而不是較小的類型;
當轉化整型的時候要小心; 整型轉換和提升promotion會引起一些反直覺non-intuitive的行爲;
<<<
關於無符號整數 On Unsigned Integers:
有些人, 包括一些教科書作者, 推薦使用無符號整型表示非負數; 這種做法試圖達到自我文檔化self-documentation; 但是在C語言中, 這一優點被由其導致的bug所淹沒outweighed; 看看下面的例子:
1
|
for (unsigned int i
= foo.Length()-1; i >= 0; --i) //... |
上述循環永遠不會退出! 有時gcc會發現該bug並報警, 但大部分情況下都不會; 類似的bug還會出現在比較有符號變量和無符號變量時; 主要是C的類型提升機制會導致無符號類型的行爲出乎你的意料;
因此, 使用斷言來指出變量爲非負數, 而不要使用無符號類型;
5.13 64位下的可移植性 64-bit Portability
Tip 代碼應該對64位和32位系統友好; 處理打印printing, 比較comparison, 結構體對齊structure alignment時應該切記;
- 對於某些類型, printf()的指示符在32位和64位系統上可移植性不是很好; C99標準定義了一些可移植的格式化指示符; 不幸的是, MSVC7.1並非全部支持; 而且標準中也有所遺漏, 所以有時我們不得不自己定義一個醜陋的版本(頭文件inttype.h仿標準風格):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// printf macros for size_t, in the style of inttypes.h #ifdef _LP64 #define __PRIS_PREFIX "z" #else #define __PRIS_PREFIX #endif // Use these macros after a % in a printf format string // to get correct 32/64 bit behavior, like this: // size_t size = records.size(); // printf("%"PRIuS"\n", size); #define PRIdS __PRIS_PREFIX "d" #define PRIxS __PRIS_PREFIX "x" #define PRIuS __PRIS_PREFIX "u" #define PRIXS __PRIS_PREFIX "X" #define PRIoS __PRIS_PREFIX "o" |
check table:
Type | DO NOT use | DO use | Notes |
---|---|---|---|
void * (or any pointer) |
%lx |
%p |
|
int64_t |
%qd , %lld |
%"PRId64" |
|
uint64_t |
%qu , %llu , %llx |
%"PRIu64" , %"PRIx64" |
|
size_t |
%u |
%"PRIuS" , %"PRIxS" |
C99 specifies %zu |
ptrdiff_t |
%d |
%"PRIdS" |
C99 specifies %td |
Note PRI*宏會被編譯器擴展concatenated爲獨立字符串; 因此如果使用非常量的格式化字符串, 需要將宏的值而不是宏名插入格式中; 使用PRI*宏同樣可以在 %後包含長度指示符,etc; 例如: printf("x = %30"PRIuS"\n", x); 在32位Linux將被展開爲: printf("x = %30" "u" "\n", x); 編譯器當成: printf("x = %30u\n", x);處理; (譯註: 這在MSVC6.0上不行, VC6編譯器不會自動把引號間隔的多個字符串連接成一個長字符串);
- 記住 sizeof(void *) != sizeof(int); 如果需要一個指針大小的整數要用 intptr_t; [不同編譯器, 系統不一樣]
- 要非常小心地對待結構體對齊, 尤其是要持久化存儲到磁盤上的結構體; (譯註: 持久化--將數據按字節流順序保存在磁盤文件或數據庫中); 在64位系統中, 任何含有 int64_t/uint64_t成員的類/結構體, 缺省都以8字節在結尾對齊; 如果32位和64位代碼要共用持久化在磁盤上的結構體; 需要確保兩種體系結構下的結構體對齊一致;(packed) 大多數編譯器都允許調整結構體對齊; gcc中可使用 __attribute__((packed)); MSVC則提供了 #pragma pack()和 __deslspec(align());
(譯註: 解決方案的項目屬性欄裏也可以直接設置) [VS的項目--solution]
[http://en.wikipedia.org/wiki/Data_structure_alignment ]
- 創建64位常量時使用 LL或 ULL作爲後綴suffixes, 如:
1
2
|
int64_t my_value = 0x123456789LL; uint64_t my_mask = 3ULL << 48; |
- 如果你確實需要32位和64位系統具有不同代碼, 可以使用 #ifdef _LP64指令在代碼變量中區分 32/64位代碼; (儘量不要這麼做, 如果非用不可, 儘量使修改局部化);
1
2
3
|
#if defined(__LP64__) || defined(_LP64) #define BUILD_64 1 #endif |
]
5.14 預處理宏 Preprocessor Macros
Tip 使用宏時要非常謹慎, 儘量以內聯函數, 枚舉和常量替代之;
宏意味着你和編譯器看到的代碼是不同的; 這可能會導致異常行爲, 尤其因爲宏具有全局作用域;
值得慶幸的是, C++中, 宏不像在C中那麼必不可少; 以往用宏展開性能關鍵performance-critical的代碼, 現在可以用內聯函數替代; 用宏表示的常量可以被 const變量代替; 用宏"縮寫"長變量的別名abbreviate可被引用代替reference; [以及typedef]; 用宏進行條件編譯...這個, 千萬別這麼做, (#define防止頭文件重複包含當然是個特例) 會令測試更加痛苦;
宏可以做一些其他技術無法實現的事情, 在一些代碼庫(尤其是底層庫中)可以看到宏的某些特性(如用#字符串化stringifying, 用##連接concatenation等); 但在使用前, 仔細考慮一下能不能不使用宏達到同樣的目的; [Hack: private和public]
下面給出的用法模式可以避免使用宏帶來的問題; 如果要用宏, 儘可能遵守:
- 不要在 .h文件中定義宏;
- 在馬上要使用時才進行 #define, 使用完要立即 #undefine;
- 不要只是對已經存在的宏使用 #undef, 選擇一個不會衝突的獨特名稱;
- 不要試圖使用展開後會導致C++構造不穩定的宏, 否則至少要附上文檔說明其行爲; [不要在構造相關代碼使用宏?]
- 最好不要使用 ##來產生 function/class/variable的名字;
5.15 0 and nullptr/NULL
Tip 整數用 0, 實數用 0.0, 指針用 NULL或nullptr, 字符chars(串)用 '\0';
整數用0, 實數用0.0, 這一點毫無爭議controversial;
對於指針(地址值), 到底是使用0還是NULL/nullptr,
[Remove]Bjarne Stroustrup建議使用最原始的0; 我們建議使用看上去像是指針的 NULL; [C++11: nullptr]<<<
[Add] 對允許C++11的項目, 使用nullptr, C++03項目使用NULL, 看起來比較像個指針; <<<
事實上一些C++編譯器(如gcc4.1.0)對 NULL進行了特殊的定義, 可以給出有用的警告信息, 尤其是 sizeof(NULL)和 sizeof(0)不相等的情況;
字符(串)用 '\0', 不僅類型正確而且可讀性好; [http://bbs.csdn.net/topics/390615761]
5.16 sizeof
Tip 儘可能用 sizeof(varname)代替 sizeof(type);
使用 sizeof(varname)是因爲當代碼中變量類型改變時會自動更新; 某些特定情況下 sizeof(type)或許有意義, 比如管理external或internal數據類型變量的代碼, 而沒有方便的C++類型; 但還是要儘量避免, 因爲它會導致變量類型改變後不能同步;
1
2
|
Struct data; memset (&data,
0, sizeof (data)); |
WARNING
1
|
memset (&data,
0, sizeof (Struct)); |
[Add]
Other
1
2
3
4
|
if (raw_size
< sizeof ( int ))
{ LOG(ERROR)
<< "compressed record not big enough for count: " <<
raw_size; return false ; } |
<<<
[Add]
auto
使用auto來避免類型名字雜亂clutter; 當有助於可讀性的時候繼續使用明顯的manifest類型聲明, 除了本地local變量之外不要使用auto;
定義:
C++11中, 一個由auto指定類型的變量會給出符合初始化它的表達式的類型; 可以使用auto來用copy初始化initialize它, 或者綁定bind一個引用;
[http://en.cppreference.com/w/cpp/language/auto]
1
2
3
4
|
vector<string> v; ... auto s1
= v[0]; // Makes a copy of v[0]. const auto &
s2 = v[0]; // s2 is a reference to v[0]. |
優點:
C++類型名稱有時候很長而且笨重cumbersome, 特別是在模板或名字空間中:
1
|
sparse_hash_map<string, int >::iterator
iter = m.find(val); |
返回值很難讀懂, 矇蔽obscure了語句的原本意圖; 改爲:
1
|
auto iter
= m.find(val); |
更易讀懂;
沒有auto的話我們有時不得不將一個類型名字在一個表達式內寫兩遍, 對於閱讀者來說沒有意義:
1
|
diagnostics::ErrorStatus* status = new diagnostics::ErrorStatus( "xyz" ); |
使用auto讓中間intermediate變量的使用更合理, 減少了顯式書寫類型的負擔;
缺點:
有時候變量是manifest的會更清晰, 特別是變量初始化依賴於我們之前聲明的東西; 像這樣的表達式:
1
|
auto i
= x.Lookup(key); |
如果x是幾百行之前聲明的, i的類型可能不夠明顯;
程序員不得不去理解auto和const auto&之間的區別, 有時候他們會在沒有意識到的時候拿到一份copy;
auto和C++11 brace-initialization之間的交互interaction可能會令人混淆; 聲明:
1
2
|
auto x(3); //
Note: parentheses. auto y{3}; //
Note: curly braces. |
它們表示不同的東西, x是個int, 但y是一個std::initializer_list<int>; 相同情況會發生在其他普通隱式代理normally-invisible proxy類型上;
如果一個auto變量被用作接口的一部分, e.g. 在頭文件中作爲一個const, 程序員可能只是爲了改變它的值而改變它的類型, 導致沒有想到的一系列API的徹底radical改變;
結論:
auto只對本地變量開放; 不要在文件範圍file-scopye或名字空間範圍namespace-scope中對變量, 或類成員使用auto; 永遠不要用大括號初始化列表braced initializer list初始化一個auto類型auto-typed變量;
auto關鍵字也用在C++feature無關的地方: 它作爲一種新的函數聲明的語法的一部分, 尾隨返回值類型 trailing return type; trailing return type只在lambda表達式中被允許使用;
Braced Initializer List
braced initializer lists; [http://en.cppreference.com/w/cpp/language/list_initialization]
在C++03, 聚合類型aggregate type(沒有ctor的數組和結構體)可以用braced initializer list初始化;
1
2
|
struct Point
{ int x; int y;
}; Point p = {1, 2}; |
C++11中, 這個語法被普遍化generalized了, 任何一個對象類型都可以用braced initializer list來初始化, 作爲一個braced-init-list的C++語法grammar; 這裏有幾個例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// Vector takes a braced-init-list of elements. vector<string> v{ "foo" , "bar" }; // Basically the same, ignoring some small technicalities. // You may choose to use either form. vector<string> v = { "foo" , "bar" }; // Usable with 'new' expressions. auto p
= new vector<string>{ "foo" , "bar" }; // A map can take a list of pairs. Nested braced-init-lists work. map< int ,
string> m = {{1, "one" },
{2, "2" }}; // A braced-init-list can be implicitly converted to a return type. vector< int >
test_function() { return {1,
2, 3}; } // Iterate over a braced-init-list. for ( int i
: {-1, -2, -3}) {} // Call a function using a braced-init-list. void TestFunction2(vector< int >
v) {} TestFunction2({1, 2, 3}); |
一個用戶定義的類型也可以使用std::initializer_list<T> [http://en.cppreference.com/w/cpp/utility/initializer_list] 定義一個ctor或assignment operator, 會從braced-init-list自動創建;
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class MyType
{ public : //
std::initializer_list references the underlying init list. //
It should be passed by value. MyType(std::initializer_list< int >
init_list) { for ( int i
: init_list) append(i); } MyType&
operator=(std::initializer_list< int >
init_list) { clear(); for ( int i
: init_list) append(i); } }; MyType m{2, 3, 5, 7}; |
最後 brace initialization也能調用普通的數據類型的ctor, 即使沒有std::initializer_list<T>構造函數;
1
2
3
4
5
6
7
8
9
10
11
|
double d{1.23}; // Calls ordinary constructor as long as MyOtherType has no // std::initializer_list constructor. class MyOtherType
{ public : explicit MyOtherType(string); MyOtherType( int ,
string); }; MyOtherType m = {1, "b" }; // If the constructor is explicit, you can't use the "= {}" form. MyOtherType m{ "b" }; |
Note 永遠不要把一個braced-init-list分配給一個auto的本地變量; 在單個元素的case中, 其意義可能會混淆:
Bad:
1
|
auto d
= {1.23}; // d is a std::initializer_list<double> |
Good:
1
|
auto d
= double {1.23}; //
Good -- d is a double, not a std::initializer_list. |
參見 Braced Initializer List Format;
<<<
---TBC---YCR