作用域

< 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 在作用域中
// (这声明了一个返回(含有 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(...) {
   ++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];      // 错误:类体内的 X::n 不在作用域中
    static const int n = 1;
    int x[n];      // OK : 类体内的 X::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)前开始
                         // 这导致第二个 'x' 不以值 32 初始化,
                         // 而是以其自身的不确定值初始化
}
std::function<int(int)> f = [&](int n){return n>1 ? n*f(n-1) : n;};
           // lambda 内的函数对象名 'f' 在作用域中,
           // 而且能正确地被按引用俘获,给出递归函数
const int x = 2; // 第一个 'x' 的作用域开始
{
    int x[x] = {}; // 第二个 x 的作用域在初始化器(= {})前,但在声明器(x[x])后开始。
                   // 在声明器内,外层 'x' 仍在作用域中,这声明了一个含有 2 个 int 元素的数组。
}

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

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

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

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

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

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

不指名构造函数的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
};

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

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) { // OK :第二个 x 指代 std::vector<int>
// 循环体内 x 指代循环变量
}
(C++11 起)

模板形参的声明点紧随其完整模板形参(包括可选的默认实参)之后:

typedef unsigned char T;
template<class T
  = T               // 查找找到 unsigned char 的 typedef 名
  , T               // 查找找到模板形参
    N = 0> struct A { };

引用

  • C++17 standard (ISO/IEC 14882:2017):
  • 6.3 Scope [basic.scope]
  • C++14 standard (ISO/IEC 14882:2014):
  • 3.3 Scope [basic.scope]
  • 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]

参阅