說明:本文面向有經驗的 C++ 程序員,不適合初學者。node
此爲《你可能不知道的 C++》的第一部分,討論 C & C++,編譯單元,及對象。express
C++ has indeed become too "expert friendly".
C++ 着實已經變得太「面向專家」了。segmentfault
Bjarne Stroustrup, The Problem with Programming, Technology Review, Nov 2006.數組
C++ 是 C 的超集,可是 C++ 中的子集 C 跟原始的 C 仍是有點不同。less
C 的結構(struct)不是一種類型,使用時得帶着關鍵字struct
,通常用typedef
來避免這種不便。函數
C++ 的結構幾乎等價於類,只是缺省的訪問權限爲public
而非private
。ui
C++ 的聯合(union)能夠有成員函數,甚至能夠有構造和析構函數。spa
對 C 來講,一個不帶參數的函數意味着能夠接受任意參數。因此void f()
就至關於void f(...)
,而下面三個函數指針類型中:ssr
typedef void (*foox)(); typedef void (*foo1)(int); typedef void (*foo2)(void);
foo1
和foo2
能夠隱式地轉型爲foox
,就比如能夠從int*
或char*
隱式地轉型成void*
。
要想讓一個 C 函數真正沒有參數,得用void
:
void foo(void);
對 C++ 來講,一個不帶參數的函數就是指不接受參數。往參數列表裏放void
是多餘的。
void*
詳見:Brian W. Kernighan & Rob Pike, The Practice Of Programming, 2.6
C 會自動提高(promote)void*
:
int* pi = malloc(sizeof(int));
函數malloc
返回void*
,賦值給int*
時不須要顯式轉型。而 C++ 必須顯式轉型:
int* pi = static_cast<int*>(malloc(sizeof(int)));
C++ 容許 consts 用在常量表達式中:
const int MAX = 4; int a[MAX + 1]; switch (i) { case MAX: ...
而 C 則必須使用宏:
#define MAX 4
引一段《C++ 的設計和演化》的原文:
(Bjarne Stroustrup, The Design and Evolution Of C++, 3.8)
In C, consts may not be used in constant expressions. This makes consts far less useful in C than in C++ and leaves C dependent on the preprocessror while C++ programmers can use properly typed and scoped consts.
C 的 consts(特指用 const 關鍵字修飾的常量)不能夠用在常量表達式中。這讓 C 的 consts 遠不如 C++ 的有用,也讓 C 依賴於預處理器,而 C++ 程序員則可使用有適當類型和做用域的 consts。
C 代碼塊中,全部聲明必須出如今任何程序語句以前,好比函數定義時,先聲明全部局部變量:
void foo() { int ival, *p; /* … */ }
而 C++ 的聲明,諸如int ival;
,其自身就是一個程序語句,也所以能夠出如今程序文本中的任何位置。
C/C++ 中的一個源文件(.c, .cpp, .cc
)就是一個編譯單元(compilation unit)。
頭文件(.h, .hpp
)不是編譯單元,是不能單獨編譯的。
源文件通過預處理,先搞定下面這些東西:
宏:包括用戶定義的,和預約義的(__cplusplus
, __FILE__
, ...)
包含語句:源文件中的include
語句所有展開
條件編譯: #if
, #else
, #ifudef
, ...
#error
, #warning
, ...
預處理過的源文件,通過編譯,生成對象文件(.o, .obj
)。對象文件通過連接或打包,生成可執行文件或程序庫。雖然這裏的步驟不太嚴格,可是大抵就是這樣。
若是你對預處理的結果很感興趣,能夠試試編譯器的預處理命令:gcc -E (GCC)
,cl /E or /P (VC)
。
這裏所說的對象(object),泛指一切類型的實例,不僅是類的實例。
關於對象,咱們將探討如下幾個方面:
對象的大小(size)
按存儲(storage)分類的對象
聚合(aggregate)
自由存儲(free-store)對象的構造和析構
RAII
先來考慮幾個問題:
sizeof
是一個函數嗎?
你知道sizeof(int)
, sizeof(long)
各爲多少嗎?
爲何應該用size_t
?
爲了回答這些問題,有必要了解一下 C/C++ 的數據模型標記(Data Model Notation)。
大寫字母表明數據類型(I: int; L: long; LL: long long; P: 指針
),而下標則表示這個類型的大小(通常爲 16/32/64)。
有了這個標記,就能夠方便地表示不一樣平臺上 C/C++ 的數據模型。好比 32 位
x86 Linux 平臺,數據模型爲I32L32LL64P32
,或者簡寫成IL32LL64P32
。
標準庫裏處處都是size_t
的身影:
void *malloc(size_t n); void *memcpy(void *s1, void const *s2, size_t n); size_t strlen(char const *s);
回到前面的問題,不難理解如下幾點:
size_t
是sizeof
返回值的類型
size_t
是一個typedef
sizeof
不是一個函數,它是一個編譯時操做符
size_t
可以表示任何類型理論上可能的數組的最大大小
其實,size_t
通常就是unsigned int
的typedef
,那爲何不直接用unsigned int
?在IP16
或IP32
平臺上(即int
和指針大小一致時),確實沒有問題,但I16LP32
就不行了。此外,直接用unsigned long
當然沒錯,但畢竟得多花了幾個字節,稍微有點浪費了。反正只要用size_t
,你就能夠同時獲得正確性和可移植性。
請問mixed_data
的大小是多少?是 8 嗎?
struct mixed_data { char data1; short data2; int data3; char data4; };
在 32 位 x86 平臺上編譯後的樣子:
struct mixed_data { char data1; char padding1[1]; short data2; int data3; char data4; char padding2[3]; };
爲了數據對齊,編譯器塞了一些邊角料進去,最終的大小爲 12。
C/C++ 的對象,按存儲類型分爲如下幾種:
自動的(auto, register)
靜態的(static)
自由存儲的(free-store)
關鍵字auto
有點多餘,下面兩條聲明語句其實等價,b
前面的auto
加不加一個效果:
{ int a; auto int b; }
到了 C++11,auto
這個關鍵字就被拿來另做他用了:auto
可讓編譯器從變量的初始化上自動推斷出它的類型:
auto a = std::max(1.0, 4.0); // 編譯器推斷出 a 的類型爲 double
首先,什麼叫聚合?
對 C 來講,數組和結構是聚合。
對 C++ 來講,除了數組外,知足如下條件的類(或結構)也是聚合:
沒有用戶聲明的構造函數
沒有private
或protected
非靜態數據成員
沒有基類
沒有虛函數
因此,下面幾個類型都是聚合:
int[5]; struct person { std::string name; int age; }; boost::array;
自動聚合對象能夠用「初始化列表」,即花括號,這種初始化方式很是方便。
typedef int ints_t[5]; ints_t ints1 = {}; // { 0, 0, 0, 0, 0 } ints_t ints2 = { 0, 1, 2 }; // { 0, 1, 2, 0, 0 } ints_t ints3 = { 0, 1, 2, 3, 4 }; // { 0, 1, 2, 3, 4 }
struct person { std::string name; int age; }; person p1 = {}; // { "", 0 } person p2 = { "john" }; // { "john", 0 } person p3 = { "john", 26 }; // { "john", 26 }
boost::array<int, 3> a = { 0, 1, 2 };
對聚合對象來講,缺省初始化(default-initalization)就是指零值初始化(zero-initialization)。
若是不指定初始化列表的話,對象各元素的初始值就不肯定了。
boost::array<int, 3> a; // 三個元素的初始值不肯定
初始化列表對自由的聚合對象並不適用,可是自由的聚合對象能夠經過()
來作零值初始化。
struct list_node { int value; list_node* next; }; list_node* n1 = new list_node(); list_node* n2 = new list_node;
n1
和n2
的差異在於,n1
所指的對象是通過零值初始化的,而n2
所指的對象則不肯定。具體來講,n1
爲{ 0, NULL }
,而n2
的value
和next
是什麼就說不許了。
成員名字後面加()
就缺省初始化了這個成員。
對於聚合成員來講,缺省初始化就是指零值初始化。
若是成員有一個 non-trivial 的構造函數,那麼缺省初始化就意味着調用它的缺省構造函數。
如何實現下面這個類的缺省構造函數?
class C { public: C(); // 如何實現這個? private: struct S { int x; int* p; bool b; }; S s; int d[5]; };
由 C 轉過來的 C++ 程序員可能會這樣實現:
C::C() { memset(&s, 0, sizeof(S)); d[0] = d[1] = d[2] = d[3] = d[4] = 0; }
沒錯,百分之百正確!可是,更簡單更 C++ 的方式是:
C::C() : s(), d() {}
由於s
和d
這兩個成員都是聚合對象,使用()
就能夠初始化爲零值。
只能手動賦值了:
C::C() { s.x = 5; s.p = new char[s.x]; s.b = true; d[0] = 9; ... }
C++11 新標準容許聲明時使用初始化列表:
class C { S s { 5, new char[5], true } int d[5] { 9, 9, 9, 9 } };
雖然成員變量的初始化變得簡單直觀了,可是在頭文件裏幹這種事也有暴露實現細節的嫌疑。
用new operator
建立一個std::string
對象:
std::string* name = new std::string("Adam");
new operator
的實現大體以下:
void* p = ::operator new(sizeof(std::string)); std::string* name = static_cast<std::string*>(p); name->basic_string::basic_string("Adam");
先用operator new
分配內存,再轉型成std::string
指針,而後再調用std::string
的構造函數。
負責分配內存的operator new
的實現大體以下:
void* operator new(...) { void* p; while ((p = malloc(size)) == 0) { // 嘗試調用 new handler 來獲得更多可用內存,要不就返回 NULL 或拋出異常 } return p; }
new operator
分配內存失敗後,缺省的行爲不是返回NULL
,而是拋出異常std::bad_alloc
。因此判斷返回值是否爲NULL
沒有任何意義。
A* a = new A(); if (a == NULL) { // 沒有任何意義! return; }
在new
後面加上(std::nothrow)
能夠改變這一行爲。
char* p = new (std::nothrow) char[0x7ffffffe]; if (p == NULL) { // 如今能夠這樣判斷了 // 內存分配失敗了! }
不然就得用 try / catch
:
char* p; try { p = new char[0x7ffffffe]; } catch (std::bad_alloc&) { // 哦,內存分配失敗了! // ... }
實踐中通常沒必要這麼麻煩,能夠假定new operator
總能成功。畢竟連通常對象的內存都分配不了時,程序也沒有繼續執行的意義了。
placement new
在一塊預先分配好的內存上建立對象,通常用來實現內存池。好比:
先分配容得下 3 個std::string
的內存:
const size_t count = 3; char* buf = new char[sizeof(std::string) * count];
而後聲明三個std::string
對象指針,並逐一調用placement new
進行初始化:
std::string* strs[count]; for (size_t i = 0; i < count; ++i) { strs[i] = new (buf + i * sizeof(std::string)) std::string("A"); }
最後清理的時候,不能用delete
,要手動調用std::string
的析構函數~basic_string()
來充值對象狀態,而內存還留在buf
裏。要釋放buf
就單獨對它調用delete[]
。
for (size_t i = 0; i < count; ++i) { strs[i]->~basic_string(); }
RAII 指「資源獲取即初始化」(Resource Acquisition Is Initialization),概念上不是很好理解。
也不是全部語言都能支持 RAII。
有些語言,能夠把自定義(user-defined)類型的對象分配在棧上(C/C++ 的術語叫「自動的」對象),而且於正常的棧清理時(要麼是函數返回,要麼是異常拋出)也能一併清理對象,那麼它就支持 RAII。典型的語言如 C++。
有些語言,有基於引用計數的垃圾收集,也所以對於只有一個引用的對象具有可預測的清理時,那麼它也是支持 RAII 的。典型的語言如 Python。
RAII 的類,設計上都比較純粹,或者至少主要是用來提供 RAII 的語意。這些類通常都是爲某種資源提供一種抽象級別的訪問。
C++ STL 中有很多 RAII 類,好比頗具爭議性的std::auto_ptr
。還有 std::basic_ifstream
,std::basic_ofstream
,std::basic_fstream
,等等。
C 沒有 RAII 這東西,C 也沒有自定義類型一說(C 的結構不是一種獨立的類型)。
以文件操做爲例,C 的作法是:
{ FILE* file = fopen( ... ); // ... fclose(file); }
而 C++ 就方便不少,不須要手動關閉文件,由於std::ofstream
在析構時會自動釋放文件資源,即便中途有異常發生也不會出現問題。
{ std::ofstream file( ... ); // 繼續操做文件 ... } // 在此 file 對象被自動清理,它的析構函數負責釋放文件資源。
Stack Winding & Unwinding
當程序運行時,每個函數(包括數據、寄存器、程序計數器,等等)在調用時,都被映射到棧上。這就是 stack winding。
Unwinding 是以相反順序把函數從棧上移除的過程。
正常的 stack unwinding 發生在函數返回時;不正常的狀況,好比引起異常,調用setjmp
和longjmp
,也會致使 stack unwinding。
關於異常發生時,stack unwinding 的過程,不妨引用一段《C++ 程序設計語言》裏的原文(Bjarne Stroustrup, The C++ Programming Language, 14.4):
The process of searching 「up through the stack」 to find a handler for an exception is commonly called 「stack unwinding.」 As the call stack is unwound, the destructors for constructed local objects are invoked.
可見 stack unwinding 的過程當中,局部對象的析構函數將逐一被調用。這也就是 RAII 工做的原理,它是由語言和編譯器來保證的。
第一部分完。