2. 作用域

2018-02-24 15:11 更新

2.1. 名字空間

Tip

鼓勵(lì)在 .cc 文件內(nèi)使用匿名名字空間. 使用具名的名字空間時(shí), 其名稱可基于項(xiàng)目名或相對(duì)路徑. 不要使用 using 關(guān)鍵字.

定義:名字空間將全局作用域細(xì)分為獨(dú)立的, 具名的作用域, 可有效防止全局作用域的命名沖突.優(yōu)點(diǎn):
雖然類已經(jīng)提供了(可嵌套的)命名軸線 (YuleFox 注: 將命名分割在不同類的作用域內(nèi)), 名字空間在這基礎(chǔ)上又封裝了一層.

舉例來說, 兩個(gè)不同項(xiàng)目的全局作用域都有一個(gè)類 Foo, 這樣在編譯或運(yùn)行時(shí)造成沖突. 如果每個(gè)項(xiàng)目將代碼置于不同名字空間中, project1::Fooproject2::Foo 作為不同符號(hào)自然不會(huì)沖突.

缺點(diǎn):
名字空間具有迷惑性, 因?yàn)樗鼈兒皖愐粯犹峁┝祟~外的 (可嵌套的) 命名軸線.

在頭文件中使用匿名空間導(dǎo)致違背 C++ 的唯一定義原則 (One Definition Rule (ODR)).

結(jié)論:根據(jù)下文將要提到的策略合理使用命名空間.

2.1.1. 匿名名字空間

  • .cc 文件中, 允許甚至鼓勵(lì)使用匿名名字空間, 以避免運(yùn)行時(shí)的命名沖突:
namespace {                             // .cc 文件中

// 名字空間的內(nèi)容無需縮進(jìn)
enum { kUNUSED, kEOF, kERROR };         // 經(jīng)常使用的符號(hào)
bool AtEof() { return pos_ == kEOF; }   // 使用本名字空間內(nèi)的符號(hào) EOF

} // namespace

然而, 與特定類關(guān)聯(lián)的文件作用域聲明在該類中被聲明為類型, 靜態(tài)數(shù)據(jù)成員或靜態(tài)成員函數(shù), 而不是匿名名字空間的成員. 如上例所示, 匿名空間結(jié)束時(shí)用注釋 // namespace 標(biāo)識(shí).

  • 不要在 .h 文件中使用匿名名字空間.

2.1.2. 具名的名字空間

具名的名字空間使用方式如下:

  • 用名字空間把文件包含, gflags [http://code.google.com/p/google-gflags/] 的聲明/定義, 以及類的前置聲明以外的整個(gè)源文件封裝起來, 以區(qū)別于其它名字空間:> > // .h 文件
    
    namespace mynamespace {

// 所有聲明都置于命名空間中
// 注意不要使用縮進(jìn)
class MyClass {
public:

void Foo();
};

} // namespace mynamespace

// .cc 文件
namespace mynamespace {

// 函數(shù)定義都置于命名空間中
void MyClass::Foo() {

}

} // namespace mynamespace


> 通常的 `.cc` 文件包含更多, 更復(fù)雜的細(xì)節(jié), 比如引用其他名字空間的類等.

> >     #include “a.h”

    DEFINE_bool(someflag, false, “dummy flag”);

    class C;                    // 全局名字空間中類 C 的前置聲明
    namespace a { class A; }    // a::A 的前置聲明

    namespace b {

    …code for b…                // b 中的代碼

    } // namespace b

不要在名字空間 `std` 內(nèi)聲明任何東西, 包括標(biāo)準(zhǔn)庫的類前置聲明. 在 `std` 名字空間聲明實(shí)體會(huì)導(dǎo)致不確定的問題, 比如不可移植. 聲明標(biāo)準(zhǔn)庫下的實(shí)體, 需要包含對(duì)應(yīng)的頭文件. 最好不要使用 ``using`` 關(guān)鍵字, 以保證名字空間下的所有名稱都可以正常使用.   // 禁止 —— 污染名字空間
    using namespace foo;

在 `.cc` 文件, `.h` 文件的函數(shù), 方法或類中, 可以使用 _``using`` 關(guān)鍵字_.

> > >   // 允許: .cc 文件中
    // .h 文件的話, 必須在函數(shù), 方法或類的內(nèi)部使用
    using ::foo::bar;

在 `.cc` 文件, `.h` 文件的函數(shù), 方法或類中, 允許使用名字空間別名.

> > >   // 允許: .cc 文件中
    // .h 文件的話, 必須在函數(shù), 方法或類的內(nèi)部使用

    namespace fbz = ::foo::bar::baz;

## 2.2. 嵌套類

> Tip

> 當(dāng)公有嵌套類作為接口的一部分時(shí), 雖然可以直接將他們保持在全局作用域中, 但將嵌套類的聲明置于名字空間內(nèi)是更好的選擇.

定義: 在一個(gè)類內(nèi)部定義另一個(gè)類; 嵌套類也被稱為 _成員類 (member class)_.

    class Foo {

    private:
        // Bar是嵌套在Foo中的成員類
        class Bar {
            …
        };

    };

優(yōu)點(diǎn): 當(dāng)嵌套 (或成員) 類只被外圍類使用時(shí)非常有用; 把它作為外圍類作用域內(nèi)的成員, 而不是去污染外部作用域的同名類. 嵌套類可以在外圍類中做前置聲明, 然后在 `.cc` 文件中定義, 這樣避免在外圍類的聲明中定義嵌套類, 因?yàn)榍短最惖亩x通常只與實(shí)現(xiàn)相關(guān). 缺點(diǎn): 嵌套類只能在外圍類的內(nèi)部做前置聲明. 因此, 任何使用了 `Foo::Bar*` 指針的頭文件不得不包含類 `Foo` 的整個(gè)聲明. 結(jié)論: 不要將嵌套類定義成公有, 除非它們是接口的一部分, 比如, 嵌套類含有某些方法的一組選項(xiàng).

