天天看點

C++風格_作用域

文章目錄

    • 1、命名空間
    • 2、匿名命名空間和靜态變量
    • 3、非成員函數、靜态成員函數和全局函數
    • 4、局部變量
    • 5、靜态和全局變量

1、命名空間

鼓勵在 .cpp 檔案内使用匿名命名空間或 static 聲明. 使用具名的命名空間時, 其名稱可基于項目名或相對路徑. 禁止使用 using 訓示(using-directive)。禁止使用内聯命名空間(inline namespace)。

定義:

命名空間将全局作用域細分為獨立的, 具名的作用域, 可有效防止全局作用域的命名沖突.

優點:

雖然類已經提供了(可嵌套的)命名軸線 (YuleFox 注: 将命名分割在不同類的作用域内), 命名空間在這基礎上又封裝了一層.

舉例來說, 兩個不同項目的全局作用域都有一個類 Foo, 這樣在編譯或運作時造成沖突. 如果每個項目将代碼置于不同命名空間中,

project1::Foo

project2::Foo

作為不同符号自然不會沖突.

内聯命名空間會自動把内部的辨別符放到外層作用域,比如:

namespace X {
inline namespace Y {
void foo();
}  // namespace Y
}  // namespace X
           

X::Y::foo()

X::foo()

彼此可代替。内聯命名空間主要用來保持跨版本的 ABI 相容性。

缺點:

命名空間具有迷惑性, 因為它們使得區分兩個相同命名所指代的定義更加困難。

内聯命名空間很容易令人迷惑,畢竟其内部的成員不再受其聲明所在命名空間的限制。内聯命名空間隻在大型版本控制裡有用。

有時候不得不多次引用某個定義在許多嵌套命名空間裡的實體,使用完整的命名空間會導緻代碼的冗長。

在頭檔案中使用匿名空間導緻違背 C++ 的唯一定義原則 (One Definition Rule (ODR)).

結論:

根據下文将要提到的政策合理使用命名空間.

  • 遵守 命名空間命名 中的規則。
  • 像之前的幾個例子中一樣,在命名空間的最後注釋出命名空間的名字。
  • 用命名空間把檔案包含, gflags 的聲明/定義, 以及類的前置聲明以外的整個源檔案封裝起來, 以差別于其它命名空間:
// .h 檔案
namespace mynamespace {

// 所有聲明都置于命名空間中
// 注意不要使用縮進
class MyClass {
    public:
    ...
    void Foo();
};
           
} // namespace mynamespace
// .cc 檔案
namespace mynamespace {
// 函數定義都置于命名空間中
void MyClass::Foo() {
    ...
}
} // namespace mynamespace
           

更複雜的 .cpp 檔案包含更多, 更複雜的細節, 比如 gflags 或 using 聲明。

#include "a.h"
DEFINE_FLAG(bool, someflag, false, "dummy flag");
namespace a {
...code for a...                // 左對齊
} // namespace a
           
  • 不要在命名空間 std 内聲明任何東西, 包括标準庫的類前置聲明. 在 std 命名空間聲明實體是未定義的行為, 會導緻如不可移植. 聲明标準庫下的實體, 需要包含對應的頭檔案.
  • 不應該使用 using 訓示 引入整個命名空間的辨別符号。
// 禁止 —— 污染命名空間
using namespace foo;
           
  • 不要在頭檔案中使用 命名空間别名 除非顯式标記内部命名空間使用。因為任何在頭檔案中引入的命名空間都會成為公開API的一部分。
// 在 .cc 中使用别名縮短常用的命名空間
namespace baz = ::foo::bar::baz;
// 在 .h 中使用别名縮短常用的命名空間
namespace librarian {
namespace impl {  // 僅限内部使用
namespace sidetable = ::pipeline_diagnostics::sidetable;
}  // namespace impl

inline void my_inline_function() {
  // 限制在一個函數中的命名空間别名
  namespace baz = ::foo::bar::baz;
  ...
}
}  // namespace librarian
           
  • 禁止用内聯命名空間

2、匿名命名空間和靜态變量

在 .cpp 檔案中定義一個不需要被外部引用的變量時,可以将它們放在匿名命名空間或聲明為 static 。但是不要在 .h 檔案中這麼做。

定義:

所有置于匿名命名空間的聲明都具有内部連結性,函數和變量可以經由聲明為 static 擁有内部連結性,這意味着你在這個檔案中聲明的這些辨別符都不能在另一個檔案中被通路。即使兩個檔案聲明了完全一樣名字的辨別符,它們所指向的實體實際上是完全不同的。

結論:

推薦、鼓勵在 .cpp 中對于不需要在其他地方引用的辨別符使用内部連結性聲明,但是不要在 .h 中使用。

匿名命名空間的聲明和具名的格式相同,在最後注釋上

namespace :

namespace {
...
}  // namespace
           

3、非成員函數、靜态成員函數和全局函數

使用靜态成員函數或命名空間内的非成員函數, 盡量不要用裸的全局函數. 将一系列函數直接置于命名空間中,不要用類的靜态方法模拟出命名空間的效果,類的靜态方法應當和類的執行個體或靜态資料緊密相關.

優點:

