作用域
1. 命名空間(Namespaces)
在.cc文件中,提倡使用不具名的命名空間(unnamed namespaces,譯者注:不具名的命名空間就像不具名的類一樣,似乎被介紹的很少:-()。使用具名命名空間時,其名稱可基於項目或路徑名稱,不要使用using指示符。
定義:命名空間將全局作用域細分為不同的、具名的作用域,可有效防止全局作用域的命名沖突。
優點:命名空間提供了(可嵌套)命名軸線(name axis,譯者注:將命名分割在不同命名空間內),當然,類也提供了(可嵌套)的命名軸線(譯者注:將命名分割在不同類的作用域內)。
舉例來說,兩個不同項目的全局作用域都有一個類Foo,這樣在編譯或運行時造成沖突。如果每個項目將代碼置於不同命名空間中,project1::Foo和project2::Foo作為不同符號自然不會沖突。
缺點:命名空間具有迷惑性,因為它們和類一樣提供了額外的(可嵌套的)命名軸線。在頭文件中使用不具名的空間容易違背C++的唯一定義原則(One Definition Rule (ODR))。
結論:根據下文將要提到的策略合理使用命名空間。
1) 不具名命名空間(Unnamed Namespaces)
在.cc文件中,允許甚至提倡使用不具名命名空間,以避免運行時的命名沖突:
namespace { // .cc 文件中
// 命名空間的內容無需縮進
enum { UNUSED, EOF, ERROR }; // 經常使用的符號
bool AtEof() { return pos_ == EOF; } // 使用本命名空間內的符號EOF
} // namespace
然而,與特定類關聯的文件作用域聲明在該類中被聲明為類型、靜態數據成員或靜態成員函數,而不是不具名命名空間的成員。像上文展示的那樣,不具名命名空間結束時用注釋// namespace標識。
不能在.h文件中使用不具名命名空間。
2) 具名命名空間(Named Namespaces)
具名命名空間使用方式如下:
命名空間將除文件包含、全局標識的聲明/定義以及類的前置聲明外的整個源文件封裝起來,以同其他命名空間相區分。
// .h文件
namespace mynamespace {
// 所有聲明都置於命名空間中
// 注意不要使用縮進
class MyClass {
public:
...
void Foo();
};
} // namespace mynamespace
// .cc文件
namespace mynamespace {
// 函數定義都置於命名空間中
void MyClass::Foo() {
...
}
} // namespace mynamespace
通常的.cc文件會包含更多、更復雜的細節,包括對其他命名空間中類的引用等。
#include "a.h"
DEFINE_bool(someflag, false, "dummy flag");
class C; // 全局命名空間中類C的前置聲明
namespace a { class A; } // 命名空間a中的類a::A的前置聲明
namespace b {
...code for b... // b中的代碼
} // namespace b
不要聲明命名空間std下的任何內容,包括標准庫類的前置聲明。聲明std下的實體會導致不明確的行為,如,不可移植。聲明標准庫下的實體,需要包含對應的頭文件。
最好不要使用using指示符,以保證命名空間下的所有名稱都可以正常使用。
// 禁止——污染命名空間
using namespace foo;
在.cc文件、.h文件的函數、方法或類中,可以使用using。
// 允許:.cc文件中
// .h文件中,必須在函數、方法或類的內部使用
using ::foo::bar;
在.cc文件、.h文件的函數、方法或類中,還可以使用命名空間別名。
// 允許:.cc文件中
// .h文件中,必須在函數、方法或類的內部使用
namespace fbz = ::foo::bar::baz;
2. 嵌套類(Nested Class)
當公開嵌套類作為接口的一部分時,雖然可以直接將他們保持在全局作用域中,但將嵌套類的聲明置於命名空間中是更好的選擇。
定義:可以在一個類中定義另一個類,嵌套類也稱成員類(member class)。
class Foo {
private:
// Bar是嵌套在Foo中的成員類
class Bar {
...
};
};
優點:當嵌套(成員)類只在被嵌套類(enclosing class)中使用時很有用,將其置於被嵌套類作用域作為被嵌套類的成員不會污染其他作用域同名類。可在被嵌套類中前置聲明嵌套類,在.cc文件中定義嵌套類,避免在被嵌套類中包含嵌套類的定義,因為嵌套類的定義通常只與實現相關。
缺點:只能在被嵌套類的定義中才能前置聲明嵌套類。因此,任何使用Foo::Bar*指針的頭文件必須包含整個Foo的聲明。
結論:不要將嵌套類定義為public,除非它們是接口的一部分,比如,某個方法使用了這個類的一系列選項。
3. 非成員函數(Nonmember)、靜態成員函數(Static Member)和全局函數(Global Functions)
使用命名空間中的非成員函數或靜態成員函數,盡量不要使用全局函數。
優點:某些情況下,非成員函數和靜態成員函數是非常有用的,將非成員函數置於命名空間中可避免對全局作用域的污染。
缺點:將非成員函數和靜態成員函數作為新類的成員或許更有意義,當它們需要訪問外部資源或具有重要依賴時更是如此。
結論:
有時,不把函數限定在類的實體中是有益的,甚至需要這麼做,要麼作為靜態成員,要麼作為非成員函數。非成員函數不應依賴於外部變量,並盡量置於某個命名空間中。相比單純為了封裝若干不共享任何靜態數據的靜態成員函數而創建類,不如使用命名空間。
定義於同一編譯單元的函數,被其他編譯單元直接調用可能會引入不必要的耦合和連接依賴;靜態成員函數對此尤其敏感。可以考慮提取到新類中,或者將函數置於獨立庫的命名空間中。
如果你確實需要定義非成員函數,又只是在.cc文件中使用它,可使用不具名命名空間或static關聯(如static int Foo() {...})限定其作用域。
4. 局部變量(Local Variables)
將函數變量盡可能置於最小作用域內,在聲明變量時將其初始化。
C++允許在函數的任何位置聲明變量。我們提倡在盡可能小的作用域中聲明變量,離第一次使用越近越好。這使得代碼易於閱讀,易於定位變量的聲明位置、變量類型和初始值。特別是,應使用初始化代替聲明+賦值的方式。
int i;
i = f(); // 壞——初始化和聲明分離
nt j = g(); // 好——初始化時聲明
注意:gcc可正確執行for (int i = 0; i < 10; ++i)(i的作用域僅限for循環),因此其他for循環中可重用i。if和while等語句中,作用域聲明(scope declaration)同樣是正確的。
while (const char* p = strchr(str, '/')) str = p + 1;
注意:如果變量是一個對象,每次進入作用域都要調用其構造函數,每次退出作用域都要調用其析構函數。
// 低效的實現
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. 全局變量(Global Variables)
class類型的全局變量是被禁止的,內建類型的全局變量是允許的,當然多線程代碼中非常數全局變量也是被禁止的。永遠不要使用函數返回值初始化全局變量。
不幸的是,全局變量的構造函數、析構函數以及初始化操作的調用順序只是被部分規定,每次生成有可能會有變化,從而導致難以發現的bugs。
因此,禁止使用class類型的全局變量(包括STL的string, vector等等),因為它們的初始化順序有可能導致構造出現問題。內建類型和由內建類型構成的沒有構造函數的結構體可以使用,如果你一定要使用 class類型的全局變量,請使用單件模式(singleton pattern)。
對於全局的字符串常量,使用C風格的字符串,而不要使用STL的字符串:
const char kFrogSays[] = "ribbet";
雖然允許在全局作用域中使用全局變量,使用時務必三思。大多數全局變量應該是類的靜態數據成員,或者當其只在.cc文件中使用時,將其定義到不具名命名空間中,或者使用靜態關聯以限制變量的作用域。
記住,靜態成員變量視作全局變量,所以,也不能是class類型!
譯者:這一篇主要提到的是作用域的一些規則,總結一下:
1. .cc中的不具名命名空間可避免命名沖突、限定作用域,避免直接使用using提示符污染命名空間;
2. 嵌套類符合局部使用原則,只是不能在其他頭文件中前置聲明,盡量不要public;
3. 盡量不用全局函數和全局變量,考慮作用域和命名空間限制,盡量單獨形成編譯單元;
4. 多線程中的全局變量(含靜態成員變量)不要使用class類型(含STL容器),避免不明確行為導致的bugs。
作用域的使用,除了考慮名稱污染、可讀性之外,主要是為降低耦合度,提高編譯、執行效率。