7-13 using声明与using指令
你可能在许多教科书和教程中都见过这个程序:
#include <iostream>
using namespace std;
int main()
{
cout << "Hello world!\n";
return 0;
}
若你看到此文,请立即行动。你的教材或教程很可能已过时。本节课我们将探究原因。
提示
某些集成开发环境(IDE)在创建新的C++项目时,也会自动生成类似的程序(这样您就能立即编译代码,而非从空白文件开始)。
简短的历史回顾
在C++尚未支持命名空间之前,如今属于std命名空间的所有名称都位于全局命名空间中。这导致程序标识符与标准库标识符之间发生命名冲突。某些在旧版C++下运行的程序,在新版C++中可能出现命名冲突问题。
1995年,命名空间成为标准特性,标准库的所有功能均从全局命名空间移入命名空间std。这一变更导致仍使用未加std::前缀的旧代码失效。
任何参与过大型代码库开发的人都知道,对代码库的任何改动(无论多么微小)都可能导致程序失效。将所有移入std命名空间的名称都添加std::前缀,风险极高。人们迫切需要解决方案。
快进到今天——如果你频繁使用标准库,为每个标准库函数都添加 std:: 前缀会变得重复乏味,某些情况下甚至会降低代码可读性。
C++ 通过 using 语句为这两个问题提供了解决方案。
但首先,让我们定义两个术语。
限定名与非限定名
名称可分为限定名与非限定名。
限定名qualified name是指包含关联作用域的名称。通常,名称通过作用域解析运算符(::)使用命名空间进行限定。例如:
std::cout // identifier cout is qualified by namespace std
::foo // identifier foo is qualified by the global namespace
对于高级读者
名称也可通过作用域解析运算符(::)由类名限定,或通过成员选择运算符(. 或 ->)由类对象限定。例如:class C; // some class C::s_member; // s_member is qualified by class C obj.x; // x is qualified by class object obj ptr->y; // y is qualified by pointer to class object ptr
未限定名unqualified name是指不包含作用域限定符的名称。例如,cout 和 x 就是未限定名称,因为它们不包含关联的作用域。
using声明
减少重复输入std::的一种方法是使用using声明using declaration语句。using声明允许我们使用未限定名称(无作用域)作为限定名称的别名。
以下是我们使用using声明的基本Hello world程序(第5行):
#include <iostream>
int main()
{
using std::cout; // this using declaration tells the compiler that cout should resolve to std::cout
cout << "Hello world!\n"; // so no std:: prefix is needed here!
return 0;
} // the using declaration expires at the end of the current scope
使用声明 using std::cout; 告知编译器我们将使用来自 std 命名空间的 cout 对象。因此每当遇到 cout 时,编译器会默认指代 std::cout。若在 main() 函数内同时存在 std::cout 与其他 cout 变量且发生命名冲突,则优先使用 std::cout。因此在第6行代码中,我们可以直接使用cout替代std::cout。
在这个简单的示例中节省的操作并不多,但若在函数内部频繁使用cout,使用声明能显著提升代码可读性。需注意每个名称都需要单独的using声明(例如std::cout、std::cin等各需一条声明)。
using声明的有效范围从声明点延伸至声明所在作用域的结尾。
尽管using声明不如使用std::前缀明确,但通常被认为在源文件(.cpp)中使用是安全且可接受的,下面将讨论的一个例外情况除外。
using 指令
另一种简化方法是使用 using 指令using directive。using 指令允许在指令作用域内直接引用给定命名空间中的所有标识符,无需限定。
面向高级读者
出于技术原因,using指令并不会在当前作用域中为名称引入新含义——相反,它们会在外部作用域中为名称引入新含义(有关选择哪个外部作用域的更多细节可参见此处)。
以下是我们再次呈现的Hello world程序,第5行添加了using指令:
#include <iostream>
int main()
{
using namespace std; // all names from std namespace now accessible without qualification
cout << "Hello world!\n"; // so no std:: prefix is needed here
return 0;
} // the using-directive ends at the end of the current scope
using指令 using namespace std; 告诉编译器,在当前作用域(此处为函数 main())中,所有 std 命名空间中的名称均可无需限定直接访问。当我们使用未限定标识符 cout 时,它将解析为 std::cout。
using 指令是为解决旧版代码库的问题而设计的方案——这些代码库在调用标准库功能时使用了未限定名称。与其手动将每个未限定名称更新为限定名称(存在风险),不如在每个文件开头添加单条 using 指令(using namespace std;),这样所有移至 std 命名空间的名称仍可直接使用。
使用指令的问题(又名为何应避免使用“using namespace std;”)
在现代C++中,使用指令带来的收益(节省少量输入)通常远低于其风险。这源于两个因素:
- 使用指令允许对命名空间中所有名称进行无限定访问(可能包含大量永远不会使用的名称)。
- using 指令不会优先选择该指令所标识命名空间中的名称,而忽略其他名称。
最终结果是命名冲突的可能性显著增加(尤其当导入 std 命名空间时)。
首先,让我们通过一个示例说明 using 指令如何引发命名冲突:
#include <iostream>
namespace A
{
int x { 10 };
}
namespace B
{
int x{ 20 };
}
int main()
{
using namespace A;
using namespace B;
std::cout << x << '\n';
return 0;
}

