《C++Primer》第十九章 第十九章 特殊工具與技術

第十九章 特殊工具與技術

控制內存分配

1. 重載new和delete

重載這兩個運算符與重載其他運算符的過程大不相同。想要真正重載new和delete的方法,首先要對new表達式和delete表達式的工作機制足夠了解:

// new表達式
string *sp = new string("a value");  // 分配並初始化一個string對象
string *arr = new string[10];        // 分配10個默認初始化的string對象

當我們使用一條new表達式時,實際上執行了三步操作:

  • 第一步:new表達式調用一個名爲operator new或者operator new[]的標準庫函數,該函數分配一塊足夠大的、原始的、未命名的空間以便存儲特定類型的對象(或者對象的數組)
  • 第二步:編譯器運行相應的構造函數以構造這些對象,併爲其傳入初始值
  • 第三步:對象被分配了空間並構造完成,返回一個指向該對象的指針
delete sp;       // 銷燬*sp, 然後釋放sp指向的內存空間
delete [] arr;   // 銷燬數組中的元素, 然後釋放對應的內存空間

當我們使用一條delete表達式刪除一個動態分配的對象時,實際上執行了兩步操作:

  • 第一步:對sp所指的對象或者arr所指的數組中的元素執行對應的析構函數
  • 第二步:編譯器調用名爲operator delete或者operator delete[]的標準庫函數釋放內存空間

應用程序可以在全局作用域中定義operator new函數和operator delete函數,也可以把它們定義爲成員函數。當編譯器發現一條new表達式或者delete表達式後,將在程序中查找可供調用的operator函數:

  • 如果被分配(釋放)的對象是類類型,則編譯器首先在類及其基類的作用域中查找
  • 否則在全局作用域中查找,如果找到了用戶自定義的版本,則使用該版本執行new或者delete表達式
  • 沒找到的話,則使用標準庫定義的版本

我們可以使用作用域運算符使得new表達式或delete表達式忽略定義在類中的函數,直接執行全局作用域的版本。比如::new::delete

2. operator new接口和operator delete接口

標準庫定義了operator new函數和operator delete函數的8個重載版本。其中前4個可能拋出bad_alloc異常,後4個版本不會拋出異常:

// 這些版本可能拋出異常
void *operator new(size_t);        // 分配一個對象
void *operator new[](size_t);      // 分配一個數組
void *operator delete(void*) noexcept;   // 釋放一個對象
void *operator delete[](void*) noexcept; // 釋放一個數組

// 這些版本承諾不會拋出異常
void *operator new(size_t, nothrow_t&) noexcept;
void *operator new[](size_t, nothrow_t&) noexcept;
void *operator delete(void*, nothrow_t&) noexcept;
void *operator delete[](void*, nothrow_t&) noexcept;

標準庫函數operator new和operator delete的名字讓人容易誤解。和其他operator函數不同,這兩個函數並沒有重載new表達式或者delete表達式。實際上我們根本無法自定義new表達式或者delete表達式的行爲。一條new表達式的執行過程總是先調用operator new函數以獲取內存空間,然後在得到的內存空間中構造對象。與之相反,一條delete表達式的執行過程總是先銷燬對象,然後調用operator delete函數釋放對象所佔空間。

我們提供新的operator new和operator delete函數的目的在於改變內存分配的方式。

3. malloc函數和free函數

malloc函數接受一個表示待分配字節數的size_t,返回指向分配空間的指針或者返回0以表示分配失敗。free函數接受一個void*,它是malloc返回的指針的副本,free將相關內存返回給系統。調用free(0)沒有任何意義。

下面給出了operator new和operator delete的簡單方式:

void *operator new(size_t size) {
    if (void *mem = malloc(size))
        return mem;
    else
        throw bad_alloc();
}
void operator delete(void *mem) noexcept { free(mem); }

4. 定位new表達式

C++早期版本中,allocator類還不是標準庫一部分。應用程序如果想把內存分配和初始化分離開的話,需要調用operator new和operator delete。這兩個函數的行爲與allocator的allocate成員和deallocate成員非常類似,它們負責分配或釋放內存空間,但是不會構造或銷燬對象

與allocator不同的是,對於operator new分配的內存空間,我們不能使用construct函數構造對象。相反我們應該用new的定位new形式構造對象。

