1 引言
考虑下面这段代码:
c
#include <stdio.h>
int global = 100; /* 全局变量 */
void func(void)
{
int local = 10; /* 局部变量 */
static int static_local = 0; /* 静态局部变量 */
local++;
static_local++;
printf("local=%d, static_local=%d, global=%d\n",
local, static_local, global);
}
int main(void)
{
func();
func();
func();
return 0;
}
输出:
text
local=11, static_local=1, global=100
local=11, static_local=2, global=100
local=11, static_local=3, global=100
为什么每次调用func,local总是11,而static_local会累加?为什么global一直存在?这就是作用域和生命周期在起作用。
2 基本概念
2.1 作用域
作用域(Scope)是指程序中可以访问该变量的区域。通俗地说,就是变量在哪些地方"可见"。
C语言中的作用域分为:
-
块作用域 :在代码块
{ }内定义的变量 -
文件作用域:在所有函数之外定义的变量
-
函数作用域 :仅适用于
goto标签 -
函数原型作用域:函数声明中的参数
本章主要关注块作用域和文件作用域。
2.2 生命周期
生命周期(Lifetime)是指变量在内存中存在的时间段。从变量分配内存到释放内存的整个过程。
C语言中的生命周期分为:
-
静态存储期:程序开始到结束
-
自动存储期:进入代码块到退出代码块
-
动态存储期 :
malloc到free(后续章节讨论)
2.3 存储类别
C语言提供了几种存储类别说明符,用于控制变量的作用域和生命周期:
| 存储类别 | 关键字 | 作用域 | 生命周期 | 默认初始值 |
|---|---|---|---|---|
| 自动 | auto |
块作用域 | 自动存储期 | 不确定 |
| 寄存器 | register |
块作用域 | 自动存储期 | 不确定 |
| 静态无链接 | static |
块作用域 | 静态存储期 | 0 |
| 静态外部链接 | 无(或extern) |
文件作用域 | 静态存储期 | 0 |
| 静态内部链接 | static |
文件作用域 | 静态存储期 | 0 |
3 局部变量
3.1 定义与特性
局部变量(Local Variable)是在函数内部或代码块内定义的变量。
c
void func(void)
{
int local_var; /* 局部变量 */
int another = 10; /* 可初始化 */
if (local_var > 0) {
int block_var; /* 块级局部变量 */
}
}
特性:
-
作用域:从定义处开始,到所在块结束
-
生命周期:进入块时创建,退出块时销毁(自动存储期)
-
默认初始值:不确定(垃圾值)
-
存储位置:栈
3.2 作用域规则
c
#include <stdio.h>
void func(void)
{
int x = 10; /* 函数级局部变量 */
if (x > 0) {
int x = 20; /* 块级局部变量,隐藏外层的x */
printf("块内:x = %d\n", x); /* 输出20 */
}
printf("函数内:x = %d\n", x); /* 输出10 */
}
int main(void)
{
func();
/* printf("%d\n", x); */ /* 错误!x在此处不可见 */
return 0;
}
变量隐藏:内层块中可以定义与外层同名的变量,此时内层变量"隐藏"外层变量。
3.3 生命周期示例
c
#include <stdio.h>
void func(void)
{
int count = 0;
count++;
printf("count = %d\n", count);
}
int main(void)
{
func(); /* 输出 count = 1 */
func(); /* 输出 count = 1 */
func(); /* 输出 count = 1 */
return 0;
}
每次调用func,count都被重新创建并初始化为0,所以始终输出1。
3.4 auto关键字
auto是C语言中用于显式声明自动存储类别的关键字,但几乎从不使用,因为局部变量默认就是auto。
c
void func(void)
{
auto int x; /* 等价于 int x; */
}
4 全局变量
4.1 定义与特性
全局变量(Global Variable)是在所有函数之外定义的变量。
c
#include <stdio.h>
int global_count = 0; /* 全局变量,可被其他文件访问 */
static int file_count = 0; /* 静态全局变量,仅本文件可见 */
void func1(void)
{
global_count++;
file_count++;
}
void func2(void)
{
global_count++;
file_count++;
}
int main(void)
{
func1();
func2();
printf("global_count = %d\n", global_count); /* 2 */
printf("file_count = %d\n", file_count); /* 2 */
return 0;
}
特性:
-
作用域:从定义处到文件结束(文件作用域)
-
生命周期:程序开始到结束(静态存储期)
-
默认初始值:0(如果没有显式初始化)
-
存储位置:数据段(初始化)或BSS段(未初始化)
4.2 跨文件访问全局变量
如果要在其他文件中访问全局变量,需要使用extern声明:
c
/* file1.c */
int global = 100; /* 定义全局变量 */
/* file2.c */
extern int global; /* 声明,告诉编译器这个变量在其他文件 */
void func(void)
{
global = 200; /* 可以访问 */
}
4.3 静态全局变量
用static修饰的全局变量具有内部链接,只能在定义它的文件内访问:
c
/* file1.c */
static int hidden = 100; /* 只能在本文件使用 */
/* file2.c */
extern int hidden; /* 错误!无法链接到static变量 */
4.4 全局变量的风险
c
#include <stdio.h>
int counter = 0; /* 全局变量 */
void increment(void)
{
counter++;
}
void reset(void)
{
counter = 0;
}
int main(void)
{
increment();
increment();
printf("%d\n", counter); /* 2 */
reset();
printf("%d\n", counter); /* 0 */
return 0;
}
全局变量的问题:
-
命名冲突:容易与其他文件或库的变量重名
-
代码耦合:函数依赖于全局变量,难以独立测试
-
调试困难:任何函数都可能修改它,难以追踪
-
线程不安全:多线程环境下需要同步
最佳实践:尽量少用全局变量,必要时用静态全局变量限制作用域。
5 静态局部变量
5.1 定义与特性
静态局部变量 (Static Local Variable)是用static修饰的局部变量。
c
#include <stdio.h>
void counter(void)
{
static int count = 0; /* 静态局部变量 */
count++;
printf("count = %d\n", count);
}
int main(void)
{
counter(); /* 输出 count = 1 */
counter(); /* 输出 count = 2 */
counter(); /* 输出 count = 3 */
return 0;
}
特性:
-
作用域:块作用域(和普通局部变量一样)
-
生命周期:程序开始到结束(静态存储期)
-
默认初始值:0(如果没有显式初始化)
-
存储位置:数据段(初始化)或BSS段(未初始化)
-
初始化:只在程序启动时初始化一次
5.2 初始化时机
c
#include <stdio.h>
void func(void)
{
static int x = 10; /* 程序启动时初始化一次 */
static int y; /* 程序启动时初始化为0 */
printf("x = %d, y = %d\n", x++, y++);
}
int main(void)
{
func(); /* x=10, y=0 */
func(); /* x=11, y=1 */
func(); /* x=12, y=2 */
return 0;
}
静态局部变量的初始化只在程序启动时执行一次,后续调用不再重新初始化。
5.3 应用场景
5.3.1 保持函数状态
c
#include <stdio.h>
int next_id(void)
{
static int id = 1000; /* 从1000开始 */
return id++;
}
int main(void)
{
for (int i = 0; i < 5; i++) {
printf("ID: %d\n", next_id());
}
return 0;
}
5.3.2 一次性初始化
c
#include <stdio.h>
void initialize(void)
{
static int initialized = 0;
if (!initialized) {
printf("执行一次性初始化...\n");
/* 执行初始化操作 */
initialized = 1;
}
printf("函数正常执行\n");
}
int main(void)
{
initialize();
initialize();
initialize();
return 0;
}
输出:
text
执行一次性初始化...
函数正常执行
函数正常执行
函数正常执行
5.3.3 计数器
c
#include <stdio.h>
int factorial(int n)
{
static int depth = 0; /* 递归深度计数 */
depth++;
printf("depth: %d\n", depth);
if (n <= 1) {
depth--; /* 返回时减少深度 */
return 1;
}
int result = n * factorial(n - 1);
depth--;
return result;
}
6 存储类别对比
6.1 三种变量的对比
| 特性 | 局部变量 | 静态局部变量 | 全局变量 |
|---|---|---|---|
| 作用域 | 定义它的块内 | 定义它的块内 | 整个文件(可扩展) |
| 生命周期 | 函数调用期间 | 整个程序运行期 | 整个程序运行期 |
| 存储位置 | 栈 | 数据段/BSS | 数据段/BSS |
| 默认初始值 | 垃圾值 | 0 | 0 |
| 可被其他文件访问 | 否 | 否 | 是(除非static) |
| 同名隐藏 | 可被内层隐藏 | 可被内层隐藏 | 可被局部隐藏 |
6.2 存储类别小结
c
#include <stdio.h>
int g1 = 10; /* 全局变量,外部链接 */
static int g2 = 20; /* 全局变量,内部链接(本文件私有) */
void func(void)
{
int a = 30; /* 局部变量,自动存储期 */
static int b = 40; /* 静态局部变量,静态存储期 */
register int c = 50; /* 建议放到寄存器(编译器决定) */
a++; /* 每次调用重新初始化,所以永远是31?不,每次都从30开始 */
b++; /* 保持上次的值 */
printf("a=%d, b=%d\n", a, b);
}
int main(void)
{
func(); /* a=31, b=41 */
func(); /* a=31, b=42 */
func(); /* a=31, b=43 */
return 0;
}
7 常见错误与注意事项
7.1 使用未初始化的局部变量
c
void func(void)
{
int x; /* 未初始化,值是垃圾 */
if (x > 0) { /* 危险!x的值不确定 */
/* ... */
}
}
7.2 在函数内返回局部变量的地址
c
int* get_int(void)
{
int x = 100;
return &x; /* 危险!x是局部变量,函数返回后销毁 */
}
7.3 混淆静态局部变量和全局变量
c
int counter = 0; /* 全局变量 */
void func(void)
{
static int counter = 0; /* 静态局部,隐藏全局的counter */
counter++;
printf("%d\n", counter);
}
int main(void)
{
func(); /* 输出1 */
func(); /* 输出2 */
printf("%d\n", counter); /* 输出0(全局的) */
return 0;
}
7.4 全局变量滥用
c
/* 不好的设计 */
int total; /* 全局变量,到处被修改 */
void add(int x) { total += x; }
void sub(int x) { total -= x; }
void mul(int x) { total *= x; }
/* 更好的设计:封装成模块 */
static int total; /* 文件内静态,外部不可见 */
void add(int x) { total += x; }
void sub(int x) { total -= x; }
int get_total(void) { return total; } /* 提供访问接口 */
7.5 忽略静态局部变量的初始化
c
void func(void)
{
static int x; /* 自动初始化为0 */
x++;
printf("%d\n", x); /* 第一次输出1 */
}
void func2(void)
{
static int x = 0; /* 显式初始化,效果相同 */
x++;
printf("%d\n", x);
}
7.6 误解register关键字
c
register int x; /* 建议编译器将x放到寄存器 */
/* 但不能对register变量取地址 */
int *p = &x; /* 错误!不能取register变量的地址 */
现代编译器优化能力很强,很少需要手动使用register。
8 综合示例
8.1 银行账户模拟
c
#include <stdio.h>
#include <string.h>
/* 账户模块 - 使用静态全局变量封装内部状态 */
static double balance = 0.0; /* 余额,文件内私有 */
static int transaction_count = 0; /* 交易次数统计 */
/* 公开接口 */
double get_balance(void)
{
return balance;
}
int get_transaction_count(void)
{
return transaction_count;
}
void deposit(double amount)
{
if (amount > 0) {
balance += amount;
transaction_count++;
printf("存入 %.2f,当前余额 %.2f\n", amount, balance);
}
}
int withdraw(double amount)
{
if (amount <= 0) {
return -1; /* 金额无效 */
}
if (amount > balance) {
return -2; /* 余额不足 */
}
balance -= amount;
transaction_count++;
printf("取出 %.2f,当前余额 %.2f\n", amount, balance);
return 0; /* 成功 */
}
int main(void)
{
deposit(1000);
deposit(500);
withdraw(200);
withdraw(2000); /* 余额不足 */
printf("最终余额:%.2f\n", get_balance());
printf("交易次数:%d\n", get_transaction_count());
return 0;
}
8.2 生成唯一ID
c
#include <stdio.h>
/* 生成唯一ID的函数 */
unsigned long generate_id(void)
{
static unsigned long counter = 0;
return counter++;
}
/* 带前缀的ID生成器 */
const char* generate_prefixed_id(char *buffer, size_t size, const char *prefix)
{
static unsigned long counter = 0;
snprintf(buffer, size, "%s-%lu", prefix, counter++);
return buffer;
}
int main(void)
{
for (int i = 0; i < 5; i++) {
printf("ID: %lu\n", generate_id());
}
char id_buf[50];
for (int i = 0; i < 3; i++) {
printf("订单号:%s\n", generate_prefixed_id(id_buf, sizeof(id_buf), "ORD"));
}
return 0;
}
8.3 函数调用次数统计
c
#include <stdio.h>
void important_function(void)
{
static int call_count = 0;
call_count++;
printf("important_function 被调用 %d 次\n", call_count);
/* 函数的主要功能 */
}
int get_function_call_count(void)
{
static int count = 0; /* 这其实没用,因为没法从外部获取 */
/* 需要另外的方式,这里只是演示 */
return 0;
}
int main(void)
{
important_function();
important_function();
important_function();
important_function();
return 0;
}
9 本章小结
本章系统介绍了变量的作用域与生命周期:
1. 基本概念
-
作用域:变量可见的区域(块作用域、文件作用域)
-
生命周期:变量存在的时间段(自动存储期、静态存储期)
2. 局部变量
-
作用域:定义它的块内
-
生命周期:进入块时创建,退出块时销毁
-
默认值:不确定(必须初始化后才能使用)
-
存储位置:栈
3. 全局变量
-
作用域:整个文件(可通过
extern扩展) -
生命周期:程序开始到结束
-
默认值:0
-
存储位置:数据段/BSS
-
静态全局变量(
static):限制在当前文件
4. 静态局部变量
-
作用域:定义它的块内(同局部变量)
-
生命周期:程序开始到结束(同全局变量)
-
默认值:0
-
存储位置:数据段/BSS
-
初始化只执行一次
5. 对比总结
| 变量类型 | 作用域 | 生命周期 | 默认值 | 存储位置 |
|---|---|---|---|---|
| 局部变量 | 块内 | 函数调用期间 | 垃圾值 | 栈 |
| 静态局部 | 块内 | 整个程序 | 0 | 数据段/BSS |
| 全局变量 | 文件 | 整个程序 | 0 | 数据段/BSS |
6. 最佳实践
-
尽量使用局部变量
-
限制全局变量的使用
-
用静态局部变量保持函数状态
-
用静态全局变量实现模块封装
-
避免返回局部变量的地址