写《宏常量和常量》这篇,其实源于我昨天在技术群里看到的一个连续翻车现场。一位老哥贴了段C语言代码,数组长度用const修饰的变量定义,编译器直接甩脸“expression must have a constant value”,底下跟了十来条回复,连“用宏啊”和“要加static const啊”都能吵起来。这问题看似基础,但真要掰扯清楚“宏常量和常量到底有啥区别、什么时候该用谁”,还真没人能几句话讲明白。我也见过不少工作三五年的开发,脑子里对这两样东西的理解还是靠背结论,换个语言、换个编译选项就露馅。这篇就把这层窗户纸捅破,试着从原理到底层到实践,把这条线一次捋顺。
1. 一个困扰初学者的编译错误:数组长度为什么不能是"变量"
这个话题最好的切入点,就是那个让我在群里目睹惨案的发生现场。写C语言的朋友十有八九都撞过这堵墙,报错信息连英文带中文五花八门,但核心就一句:表达式必须含有常量值。有些人靠经验把const换成#define或者直接写数字解决了问题,但根本原因是什么,说不清楚的人占大多数。
1.1 从一段报错代码说起
先看这段典型的失败代码。
c复制#include <stdio.h>
const int N = 10;
int main(void) {
int arr[N] = {0}; // 编译报错
printf("size: %d\n", (int)(sizeof(arr) / sizeof(arr[0])));
return 0;
}
我用GCC编译时,报错是 variably modified 'arr' at file scope 或者 array size is not a constant expression。在MSVC环境下,就是热搜词里那句经典的“表达式必须含有常量值”。也就是说,N 被 const 修饰后,看起来是个“常量”,但C语言根本不认它作为数组长度的“常量身份”。这就要追到C语言标准对“常量表达式(constant expression)”的严格定义了。
1.2 编译期常量和运行时常量的分界线
要解释清楚这个错,必须先建立两条至少90%初学者没明确区分过的认知:编译期常量 和运行时常量。
所谓编译期常量,是指在编译阶段就能确定取值、并且编译器能直接在生成的二进制代码里把它当成字面量嵌入的数值。比如 3、'A'、0xFF,以及宏展开后形成的字面量表达式。运行时常量则是在程序运行到某个时刻,虽然值不会被修改(有const保护),但它的值在编译阶段计算不出来——函数形参传入的const变量、结构体成员中的const字段,都是典型场景。
const int N = 10; 在C语言里,N 在大多数编译上下文里被当成一个“不可被当前代码修改的变量”,但它本质上仍然是变量。编译器在优化时可能将其解释成常量,但从语言标准层面,它不具备编译期常量的身份。数组长度在C89/C90和C99的主要实现中,要求必须是编译期常量,所以编译器面对 int arr[N] 时只能皱眉。
1.3 一个鲜为人知的细节:C和C++在这件事上手拉手分道扬镳
这里藏着全篇第一个关键知识点:在C++里,const int N = 10; 在文件作用域默认具有内部链接,且作为整型常量,是编译期常量,可以用作数组长度。但C语言没有这待遇。所以很多靠C++编译器跑C代码的初学者,用同一个写法在.cpp文件里不报错、在.c文件里报错,顿时满头问号。这不是编译器抽风,而是语言标准的分野。
我当年就是从这种“半个名额的糊涂”开始,才真正理解“常量”两个字在不同语言里从来不是一个东西。之后在任何语言里看到“常量”,第一个反应永远是:它到底能不能在编译期确定?这一步想清楚了,后面所有用法都是水到渠成。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 宏常量:预处理阶段的"文本级替换"
聊完了const常量在数组长度上的尴尬,就该回头看看那段解决问题的“宏”是什么来路。宏常量的核心身份,早在编译开始之前就已经定型了——它属于预处理阶段的一等公民。
2.1 宏定义跑在代码编译之前
C/C++的编译流程大致是:预处理 → 编译 → 汇编 → 链接。宏定义(#define)工作在预处理阶段,它做的事情可以用四个字概括:文本替换。编译器看到源代码时,代码里的宏早就被展开成对应的替换文本了。
c复制#define MAX_BUF_SIZE 1024
void do_something(void) {
char buf[MAX_BUF_SIZE]; // 这里实际是 char buf[1024];
}
预处理器把 MAX_BUF_SIZE 直接替换成 1024,编译器拿到的是 char buf[1024];。数组长度是一个字面量,编译器当然没意见。这是宏常量能当数组长度而const常量不能的根本原因——宏在编译前就把自己变成了字面量。
这一点衍生出一个重要特点:宏不参与类型检查。因为它在编译开始前就被替换没了,编译器的类型检查系统根本看不到“宏常量”这个东西的存在。所以宏定义很少写类型,它本质上就是一个词汇层面的别名。
2.2 为什么"带参宏要疯狂加括号"
宏常量最常见的翻车现场就是不加括号。造个简单但真实的例子:
c复制#define WIDTH 10
#define HEIGHT 20
#define AREA WIDTH * HEIGHT
如果代码里写 AREA / 2,你以为是 (10 * 20) / 2 = 100,实际展开是 10 * 20 / 2,第一步变成 10 * 10 = 100——哎,等等,这个结果碰巧对。那就再来一个:
c复制#define X 5
#define Y 2
#define SUM X + Y
代码写 SUM * 3,期望是 (5 + 2) * 3 = 21,实际展开是 5 + 2 * 3 = 11。这就是优先级陷阱。正确写法是所有宏常量都带括号:
c复制#define SUM (X + Y)
带参宏也一样,参数也要每个都套括号,整个表达式也要套括号。
c复制#define SQUARE(x) ((x) * (x))
不这样写,SQUARE(a + 1) 就变成 a + 1 * a + 1,结果乱套。我见过一个同事为了精简宏定义,省了一对括号,结果线上发现某块缓存命中率忽高忽低,查了两天才定位到是一处面积计算的宏在特定参数组合下数值膨胀。从那以后我们团队的代码规范直接写死:所有宏常量,不管简单复杂,最终结果先套一层小括号,参数每个都套括号,一个都不能少。
2.3 宏常量的副作用失控
括号问题属于看得见的坑,副作用问题则是更深层的暗雷。宏既然是文本替换,那对“表达式参数”就会产生反复求值的副作用。
c复制#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 3;
int y = 5;
int z = MAX(x++, y++);
期望最大那个数自增一次,实际上 x++ 或 y++ 会被求值不止一次。不用纠结具体展开结果,只要记住:带着自增、自减、函数调用这类有副作用的表达式传给宏,行为就是未定义的。这在C语言面试题里几乎成了保留项目。
宏常量的替换能力甚至能被用来干些非常危险的事:宏定义函数名、宏定义类名、宏改变关键字。有些老代码里能看到 #define local static 这种表达,这属于滥用宏定义的范畴,除了增加理解成本几乎没任何好处。我自己维护老模块的时候,每次碰到满屏乱飞的宏,内心都祈祷它们能带文档。
3. const常量:编译期的"类型化绑定"
宏在预处理阶段“处理自己”,const常量则在编译阶段得到了编译器的正式承认。它跟宏完全是两种物种:const常量是一个具备类型、具备存储位置的真正实体。
3.1 const修饰变量后的底层变化
const int N = 10; 在C语言里做了两件事:第一,它让 N 在作用域内不可通过该变量名写值;第二,它获得了类型系统中的身份——const int 类型的对象。
重点来了:const保护的是“对变量的访问方式”,不保证变量一定会被放进只读存储区。Linux下C程序里 const int N = 10;,被修改的情况包括:
- 通过指针绕过const修饰直接改内存(这是未定义行为,但跑起来不一定立刻崩)
- 取地址后转成非const指针,再写值(编译带警告,往往能写进去,因为数据段不一定只读)
真正被放进只读段(.rodata)的,通常是字符串字面量、用了 __attribute__((section(".rodata"))) 之类的强制安排、以及在C++里明确声明的 constexpr 变量。
这个知识点很多博客带过但没讲透:const的本质是“语义约束”+“编译器辅助”,不是“物理只读”。你在代码里遵守了const约束,编译器就帮你查漏;你要是硬不遵守,编译器管不住你的野路子访问。
3.2 为什么const能通过指针修改
很多初学者会钻牛角尖:const变量到底能不能改?直接修改会被编译器拦住:
c复制const int a = 10;
a = 20; // 编译报错
但是用指针绕过就直接进了一个模糊地带:
c复制const int a = 10;
int *p = (int *)&a;
*p = 20;
printf("%d\n", a); // 结果不确定,可能改掉了,可能没改掉
在不同优化级别下,编译器可能把 a 直接优化成立刻数,因为你的代码“保证”了它不会变,所以 printf 里其实是 printf("%d\n", 10)。而另一个位置,你通过指针改了那块内存,输出便可能完全不一致。这在嵌入式场景尤其危险——某些平台把 .rodata 放在只读flash的映射区,写进去直接触发异常中断。我至今记得一位同事用指针硬改了const修饰的版本号配置变量,结果现场设备烧录后,在某个特定地址跳变异常,排查了一圈结果发现是自己在代码里种下的雷。
3.3 C和C++中const的差别,以及constexpr的补充
前面已经提过C++对待 const int N = 10; 是可以作为编译期常量的,但C++里还有更严谨的 constexpr 关键字,专门用来声明编译期常量。它比const更严格:constexpr 变量必须在编译期就能算出值,且这个值可以被用在模板参数、数组长度、case标签等场景。const 则保留“运行时常量”的语义空间。
cpp复制constexpr int MAX_SIZE = 100;
int arr[MAX_SIZE]; // OK
在C++里 constexpr 就相当于“加强版const”,它的引入也解决了C++里 const 在不同上下文语义复杂度高的问题。如果你用C++写代码,新代码里需要编译期常量,优先 constexpr,const 留着表达“运行时不可修改的引用绑定”。
4. 不同语言中的"常量"各有各的规矩
跨出C/C++,其他语言对“常量”的概念进一步分化。这也是为什么程序员之间交流时,永远要对“常量”这个词保持警惕——每个语言的常量,背后都有一套自己的语义。这一节结合热搜词里几个典型场景来展开。
4.1 Java的final与字符串常量池
热搜词里有一个是“java类存字符串常量”。这背后是Java里的 final 关键字和“字符串常量池”的概念。
Java里声明常量通常用:
java复制public static final String DEFAULT_NAME = "unknown";
在Java中,final 表示“引用不可变”(引用指向的对象内容可能可变),static final 组合起来近似其他语言的常量。但字符串不同:字符串常量因为使用频繁,Java内置了字符串常量池机制。当代码里出现字符串字面量时,如果池里已有相同内容的字符串,直接复用,不新建对象。
java复制String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true,同一个池内对象
注意,这是字面量的情况。如果用 new String("hello"),则会在堆上新建对象,池里即使有也不直接引用。面试题里最爱问 == 和 equals 在这个场景下的差异,原因就是字符串常量池的存在。搜这个词的人,大概率是碰到了String对象比较的困惑。记住结论:字面量字符串能进池,new出来的String默认不进池。
Java还有一个编译期常量的概念:被 static final 修饰的原始类型和String类型变量,如果初始化是常量表达式,会被编译器内联。也就是说使用该常量的代码,在编译期就直接替换成了字面量。这和C语言宏的最终效果有点像了,但是机制完全不同——Java是在编译后期做常量折叠,不是预处理文本替换。
4.2 MATLAB里把字符串转成常量的别扭之处
“转换为住字符串常量+matlab”这个热搜词也很有意思——应该是“主字符串常量”,可能是语音输入或者拼音输入导致的错字。MATLAB里用户经常会写:
matlab复制path = 'C:\Users\Temp';
当需要给某个函数固定传参时,你会希望像C那样定义一个“宏常量”或者“const变量”。但MATLAB没有预处理器,也没有严格的const语义。通常的做法是定义一个script或function返回字符串,或者用enumeration定义枚举成员,再或者直接定义成结构体字段:
matlab复制CONFIG.TEMP_DIR = 'C:\Users\Temp';
而热搜词里“转换为字符串常量”,很可能是用户想把一个数字或变量转换成字符串常量,用到 char、string、num2str 等函数。MATLAB从R2016b开始引入了 string 类型,它和 char 数组不同,更接近“字符串对象”的概念。如果你要做配置常量,推荐直接用到 string 类型配合 arguments 校验,比纯char数组更可控。但说句实话,MATLAB本身不是做大型工程的最佳选择,对“常量”的约束方式也比较松散,这里不打算展开太深,知道这几点对日常跑脚本就够用了。
4.3 系统路径常量这类"环境常量"的正确打开方式
热搜词里的“win10 临时目录常量”就更贴近实际开发了。Windows环境下,临时目录路径并不是一个固定的字符串常量,它会随着用户环境变量变化,比如 %TEMP% 在不同用户名下指向不同位置。你在代码里硬编码:
c复制const char *temp_dir = "C:\\Users\\johndoe\\AppData\\Local\\Temp";
大错特错。不仅换用户就崩,而且Windows目录还可以被用户改到其他盘。正确做法是运行时获取:
- C/C++:调用
GetTempPathAPI,或者读环境变量TEMP/TMP - Java:
System.getProperty("java.io.tmpdir") - Python:
tempfile.gettempdir()
这就是“环境常量”和“字面常量”的关键差异。硬编码的常量一旦涉及环境相关、平台相关、用户相关,就失去了“恒定”的前提。这种常量应该是运行时从系统接口拿到的,而不是写在代码里。我见过有人为了省事,把临时目录写死在配置文件里,上线之后运维同事要一台一台改文件,纯属给自己挖坑。
5. 实际项目中常量的选型经验
概念讲完,回到最实用的问题:项目里写代码,到底怎么选?这一节给一些可以直接抄作业的建议,也会把前面若干概念的注意点串起来。
5.1 优先问自己三个问题
写一个常量之前,先问:
- 这个值在编译期能确定吗?
- 它需要类型信息吗?
- 它会被多次使用吗?
如果三个答案分别是“能”“需要”“会”,那首选就不是宏,而是 const(C++里可以进一步提到 constexpr)。如果是一个完全跟类型无关的纯数字或文本片段,同时需要被预处理器在其他场景使用(比如 #if 条件编译),就只能用宏。
典型应用场景对比如下表:
| 场景 | 推荐用法 | 理由 |
|---|---|---|
| 数组长度、case标签、模板参数 | 宏或constexpr(C++) | 需要编译期常量 |
| 函数参数保护 | const形参 | 运行时语义约束 |
| 全局配置常量 | const + 头文件声明(C) | 类型安全,可调试 |
| 日志级别枚举 | enum / 枚举类 | 有类型、可打印、可比较 |
| 平台差异开关 | #define/#if | 条件编译只能靠宏 |
这里的核心是:别把宏当普通变量用,也别把const当编译期常量用。两者擅长的事各不同。
5.2 C语言里模拟“真正常量”的几种方案
C语言在“编译期常量”这件事上确实不够优雅。除了宏,还有几种可靠的模拟方案。
枚举类型(enum)是一个很好的选择,尤其是一组相关的整数常量。比如:
c复制typedef enum {
LOG_LEVEL_DEBUG = 0,
LOG_LEVEL_INFO,
LOG_LEVEL_WARN,
LOG_LEVEL_ERROR
} log_level_t;
这在调试器里能看到符号名,比宏的可调试性好太多。宏在调试器里根本不存在,断点看变量时只能看到展开后的字面量。如果你维护的代码里需要输出日志等级,用枚举能少很多心智负担。
另外还有C23标准刚引入的 constexpr 支持,这是对C语言一个大补强。C23里可以写:
c复制constexpr int MAX_LEN = 100;
虽然目前主流编译器对C23的支持还在路上,但长远看,C语言也终于有了正式的编译期常量语法。如果你手头的编译器支持C23,可以逐步从宏迁移到constexpr。
5.3 命名规范与维护上的血泪建议
最后一点,也最容易被忽略:常量的命名规范和维护纪律。我现在给自己和团队定了几条规则:
- 宏常量全大写加下划线,比如
BUFFER_SIZE_MAX,防止和普通变量混淆。 - const常量用驼峰或者全大写加前缀,C里面常写成
kMaxBufferSize(Google风格)或者MAX_BUFFER_SIZE,看团队规范。 - 不要用宏定义函数。用内联函数或者模板函数,否则排查问题的时候,调试器里看到的是一团展开后的表达式,不是函数名。定位缺陷时极其痛苦。
- 不要用宏改变语言语法,例如
#define IF if这种自作聪明,一旦团队出现第二个使用者,代码就变成了解密游戏。 - 一个常量值不要两处定义。要么集中在一个头文件或配置文件里,要么通过接口获取,否则某天只改了一处,另一处忘了,就等着玄学bug上位吧。
我见过一个老模块里,同一个超时时间分别用宏和const各定义了一个,宏定义的值被改过一次,const的没改,结果系统出现“部分请求超时立刻失败、部分拖到两倍时间才失败”的诡异现象。抓了大半天才追到是宏与const的八字不合。
6. 跨文件共享常量时最容易踩的坑
章节最后单独拎一个高发雷区出来讲讲,因为这是我在实际项目里见过最多的一种问题:在头文件里定义宏常量和const常量,然后在多个 .c 文件里包含同一个头文件。这里面的陷阱,值得单独开一节。
6.1 宏跨文件随便用,const跨文件却要注意链接
宏没有存储和链接的概念。你只要在头文件里 #define,任何包含该头文件的源文件都能直接用,展开后各编译单元各用各的字面量,完全无冲突。所以宏在跨文件共享上非常自由。
const常量就不同了。C语言的const变量默认是外部链接(C++里文件作用域的const默认是内部链接,这是另一个差异点)。在头文件里写:
c复制const int GLOBAL_COUNT = 100;
然后两个 .c 文件都 #include 它,链接阶段可能报重复定义。原因在于每个包含它的源文件都定义了同一个全局符号。解决办法是把定义和声明分开:头文件只写 extern const int GLOBAL_COUNT;,某个 .c 文件里写 const int GLOBAL_COUNT = 100;。这样所有文件共享一个常量实体。
C++为了避免这个问题,默认把文件作用域的const设为内部链接,每个包含它的源文件都得到一份独立副本,不会链接冲突,代价是内存中可能有多个副本。C++17之后,还提供了 inline 变量来定义头文件内的共享常量:
cpp复制inline constexpr int GLOBAL_COUNT = 100;
这个设计非常实用,建议C++17及以上项目直接用。
6.2 枚举和字符串常量跨文件注意什么
枚举跨文件共享很简单,放头文件里,各源文件包含即可,不会产生存储冲突。字符串常量的跨文件共享,C语言里一般用宏或 extern const char[] 的写法:
c复制// config.h
extern const char APP_VERSION[];
// config.c
const char APP_VERSION[] = "v2.3.0";
如果在头文件里直接写 const char *APP_VERSION = "v2.3.0";,每个包含此头文件的源文件都会生成一个指向字符串字面量的指针,跨文件比较地址(而不是内容)可能不相等。这就是字符串常量场景里又一个经典暗坑。你要比较字符串,老老实实用 strcmp,别拿 == 比指针。
6.3 从踩坑到定规范:我的个人体会
踩过这么多坑之后,我自己的习惯就是:在一个项目刚起步时就把常量的使用方式定成规范,写进项目的CONTRIBUTING文档里:
- 能用枚举解决的问题,不要用宏。
- 能用constexpr/constexpr函数解决的问题,不要用宏。
- 凡是跨文件共享的编译期字符串,用宏或extern const char数组,二选一,全项目统一。
- 任何常量定义都要配注释,说明为什么是这么个值,可以从哪里修改。
配注释这点,听起来简单,实践里少有人做。几个月后回去改代码,看到一个陌生的数字常量,只能靠猜。宏常量和const常量最大的威力从来不是省几个字母,而是让代码里的“魔法数字”有了名字和上下文。为了这个,多写两行注释完全值回票价。
7. 组合使用宏和const的进阶场景
写到这里,很多人可能会觉得宏和const是非黑即白的关系。实际工程里,它们经常组合出现,通过取长补短解决更复杂的问题。
7.1 用宏控制const常量的定义
一个典型的场景:同一个常量在不同编译配置下取值不同。例如调试模式输出更详细日志,发布模式直接关闭:
c复制#ifdef DEBUG_MODE
#define LOG_LIMIT 500
#else
#define LOG_LIMIT 50
#endif
static const int log_limit = LOG_LIMIT;
这里宏负责在编译期切换配置,const负责给运行时提供一个带类型的、可调试的量。这种组合在模块化开发里非常常见。你用宏控制“代码走哪条路”,用const给代码编译期确定的值,各司其职。
7.2 宏常量和const常量配合的模板技巧
C++里还可以更进一步,用宏定义常量配合模板参数,实现同一份逻辑针对不同配置实例化:
cpp复制#define BUFFER_SIZE_SMALL (64)
#define BUFFER_SIZE_LARGE (1024)
template<int Size>
class Buffer {
char data_[Size];
public:
int size() const { return Size; }
};
Buffer<BUFFER_SIZE_SMALL> small_buffer;
Buffer<BUFFER_SIZE_LARGE> large_buffer;
这里宏在预处理阶段就已经把模板参数替换为整数,编译器得到的是 Buffer<64> 和 Buffer<1024> 两个独立类型。如果你改用const int变量(非constexpr),模板匹配就可能失败。这就是编译期常量的力量在模板场景下的具体体现。
7.3 宏常量做“开关”,const常量做“数值”
最后再给一个通用原则,方便记忆:宏擅长当开关,const擅长当数值。 当你想让一段代码在某种平台、某种模式下生效或失效,用 #ifdef / #if 加宏;当你想给某个逻辑定一个取值且有类型约束,用const或constexpr。宏和const不是pk关系,而是协作关系。理解这层,你写出来的代码会明显比只会“一键替换”的人更成熟。
8. 宏常量和常量相关的调试手段与工具
讲到工程实践,就绕不开调试。这一节聊点实在的:怎么查宏展开,怎么在调试器里看常量的值,以及一些实用的编译器选项。
8.1 让编译器帮你查看宏展开
GCC提供的 -E 选项非常实用。你可以让编辑器停在预处理阶段,把宏展开成纯文本看看:
bash复制gcc -E -P -dD source.c
-E 表示只做预处理;-P 抑制行号标记,以便阅读;-dD 展示所有宏定义。当你怀疑某个宏展开有问题,用这条命令输出到文件,慢慢排查。我排查那种“宏变量加上一堆括号仍然不对”的诡异问题时,这招基本百试百灵。
8.2 在调试器里查看宏和常量
宏是不能在调试器里直接查看的,因为它已经不存在于编译产物中。你只能看它展开后的结果。const常量却能在调试器里显示名字和值,在GDB里执行 p N 就能看到 N = 10,这也是const比宏“可调试”的重要原因。
如果你在调试时想临时修改const的值,一般做不到(编译期优化的原因),但你可以看到它的值并判断逻辑分支。调试大规模代码时,有个好名字的const常量,简直比日志还直观。
8.3 编译器警告选项与代码扫描
编译器其实有一颗关心你的心,就看你怎么唤醒它。
- GCC/Clang:
-Wall -Wextra -Wconversion -Wshadow - MSVC:
/W4或/Wall - 开启静态分析:
clang-tidy、cppcheck
这些工具能帮你发现宏展开错误、常量类型不匹配、隐式类型转换等隐患。很多“宏和const常量混用”引发的bug,其实在编译期就有蛛丝马迹,只是警告没开。我现在的习惯是:新项目从一开始就把警告视为错误(-Werror),宁可多花时间修警告,也绝不带着警告上线。
9. 最后再分享一个小技巧
技术框架都讲完了,最后分享一个我这些年写代码很受用的小习惯:常量集中放置,旁边写清来源。
具体来说,我会在模块头文件里专门开一块区域,集中放置所有宏常量和const常量,并且每个常量边上用一行注释说明“这个值从哪来、为什么是这个数、改动它会影响什么”。比如:
c复制/* 数据帧最大长度:协议规定 1500 字节,预留 32 字节扩展 */
#define FRAME_MAX_LEN (1532)
/* 接收超时时间:该值根据弱网环境重传间隔调整,一般不小于 3000ms */
static const int RX_TIMEOUT_MS = 3000;
这样做的好处,是当某个数值在系统里反复出现时,你只需要处理一个文件,而不是去几十个文件里大海捞针。真正把常量的“常数”属性变成项目里的“定数”属性——既稳定又清晰。这个经验和选择宏还是const本身无关,但能决定你写出来的代码在三个月后还看不看得懂。
宏常量和常量这个问题,扯起来可以写成一本小册子,但落到日常开发,核心还是那几句:先搞清楚编译期还是运行期,再选宏还是const,最后配合合理的命名、注释和调试习惯。搞懂这一点,你写出的代码至少能少挨几次线上故障的毒打。
