作用域

来自cppreference.com
< cpp‎ | language

C++ 程序中出现的每个名字,只在某些可能不连续的源码部分中有效,这些部分被称为其作用域

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

块作用域

块(复合语句)中的声明所引入的变量的潜在作用域,开始于其声明点并终止于该块末尾。实际作用域与潜在作用域相同,除非有内嵌块带有引入了相同名字的声明(这种情况下,从外层声明的作用域中排除掉嵌套声明的整个潜在作用域)。

int main()
{
    int a = 0; // 第一个 'a' 的作用域开始
    ++a; // 名字 'a' 在作用域中并指代第一个 'a'
    {
        int a = 1; // 第二个 'a' 的作用域开始
                   // 第一个 'a' 的作用域间断
        a = 42;    // 'a' 在作用域中并指代第二个 'a'
    } // 块结束,第二个 'a' 的作用域结束
      //       第一个 'a' 的作用域恢复
} // 块结束,第一个 'a' 的作用域结束
int b = a; // 错误:名字 'a' 不在作用域中

声明于异常处理块中的名字的潜在作用域开始于其声明点,并在该异常处理块结束时结束,而且在其他异常处理块或外围块中不在作用域中。

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 不在作用域中
}

for 循环初始化语句中,在 for 循环条件中,在范围 for 循环范围声明中,if 语句switch 语句初始化语句中, (C++17 起)if 语句while 循环switch 语句条件中,声明的名字的潜在作用域,开始于其声明点,并结束于控制语句的末尾。

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 表达式的形参)或函数局部预定义变量的潜在作用域开始于其声明点。

  • 若最内层的外围函数声明符不是函数定义的声明符,则其潜在作用域终止于该函数声明符的结尾。
  • 否则,其潜在作用域终止于函数 try 块的最后异常处理块的末尾,或若不使用函数 try 块则为函数体的末尾。
const int n = 3;
 
int f1(int n,     // 全局 'n' 的作用域间断
                  // 参数 'n' 的作用域开始
       int y = n); // 错误:默认实参涉指了形参
 
int (*(*f2)(int n))[n]; // OK :函数形参 'n' 的作用域终止于其函数声明符的末尾
                        // 数组声明符中,全局 n 在作用域中
// (这声明了返回 int 的 3 元素数组的指针的函数的指针)
 
// 相反
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(...) {
   ++n; // n 在作用域中并指代函数形参
   throw;
} // 最后异常处理块结束,函数形参 'n' 的作用域结束
int a = n; // OK :名称 'n' 在作用域中

函数作用域

声明于函数内的标号(且只有标号),在该函数和其所有内嵌代码块的任何位置都在作用域中,无论在其自身声明的前后。

void f()
{
   {   
       goto label; // label 在作用域中,尽管之后才声明
label:;
   }
   goto label; // label 忽略块作用域
}
 
void g()
{
    goto label; // 错误: g() 中 label 不在作用域中
}

命名空间作用域

命名空间中声明的任何实体的潜在作用域均开始于其声明,并由其后的同一命名空间名的所有命名空间定义拼合起来,再加上对于将这个名字或其整个命名空间引入到其他作用域的每个 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 的作用域间断

类作用域

中声明的名字的潜在作用域开始于其声明点,并包含类体的剩余部分和所有函数体(无论是否定义于类定义外或在该名字的声明之前)、默认实参、异常规定、类内花括号或等号初始化器,还(递归地)包括嵌套类中的所有这些内容。

class X {
    int f(int a = n) { // X::n 在默认实参中在作用域
         return a*n;   // X::n 在函数体内在作用域中
    }
    using r = int;
    r g();
    int i = n*2;   // X::n 在初始化器内在作用域中
 
//  int x[n];      // 错误: n 在类体内不在作用域中
    static const int n = 1;
    int x[n];      // OK : n 现在在类体内在作用域中
};
 