## 2.3. 非成員函數(shù)、靜態(tài)成員函數(shù)和全局函數(shù)

> Tip

> 使用靜態(tài)成員函數(shù)或名字空間內(nèi)的非成員函數(shù), 盡量不要用裸的全局函數(shù).

優(yōu)點(diǎn):

某些情況下, 非成員函數(shù)和靜態(tài)成員函數(shù)是非常有用的, 將非成員函數(shù)放在名字空間內(nèi)可避免污染全局作用域.

缺點(diǎn):

將非成員函數(shù)和靜態(tài)成員函數(shù)作為新類的成員或許更有意義, 當(dāng)它們需要訪問外部資源或具有重要的依賴關(guān)系時(shí)更是如此.

結(jié)論:

有時(shí), 把函數(shù)的定義同類的實(shí)例脫鉤是有益的, 甚至是必要的. 這樣的函數(shù)可以被定義成靜態(tài)成員, 或是非成員函數(shù). 非成員函數(shù)不應(yīng)依賴于外部變量, 應(yīng)盡量置于某個(gè)名字空間內(nèi). 相比單純?yōu)榱朔庋b若干不共享任何靜態(tài)數(shù)據(jù)的靜態(tài)成員函數(shù)而創(chuàng)建類, 不如使用命名空間.

定義在同一編譯單元的函數(shù), 被其他編譯單元直接調(diào)用可能會(huì)引入不必要的耦合和鏈接時(shí)依賴; 靜態(tài)成員函數(shù)對(duì)此尤其敏感. 可以考慮提取到新類中, 或者將函數(shù)置于獨(dú)立庫的名字空間內(nèi).

如果你必須定義非成員函數(shù), 又只是在?`.cc`?文件中使用它, 可使用匿名名字空間或?`static`?鏈接關(guān)鍵字 (如?`static?int?Foo()?{...}`) 限定其作用域.

## 2.4. 局部變量

> Tip

> 將函數(shù)變量盡可能置于最小作用域內(nèi), 并在變量聲明時(shí)進(jìn)行初始化.

C++ 允許在函數(shù)的任何位置聲明變量. 我們提倡在盡可能小的作用域中聲明變量, 離第一次使用越近越好. 這使得代碼瀏覽者更容易定位變量聲明的位置, 了解變量的類型和初始值. 特別是,應(yīng)使用初始化的方式替代聲明再賦值, 比如:

int i;
i = f(); // 壞——初始化和聲明分離
int j = g(); // 好——初始化時(shí)聲明