在上例中,编译器无法确定 main 中的 x 指代 A::x 还是 B::x。这种情况下,编译会因“符号歧义”错误而失败。可通过移除其中一条 using 指令、改用 using 声明,或明确限定 x(如 A::x 或 B::x)来解决此问题。
下面是另一个更微妙的示例:
#include <iostream> // imports the declaration of std::cout
int cout() // declares our own "cout" function
{
return 5;
}
int main()
{
using namespace std; // makes std::cout accessible as "cout"
cout << "Hello, world!\n"; // uh oh! Which cout do we want here? The one in the std namespace or the one we defined above?
return 0;
}

在此示例中,编译器无法确定我们未加限定的 cout 是指 std::cout 还是我们定义的 cout 函数,因此会再次因“符号歧义”错误导致编译失败。虽然这个例子很简单,但如果我们像这样显式添加前缀 std::cout:
std::cout << "Hello, world!\n"; // tell the compiler we mean std::cout
或使用 using 声明代替 using 指令:
using std::cout; // tell the compiler that cout means std::cout
cout << "Hello, world!\n"; // so this means std::cout

那么我们的程序从一开始就不会出现任何问题。虽然你可能不会写一个名为“cout”的函数,但std命名空间中还有数百个其他名称正等着与你的名称发生冲突。
即使当前using指令未引发命名冲突,它仍会使代码更易受未来冲突影响。例如,若你的代码包含某个库的using指令,而该库后续被更新,则更新库中引入的所有新名称都可能与现有代码发生命名冲突。
例如,以下程序能正常编译运行:
FooLib.h(某第三方库的一部分):
#ifndef FOOLIB
#define FOOLIB
namespace Foo
{
int a { 20 };
}
#endif
main.cpp
#include <iostream>
#include <FooLib.h> // a third-party library we installed outside our project directory, thus angled brackets used
void print()
{
std::cout << "Hello\n";
}
int main()
{
using namespace Foo; // Because we're lazy and want to access Foo:: qualified names without typing the Foo:: prefix
std::cout << a << '\n'; // uses Foo::a
print(); // calls ::print()
return 0;
}

现在假设你将FooLib更新到新版本,FooLib.h文件变更为如下内容:
FooLib.h(更新版):
#ifndef FOOLIB
#define FOOLIB
namespace Foo
{
int a { 20 };
void print() { std::cout << "Timmah!"; } // This function added
}
#endif

您的 main.cpp 文件并未修改,却无法再编译!这是因为使用声明导致 Foo::print() 可被直接调用为 print(),而此时调用 print() 究竟指代 ::print() 还是 Foo::print() 存在歧义。
此类问题还存在更隐蔽的变体。更新后的库可能引入不仅名称相同、且与某些函数调用匹配度更高的函数。此时编译器可能优先选用新函数,导致程序行为发生意外且无提示的改变。
请看以下程序:
Foolib.h(某第三方库的组成部分):
#ifndef FOOLIB_H
#define FOOLIB_H
namespace Foo
{
int a { 20 };
}
#endif
main.cpp:
#include <iostream>
#include <Foolib.h> // a third-party library we installed outside our project directory, thus angled brackets used
int get(long)
{
return 1;
}
int main()
{
using namespace Foo; // Because we're lazy and want to access Foo:: qualified names without typing the Foo:: prefix
std::cout << a << '\n'; // uses Foo::a
std::cout << get(0) << '\n'; // calls ::get(long)
return 0;
}

该程序运行并输出1。
现在假设我们更新了Foolib库,其中包含更新后的Foolib.h文件,内容如下:
Foolib.h(更新版):
#ifndef FOOLIB_H
#define FOOLIB_H
namespace Foo
{
int a { 20 };
int get(int) { return 2; } // new function added
}
#endif

