C++中的作用域解析
Roger.Yi (
roger2yi@gmail.com)
C++到底有多少作用域,它们的作用是什么,又是如何相互影响的。
上图显示了C++中所有的作用域,也试图图形化它们之间粗略关系,这里当然无法表达的很准确。这是因为它们之间并没有语义上的一致性,通常着眼点于不同的领域,相互间存在复杂的互动关系。
名字空间域
名字空间主要用于解决名字冲突的问题,在名字空间出现之前,库的作者通常通过附加给库中的类型,全局变量和函数予特定的前缀来防止名字冲突的问题,例如dbus库的Error类型和Error初始化函数被命名为:
DBusError
dbus_init_error
有了名字空间后,我们就可以通过附加名字空间的名字来构成名字的限定名(Qualified Name)来解决名字冲突的问题。
当然更主要的是我们可以通过名字空间别名,使用声明(特定的名字)和使用指示(全部名字)来达成即能有效防止冲突,又能在已确定的上下文中更方便的访问名字的作用。
跟Java的包机制不同,名字空间是纯逻辑上的,它不具备对文件组织上任何的物理约束,一个名字空间可以跨越多个编译单元(常见的方式,一个库一个名字空间),但也可以一个编译单元包含多个名字空间(比较少见,通常是用来通过嵌套的子名字空间来防止一些函数重载上的意外发生)。(Java中的包,编译单元,类型域的包含关系更加明确,容易理解和使用,一个包必然包含一个或多个编译单元,一个编译单元也必然包括一个或多个类型,而且只能有一个是包可见的——公共类)
有的意见认为,名字空间引起的问题比它解决的要多,比如连接时名字解析的问题,特别是不同编译器编译出来的程序片段(静态库,导入库,Object文件)如何能够正确的连接。名字空间也使得重载的决议规则变的更复杂。所以像有些著名的库仍然坚持使用前缀的方式,比如QT。
名字空间出现后,以前的全局域就变成了名字空间的一个特例——全局名字空间。没有放置在某个名字空间的类型,具有编译单元外部链接的非成员函数和变量就缺省属于全局名字空间。
编译单元域
编译单元域是个比较特殊的域,它通常跟代码的物理组织方式有关。一个编译单元中非成员变量和函数的名字可以有外部链接,从而使得链接器可以用来解决跨编译单元的名字引用的问题(跨编译单元的变量的名字引用通常被视为是邪恶的东西)。但是也可以没有外部链接,从而防止一些无意中产生的副作用(错误的引用了不是预想中的名字)。
没有外部链接的名字处于编译单元域,这个可以通过附加static修饰符来达成。例如:
static double static_d = 3.0;
也可以放置在一个特殊的名字空间里,这个名字空间叫做匿名名字空间,每个编译单元都可以有一个匿名名字空间,不同编译单元之间的匿名名字空间不会相互影响,处于匿名名字空间的名字只能被该编译单元所访问。例如:
namespace
{
double intern_d = 2.0;
}
namespace foo
{
static double static_d = 3.0;
Foo::Foo(void)
{
intern_d = 3.0;
}
Foo::~Foo(void)
{
}
}
像上面的非成员变量intern_d就是处于编译单元foo(foo.cpp)的匿名名字空间中。如果在头文件中试图导出这个变量,例如:
extern double intern_d;
实际上你会得到一个“
ambiguous symbol”的编译错误(在VC 2008中)。
类型域
用户使用struct和class定义一个自定义类型,也同时构成了一个类型域,处于类型域里面的变量和函数被称为成员变量和成员函数,它们可以是静态的(属于类型),也可以是非静态的(属于实例)。静态的成员变量和成员函数与非成员变量和函数类似,而类型在这里只是起到一个特殊的名字空间的作用,或者说是附加类型成员访问规则的名字空间,公共的静态成员函数如果是可见的,那也是可访问的,也就是具备外部链接。
函数域
每个函数都构成了一个函数域,函数域的概念跟变量的存储位置和生命期有关。函数的参数和在函数中声明并定义的变量被称为局部变量或者是自动变量,它们分配在堆栈上,它们随着函数的执行而生成,随着函数的退出而消亡。而静态成员变量和非成员变量则分配在静态存储区中,它们的位置是固定的,生命期从程序启动一直到程序关闭。(在自由存储区中动态分配的对象是由使用者来控制其生命周期的。)
函数里面还可以有多个局部域,比如说每个if,else,else if,for,while,do,switch,try,catch块,或者是用户自己产生的代码块(使用{}包围)。局部域的作用通常是用来进一步限制局部变量的使用范围,在某个局部域声明的局部变量,在退出该局部域时会被自动销毁。用户自己产生的代码块(局部域)多用于所谓的关键区,用来同步线程对外部状态的访问。如果函数需要写的很长,刻意的区分不同的局部域也有助于代码的可读性和防止不必要的错误。