自学内容网 自学内容网

【Cpp】命名空间

前言

在C语言中,命名冲突通常发生在不同的作用域中使用了相同的标识符:

  1. 全局变量和局部变量同名: 如果在全局作用域和局部作用域中都定义了同名的变量,那么在局部作用域中,全局变量会被局部变量遮蔽。

    int globalVar; // 全局变量
    
    void someFunction() {
        int globalVar = 10; // 局部变量,遮蔽了全局变量
    }
    
  2. 函数参数和局部变量同名: 如果在函数参数列表中定义了一个参数,然后在函数体中又定义了一个同名的局部变量,那么参数会被局部变量遮蔽。

    void someFunction(int localVar) {
        int localVar = 20; // 局部变量,遮蔽了函数参数
    }
    
  3. 宏定义和变量名冲突: 如果使用#define定义了一个宏,而这个宏的名字和某个变量名相同,那么在宏展开时可能会导致命名冲突。

    #define MAX 100 // 宏定义
    
    int MAX; // 变量定义,与宏定义冲突
    
  4. 不同文件中的全局变量同名: 如果在不同的源文件中定义了同名的全局变量,并且这些文件被编译到同一个程序中,那么链接时可能会发生命名冲突。

    // file1.c
    int globalVar;
    
    // file2.c
    int globalVar;
    
  5. 结构体和变量名冲突: 如果在同一个作用域中定义了一个结构体和一个同名的变量,那么结构体的名称会被变量名遮蔽。

    struct MyStruct {
        int a;
    };
    
    struct MyStruct myVar; // 变量名,遮蔽了结构体名
    
    struct MyStruct *ptr; // 错误:这里会报错,因为MyStruct被遮蔽了
    
  6. typedef和变量名冲突: 如果使用typedef定义了一个类型,而这个类型的名字和某个变量名相同,那么在定义变量时可能会导致命名冲突。

    typedef int MyInt;
    
    MyInt myVar; // 变量定义
    
    int MyInt; // 错误:这里会报错,因为MyInt被遮蔽了
    

为了解决这一问题,Cpp添加了一个特性,namespace,命名空间,也有称作名字空间,通过namespace的特性来减少对变量名的污染

命名空间

把一个全局命名空间分成多个可管理的小空间。关键字namespace,如同class、struct、enum和union一样,把它们的成员的命名放到了不同的空间中去,与其他的关键字目的不同的是,namespace唯一一的目的是产生一个新的命名空间

创建一个命名空间

创建一个命名空间与创建一个类非常相似:

// C10:MyLib.cpp
namespace MyLib {
    // Declarations
}
int main() {}

这就产生了一个新的命名空间,其中包含了各种声明。然而,namespace与class、struct、union和enum有明显的区别:

  • namespace只能在全局范围内定义,但它们之间可以互相嵌套。
  • 在namespace定义的结尾,右花括号的后面不必跟一个分号。
  • 一个namespace可以在多个头文件中用一个标识符来定义,就好像重复定义一个类一样。
// C10:Header1.h
#ifndef HEADER1_H
#define HEADER1_H
namespace MyLib {
    extern int x;
    void f();
}
#endif // HEADER1_H
// C10:Header2.h
#ifndef HEADER2_H
#define HEADER2_H
#include "Header1.h"
// Add more names to MyLib
namespace MyLib { // NOT a redefinition!
    extern int y;
    void g();
}
#endif // HEADER2_H
// C10:Continuation.cpp
#include "Header2.h"
int main() {}
  • 一个namespace的命名可以用另一个命名来作它的别名,这样就不必敲那些开发商提供的冗长的命名了。
// C10:BobsSuperDuperLibrary.cpp
namespace BobsSuperDuperLibrary {
    class Widget { /*...*/ };
    class Poppit { /*...*/ };
    //...
}
// Too much to type! I'll alias it:
namespace Bcb = BobsSuperDuperLibrary;
int main() {}

未命名的命名空间

