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

变量隐藏
在程序中,使用大括号会形成一个名称限定域,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;
}

函数参数可见性
函数参数属于整个函数体,函数体自身又是一个域,因此该变量在函数体中不同的域都可以被访问到,同样它也遵循变量隐藏机制。
#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;
}

程序使用#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;
}

第一次接触全局变量概念时,你可能会认为变量定义的位置不会影响它的使用范围,毕竟是顶着全局的名号。由程序中globalVar2可以看到事实并非如此,虽然它是全局的,但在变量定义的位置点之前的函数依旧看不到这个名称,因此依旧无法使用它。
练习
1 编写函数测试变量的作用域
2 定义几个不同类型的全局变量但不初始化,观察它们与局部变量的区别。