注意, GCC 可正確實(shí)現(xiàn)了?`for?(int?i?=?0;?i??10;?++i)`?(`i`?的作用域僅限?`for`?循環(huán)內(nèi)), 所以其他?`for`?循環(huán)中可以重新使用?`i`. 在?`if`?和?`while`?等語句中的作用域聲明也是正確的, 如:

while (const char* p = strchr(str, ‘/’)) str = p + 1;


> Warning

> 如果變量是一個(gè)對(duì)象, 每次進(jìn)入作用域都要調(diào)用其構(gòu)造函數(shù), 每次退出作用域都要調(diào)用其析構(gòu)函數(shù).

// 低效的實(shí)現(xiàn)
for (int i = 0; i 1000000; ++i) {
Foo f; // 構(gòu)造函數(shù)和析構(gòu)函數(shù)分別調(diào)用 1000000 次!
f.DoSomething(i);
}


在循環(huán)作用域外面聲明這類變量要高效的多:

Foo f; // 構(gòu)造函數(shù)和析構(gòu)函數(shù)只調(diào)用 1 次
for (int i = 0; i 1000000; ++i) {
f.DoSomething(i);
}



## 2.5. 靜態(tài)和全局變量

> Tip

> 禁止使用 `class` 類型的靜態(tài)或全局變量: 它們會(huì)導(dǎo)致很難發(fā)現(xiàn)的 bug 和不確定的構(gòu)造和析構(gòu)函數(shù)調(diào)用順序.

靜態(tài)生存周期的對(duì)象, 包括全局變量, 靜態(tài)變量, 靜態(tài)類成員變量, 以及函數(shù)靜態(tài)變量, 都必須是原生數(shù)據(jù)類型 (POD : Plain Old Data): 只能是 int, char, float, 和 void, 以及 POD 類型的數(shù)組/結(jié)構(gòu)體/指針. 永遠(yuǎn)不要使用函數(shù)返回值初始化靜態(tài)變量; 不要在多線程代碼中使用非 `const` 的靜態(tài)變量.

不幸的是, 靜態(tài)變量的構(gòu)造函數(shù), 析構(gòu)函數(shù)以及初始化操作的調(diào)用順序在 C++ 標(biāo)準(zhǔn)中未明確定義, 甚至每次編譯構(gòu)建都有可能會(huì)發(fā)生變化, 從而導(dǎo)致難以發(fā)現(xiàn)的 bug. 比如, 結(jié)束程序時(shí), 某個(gè)靜態(tài)變量已經(jīng)被析構(gòu)了, 但代碼還在跑 – 其它線程很可能 – 試圖訪問該變量, 直接導(dǎo)致崩潰.

所以, 我們只允許 POD 類型的靜態(tài)變量. 本條規(guī)則完全禁止 `vector` (使用 C 數(shù)組替代), `string` (使用 `const char*`), 及其它以任意方式包含或指向類實(shí)例的東東, 成為靜態(tài)變量. 出于同樣的理由, 我們不允許用函數(shù)返回值來初始化靜態(tài)變量.

如果你確實(shí)需要一個(gè) `class` 類型的靜態(tài)或全局變量, 可以考慮在 ``main()` 函數(shù)或 `pthread_once()` 內(nèi)初始化一個(gè)你永遠(yuǎn)不會(huì)回收的指針.

> Note

> yospaly 譯注:

> 上文提及的靜態(tài)變量泛指靜態(tài)生存周期的對(duì)象, 包括: 全局變量, 靜態(tài)變量, 靜態(tài)類成員變量, 以及函數(shù)靜態(tài)變量.

## 譯者 (YuleFox) 筆記

1. `cc` 中的匿名名字空間可避免命名沖突, 限定作用域, 避免直接使用 `using` 關(guān)鍵字污染命名空間;
1. 嵌套類符合局部使用原則, 只是不能在其他頭文件中前置聲明, 盡量不要 `public`;
1. 盡量不用全局函數(shù)和全局變量, 考慮作用域和命名空間限制, 盡量單獨(dú)形成編譯單元;
1. 多線程中的全局變量 (含靜態(tài)成員變量) 不要使用 `class` 類型 (含 STL 容器), 避免不明確行為導(dǎo)致的 bug.
1. 作用域的使用, 除了考慮名稱污染, 可讀性之外, 主要是為降低耦合, 提高編譯/執(zhí)行效率.
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)