张银峰的编程课堂

名称可见

函数名的可见性

在编译期间,函数在被调用时其名称必须是可见的,即被调用的函数签名是要存在的;在连接时,函数必须定义完整的,只有这样才可以生成最终的可执行程序。

#include <stdio.h>

// #1 直接定义函数
void printHello()
{
    printf("Hello\n");
}

// #2 函数声明
void printOk(int times);

// 主函数
int main()
{
    // 调用printHello函数,由于#1的存在,函数名称在此是可见的。
    printHello();

    // 调用printOk函数,由于#2函数声明的存在,函数名称是可见的。
    // 注释掉#2的声明后,程序将出错。
    printOk(3);

    return 0;
}

// #3 函数实现
void printOk(int times)
{
    for (int i = 0; i < times; ++i)
        printf("Ok\n");
}

在编译阶段,编译器从源代码第一行开始向后处理。当在主程序中调用printfHello时,由于函数体已经由#1处定义,对于main函数而言,该名称printfHello是可见的,因此通过编译。printOk也能顺利编译,因为有函数声明在先,在链接时,链接器会找到printOk的实现,因此整个程序顺利编译连接完成。

你可以尝试注释掉printOk函数的实现,再点击【生成】菜单下的【编译】命令,程序会编译成功。再点击【生成】菜单下的【生成 你的项目名称】命令,在输出窗口会看到如下的连接错误信息:error LNK2019: 无法解析的外部符号 _printOk,该符号在函数 _main 中被引用。 这说明没有找到printfOK函数的定义。

变量的可见性

一个变量的名称在一定范围内必须是唯一的,这个范围可以称为限定域,一对大括号可以形成一个域。函数体自身由大括号包围,这表明函数体自身就是一个限定域,函数体内声名的变量属于该函数体,称之为局部变量,不同的函数体声明的同名变量互不影响。由下面的程序可以看出,变量名称a在不同的域中被多次使用,正是因为它们处于不同的域,所以互不影响,不会产生错误。

#include <stdio.h>

void test1()
{
    // #1: 名称a限定于函数test1
    int a = 1;
    //float a = 4.0f; // 在同一域不能存在同名变量,即使类型也不同。
    printf("test1: a=%d\n", a);
}

void test2()
{
    // #2: 名称a限定于函数test2
    int a = 2;
    printf("test2: a=%d\n", a);

    // 使用大括号构成一个域
    {
        // #3: 名称a限定于函数test2{}
        int a = 3;
        printf("test2{}: a=%d\n", a);
    }
}

int main()
{
    // #4: 名称a限定于函数main
    int a = 0;
    printf("main: a=%d\n", a);

    test1();
    test2();

    return 0;
}

glimix.com

变量隐藏

ZYF

在程序中,使用大括号会形成一个名称限定域,if/for/while语句的复合语句也是如此。如果一个名称限定域包含多个子域,如域A包含域B,假设被包含的域B声明的变量与外围域A某变量名称相同,表示外围域的变量名被隐藏,此变量将不可访问,其它未隐藏的变量在当前域仍可访问。

#include <stdio.h>

void test()
{
    int a = 2;
    int b = 3;
    printf("a: %d\n", a);     // 使用函数域内变量a
    printf("b: %d\n", b);     // 使用函数域内变量b

    if (1)
    {
        float a = 4.2;        // 隐藏外围域变量a
        printf("a: %f\n", a); // 使用if域内变量a
        printf("b: %d\n", b); // 使用外围域变量b;
    }
}

int main()
{
    test();
    return 0;
}

glimix.com

函数参数可见性

函数参数属于整个函数体,函数体自身又是一个域,因此该变量在函数体中不同的域都可以被访问到,同样它也遵循变量隐藏机制。

#include <stdio.h>

void printOk(int times)
{
    // 同一域内不能有两个名称相同的变量,即使类型也不同。
    // float times = 250;

    printf("#1 times: %d\n", times);

    // 用大括号形成限定域
    {
        printf("#2 times: %d\n", times);

        // 用if语句构成限定域
        if (1)
        {
            // 定义新变量times,传入参数(函数形参)在此区域被隐藏,不可访问。
            int times = 4;
            printf("#3 times: %d\n", times);
        }
    }

    printf("#1 times: %d\n", times);
}

int main()
{
    printOk(3);
    return 0;
}

glimix.com

程序使用#num的形式标注了函数体内存在的域。在if语句中使用了自身域内新定义的变量times,其它域则使用了传入的变量times,该变量属于整个函数体。

全局变量

变量也可以定义在函数之外,与函数内部的局部变量相对,这称为全局变量,即在整个程序中都可以访问到的变量。当然,在一个c文件中,全局变量也遵循名称可见性原则:要使用这个名称,它必须在之前被声明或定义过。

#include <stdio.h>

int globalVar1 = 1;  // 全局变量1

void funA()
{
    printf("function A\n");
    printf("globalVar1: %d\n", globalVar1);     // 使用全局变量globalVar1
    //printf("globalVar2: %d\n", globalVar2);   // 错误:这里看不到globalVar2这个名称
    printf("\n");
}

int globalVar2 = 2; // 全局变量2

void funB()
{
    printf("function B\n");
    printf("globalVar1: %d\n", globalVar1);     // 使用全局变量globalVar1
    printf("globalVar2: %d\n", globalVar2);     // 使用全局变量globalVar2
    printf("\n");
}

int main()
{
    funA();
    funB();

    int globalVar1 = 9;                     // 隐藏全局变量globalVar1(不恰当的变量名)
    printf("main\n");
    printf("globalVar1: %d\n", globalVar1); // 使用局部变量globalVar1
    printf("globalVar2: %d\n", globalVar2); // 使用全局变量globalVar2

    return 0;
}

glimix.com

第一次接触全局变量概念时,你可能会认为变量定义的位置不会影响它的使用范围,毕竟是顶着全局的名号。由程序中globalVar2可以看到事实并非如此,虽然它是全局的,但在变量定义的位置点之前的函数依旧看不到这个名称,因此依旧无法使用它。

ZYF

练习

1 编写函数测试变量的作用域

2 定义几个不同类型的全局变量但不初始化,观察它们与局部变量的区别。