new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }

其中place_address必須是一個指針,同時在initializers中提供一個(可能爲空)的以逗號值分割的初始值列表,該初始值列表用於構造新分配的對象。當僅通過一個地址值調用時,定位new使用operator new(size_t, void*),這是以一個我們無法自定義的operator new版本,它只是簡單地返回指針實參,然後由new表達式負責在指定的地址初始化對象以完成整個工作。

當只傳入一個指針類型的實參時,定位new表達式構造對象但是不分配內存,它允許我們在一個特定的、預先分配的內存地址上構造對象。

儘管定位new與allocator的construct非常相似,但是有一個重要的區別:我們傳給construct的指針必須指向同一個allocator對象分配的空間,但是傳給定位new的指針無須指向operator new分配的內存,甚至不需要指向動態內存。

5. 顯式的析構函數調用

就像定位new與使用allocate類似一樣,對析構函數的顯式調用也與使用destroy很類似。

string *sp = new string("a value");   // 分配並初始化一個string對象
sp->~string();

和調用destroy類似,調用析構函數可以清除給定的對象但是不會釋放該對象所在的空間。如果需要的話,我們可以重新使用該空間。

調用析構函數會銷燬對象,但是不會釋放內存。

運行時類型識別

運行時類型識別run-time type identification, RRTTI的功能由兩個運算符實現:

  • typeid運算符,用於返回表達式的類型
  • dynamic_cast運算符,用於將基類的指針或引用安全地轉換成派生類的指針或引用

當我們將兩個運算符用於某種類型的指針或者引用時,並且該類型含有虛函數時,運算符將使用指針或者引用所綁定對象的動態類型。

這兩個運算符特別適用於如下情況:當我們想使用幾類對象的指針或者引用執行某個派生類操作並且該操作不是虛函數。一般來說,只要有可能我們應該儘量使用虛函數,當操作被定義成虛函數時,編譯器將根據對象的動態類型自動地選擇正確的函數版本。

然而並非任何時候都能定義一個虛函數。假設我們無法使用虛函數,那麼可以使用一個RTTI運算符。另一方面,與虛成員函數相比,使用RTTI運算符蘊涵着更多潛在的風險:程序員必須清楚地知道轉換的目標類型並且必須檢查類型轉換是否被成功執行。

使用RTTI必須加倍小心,在可能的情況下,最好定義虛函數而非直接接管類型管理的重任。

1. dynamic_cast運算符

dynamic_cast運算符的使用形式如下所示:

dynamic_cast<type*>(e)   // e必須是一個有效的指針
dynamic_cast<type&>(e)   // e必須是一個左值
dynamic_cast<type&&>(e)  // e不能是左值

在上面的所有形式中,e的類型必須符合以下三個條件的任意一個:

  • e的類型是目標type的公有派生類
  • e的類型是目標type的公有基類
  • e的類型是目標type本身

如果符合則轉換可以成功,否則轉換失敗。如果一條dynamic_cast的轉換目標是指針類型並且失敗了,則結果爲0;如果轉換目標是引用類型並且失敗了,則拋出一個bad_cast異常。

1.1 指針類型的dynamic_cast

假定Base類至少含有一個虛函數,Derived是Base的公有派生類。如果有一個指向Base的指針bp,則我們在運行時將它轉換成指向Derived的指針:

if (Derived *dp = dynamic_cast<Derived*>(bp))
{
    // 使用dp指向的Derived對象
} else {  // bp指向一個Base對象
    // 使用bp指向的Base對象
}
1.2 引用類型的dynamic_cast
void f(const Base &b)
{
    try {
        const Derived &d = dynamic_cast<const Derived&>(b);
        // 使用b引用的Derived對象
    } catch (bad_cast) {
        // 處理類型轉換失敗的情況
    }
}

2. typeid運算符

typeid可以作用於任意類型的表達式。和往常一樣,頂層const被忽略,如果表達式是一個引用,則typeid返回該引用所引對象的類型。不過當typeid作用於數組或者函數時,並不會執行向指針的標準類型轉換。比如我們對數組a執行typeid(a)。所得的結果是數組類型而非指針類型。