我们的 main.cpp 文件再次未作任何改动,但该程序现在能够编译、运行并输出 2!
当编译器遇到函数调用时,它必须确定应将该调用与哪个函数定义匹配。在从一组潜在匹配函数中选择时,它会优先选择无需参数转换的函数,而非需要参数转换的函数。由于常量0是整数,C++会优先将print(0)匹配到新引入的print(int)函数(无需转换),而非print(long)函数(需要从int到long的转换)。这导致程序行为发生意外变化。
在此案例中,行为变化相当明显。但在更复杂的程序中,当返回值不仅被打印时,此问题可能极难发现。
若使用 using 声明或显式作用域限定符,此问题本可避免。
最后,缺乏显式作用域前缀会使读者难以区分库函数与程序自定义函数。例如使用 using 指令时:
using namespace NS;
int main()
{
foo(); // is this foo a user-defined function, or part of the NS library?
}
目前尚不清楚对 foo() 的调用究竟是调用 NS::foo(),还是调用用户自定义的 foo() 函数。现代集成开发环境(IDE)在将鼠标悬停于名称上方时应能自动消除这种歧义,但每次都要悬停查看函数来源实在令人厌烦。
若不使用 using 指令,代码将清晰得多:
int main()
{
NS::foo(); // clearly part of the NS library
foo(); // likely a user-defined function
}
在此版本中,对 NS::foo() 的调用显然是库函数调用。而对普通 foo() 的调用很可能是对用户自定义函数的调用(某些库——包括特定标准库头文件——确实会将名称置于全局命名空间中,因此无法完全确定)。
using 语句的作用域
若在代码块内使用 using 声明或 using 指令,则所声明的名称仅适用于该代码块(遵循常规代码块作用域规则)。这种设计颇具优势,因为它将命名冲突的可能性限制在该代码块内部。
若在命名空间(包括全局命名空间)中使用using声明或using指令,则这些名称适用于文件其余部分(具有文件作用域)。
请勿在头文件中或#include指令之前使用using语句
一条经验法则是:using语句不应出现在可能影响其他文件代码的位置,也不应出现在其他文件代码可能影响它们的位置。
更具体地说,这意味着不应在头文件中使用using语句,也不应在#include指令之前使用。
例如,若在头文件的全局命名空间中放置using语句,则所有包含该头文件的文件都会继承该using语句。这显然是不妥的。基于相同原因,此规则同样适用于头文件内的命名空间。
那么在头文件内部定义的函数中使用using语句呢?既然其作用域仅限于函数内部,应该没问题吧?答案依然是否定的。其原因与不应在#include指令前使用using语句相同。
事实证明,using语句的行为取决于已引入的标识符。这使得它们具有顺序依赖性——若先前引入的标识符发生变化,其功能也可能随之改变。
以下示例将说明此现象:
FooInt.h:
namespace Foo
{
void print(int)
{
std::cout << "print(int)\n" << std::endl;
}
}
FooDouble.h:
namespace Foo
{
void print(double)
{
std::cout << "print(double)\n" << std::endl;
}
}
main.cpp(正常):
#include <iostream>
#include "FooDouble.h"
#include "FooInt.h"
using Foo::print; // print means Foo::print
int main()
{
print(5); // Calls Foo::print(int)
}

运行时,该程序调用 Foo::print(int),该函数会输出 print(int)。
现在我们稍作修改 main.cpp。
main.cpp(糟糕版本):
#include <iostream>
#include "FooDouble.h"
using Foo::print; // we moved the using-statement here, before the #include directive
#include "FooInt.h"
int main()
{
print(5); // Calls Foo::print(double)
}

我们所做的只是将 using Foo::print; 移至 #include “FooInt.h” 之前。现在程序竟会输出 print(double)!无论原因如何,你应该会同意这种行为正是我们需要避免的!
那么回到正题,我们在头文件中定义的函数里不该使用using语句的原因也是同样道理——我们无法控制其他头文件会在我们头文件之前被#include,而这些头文件可能做了某些会改变using语句行为的事情!
唯一真正安全使用using语句的位置,是在源文件(.cpp)中完成所有#include包含之后。
面向进阶读者
本例涉及一个尚未讲解的概念——“函数重载”(详见第11.1节--函数重载入门)。理解本例只需知道:同一作用域内两个函数可拥有相同名称,只要它们的形参类型不同即可。由于 int 和 double 是不同类型,因此 Foo::print(int) 和 Foo::print(double) 可并存。
在正常版本中,当编译器遇到 using Foo::print 时,它已识别出 Foo::print(int) 和 Foo::print(double) 两个声明,因此允许直接调用 print()。由于 Foo::print(int) 比 Foo::print(double) 匹配度更高,最终调用的是 Foo::print(int)。
在错误版本中,编译器遇到using Foo::print时,仅见过Foo::print(double)的声明,因此仅使Foo::print(double)可作为无限定形式调用。当调用print(5)时,只有Foo::print(double)符合匹配条件,最终调用的是Foo::print(double)!
取消或替换using语句
一旦using语句被声明,就无法在声明作用域内取消或替换为其他using语句。
int main()
{
using namespace Foo;
// there's no way to cancel the "using namespace Foo" here!
// there's also no way to replace "using namespace Foo" with a different using statement
return 0;
} // using namespace Foo ends here
你所能做的就是从一开始就利用块作用域规则,有意限制using语句的作用域范围。
int main()
{
{
using namespace Foo;
// calls to Foo:: stuff here
} // using namespace Foo expires
{
using namespace Goo;
// calls to Goo:: stuff here
} // using namespace Goo expires
return 0;
}
当然,所有这些麻烦都可以通过在最初就明确使用作用域解析运算符(::)来避免。
using 语句的最佳实践
最佳实践
优先使用显式命名空间限定符而非 using 语句。
完全避免使用 using 指令(使用命名空间 std::literals 访问 s 和 sv 字面量后缀除外)。using 声明可在 .cpp 文件中使用,但需置于 #include 指令之后。请勿在头文件中使用 using 语句(尤其避免在头文件的全局命名空间中使用)。
相关内容
using 关键字还用于定义类型别名,这与 using 语句无关。类型别名将在第 10.7 课——类型别名与类型别名中讲解。

浙公网安备 33010602011771号