重要的事情说三遍:
作用域(Scopes)
在C++中,变量、函数和复合类型等命名实体需要在使用前声明。程序中声明的位置会影响其可见性:
在任何块外部声明的实体具有全局作用域,这意味着它的名称在代码中的任何地方都是有效的。而在块内(例如函数或选择语句)声明的实体具有块作用域,并且仅在声明它的特定块内可见,而在块外部不可见。
具有块作用域的变量称为局部变量(local variables)。
例如,在函数体内声明的变量是局部变量,其作用域延伸到函数的结束(即关闭函数定义的右大括号}
),但不包括函数外部:
int foo; // 全局变量
int some_function ()
{
int bar; // 局部变量
bar = 0;
}
int other_function ()
{
foo = 1; // 可以:foo 是全局变量
bar = 2; // 错误:bar 在此函数中不可见
}
在每个作用域中,名称只能代表一个实体。例如,在同一作用域中不能有两个相同名称的变量:
int some_function ()
{
int x;
x = 0;
double x; // 错误:此作用域中名称已被使用
x = 0.0;
}
具有块作用域的实体的可见性延伸到块的末尾,包括内嵌块。然而,内嵌块因为是不同的块,可以重用外部作用域中存在的名称来指代不同的实体;在这种情况下,该名称在内嵌块中只指代不同的实体,而在外部它仍指代原始实体。例如:
// 内嵌块作用域
#include <iostream>
using namespace std;
int main () {
int x = 10;
int y = 20;
{
int x; // 可以,内嵌作用域
x = 50; // 为内嵌 x 赋值
y = 50; // 为(外部)y 赋值
cout << "内嵌块:\n";
cout << "x: " << x << '\n';
cout << "y: " << y << '\n';
}
cout << "外部块:\n";
cout << "x: " << x << '\n';
cout << "y: " << y << '\n';
return 0;
}
注意,y
在内嵌块中没有被隐藏,因此访问 y
仍然访问的是外部变量。
在引入块的声明中声明的变量(例如函数参数和在循环和条件语句中声明的变量,如在 for
或 if
中声明的变量)是引入块的局部变量。
命名空间(Namespaces)
在一个特定作用域中只能有一个实体具有特定的名称。对于局部名称来说,这很少是个问题,因为块通常比较短,名称在其中有特定的用途,例如命名计数变量、参数等。
但对于非局部名称来说,名称冲突的可能性更大,特别是考虑到库可能声明了许多函数、类型和变量,这些都不是局部的,而且有些非常通用。
命名空间允许我们将本应具有全局作用域的命名实体分组到更狭窄的作用域中,赋予它们命名空间作用域。这允许将程序元素组织到不同的逻辑作用域中,以名称引用它们。
声明命名空间的语法是:
namespace identifier
{
named_entities
}
其中 identifier
是任何有效的标识符,named_entities
是包含在命名空间中的变量、类型和函数的集合。例如:
namespace myNamespace
{
int a, b;
}
在这种情况下,变量 a
和 b
是在名为 myNamespace
的命名空间中声明的普通变量。
这些变量可以在其命名空间内正常访问,使用其标识符(无论是 a
还是 b
),但如果从 myNamespace
命名空间外部访问它们,则必须用作用域操作符 ::
进行适当的限定。例如,要从 myNamespace
外部访问前面的变量,应按以下方式限定:
myNamespace::a
myNamespace::b
命名空间特别有助于避免名称冲突。例如:
// 命名空间
#include <iostream>
using namespace std;
namespace foo
{
int value() { return 5; }
}
namespace bar
{
const double pi = 3.1416;
double value() { return 2*pi; }
}
int main () {
cout << foo::value() << '\n';
cout << bar::value() << '\n';
cout << bar::pi << '\n';
return 0;
}
在这个例子中,有两个同名的函数:value
。一个在 foo
命名空间中定义,另一个在 bar
中。由于有命名空间,没有发生重新定义错误。还请注意,pi
在 bar
命名空间内未限定地访问(仅作为 pi
),而在 main
中再次访问时,需要限定为 bar::pi
。
命名空间可以拆分:代码的两个片段可以在同一个命名空间中声明:
namespace foo { int a; }
namespace bar { int b; }
namespace foo { int c; }
这声明了三个变量:a
和 c
在命名空间 foo
中,而 b
在命名空间 bar
中。命名空间甚至可以跨不同的翻译单元(即跨不同的源代码文件)扩展。
using
关键字 using
将名称引入当前的声明区域(如块),从而避免了需要限定名称。例如:
// using
#include <iostream>
using namespace std;
namespace first
{
int x = 5;
int y = 10;
}
namespace second
{
double x = 3.1416;
double y = 2.7183;
}
int main () {
using first::x;
using second::y;
cout << x << '\n';
cout << y << '\n';
cout << first::y << '\n';
cout << second::x << '\n';
return 0;
}
注意在 main
中,变量 x
(没有任何名称限定符)指的是 first::x
,而 y
指的是 second::y
,正如 using
声明所指定的。变量 first::y
和 second::x
仍然可以访问,但需要完全限定的名称。
关键字 using
也可以作为指令引入整个命名空间:
// using
#include <iostream>
using namespace std;
namespace first
{
int x = 5;
int y = 10;
}
namespace second
{
double x = 3.1416;
double y = 2.7183;
}
int main () {
using namespace first;
cout << x << '\n';
cout << y << '\n';
cout << second::x << '\n';
cout << second::y << '\n';
return 0;
}
在这种情况下,通过声明我们正在使用命名空间 first
,所有不带名称限定符的 x
和 y
的直接使用也被视为在命名空间 first
中。
using
和 using namespace
的有效性仅在它们声明的同一块中或在全局作用域中使用时在整个源代码文件中有效。例如,可以通过将代码分成不同的块,先使用一个命名空间的对象,然后使用另一个命名空间的对象:
// using namespace 示例
#include <iostream>
using namespace std;
namespace first
{
int x = 5;
}
namespace second
{
double x = 3.1416;
}
int main () {
{
using namespace first;
cout << x << '\n';
}
{
using namespace second;
cout << x << '\n';
}
return 0;
}
命名空间别名(Namespace aliasing)
现有的命名空间可以用新名称别名化,语法如下:
namespace new_name = current_name;
std 命名空间
C++ 标准库的所有实体(变量、类型、常量和函数)都在 std
命名空间中声明。这些教程中的大多数示例实际上都包含以下行:
using namespace std;
这将 std
命名空间的所有名称直接引入代码的可见范围。这是为了便于理解和缩短示例的长度而在这些教程中这样做的,但许多程序员更喜欢在其程序中对标准库的每个元素进行完全限定。例如,不是:
cout << "Hello world!";
更常见的是:
std::cout << "Hello world!";
无论标准库中的元素是通过 using
声明引入还是每次使用时完全限定,都不会改变程序的行为或效率。这主要是风格偏好的问题,尽管对于混合库的项目,通常更喜欢显式限定。
存储类(Storage classes)
具有全局或命名空间作用域的变量的存储在整个程序的持续时间内分配。这称为静态存储(static storage),与块内声明的局部变量的存储形成对比。这些变量使用称为自动存储(automatic storage)。局部变量的存储仅在声明它们的块期间可用;之后,该存储可以用于其他函数的局部变量,或以其他方式使用。
但具有静态存储的变量和具有自动存储的变量之间还有另一个实质性差异:
- 具有静态存储的变量(例如全局变量),如果未显式初始化,将自动初始化为零。
- 具有自动存储的变量(例如局部变量),如果未显式初始化,将未初始化,因此具有不确定的值。
例如:
// 静态存储 vs 自动存储
#include <iostream>
using namespace std;
int x;
int main ()
{
int y;
cout << x << '\n'; // 0
cout << y << '\n'; // 任何可能的值
return 0;
}
实际输出可能会有所不同,但只有 x
的值保证为零。y
实际上可以包含任何值(包括零)。