名称可见性

作用域

在C++中,诸如变量、函数和复合类型等命名实体在使用前需要先声明。声明在程序中出现的位置会影响其可见性。

在任何块之外声明的实体具有全局作用域,意味着其名称在代码的任何地方都有效。而在块(如函数或选择语句)内声明的实体具有块作用域,它只在声明所在的特定块内可见,在块外不可见。

具有块作用域的变量被称为局部变量

例如,在函数体中声明的变量是一个局部变量,其作用域延伸至函数末尾(即,直到关闭函数定义的右花括号 }),但在函数之外无效。

1
2
3
4
5
6
7
8
9
10
11
12
13
int foo;        // global variable

int some_function ()
{
  int bar;      // local variable
  bar = 0;
}

int other_function ()
{
  foo = 1;  // ok: foo is a global variable
  bar = 2;  // wrong: bar is not visible from this function
}

在每个作用域中,一个名称只能代表一个实体。例如,在同一作用域中不能有两个同名的变量。

1
2
3
4
5
6
7
int some_function ()
{
  int x;
  x = 0;
  double x;   // wrong: name already used in this scope
  x = 0.0;
}

具有块作用域的实体的可见性会延伸到该块的末尾,包括内部块。然而,内部块由于是不同的块,可以重用外部作用域中已存在的名称来指代不同的实体;在这种情况下,该名称将仅在内部块中指代不同的实体,从而隐藏了外部同名的实体。而在内部块之外,它仍然指代原始的实体。例如:

// inner block scopes
#include <iostream>
using namespace std;

int main () {
  int x = 10;
  int y = 20;
  {
    int x;   // ok, inner scope.
    x = 50;  // sets value to inner x
    y = 50;  // sets value to (outer) y
    cout << "inner block:\n";
    cout << "x: " << x << '\n';
    cout << "y: " << y << '\n';
  }
  cout << "outer block:\n";
  cout << "x: " << x << '\n';
  cout << "y: " << y << '\n';
  return 0;
}
inner block:
x: 50
y: 50
outer block:
x: 10
y: 50

请注意,y 在内部块中没有被隐藏,因此访问 y 仍然是访问外部变量。

在引入块的声明中声明的变量,例如函数参数和在循环和条件语句(如 for 或 if 中声明的变量)中声明的变量,其作用域仅限于它们所引入的块。

命名空间

在特定的作用域中,一个名称只能对应一个实体。这对于局部名称来说很少成为问题,因为块通常相对较短,并且名称在其中有特定的用途,例如命名一个计数器变量、一个参数等……

但是非局部名称带来了更多名称冲突的可能性,特别是考虑到库可能会声明许多函数、类型和变量,它们本质上都不是局部的,而且其中一些非常通用。

命名空间允许我们将本应具有全局作用域的命名实体分组到更窄的作用域中,赋予它们命名空间作用域。这使得可以将程序的元素组织到由名称引用的不同逻辑作用域中。

声明命名空间的语法是:


namespace identifier
{
  named_entities
}


其中 identifier 是任何有效的标识符,而 named_entities 是包含在命名空间内的一组变量、类型和函数。例如:

1
2
3
4
namespace myNamespace
{
  int a, b;
}

在这种情况下,变量 ab 是在名为 myNamespace 的命名空间内声明的普通变量。

这些变量可以在其命名空间内部正常地通过其标识符(ab)访问,但如果从 myNamespace 命名空间外部访问,则必须使用作用域解析运算符 :: 进行适当的限定。例如,要从 myNamespace 外部访问前面的变量,它们应被限定为:

1
2
myNamespace::a
myNamespace::b

命名空间对于避免名称冲突特别有用。例如:

// namespaces
#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;
}
5
6.2832
3.1416

在这种情况下,有两个同名函数:value。一个在命名空间 foo 中定义,另一个在 bar 中定义。由于命名空间的存在,没有发生重定义错误。另请注意,pi 在命名空间 bar 内部以非限定方式访问(直接用 pi),而在 main 函数中再次访问时,则需要将其限定为 bar::pi

命名空间可以被分割:一段代码的两个部分可以被声明在同一个命名空间中。

1
2
3
namespace foo { int a; }
namespace bar { int b; }
namespace foo { int c; }

这里声明了三个变量:ac 在命名空间 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;
}
5
2.7183
10
3.1416

注意在 main 函数中,变量 x(没有任何名称限定符)指向 first::x,而 y 指向 second::y,这正是 using 声明所指定的。变量 first::ysecond::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;
}
5
10
3.1416
2.7183

在这种情况下,通过声明我们正在使用命名空间 first,所有不带名称限定符直接使用的 xy 也会在命名空间 first 中查找。

usingusing namespace 仅在其声明所在的块中有效,或者如果它们直接在全局作用域中使用,则在整个源文件中有效。例如,可以通过将代码分割到不同的块中,先使用一个命名空间的对象,然后再使用另一个命名空间的对象:

// using namespace example
#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;
}
5
3.1416

命名空间别名

现有的命名空间可以用新名称设置别名,语法如下:

namespace new_name = current_name;

std 命名空间

C++标准库的所有实体(变量、类型、常量和函数)都在 std 命名空间中声明。实际上,这些教程中的大多数示例都包含了以下这行代码:

1
using namespace std;

这使得 std 命名空间中的所有名称在代码中可以直接访问。在这些教程中这样做是为了方便理解并缩短示例的长度,但许多程序员更喜欢在他们的程序中为每个使用的标准库元素进行限定。例如, вместо

1
cout << "Hello world!";

更常见的写法是:

1
std::cout << "Hello world!";

无论 std 命名空间中的元素是通过 using 声明引入,还是在每次使用时都进行完全限定,都不会以任何方式改变最终程序的行为或效率。这主要是一个风格偏好问题,尽管对于混合使用多个库的项目,显式限定通常是首选。

存储类

具有全局命名空间作用域的变量,其存储空间在整个程序运行期间都被分配。这被称为静态存储,与局部变量(在块内声明的变量)的存储方式形成对比。局部变量使用所谓的自动存储。局部变量的存储空间仅在其声明的块内可用;之后,该存储空间可能会被用于其他函数的局部变量,或作他用。

但具有静态存储的变量和具有自动存储的变量之间还有另一个实质性的区别:
- 具有静态存储的变量(如全局变量),如果没有被显式初始化,会自动初始化为零。
- 具有自动存储的变量(如局部变量),如果没有被显式初始化,则保持未初始化状态,因此其值是不确定的。

例如:
// static vs automatic storage
#include <iostream>
using namespace std;

int x;

int main ()
{
  int y;
  cout << x << '\n';
  cout << y << '\n';
  return 0;
}
0
4285838

实际输出可能会有所不同,但只有 x 的值保证为零。y 实际上可以包含任何值(包括零)。
Index
目录