1. 变量作用域

C 语言中,作用域(Scope)是指程序中变量、函数的可见性和生命周期的范围。作用域规定了在程序中的哪些地方可以访问变量、函数。

C 语言中主要有以下几种作用域:

  1. 文件作用域(File Scope):函数之外定义的变量具有文件作用域,也称为全局作用域。在文件的任何地方都可以访问这些变量,直到文件结束。

  2. 块作用域(Block Scope):在函数中、循环或复合语句块内部定义的变量具有块作用域,也称为局部作用域。只能在定义它们的块({}之间的一段代码)内部访问。

  3. 函数原型作用域(Function Prototype Scope):函数原型中声明的形式参数具有函数原型作用域。它们只在函数原型内部可见,用于声明函数的参数类型

  4. 函数作用域(Function Scope):在早期版本的 C 语言中(如 C89),函数内部定义的变量具有函数作用域。它们只能在定义它们的函数内部访问。但是在较新的 C 语言标准中,函数内部定义的变量具有块作用域。

变量的作用域可以通过声明位置和跨越的区域来确定。一般来说,变量的作用域从声明点开始,直到其所在的块(或函数)的结尾。在嵌套的块中,如果内部块中定义了与外部块中同名的变量,则内部块中的变量会遮盖(隐藏)外部块中的变量。

1.1 局部变量

局部变量(Local Variable)是在函数内部或代码块内部(复合语句块)定义的变量,在其定义的函数或代码块范围内可见和有效。它有如下特点:

  • 在一个函数内定义,只在函数范围内有效
  • 在复合语句({})中定义,只在复合语句中有效
  • 随着函数调用的结束或复合语句的结束局部变量的生命周期也结束
  • 如果没有赋初值,局部变量的默认值是随机的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

void test()
{
int b = 10;
}

int main(void)
{
//b = 100; // error, 在main作用域中没有b
int a = 99;
if (1)
{
// 局部变量 a 只在当前 {} 中有效
int a = 10;
printf("a = %d\n", a);
}
printf("a = %d\n", a);

return 0;
}

如果在嵌套的代码块中定义了与外部代码块相同名称的局部变量,内部代码块中的变量会覆盖(隐藏)外部代码块中同名的变量。在内部代码块内,访问该变量时将使用内部代码块中定义的变量。所以上面代码中:

  • 第16行打印的结果应该是a = 10
  • 第18行打印的结果应该是a = 99

局部变量的作用在于:

  • 提供了一个容器来存储临时数据,只在需要时才存在,可以避免命名冲突和数据混乱的可能性。
  • 避免了全局变量的污染,使得不同函数或代码块之间可以独立操作,提高代码的可读性和可维护性。
  • 有效地利用了内存资源,当局部变量不再使用时,相应的内存可以被释放,避免了不必要的内存占用。

1.2 静态局部变量

静态局部变量(Static Local Variable)是在函数内部声明的局部变量,其生命周期延长到整个程序运行期间,但作用域仍然局限于所在的函数内部。

如果想要把变量标记为静态的,定义变量的时候需要添加static关键字。

1
2
int number;          // 普通变量
static int number; // 静态变量

静态局部变量有如下特点:

  • 作用域限制:静态局部变量仍然具有函数作用域,只能在其所在函数内部访问。在函数外部不可见。
  • 初始化一次:静态局部变量在程序运行期间只会被初始化一次。在第一次进入函数时,会执行一次初始化操作,并将初始化的值保留下来,之后每次函数调用都会使用上一次保留的值。
  • 自动初始化:静态局部变量若未赋以初值,则由系统自动赋值:
    • 数值型变量自动赋初值0
    • 字符型变量赋空字符
  • 生命周期延长:与普通的局部变量不同,静态局部变量在函数执行结束后不会被销毁。它的生命周期从第一次定义时开始,直到整个程序运行结束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>

void func1()
{
int i = 0;
i++;
printf("i = %d\n", i);
}

void func2()
{
// 静态局部变量,没有赋值,系统赋值为0,而且只会初始化一次
static int a;
a++;
printf("a = %d\n", a);
}

int main(void)
{
func1();
func1();
func2();
func2();

return 0;
}

程序输出的结果如下:

