ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从“计算长方体体积”吃透C语言入门四大核心概念

从“计算长方体体积”吃透C语言入门四大核心概念 很多C语言教材在第二章附近都会放这样一道题从键盘输入长方体的长、宽、高计算体积并输出。我当年学C语言时觉得例1.2简单得有点无聊照着书敲一遍就翻页了。后来带新人、看别人写的代码才发现这道计算长方体的体积的题其实把C语言入门的四个关键概念全串起来了头文件、变量类型、地址传递、格式控制。更扎心的是有些写了两三年程序的人在这道题上依然会犯scanf忘加、%f和%lf混用的低级错误。这篇博文就围绕这个经典例题展开把每一行代码拆开揉碎。适合刚接触C语言的初学者也适合想帮别人答疑的进阶者甚至可以当作一份C语言基础复习清单。文中的所有代码我都用gcc实测过你直接照着敲就能跑下面开始。1. 几乎所有教材都拿它开场这个例子的真实教学分量1.1 一份能直接运行的参考代码先给出一份完整的实现长这样#include stdio.h int main() { double length, width, height; double volume; printf(请输入长方体的长、宽、高用空格隔开\n); scanf(%lf %lf %lf, length, width, height); volume length * width * height; printf(长方体的体积是%.2f\n, volume); return 0; }如果你的环境已经装好了gcc保存成volume.c然后执行gcc -Wall -Wextra volume.c -o volume ./volume输入3 4 5屏幕上会输出长方体的体积是60.00。到这一步程序就跑通了。1.2 这段代码到底由哪几部分构成很多人会照着敲但敲完不知道自己敲了什么。其实这段代码只有四类零件#include stdio.h把标准输入输出库引进来printf和scanf的原型在这里声明。如果不写这一行编译器根本不认识printf和scanf会报隐式声明警告甚至直接编译失败。int main()程序入口。C语言程序启动后系统会去找main函数从它的第一行开始执行。一对花括号里的语句这是程序真正干活的区域。return 0;把程序正常结束这个状态交还给操作系统。很多初学者觉得头文件和main是死模板背下来就行。我建议换个角度理解程序像一个工厂生产线头文件是原料进货单main是车间入口花括号里是每道工序return 0是质检合格出厂。后面学函数、学模块化编程本质还是在往这个框架里加工序。1.3 顺序执行模型程序不是魔法是指令序列这个例子最容易被忽略的教学价值是它展示了C语言最基本的执行模型从上到下、逐条执行。scanf那一行先执行程序停在这里等用户输入用户敲完回车后数据被读入变量紧接着执行乘法最后把结果打印出来。这个输入→处理→输出的三段式是后面几乎所有程序的骨架。比如文件读写程序本质是从文件输入→处理→输出到屏幕或文件网络程序本质是从socket输入→处理→输出到socket。例1.2虽然只有三行核心逻辑但它是一个可以用来类比所有IO程序的缩略模型。2. 变量声明那一行已经决定程序一半的命运为什么不用int2.1 数据类型的本质给内存定房间规格C语言里声明变量就是在内存里订房间。int相当于订了一间4平米的小房间只能存整数double相当于订了一间8平米的大房间能存小数还能存特别大或特别小的数。为什么会有这种差别因为计算机内存里全是二进制位类型决定了编译器怎么解释这一串01以及给它分配多少空间。同样一段内存用int解释可能是一个数用double解释就是完全不同的数。这就是为什么类型选错程序不会报错但结果会莫名其妙地错。2.2 用int算体积会翻什么车大多数教材在讲这个例题前刚讲过int。所以很多学生会写int length, width, height; int volume;如果长宽高都恰好是整数比如3、4、5结果60是对的。但只要任何一个尺寸带小数比如2.5 3 4问题就来了scanf用%d读取时遇到小数点后面的内容直接读不进去。就算你用scanf(%lf %lf %lf, length, width, height)读进来了赋值给int变量时小数部分会被截断2.5变成2最后体积算出来是24而不是30。更隐蔽的问题是溢出。int能表示的最大整数约21亿。假设长宽高是1万、1万、1万体积是1万亿远超int范围结果是负数或者一个完全没意义的值。我见过有人用int算面积、算价格调了半天发现是溢出这种坑完全可以在声明变量时用double避开。2.3 float和double怎么选float也是浮点型占4字节double占8字节。光看书上那句float精确到6~7位double精确到15~17位新手一般没感觉。我用一个实际对比说明类型字节数典型范围约有效十进制位数适合场景int4-21亿到21亿10位以内整数计数器、编号、循环float4±3.4E386~7位图像、显示、对精度不敏感的场景double8±1.7E30815~17位科学计算、面积体积、金额相关对于长方体体积尺寸可能是1.25 × 3.7 × 8.9float的精度也够用。但我的建议是入门阶段一律用double。原因有两个一是double的精度余量更大不容易积累误差二是printf和scanf在处理double时格式清晰不容易和float的隐式提升搅在一起。等你以后真正遇到性能瓶颈再考虑用float不迟。2.4 隐式类型转换的坑就算声明了double还有一个隐形问题如果计算式中混入intC语言会做隐式类型转换。比如double a 1; double result a / 3 * 3;a是double3会被转换成double这种没问题。但如果是int a 1; double result (a / 3) * 3;结果是0。因为a / 3在括号里是先按int除法执行的1 / 3的整数结果是0然后再转成double乘3最终还是0。这就是经典的整数除法截断坑。在写体积计算时如果某个中间量是int务必留个心眼。3. scanf里的让无数初学者崩溃的内存投递协议3.1 scanf到底在做什么scanf做的事本质上是从键盘输入更准确说是从标准输入缓冲区里按照格式控制符去解析数据再把解析结果写到变量对应的内存地址里。比如scanf(%lf %lf %lf, length, width, height)的意思是从输入流里读取三个double分别放在变量length、width、height的内存位置上。这里有个好玩的地方printf不需要scanf需要。原因是printf只需要读变量的值而scanf需要写进变量。C语言函数传参默认是值传递函数内部拿到的只是变量的副本改副本不影响外部变量。想要在函数内部真正修改外部变量必须把变量的地址传进去这就是的价值。如果一个快递员要送包裹你需要给他门牌号。如果你只告诉他有一个叫length的收件人他找不到地方只能把包裹乱扔甚至扔到邻居家。这就是scanf忘加的后果。3.2 忘加会看到什么忘加的典型代码scanf(%lf %lf %lf, length, width, height);麻烦的是很多编译器只会给你一个警告不会直接报错。程序编译能通过运行时行为完全不可预测可能直接崩溃提示段错误Segmentation fault可能系统恰好把某个随机内存地址当成了目标地址写坏别的东西最磨人的是它平时好像能用偶尔才崩一次。这种随机崩溃比稳定崩溃更难排查。所以我的建议是从一开始就把scanf的当成呼吸一样自然每次写完scanf立刻检查三个东西——有没有、格式控制符和变量类型是否匹配、变量的地址是否合法。3.3 %f和%lfscanf和printf的规则居然不一样这是初学者最容易记混的规则我直接放一张对比表场景intfloatdoublescanf%d%f%lfprintf%d%f或%lf%f或%lf也就是说在scanf里float必须用%fdouble必须用%lf混用会导致数据在内存中的读取长度和解释方式错乱。在printf里由于C语言有可变参数提升机制float传入时会自动转成double所以你用%f输出float和double都没问题C99标准之后printf使用%lf输出double也合法。最稳妥的实践是scanf严格区分printf统一用%f输出float和double都安全。如果scanf里对double用了%f程序基本不会崩溃但读进来的数据会是错乱值。比如输入3.14你打印出来可能是-2.6E-99这类天文数字。这种错值很难猜而且和代码逻辑没关系白白浪费时间。3.4 scanf的返回值它其实会告诉你读没读对scanf有一个返回值表示成功读取的项数。比如int ret scanf(%lf %lf %lf, length, width, height); if (ret ! 3) { printf(输入有问题只成功读了 %d 项\n, ret); }如果用户输入的是3 4 helloscanf读到第三个时失败ret的值是2。这个信息非常有用它是我们做输入校验的基础。不过很多初学者根本不知道scanf有返回值也更不知道应该检查它。这里先记住有这个机制下一节我会用它来写一个防手滑的输入循环。4. printf的输出是程序的脸面格式控制、精度和转义字符4.1 格式控制符速查与常见错误printf的第一个参数是格式字符串里面的%开头的是占位符。占位符和后面的参数必须一一对应、类型匹配。最容易翻车的场景是用%d去输出一个double。例如double volume 60.0; printf(体积是%d\n, volume);这样不会把60打印出来因为printf按int的规则去解释double在内存中的二进制位你会看到一个毫无规律的大整数。C语言是强类型语言但printf这种可变参数函数因为缺少编译期检查现代编译器会做格式字符串检查但必须开-Wall才会给警告类型不匹配往往只产生运行时错误初学者很难察觉。4.2 保留小数和字段宽度让输出更专业默认情况下%f会输出6位小数比如60.000000。对于体积这个场景两位小数更常见写成%.2f即可printf(长方体的体积是%.2f\n, volume);如果需要对齐多行数据还可以指定最小宽度比如%10.2f表示输出宽度至少10个字符不足部分补空格超过正常输出。宽度参数在输出表格、报表时非常有用我后面做多组数据验证时会用printf(%8.2f %8.2f %8.2f %12.2f\n, length, width, height, volume);这里有个小细节%.2f会做舍入但要注意浮点数不是十进制精确存储的。比如0.1 0.2在double里其实是一个接近0.3但略有误差的数printf显示时会按舍入规则输出0.30大多数情况下没问题。但如果你要做银行、计费这类对精度极其敏感的计算不要依赖double应该换算成整数分、厘来做。4.3 中文输出和换行编码问题让新手抓狂题目本身没有中文但很多初学者会忍不住加上中文提示。在Windows控制台里如果源码用UTF-8保存而控制台默认用GBK编码输出的中文就会变成乱码。这不是C语言的问题是字符编码不一致。处理方法有三种把源码保存成和系统一致的编码Windows常见GBK即GB2312。在程序开头调用SetConsoleOutputCP(65001)设置控制台为UTF-8Windows专属。直接用英文提示避开编码问题等学到文件编码相关知识再回来解决。我个人建议学习阶段直接用英文提示或者干脆不写中文先保证逻辑正确。中文乱码问题很容易把新手的注意力从算法逻辑转移到环境杂音上没必要一开始就硬碰。另外别忘了\n。\n是换行转义字符在格式字符串里占一个字符但表示的是回车换行。很多初学者会把它和C语言的多个print语句刷屏混淆。比如printf(请输入长宽高\n);如果不加\n提示语和输入光标挤在同一行用户体验很糟。printf里的转义字符不只是\n还有\t制表符、\输出双引号、\\输出反斜杠等这些都属于格式层面的基本功。5. 别让程序在非法输入前裸奔给例1.2加上防护5.1 用户的输入不会老实听话教科书例题默认用户会规规矩矩输入三个正数但现实中不是这样。用户可能输入负数、零、小数、字母、超长字符串甚至什么都不输直接敲回车。一个没有校验的程序遇到非法输入往往出现两种极端要么是垃圾输出要么是死循环。比如输入abc时scanf解析%lf失败a中的值保持原来的样子可能是垃圾值或初始值程序还会继续算一个体积出来用户根本看不出哪里错了。5.2 用scanf返回值做第一道防线最简单的防护是检查scanf返回值是否为3#include stdio.h int main() { double length, width, height; double volume; printf(请输入长方体的长、宽、高用空格隔开\n); if (scanf(%lf %lf %lf, length, width, height) ! 3) { printf(输入格式错误请确保输入三个数字。\n); return 1; } volume length * width * height; printf(长方体的体积是%.2f\n, volume); return 0; }这段代码至少能把格式错误和正常计算区分开。返回1表示程序因输入问题终止这种设计比写死return 0要负责任。5.3 循环重试让用户有机会重新输入光退出还不够更友好的是循环重试。这里有个经典陷阱如果scanf读入失败坏字符并没有被消费它还在输入缓冲区里。下次scanf会继续读到同一个坏字符再次失败形成死循环。解决方法是在scanf失败后清空缓冲区把残留字符全部吃掉while (getchar() ! \n);这行代码不断读取字符直到读完换行符。完整的重试版本#include stdio.h int main() { double length, width, height; double volume; while (1) { printf(请输入长方体的长、宽、高用空格隔开\n); if (scanf(%lf %lf %lf, length, width, height) 3) { break; } while (getchar() ! \n); printf(输入无效请重新输入。\n); } volume length * width * height; printf(长方体的体积是%.2f\n, volume); return 0; }实际调试中我发现while (getchar() ! \n);在输入EOFCtrlZ或CtrlD时可能陷入死循环因为getchar返回EOF而不是换行符。更安全一点的写法还要判断EOF但对教材例题来说上面的写法已经足够了。操作中注意语法分号别漏不然就变成一个空循环体整个程序行为都会改变。5.4 负数、零和超大值的边界处理即使格式正确数据也可能在物理上不合理。长方体的边长不应该是负数或零。负的边长算出来体积是负的这在数学上成立在物理上没有意义。所以更完善的校验应该加上范围判断if (length 0 || width 0 || height 0) { printf(长、宽、高必须为正数。\n); return 1; }另外double虽然是浮点型里精度较高的但也不是无限大。如果用户输入1e300 1e300 1e300理论上体积是1e900远超double上限结果会是inf无穷大。printf打印出来是inf这不算程序崩溃但显然是无效结果。对这种极端输入除非是专门的科学计算程序一般不做额外处理但作为程序员你要知道这个现象是怎么来的不要被inf和nan吓住。6. 从gcc编译到运行结果不对我的排错顺序和经验6.1 编译命令和警告开关很多新人拿VC6或Dev-C点一下运行看到弹窗就以为万事大吉。我建议从第一天就用gcc把警告全开gcc -Wall -Wextra volume.c -o volume-Wall开启大部分警告-Wextra开启更多额外警告。上面的代码如果漏了scanf的开启警告后gcc会提示format %lf expects argument of type double *, but argument 2 has type double。这条提示能帮你省下大量调试时间。如果你用VS Code装好C/C插件后同样可以用terminal执行gcc命令别只依赖一键编译。学会看编译器的原始输出是程序员的基本功。6.2 编译错误信息怎么读初学者看到一屏英文错误就慌。其实只需要抓三样文件名、行号、错误描述。比如volume.c:14:26: error: expected ; before return意思是volume.c第14行第26列附近在return前面缺少分号。中文输入法把分号;输成全角是超级常见的入门错误编译器说的expected ;十有八九是这个原因。还有一种情况是错误信息很多但真正的错只有一个后面的错误是前面错误导致的连锁反应。比如少了头文件printf和scanf的隐式声明会引发一大堆警告和错误。这时候改掉第一个错误重新编译往往后面全好了。6.3 运行结果不对的逐层排查顺序程序能编译但结果不符合预期我一般按这个顺序排查先检查格式控制符printf里是%f还是%dscanf里是%lf还是%f这一项能解决一半问题。再检查变量类型声明的是doublescanf和printf是否都按double处理。然后检查输入本身在scanf后面加一行printf把你读到的值原样打印出来。比如printf(debug: %f %f %f\n, length, width, height);这一步能立刻确认是读坏了还是算坏了。最后检查运算和括号如果代码里有中间量看看有没有整数除法截断、类型转换混用。这种由外向内的排查法比盯着代码发呆高效得多。不要觉得加临时printf很丢人我在正经项目里也是这样定位问题的。6.4 一点点代码风格建议例1.2太短风格问题体现不明显但现在养成习惯后面写几百行的代码会受益。我建议一行一句别写成一堆分号连在一起。变量名见名知义a、b、c不如length、width、height。花括号的缩进统一要么KR风格左花括号在行尾要么Allman风格左花括号独占一行别混着用。表达式里适当加空格volumelength*width*height;和volume length * width * height;后者一眼能看懂。还有加注释不是为了凑字数是给自己留线索。这个例子中的乘法可以补一句体积 长 × 宽 × 高让自己三个月后再回来看时不用重新想。最后分享一个我自己的小习惯每次学新的C语法我都会强迫自己写一个基于例1.2变体的测试程序比如把计算体积改成计算表面积或者把固定长宽高改成从命令行参数读取。这样做比反复抄教材例题有效得多。这个习惯从学C语言第一天保持到现在帮我打下了很扎实的基础。你也试试。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进