首页 > 编程笔记 > C语言笔记 阅读:25

C语言作用域详解(3种作用域,图文并茂)

本节我们将深入探讨C语言中“作用域”的概念,帮助你理解变量在代码中的生命周期和可访问性。

下面的程序展示了一个关于标识符作用域的示例。
#include <stdio.h>

void func()
{
    int n;
    n = 100;
    printf("n in func %d\n", n);
}

int main()
{
    int n = 0;
    printf("n in main %d\n", n);
    func();
    printf("n in main %d\n", n);
    return 0;
}
在上述代码中,main() 函数有一个名为 n 的变量,其初始值为 0,在 func() 函数同样有一个名为 n 的变量,其被赋值 100。

运行结果为:

n in main 0
n in func 100
n in main 0

main() 函数中的变量 n 始终保持为 0,而 func() 函数中的变量 n 始终保持为 100。由此可见,func() 函数中的变量 n 与 main() 函数中的变量 n 并非同一个变量。

在 C语言中,作用域定义了标识符在程序中可被访问的范围。C 语言共有三种作用域类型:块作用域、函数作用域和文件作用域。

C语言块作用域

块作用域是指在代码块中定义的标识符的作用范围。这些标识符仅在它们所属的代码块内可见,而无法在代码块之外进行访问。

例如:
{
   //  代码块A
   xxxxxxxxxxx
}
{
   //  代码块B
   xxxxxxxxxx
}
在 C语言中,由花括号包围的代码组成一个代码块,如上例代码所示,分别形成了代码块 A 和代码块 B。

假设我们在代码块 A 中声明一个整型变量 int n = 3:
{
   //  代码块A
   xxxxxxxxxx
   int n = 3;
   xxxxxxxxxx
}
查看下图,标识符 n 具有一定的使用范围:


图 1 作用域的范围

当变量声明在代码块内时,其使用范围从变量声明开始,一直到包含该声明的代码块结束。这段使用范围称为标识符 n 的块作用域。标识符 n 在这个作用域之外是不可访问的。例如,在代码块 A 中声明的变量 n 在代码块 B 中是不可访问的。

实际上,我们常见的 if 语句、for 循环等都属于块作用域。具体代码如下:
#include <stdio.h>

int main() {
    int a = 10;
    if (a > 5) {
        int b = 20;
        printf("b is %d\n", b);
    }
    printf("a is %d\n", a);
    return 0;
}
在此示例中,变量 b 的作用域仅限于 if 语句内部,因此该变量在 if 语句之外是不可访问的。

1) 同级代码块

现在我们来研究两个同级关系的代码块的情况。具体代码如下:
#include <stdio.h>

int main()
{
    {
        int n;
        printf("&n=%11u\n", &n);
    }
    {
        float n;
        printf("&n=%11u\n", &n);
    }
    return 0;
}
程序的两个代码块都定义了一个名为 n 的变量。运行结果为:

&n=    6487580
&n=    6487576

我们输出两个变量 n 的地址,发现它们的地址并不相同。这表明它们并不指代同一个内存空间。

查看下图,在代码块 A 中,标识符 n 具有作用域。一旦代码块 A 结束,标识符 n 就失去其作用域。因此,代码块 B 可以重新使用标识符 n。


图 2 同名标识符地址不一致

若我们删除代码块 B 中变量 n 的定义,如下图所示,那么程序将无法编译,因为编译器无法识别代码块 B 中的标识符 n。


图 3 离开作用域后,标识符失效

2) 嵌套代码块

现在,我们调整代码,让代码块 A 包含代码块 B,使两个代码块形成嵌套关系。此外,代码块 B 中没有标识符 n 的声明。具体代码如下:
#include <stdio.h>

int main()
{
    // 代码块 A
    {
        int n;
        printf("&n=%11u\n", &n);
        // 代码块 B
       {
           printf("&n=%11u\n", &n);
       }
       return 0;
    }
}
代码块 A 中定义了整型变量 n,而代码块 B 中没有任何定义,但是代码块 B 可以使用标识符 n。运行结果为:

&n=    6487580
&n=    6487580

它们指代的都是同一个数据对象。这是为什么呢?

首先,我们来确定标识符 n 的作用域范围,它的作用域从变量声明开始,即代码块 A 中的第一条语句,一直到包含声明的块结束,即代码块 A 结束。代码块 B 恰好位于变量 n 的作用域范围内。因此,代码块 B 也能使用变量 n。