當運算對象不屬於類類型或者是一個不包含任何虛函數的類時,typeid運算符指示的是運算對象的靜態類型。而當運算對象是定義了至少一個虛函數的類的左值時,typeid的結果直到運行時纔會求得。

通常情況下我們使用typeid比較兩條表達式的類型是否相同,或者比較一條表達式的類型是否與指定類型相同:

Derived *dp = new Derived;
Base *bp = dp;   // 兩個指針都指向Derived對象

// 在運行時比較兩個對象的類型
if (typeid(*bp) == type(*dp)) {
    // bp和dp指向通醫藥類型對象 
}
// 檢查類型是否是某種指定類型
if (typeid(*bp) == typeid(Derived)) {
    // bp實際指向Derived類型
}

注意typeid應該作用於對象,因此我們使用*bp而不是bp:

// 下面檢查永遠失敗: bp類型是指向Base的指針
if (typeid(bp) == typeid(Derived)) {
    // 此處代碼永遠不會執行
}

當typeid作用於指針時(而非指針指向的對象),返回的結果是該指針的靜態編譯時類型。

3. 使用RTTI

在某些情況下RTTI非常有用,比如我們想爲具有繼承關係的類實現相等運算符時。對於兩個對象來說,如果他們的類型相同並且對應的數據成員取值相同,則我們說這兩個類是相等的。

我們定義兩個示例類:

class Base {
    friend bool operator==(const Base&, const Base&);
public:
    // Base的接口成員
protected:
    virtual bool equal(const Base&) const;
    // Base的數據成員和其他用於實現的成員
};

class Derived: public Base {
public: 
    // Derived的其他接口成員
protected:
    bool equal(const Base&) const;
    // Derived的數據成員和其他用於實現的成員
};

類型敏感的相等運算符:

bool operator==(const Base &lhs, const Base &rhs)
{
    // 如果typeid不相同,則返回fallse; 否則虛調用equal
    // 當運算對象是Base的對象時調用Base::equal, 否則調用Derived::equal
    return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}

虛equal函數:繼承體系中的每個類都必須定義自己的equal函數,派生類的所有函數要做的第一件事情就是將實參的類型轉換爲派生類類型:

bool Derived::equal(const Base &rhs) const
{
    // 我們清楚兩個類型是相等的, 所以轉換不會拋出異常
    auto r = dynamic_cast<const Derived&>(rhs);
    // 執行比較兩個Derived對象的操作並返回結果
}

基類equal函數:

bool Base::equal(const Base &rhs) const
{
    // 執行比較Base對象的操作    
}

4. type_info類

type_info的操作包括:

  • t1 == t2:如果type_info對象t1和t2表示同一種類型,則返回true
  • t1 != t2:如果type_info對象t1和t2表示不同的類型,則返回true
  • t.name():返回一個C風格字符串,表示類型名字的可打印形式
  • t1.before(t2):返回一個bool值,表示t1是否位於t2之前,順序關係依賴於編譯器

type_info類沒有默認構造函數,而且它的拷貝和移動構造函數以及賦值運算符都被定義爲刪除的。因此,我們無法定義或者拷貝type_info類型的對象,也不能爲type_info對象賦值。創建type_info對象的唯一途徑就是使用typeid運算符。

枚舉類型

C++包含兩種枚舉:限定作用域和不限定作用域的。C++新標準引入了限定作用域的枚舉類型。

定義限定作用域的枚舉類型:

enum class open_modes {input, output, append};
// 等價
enum struct open_modes {input, output, append};

定義不限定作用域的枚舉類型:

  • 省略掉關鍵字class
  • 枚舉名字是可選的
enum color {red, yellow, green};
enum {floatPrec = 6, doublePrec = 10, double_doublePrec = 10};

1. 枚舉也可以定義新的類型

enum color {red, yellow, green};          // 不限定作用域的枚舉類型
enum stoplight {red, yellow, green};      // 錯誤: 重複定義了枚舉成員
enum class peppers {red, yellow, green};  // 正確: 枚舉成員被隱藏了

int i = color::red;     // 正確: 不限定作用域的枚舉類型的枚舉成員隱式地轉換成int
int j = peppers::red;   // 錯誤: 限定作用域的枚舉類型不會進行隱式轉換

2. 指定enum的大小

