>> >> >> Reference << << << <<<<<<Ref>>>>>>
Value
Modified: 2025-12-31 | Author:ljf12825

Variable

定义: C中的变量 = 一个有名字的,可寻址的存储区域 + 一组由类型定义的解释规则

拆开来看

维度含义
名字符号(symbol),供程序员与编译器使用
存储区域实实在在的内存(或寄存器)
类型决定:大小,对齐,读写方式,解释方式

变量不是值,值只是变量在某一刻内存中的比特状态

概念含义
标识符(identifier)名字本身,比如x
对象(object)一块存储区域
变量(variable)具名对象(有名字的object)
int x = 10;

变量的本质分类维度

C中变量不是靠一种方式分类的,而是多维度叠加的

按存储期(Storage Duration)分类(核心)

这是理解C的第一关键点

自动存储期(automatic)

void f() {
    int x = 10;
}
特性说明
分配时间进入作用域时
释放时间离开作用域时
典型位置
默认初始化未初始化(垃圾值)

静态存储期(static)

static int x;
int y; 
特性说明
生命周期整个程序运行期间
分配位置.data/.bss
默认初始化初始化 0
作用域看声明位置

包括:全局变量,static局部变量,文件级static

动态存储期(dynamic)

int* p = malloc(sizeof(int));

| 特性 | 说明 | | 分配 | 运行时 | | 释放 | free | | 管理者 | 程序员 | | 存储位置 | 堆 |

p是自动变量,*p指向的对象是动态存储期

按作用域(Scope)分类

块作用域(block scope)

{
    int x;
}

只在{}内可见

文件作用域(file scope)

int g;

从声明点到文件末尾

函数原型作用域

void f(int x);

x只在参数列表中有效

按链接属性(Linkage)分类

这是编译/链接阶段的核心概念

链接属性说明
无链接仅在当前作用域
内部链接当前翻译单元
外部链接跨翻译单元
static int x;
int y;

链接属性决定:符号是否能被Id看见

按类型(Type)分类(值的解释规则)

int x;
float y;
struct S s;
int* p;

类型决定

类型是“解释规则”,不是内存本身

定义 vs 声明

定义(Definition)

分配存储空间

int x;

声明(Declaration)

告诉编译器“它存在”

extern int x;

一个变量只能定义一次,可以声明多次

初始化的本质

自动变量

int x; // 未初始化
// 读取未初始化的自动对象的值是未定义行为

静态变量

static int x; // 自动初始化为0

原因不是“语法规则”,而是:静态存储期变量在程序加载时由运行时系统清零

变量与内存

int x = 10;

本质

地址 A:
0000000A 00 00 00 

C中的变量是:由编译器管理的符号 + 存储期 + 作用域 + 链接属性 + 类型解释规则,本质是对一段内存的而语言级约束

Constant

C语言中没有“真正不可变的变量”这个概念
常量 != 只读内存,常量 != const

在C中,“常量”是一个语义概念,而不是一个统一的实体

C中的常量分类

字面量(Literal Constant)

42
3.14
'a'
"hello"

本质:编译期就确定的值

分类

类型示例存储
整数字面量42, 0xFF通常直接嵌入指令
浮点字面量3.14只读数据段
字符字面量'a'整型常量
字符串字面量"hello"静态存储区(只读)
char *p = "hello";
p[0] = 'H'; // 未定义行为

字符串字面量不是数组变量,而是静态只读对象

#define宏常量

#define N 100 

本质:预处理阶段的纯文本替换

int a[N]; // 等价于 int a[100];

特点:

编译器根本不知道它“是常量”

const修饰的对象

const int x = 10;

const不是“常量”,而是“通过该名字不可修改”
也就是说

const int x = 10;

反例

int y = 20;
const int *p = &y;
*p = 30; // 编译错误
y = 30; // 合法

const约束的是访问路径(lvalue),不是对象本身

enum枚举常量

enum { A = 1, B = 2 };
int arr[A]; // 合法

