作用域

来自cppreference.com
< cpp‎ | language


 
 
C++ 语言
 
 

C++ 程序中出现的每个声明,只在某些可能不连续的作用域 中有效。

在作用域内,能用无限定名字查找将名字与它的声明关联起来。

概述

每个程序都有一个全局作用域,它包含 了整个程序。

其他每个作用域 S 会以以下之一引入:

S 会始终在其他作用域中出现,这些作用域从而包含 S

程序某一点的外围作用域 是任何包含该点的作用域;该点的立即作用域 是所有外围作用域中最小的作用域。

给定程序点 P 和作用域 S,如果一个作用域是 S 本身或包含 S,并且不包含 P,那么该作用域介入 PS 之间。

对于模板形参作用域以外的任何作用域 S,它的父作用域 是包含作用域 S 且不是模板形参作用域的最小作用域。

除非另外说明:

  • 声明在它的声明点居于 该点的立即作用域中。
  • 声明的目标作用域 是该声明居于的作用域。
  • 由声明(重新)引入的名字在目标作用域中绑定 到该声明。

如果一个实体的声明的目标作用域是 S,那么该实体属于 S

//                全局     作用域 作用域
//                作用域     S      T
int x;         //   ─┐                 // 程序点 X
               //    │
{              //    │      ─┐
    {          //    │       │     ─┐
        int y; //    │       │      │  // 程序点 Y
    }          //    │       │     ─┘
}              //   ─┘      ─┘

在以上程序中:

  • 全局作用域、作用域 S 和作用域 T 都包含程序点 Y
  • 也就是说,在程序点 Y 这三个作用域都是外围作用域。
  • 全局作用域包含作用域 S 和作用域 T,并且作用域 S 包含作用域 T
  • 因此,作用域 T 在三者中最小,这意味着:
  • 程序点 Y 处的立即作用域是作用域 T
  • 变量 y 的声明在它的声明点居于作用域 T
  • 作用域 Ty 的声明的目标作用域。
  • 变量 y 属于作用域 T
  • 作用域 S 是作用域 T 的父作用域,而全局作用域是作用域 S 的父作用域。
  • 作用域 S 介入程序点 X 和作用域 T 之间。

块作用域

每个

都会引入一个包含该语句或处理块的块作用域

属于块作用域的变量是块变量

int i = 42;
int a[10];
 
for (int i = 0; i < 10; i++) // 内部 “i” 居于 for 语句引入的块作用域
    a[i] = i;
 
int j = i; // j = 42

块作用域的 extern 声明会直接将名字绑定到声明的立即作用域中,但它的目标作用域是更大的外围作用域。

如果一个与名字无关 (C++26 起)声明的目标作用域是由以下复合语句或处理块之一引入的某个块作用域 S,并且该声明与名字绑定到 S 的父作用域的另一声明有潜在冲突,那么程序非良构:

(C++11 起)
  • 自身不是选择语句或循环语句,但是是选择语句或循环语句的子语句的复合语句。
  • 函数 try 块的处理块。
if (int x = f())  // 声明了 “x”
{ // 此 if 块是 if 语句的子语句
    int x;        // 错误:重声明了 “x”
}
else
{ // 此 else 块也是 if 语句的子语句
    int x;        // 错误:重声明了 “x”
}
 
void g(int i)
{
    extern int i; // 错误:重声明了 “i”
}

函数形参作用域

每个形参声明 P 都会引入一个包含 P函数形参作用域

  • 如果声明的形参所在的形参列表是函数声明的形参列表:
  • 如果该函数声明是函数定义,那么引入的作用域会延长到该函数定义的末尾。
  • 否则(该函数声明是函数原型),引入的作用域会延长到该函数声明的声明符的末尾。
  • 不管属于哪种情况,引入的作用域都不会包含该函数声明的声明点
  • 如果声明的形参所在的形参列表是 lambda 表达式的形参列表,那么引入的作用域会延长到 { 函数体 } 的末尾。
(C++11 起)
  • 如果声明的形参所在的形参列表是推导指引的形参列表,那么引入的作用域会延长到该推导指引的末尾。
(C++17 起)
  • 如果声明的形参所在的形参列表是 requires 表达式的形参列表,那么引入的作用域会延长到 { 要求序列 } 的末尾。