儘管每個enum都定義了唯一的類型,但是實際上enum是由某種整數類型表示的。在C++11新標準中,我們可以在enum的名字後加上冒號以及我們想在該enum使用的類型:

enum intValues : unsigned long long {
    charTyp = 255, shortTyp = 65535, intTyp = 65535,
    longTyp = 4394967295UL,
    long_longTyp = 18446744073709551615ULLL
};

3. 形參匹配與枚舉類型

// 不限定作用域的枚舉類型,潛在類型因機器而異
enum Tokens {INLINE = 128, VIRTUAL = 129};
void ff(Tokens);
void ff(int);
int main() {
    Tokens curTok = INLINE;
    ff(128);        // 精確匹配ff(int)
    ff(INLINE);     // 精確匹配ff(Tokens)
    ff(curTok);     // 精確匹配ff(Tokens)
    return 0;
}

類成員指針

成員指針是指可以指向類的非靜態成員的指針。一般情況下,指針指向一個對象,但是成員指針指示的是類的成員,而非類的對象。類的靜態成員不屬於任何對象,因此無須特殊的指向靜態成員的指針,指向靜態成員的指針和普通指針沒有任何區別。

成員指針的類型囊括了類的類型以及成員的類型。當初始化一個這樣的指針時,我們令其指向類的某個成員,但是不指定該成員所屬的對象;直到使用成員指針時,纔給提供成員所屬的對象。

爲了解釋成員指針的原理,我們使用該Screen類:

class Screen {
public:
    typedef std::string::size_type pos;
    char get_cursor() const { return contents[cursor]; }
    char get() const;
    char get(pos ht, pos wd) const;
private:
    std::string contents;
    pos cursor;
    pos height, width;
};

1. 數據成員指針

與普通指針不同的是,成員指針還必須包含成員所屬的類。因此,我們必須在*之前添加classname::以表示當前定義的指針可以指向classname的成員,例如:

// pdata可以指向一個常量(非常量)Screen對象的string成員
// 將pdata聲明爲"一個指向Screen類的const string成員的指針"
const string Screen::*pdata;

當我們初始化一個成員指針(或者向它賦值)時,需要指定它所指的成員。例如我們可以令pdata指向某個非特定Screen對象的contents成員:

pdata = &Screen::contents;

在C++11新標準中聲明成員指針最簡單的方法是使用auto或者decltype:

auto pdata = &Screen::contents;
1.1 使用數據成員指針

當我們初始化一個成員指針或者爲成員指針賦值時,該指針並沒有指向任何數據。成員指針指定了成員而非該成員所屬的對象,只有當解引用成員指針時我們才提供對象的信息。

我們可以通過.*->*兩個運算符解引用指針以獲得該對象的成員:

Screen myScreen, *pScreen = &myScreen;
// .*解引用pdata以獲得myScreen對象的contents成員
auto s = myScreen.*pdata;
// ->*解引用pdata以獲得pScreen所指對象的contents成員
s = pScreen->*pdata;
1.2 返回數據成員指針的函數

Screen的contents成員是私有的,因此之前對於pdata的使用必須位於Screen類的成員或友元內部,否則程序將發生錯誤。如果像Screen這樣的類希望我們可以訪問它的contents成員,最好定義一個函數:

class Screen {
public:
    // data是一個靜態成員, 返回一個成員指針
    static const std::string Screen::*data()
        { return &Screen::contents; }
}

// 我們調用data函數時, 將得到一個成員指針
// data()返回一個指向Screen類的contents成員的指針
const string Screen::*pdata = Screen::data();

// pdata指向Screen類的成員而非實際數據, 要想使用pdata必須把它綁定到Screen類型的對象上
auto s = myScreen.*pdata;

2. 成員函數指針

我們也可以定義指向類的成員函數的指針:

// pmf是一個指針, 它可以指向Screenn的某個常量成員函數
// 前提是該函數不接受任何實參, 並且返回一個char
auto pmf = &Screen::get_cursor;
  • 指向成員函數的指針也需要指定目標函數的返回類型和形參列表
  • 如果成員函數是const成員或引用成員,我們必須將const限定符或者引用限定符包含進來
  • 如果成員存在重載的問題,那麼我們必須顯式地聲明函數類型以明確指出來我們想要使用的是哪個函數
