写代码这些年,见过太多初学者在“常量、变量、表达式”这三个看似基础的概念上翻车。有人以为变量就是“存东西的盒子”,有人把常量理解成“不能改的数字”,还有人一遇到“表达式必须含有常量值”这种编译报错就懵了。其实这三个概念是编程语言的地基,地基没打牢,后面学指针、学对象、学框架全是空中楼阁。这篇内容就围绕这三个词,把我踩过的坑、总结出的底层规律、以及实际调试时的排查思路都摊开讲一遍,希望能帮你把这层窗户纸捅破。
1. 常量:程序里那些“一开始就定死”的值
1.1 字面量与符号常量,先分清这两个容易混的概念
新手最容易把“常量”理解成“不能变的变量”,这个说法不算错,但不够准确。严格来说,常量分为两类:一类是字面量,直接写在代码里的数字、字符串、布尔值,比如 3.14、"hello"、true;另一类是符号常量,也就是用标识符给一个固定值起的名字,比如 C 语言里的 #define PI 3.1415926,Java 里的 final double PI = 3.14,Python 里约定俗成的 PI = 3.14。
我在给入门者讲这个概念时喜欢打一个比方:字面量就像你直接在纸上写“3.14”,符号常量就像你在手机通讯录里把某个号码存成“老王”,以后你只需要说“给老王打电话”,而不用每次都背那串数字。符号常量的意义在于两点,一是可读性,二是可维护性。你想想,如果一个项目里有几百处用了 3.1415926,突然要改成更精确的 3.1415926535,难道要全局搜索一个个替换?定义了符号常量后,只需要改一行。
还有一类容易被忽略的是枚举常量。比如 Java 里的 enum,C 语言里的 enum 关键字,它们本质上也是常量,而且把相关的常量归成了一组,逻辑上更清晰。举个实际场景,一个订单状态可能有“待支付”“已支付”“已发货”“已完成”,如果用字面量 1、2、3、4 去表示,代码里到处都是魔法数字,过三个月再看根本不知道 2 代表什么。用枚举常量就好多了,代码自己会说话。
1.2 常量的存储:它到底存在内存的哪里
我经常被问到:“常量是不是存在只读区域,不能被修改?”这个问题要分语言和上下文来看。
在 C 语言里,字符串字面量通常被存放在只读数据段(.rodata),所以尝试修改字符串字面量的行为是未定义的,很多编译器会直接报段错误。但用 const 修饰的变量,严格意义上并不是常量,而是“运行时不可变变量”。const int a = 5; 虽然不能通过 a 这个名字去修改它,但如果你拿到 a 的地址再通过指针去改,C 标准说这是未定义行为,但在很多嵌入式环境下真的能改掉。这也是为什么 C 语言里 const 变量不能用来定义数组长度的原因——在编译期,它的值是不确定的,因为编译器可能在运行时才给它赋值。
而在 Java、Python 这类高级语言里,常量通常会放进方法区/常量池或者作为对象的一部分被管理。Java 的 final 修饰的引用类型变量,只是保证引用不能变,但引用的对象内部状态还是可以变的。这一点非常容易踩坑:
java复制final List<String> list = new ArrayList<>();
list.add("a"); // 没问题,引用没变,对象在变
list = new ArrayList<>(); // 编译报错,引用不能重定向
所以你看,常量这个概念的边界完全取决于语言的设计。搞清楚你所用语言的常量机制,比死记“常量不能改”这类一刀切的结论重要得多。
1.3 不同语言里定义常量的姿势对比
我整理了一下主流语言里定义常量的常见写法,方便你对照着看:
| 语言 | 常量定义方式 | 特点 |
|---|---|---|
| C/C++ | #define PI 3.14 或 const double PI = 3.14 |
宏是编译期文本替换,const 是类型安全的只读变量 |
| Java | final double PI = 3.14; 常配合 static |
编译期常量可参与优化,运行时常量则延迟加载 |
| Python | PI = 3.14(约定全大写) |
没有真正的常量关键字,全靠自觉 |
| Jav |