(C++20 起)
int f(int n) // 形参 “n” 的声明引入了一个函数形参作用域
{
    /* ... */
}            // 引入的函数形参作用域在此结束

lambda 作用域

每个 lambda 表达式都会引入一个 lambda 作用域,它在 [捕获 ] 之后立即开始,并延长到 { 函数体 } 的末尾。

某个 lambda 表达式 E 中带有初始化器的捕获居于由 E 引入的 lambda 作用域。

auto lambda = [x = 1, y]() // 此 lambda 表达式引入了一个 lambda 作用域,
{                          // 它是捕获 “x” 的目标作用域
    /* ... */
};                         // 引入的 lambda 作用域在分号前结束
(C++14 起)

命名空间作用域

命名空间 N 的每个命名空间定义都会引入一个命名空间作用域 S,它包含 N 的所有命名空间定义中的声明序列

对于目标作用域是 S 或者被 S 包含的另一作用域的每条非友元重声明或特化,作用域 S 也包含以下部分:

  • (模板)重声明或类模板特化中类头名 之后的部分。
  • 枚举重声明中枚举头名 之后的部分。
  • 其他重声明或中特化中声明符无限定标识 或有限定标识 之后的部分。

全局作用域全局命名空间引入的命名空间作用域。

namespace V   // “V” 的命名空间定义引入了一个命名空间作用域 “S”
{
    // 作用域 “S” 的第一部分在此开始
    void f();
    // 作用域 “S” 的第一部分在此结束
}
 
void V::f()   // “f” 之后的部分也是作用域 “S” 的一部分
{
    void h(); // 声明了 V::h
}             // 作用域 “S” 的第二部分在此结束

类作用域

类或类模板 C 的每个声明都会引入一个类作用域 S,它包含 C 的所有类定义中的成员说明

对于目标作用域是 S 或者被 S 包含的另一作用域的非友元重声明或特化,作用域 S 也包含以下部分:

  • (模板)重声明或类模板特化中类头名 之后的部分。
  • 枚举重声明中枚举头名 之后的部分。
  • 其他重声明或中特化中声明符无限定标识 或有限定标识 之后的部分。
class C       // “C” 的类定义引入了一个类作用域 “S”
{
    // 作用域 “S” 的第一部分在此开始
    void f();
    // 作用域 “S” 的第一部分在此结束
}
 
void C::f()   // “f” 之后的部分也是作用域 “S” 的一部分
{
    /* ... */
}             // 作用域 “S” 的第二部分在此结束

枚举作用域

枚举 E 的每个声明都会引入一个枚举作用域,它包含 E非笼统 (C++11 起)声明中的枚举项列表(如果存在)。

enum class E // “E” 的枚举定义引入了一个枚举作用域 “S”
{
    // 作用域 “S” 在此开始
    e1, e2, e3
    // 作用域 “S” 在此结束
}

模板形参作用域

每个模板模板形参都会引入一个模板形参作用域,它包含该模板模板形参的整个模板形参列表以及require 子句 (C++20 起)

每个模板声明 D 也都会引入一个模板形参作用域 S,它从 D 的模板形参列表开始,到 D 的末尾结束。在该模板形参列表之外且按之前的语法居于 S 的声明会改为居于 D 居于的作用域。

// “X” 的类模板声明引入了一个模板形参作用域 “S1”
template
<
    // 作用域 “S1” 在此开始
    template // 模板模板形参 “T” 引入了另一个模板形参作用域 “S2”
    <
        typename T1
        typename T2
    > requires std::convertible_from<T1, T2> // 作用域 “S2” 在此结束
    class T,
    typename U
>
class X; // 作用域 “S1” 在分号前结束

声明点

一般来说,一个名字从其首条声明的声明点 之后即可见,它定位如下:

对于简单声明所声明的名字,声明点紧随该名字的声明符之后,且在它的初始化器之前(如果存在):

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
{
};

概念定义的声明点紧随概念名之后,但概念定义中禁止提及所声明的概念名。

(C++20 起)

具名命名空间定义的声明点紧随命名空间名之后。

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 2793 C++98 块作用域中的 extern 声明可以与父作用域的另一声明冲突 已禁止

引用

  • C++23 标准(ISO/IEC 14882:2024):
  • 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]

参阅