每个翻译单元都可包含一个未命名的命名空间——可以不用标识符而只用“namespace”再加一个命名空间。

// C10:UnnamedNamespaces.cpp
namespace {
    class Arm { /*...*/ };
    class Leg { /*...*/ };
    class Head { /*...*/ };
    class Robot {
        Arm arm[4];
        Leg leg[16];
        Head head[3];
        //...
    };
}
int main() {}

在这个空间中的命名自动地在翻译单元内无限制地有效。但要确保每个翻译单元只有一个未命名的命名空间。如果把一个局部命名放在一个未命名的命名空间中,不需要加上static说明就可以让它们作内部连接。

友元

可以在一个命名空间的类定义之内插入(inject)一个友元(friend)声明:

// C10:FriendInjection.cpp
namespace Me {
    class Us {
        //...
        friend void you();
    };
}
int main() {}

这样函数you()就成了命名空间Me的一个成员。

使用命名空间

在一个命名空间中引用一个命名可以采取三种方法:第一种方法是用作用域运算符,第二种方法是用using指令把所有命名引入到命名空间中,第三种方法是用using声明一次性引用命名

作用域解析

命名空间中的任何命名都可以用作用域运算法则明确地指定,就像引用一个类中的命名一样,学过java的人员也会想到一个包package的概念,namespace也像package一样,但:

  1. Java 的 package 与文件系统结构紧密相关,而 C++ 的 namespace 则不强制要求与文件系统结构对应。
  2. 访问控制:Java 的 package 通过访问修饰符和 package 声明来控制访问权限,而 C# 和 C++ 的 namespace 主要用于组织代码,访问控制由访问修饰符决定。
// C10:ScopeResolution.cpp
namespace X {
    class Y {
        static int i;
    public:
        void f();
    };
    class Z {
        void func();
    public:
        int X::Y::i = 9;
        class X::Z {
            int u, v, w;
        public:
            Z(int i);
            int g();
        };
        int X::Z::g() { return u = v = w = 0; }
        void X::func() {
            Z a(1);
            a.g();
        }
    }
    int main() {}
}

注意定义X::Y::i就像引用一个类Y的数据成员一样容易,Y如同被嵌套在类X中而不像是被嵌套在命名空间X中。

到目前为止,命名空间看上去很像类。

使用指令

用using关键字可以让我们立即进入整个命名空间,摆脱输入一个命名空间中完整标识符的麻烦。这种using和namespace关键字的搭配使用称为使用指令(using directive)。using关键字声明了一个命名空间中的所有命名是在当前范围内,所以可以很方便地使用这些未限定的命名。如果以一个简单的命名空间开始:

// C10:NamespaceInt.h
#ifndef NAMESPACEINT_H
#define NAMESPACEINT_H
namespace Int {
    enum sign { positive, negative };
    class Integer {
        int i;
        sign s;
    public:
        Integer(int ii = 0) : i(ii), s(i >= 0? positive : negative) {}
        sign sign() const { return s; }
    };
}
#endif // NAMESPACEINT_H
// C10:NamespaceOverriding1.cpp
#include "NamespaceMath.h"
int main() {
    using namespace Math;
    Integer a; // Hides Math::a
    a.setSign(negative);
    // Now scope resolution is necessary
    // to select Math::a:
    Math::a.setSign(positive);
}

如果有第二个命名空间,它包含命名空间Math的某些命名:

// C10:NamespaceOverriding2.cpp
#ifndef NAMESPACEOVERRIDING2_H
#define NAMESPACEOVERRIDING2_H
#include "NamespaceInt.h"
namespace Calculation {
    using namespace Int;
    Integer divide(Integer, Integer);
}
#endif // NAMESPACEOVERRIDING2_H

因为这个命名空间也是用using指令来引入的,这样就可能产生冲突。不过,这种二义性出现在命名的使用时,而不是在using指令使用时。

