首页 > 代码库 > C++命名空间

C++命名空间

在一个给定作用域中定义的每个名字在该作用域中必须是唯一的,同一个作用域内名字冲突问题称为命名空间污染问题。

命名空间为防止名字冲突提供了更加可控的机制,命名空间能够划分全局命名空间,一个命名空间就是一个作用域。

 

1:命名空间定义以关键字 namespace 开始,后接命名空间的名字:

namespace cplusplus_primer {
    ...
}

 

命名空间的名字在定义该命名空间的作用域中必须是唯一的。命名空间可以在全局作用域或其他作用域内部定义,但不能在函数或类内部定义。命名空间作用域不能以分号结束。

 

2:命名空间中的每个名字必须引用该命名空间中的唯一实体。因为不同命名空间引入不同作用域,所以不同命名空间可以具有同名成员。

在命名空间中定义的名字可以被命名空间中的其他成员直接成员,命名空间外部的代码必须指出名字定义在哪个命名空间中。从命名空间外部使用命名空间成员时,总是使用限定名:namespace_name::member_name

 

3:命名空间可以在几个部分中定义。命名空间由它的分离定义部分的总和构成,命名空间是累积的。一个命名空间的分离部分可以分散在多个文件中,在不同文本文件中的命名空间定义也是累积的。

namespace namespace_name {
    // declarations
}

 

上面的定义既可以定义新的命名空间,也可以添加到现在命名空间中。如果名字 namespace_name 不是引用前面定义的命名空间,则用该名字创建新的命名空间,否则,这个定义打开一个已存在的命名空间,并将这些新声明加到那个命名空间。

 

4:可以在命名空间外部定义命名空间成员,用类似于在类外部定义类成员的方式:名字的命名空间声明必须在作用域中,并且定义必须指定该名字所属的命名空间:

// namespace members defined outside the namespace must use qualified names
cplusplus_primer::Sales_item 
cplusplus_primer::operator+(const Sales_item& lhs, const Sales_item& rhs)
{
    Sales_item ret(lhs);
    // ...
}

 

这个定义看起来类似于定义在类外部的类成员函数,返回类型和函数名由命名空间名字限定。一旦看到完全限定的函数名,就处于命名空间的作用域中。因此,形参表和函数体中的命名空间成员引用可以使用非限定名引用 Sales_item。

 

虽然可以在命名空间定义的外部定义命名空间成员,对这个定义可以出现的地方仍有些限制,只有包围成员声明的命名空间可以包含成员的定义。例如,operator+ 既可以定义在命名空间 cplusplus_primer 中,也可以定义在全局作用域中,但它不能定义在不相关的命名空间中。

 

5:定义在全局作用域的名字(在任意类、函数或命名空间外部声明的名字)是定义在全局命名空间中的。全局命名空间是隐式声明的,存在于每个程序中。在全局作用域定义实体的每个文件将那些名字加到全局命名空间。

可以用作用域操作符引用全局命名空间的成员。因为全局命名空间是隐含的,它没有名字,所以记号 ::member_name 引用全局命名空间的成员。

 

6:一个嵌套命名空间即是一个嵌套作用域——其作用域嵌套在包含它的命名空间内部。嵌套命名空间中的名字遵循常规规则:外围命名空间中声明的名字被嵌套命名空间中同一名字的声明所屏蔽。嵌套命名空间内部定义的名字局部于该命名空间。外围命名空间之外的代码只能通过限定名引用嵌套命名空间中的名字:

namespace cplusplus_primer {
    // first nested namespace:
    // defines the Query portion of the library
    namespace QueryLib {
        class Query { /* ... */ };
        Query operator&(const Query&, const Query&);
        // ...
    }
    
    // second nested namespace:
    // defines the Sales_item portion of the library
    namespace Bookstore {
        class Item_base { /* ... */ };
        class Bulk_item : public Item_base { /* ... */ };
        // ...
    }
}

 

嵌套命名空间中成员的名字由外围命名空间的名字和嵌套命名空间的名字构成。例如,嵌套命名空间 QueryLib 中声明的类的名字是 cplusplus_primer::QueryLib::Query 。

 

7:命名空间可以是未命名的,未命名的命名空间在定义时没有给定名字。未命名的命名空间以关键字 namespace 开头,接在关键字 namespace 后面的是由花括号定界的声明块。

未命名的命名空间与其他命名空间不同,未命名的命名空间的定义局部于特定文件,从不跨越多个文本文件。未命名的命名空间可以在给定文件中不连续,但不能跨越文件,每个文件有自己的未命名的命名空间。

 

未命名的命名空间用于声明局部于文件的实体。在未命名的命名空间中定义的变量在程序开始时创建,在程序结束之前一直存在。

未命名的命名空间中定义的名字可直接使用,毕竟,没有命名空间名字来限定它们。不能使用作用域操作符来引用未命名的命名空间的成员。

 