1
2
3
4
i = 1
i = 1
a = 1
a = 2
  • func1函数内部i是局部变量
    • 每次调用该函数,都会通过第5行代码定义新的局部变量i
    • 函数调用完毕,i就被释放了
  • func2函数内部a是静态局部变量
    • 第1次调用该函数,会通过第13行代码定义新的静态局部变量a
    • 从第2次调用开始,静态局部变量a已经存在,不会进行再次定义
    • 函数调用完毕,a没有被释放,其对应的存储空间依然有效

静态局部变量常用于需要在函数调用之间保持数据持久性和状态的情况,例如计数器、缓存等。

1.3 全局变量

全局变量(Global Variables)是在函数外部定义的变量,在整个程序中都可见和有效。全局变量具有全局作用域,可以被程序中的所有函数访问。

全局变量的特点包括:

  1. 全局作用域:全局变量在声明之后,从声明点开始,到整个程序结束都是可见的。它可以在程序的任何地方被访问和使用。

  2. 静态存储期:全局变量在程序运行期间始终存在内存中,不像局部变量在函数调用结束后销毁。全局变量的内存分配和释放是由系统负责的。

  3. 默认初始化:全局变量会默认被初始化为0(数值类型)或者NULL(指针类型)。如果显式地给全局变量赋予了初值,则会使用该初值进行初始化。

  4. 命名空间:全局变量在整个程序中都有唯一的名称,可以在不同的函数中使用相同的变量名,但是会引起命名冲突和可读性问题,应谨慎使用。

全局变量的使用需要注意一些问题:

  • 全局变量可以在各个函数中访问和修改,容易造成对全局状态的混乱,建议合理使用全局变量,避免过度依赖和滥用。
  • 如果在多个文件中使用同名的全局变量,会引发多个定义的冲突问题,可以使用 extern 关键字进行声明而不是重新定义,以避免链接错误。

以下是一个示例,展示了全局变量的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

// 全局变量
int globalVar = 10;

void modifyGlobalVariable()
{
globalVar += 5;
}

void printGlobalVariable()
{
printf("全局变量 globalVar 的值:%d\n", globalVar);
}

int main()
{
printGlobalVariable();
modifyGlobalVariable();
printGlobalVariable();

return 0;
}

在上述示例中,定义了一个全局变量 globalVar,并初始化为 10。在 modifyGlobalVariable 函数中修改了全局变量 globalVar 的值,使其增加 5。在 printGlobalVariable 函数中,我们输出了全局变量 globalVar 的值。

运行程序后的输出结果如下所示:

1
2
全局变量 globalVar 的值:10
全局变量 globalVar 的值:15

注意事项:

  • 全局变量在程序中具有全局作用域,可以在程序的任何地方被访问和修改。但是过度使用全局变量可能导致代码的可读性和可维护性下降。
  • 同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用。

因此,应该慎重选择使用全局变量,并遵循良好的编程实践。

1.4 静态全局变量

静态全局变量(Static Global Variables)是在代码文件(源文件)的顶层定义的全局变量,并且使用 static 关键字修饰。静态全局变量只在当前代码文件中可见,对于其他代码文件是不可访问的

静态全局变量的特点包括:

  1. 文件作用域:静态全局变量的作用域仅限于定义它的源文件内部,不会影响到其他源文件(不同源文件中的静态全局变量可以重名)。它在定义之后,从定义点到整个源文件结束的范围内都是可见的。

  2. 静态存储期:静态全局变量在程序运行期间始终存在内存中,类似于普通的全局变量。但是,它只能在定义它的源文件中访问和修改。

  3. 默认初始化:静态全局变量和普通全局变量一样,默认会被初始化为零值(数值类型)或者 NULL(指针类型),除非显式地给它们赋予初值。

静态全局变量适用于在当前源文件内共享数据,但希望限制其它源文件的访问权限的情况。它们可以避免全局命名空间的干扰,提高代码的可维护性,避免名称冲突。

以下是一个示例,展示了静态全局变量的用法:

文件 a.c

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

// 静态全局变量
static int staticGlobalVar = 10;
void modifyStaticGlobalVariable()
{
staticGlobalVar += 5;
}

void printStaticGlobalVariable()
{
printf("静态全局变量 staticGlobalVar 的值:%d\n", staticGlobalVar);
}

文件 b.c

1
2
3
4
5
6
7
8
9
#include <stdio.h>

// 静态全局变量在其他文件中不可访问
extern int staticGlobalVar;