然而,如果将代码块 B 移动到变量 n 声明之前,如下图所示:


图 4 代码块在作用域外

那么代码块 B 将不再位于变量 n 的作用域范围内。这样,代码块 B 就无法使用变量 n 了。

3) 内层覆盖外层标识符

现在我们在代码块 B 中也定义一个标识符 n,参阅下面的实例:
#include <stdio.h>
int main()
{
    // 代码块 A
    {
        int n;
        printf("&n=%llu\n", &n);
        // 代码块 B
        {
            int n;
            printf("&n=%llu\n", &n);
        }
    }
    return 0;
}
我们知道,在同一个代码块内定义相同的标识符会出现标识符重定义错误。然而,在上面的实例中,代码块 A 内定义了一个标识符后,在代码块 A 内的代码块 B 中定义了一个同名标识符,这种情况却可以通过编译。这是因为两个标识符都具有自己的作用域,并且两个作用域为嵌套关系,如下图所示。


图 5 嵌套代码块

程序的运行结果为:

&n=6487580
&n=6487576

从结果来看,这两个同名标识符指代的并不是同一个数据对象。

那么,我们如何确定标识符 n 应该指代哪个声明的数据对象呢?这其实很简单,内层作用域将覆盖外层作用域。

查看下图,第一个 printf() 使用的标识符 n 指代在代码块 A 开头声明的变量 n,而第二个 printf() 使用的标识符 n 指代在代码块 B 开头声明的变量 n。


图 6 指代关系1

在代码块 B 后再加一个 printf(),如下图所示:


图 7 指代关系2

由于代码块 B 中的标识符 n 作用域已结束,覆盖效果将消除。那么,它使用的标识符 n 将指代代码块 A 开头声明的变量 n。

C语言函数作用域

函数作用域是指在函数中定义的标识符的作用范围。这些标识符只在该函数内部可见,超出该函数就无法访问。

例如:
#include <stdio.h>
void func()
{
    int n;
    n = 100;
    printf("n in func %d\n", n);
}
int main()
{
    int n = 0;
    printf("n in main %d\n", n);
    func();
    printf("n in main %d\n", n);
    return 0;
}
查看下图:


图 8 函数中的同名标识符

两个函数的花括号之间没有嵌套关系,所以这是两个同级的代码块。两个代码块中声明的变量的作用域互不重叠。main() 函数中使用标识符 n 仅指代 main() 函数中声明的变量 n。同理,func() 函数中使用标识符 n 仅指代 func() 函数中声明的变量 n。

函数可以形成带花括号的块作用域,而参数列表中声明的标识符,作用范围为整个函数,例如:
void func(int p)
{
    printf("p = %d\n", p);
}
在上面的代码中,参数列表中声明了变量 p,那么变量 p 的作用域在函数花括号内。在整个函数花括号的范围内均可以使用这个变量 p。

需要注意的是,函数作用域中的变量生命周期也是有限的,它们在函数调用结束时被销毁。因此,在函数作用域中定义的变量也应该尽量避免在函数调用结束后继续使用。

C语言文件作用域

在 C语言中,文件作用域是指在一个源文件中定义的标识符的作用范围。这种作用域的标识符可以在整个文件中访问。文件作用域中定义的变量被称为全局变量,因为它们可以在整个文件中访问。

下面是一个使用文件作用域的示例:
#include <stdio.h>
int n = 0;
void func()
{
    printf("&n= %llu\n", &n);
}
int main()
{
    printf("&n= %llu\n", &n);
    func();
    printf("&n= %llu\n", &n);
    return 0;
}

在这个例子中,变量 n 是在花括号外声明的,那么它的作用范围是整个文件。运行结果为:

&n= 4223024
&n= 4223024
&n= 4223024

可以看出,函数 func() 与函数 main() 中的标识符 n 指代的是同一个数据对象。

查看下图,在代码块外声明的标识符,它的作用范围从声明开始,直到该源文件结束。这种作用域被称作文件作用域。


图 9 文件作用域

查看下图,如果在函数 func() 中也声明了同名标识符 n,则和之前的处理原则一样,下级作用域将覆盖上级作用域:


图 10 下级作用域将覆盖上级作用域

函数 func() 中的标识符 n 仅指代函数 func() 中声明的数据对象 n。函数 main() 中的标识符 n 仅指代文件作用域中声明的数据对象 n。

相关文章