// C10:OverridingAmbiguity.cpp
#include "NamespaceMath.h"
#include "NamespaceOverriding2.h"
void s() {
    using namespace Math;
    using namespace Calculation;
    // Everything's ok until:
    // divide(1, 2); // Ambiguity
}
int main() {}

这样,即使永远不产生歧义性,使用using指令引入带命名冲突的命名空间也是可能的。

使用声明

可以用使用声明(using declaration)一次性引入到当前范围内。这种方法不像using指令那样把那些命名当成当前范围的全局名来看待,using声明是在当前范围之内进行的一个声明,这就意味着在这个范围内它可以不顾来自using指令的命名。

// C10:UsingDeclaration.h
#ifndef USINGDECLARATION_H
#define USINGDECLARATION_H
namespace U {
    inline void f() {}
    inline void g() {}
}
namespace V {
    inline void f() {}
    inline void g() {}
}
namespace V {
    inline void f() {}
    inline void g() {}
}
#endif // USINGDECLARATION_H
// C10:UsingDeclaration1.cpp
#include "UsingDeclaration.h"
void h() {
    using namespace U; // Directive
    using V::f; // Declaration
    f(); // Calls V::f();
    U::f(); // Must fully qualify to call
}
// C10:UsingDeclaration2.cpp
#include "UsingDeclaration.h"
void m() {
    using namespace Q;
    f(); // Calls U::f();
    g(); // Calls V::g();
}
int main() {}

一个using声明是一个别名,它允许在不同的命名空间声明同样的函数。如果不想由于引入不同命名空间而导致重复定义一个函数时,可以使用using声明,它不会引起任何二义性和重复。

命名空间的使用

上面所介绍的一些规则刚开始时也许会使我们感到气馁,特别是当我们知道将来一直使用它们会有什么感觉时,尤其如此。一般说来,只要真正理解了它们的工作机理,使用它们也会变得非常简单。需要记住的关键问题是当引入一个全局using指令时(可以在任何范围之外通过使用namespace),就已经为那个文件打开了该命名空间。对于一个实现文件(一个.cpp文件)来说,这通常是一个好方法,因为只有在该文件编译结束时,using指令才会起作用。也就是说,它不会影响任何其他的文件,所以可以每次在一个实现文件中调整对命名空间的控制。 例如,如果发现由于在一个特定的实现文件中使用太多的using指令而产生命名冲突,就要对该文件做简单的改变,以致使用明确的限定或者using声明来消除命名冲突,这样不用修改其他的实现文件。

头文件的情况与此不同。不要把一个全局的using指令引入到一个头文件中,因为那将意味着包含这个头文件的任何其他头文件也会打开这个命名空间(头文件可以被另一个头文件包含)。

所以,在头文件中,最好使用明确的限定或者被限定在一定范围内的using指令和using声明。在本专栏中将讨论这种用法,通过这种方法,就不会“污染”全局命名空间和退到C++的命名空间引入前的世界。

小结

虽然命名可以嵌套在类中,但全局函数、全局变量以及类的命名还是在同一个全局命名空间中。虽然static关键字可以使变量和函数进行内部连接(使它们文件静态,从而做到一定的控制。但在一个大项目中,如果对全局的命名空间缺乏控制就会引起很多问题。为了解决这些问题,开发商常常使用冗长、难懂的命名,以使冲突减少,但这样我们不得不一个一个地敲这些命名(typedef常常用来简化这些命名),namespace的要求也就应运而生,它可以使我们的变量名称和函数名称变得更短,虽然不一定清晰,但是可以在一定程度上减少冲突。利用using 指令的时候,我们要考虑是否会对该变量名称是否会对后续的程序产生一定的影响,毕竟using指令是全局作用产生的,如果会对后续程序产生影响,我们就用作用域解析,这时候就像一个类一样,虽然说稍稍麻烦了一些,但是我们还是可以解决命名冲突的问题————毕竟这就是namespace这一特性存在的原因。


原文地址:https://blog.csdn.net/Mongxin_Chan/article/details/143580802

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!