在很多底层代码中,enum被当作类型安全的宏常量

常量和变量在内存中的真实样子

典型内存布局

.text 代码
.rodata 字符串字面量 / const 对象(可能)
.data 已初始化全局变量
.bss 未初始化全局变量
.stack 自动变量
.heap 动态分配

C标准并不规定const一定放在只读区,这是编译器行为,不是语言保证

初始化 vs 赋值

int x = 10; // 初始化
x = 20; // 赋值
const int n = 10;
int arr[n]; // 在C中不一定合法,C90/C99不允许,C99VLA或C11开启VLA允许

原因

GCC扩展可能允许,但这不是标准C

编译期 vs 运行期

编译期行为

严格说,编译期 != 只有compiler,它是一个阶段链

1. 预处理期(Preprocessing)

#define N 10 
#include <stdio.h> 

发生的事情:

这一阶段

2. 编译期(狭义的compiler)

编译器做的事

int x = 3 * 4 + 5; // 编译期算成17 
enum { N = 16 };
int a[N]; // OK 

const int n = 16;
int a[n]; // 标准C不行,n是运行期对象,不是编译期常量

3. 链接期(Linking)

extern int g;

此时

运行期

运行期开始于

程序被 OS 加载
main() 开始执行

运行期发生的事情

总结

永远问三个问题

  1. 这东西有没有对象
  2. 它的值是不是在编译时就确定
  3. 是否需要运行时指令来获得

只要第3个是YES,那就是运行期

编译期决定“形状”,运行期决定“状态”

问题编译期运行期
是否有 CPU 指令在跑
是否有内存地址部分(符号)全部
是否能访问变量值
是否能调用函数
是否能分配栈
是否能决定数组大小✅(需常量表达式)
是否能优化代码

从汇编角度的一句判断法:只要需要生成“运行时指令去算”的,就是运行期

int f(int n) {
    int a[n]; // 运行期分配
}

编译器必须生成

编译器、编辑器、语言语义、程序分析边界

编辑器/IDE可以在编辑期发现一部分编译器必然错误,但原则上无法在编辑期可靠地发现依赖运行期状态的错误\

为什么编辑器能报编译期错误

编译器并不是猜,而是在调用编译器能力
现代编辑器(VS Code, CLion, Vim + clangd)做的事情是

例如

int x = "abc"; // 类型错误

这是一个不依赖输入,不依赖环境,对所有执行路径都成立的错误
这种错误在任何运行期都会失败 -> 可在编辑期100%确定

这类错误的共同特征

因此,编辑器可以提前报错,本质是静态分析

为什么编辑器不能报出运行期错误

典型例子

int f(int x) {
    return 10 / x;
}

问题

编辑器在编辑器无法知道

这是一个理论上的不可能性,这里不是“技术不够好”,而是计算理论极限
核心问题:停机问题(Halting Problem):不存在一个程序,能够对任意程序,判断它是否在运行时发生错误

运行期错误本质上是

这些都属于不可完全静态判定的问题

为什么有时编辑器好像能发现运行期问题

编辑器有时候会提示空指针、越界等行为,这是因为,编辑器做的是保守静态分析
例如

int *p = NULL;
*p = 10;

这是100%必然的运行期错误,不需要输入,不存在分支,所有路径必崩,因此可以在编辑期报错

但一旦引入条件,就不再是”必然“

if (p != NULL)
    *p = 10;

是否安全取决于p的来源,取决于执行路径;编辑器此时只能给warning或保持沉默

编辑器/编译器的”能力边界“

能做到的(静态、确定性)

做不到的(动态、路径相关)

这也就是为什么需要”运行期工具“,既然编辑期/编译期有理论极限,工程上怎么解决

  1. 运行期检测(动态分析)

    • AddressSanitizer
    • Valgrind
    • ThreadSanitizer

    它们的特点:需要执行,基于真实路径,有性能开销(插桩)

  2. 类型系统作为”前移防线“ 例如const, restrict,不可空指针(其他语言),能在编译期消灭一类运行期错误