// 例如我們可以聲明一個指針, 令其指向含有兩個形參的get:
char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
pmf2 = &Screen::get;  // 必須加取地址符&, 在成員函數和指針之間不存在自動轉換規則
2.1 使用成員函數指針
Screen myScreen, *pScreen = &myScreen;
// 通過myScreen所指的對象調用pmf所指的函數
char c1 = (pScreen->*pmf)();
// 通過myScreen對象將實參0, 0 傳給含有兩個形參的get函數
char c2 = (myScreen.*pmf2)(0, 0);
2.2 使用成員指針的類型別名

使用類型別名或者typedef可以讓成員指針更容易理解,例如下面的類型別名將Action定義爲兩個參數get函數的同義詞:

// Action是一種可以指向Screen成員函數的指針, 它接收兩個pos實參, 返回一個char
using Action = char (Screen::*)(Screen::pos, Screen::pos) const;

通過使用Action,我們可以簡化指向get的指針定義:

Action get = &Screen::get;  // get指向Screen的get成員

我們可以將指向成員函數的指針作爲某個函數的返回類型或者形參類型:

// action接受一個Screen的引用和一個指向Screen成員函數的指針
Screen& action(Screen&, Action = &Screen::get);

Screen myScreen;
// 等價調用
action(myScreen);                // 使用默認實參
action(myScreen, get);           // 使用我們之前定義的變量get
action(myScreen, &Screen::get);  // 顯式地傳入地址 
2.3 成員指針函數表

對於普通函數指針和指向成員函數的指針來說,一種常見的用法是將其存入一個函數表當中。如果一個類含有幾個相同類型的成員,則這樣一張表可以幫助我們從這些成員中選擇一個。假定Screen類中含有幾個成員,每個函數負責將光標向指定的方向移動:

class Screen {
public:
    // 其他接口和實現成員與之前一致
    // 這幾個函數共同點: 不接受任何參數, 並且返回值是發生光標移動的Screen的引用
    Screen& home();        // 光標移動函數
    Screen& froward();
    Screen& back();
    Screen& up();
    Screen& down();
}

我們希望定義一個move函數,使其可以調用上面任意一個函數並執行對應的操作。爲了支持這個新函數,我們將在Screen中添加一個靜態成員,該成員是指向光標移動函數的指針的數組:

class Screen {
public:
    // Action是一個指針, 可以用任意一個光標移動函數對其賦值
    using Action = Screen& (Screen::*)();
    // 指定具體要移動的放共享
    enum Directions { HOME, FORWARD, BACK, UP, DOWN };
    Screen& move(Directions);
private:
    static Action Menu[];   // 函數表
};

Screen& Screen::move(Directions cm)
{
    // 運行this對象中索引值爲cm的元素
    return (this->*Menu[cm])();  // Menu[cm]指向一個成員函數
}

Screen::Action Screen::Menu[] = {
    &Screen::home,
    &Screen::forward,
    &Screen::back,
    &Screen::up,
    &Screen::down,
};

當我們調用move函數式,給它傳入一個表示光標移動方向的枚舉成員:

Screen myScreen;
myScreen.move(Screen::HOME);   // 調用myScreen.home
myScreen.move(Screen::DOWN);   // 調用myScreen.down

3. 將成員函數用作可調用對象

要想通過有一個指向成員函數的指針進行函數調用,必須首先利用.*或者->*運算符將該指針綁定到特定的對象上。因此與普通的函數指針不同,成員指針不是一個可調用對象,這樣的指針不支持函數調用運算符。

因爲成員指針不是可調用對象,因此我們不能直接將一個指向成員函數的指針傳遞給算法。比如我們想在一個string的vector中找到第一個空string,顯然不能這麼寫:

auto fp = &string::empty;   // fp指向string的empty函數
// 錯誤, 必須使用.*或者->*調用成員指針
find_if(svec.begin(), svec.end(), fp);

// 在find_if內部試圖執行如下代碼, 但是要想通過成員指針調用函數, 必須使用該->*運算符, 所以失敗
if (fp(*it))
3.1 使用fuction生成一個可調用對象
vector<string> svec;
function<bool (const string&)> fcn = &string::empty;
find_if(svec.begin(), svec.end(), fcn);