void printStaticGlobalVariableFromB()
{
printf("从文件B中访问静态全局变量 staticGlobalVar 的值:%d\n", staticGlobalVar);
}

主程序文件 main.c

1
2
3
4
5
6
7
8
#include <stdio.h>

void printStaticGlobalVariableFromB(); // 函数声明
int main()
{
printStaticGlobalVariableFromB();
return 0;
}

在上述示例代码中:

  • 文件 a.c 中定义了一个静态全局变量 staticGlobalVar,初始值为 10
  • 文件b.c 中,使用 extern 关键字声明了静态全局变量,试图对其进行访问。
  • 文件 main.c 中调用了来自 b.c 中的函数,以间接访问静态全局变量。

编译程序会提示如下错误信息:

1
error LNK2001: 无法解析的外部符号 _staticGlobalVar

需要注意的是,静态全局变量只能在定义它的源文件中访问,对于其他文件是不可知和不可访问的。即便使用 extern 关键字进行声明,也无法实现静态全局变量的跨文件访问。

2. 函数作用域

2.1 全局函数

在 C 语言中,全局函数(Global Functions)是在全局作用域(函数外部)定义的函数,可以在程序的任何地方被调用。

全局函数的特点包括:

  1. 全局作用域:全局函数在声明之后,从声明点开始,到整个程序结束都是可见的。它可以在程序的任何地方被调用和执行。

  2. 函数声明:如果需要在函数定义之前调用全局函数,需要进行函数声明。也就是在调用位置之前使用函数原型或函数声明,以告诉编译器该函数的名称、参数和返回值类型。

  3. 分离编译:全局函数可以被多个源文件共享并调用,从而实现代码的分离编译。可以将函数的声明放在头文件中,在多个源文件中通过包含头文件来使用全局函数。

  4. 代码重用:全局函数的定义和实现可以在整个程序中重用,避免了重复编写相同的代码。

以下是一个示例,展示了全局函数的用法:

头文件 example.h:

1
2
3
4
5
6
7
#ifndef EXAMPLE_H
#define EXAMPLE_H

// 函数声明
void globalFunction();

#endif

源文件 example.c:

1
2
3
4
5
6
7
8
#include <stdio.h>
#include "example.h"

// 函数定义
void globalFunction()
{
printf("这是一个全局函数\n");
}

主程序文件 main.c:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include "example.h"

int main()
{
// 调用全局函数
globalFunction();
return 0;
}

在上述示例中,我们创建了一个名为 example.h 的头文件,用于存放函数声明。源文件 example.c 中定义了全局函数 globalFunction,在里面输出一条信息。主程序文件 main.c 包含了头文件 example.h 并调用了全局函数 globalFunction

运行程序后的输出结果如下所示:

1
这是一个全局函数

需要注意的是,全局函数可以在程序的任何地方被调用,但是应该根据需要进行函数声明或包含头文件,以确保函数定义可见。全局函数可以跨多个源文件共享和重用代码,有利于模块化开发,并提高程序的可维护性和可读性。

2.2 静态函数

在 C 语言中,静态函数(Static Functions)是使用 static 关键字修饰的函数,它们具有以下特点:

  1. 隐藏性:静态函数只在所在源文件中可见,无法被其他源文件调用。使用 static 关键字修饰函数后,该函数的作用域被限制在当前源文件中。
  2. 冲突避免:定义一个静态函数与其他源文件中的同名函数不会产生冲突。每个源文件的静态函数都是独立的,即使它们的函数名相同。
  3. 链接优化:编译器可以对静态函数进行链接优化,消除未使用的静态函数,减小可执行程序的大小。

以下是一个示例,展示了静态函数的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

// 静态函数定义
static void staticFunction()
{
printf("这是一个静态函数\n");
}

// 全局函数定义
void globalFunction()
{
printf("这是一个全局函数\n");
}

int main()
{
// 函数调用
globalFunction();
staticFunction();

return 0;
}

在上述示例中,我们定义了一个静态函数 staticFunction 和一个全局函数 globalFunction。函数被声明为静态后只在当前源文件中可见,不会干扰其他源文件。全局函数可以被其他源文件调用。

使用静态函数可以隐藏实现细节,并避免函数名冲突。它们被广泛用于模块化设计和代码组织,并能提高程序的可维护性和可读性。