你可能不知道的 C++(一)

說明:本文面向有經驗的 C++ 程序員,不適合初學者。node

第二部分:你可能不知道的 C++(二)程序員

此爲《你可能不知道的 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++ 中的子集 C 跟原始的 C 仍是有點不同。less

結構 & 聯合

  • C 的結構(struct)不是一種類型,使用時得帶着關鍵字struct,通常用typedef來避免這種不便。函數

  • C++ 的結構幾乎等價於類,只是缺省的訪問權限爲public而非privateui

  • C++ 的聯合(union)能夠有成員函數,甚至能夠有構造和析構函數。spa

不帶參數的函數

對 C 來講,一個不帶參數的函數意味着能夠接受任意參數。因此void f()就至關於void f(...),而下面三個函數指針類型中:ssr

typedef void (*foox)();
typedef void (*foo1)(int);
typedef void (*foo2)(void);

foo1foo2能夠隱式地轉型爲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)));

CONSTS

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++ 的數據模型

爲了回答這些問題,有必要了解一下 C/C++ 的數據模型標記(Data Model Notation)。

C Data Model Notation

大寫字母表明數據類型(I: int; L: long; LL: long long; P: 指針),而下標則表示這個類型的大小(通常爲 16/32/64)。

有了這個標記,就能夠方便地表示不一樣平臺上 C/C++ 的數據模型。好比 32 位
x86 Linux 平臺,數據模型爲I32L32LL64P32,或者簡寫成IL32LL64P32

size_t

標準庫裏處處都是size_t的身影:

void *malloc(size_t n);
void *memcpy(void *s1, void const *s2, size_t n);
size_t strlen(char const *s);

回到前面的問題,不難理解如下幾點:

  • size_tsizeof返回值的類型

  • size_t是一個typedef

  • sizeof不是一個函數,它是一個編譯時操做符

  • size_t可以表示任何類型理論上可能的數組的最大大小

其實,size_t通常就是unsigned inttypedef,那爲何不直接用unsigned int?在IP16IP32平臺上(即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++ 來講,除了數組外,知足如下條件的類(或結構)也是聚合:

  • 沒有用戶聲明的構造函數

  • 沒有privateprotected非靜態數據成員

  • 沒有基類

  • 沒有虛函數

因此,下面幾個類型都是聚合:

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;

n1n2的差異在於,n1所指的對象是通過零值初始化的,而n2所指的對象則不肯定。具體來講,n1{ 0, NULL },而n2valuenext是什麼就說不許了。

成員聚合對象的零值初始化

  • 成員名字後面加()就缺省初始化了這個成員。

  • 對於聚合成員來講,缺省初始化就是指零值初始化。

  • 若是成員有一個 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() {}

由於sd這兩個成員都是聚合對象,使用()就能夠初始化爲零值。

成員聚合對象的非零值初始化

只能手動賦值了:

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 & operator new

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;
}

nothrow & throw

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

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

RAII 指「資源獲取即初始化」(Resource Acquisition Is Initialization),概念上不是很好理解。

也不是全部語言都能支持 RAII。

有些語言,能夠把自定義(user-defined)類型的對象分配在棧上(C/C++ 的術語叫「自動的」對象),而且於正常的棧清理時(要麼是函數返回,要麼是異常拋出)也能一併清理對象,那麼它就支持 RAII。典型的語言如 C++。

有些語言,有基於引用計數的垃圾收集,也所以對於只有一個引用的對象具有可預測的清理時,那麼它也是支持 RAII 的。典型的語言如 Python。

RAII 的類,設計上都比較純粹,或者至少主要是用來提供 RAII 的語意。這些類通常都是爲某種資源提供一種抽象級別的訪問。

C++ STL 中有很多 RAII 類,好比頗具爭議性的std::auto_ptr。還有 std::basic_ifstreamstd::basic_ofstreamstd::basic_fstream,等等。

C 沒有 RAII 這東西,C 也沒有自定義類型一說(C 的結構不是一種獨立的類型)。

以文件操做爲例,C 的作法是:

{
    FILE* file = fopen( ... );
    // ... 
    fclose(file);
}

而 C++ 就方便不少,不須要手動關閉文件,由於std::ofstream在析構時會自動釋放文件資源,即便中途有異常發生也不會出現問題。

{
    std::ofstream file( ... );
    // 繼續操做文件 ...
} // 在此 file 對象被自動清理,它的析構函數負責釋放文件資源。

RAII 的原理

Stack Winding & Unwinding

當程序運行時,每個函數(包括數據、寄存器、程序計數器,等等)在調用時,都被映射到棧上。這就是 stack winding。

Unwinding 是以相反順序把函數從棧上移除的過程。

正常的 stack unwinding 發生在函數返回時;不正常的狀況,好比引起異常,調用setjmplongjmp,也會致使 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 工做的原理,它是由語言和編譯器來保證的。


第一部分完。

第二部分:你可能不知道的 C++(二)

相關文章
相關標籤/搜索