目录
1 栈与全局静态区域
1.1 栈区域
1.1.1 定义与功能
栈是一种遵循后进先出(LIFO, Last In First Out)原则的数据结构,在程序执行中扮演着核心角色。
它主要用于存储局部变量、局部数组、局部常量、函数参数(形参)以及函数调用的返回地址等。在程序执行过程中,每当函数调用发生时,其上下文(包括参数、局部变量及返回地址)会被压入栈中,形成函数调用栈帧。函数执行完毕后,其对应的栈帧会从栈中弹出,从而恢复调用前的状态,确保程序能够正确地返回到调用点继续执行。
1.1.2 特点
动态分配:栈的大小在程序运行时动态变化,随着函数的调用和返回而增长和缩小。
自动管理:栈的分配和释放由编译器自动管理,程序员无需手动干预。
空间限制:栈的大小通常有限制(例如,在大多数操作系统中,默认栈大小可能为 1MB 或更小),过大的栈使用可能导致栈溢出错误。
访问速度快:由于栈数据在内存中连续存储,因此访问速度较快。
初始化特性:在栈空间中定义的数据(局部变量等)如果没有显式地进行初始化,那么这些数据将不会有默认值,而是会包含所谓的 “垃圾值” 或 “未定义值”。这些值实际上是之前存储在相同内存位置的数据的残留。
生命周期:局部数据的生命周期受限于其定义的函数或代码块的执行周期。它们仅在函数执行或代码块激活期间存在,一旦函数返回或调用结束或块级作用域结束,这些局部变量的内存就会被自动释放,以便栈空间可以被重用。
内存管理:每当函数被调用或代码块被激活时,系统都会在栈上为局部变量分配空间。函数执行完毕或代码块结束时,局部变量所占用的内存会被自动回收,以便下一次函数调用时可以再次利用这部分栈空间来存储新的局部变量。
应用场景:
栈常用于实现递归函数、系统调用、中断处理、异常处理等功能。
1.2 全局静态区域
1.2.1 定义与功能
全局静态区也称为静态存储区或全局数据区,作为程序内存布局的关键部分,在程序编译时就已经确定了其所需的空间大小,并在程序启动时由操作系统根据可执行文件中的信息一次性分配内存。内存管理相对简洁高效,仅在程序结束时统一释放。它主要存储全局变量、全局数组、全局常量、静态局部变量和静态全局变量等。全局静态区进一步细分为数据段和 BSS 段,前者存储已初始化的数据,后者则用于未初始化的数据。
1.2.2 特点
静态分配:全局静态区在程序编译时就已经确定了其所需的空间大小,并在程序启动时由操作系统根据可执行文件中的信息一次性分配好这部分内存。这意味着全局静态区的内存分配是静态的,与程序的动态执行过程相对独立。即全局变量和静态变量在程序执行前就已获得固定的内存地址。
持久性:存储在全局静态区的变量具有持久性,它们在程序的整个运行期间都存在,不会被自动销毁。
共享性:全局变量在全局静态区中定义后,可以在程序的多个部分之间共享。这意味着任何函数或模块都可以访问和修改这些全局变量的值(除非有特定的访问控制机制,如 static 关键字限制静态全局变量仅在当前文件内可见)。然而,静态局部变量虽然也存储在全局静态区中,但它们的可见性和作用域被限制在定义它们的函数内部。
初始化特性:全局静态区的变量在程序启动时会被初始化。对于已初始化的变量,它们会按照程序中的初始值进行设置;对于未初始化的变量,编译器会默认将它们初始化为零(整型变量为 0,浮点型为 0.000000,字符型为 '\0',指针类型为 NULL)。
生命周期:全局静态区中的数据的生命周期与程序的执行周期相同。它们从程序启动时开始存在,直到程序正常结束或操作系统回收程序资源时才会被销毁。这种长期存在的特性使得全局数据能够在程序的多个执行阶段中保持其值不变(除非被显式修改)。
内存管理:与局部变量相比,全局静态区域的数据管理更为简洁高效。局部变量通常需要在函数调用时动态分配内存,并在函数返回时释放,这一过程增加了内存管理的复杂性和开销。而全局静态区域的数据则在整个程序运行期间持续存在,仅在程序正常结束或操作系统回收资源时统一释放,大大减少了内存管理的负担。
潜在风险:全局静态区的长期存在和共享性也带来了一定的潜在风险。例如,如果程序中的多个部分都修改了同一个全局变量的值,就可能导致数据不一致的问题。此外,全局变量的过度使用还可能导致代码的可读性和可维护性降低。因此,在编程时应谨慎使用全局变量,并尽量通过函数参数、返回值或静态局部变量等方式来传递数据。
1.3 两区域对比总结
栈区域和全局静态区域是程序内存布局中的两个重要部分,它们在生命周期、内存管理、初始化特性等方面存在显著差异。以下是对这两个区域的详细对比总结:
栈区域 | 全局静态区域 | |
---|---|---|
定义与功能 | 栈是一种遵循后进先出(LIFO)原则的数据结构。 在程序执行过程中,每当函数调用发生时,其上下文(包括参数、局部变量及返回地址)会被压入栈中,形成函数调用栈帧。函数执行完毕后,其对应的栈帧会从栈中弹出,从而恢复调用前的状态,确保程序能够正确地返回到调用点继续执行。 | 全局静态区也称为静态存储区或全局数据区。在程序编译时就已经确定了其所需的空间大小,并在程序启动时由操作系统根据可执行文件中的信息一次性分配内存。内存管理相对简洁高效,仅在程序结束时统一释放。 全局静态区进一步细分为数据段和 BSS 段,前者存储已初始化的数据,后者则用于未初始化的数据。 |
存储数据类型 | 主要用于存储局部变量、局部数组、局部常量、函数参数(形参)以及函数调用的返回地址等。 | 主要用于存储全局变量、全局数组、全局常量、静态局部变量和静态全局变量等。 |
生命周期 | 局部数据的生命周期受限于其定义的函数或代码块的执行周期。 它们仅在函数执行或代码块激活期间存在,一旦函数返回或调用结束或块级作用域结束,这些局部变量的内存就会被自动释放。 | 全局静态区的变量的生命周期与程序的执行周期相同。 它们从程序启动时开始存在,直到程序正常结束或操作系统回收程序资源时才会被销毁。 |
内存管理 | 动态分配和释放,由编译器自动管理。 每当函数被调用或代码块被激活时,系统都会在栈上为局部变量分配空间。函数执行完毕或代码块结束时,局部变量所占用的内存会被自动回收。 | 静态分配,编译时就已经确定了其所需的空间大小。 全局静态区域的数据则在整个程序运行期间持续存在,仅在程序正常结束或操作系统回收资源时统一释放。 |
初始化特性 | 如果没有显式初始化,栈上的局部变量将包含未定义值(垃圾值)。 | 全局静态区的变量在程序启动时会被初始化。已初始化的变量按指定值设置,未初始化的变量默认为零(整型为 0,浮点型为 0.0,字符型为 '\0',指针类型为 NULL)。 |
访问速度 | 由于栈数据在内存中连续存储,访问速度较快。 | 访问速度适中,因为全局静态区域的数据可能不连续存储,但通常访问速度不是性能瓶颈。 |
空间限制 | 栈的大小通常有限制(如 1MB 或更小),过大的栈使用可能导致栈溢出错误。 | 全局静态区域的大小理论上受限于物理内存大小,但在实际应用中,过大的全局变量使用可能会增加程序的内存占用,影响性能和可移植性。 |
潜在风险 | 栈溢出错误是栈使用中的主要风险,可能由于递归过深或大量局部变量分配导致。此外,未初始化的局部变量可能导致未定义行为。 | 全局变量的过度使用可能导致数据不一致、降低代码的可读性和可维护性。全局变量还可能引起命名冲突和难以追踪的副作用。 |
应用场景 | 栈常用于实现递归函数、系统调用、中断处理、异常处理等功能。 | 全局静态区域适合存储需要在程序多个部分之间共享的数据,如配置信息、状态变量等。也常用于存储需要持久保存的数据。 |
通过对比可以看出,栈区域和全局静态区域在程序设计中扮演着不同的角色,各有其独特的优势和适用场景。理解它们的特性和用途对于编写高效、可维护的程序至关重要。
1.4 全局变量与局部变量的生命周期及内存管理分析
#include <stdio.h>
// 定义全局数组
int arr[5] = {10, 20, 30, 40, 50};
// 定义全局变量,数组长度
int len = 5;
// 定义函数
void fn(int num)
{
// 定义局部变量
int a = 250;
printf("%d \n", num + a); // 输出 num 与 a 的和
}
// 主函数
int main()
{
// 调用函数
fn(20); // 输出 20 + 250
// 遍历数组
for (int i = 0; i < len; i++) // 使用全局变量 len 作为数组长度
{
printf("%d ", arr[i]); // 输出数组 arr 的每个元素
}
printf("\n");
// 再次调用函数
fn(60); // 输出 60 + 250
return 0;
}
输出结果如下所示:
内存分析:
1. 全局静态区在程序编译时就已经确定了其所需的空间大小,并在程序启动时由操作系统根据可执行文件中的信息一次性分配好这部分内存,全局数据在整个程序执行期间都是可访问的。如下所示:
2. 当函数 fn 被调用时,栈区会为 fn 中的局部变量分配空间,如下所示:
3. 当函数 fn 调用结束后,栈区会自动释放为 fn 中的局部变量分配的空间,如下所示:
4. 当执行到 for 循环时,栈区会为局部变量(如循环控制变量 int i)分配空间,如下所示:
5. 当 for 循环结束时,意味着其所在的块级作用域也随之结束,系统将自动从栈区回收为循环控制变量 int i 所分配的空间,如下所示:
6. 当函数 fn 再次被调用时,栈区将重新分配空间以存放 fn 中的局部变量,如下所示:
7. 当函数 fn 调用结束后,栈区会自动释放为 fn 中的局部变量分配的空间,如下所示:
8. 全局静态区中的数据从程序启动时开始存在,直到程序正常结束或操作系统回收程序资源时才会被销毁,如下所示:
2 static 关键字
2.1 概述
通常,我们会将常量定义在全局作用域中,以便在多个文件或模块间共享不变的数据。而对于变量,我们倾向于将其作用域限制在单个源文件或函数内部,以减少内存消耗并提高代码的可维护性。这样不仅可以防止其他部分意外修改这些变量,还能增强程序的安全性,因为全局变量可以被程序中的任何部分访问和修改,从而可能导致难以追踪的副作用和程序不稳定。
由于局部变量的生命周期较短,使用起来不太方便,因此引入了 static 关键字。static 关键字可以用于声明静态变量(局部和全局)和静态函数等对象,以控制变量和函数等对象的作用范围和生存周期。
2.2 静态局部变量
2.2.1 功能与特点
使用 static 关键字修饰的局部变量称为静态局部变量,这类变量与全局变量一样存储在内存中的全局静态区。静态局部变量具有以下特点:
存储位置改变:当 static 修饰局部变量时,该变量的存储位置从栈改为了全局静态区,这意味着该变量的生命周期贯穿整个程序运行期间,但其作用域仍然限制在声明它的代码块内。
初始化与生命周期:静态局部变量只在函数第一次调用时初始化一次,即使在多次进入和退出其作用域时,其值也不会丢失(除非被显示的修改),并且其生命周期延续至整个程序的执行期间。
默认初始化:如果静态局部变量在声明时没有初始赋值,编译器会默认将它们初始化为零(整型变量为 0,浮点型为 0.000000,字符型为 '\0',指针类型为 NULL),这一规则与全局变量的初始化规则一致。
2.2.2 功能演示
#include <stdio.h>
void func()
{
// 静态局部变量,只初始化一次,默认为零
// 即使在多次进入和退出其作用域时,其值也不会丢失
static int Icount; // 默认为 0
Icount++;
printf("%d\n", Icount); // 第一次调用输出 1,第二次调用输出 2,以此类推
static double Dcount; // 默认为 0.000000
Dcount++;
printf("%lf\n", Dcount); // 第一次调用输出 1.000000,第二次调用输出 2.000000,以此类推
static char Ccount; // 默认为 '\0'
Ccount += 49;
printf("%c\n", Ccount); // 第一次调用输出 '1'(ASCII 码 49),第二次调用输出 'b' (ASCII 码 98)
static int arr[10]; // 各元素默认为 0
printf("数组初始值为:\n");
for (int i = 0; i < 10; i++)
{
printf("arr[%d] = %d ", i, arr[i]); // 默认全是 0
}
for (int i = 0; i < 10; i++)
{
arr[i] += i + 1; // 赋值
}
printf("\n重新赋值后,现在数组的值为:\n");
for (int i = 0; i < 10; i++)
{
printf("arr[%d] = %d ", i, arr[i]); // 赋值之后的数据
}
printf("\n\n");
}
int main()
{
printf("第一次调用 func 函数:\n");
func(); // 输出 1
printf("第二次调用 func 函数:\n");
func(); // 输出 2
// 静态局部变量的作用域仍然限制在声明它的代码块内
// printf("%d\n", Icount);
// printf("%ld\n", Dcount);
// printf("%c\n", Ccount);
return 0;
}
输出结果如下所示:
注意,虽然静态局部变量的生命周期贯穿整个程序运行期间,但其作用域仍然限制在声明它的代码块内。如果在其他代码块中尝试访问它,将会导致编译错误,如下所示:
2.2.3 案例:记录函数被调用次数
#include <stdio.h>
// 定义一个函数,记录自己被调用了多少次
int count()
{
int n = 0;
n++;
printf("我被调用了%d次\n", n);
}
int main()
{
count();
count();
count();
return 0;
}
在上述程序中,如果仅使用局部变量 n,则每次调用 count 函数时,n 都会被重置为 0,导致输出结果始终为“我被调用了1次”。
输出结果如下所示:
为了记录函数被调用的总次数,可以采用全局变量或静态局部变量来保存 n 的值,使其在多次函数调用之间持续有效。下面将通过使用全局变量来实现这一点:
#include <stdio.h>
// 定义全局变量
int num = 0;
// 定义一个函数,记录自己被调用了多少次
int count()
{
num++;
printf("我被调用了%d次\n", num);
}
int main()
{
count();
count();
count();
return 0;
}
输出结果如下所示:
虽然使用全局变量可以实现预期功能,但这可能会导致后续代码中出现变量名称冲突的问题,不够便捷。因此,推荐使用静态全局变量来完成这一任务。这样做既能保证变量在多次函数调用间保持持久状态,又能将其作用域限定在定义它的文件范围内,有效避免命名冲突。
#include <stdio.h>
// 定义一个函数,记录自己被调用了多少次
int count()
{
static int num = 0;
num++;
printf("我被调用了%d次\n", num);
}
int main()
{
count();
count();
count();
return 0;
}
输出结果如下所示:
2.2.4 局部变量与静态局部变量的生命周期及内存管理分析
#include <stdio.h>
// 定义一个普通函数 fn,用于展示普通局部变量的行为
void fn()
{
int n = 10; // 在函数每次调用时,局部变量 n 都会被重新初始化为 10
int a; // 局部变量 a 未初始化,其值将是未定义的(垃圾值)
printf("n=%d, a(未初始化,垃圾值)=%d \n", n, a); // 打印 n 和 a 的值,a 的值不可预测
n++;
printf("n++=%d \n", n);
printf("\n");
}
// 定义一个带有静态局部变量的函数 fn_static,用于展示静态局部变量的行为
void fn_static()
{
static int n = 10; // 静态局部变量 n 只在第一次调用时初始化为 10,之后调用会保持上次的值
static int a; // 静态局部变量 a 只在第一次调用时默认初始化为 0,之后调用会保持上次的值
printf("static n=%d, a=%d\n", n, a);
n++;
printf("static n++=%d\n", n);
printf("\n");
}
int main()
{
// 第一次调用 fn 函数
fn();
// 第一次调用 fn_static 函数,静态变量 n 和 a 只初始化一次
fn_static();
// 再次调用 fn 函数,局部变量 n 和 a 重新初始化
fn();
// 再次调用 fn_static 函数,静态变量 n 和 a 保持上次的值,不会再进行初始化
fn_static();
return 0;
}
输出结果如下所示:
内存分析:
1. 当函数 fn 被调用时,栈区会为 fn 中的局部变量分配空间,如下所示:
2. 在执行函数 fn 中的 n++ 操作后,n 的值由 10 增加到了 11,如下所示:
3. 当函数 fn 调用结束后,栈区会自动释放为 fn 中的局部变量分配的空间,如下所示:
4. 当函数 fn_static 被调用时,全局静态区会为 fn_static 中的静态局部变量分配空间,如下所示:
5. 在执行函数 fn_static 中的 n++ 操作后,n 的值由 10 增加到了 11,如下所示:
6. 当函数 fn_static 调用结束后,全局静态区不会释放为 fn_static 中的静态局部变量分配的空间,如下所示:
7. 当函数 fn 再次被调用时,栈区将重新分配空间以存放 fn 中的局部变量,如下所示:
8. 在执行函数 fn 中的 n++ 操作后,n 的值依然是由 10 增加到了 11,如下所示:
9. 当函数 fn 调用结束后,栈区会自动释放为 fn 中的局部变量分配的空间,如下所示:
10. 当函数 fn_static 再次被调用时,静态局部变量不会再次初始化(因为它已经在首次调用时完成了初始化),而是直接执行 fn_static 中的 n++ 操作,使 n 的值从 11 增加到了 12,如下所示:
11. 当函数 fn_static 调用结束后,全局静态区不会释放为 fn_static 中的静态局部变量分配的空间,如下所示:
12. 全局静态区中的数据从程序启动时开始存在,直到程序正常结束或操作系统回收程序资源时才会被销毁,如下所示:
2.3 静态全局变量
2.3.1 功能与特点
在 C 或 C++ 中,使用 static 关键字修饰的全局变量被称为静态全局变量。与普通全局变量相比,静态全局变量具有以下几个关键特性和用途:
作用域限制:普通全局变量在整个项目中可见,其他文件可以通过 extern 声明直接使用。相比之下,静态全局变量的作用域被限制在其声明的文件内部,即使它们具有全局的生命周期,但在链接时对其他文件不可见。因此,其他文件不能通过 extern 声明直接访问静态全局变量,除非通过该文件提供的函数接口。
初始化:静态全局变量在程序启动时进行初始化。与普通全局变量一样,它们存储在全局静态区域(即数据段),其生命周期贯穿整个程序执行过程。如果显式指定了初始值,则使用该值进行初始化;若未指定初始值,则编译器会默认将其初始化为零(整型变量为 0,浮点型为 0.0,字符型为 '\0',指针类型为 NULL)。由于它们位于全局静态区域内,其初始化规则与普通全局变量相同,并且这种初始化仅在程序开始执行时发生一次。
降低耦合:由于静态全局变量的作用域限制,它们有助于减少不同源文件之间的耦合。每个文件可以拥有自己的一组静态全局变量,这些变量在文件内部是全局的,但在文件外部是不可见的。这有助于封装和模块化代码,使得代码更加易于理解和维护。
避免命名冲突:在多文件项目中,如果不同文件中有同名的全局变量,这通常会导致链接错误(除非这些变量在逻辑上应该通过某种方式共享,这通常通过 extern 来实现)。然而,静态全局变量由于其作用域限制,可以有效地避免这种命名冲突。不同文件中的静态全局变量即使名字相同,也不会相互影响。
2.3.2 功能演示
static 关键字用于限制变量的链接性和生命周期。当变量被声明为 static 时,它的链接性变为内部链接,这意味着它只能在其定义的文件内部被访问。因此,其他文件不能通过 extern 声明来访问一个被声明为 static 的变量或函数。
下面我们演示一个静态全局变量的案例,创建两个源文件,分别命名为 static_global_data.c 和 static_global_main.c,源文件代码如下:
static_global_data.c:
#include <stdio.h>
// 外部可访问的全局变量
int num01 = 100;
const double PI01 = 3.14;
char msg01[] = "Hello msg01";
// 静态全局变量,只能在 static_global-data.c 内部访问
// 其他文件不能通过 extern 声明来访问一个被声明为 static 的变量
static int num02 = 200;
static const double PI02 = 3.15;
static char msg02[] = "Hello msg02";
// 外部可访问的函数
void fn01()
{
printf("function fn01 \n");
}
// 静态函数,只能在 static_global-data.c 内部调用
// 其他文件不能通过 extern 声明来访问一个被声明为 static 的函数
static void fn02()
{
printf("function fn02 \n"); // 注意这里应该是 fn02 而不是 fn01
}
static_global_main.c:
#include <stdio.h>
// 外部声明 static_global-data.c 中定义的全局变量和函数
extern int num01;
extern const double PI01;
extern char msg01[];
extern void fn01();
// 尝试外部声明 static_global-data.c 中定义的静态全局变量和函数会导致编译错误
extern int num02; // 错误:num02 是静态的,外部不可见,如果不用这些变量,这里不会报错
extern const double PI02; // 错误:PI02 是静态的,外部不可见,如果不用这些变量,这里不会报错
extern char msg02[]; // 错误:msg02 是静态的,外部不可见,如果不用这些变量,这里不会报错
extern void fn02(); // 错误:fn02 是静态的,外部不可见,如果不用这些变量,这里不会报错
int main()
{
// 使用 static_global-data.c 中定义的全局变量和函数
printf("%d \n", num01);
printf("%f \n", PI01);
printf("%s \n", msg01);
fn01();
// 以下代码由于尝试访问静态变量和函数,会被编译器报错(如果取消注释)
// printf("%d \n", num02); // 错误:num02 是静态的
// printf("%f \n", PI02); // 错误:PI02 是静态的
// printf("%s \n", msg02); // 错误:msg02 是静态的
// fn02(); // 错误:fn02 是静态的
return 0;
}
输出结果如下所示:
2.3.3 VS Code 编译多个源文件
在此之前,我们一直在命令行中使用 gcc 指令来一次性编译多个源文件。现在,我们将介绍如何在 VS Code 中实现这一操作。
默认情况下,VS Code 并不支持直接编译多个源文件,因此我们需要修改 .vscode 目录下的 tasks.json 文件来实现这一功能。
在修改前,请务必先备份现有的配置文件,因为我们完成当前示例后需要恢复原始设置。打开 tasks.json 文件,备份初始配置文件:
{
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: gcc.exe 生成活动文件",
"command": "C:\\mingw64\\bin\\gcc.exe",
"args": [
"-fdiagnostics-color=always",
"-g",
"${file}",
"-o",
"${fileDirname}\\${fileBasenameNoExtension}.exe"
],
"options": {
"cwd": "${fileDirname}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "调试器生成的任务。"
}
],
"version": "2.0.0"
}
将 tasks.json 里面的内容替换成下面的内容:
{
"version": "2.0.0",
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: gcc.exe 生成活动文件",
"command": "C:\\mingw64\\bin\\gcc.exe",
"args": [
"-fdiagnostics-color=always",
"-Wall",
"-g",
"static_dlobal_data.c",
"static_global_main.c",
"-o",
"${fileDirname}\\${fileBasenameNoExtension}.exe"
],
"options": {
"cwd": "${fileDirname}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "调试器生成的任务。"
}
]
}
配置代码解释如下:
- version: "2.0.0" 指定了 tasks.json 文件的版本。
- tasks: 一个数组,包含了所有定义的任务。在这个例子中,只定义了一个任务。
- type: "cppbuild" 指定了任务的类型,这里用于 C/C++ 的构建。
- label: "C/C++: gcc.exe 生成活动文件" 是这个任务的名称,用于在 VS Code 的任务列表中识别它。
- command: "C:\\mingw64\\bin\\gcc.exe" 指定了执行命令,这里是 GCC 编译器的路径。
- args: 一个数组,包含了传递给 GCC 编译器的参数。
- -fdiagnostics-color=always:使编译器输出带有颜色的诊断信息。
- -Wall:开启所有警告信息。
- -g:生成调试信息,方便使用调试器。
- static_global_data.c 和 static_global_main.c:要编译的源文件。
- -o:指定输出文件的名称。
- "${fileDirname}\\${fileBasenameNoExtension}.exe":使用变量来动态指定输出文件的路径和名称。${fileDirname} 是当前文件所在的目录,${fileBasenameNoExtension} 是当前文件的名称但不包括扩展名,最终会生成一个 exe 文件。
- options: 定义了任务执行的选项。
- "cwd": "${fileDirname}":指定了命令的工作目录为当前文件的目录。
- problemMatcher: ["$gcc"] 指定了如何解析 GCC 编译器输出的错误和警告信息,以便在 VS Code 的问题面板中显示。
- group: 定义了任务的分组和是否作为默认任务。
- "kind": "build":指定这是一个构建任务。
- "isDefault": true:标记这个任务为默认任务,这样可以通过快捷键(通常是 Ctrl+Shift+B)直接运行。
- detail: "调试器生成的任务。" 提供了关于这个任务的额外信息,通常用于在 VS Code 的 UI 中显示。
在替换完配置文件后,打开 static_global_main.c。现在,可以通过点击运行按钮直接运行程序,VS Code 将会同时编译 static_global_data.c 和 static_global_main.c 这两个文件,最终将会生成 static_global_main.exe 的可执行文件 。如下所示:
输出结果如下所示:
案例演示完毕,请将配置文件改回原先的内容。
2.4 static 修饰函数、数组、常量等
static 关键字不仅适用于局部变量和全局变量,还可用于修饰函数、全局数组和全局常量等。
- 静态函数:限制函数的可见范围至其声明的文件内部,禁止其他文件直接调用。
- 静态全局数组和静态全局常量:与静态全局变量类似,作用域限定在声明的文件内部,有助于避免命名冲突,并促进代码的封装与模块化。
需要强调的是,静态常量通常是指同时被 static 和 const 修饰的常量,这类常量不仅具有作用域限制,而且其值不可更改。在 C 语言中,声明一个常量一般会同时使用 static 和 const 修饰符。
3 静态与非静态变量及函数的特性分析总结
3.1 全局变量
定义与位置:全局变量是在所有函数外部定义的变量,它们的作用域覆盖整个程序,即程序中的所有函数都可以访问这些变量。全局变量通常在文件的顶部声明,但也可以在任何函数外部的位置声明。
作用域:全局变量的作用域是整个程序,这意味着无论函数在哪里定义,它们都可以访问这些全局变量。这种广泛的可见性可以方便地在不同函数之间共享数据,但也可能导致程序难以理解和维护,因为全局变量可以被程序中的任何部分修改。
生命周期:全局变量的生命周期贯穿整个程序执行期间,存储于全局静态区。
初始化特性:它们在程序启动时被初始化,如果指定了初始值,或者默认为零(0、0.000000、'\0'、NULL),并在程序结束时才被销毁。这意味着全局变量在整个程序运行期间都占用内存。
链接性:全局变量具有外部链接性,即如果多个源文件(.c 文件)包含了同一个全局变量的声明(通常通过 extern 关键字),则这些源文件中的全局变量引用将指向同一个内存位置。然而,如果全局变量在定义时使用了 static 关键字(静态全局变量),则它只有内部链接性,即它只在定义它的源文件中可见和可访问。
存在理由:全局变量存在的主要理由是支持跨函数的数据共享。在某些情况下,可能需要在整个程序中维护某些状态或配置信息,这时全局变量就非常有用。然而,过度使用全局变量会导致程序结构混乱、难以维护和调试,因此应谨慎使用。
3.2 局部变量
定义与位置:局部变量是在函数或代码块内部定义的变量。它们的作用域仅限于定义它们的函数或代码块内部。局部变量可以在函数的任何地方声明,但通常会在函数的开始部分或需要时声明。
作用域:局部变量的作用域是它们被声明的那个函数或代码块。一旦离开这个作用域,局部变量就不再可访问,其名称和存储位置可以被重新用于其他目的。这种限制有助于减少函数之间的耦合,并提高代码的可读性和可维护性。
生命周期:局部变量的生命周期始于它们被声明的那个代码块的开始,并结束于该代码块的结束。一旦执行流离开该代码块,局部变量就会被销毁,其占用的内存会被释放(对于自动变量而言),存储于栈区。
初始化特性:如果没有显式地进行初始化,那么这些数据将不会有默认值,而是会包含所谓的 “垃圾值” 或 “未定义值”。这些值实际上是之前存储在相同内存位置的数据的残留。
链接性:局部变量没有链接性。它们的作用域仅限于定义它们的函数或代码块,因此每个局部变量的实例都是唯一的,并且与其他函数或代码块中的同名局部变量无关。
存在理由:局部变量存在的主要理由是为了支持函数内部的数据封装和状态管理。通过使用局部变量,函数可以处理输入数据并产生输出,而不需要影响程序的其他部分。这有助于减少函数之间的副作用和错误,并提高代码的可读性和可重用性。此外,局部变量还有助于减少程序的内存占用,因为它们只在需要时存在,并在不再需要时被销毁。
3.3 静态全局变量
定义与位置:静态全局变量是在所有函数外部定义的变量,但在其声明时前面加上了 static 关键字。这意味着它们的作用域仍然限制在定义它们的文件(源文件)内部,即它们对同一程序中的其他源文件是不可见的。静态全局变量通常在文件的顶部声明。
作用域:静态全局变量的作用域是定义它们的文件内部。尽管它们是在全局作用域中声明的,但由于 static 关键字的作用,它们不能被同一程序中的其他源文件直接访问。
生命周期:静态全局变量的生命周期贯穿整个程序执行期间,存储于全局静态区。
初始化特性:它们在程序启动时被初始化(只初始化一次),如果指定了初始值,或者默认为零(0、0.000000、'\0'、NULL),并在程序结束时才被销毁。与全局变量一样,静态全局变量在整个程序运行期间都占用内存。
链接性:静态全局变量具有内部链接性。这意味着它们只在定义它们的源文件中可见和可访问,对程序中的其他源文件是隐藏的。这有助于避免命名冲突,并允许在不同源文件中使用相同名称的变量。
存在理由:静态全局变量的存在理由主要是为了限制变量的可见性和作用域,以避免命名冲突和不必要的外部依赖。它们允许在多个函数之间共享数据,同时保持数据的封装性和模块性。此外,静态全局变量还可以用于存储需要在程序执行期间保持不变的数据。
3.4 静态局部变量
定义与位置:静态局部变量是在函数内部定义的变量,但在其声明时前面加上了 static 关键字。这意味着它们的作用域仍然限制在定义它们的函数内部,但它们的生命周期与整个程序相同。
作用域:静态局部变量的作用域是定义它们的函数内部。与普通的局部变量一样,它们只能在声明它们的函数内部被访问。
生命周期:生命周期贯穿整个程序执行期间,存储于全局静态区。
初始化特性:它们在程序启动时被初始化(只初始化一次),如果指定了初始值,或者默认为零(0、0.000000、'\0'、NULL),并在程序结束时才被销毁。与普通局部变量不同,静态局部变量在函数调用之间保持其值不变(除非被显示修改)。
链接性:静态局部变量同局部变量一样没有链接性。它们的作用域仅限于定义它们的函数内部,并且每个静态局部变量都是唯一的,与其他函数中的同名静态局部变量无关。
存在理由:静态局部变量的存在理由主要是为了在函数调用之间保持变量的值。由于它们的生命周期贯穿整个程序执行期间,并且在函数调用之间保持其值不变,因此它们可以用于存储需要在多次函数调用之间保持的数据。这有助于减少函数之间的耦合,并提高代码的可读性和可维护性。此外,静态局部变量还可以用于实现递归函数中的状态保存。
3.5 普通函数
定义与位置:普通的函数是在所有函数外部定义的,但通常放在特定的源文件(.c 文件)中。它们可以在整个程序中被其他函数或源文件调用,只要它们被正确地声明和链接。
作用域:普通的函数具有全局作用域,这意味着只要它们被正确地声明(通常通过原型声明),就可以在整个程序中被调用。
生命周期:函数定义的代码在程序加载到内存时就已经存在,并在程序执行期间一直可用。
链接性:普通的函数具有外部链接性,即它们可以被同一程序中的其他源文件调用。为了在不同源文件之间共享函数,需要在某个头文件中声明函数原型,并在链接时确保所有源文件都包含了这个头文件的正确版本。
存在理由:普通的函数是 C 语言程序中组织代码的基本单元之一。它们允许将程序分解为更小、更易于管理的部分,提高了代码的可读性、可重用性和可维护性。
3.6 静态函数
定义与位置:静态函数也是在所有函数外部定义的,但在定义时前面加上了 static 关键字。这意味着它们的作用域被限制在定义它们的源文件内部,即它们只能在同一个源文件中被调用。
作用域:静态函数的作用域是定义它们的源文件内部。这意味着它们不能被同一程序中的其他源文件直接调用。这种限制有助于隐藏函数实现细节,减少函数之间的耦合,并提高代码的模块性。
生命周期:它们的代码在程序加载到内存时就已经存在,并在程序执行期间一直可用。
链接性:静态函数具有内部链接性,即它们只能在定义它们的源文件中被调用。这意味着链接器不会将静态函数的名称暴露给其他源文件,从而避免了命名冲突和不必要的外部依赖。
存在理由:静态函数的存在理由主要是为了限制函数的可见性和作用域,以提高代码的封装性和模块性。它们允许在单个源文件中组织相关的函数,同时隐藏这些函数的实现细节,使其他源文件无法直接访问这些函数。这有助于减少函数之间的耦合,使得代码更加清晰、易于理解和维护。此外,静态函数还可以用于实现只在单个源文件中使用的辅助函数或工具函数。
3.7 静态全局常量
当 const 前面加上 static 时,这个常量就被声明为静态常量。这种组合通常用于以下几种情况:
限制作用域:在 C 语言中,全局常量(即不在函数内部定义的常量)默认就具有外部链接性,这意味着它们可以被同一程序中的其他源文件访问。如果不希望这种情况发生,就可以通过将常量声明为静态的来限制其作用域和链接性。静态全局常量仅在定义它的文件(源文件)内部可见。这有助于避免命名冲突,并隐藏实现细节,提高代码的封装性。
内部链接性:与静态全局变量类似,静态全局常量也具有内部链接性。这意味着它们不会被链接器暴露给其他源文件,从而减少了外部依赖和潜在的命名冲突。
4 测试题
1. 请写出下面程序的运行结果。
void func()
{
int n1 = 10;
static int n2 = 20;
n1 ++;
n2 ++;
printf("%d %d \n", n1, n2);
}
int main()
{
func();
func();
return 0;
}
【答案】
11 21
11 22
【解析】
(1)n1 是普通的局部变量,函数调用时创建,函数调用结束销毁,func() 调用两次,每次都会创建新的变量 n1,故两次 n1 的值都是 11。
(2)n2 是静态局部变量,只在函数第一次调用时创建,函数调用结束也不会销毁,所以第二次调用 func() 的时候并没有创建新的 n2,会继续在上次基础上累加,所以第一次值是 21, 第二次是 22。