作用域
C++ 程序中出现的每个名字,只在某些可能不连续的源码部分中有效,这些部分被称为其作用域。
在作用域内,能用无限定名字查找将名字与其声明关联起来。
块作用域
块(复合语句)中声明的名字的潜在作用域从它的声明点开始,并在该块的末尾结束。实际作用域与潜在作用域相同,除非有内嵌块带有引入了相同名字的声明(这种情况下,从外层声明的作用域中排除掉嵌套声明的整个潜在作用域)。
int main() { int i = 0; // 外部 i 的作用域开始 ++i; // 外部 i 的作用域中 { int i = 1; // 内部 i 的作用域开始 // 外部 i 的作用域中断 i = 42; // 内部 i 的作用域中 } // 块结束,内部 i 的作用域结束, // 外部 i 的作用域恢复 } // 块结束,外部 i 的作用域结束 int j = i; // 错误:不在 i 的作用域中
在异常处理块中声明的名字的潜在作用域从它的声明点开始,并在该异常处理块的末尾结束,其他异常处理块和外围块都在作用域以外。
try { f(); } catch (const std::runtime_error& re) { // re 的作用域开始 int n = 1; // n 的作用域开始 std::cout << re.what(); // re 的作用域中 } // re 的作用域结束,n 的作用域结束 catch (std::exception& e) { // std::cout << re.what(); // 错误:不在 re 的作用域中 // ++n; // 错误:不在 n 的作用域中 }
以下名字的潜在作用域从它的声明点开始,并在相应控制语句的末尾结束。
|
(C++11 起) |
(C++17 起) |
Base* bp = new Derived; if (Derived* dp = dynamic_cast<Derived*>(bp)) { dp->f(); // dp 的作用域中 } // dp 的作用域结束 for (int n = 0; // n 的作用域开始 n < 10; // n 的作用域中 ++n) // n 的作用域中 { std::cout << n << ' '; // n 的作用域中 } // n 的作用域结束
函数形参作用域
函数形参(包括 lambda 表达式的形参)或函数局部预定义变量(例如 {[c (C++11 起))}}的潜在作用域从它的声明点开始。
- 如果最内层的外围函数声明符不是函数定义的声明符,那么它的潜在作用域在该函数声明符的末尾结束。
- 否则,它的潜在作用域在函数 try 块的最后异常处理块的末尾结束,或者在不使用函数 try 块时在函数体的末尾结束。
const int n = 3; int f1(int n, // 全局 n 的作用域中断 // 函数形参 n 的作用域开始 // , int y = n); // 错误:默认实参涉指了函数形参 int (*(*f2)(int n))[n]; // OK:函数形参 n 的作用域在它的函数声明符的末尾结束 // 在数组声明符中,全局 n 的作用域中 // 这声明了一个返回(含有 3 个 int 元素的数组的指针)的函数的指针 auto (*f3)(int n)->int (*)[n]; // 错误:以函数形参 n 作为数组边界 int f(int n = 2) // 函数形参 n 的作用域开始 try // 函数 try 块 { // 函数体开始 ++n; // 函数形参 n 的作用域中 { int n = 2; // 局部变量 n 的作用域开始 // 函数形参 n 的作用域中断 ++n; // 局部变量 n 的作用域中 } // 局部变量 n 的作用域结束 // 函数形参 n 的作用域恢复 } catch (std::exception& e) { ++n; // 函数形参 n 的作用域中 throw; } // 最后的异常处理块结束,函数形参 n 的作用域结束 int a = n; // 全局变量 n 的作用域中
命名空间作用域
命名空间中声明的任何实体的潜在作用域都从它的声明点开始,并由后续的同一命名空间名的所有命名空间定义拼合起来,再加上对于将这个名字或它所在的整个命名空间引入到其他作用域的每个 using 指令来说,包括这个作用域的剩余部分。
翻译单元的顶层作用域(“文件作用域”或“全局作用域”)也是命名空间,且被正式称作“全局命名空间作用域”。任何在全局命名空间作用域声明的实体的潜在作用域都从它的声明开始,并在翻译单元的末尾结束。
在无名命名空间或内联命名空间声明的实体的潜在作用域包括假设它在外围命名空间声明时具有的潜在作用域。
namespace N { // N 的作用域开始(作为全局命名空间的成员) int i; // i 的作用域开始 int g(int a) { return a; } // g 的作用域开始 int j(); // j 的作用域开始 void q(); // q 的作用域开始 namespace { int x; // x 的作用域开始 } // x 的作用域继续(无名命名空间的成员) inline namespace inl { // inl 的作用域开始 int y; // y 的作用域开始 } // y 的作用域继续(内联命名空间的结束) } // i、g、j、q、inl、x、y 的作用域中断 namespace { int l = 1; // l 的作用域开始 } // l 的作用域继续(无名命名空间的成员) namespace N { // i、g、j、q、inl、x、y 的作用域继续 int g(char a) // 重载 N::g(int) { return l + a; // 来自无名命名空间的 l 的作用域中 } // int i; // 错误:重复定义(已经在 i 的作用域中) int j(); // OK :允许重复的函数声明 int j() // OK :定义先前声明的 N::j() { return g(i); // 调用 N::g(int) } int q(); // 错误:已经在 q 的作用域中,并且返回类型不同 } // i、g、j、q、inl、x、y 的作用域中断 int main() { using namespace N; // i、g、j、q、inl、x、y 的作用域恢复 i = 1; // N::i 的作用域中 x = 1; // N::(匿名)::x 的作用域中 y = 1; // N::inl::y 的作用域中 inl::y = 2; // N::inl 的作用域中 } // i、g、j、q、inl、x、y 的作用域中断
这些名字也在导入了当前翻译单元的翻译单元中也允许可见。 |
(C++20 起) |
类作用域
类中声明的名字的潜在作用域它的声明点开始,并包含类体的剩余部分和所有函数体(无论是否在类定义外或在该名字的声明之前定义)、默认实参、异常规定、类内花括号或等号初始化器,还递归地包括嵌套类中的所有这些内容。
struct X { int f(int a = n) // 默认实参内,在 n 的作用域中 { return a * n; // 函数体内,在 n 的作用域中 } using r = int; r g(); int i = n * 2; // 初始化器内,在 n 的作用域中 // int x[n]; // 错误:类体内,不在 n 的作用域中 static const int n = 1; // n 的作用域开始 int x[n]; // OK:类体内,在 n 的作用域中 }; // n 的作用域中断 struct Y : X { // n 的作用域恢复 int y[n]; // n 的作用域中 }; // n 的作用域结束 //r X::g() // 错误:类外成员函数内,不在 r 的作用域中 auto X::g()->r // OK :尾随返回类型内,在 r 的作用域中 { return n; // 类外成员函数体内,在 n 的作用域中 }
如果一个名字在被声明前已经在类体中被使用,并且使用的地方在该名字的另一声明的作用域中,那么程序非良构,不要求诊断。
typedef int c; // ::c enum { i = 1 }; // ::i class X { // char v[i]; // 错误:此处 i 指代 ::i,但 X::i 也存在 int f() { return sizeof(c); // OK :成员函数体内,在 X::c 的作用域中 } enum { i = 2 }; // X::i char c; // X::c char w[i]; // OK:i 现在指代 X::i }; // 外部 i 的作用域恢复 typedef char* T; struct Y { // T a; // 错误:此处,T 指代 ::T,但 Y::T 也存在 typedef long T; T b; };
类成员的名字可以用于以下语境中:
- 在它所在的类作用域或在该类的派生类的类作用域之中
- 在对它所在的类或该类的派生类的类型的表达式运用
.
运算符之后 - 在对它所在的类或该类的派生类的指针类型的表达式运用
->
运算符之后 - 在对它所在的类或该类的派生类的名字运用
::
运算符之后
枚举作用域
无作用域枚举的枚举项的潜在作用域在它的声明点开始,并在外层作用域的末尾结束。
有作用域枚举的枚举项的潜在作用域在它的声明点开始,并在 enum 说明符的末尾结束。 |
(C++11 起) |
enum e1_t // 无作用域枚举 { A, B = A * 2 }; // A 与 B 的作用域继续 enum class e2_t // 有作用域枚举 { SA, SB = SA * 2 // SA 的作用域中 }; // SA 与 SB 的作用域结束 e1_t e1 = B; // OK :B 的作用域中 // e2_t e2 = SB; // 错误:不在 SB 的作用域中 e2_t e2 = e2_t::SB; // OK
模板形参作用域
模板形参名的潜在作用域在它的声明点开始,并持续到在其中引入了它的最小模板声明的末尾。具体而言,模板形参能用于后续的模板形参的声明及基类的指定,但不能用于之前的模板形参的声明。
template< typename T, // T 的作用域开始 T* p, // T 的作用域中 class U = T // T 的作用域中 > class X : public std::vector<T> // T 的作用域中 { T f(); // T 的作用域中 }; // T 与 U 的作用域结束
模板模板形参的形参名的潜在作用域,是该名字在其中出现的最小模板形参列表:
template< template< // 模板模板形参 typename Y, // Y 的作用域开始 typename G = Y // Y 的作用域中 > // Y 与 G 的作用域结束 class T, // typename U = Y // 错误:不在 Y 的作用域中 typename U > class X { }; // T 与 U 的作用域结束
与其他嵌套作用域类似,模板形参名在持续期间隐藏来自外层作用域的相同名字:
typedef int N; template< N X, // ::N 的作用域中 typename N, // N 的作用域开始,::N 的作用域中断 template<N Y> class T // N 的作用域中 > struct A; // N 的作用域结束
声明点
作用域从声明点 开始,它定位如下:
对于简单声明引入的变量和其他名字,声明点紧随该名字的声明符之后,且在它的初始化器之前(如果存在):
int x = 32; // 外部 x 的作用域中 { int x = x; // 内部 x 的作用域在初始化器(= x)前就已经开始 // 这导致内部 x 不以外部 x 的值(32)初始化, // 而是以自己持有的(不确定)值初始化 } std::function<int(int)> f = [&](int n){ return n > 1 ? n * f(n - 1) : n; }; // lambda 函数体在函数对象 f 的名字的作用域中 // 因此 f 能正确地被按引用捕获,给出递归函数
const int x = 2; // 外部 x 的作用域中 { int x[x] = {}; // 内部 x 的作用域在初始化器(= {})前,但在声明符(x[x])后开始。 // 声明符内仍在外部 x 的作用域中,这声明了一个含有 2 个 int 元素的数组。 }
类或类模板的声明点紧随它的类头中出现的命名该类的标识符(或命名该模板特化的 模板标识)之后。也就是说基类列表已经在该类名或类模板名的作用域中。
struct S: std::enable_shared_from_this<S> {}; // S 的作用域从冒号开始
enum 说明符或不可见枚举声明 (C++11 起)的声明点紧随命名它的标识符之后:
enum E : int // E 的作用域从冒号开始 { A = sizeof(E) };
类型别名或别名模板的声明点紧随该别名代表的类型标识之后: using T = int; // 外部 T 的作用域从分号开始 { using T = T*; // 内部 T 的作用域从分号开始, // 但分号前还在外部 T 的作用域中, // 因此等同于 T = int* } |
(C++11 起) |
不指名构造函数的using 声明内的声明符的声明点紧随该声明符之后: template<int N> class Base { protected:
static const int next = N + 1; static const int value = N;
};
struct Derived: Base<0>, Base<1>, Base<2> {
using Base<0>::next, // next 的作用域从逗号开始 Base<next>::value; // Derived::value 是 1
};
枚举项的声明点紧随它的定义之后(而不是在初始化器之前,这点与变量不同):
const int x = 12; { enum { x = x + 1, // 枚举项 x 的作用域从逗号开始, // 但逗号前还在外部 x 的作用域中, // x 初始化为 13 y = x + 1 // y 初始化为 14 }; }
注入类名 的声明点紧随它的类定义或类模板定义的左花括号之后:
template<typename T> struct Array // : std::enable_shared_from_this<Array> // 错误:不在注入类名的作用域中 : std::enable_shared_from_this< Array<T> > // OK :在模板名 Array 的作用域中 { // 现在在注入类名 Array 的作用域中,如同它是公开成员名 Array* p; // 指向 Array<T> 的指针 };
函数局部预定义变量 __func__ 的声明点紧接函数定义的函数体之前。 |
(C++11 起) |
结构化绑定的声明点紧随该结构化绑定声明的标识符列表之后,但结构化绑定的初始化器禁止提及其所引入的任何名字。 |
(C++17 起) |
声明于范围 for 语句的范围声明 的变量或结构化绑定 (C++17 起)的声明点紧随相应的范围表达式 之后: std::vector<int> x; for (auto x : x) // 右括号前仍然属于 vector x 的作用域, // 而 auto x 的作用域从右括号开始 { // auto x 的作用域中 } |
(C++11 起) |
模板形参的声明点紧随其完整模板形参(包括可选的默认实参)之后:
typedef unsigned char T; template< class T = T, // 模板形参 T 的作用域从逗号开始, // 但逗号前还在 unsigned char 的 typedef 名的作用域中, T, // 模板形参 T 的作用域中 N = 0 > struct A {};
本节未完成 原因:[basic.scope.pdecl] 的剩余内容 |
引用
- C++23 标准(ISO/IEC 14882:2023):
- 6.4 Scope [basic.scope]
- C++20 标准(ISO/IEC 14882:2020):
- 6.4 Scope [basic.scope]
- C++17 标准(ISO/IEC 14882:2017):
- 6.3 Scope [basic.scope]
- C++14 标准(ISO/IEC 14882:2014):
- 3.3 Scope [basic.scope]
- C++11 标准(ISO/IEC 14882:2011):
- 3.3 Scope [basic.scope]
- C++98 标准(ISO/IEC 14882:1998):
- 3.3 Declarative regions and scopes [basic.scope]