Value


Variable

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

拆开来看

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

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

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

变量的本质分类维度

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;

类型决定

  • 占用字节数
  • 对齐方式
  • 运算规则
  • 是否可修改
  • 是否可别名(alias)

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

定义 vs 声明

定义(Definition)

分配存储空间

int x;

声明(Declaration)

告诉编译器“它存在”

extern int x;

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

初始化的本质

自动变量

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

静态变量

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

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

变量与内存

int x = 10;

本质

地址 A:
0000000A 00 00 00 
  • x -> 符号 -> 地址A
  • 10 -> 比特模式
  • int -> 如何解释这4个字节

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;
  • x是一个变量
  • 有存储
  • 可能被放在只读区,也可能不

反例

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

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

enum枚举常量

enum { A = 1, B = 2 };
  • A, B是编译期整型常量
  • 不占内存
  • 可用于数组长度、case标签
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允许

原因

  • n不是编译期常量
  • 即使n的值不变,它仍是运行期对象

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

编译期 vs 运行期

  • 编译期:程序还没有变成可执行文件之前,所有“由编译器完成的计算与决策”
  • 运行期:程序已经开始执行,由CPU + 操作系统参与的所有行为

编译期行为

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

1. 预处理期(Preprocessing)

#define N 10 
#include <stdio.h> 

发生的事情:

  • 宏替换
  • 条件编译
  • 头文件展开

这一阶段

  • 没有类型
  • 没有变量
  • 没有作用域
  • 只是文本

2. 编译期(狭义的compiler)

编译器做的事

  • 词法/语法分析
  • 语义分析(类型检查)
  • 常量折叠(const folding)
  • 生成中间表示(IR)
  • 优化
  • 生成目标代码(.o)
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;

此时

  • 才知道g的最终地址
  • 才能确定跨文件符号

运行期

运行期开始于

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

运行期发生的事情

  • 栈帧建立/销毁
  • 变量分配
  • 函数调用
  • 分支跳转
  • 内存读写
  • 系统调用
  • 线程调度

总结

永远问三个问题

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

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

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

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

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

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

编译器必须生成

  • 计算n
  • 调整rsp
  • 恢复栈

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

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

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

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

  • 实时分析源码
  • 运行语法分析 + 语义分析
  • 使用与编译器相同或相似的前端

例如

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

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

这类错误的共同特征

  • 不需要执行程序
  • 不依赖输入
  • 不依赖分支
  • 对所有路径成立

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

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

典型例子

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

问题

  • x == 0 -> 崩溃
  • x != 0 -> 正常

编辑器在编辑器无法知道

  • x是来自哪里
  • 用户输入是什么
  • 走哪条分支

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

运行期错误本质上是

  • 程序行为
  • 状态演化
  • 动态路径

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

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

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

int *p = NULL;
*p = 10;

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

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

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

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

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

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

  • 语法错误
  • 类型错误
  • 必然的未定义行为
  • 明显越界(常量索引)
  • 明显空指针解引用

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

  • 输入相关崩溃
  • 数据竞争(大多数情况)
  • 逻辑错误
  • 时序问题
  • 资源泄漏(完全精确)

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

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

    • AddressSanitizer
    • Valgrind
    • ThreadSanitizer

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

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