vector<string*> pvec;
function<bool (const string*)> fp = &string::empty;
find_if(pvec.begin(), pvec.end(), fp);
3.2 使用mem_fn生成一個可調用對象
find_if(svec.begin(), svec.end(), mem_fn(&string::empty));

// mem_fn生成的對象可以通過對象調用, 也可以通過指針調用
auto f = mem_fn(&string::empty);  // f接收一個string或者一個string*
f(*svec.begin());    // 正確: 傳入一個string對象, f使用.*調用empty
f(&svec[0]);         // 正確: 傳入一個string指針, f使用->*調用empty
3.3 使用bind生成一個可調用對象
auto it = find_if(svec.begin(), svec.end(), bing(&string::empty, _1));

// bind生成的可調用對象第一個實參既可以是string的指針, 也可以是string的引用
auto f = bind(&string::empty, _1);
f(*svec.begin());
f(&svec[0]);

嵌套類

一個類可以定義在另一個類的內部,前者被定義爲嵌套類。嵌套類的名字在外層類作用域中是可見的,在外層作用域之外不可見。

1. 聲明一個嵌套類

我們爲TextQuery類定義了一個名爲QueryResult的配套類。QueryResult類的主要作用是表示TextQuery對象上query操作的結果,顯然將QueryResult用作其他目的沒有任何意義。

class TextQuery {
public:
    class QueryResult;   // 嵌套類稍後定義
}

2. 在外層類之外定義一個嵌套類

// QueryResult是TextQuery的成員
class TextQuery::QueryResult {
    // 位於類的作用域內, 因此我們不必對QueryResult形參進行限定
    friend std::ostream& print(std::ostream&, const QueryResult&);
public:
    // 嵌套類可以直接使用外層類的成員, 無須對該名字進行限定
    QueryResult(std::string, std::shared_ptr<std::set<line_no>>,
               std::shared_ptr<std::vector<std::string>>);
};

3. 定義嵌套類的成員

TextQuery::QueryResult::QueryResult(string s, shared_ptr<set<line_no>> p,
                                   std::shared_ptr<std::vector<std::string>> f) :
    sought(s), lines(p), file(f) { }

union: 一種節省空間的類

聯合union是一種特殊的類,一個union可以有多個數據成員,但是在任意時刻只有一個數據成員有值。當我們給union的某個成員賦值之後,該union的其他成員就變成未定義的狀態了。

1. 定義union

union提供了一種有效的途徑使得我們可以方便地表示一組類型不同的互斥值。舉個例子,假設我們需要處理一些不同種類的數字數據和字符數據,則可以定義一個union來保存這些值:

// Token類型的對象只有一個成員, 該成員的類型可能是下列類型中的任意一個
union Token {
    // 默認情況下成員是公有的
    char cval;
    int ival;
    double dval;
};

2. 使用union類型

和其他內置類型一樣,默認情況下union是未初始化的,我們可以像顯式地初始化聚合類一樣用一對花括號內的初始值顯式地初始化一個union:

Token first_token = {'a'};      // 初始化cval成員, 如果提供初始值則用於初始化第一個成員
Token last_token;               // 未初始化的Token對象
Token *pt = new Token;          // 指向一個未初始化的Token對象的指針

3. 匿名union

union {
    char cval;
    int ival;
    double dval;
};  // 未命名對象, 我們可以直接訪問它的成員
cval = 'a';    // 爲匿名union賦一個新值
ival = 42;     // 該對象當前保存的值是42

4. 其他

由於現在電腦普遍內存較大,使用union的地方比較少,故這一塊後續碰上了再學習

局部類

類可以定義在某個函數的內部,我們稱這樣的類爲局部類local class

  • 局部類的成員必須完整定義在類的內部,所以成員函數的複雜性不能太高,一般只有幾行代碼
  • 在局部類中不允許聲明靜態數據成員

1. 局部類不能使用函數作用域中的變量

局部類只能訪問外層作用於定義的類型名、靜態變量以及枚舉成員。

