1. 从一道“太简单”的题目说起:4-3到底在考什么
很多初学者第一次接触C语言,写的多半是类似这样的代码:
#include <stdio.h> int main(void) { printf("%d\n", 4 - 3); return 0; }运行结果毫无疑问是1。就这么一道连小学算术都算不上的题目,有什么好“完美演绎”的?
说实话,我第一次看到“C语言完美演绎4-3”这个题目时也愣了一下。但作为一个常年写C、也带过不少新人的从业者,我很快就意识到:这根本不是一道计算题,而是一道“你对C语言理解有多深”的试金石。4-3这个表达式本身毫无难度,难的是你能围绕它展开多少层思考。
在我面试过的候选人里,有人能对着“4-3”聊出运算符优先级、整型提升、字面量类型、求值顺序、甚至UB(未定义行为);也有人只会说“就是1啊”。差距就在这里。所以这篇博文我想以“4-3”为线索,从最基本的表达式求值一路拆到指针、数组、位运算、结构体,把这道题的“完美演绎”拆给你看。适合刚学完C语法、想夯实基础的初学者,也适合准备面试、想系统梳理C语言知识体系的人。
2. 表达式求值的底层逻辑:为什么4-3不是“4减3”那么简单
2.1 运算符优先级与结合性:先搞清楚谁先动
C语言里,4 - 3中的减号是二元减法运算符,优先级在运算符表中处于中低位置——低于括号、一元运算符、乘除取模,高于赋值和逗号。但优先级只是第一层,还要看结合性。减法的结合性是从左到右,所以4 - 3 - 2会被解析成(4 - 3) - 2,结果是-1,而不是4 - (3 - 2)的3。
这一点很多人觉得“我当然知道”,但换到复杂表达式里就容易翻车。比如:
int a = 10, b = 20, c = 30; int result = a - b - c; // 实际是 (a - b) - c = -40如果某人误以为减法满足结合律,把它看成a - (b - c),那结果就是20,整整差了60。优先级和结合性共同决定了一个表达式的解析树形态,而不是简单“从左往右算”一句话能概括的。用解析树的视角来看,4 - 3是一棵只有两个叶子节点的二叉树,根节点是减法运算。真正复杂的表达式,不过是这棵树的深层次递归组合而已。
2.2 字面量的类型:4和3到底是什么类型
还有一个几乎没人注意的细节:4和3这两个数字在C语言里是整型字面量(integer constant),默认类型是int。这意味着4 - 3实际上做的是两个int类型的减法运算,结果也是int。
如果我把题目改一下,变成4.0 - 3,情况就完全不同了。这里涉及隐式类型转换(usual arithmetic conversions):4.0是double类型,整数3会被提升为double,然后做double减法,结果是1.0,一个double值。用printf打印时:
printf("%f\n", 4.0 - 3); // 输出 1.000000 printf("%d\n", 4 - 3); // 输出 1%d和%f用错就会产生未定义行为,这又是一个隐藏考点。
理解了字面量类型之后,你就知道“4-3”这个表达式背后其实藏着一整套类型系统规则:整数提升、寻常算术转换、赋值转换。关于整型提升,有个非常经典的例子是char与int的运算:
char c = 100; char d = 50; int result = c + d; // 两个char先提升为int,再做加法,结果是150如果没有整型提升,char + char极可能溢出(char通常是8位,最大值127或255)。正是因为C语言在运算前会把char、short等窄类型提升为int,c + d才能安全得到150。而4 - 3这种int对int的运算,反而不需要任何提升——因为它们本来就是标准int。
2.3 四则运算与CPU的ALU:计算机真的“算”减法吗
再说一个层面的东西:从计算机组成原理的角度来看,CPU的算术逻辑单元(ALU)压根不“做”减法。减法是转换成补码加法来完成的。
4 - 3在机器层面会被翻译成4 + (-3)。假设我们用8位二进制表示:
- 4的补码:
0000 0100 - -3的补码:
1111 1101 - 两者相加:
0000 0100 + 1111 1101 = 1 0000 0001,截断最高位进位后得到0000 0001,也就是1。
这就是为什么C语言里减法运算的效率“看起来”和加法一样——因为它本来就是加法。这个知识点放在“4-3”这道题里特别有意思:你以为CPU在做减法,其实它只是把减数取反加一,再做了一次加法。很多人在学习原码、反码、补码时觉得枯燥,但只要把它映射到4-3这种具体例子上,立刻就有实感了。
3. 从整数字面量到ASCII码:4-3还可以是字符运算
3.1 数字字符的ASCII差值:'4' - '3'等于多少
现在我们把题目变一下形:如果写成'4' - '3',结果还是1吗?
答案是:至少从数值上看,仍然是1,但理由完全变了。
在C语言中,字符字面量'4'和'3'是int类型(注意,C++里它们是char,但C里是int),它们的值分别是ASCII码表中的十进制值:'4'对应52,'3'对应51。所以'4' - '3'等于52 - 51,也就是1。
这个例子特别能说明一个问题:字符在C语言里本质上就是整数。你可以在字符和整数之间做算术运算,甚至可以直接遍历字母表:
for (char ch = 'A'; ch <= 'Z'; ch++) { printf("%c ", ch); }因为'A'是65,'Z'是90,这个循环就是在整数65到90之间遍历,只不过用%c打印时被解释成了字符。
3.2 用字符差值实现大小写转换和数字提取
既然'4' - '3' = 1,那反过来,'0' + digit就可以把数字0-9转成对应的字符。比如:
int digit = 7; char ch = '0' + digit; // '7'这在手写itoa(整数转字符串)时非常常用。我当年自己实现十进制转字符串时,最核心的一行就是这个:
char buffer[16]; int num = 12345, pos = 15; buffer[pos--] = '\0'; do { buffer[pos--] = '0' + (num % 10); num /= 10; } while (num > 0); printf("%s\n", &buffer[pos + 1]);用到的就是“字符0的ASCII值 + 个位数字 = 对应数字字符”的原理。同样地,判断一个字符是不是数字字符,也可以写成ch >= '0' && ch <= '9',其本质是判断ch的ASCII码是否落在48到57这个区间内。
3.3 再进一步:位运算视角下的4-3
如果说字符差值还算常规,那下面这个角度估计不少人是第一次见:用位运算来实现4-3。
我们已经知道减法在硬件层面是加法,那么用C语言模拟这个逻辑,就可以手写一个减法函数:
int subtract(int a, int b) { // 实现 a - b while (b != 0) { int borrow = (~a) & b; // 找出需要借位的位 a = a ^ b; // 异或得到不考虑借位的减法结果 b = borrow << 1; // 借位左移,继续循环 } return a; }验证一下:subtract(4, 3)返回1。
这个版本的思路和加法器里的减法器如出一辙。如果你把4 - 3放到这个函数里拆解:
- 第一轮:
borrow = (~4) & 3 = 3,a = 4 ^ 3 = 7,b = 3 << 1 = 6; - 第二轮:
borrow = (~7) & 6 = 0,a = 7 ^ 6 = 1,b = 0; - 循环结束,返回1。
位运算版本的减法可能在实际生产中不常用——因为编译器早就帮你优化好了,但这绝对是面试时展示功底的加分项。它能证明你理解“减法是借位与异或的组合”,而不是只会用-号。
4. 从常量到内存布局:把4-3放进数组、指针和结构体
4.1 数组维度里的4-3:指针运算的直观案例
把4-3用在数组下标里,是C语言里非常常见的操作。比如:
int arr[10] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90}; int *p = &arr[4]; int *q = &arr[3]; int diff = p - q; // 结果是1,而不是4-3的“字节差”指针减法返回的是两个指针之间相差的元素个数,不是地址值直接相减。arr[4]和arr[3]在内存中相隔4个字节(假设int为4字节),但p - q的结果是1。
这个特性让指针在数组遍历中特别好用。比如:
int *start = arr; int *end = arr + 10; ptrdiff_t count = end - start; // 10用ptrdiff_t类型接收差值,可以安全处理任意大小的连续内存区域。我早年写网络协议解析时,经常用“尾部指针减头部指针”来计算缓冲区里已经积压了多少字节的待处理数据,比维护一个独立的计数值变量要少绕一层——因为缓冲区本身就是数组,长度和位置就在指针差里。
4.2 结构体成员偏移量:offsetof与4-3的关系
另一个跟4-3相关的冷门知识点,是结构体成员的偏移量计算(offsetof)。比如:
#include <stddef.h> struct Sample { char a; int b; char c; }; int main(void) { printf("%zu\n", offsetof(struct Sample, b)); printf("%zu\n", offsetof(struct Sample, c)); return 0; }在我的机器上,offsetof(struct Sample, b)是4,offsetof(struct Sample, c)是8。换句话说,成员c的偏移减成员b的偏移等于4,这里又出现了一次“4-3”式的差值——但它跟加减法没任何关系,纯粹是内存对齐(memory alignment)的结果。
这么说可能有点绕,我解释一下。结构体里char a占1字节,紧接着的int b必须按4字节对齐,所以编译器会在a后面填充3个空洞字节,把b放到偏移4的位置。c是char,放在偏移8处。这里的“4”是对齐要求导致的,不是算术出来的。
很多新手写嵌入式程序时踩过结构体大小“出乎意料”的坑,就是因为没意识到填充字节的存在。想验证的话,用printf("%zu\n", sizeof(struct Sample)),你会发现结果往往不是1+4+1=6,而是12——尾部还会再填充3个字节,让整个结构体大小成为最大成员对齐数的整数倍。
4.3 二维数组里的“4-3”:多维下标究竟是啥
二维数组也是理解指针和内存布局的经典场景。定义一个arr[4][3],你想访问第3行第2列的元素,写作arr[2][1]。这里就有一个很自然的理解方式:这个数组的总元素数是4×3=12个,而下标是以“行优先”的方式连续铺开的。
int matrix[4][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12} }; int x = matrix[2][1]; // 8从内存视角看,matrix[2][1]对应的是线性化后的第2*3+1=7个元素(从0开始计数),也就是8。这就是为什么你完全可以把二维数组“降维”成一维数组来处理:
int *ptr = &matrix[0][0]; int v = *(ptr + 2 * 3 + 1); // 等价于 matrix[2][1]C语言里没有任何真正的多维数组,所谓二维数组,本质上是“数组的数组”,内层每个数组大小固定为3。表达式matrix[2]的类型其实是int [3],它在参与运算时会退化成int*指针。理解了这个,你就能解释为什么matrix和&matrix[0]虽然数值相同,但类型完全不同、做指针运算时步长也完全不同。
4.4 结构体数组配合字符串:4-3在数据查找里的小应用
再说一个实际工程的例子。假设你有一张状态码表,需要根据状态码找到对应的描述。你可能会下意识用一个switch-case把每个状态码都写死,但更可维护的做法是定义一张结构体数组:
struct StatusEntry { int code; const char *desc; }; struct StatusEntry status_table[] = { {200, "OK"}, {404, "Not Found"}, {500, "Internal Server Error"}, {503, "Service Unavailable"}, }; int table_len = sizeof(status_table) / sizeof(status_table[0]);这段代码里看起来跟4-3没关系?其实它隐含了一个很经典的比值运算:sizeof(status_table)是整个数组占用的总字节数,sizeof(status_table[0])是单个元素的大小,两者相除的结果就是元素个数。如果把表长度固定写死成4,哪天新增一条状态码但忘了改长度,就越界了。用sizeof动态估算长度才是稳的。
这个技巧我在解析协议帧、维护配置项列表时用了无数次。凡是“一张表 + 一个for循环”的结构,都值得考虑用这种方式来声明长度。它虽然不会直接让你的代码跑得飞快,但维护性会大幅提升。
5. 把4-3放进“未定义行为”的陷阱里:一个危险的小改动
5.1 求值顺序与副作用:i++里的隐藏地雷
如果我把题目改成一个带副作用的版本,情况就变得危险了:
int i = 4; int r = i++ - 3;这里i++的求值结果是多少?答案是4(后置自增返回自增前的值),所以r = 1。但关键是,i本身变成了5。
如果我再改一版:
int i = 4; int r = i-- + i;这行代码在C语言里属于未定义行为(undefined behavior, UB)。原因很简单:i--和i都读取了i的值,但i--同时还在修改i,而C标准不保证这两次访问的顺序。所以r可能是7(先取4和3),也可能是8(两次都取4),具体结果取决于编译器和优化级别。
“4-3”怎么和UB扯上关系的?这正是它有意思的地方。一个看起来人畜无害的表达式,一旦混入带副作用的运算符,就进入一片谁也说不准的区域。我在面试中经常用这类表达式作为考点——不是真的想考谁,而是想看看候选人会不会被“理所当然”绑架。被问到时,很多人第一反应是“不是8就是7啊”,而正确反应应该是:“这是UB,不能写。”
5.2 编译器优化的隐性影响:-O2可能会改变结果
UB的可怕之处在于:它不只是“结果不确定”,而是整个程序行为都不被标准约束。现代编译器在优化时,会假设你的代码没有UB,然后在此前提下做各种变换。一旦UB真的出现,编译器做出的优化可能导致完全超乎预期的表现。
拿上面的例子来说,用某主流编译器在默认优化级别下运行,可能得到7;但开了-O2之后,程序可能直接打印出0,甚至跳过后面的语句。这不是编译器“不讲道理”,而是你的代码已经不属于这个标准管辖的范畴了。
所以我的经验是:写C代码时,尽量把“求值”和“副作用”分开。要么先单独执行自增再参与运算,要么干脆用临时变量:
int i = 4; int old = i; i = i - 1; int r = old + i;虽然多写了两行,但每个人的CPU时间都不差这几行,程序可依赖的确定性才是无价的。
5.3 有没有办法安全检测UB
很遗憾,C语言标准没有提供运行时检测UB的手段。能做的只能靠工具辅助排查:
- 打开编译器警告:比如GCC的
-Wall -Wextra,能拦截一部分常见异常表达式; - 使用运行时消毒器(sanitizer):
-fsanitize=undefined是GCC和Clang都支持的一个参数,它会在运行时监测一些UB模式,比如有符号整数溢出、空指针解引用等。
我平时写一些底层小工具时,会顺手带上-fsanitize=undefined跑一遍自测用例。它对性能有一定影响,不建议生产环境开启,但开发调试阶段非常值得用。用它对“4-3”这种小代码做火力侦察,可能什么都没检测出来,但一旦代码里埋了雷,它往往能帮你精准定位到出问题的表达式和那一行源码。
6. 循环与递归里的4-3:用两种风格求“减法”
6.1 while循环版计数减法
除了直接使用-运算符,我们完全可以通过循环来模拟减法过程。这听起来很傻,但对理解“减法是重复减1”的本质很有帮助,尤其适合刚学循环的人去琢磨。
int subtract_by_loop(int a, int b) { while (b > 0) { a--; b--; } return a; }subtract_by_loop(4, 3)的执行过程是:4先减到3,b变成2;再减到2,b变成1;最后减到1,b变成0。总共循环3次,结果是1。
这个版本只支持正整数减法,而且性能比直接-差得多。但它不是拿来在生产代码里用的,而是用来展示“程序运行的每一步到底发生了什么”。对初学者来说,用断点逐行跟一遍这个函数,比单纯看4-3=1要有体感得多。我在带新人时,经常让他们用这种方式“制造”一个减法,以此强化循环和变量变化的概念。
6.2 递归版减法
循环能做减法,递归当然也能:
int subtract_by_recursion(int a, int b) { if (b == 0) { return a; } return subtract_by_recursion(a - 1, b - 1); }当b递减到0时,a也已经从4递减到了1。递归在这里展示的是一种“分而治之”的思路:把一次减法拆成“减1”这个原子操作,然后递归调用自己。
要注意的是,如果b很大,递归版本可能会栈溢出。比如subtract_by_recursion(1000000, 1),仅仅减了一次,绝大多数平台不会出问题;但如果你写subtract_by_recursion(1000000, 999999),大概率的默认栈空间也撑得住。真正危险的是递归深度达到数十万层级时,栈就会被打爆。所以递归适合用来展示思路,不适合在深度不可控的场景替代循环。
6.3 把“-”换成函数指针
还有一种进阶玩法:用函数指针来体现“运算是可抽象的”这一思想。我们可以把运算函数作为参数传递,让同一个入口支持加减乘除。
int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int compute(int (*op)(int, int), int a, int b) { return op(a, b); } printf("%d\n", compute(sub, 4, 3)); // 1 printf("%d\n", compute(add, 4, 3)); // 7这种方式看起来绕,但在设计可插拔逻辑时非常实用。比如一个简单的计算器程序,不需要写一堆switch-case,只要维护一个函数指针数组就能动态绑定不同的运算逻辑:
int (*ops[])(int, int) = {add, sub, mul}; int result = ops[1](4, 3); // 调用sub这段代码里ops[1]就是sub,对4和3做减法,得到1。“4-3”在这里已经不是一撮写死在代码里的数字,而是变成了一组可以被变量引用的操作。这就是C语言“函数是一等公民”思想的一个最简演示——虽然C里的函数不是完全的一等公民,但函数指针已经足够让我们做很多灵活的设计。
7. 编译器的视角:预处理、编译、汇编到底干了什么
7.1 预处理:4-3有没有被直接替换
很多初学者以为C代码从上到下都是“同一个人”在处理,但实际上整个编译过程分成多个阶段。第一个阶段是预处理,它处理#include、#define、#ifdef这类指令。对于4 - 3这种简单表达式,预处理阶段并不会做任何数值计算——宏展开之后,表达式依旧原封不动地传给编译器。
但如果你写了类似这样的宏:
#define SUB(a, b) ((a) - (b))预处理之后,SUB(4, 3)会变成((4) - (3)),仍然不会被计算成1。这是因为预处理只做文本层面的展开替换,不负责数学运算。如果你在宏里忘了加括号,比如写成#define SUB(a, b) a - b,那SUB(4, 3) * 2展开后就是4 - 3 * 2,结果是-2而不是(4-3)*2=2,这种坑我在工程里见过不止一次。
7.2 汇编语言:减法到底长什么样
预处理之后是编译和汇编。拿int r = 4 - 3;来说,GCC在默认优化级别下生成的汇编代码大概是这样(x86-64架构):
movl $4, -4(%rbp) movl $3, -8(%rbp) movl -4(%rbp), %eax subl -8(%rbp), %eax movl %eax, -12(%rbp)你会发现这里确实出现了sub指令——在未经优化时,编译器不会把4 - 3直接折叠成1。但如果开启-O2,那编译器会把这个表达式直接替换成常量1,连运行时代码都不生成。这就是编译优化的一个简单例子:常量折叠(constant folding)。
用gcc -S命令可以生成汇编文件,自己看一眼就能感受到编译器的工作痕迹。我建议所有学C的人在某个节点都干一次这件事——不是要你成为汇编专家,而是让你意识到“高级语言一行代码”在机器层面可能是好几条指令,也可能只是零条指令(因为被优化掉了)。
7.3 加法的机器码比减法更快吗
既然减法是补码加法,那“加法比减法快”是不是真的?
现代CPU里,整数加法和减法通常都在一个时钟周期内完成,速度几乎一致。真正会影响性能的是乘法、除法这类复杂运算,尤其是除法,往往需要多个周期。所以在这个语境下,你不用担心“把4写成-(-4)再相加”这种无意义微优化。编译器早就帮你处理好了。
8. 从4-3到泛型思维:C语言里的“通用减法”怎么做
8.1 用宏模拟泛型
C语言没有真正意义上的模板或泛型。但用宏可以做出泛型的效果。比如一个“max宏”:
#define MAX(a, b) ((a) > (b) ? (a) : (b))同理,我们也可以做一个“通用减法”?其实不行,因为减法对不同类型语义不一样:int是数值减,指针是地址差,浮点也有浮点的规则。所以宏不能万能化,但可以做“类型专用”的版本:
#define INT_SUB(a, b) ((int)(a) - (int)(b))这种宏在实际代码里要谨慎使用,因为类型暴露在外,稍不留神就出问题。更推荐的是老老实实写函数。
8.2 _Generic:C11的简陋泛型
从C11开始,C语言提供了_Generic关键字,可以根据表达式的类型选择不同的代码分支。举个例子:
#include <stdio.h> #define SUB(a, b) _Generic((a) - (b), \ int: sub_int, \ double: sub_double \ )(a, b) int sub_int(int a, int b) { return a - b; } double sub_double(double a, double b) { return a - b; } int main(void) { printf("%d\n", SUB(4, 3)); // 走 int 版本 printf("%f\n", SUB(4.0, 3.0)); // 走 double 版本 return 0; }SUB(4, 3)会走入sub_int分支,输出1;SUB(4.0, 3.0)走入sub_double分支,输出1.000000。这算是在不引入外部库的前提下,C语言能拿出来的最接近“泛型”的写法了。
不过说实话,_Generic在实际工程中的应用频率并不高。它更多是C语言社区为了弥补“没有函数重载”的一个变通方案。如果你写的是大型C项目,我建议优先考虑用宏或者函数指针来实现灵活性,而不要把_Generic当成万能药。
8.3 通用算法的两个代表:qsort和bsearch
C标准库的qsort和bsearch才是通用算法的真正代表。它们通过void*和函数指针实现了对任意数据类型的排序与查找。
#include <stdio.h> #include <stdlib.h> int compare_int(const void *a, const void *b) { int ia = *(const int *)a; int ib = *(const int *)b; return (ia > ib) - (ia < ib); } int main(void) { int arr[] = {4, 3, 9, 1, 7}; size_t n = sizeof(arr) / sizeof(arr[0]); qsort(arr, n, sizeof(int), compare_int); for (size_t i = 0; i < n; i++) { printf("%d ", arr[i]); } return 0; }这里的compare_int返回正、零、负,用来告诉qsort两个元素的相对顺序。如果你想对结构体按某个字段排序,只需改比较函数即可,排序算法本身一行不用动。这种“数据和操作分离”的设计,我认为才是C语言“通用编程”的精髓。
回到“4-3”这个主题,我要说的是:算法比你的直觉更重要。一个排好序的数组,用二分查找(bsearch)找元素的时间复杂度是O(log n),而线性查找是O(n)。数据量一大,差距就是指数级的。所谓“完美演绎4-3”,不只是做对一道算术题,而是把这种“用合适的结构解决问题”的思想贯穿到代码里。
9. 在工程里怎么用好4-3这种“小东西”:三个落地建议
9.1 把魔法数字换成具名常量
如果你在代码里看到4 - 3这种光秃秃的数字,大概率是坏味道。比如:
int total = buffer_len - HEADER_SIZE; // 假设 HEADER_SIZE=4 int actual = total - 3;如果3是某个协议的标志位长度,请务必给它起个名字:
enum { HEADER_SIZE = 4, FLAG_FIELD_SIZE = 3, }; int total = buffer_len - HEADER_SIZE; int actual = total - FLAG_FIELD_SIZE;这样做的价值不只是“美观”,而是让读代码的人不用去猜“这个3从哪来”。在真实项目里,魔法数字是bug的温床——你把计算逻辑改了一处,但忘了另一处也是同一个“3”,就会出非常隐蔽的错误。
9.2 优先用sizeof而不是硬编码数组长度
前面提到了计算数组长度的经典方法:
size_t len = sizeof(arr) / sizeof(arr[0]);这个“除”的操作跟“4-3”一样简单,但意义重大。硬编码数组长度的问题在于:一旦你改了数组定义,忘了改使用处,就越界访问或漏掉元素。用sizeof自动推导,就少一个“人肉同步”的负担。
有些编译器提供了ARRAY_SIZE这样的宏,本质也是上面的公式:
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))配合范围for(在C++里)或普通for循环(在C里),能大幅减少数组操作的低级错误。
9.3 不要过早优化
很多人在看到“4-3完美演绎”这个标题后,可能会兴奋过头,觉得任何地方都要搞点“高级写法”。但我个人踩过太多次这种坑了——为了“炫技”引入位运算,结果代码可读性暴跌、维护成本升高、运行时性能收益几乎为零。
在工程里,看的清楚比跑的飞快重要得多。除非profiler告诉你某个减法或某个循环确实成了瓶颈,否则就老老实实写最自然的形式:int r = 4 - 3;。优化永远是事后测量出来的,不是事前猜出来的。
10. 写在最后:关于4-3的一点发散思考
说实话,写这篇文章的过程中我一直在想,“完美演绎4-3”到底想表达什么。可能它最初只是一道简单的算术题,但一路拆下来,它涉及了:字面量类型、运算符优先级、ASCII码、补码、位运算、指针运算、结构体内存对齐、未定义行为、循环、递归、函数指针、宏展开、编译优化、泛型方案、工程可维护性。任何一个知识点单独拿出来,都能写一篇长文。
C语言就是这样一门语言:表面语法简单到可以一节课讲完,但深挖下去,没有底。它的一行表达式可能只是几句话,但它连接着编译器、CPU、内存模型和操作系统。初学者容易觉得C“麻烦”,但正是这种“麻烦”,才让你真正理解计算机是怎么工作的。
如果你现在还在学C的起步阶段,我给你的建议很简单:把一个很小的程序,反复拆开来看。就像这道4-3一样,先用最直白的方式写出来,然后问自己:换成字符呢?换成浮点呢?换成指针呢?放进数组里呢?写出一个产生副作用的版本呢?再想想编译器会怎么处理它?汇编长什么样?CPU又是怎么实现减法?一层一层往下追问,你的C语言水平会在这个过程中自然长起来。
别嫌题目小,小题目往往藏着大世界。4减3等于1,从来都不是终点,而是起点。