6.12 - using 声明和 using 指令
Key Takeaway
- 函数内部使用
using
声明可以提高代码可读性,同时也是安全的 - 明确指定作用域好过
using
语句。尽可能地避免使用using
指令。在语句块中使用using
声明通常是可以的。
你可能在很多教材或者教程中都看过这样的程序:
1 2 3 4 5 6 7 8 9 10 |
|
有一些古早的 IDE 甚至会自动为你新建这样的 C++ 项目(以便你可以快速开始编写代码)。
如果你看到这样的代码,千万要原谅这些课本、教程或是编译器,它们很可能已经过时了。为什么?上完这节课你就知道了。
历史课
在 C++ 开始支持命名空间之前,所有现在位于 std
命名空间中的标识符,彼时都位于全局命名空间中。这使得用户的命名很容易和标准库中的名称发生冲突。不仅如此,在某个 C++版本下可以运行的程序,当使用了新版本的 C++ 时也很可能无法工作。
命名空间的标准化是在 1995 年完成的,从那时起,所有标准库中提供的功能,都被移动到了 std
命名空间。这样一来,不具有 std::
前缀的代码就不能正常工作了。
维护过大型项目的人都知道,任何一点改动都有可能破坏整个程序(不论改动是多么的不起眼)。因此,为所有相关的标识符添加 std::
前缀的风险非常大。所以我们必须提供一种更加有效的方式。
时间一晃到了今天——如果你大量使用了标准库提供的功能,为每一个相关的标识符添加 std::
也是一种非常重复的工作,而且在某些情况下会让代码的可读性变差。
为此,C++ 为这些问题提供了一种解决方法,即 using
语句。
在继续之前,我们先来介绍两个术语
限定名和非限定名
一个变量名(函数名)可以是限定名(qualified)或者非限定名(unqualified)。
包含了作用域信息的名字,称为限定名。大多数情况下,使用作用域解析运算符(::
)可以创建限定名。例如:
1 2 |
|
扩展阅读
结合使用作用域解析运算符和类名也可以创建限定名,或者对类对象使用成员选择操作符(.
或 ->
),例如:
1 2 3 4 5 |
|
而不具有作用域限定符的变量名,则为非限定名。例如 cout
和 x
都是非限定名,因为它们不包含任何的作用域信息。
Using 声明
减少重复输入 std::
的一个方法是使用 using
声明语句。using 声明语句使我们可以将非限定名作为限定名的别名来使用。
下面这个经典的 Hello World 程序,在第五行使用了 using 声明:
1 2 3 4 5 6 7 8 9 |
|
using std:: cout;
告诉编译器,我们要使用 std namespace
中的 cout
,所以在任何使用 cout
的地方,编译器都会假定它等价于 std:: cout
。如果在遇到 std:: cout
和其他 cout
矛盾的地方,则优先认为是 std:: cout
。因此,在第六行我们可以直接使用 cout
而无需使用 std:: cout
。
从这个小例子我们很难看出 using
语句能够带来的收益,但是当你在函数中大量使用 cout
的时候,using
声明可以让代码的可读性变得更好。注意,对于不同的变量名,需要单独的 using 声明(例如 std:: cout
是一个,std:: cin
是另一个)。
尽管这种方法没有使用 std::
前缀那么明确,但通常也被认为是一种安全的、可接受的方法(在函数内部使用)。
Using 指令
另外一种简化方式是使用 using
指令,它可以将该命名空间中的标识符都导入到使用了该 using
指令的作用域。
扩展阅读
从技术角度来讲,使用 using
指令其实并没有将标识符导入到当前的作用域——实际上它们被导入到外层作用域(关于外层作用域的详细信息可以参考 这里 )。不过,在外层作用域中并不能访问这些变量名,它们只能在使用了using
指令的作用域中访问。
从实际效果来看,使用 using
指令的行为看上去就像是变量名被导入到了当前作用域。为了不把事情搞复杂,在后面的课程中我们会沿用这种简化的说法,即变量名被导入到了当前作用域。
再回过头看 Hello World 程序,第五行使用了 using
指令:
1 2 3 4 5 6 7 8 |
|
using namespace std;
告诉编译器将 std
命名空间中的全部标识符导入到当前作用域(例如 main()
函数)。当我们使用非限定标识符 cout
的时候,它就会被解析为导入的 std:: cout
。
Using
指令是为了那些在命名空间发明前就存在的项目而设计的。在这种古老的项目中,有了 using
指令之后,我们就不必逐一地将非限定名修改为限定名(这么做风险非常大),只需将 using namespace std;
放置在文件的开始处,那么所有的已经被移动到了 std
命名空间中的标识符,仍然可以以非限定名的方式来使用。
Using 指令带来的问题 (为什么要避免使用 “using namespace std;”)
在现代 C++ 中,使用 using
指令通常是弊大于利的(除了能少打字以外)。因为使用 using
指令导入命名空间中的所有标识符会极大地提高命名冲突的可能(尤其是导入 std
)。
通过下面这个例子,我们可以看到 using
指令可能带来的二义性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
在上面的例子中,编译器无法决定这里的 main
中的 x
到底指的是 a:: x
还是 b:: x
。这种情况下,编译器会因为“歧义符号”错误而无法编译。解决这个问题的办法是移除其中一个 using
语句,使用 using
声明,或者直接使用显式的作用域修饰符(a::
或 b::
)。
再看看下面这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
在上面的例子中,编译器无法确定 cout
到底指的是 std:: cout
还是我们定义的 cout
函数,因此编译器仍然会因为“歧义符号”错误而无法编译。如果我们使用前缀 std::
,像这样:
1 |
|
或者使用 using
声明而不是 using
指令:
1 2 |
|
程序将可以顺利完成编译。 虽然你很可能并不会定义一个叫做 cout
的函数,但是 std
命名空间中有成百上千的标识符可能会与你定义的标识符产生冲突,比方说“count”, “min”, “max”, “search”, “sort” 等等。
而且,即使 using
指令今天没有带来命名冲突,也难保不在未来导致命名冲突。例如,如果你的代码使用 using
导入了某个命名空间,然后某天该库更新了,添加了一些新的变量名,那没准就会和你的代码产生冲突咯。
还有其他一些阴险的问题也可能发生。当库更新之后,它定义的一些新的函数,可能不仅函数名和我们定义的函数有冲突,甚至它更符合某种某处的函数调用方式,那么编译器就可能会使用该函数来替换我们的函数,这种情况下,程序的行为将会被改变。
考虑下面的代码:
```cpp title=foolib.h"" namespace foo { // pretend there is some useful code that we use here }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
程序运行后会打印 1
。
现在,更新 foolib(使用更新的 foolib.h
),程序编程下面这样:
foolib.h | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
main.cpp | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
main.cpp
文件完全没有改变,但是程序却输出了 2!
当编译器遇到函数调用时,它需要确定与函数调用最匹配的函数定义是什么。当有多个定义可供编译器选择时,它一定会选择无需参数转换的那一个。因为 0 是一个整型,C++ 一定会选择更加匹配的新定义的 someFcn(int)
(无需转换) 而不是 someFcn(double)
(需要将 int 转换为 double)。这样就会导致程序行为被意外地改变。
但是,如果我们使用的是 using
声明或者作用域修饰符的话,就完全不会发生这类问题。
最后,对于程序的读者来说,如果不使用作用域修饰符的话,读者将难确定某个函数到底是自定义的,还是属于某个库。例如,如果使用 using
指令:
1 2 3 4 5 6 |
|
这种情况下就很难确定 foo()
指的是 ns:: foo()
还是用户自定义的 foo()
函数。在使用现代 IDE 时,如果将鼠标悬浮在变量名上时,IDE 一般都可以帮我们确定标识符到底指的是什么,但是这样显然很麻烦,毕竟每个变量名都悬浮鼠标看一下是很麻烦的。
如果不是使用 using
指令,代码的可读性就会好很多:
1 2 3 4 5 |
|
在这个版本中,ns:: foo()
显然是调用库函数。而 foo()
则是调用用户自定义函数。
Using 声明和指令的作用域
如果在语句块中使用 using
声明或 using
指令,那对应的变量名只在该作用域中有效。这是一个很好的特性,因为它将命名冲突的风险限定在了语句块中。
如果在全局作用域中使用 using
声明或 using
指令,变量名将影响到整个文件(具有文件作用域)。
取消或者替换 using 语句
一旦使用了 using
语句,在相同的作用域中将没有办法对其进行取消或替换。
1 2 3 4 5 6 7 8 9 |
|
如果希望限制 using
语句的作用范围,可以将它放在一个语句块中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
当然,使用作用域解析运算符可以一劳永逸地解决这些问题。
using 语句的最佳实践
除非遇到非常特殊的情况,请避免使用 using
指令 (尤其是 using namespace std;
)。在语句块中使用 using
声明通常被认为是安全的。绝对不要在头文件的全局命名空间中使用它们,在源文件的全局作用域中有限地使用它们。
最佳实践
明确指定作用域好过 using
语句。尽可能地避免使用 using
指令。在语句块中使用 using
声明通常是可以的。
相关内容
using
关键字还可以用来定义类型别名,我们会在8.6 - typedef 和类型别名中详细介绍类型别名。