int a, val;
void foo(int val)
{
    static int si;
    enum Loc { a = 1024, b };
    // Bar是foo的局部類
    struct Bar {
        Loc locVal;
        int barVal;
        
        void fooBar(Loc l = a)
        {
            barVal = val;    // 錯誤, val是foo的局部變量
            barVal = ::val;  // 正確: 使用一個全局變量
            barVal = si;     // 正確: 使用一個靜態局部對象
            locVal = b;      // 正確: 使用一個美劇成員
        }
    };
    // ...
}

2. 常規的訪問保護規則對局部類同樣適用

外層函數對局部類的私有成員沒有任何訪問特權。當然,局部類可以將外層函數聲明爲友元;或者更常見的是局部類將其成員聲明成公有的。在程序中有權訪問局部類的代碼非常有限,局部類已經封裝在函數作用域中,通過信息隱藏進一步封裝就顯得沒有必要。

固有的不可移植的特性

爲了支持低層編程,C++定義了一些固有的不可移植的特性。所謂不可移植的特性是指因機器而異的特性,當我們將不可移植特性的程序從一臺機器轉移到另一臺機器上時,通常需要重新編寫該程序。

1. 位域

類可以將其(非靜態)數據成員定義成位域bit-field,在一個位域中含有一定數量的二進制位。當一個程序需要向其他程序或者硬件設備傳遞二進制數據時,通常會用到位域。

typedef unsigned int Bit;
class File {
    Bit mode: 2;         // mode佔兩位
    Bit modified: 1;     // modified佔1位
    Bit prot_owner: 3;   // 佔3位
    Bit prot_group: 3;   // 佔3位
    Bit prot_world: 3;   // 佔3位
public:
    // 文件以八進制的形式表示
    enum modes { READ = 01, WRITE = 02, EXECUTE = 03 };
    File &opne(modes);
    void close();
    void write();
    bool isRead() const;
    void setWrite();
};
  • 如果可能的話,在類的內部連續定義的位域液壓鎖在同一整數的相鄰位,這意味着前面五個位域可能會存儲在一個unsigned int中,這些二進制位能否壓縮到一個整數中以及如何壓縮是與機器相關的
  • 取地址運算符&不能作用域位域,因此任何指針都無法指向類的位域
  • 最好將位域設爲無符號類型,存儲在帶符號類型中的位域的行爲將因具體實現而定

2. volatile限定符

直接處理硬件的程序通常包含這樣的數據元素,例如程序可能包含一個由系統時鐘定時更新的變量。當對象的值可能在程序的控制或檢測之外被改變時,應該將該對象聲明爲volatile,告訴編譯器不應對這樣的對象進行優化。

volatile int display_register;    // 該int值可能發生改變

3. 鏈接指示: extern "C"

C++程序有時候需要調用其他語言編寫的函數(比如C語言)。其他語言中的函數名字也必須在C++中進行聲明,並且該聲明必須指定返回類型和形參類別。

3.1 聲明一個非C++函數
// cstring頭文件中C函數的聲明
// 單語句鏈接指示
extern "C" size_t strlen(const char *);
// 複合語句鏈接指示
extern "C" {
    int strcmp(const char*, const char*);
    char *strcat(char*, const char*);
}
3.2 鏈接指示與頭文件
// 複合語句鏈接指示
extern "C" {
#include <string.h>  // 操作C風格字符串的C函數
}

上面的寫法意味着頭文件中所有普通函數聲明都被認爲是由鏈接指示的語言編寫的。

3.3 指向extern "C"函數的指針
// pf指向一個C函數, 該函數接受一個int返回void
extern "C" void (*pf)(int);

指向C函數的指針和指向C++函數的指針是不一樣的類型

3.4 鏈接指示對整個聲明都有效
// f1是一個C函數, 它的形參是一個指向C函數的指針
extern "C" void f1(void(*)(int));
3.5 導出C++函數到其他語言

通過鏈接指針對函數進行定義,我們可以令一個C++函數在其他語言編寫的程序中可用:

// calc函數可以被C程序調用
extern "C" double calc(double dparm) { /*...*/) }
3.6 重載函數與鏈接指示

C語言不支持函數重載,因爲也就不難理解一個C鏈接指示只能用於說明一組重載函數中的某一個了:

// 錯誤: 兩個extern "C"函數的名字相同
extern "C" void print(const char*);
extern "C" void print(int)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章