Namespaces
Variants

Scope

From cppreference.net

每个在C程序中出现的 标识符 仅在其 作用域 (即可被使用的源代码中可能不连续的部分)内是 可见的

在同一作用域内,一个标识符仅当对应实体处于不同 命名空间 时才能指代多个实体。

C语言有四种作用域类型:

  • 块作用域
  • 文件作用域
  • 函数作用域
  • 函数原型作用域

目录

嵌套作用域

如果两个由相同标识符命名的不同实体同时处于作用域内,且它们属于同一个 命名空间 ,则作用域呈嵌套关系(不允许其他形式的作用域重叠),此时出现在内部作用域的声明会隐藏外部作用域的声明:

// 此处的命名空间为普通标识符
int a;   // 名称a的文件作用域从此处开始
void f(void)
{
    int a = 1; // 名称a的块作用域从此处开始;隐藏了文件作用域的a
    {
      int a = 2;         // 内部a的作用域从此开始,外部a被隐藏
      printf("%d\n", a); // 内部a在作用域内,输出2
    }                    // 内部a的块作用域在此结束
    printf("%d\n", a);   // 外部a在作用域内,输出1
}                        // 外部a的作用域在此结束
void g(int a);   // 名称a具有函数原型作用域;隐藏了文件作用域的a

块作用域

任何在 复合语句 (包括函数体)内部声明的标识符作用域, 或出现在 if switch for while do-while 语句中的任何表达式、声明或语句 (C99起) ,或在 函数定义 参数列表内声明的标识符作用域,从声明点开始,到其声明所在的块或语句末尾结束。

void f(int n)  // 函数参数 'n' 的作用域开始
{         // 函数体开始
   ++n;   // 'n' 在作用域内并指向函数参数
// int n = 2; // 错误:无法在同一作用域内重新声明标识符
   for(int n = 0; n<10; ++n) { // 循环局部变量 'n' 的作用域开始
       printf("%d\n", n); // 输出 0 1 2 3 4 5 6 7 8 9
   } // 循环局部变量 'n' 的作用域结束
     // 函数参数 'n' 重新回到作用域
   printf("%d\n", n); // 输出参数的值
} // 函数参数 'n' 的作用域结束
int a = n; // 错误:名称 'n' 不在作用域内

在C99标准之前,选择语句和迭代语句不会建立自身的块作用域(尽管若语句中使用了复合语句,该复合语句仍具有其常规的块作用域):

enum {a, b};
int different(void)
{
    if (sizeof(enum {b, a}) != sizeof(int))
        return a; // a == 1
    return b; // b == 0 in C89, b == 1 in C99
}
(since C99)

块作用域变量默认具有 无链接 自动存储期 。请注意,非可变长度数组局部变量的存储期从进入代码块时开始,但在声明语句之前,该变量不在作用域内且无法被访问。

文件作用域

在任何块或参数列表外部声明的标识符,其作用域从声明点开始,持续到翻译单元结束。

int i; // i的作用域开始
static int g(int a) { return a; } // g的作用域开始(注意:"a"具有块作用域)
int main(void)
{
    i = g(2); // i和g在作用域内
}

文件作用域标识符默认具有 外部链接 静态存储期

函数作用域

在函数内部声明的 标签(且仅限标签) 在该函数的整个作用域内均有效,包括所有嵌套代码块、声明位置之前及之后。注意:标签通过在任何语句前的冒号前使用未被占用的标识符来隐式声明。

void f()
{
   {   
       goto label; // 即使标签在后面声明,仍在作用域内
label:;
   }
   goto label; // 标签不受块作用域限制
}
void g()
{
    goto label; // 错误:标签不在g()函数的作用域内
}

函数原型作用域

在非定义的 函数声明 参数列表中引入的名称,其作用域结束于函数 声明符 的末尾。

int f(int n,
      int a[n]); // n 在作用域内并指向第一个参数

请注意,如果声明中存在多个或嵌套的声明符,其作用域将在最近的封闭函数声明符处结束:

void f ( // 函数名 'f' 处于文件作用域
 long double f,            // 标识符 'f' 此时进入作用域,文件作用域的 'f' 被隐藏
 char (**a)[10 * sizeof f] // 'f' 指向当前作用域内的第一个参数
);
enum{ n = 3 };
int (*(*g)(int n))[n]; // 函数参数 'n' 的作用域
                       // 在其函数声明符末尾结束
                       // 在数组声明符中,全局 n 处于作用域内
// (此处声明了指向函数的指针,该函数返回指向含3个int元素的数组的指针)

声明点

结构体、联合体和枚举标签的作用域始于声明该标签的类型说明符中出现标签之后的位置。

struct Node {
   struct Node* next; // Node 在作用域内并指向此结构体
};

枚举常量的作用域从其定义枚举项在枚举列表中出现的下一位置立即开始。

enum { x = 12 };
{
    enum { x = x + 1, // 新的 x 在逗号之前不在作用域内,x 被初始化为 13
           y = x + 1  // 新的枚举成员 x 现在在作用域内,y 被初始化为 14
         };
}

任何其他标识符的作用域始于其声明符结束之后,并在初始化器(如果存在)之前:

int x = 2; // 第一个 'x' 的作用域开始
{
    int x[x]; // 新声明 x 的作用域在声明符 (x[x]) 之后开始
              // 在声明符内部,外层的 'x' 仍在作用域内
              // 这里声明了一个包含 2 个整数的 VLA 数组
}
unsigned char x = 32; // 外层 'x' 的作用域开始
{
    unsigned char x = x;
            // 内层 'x' 的作用域在初始化式 (= x) 之前开始
            // 这不会用值 32 初始化内层 'x',
            // 而是用其自身不确定的值初始化内层 'x'
}
unsigned long factorial(unsigned long n)
// 声明符结束,'factorial' 从此时起进入作用域
{
   return n<2 ? 1 : n*factorial(n-1); // 递归调用
}

作为特例,未声明标识符的 类型名称 作用域被认为始于类型名称中原本应出现标识符的位置之后。

注释

在C89标准之前,具有外部链接的标识符即使在块作用域内声明也具备文件作用域,正因如此,C89编译器不要求诊断已超出作用域的外部标识符使用(此类使用属于未定义行为)。

在C语言中,循环体内的局部变量可以隐藏 for 循环初始化子句中声明的变量(它们的作用域是嵌套的),但在C++中无法实现这样的隐藏。

与 C++ 不同,C 语言没有结构体作用域:在 struct/union/enum 声明内部声明的名称与结构体声明处于同一作用域(但数据成员位于其自身的 成员命名空间 中):

struct foo {
    struct baz {};
    enum color {RED, BLUE};
};
struct baz b; // baz 在作用域内
enum color x = RED; // color 和 RED 在作用域内

参考文献

  • C23 标准 (ISO/IEC 9899:2024):
  • 6.2.1 标识符、类型名和复合字面量的作用域 (p: 待定)
  • C17 标准 (ISO/IEC 9899:2018):
  • 6.2.1 标识符的作用域 (p: 28-29)
  • C11 标准 (ISO/IEC 9899:2011):
  • 6.2.1 标识符的作用域 (p: 35-36)
  • C99 标准 (ISO/IEC 9899:1999):
  • 6.2.1 标识符的作用域 (p: 29-30)
  • C89/C90 标准 (ISO/IEC 9899:1990):
  • 3.1.2.1 标识符的作用域

参考

C++ 文档 关于 作用域