未命名的命名空间中定义的名字只在包含该命名空间的文件中可见。如果另一文件包含一个未命名的命名空间,两个命名空间不相关。两个命名空间可以定义相同的名字,而这些定义将引用不同的实体。

 

未命名空间中定义的名字可以在定义该命名空间所在的作用域中找到。如果在文件的最外层作用域中定义未命名的命名空间,那么,未命名的空间中的名字必须与全局作用域中定义的名字不同:

int i; // global declaration for i
namespace {
    int i;
}
// error: ambiguous defined globally and in an unnested, unnamed namespace
i = 10;

 

上面的写法,会导致编译错误。

 

像任意其他命名空间一样,未命名的命名空间也可以嵌套在另一命名空间内部。如果未命名的命名空间是嵌套的,其中的名字按常规方法使用外围命名空间名字访问:

namespace local {
    namespace {
        int i;
    }
}
// ok: i defined in a nested unnamed namespace is distinct from global i
local::i = 42;

 

 

在标准 C++ 中引入未命名命名空间之前,程序必须将名字声明为 static,使它们局部于一个文件。文件中静态声明的使用从 C 语言继承而来,在 C语言中,声明为 static 的局部实体在声明它的文件之外不可见。C++ 不赞成文件静态声明。

 

8:像 namespace_name::member_name 这样引用命名空间的成员很麻烦,解决办法有三个:using声明、using指示、命名空间别名。

 

一个 using 声明一次只引入一个命名空间成员:using std::string

using 声明中引入的名字遵循常规作用域规则。从 using 声明点开始,直到包含 using 声明的作用域的末尾,名字都是可见的。外部作用域中定义的同名实体被屏蔽。

简写名字只能在声明它的作用域及其嵌套作用域中使用,一旦该作用域结束了,就必须使用完全限定名。

 

命名空间别名将较短的同义词与命名空间名字相关联。例如,像

namespace cplusplus_primer { /* ... */ };

 

这样的长命名空间名字,可以像下面这样与较短的同义词相关联:

namespace primer = cplusplus_primer;

 

命名空间别名声明以关键字 namespace 开头,接(较短的)命名空间别名名字,再接 =,再接原来的命名空间名字和分号。如果原来的命名空间名字是未定义的,就会出错。

 

命名空间别名也可以引用嵌套的命名空间。除了编写

cplusplus_primer::QueryLib::Query tq;

 

 

之外,我们可以定义和使用 cplusplus_primer::QueryLib: 的别名:

namespace Qlib = cplusplus_primer::QueryLib;
Qlib::Query tq;

 

一个命名空间可以有许多别名,所有别名以及原来的命名空间名字都可以互换使用。

 

像 using 声明一样,using 指示使我们能够使用命名空间名字的简写形式。与 using 声明不同,using 指示无法控制使得哪些名字可见——它们都是可见的。

using 指示以关键字 using 开头,后接关键字 namespace,再接命名空间名字。如果该名字不是已经定义的命名空间名字,就会出错。

using 指示使得特定命名空间所有名字可见,没有限制。短格式名字可从using 指示点开始使用,直到出现 using 指示的作用域的末尾。

 

用 using 指示引入的名字的作用域比 using 声明的更复杂。using 声明将名字直接放入出现 using 声明的作用域,好像 using 声明是命名空间成员的局部别名一样。因为这种声明是局部化的,冲突的机会最小。

using 指示具有将命名空间成员提升到包含命名空间本身和 using 指示的最近作用域的效果。

在最简单的情况下,假定有命名空间 A 和函数 f,二者都在全局作用域中定义。如果 f 有关于 A 的 using 指示,那么,在 f 中,将好像 A 中的名字出现在全局作用域中 f 的定义之前一样:

// namespace A and function f are defined at global scope
namespace A {
    int i, j;
}
void f()
{
    using namespace A; // injects names from A into the global scope
    cout << i * j << endl; // uses i and j from namespace A
    //...
}

 

 

复杂点的例子:

namespace blip {
    int bi = 16, bj = 15, bk = 23;
}

int bj = 0; // ok: bj inside blip is hidden inside a namespace

void manip()
{
    // using directive - names in blip "added" to global scope
    using namespace blip;

    ++bi; // sets blip::bi to 17
    ++bj; // error: ambiguous global bj or blip::bj?
    ++::bj; // ok: sets global bj to 1
    ++blip::bj; // ok: sets blip::bj to 16
    int bk = 97; // local bk hides blip::bk
    ++bk; // sets local bk to 98
}

 

blip 的成员看来好像是在定义 blip 和 manip 的作用域中定义的一样。

因为名字在不同的作用域中,manip 内部的局部声明可以屏蔽命名空间的某些成员名字,局部变量 bk 屏蔽命名空间名字 blip::bk,在 manip 内部对 bk的引用没有二义性,它引用局部变量 bk。

