V2.0
王道C++班级参考资料<br />——<br />C语言部分卷3函数<br/>节2常见变量分类<br/><br/>最新版本V2.0
<br>王道C++团队<br/>COPYRIGHT ⓒ 2021-2024. 王道版权所有概述局部变量定义位置存储位置以及生命周期(重要)初始化作用域总结全局变量定义位置存储位置以及生命周期初始化作用域全局变量的使用建议static修饰全局变量静态局部变量定义位置存储位置及生命周期初始化(重点)作用域静态局部变量的使用场景总结补充生命周期和作用域静态存储期限变量的初始化方式The End
Gn!
学习到这里,也是时候学习总结一下C语言中几种常见的变量类型了。
根据变量的定义位置、存储位置、存储期限(生命周期)、作用域等的不同,C语言中的变量主要有以下几种类型:
局部变量 (Local Variable)
全局变量 (Global Variable)
静态局部变量 (Static Local Variable)
在这些变量当中,最常见常用的当属局部变量,但剩下几种变量类型也需要我们掌握。
Gn!
在C语言中,局部变量就是在函数当中定义的变量,它最主要的特点就是只在声明它的"{}"内部有效。
Gn!
C语言中的局部变量总是定义在函数体当中的,包括:
函数体当中直接声明的变量。
函数的形式参数。
在函数体内部的某个块"{}",如函数体当中if语句或for循环中定义的变量。
Gn!
操作系统为每个执行的C程序(进程)分配虚拟内存空间,局部变量存储在一片被称之为"栈(stack)"的内存区域。
栈的工作原理是后进先出,这意味着最后放入栈的项是第一个被移除的。
在C程序的运行过程中,每当一个函数被调用,系统会为其创建一个“栈帧”来存储该函数的执行上下文,并将这个栈帧压入栈顶,这个过程称为函数进栈(push)。一个函数被调用,就是函数栈帧进栈的的过程。
栈帧中会存储此函数的局部变量(包括形式参数)。
当函数开始执行,对应的栈帧被压入栈顶时,局部变量得以初始化并生效。随着函数执行完毕,栈帧从栈顶中弹出,此时,函数内的局部变量也随之被销毁,这个过程称为函数出栈(pop)。
所以局部变量的生命周期与包含它们的函数的生命周期是一致的:
当函数被调用时,其局部变量被创建;
当函数返回时,这些变量被销毁。
在C语言当中,这种"依托于变量存储空间单元存在而存在"的变量生命周期形式被称为"自动存储期限"。局部变量的自动存储期限,依赖于函数调用栈。
如下列代码案例:
局部变量与栈的工作原理 - 示例分析:
2512
3void test(void);
4void test2(void);
5
6int main(void) {
7printf("main before\n");
8int a = 10;
9test();
10printf("mian after\n");
11return 0;
12}
13
14void test(void) {
15printf("test before\n");
16int a = 20;
17test2();
18printf("test after\n");
19}
20
21void test2(void) {
22printf("test2 before\n");
23int a = 30;
24printf("test2 after\n");
25}
函数调用的过程是:
main函数先调用,栈帧先进栈
main函数中调用test函数,test函数栈帧进栈
test函数中调用test2函数,test2函数栈帧进栈
也就是说,函数调用的顺序(也就是函数栈帧进栈的顺序)是:
main ---> test ---> test2
但函数调用结束的顺序却恰好相反,依赖于栈的先进后出的特点,函数调用结束的顺序(也就是函数栈帧出栈的顺序)是:
test2---> test ---> main
越晚调用的函数,越早调用结束。最先调用的函数,最晚结束。事实上,main函数最先调用,它是程序的入口,main函数执行完毕意味着程序就结束,所以它也确实最晚结束。
以上过程的示意图如下(栈区从高地址向低地址生长):
三个函数栈帧都有一个局部变量a,但很明显它们之间是相互独立的。理解这一点,对于理解局部变量非常有帮助。
Gn!
C语言的局部变量没有自动的初始化机制,如果一个局部变量仅有声明没有手动初始化(赋值),那么此局部变量的初始值是未定义的,它的值可以是任意的、随机的"垃圾值"。
使用未初始化的局部变量可能导致程序行为的不确定性和不可预测性。因此,为了保证程序的稳定性和预测性,最佳实践是始终在使用变量之前对其进行初始化。
例如:
局部变量初始化-演示代码
xxxxxxxxxx
1int main(void){
2int num; // 此num的取值是不确定的垃圾值。
3int num2 = 100; // 此num2的取值确定为100。
4}
切记使用局部变量,要进行手动初始化!
Gn!
变量的作用域,指的是该变量可以被访问和修改的代码范围。在C语言中,局部变量的作用域:
开始:局部变量的作用域从其声明的位置开始。
结束:局部变量的作用域到该变量所在的块或函数结束为止。通俗的说,就是到定义此局部变量的大括号结束。比如:
如果一个局部变量在函数体中被声明,那么它的作用域从声明的位置开始,直到函数结束为止。
如果局部变量在一个块(如if语句、for循环、while循环、switch语句等)内部被声明,它的作用域只在该块及嵌套在此块内的其他块中。
这里有一个小的细节,C语言的代码块"{}"具有遮蔽外层"{}"作用域的功能。比如:
81void foo(void){
2int a = 10;
3if(1){
4// int a = 20; // 这行代码在C语言中是允许的,if的{}具有遮蔽外出foo函数体{}作用域的功能
5printf("%d", a); // 如果不放开上面的注释a = 10,放开后a = 20,且两个a不是一回事
6}
7// foo中定义的a在这里依然有效,但如果是if中的a这里即失效
8}
这也是C语言语法比较奇特的地方,
{}
复合语句具有自身新的、独立的作用域,又和外层共享作用域。请看以下代码示例:
局部变量作用域-演示代码
x12
3void foo() {
4int a = 10; // a的作用域从这里开始
5
6if (a == 10) {
7// int a = 20; // 再次声明a允许的,此时if的{}会屏蔽外层的a
8int b = 20; // b的作用域从这里开始
9
10// 在这里,外层的a和b都可以被访问。但如果内层自己声明了a,那么将访问自身的a
11printf("%d, %d\n", a, b);
12
13} // b的作用域在这里结束
14
15int b = 20; // 再次声明b也是可以的,因为if中的b已经结束作用域了
16
17// 在这里只有a可以被访问
18printf("%d\n", a);
19
20} // a的作用域在这里结束
21
22int main() {
23foo();
24// 在这里,既不能访问a也不能访问b
25return 0;
26}
总之,局部变量的作用域就限定在声明它的"{}"内部,具体而言:
从变量的声明点开始,直到所在代码块"{}"结束,该局部变量都是可访问的。
在此作用域内,该局部变量是唯一的,不可被重复定义。但如果作用域内又定义了一个新的复合语句
{}
,则会出现作用域屏蔽的情况。
Gn!
在 C 语言中,局部变量是在函数内部定义的变量,它只在该函数的作用域内可见和可用。使用局部变量时,总结如下:
在C语言中,局部变量就是声明定义在函数体内部的变量。
局部变量不会自动初始化。如果一个局部变量仅有声明,那么它们的初始值是未定义的,通常称为“垃圾值,随机值”。因此,局部变量在使用之前必须手动初始化它。
局部变量的生命周期是自动存储期限,意味着它们仅在声明它们的函数调用期间存活,函数返回后它们就随栈帧销毁。
局部变量的作用域就是仅限于"{}"内部,实际使用时要注意"{}"的起止。
"{}"嵌套后,内层"{}"会共享外层大括号中的变量。但也可以屏蔽外层大括号作用域,以定义自身独立局部变量。
Gn!
在C语言中,全局变量也是一种特别常见的变量类型,有时它也被称为外部变量。
这是因为它们在函数之外被定义,并且可以在整个文件内,甚至其他文件中(通过外部链接)被访问和使用。
Gn!
只要在一个文件内,所有函数的外部,直接声明定义的变量都是全局变量。比如:
定义全局变量-演示代码
1612
3// 全局变量的定义
4int global_var = 100;
5
6void foo() {
7// ...
8}
9
10int main() {
11// ....
12return 0;
13}
14
15// 全局变量的定义
16int global_var2 = 200;
Gn!
全局变量被存储在虚拟内存空间当中,一片被称之为"数据段"的内存区域当中。
不同于局部变量随着函数的调用和返回被创建和销毁,全局变量的生命周期从程序开始执行时开始,持续到程序完全结束。
简而言之,全局变量与程序的生命周期同步:它们在程序开始时被创建并初始化,并在程序结束时被销毁。
在C语言中,这种持续存在于程序整个执行周期的生命周期特性被称为“静态存储期限”。
Gn!
当C程序开始执行时,会在程序加载时为数据段中的变量进行初始化,包括全局变量。此时:
如果全局变量已由程序员明确初始化赋值,该初始值会直接在程序启动时分配给它。
若程序员未进行显式初始化赋值,系统则会为其设置默认值(也就是0值),如:整型和浮点型变量默认值为0,指针变量默认值为NULL。
注意:
除非不确定此全局变量的初始值,否则建议对其进行手动初始化。
全局变量的初始化有且仅有程序启动时的一次。
Gn!
全局变量的作用域从"声明位置"开始,并延申至整个程序。具体来说:
在定义全局变量的文件内,全局变量可以在其声明之后的任何位置被访问和修改。
要想在其他源文件中使用该全局变量,可以通过extern关键字来引用它。
例如,在一个main.c文件中定义全局变量:
全局变量定义-演示代码
812
3int global_num = 10; // 从此处开始,整个文件都可以访问和修改global_num
4
5int main(void) {
6printf("在main.c文件中,打印全局变量的值为%d\n", global_num);
7return 0;
8}
如果想在demo.c文件中使用此全局变量,使用extern关键字,演示代码如下:
extern关键字引用全局变量-演示代码
812// 使用extern声明来表明global_num是在其他文件中定义的
3// 从这里开始该文件中也可以使用变量global_num了
4extern int global_num;
5
6void print_global(void) {
7printf("在demo.c文件中,打印全局变量的值为%d\n", global_num);
8}
为了在main.c文件中调用print_global函数,需要借助头文件进行函数声明,最终代码如下:
全局变量作用-演示代码3
xxxxxxxxxx
1// main.c
234
5int global_num = 10;
6
7int main(void) {
8printf("在main.c文件中,打印全局变量的值为%d\n", global_num);
9print_global();
10
11printf("在main.c中将全局变量的取值改为100\n");
12global_num = 100;
13print_global();
14return 0;
15}
16
17// demo.c
181920
21extern int global_num;
22
23void print_global(void) {
24printf("在demo.c文件中,打印全局变量的值为%d\n", global_num);
25}
26
27// demo.h
28void print_global(void); // 声明函数,供其他文件使用
运行此程序,结果是:
在main.c文件中,打印全局变量的值为10
在demo.c文件中,打印全局变量的值为10
在main.c中将全局变量的取值改为100
在demo.c文件中,打印全局变量的值为100
总结:
global_num变量是在main.c文件中定义的,它在本文件中声明位置以下的部分,都可以随意使用。
在demo.c文件中,使用extern关键字只是告诉编译器global_num变量在其余文件中定义,它并不会创建了一个新的变量。
关于跨文件调用函数,一般步骤如下:
在一个头文件中声明你想跨文件调用的函数。
在一个源文件中包含头文件,然后定义这个函数。(包含自定义头文件,使用#include "xx.h")
在另一个源文件中,包含头文件并直接调用该函数。
填坑:只属于变量声明但不属于变量定义的语句
我们再来回顾一下C语言中关于变量声明和定义的概念:
变量的声明:声明是给编译器看的,告诉编译器变量的类型和名字等信息,但变量的具体内存分配发生在运行时期。
变量的定义是声明一个变量,并且为运行时期为变量分配内存空间的组合动作。
变量的定义总是一个声明,但某些变量的声明并不是定义,也就是说某些变量的声明不会在运行时期分配内存空间。
在以往,我们见到的所有声明语句都是变量的定义,也就是在运行时期会为变量分配内存。那么在这里,我们就见到了仅声明,不分配内存空间的变量声明语句:
11extern int global_num;
这条语句就仅是一条声明语句,不是一条变量的定义语句。
global_num变量的内存分配并不是在这一行代码进行的,而是在真正定义全局变量global_num的位置进行的。
在本案例中,main.c文件中的:
11int global_num = 10;
这是一条变量定义的语句,会在程序运行时期分配内存。
Gn!
虽然全局变量的使用,为C语言提供了许多灵活性并为一些问题提供了解决方案,但使用全局变量也带来了一系列问题,包括:
可读性和代码维护上的困难。由于全局变量可以在任何地方被修改,这可能会使代码的流程变得难以理解,从而降低代码的可读性和可维护性。
风险增加。由于全局变量可以在文件或模块之间随意访问和修改,这增加了不小心破坏数据或引入错误的风险。
命名污染。使用大量的全局变量可能导致命名空间的污染,从而增加命名冲突的风险。
调试困难。若一个变量跨多文件使用,这无疑给程序的调试带来极大的麻烦。
内存管理风险。全局变量在程序的整个生命周期中都存在,这可能导致不必要的内存占用,尤其是在资源受限的环境中。
...
总之,对于全局变量的使用,我们给出以下建议:
程序应当尽可能的不使用全局变量。比如多个函数要共享数据,可以优先考虑用参数传递的方式共享,而不是全局变量。
如果非要使用全局变量,尽量限制其不能跨文件使用,即使用static修饰的全局变量。
如果非要使用全局变量,那么一定要给出一个清晰的命名约定。比如约定以"global_"开头的都是全局变量。
在代码的文档或注释中清晰地描述全局变量的用途、范围和正确使用方式。
在适当的场景下,全局变量是有其合理用途的,不必完全回避,但应当谨慎使用。
Gn!
static修饰的全局变量的主要效果是将其作用域限制在声明它的文件中,这意味着该变量只能在它所在的源文件中被访问和修改。其它的特性,如生命周期、初始化方式和存储位置,与普通的全局变量是相同的。
比如上面的global_num全局变量,一旦使用static关键字修饰,再次运行程序,就会产生链接错误。因为此时的全局变量已经不能被链接到外部使用了。
Gn!
静态局部变量,简单来说,就是static修饰的局部变量。但它与一般的局部变量在很多地方都是完全不同的,下面我们来详细学习一下静态局部变量。
Gn!
静态局部变量就是在原本局部变量定义的基础上,使用static关键字声明得到的变量。比如:
定义静态局部变量-演示代码
xxxxxxxxxx
31void foo() {
2static int static_var = 0; // 定义了一个静态局部变量
3}
Gn!
静态局部变量的存储位置和生命周期和全局变量是一致的:
都是存储在数据段区域当中。
生命周期都是从程序启动到程序结束,都是"静态存储期限"。
这就意味着,静态局部变量与一般的局部变量不同:
静态局部变量不会随着函数的返回而销毁,它会始终保留到程序执行完毕。
Gn!
不要将静态局部变量理解成"活得更长"的局部变量,局部变量和静态局部变量在初始化方面有非常大的区别。
静态局部变量的初始化特性:
默认初始化:如果不显式地为静态局部变量提供一个初始值,系统会默认将其初始化为0值。
初始化只有一次:静态局部变量只会在其所在函数,第一次被调用时初始化一次。此后,不论调用几次该函数,都不会再次初始化了。
下面我们举一个代码案例,来讲解这两个特点。
静态局部变量初始化-代码示例
x12
3void call_counter() {
4static int count = 0; // 静态局部变量,仅在首次调用时初始化为0,再次调用不会再次初始化
5count++; // 每次调用函数时,增加count计数器
6printf("此函数已经被调用了%d次!\n", count);
7}
8
9int main(void) {
10for (int i = 0; i < 5; i++) {
11call_counter();
12}
13return 0;
14}
程序的输出结果是:
此函数已经被调用了1次!
此函数已经被调用了2次!
此函数已经被调用了3次!
此函数已经被调用了4次!
此函数已经被调用了5次!
Gn!
静态局部变量的作用域仅限于其所在的函数。这意味着:
尽管静态局部变量的生命周期与程序的整个执行期间一致,但它只能在定义它的函数内部被访问。
Gn!
静态局部变量,可以在同一个函数的多次调用之间进行数据保存和修改,这非常有用。也就是说,如果一个数据需要被一个函数的多次调用进行操作,那么就非常适合使用静态局部变量。
一个非常经典的例子就是"生成独立序号":
定义一个函数,每次调用该函数都生成一个独立的序号。
参考代码如下:
静态局部变量生成独立序号-代码示例
111int get_id() {
2static int current_id = 1; // 初始ID为1
3return current_id++; // 返回当前ID,然后进行累加
4}
5
6int main(void) {
7printf("New ID: %d\n", get_id()); // 输出: New ID: 1
8printf("New ID: %d\n", get_id()); // 输出: New ID: 2
9printf("New ID: %d\n", get_id()); // 输出: New ID: 3
10return 0;
11}
这个代码虽然很简单,但在许多实际应用中都会用到类似的机制来生成唯一独立序号。
Gn!
用一张表格来总结我们上面讲的所有知识点,请大家务必理解、牢记。
C语言常见变量分类-总结表格
特性 局部变量 全局变量 静态局部变量 static修饰的全局变量 定义位置 函数内部 函数外部 函数内部 函数外部 存储位置 栈 数据段 数据段 数据段 存储期限(生命周期) 从函数调用开始到函数退出为止(自动存储期限) 程序启动到程序结束(静态存储期限) 程序启动到程序结束(静态存储期限) 程序启动到程序结束(静态存储期限) 作用域 仅限于所在函数 整个程序的所有文件,但在其它文件使用需要用extern声明 仅限于所在函数 仅限于定义它的文件 初始化时机 每次函数调用时都初始化,不同函数调用初始化不同 程序启动时初始化,只初始化一次。 函数第一次调用时进行初始化,只初始化一次 程序启动时初始化,只初始化一次。 默认初始化 不手动初始化,可能得到一个随机值 即便不手动初始化,也会默认初始化为0值 即便不手动初始化,也会默认初始化为0值 即便不手动初始化,也会默认初始化为0值 是否可以被其他文件访问 否 在其它文件中使用extern关键字链接访问 否 否
Gn!
下列两点,起到总结和补充的作用,大家可以看一下,记一下。
Gn!
作用域与生命周期是两个基本但重要的编程概念。虽然它们都关联到变量,但它们描述的是不同的特性。
作用域
定义: 描述变量可被访问和修改的代码范围。
特性: 作用域是编译时概念。如果在变量的非作用域内尝试访问它,编译器会报错。
种类:
局部作用域: 变量只在其定义的函数或块中有效。
文件作用域: 适用于static修饰的全局变量,其作用范围限制在定义它的文件中。
程序作用域: 全局变量可在整个程序中访问,包括其他源文件(需要使用extern关键字)。
生命周期
定义: 描述变量在内存中存在的时间段。
特性: 生命周期是运行时概念,因为只有当程序运行时,变量才会被存储在内存中。
种类:
自动存储期限: 如局部变量,其生命周期与其所在的函数调用栈相绑定。
静态存储期限: 如全局变量和静态局部变量,从程序开始到程序结束。
局部变量的作用域和生命周期确实存在重叠:它们的作用域和生命周期都局限于所在的函数。(这也是有些同学会搞混淆这两个概念的原因)
但对于全局变量、静态局部变量、和static修饰的全局变量,它们的生命周期是整个程序的运行期间,而作用域则依赖于定义和访问的位置。
具体的细节,大家可以查看上面的表格。
Gn!
静态存储期限的变量包括:
全局变量
静态局部变量
static修饰的全局变量
这些变量即使不手动初始化,也会自动得到一个默认的0值。但如果你想显式地为这些变量指定一个初始值,那么初始化操作,赋值运算符的右侧必须是一个常量表达式。
比如下列代码示例:
静态存储期限变量显式初始化需要使用常量表达式-代码示例
xxxxxxxxxx
1int get_value() {
2return 5;
3}
4
5void func() {
6int a = 10;
7static int num = get_value(); // 这是错误的
8static int num = a; // 这是错误的
9static int num = 100; // 这是正确的
10static int num = 100 / 20; // 这是正确的
11}
这些变量,之所以有同样的初始化特点是由于它们都存储在数据段这一内存区域。
在程序启动的过程中,数据段内的变量就需要进行初始化。此时,由于运行时上下文还未完全建立,用依赖于运行时的变量表达式、函数调用等作为初始值是不可行的,也是不可能的。
因此,使用常量表达式作为这些变量的明确初始值,也就成了必然。
小tips:
我们上面讲:”静态局部变量是在函数第一次调用时被初始化“。
这种说法实际上是不准确的,只是为了方便大家理解采取的一个折衷的说法。
实际上:
静态局部变量也是在程序启动时就进行了初始化,也就是说在函数调用之前,静态局部变量的初始值就已经确定了。
但静态局部变量的作用域始终局限于函数内,如果不调用函数必然无法访问它,所以粗略的认为”静态局部变量是在函数第一次调用时被初始化“也问题不大。