某些情況下, 非成員函數和靜态成員函數是非常有用的, 将非成員函數放在命名空間内可避免污染全局作用域.

缺點:

将非成員函數和靜态成員函數作為新類的成員或許更有意義, 當它們需要通路外部資源或具有重要的依賴關系時更是如此.

結論:

有時, 把函數的定義同類的執行個體脫鈎是有益的, 甚至是必要的. 這樣的函數可以被定義成靜态成員, 或是非成員函數. 非成員函數不應依賴于外部變量, 應盡量置于某個命名空間内. 相比單純為了封裝若幹不共享任何靜态資料的靜态成員函數而建立類, 不如使用 2.1. 命名空間 。舉例而言,對于頭檔案 myproject/foo_bar.h , 應當使用

namespace myproject {
namespace foo_bar {
void Function1();
void Function2();
}  // namespace foo_bar
}  // namespace myproject
           

而非

namespace myproject {
class FooBar {
 public:
  static void Function1();
  static void Function2();
};
}  // namespace myproject
           

定義在同一編譯單元的函數, 被其他編譯單元直接調用可能會引入不必要的耦合和連結時依賴; 靜态成員函數對此尤其敏感. 可以考慮提取到新類中, 或者将函數置于獨立庫的命名空間内.

如果你必須定義非成員函數, 又隻是在 .cpp 檔案中使用它, 可使用匿名 2.1. 命名空間 或

static

連結關鍵字 (如

static int Foo() {...}

) 限定其作用域.

4、局部變量

将函數變量盡可能置于最小作用域内, 并在變量聲明時進行初始化.

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

int i;
i = f(); // 壞——初始化和聲明分離
           
vector<int> v;
v.push_back(1); // 用花括号初始化更好
v.push_back(2);
           

屬于

if

,

while

for

語句的變量應當在這些語句中正常地聲明,這樣子這些變量的作用域就被限制在這些語句中了,舉例而言:

while (const char* p = strchr(str, '/')) str = p + 1;
Warning
           

注意

有一個例外, 如果變量是一個對象, 每次進入作用域都要調用其構造函數, 每次退出作用域都要調用其析構函數. 這會導緻效率降低.

// 低效的實作
for (int i = 0; i < 1000000; ++i) {
    Foo f;                  // 構造函數和析構函數分别調用 1000000 次!
    f.DoSomething(i);
}
           

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

Foo f;                      // 構造函數和析構函數隻調用 1 次
for (int i = 0; i < 1000000; ++i) {
    f.DoSomething(i);
}
           

5、靜态和全局變量

禁止定義靜态儲存周期非POD變量,禁止使用含有副作用的函數初始化POD全局變量,因為多編譯單元中的靜态變量執行時的構造和析構順序是未明确的,這将導緻代碼的不可移植。

禁止使用類的 靜态儲存周期 變量:由于構造和析構函數調用順序的不确定性,它們會導緻難以發現的 bug 。不過

constexpr

變量除外,畢竟它們又不涉及動态初始化或析構。

靜态生存周期的對象,即包括了全局變量,靜态變量,靜态類成員變量和函數靜态變量,都必須是原生資料類型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 類型的指針、數組和結構體。

靜态變量的構造函數、析構函數和初始化的順序在 C++ 中是隻有部分明确的,甚至随着建構變化而變化,導緻難以發現的 bug. 是以除了禁用類類型的全局變量,我們也不允許用函數傳回值來初始化 POD 變量,除非該函數(比如 getenv() 或 getpid() )不涉及任何全局變量。函數作用域裡的靜态變量除外,畢竟它的初始化順序是有明确定義的,而且隻會在指令執行到它的聲明那裡才會發生。

同一個編譯單元内是明确的,靜态初始化優先于動态初始化,初始化順序按照聲明順序進行,銷毀則逆序。不同的編譯單元之間初始化和銷毀順序屬于未明确行為 (unspecified behaviour)。

同理,全局和靜态變量在程式中斷時會被析構,無論所謂中斷是從

main()

傳回還是對

exit()

的調用。析構順序正好與構造函數調用的順序相反。但既然構造順序未定義,那麼析構順序當然也就不定了。比如,在程式結束時某靜态變量已經被析構了,但代碼還在跑——比如其它線程——并試圖通路它且失敗;再比如,一個靜态 string 變量也許會在一個引用了前者的其它變量析構之前被析構掉。

改善以上析構問題的辦法之一是用

quick_exit()

來代替

exit()

并中斷程式。它們的不同之處是前者不會執行任何析構,也不會執行 atexit() 所綁定的任何

handlers

. 如果您想在執行

quick_exit()

來中斷時執行某

handler

(比如重新整理 log),您可以把它綁定到

_at_quick_exit()

. 如果您想在 exit() 和

quick_exit()

都用上該 handler, 都綁定上去。

綜上所述,我們隻允許 POD 類型的靜态變量,即完全禁用 vector (使用 C 數組替代) 和 string (使用

const char []

)。

如果您确實需要一個 class 類型的靜态或全局變量,可以考慮在 main() 函數或 pthread_once() 内初始化一個指針且永不回收。注意隻能用 raw 指針,别用智能指針,畢竟後者的析構函數涉及到上文指出的不定順序問題。

繼續閱讀