//r X::g() {       // 错误: r 在类外成员函数的作用域外
auto X::g()->r {   // OK :尾随返回类型 X::r 在作用域中
    return n;      // X::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 而非 ::c
    }
    char c; // X::c
    enum { i = 2 }; // X::i
};
 
typedef char* T;
struct Y {
    T a; // 错误:此处,T 指代 ::T ,但亦有 Y::T
    typedef long T;
    T b;
};

任何类成员名只能用于四种语境中:

  • 在其自身的类作用域或在派生类的类作用域之中
  • 在对其类或其派生类的类型的表达式运用 . 运算符之后
  • 在对其类或其派生的类的指针类型的表达式运用 -> 运算符之后
  • 在对其类或其派生类的名字运用 :: 运算符之后

枚举作用域

有作用域枚举中引入的枚举项的名字的作用域开始于其声明点,并终止于 enum 说明符的末尾(与之相反,无作用域枚举项的作用域在 enum 说明符的结尾后仍在作用域中):

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 Array<T> // T 能用于基类名
{
   // T 还能在体内使用
}; // T 与 U 的作用域结束, X 的作用域持续

模板模板形参的形参名的潜在作用域,是该名字出现于其中的最小模板形参列表

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, // int 类型的非类型模板形参
          typename N, // 此 N 的作用域开始,打断 ::N 的作用域
          template<N Y> class T // 此处的 N 是模板形参,非 int
         > struct A;

声明点

作用域始于声明点,它定位如下:

对于简单声明所引入的变量和其他名字,声明点紧随该名字的声明符之后,且在其初始化器之前(若其存在):

unsigned char x = 32; // 第一个 'x' 的作用域开始
{
    unsigned char x = x; // 第二个 'x' 的作用域在初始化器 (= x) 前开始
                         // 这不以值 32 初始化第二个 'x' ,
                         // 这以其自身的不确定值初始化第二个 'x'
}
std::function<int(int)> f = [&](int n){return n>1 ? n*f(n-1) : n;};
           // 函数对象名 'f' 在 lambda 中在作用域中,
           // 而且能正确地被按引用俘获,给出递归函数
const int x = 2; // 首个 'x' 的作用域开始
{
    int x[x] = {}; // 第二个 x 的作用域在初始化器 (= {}) 前开始,但在声明器 (x[x]) 后。
                   // 在声明器内,外层 'x' 仍在作用域中,这声明 2 个 int 的数组。
}

结构化绑定的声明点紧随该结构化绑定声明的 identifier-list(标识符列表)之后,但禁止结构化绑定的初始化器提及其所引入的任何名字。

(C++17 起)

类或模板的声明点,紧随其类头中所出现的命名类名的标识符(或指名模板特化的 template-id(模板标识))之后,并且在基类列表中已处于作用域中:

// 名称 'S' 在其出现后立即处于作用域中,
// 故它能用于基类列表
struct S: std::enable_shared_from_this<S> 
{
};

枚举的声明点,紧随 enum 说明符或笼统枚举声明(取决于何者为先)中所出现的命名它的标识符之后:

enum E : int { // E 已在作用域中
    A = sizeof(E)
};

类型别名或别名模板的声明点紧随该别名所代表的类型标识(type-id)之后:

using T = int; // T 的声明点在分号处
using T = T;   // 同 T = int

枚举项的声明点紧随其定义之后(而非在初始化器之前,这点不同于变量):

const int x = 12;
{
    enum {
        x = x + 1, // 声明点在逗号处,初始化 x 为 13
        y = x + 1  // 枚举项 x 在作用域中,初始化 y 为 14
    };
}

injected-class-name(注入类名)的声明点紧随其类(或类模板)定义的开花括号之后

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> 的指针
};

引用

  • C++11 standard (ISO/IEC 14882:2011):
  • 3.3 Scope [basic.scope]
  • C++98 standard (ISO/IEC 14882:1998):
  • 3.3 Declarative regions and scopes [basic.scope]

参阅