对manip 而言,blip 成员 bj 看来好像声明在全局作用域中,但是,全局作用域存在另一名为 bj 的对象。这种冲突是允许的,但为了使用该名字,必须显式指出想要的是哪个版本,因此,在 manip 内部的 bj 使用是有二义性的:该名字既可引用全局变量又可引用命名空间 blip 的成员。为了使用像 bj 这样的名字,必须使用作用域操作符指出想要的是哪个名字。可以编写 ::bj 来获得在全局作用域中定义的变量,要使用 blip 中定义的bj,必须使用它的限定名字 blip::bj。

 

除了在函数或其他作用域内部,头文件不应该包含using 指示或 using 声明。在其顶级作用域包含using 指示或 using 声明的头文件,具有将该名字注入包含该头文件的文件中的效果。头文件应该只定义作为其接口的一部分的名字,不要定义在其实现中使用的名字。

 

9:考虑下面的简单程序:

std::string s;
// ok: calls std::getline(std::istream&, const std::string&)
getline(std::cin, s);

 

这段程序使用了 std::string 类型,但它不加限制地引用了 getline 函数。为什么可以无须特定 std:: 限定符或 using 声明而使用该函数?

它给出了屏蔽命名空间名字规则的一个重要例外:与类本身定义在同一命名空间中的函数(包括重载操作符),在用类类型对象(或类类型的引用及指针)作为实参的时候是可见的。

当编译器看到调用getline(std::cin, s)的时候,它在当前作用域,包含调用的作用域,以及定义 cin 的类型和string 类型的命名空间中查找匹配的函数。因此,它在命名空间 std 中查找并找到由 string 类型定义的 getline 函数。

 

10:当一个类声明友元函数的时候,函数的声明不必是可见的。如果不存在可见的声明,那么,友元声明具有将该函数或类的声明放入外围作用域的效果。如果类在命名空间内部定义,则没有另外声明的友元函数在同一命名空间中声明。

namespace A {
    class C {
        friend void f(const C&); // makes f a member of namespace A
    };
}

 

因为该友元接受类类型实参并与类隐式声明在同一命名空间中,所以使用它时可以无须使用显式命名空间限定符:

// f2 defined at global scope
void f2()
{
    A::C cobj;
    f(cobj); // calls A::f
}

 

 

11:有一个或多个类类型形参的函数的名字查找包括定义每个形参类型的命名空间。这个规则还影响怎样确定重载函数的候选集合,为找候选函数而查找定义形参类(以及定义其基类)的每个命名空间,将那些命名空间中任意与被调用函数名字相同的函数加入候选集合。即使这些函数在调用点不可见,也将之加入候选集合:

namespace NS {
    class Item_base { /* ... */ };
    void display(const Item_base&) { }
}
// Bulk_item‘s base class is declared in namespace NS
class Bulk_item : public NS::Item_base { };

int main() {
    Bulk_item book1;
    display(book1);
    return 0;
}

 

display 函数的实参 book1 具有类类型 Bulk_item。display 调用的候选函数不仅是在调用 display 函数的地方其声明可见的函数,还包括声明Bulk_item 类及其基类 Item_base 的命名空间中的函数。命名空间 NS 中声明的函数 display(const Item_base&) 被加到候选函数集合中。

 

using 声明或 using指示可以将函数加到候选集合。如果命名空间内部的函数是重载的,那么,该函数名字的 using 声明声明了所有具有该名字的函数。

由 using 声明引入的函数,重载出现 using 声明的作用域中的任意其他同名函数的声明。如果 using 声明在已经有同名且带相同形参表的函数的作用域中引入函数,则 using 声明出错,否则,using 定义给定名字的另一重载实例,效果是增大候选函数集合。

 

using 指示将命名空间成员提升到外围作用域。如果命名空间函数与命名空间所在的作用域中声明的函数同名,就将命名空间成员加到重载集合中:

namespace libs_R_us {
    extern void print(int);
    extern void print(double);
}
void print(const std::string &);

using namespace libs_R_us;
// using directive added names to the candidate set for calls to print:
// print(int) from libs_R_us
// print(double) from libs_R_us
// print(const std::string &) declared explicitly
void fooBar(int ival)
{
    print("Value: "); // calls global print(const string &)
    print(ival); // calls libs_R_us::print(int)
}

 

 

如果存在许多 using 指示,则来自每个命名空间的名字成为候选集合的组成部分:

namespace AW {
    int print(int);
}
namespace Primer {
    double print(double);
}
// using directives:
// form an overload set of functions from different namespaces
using namespace AW;
using namespace Primer;
long double print(long double);

int main() {
    print(1); // calls AW::print(int)
    print(3.1); // calls Primer::print(double)
    return 0;
}

 

全局作用域中 print 函数的重载集合包含函数 print(int)、print(double)和 print(long double),即使这些函数原来在不同的命名空间中声明,它们都是为 main 中函数调用考虑的重载集合的组成部分。

C++命名空间