【C陷阱与缺陷】第8章:编程建议总结 | 写出更健壮的C代码
在底层的角度下,一个程序就是一个由符号(token)或者记号组成的序列,就像一本书(程序)也只是一个单词(token)序列。还可以把程序看作语句和声明的序列,就像可以把书看作句子的序列一样。把程序分割成符号的过程叫做词法分析。
写作本书的出发点不是要批判C语言,而是帮助C程序员绕过编程过程中的陷阱和障碍。全书分为8章,分别从词法分析、语法语义、连接、库函数、预处理器、可移植性缺陷等几个方面分析了C编程中可能遇到的问题。最后,作者用一章的篇幅给出了若干具有实用价值的建议。
(关注不迷路哈!!!)
文章目录
- 【C陷阱与缺陷】第8章:编程建议总结 | 写出更健壮的C代码
- 前言
- 一、不要自我欺骗(认清现实)
- 典型示例
- 问题分析
- 建议
- 二、明确表达意图(代码即文档)
- 1. 使用括号明确优先级
- 2. 常量在左的比较技巧
- 建议
- 三、考查边界情况(简单特例验证)
- 重要原则
- 示例:数组处理
- 建议
- 四、使用不对称边界(统一处理模式)
- 核心思想
- 示例:循环处理
- 建议
- 五、警惕隐藏的Bug(了解语言特性)
- 1. 避免生僻特性
- 2. 注意移植性问题
- 建议
- 六、防御性编程(假设最坏情况)
- 1. 验证输入假设
- 2. 检查边界条件
- 3. 处理可能的错误
- 建议
- 七、综合编程准则
- 八、全书总结
前言
- 《C陷阱与缺陷》最后一章总结了全书的核心建议,帮助开发者避免常见错误并编写更健壮的C代码。
- 这些建议涵盖了代码风格、边界处理、防御性编程等多个关键方面,是C程序员的宝贵经验总结。
一、不要自我欺骗(认清现实)
典型示例
// 错误示例:自我说服的"皇帝新装"while(c=='\t'||c=' '||c=='\n')// 实际语法错误c=getc(f);问题分析
赋值运算符
=优先级最低,实际解析为:while((c=='\t'||c)=(' '||c=='\n'))// 非法表达式但开发者可能自我说服这是"正确"写法。
建议
- 直面错误:不要为明显错误找借口
- 编译检查:相信编译器警告,不要忽略任何警告
二、明确表达意图(代码即文档)
1. 使用括号明确优先级
// 模糊意图if(a&b==c)// 实际解析为 if (a & (b == c))// 明确意图if((a&b)==c)// 使用括号明确优先级2. 常量在左的比较技巧
// 传统写法(易误写为赋值)if(c=='\t')// 可能误写为 if (c = '\t')// 防御性写法if('\t'==c)// 误写为 if ('\t' = c) 会编译报错建议
- 主动明确:用括号、格式、注释明确代码意图
- 预防错误:采用常量在左的写法预防赋值误写
三、考查边界情况(简单特例验证)
重要原则
程序最容易在边界情况下出错,应优先测试:
- 空输入(空字符串、空数组、空文件)
- 单元素情况
- 最大值/最小值边界
示例:数组处理
// 考虑空数组和单元素数组voidprocess_array(int*arr,size_tn){if(n==0)return;// 空数组处理if(n==1){// 单元素特殊处理// ...return;}// 通用处理}建议
- 优先测试边界:从最简单特例开始设计和测试
- 考虑极端值:0、1、最大值、最小值等特殊情况
四、使用不对称边界(统一处理模式)
核心思想
采用[start, end)的半开区间表示法:
- 取值范围大小 = end - start
- 空范围时 start == end
- 避免差一错误(off-by-one)
示例:循环处理
// 传统对称边界(易出错)for(inti=0;i<=n-1;i++)// 容易写成 i <= n// 不对称边界(更安全)for(inti=0;i<n;i++)// 明确且安全建议
- 统一使用半开区间:
[start, end)模式 - 避免复杂计算:减少边界计算中的加减操作
五、警惕隐藏的Bug(了解语言特性)
1. 避免生僻特性
// 晦涩写法(依赖实现细节)inta=1;printf("%d %d %d\n",a,a++,a);// 未定义行为// 明确写法inta=1;intb=a++;printf("%d %d\n",a,b);2. 注意移植性问题
- 避免依赖特定字节序
- 避免依赖未定义行为
- 使用标准库而非编译器扩展
建议
- 坚持标准:只使用众所周知的语言特性
- 查阅文档:不确定时查阅语言标准文档
六、防御性编程(假设最坏情况)
1. 验证输入假设
// 不安全假设voidprocess(int*arr,intn){for(inti=0;i<n;i++){arr[i]=i*2;// 假设arr不为NULL}}// 防御性写法voidprocess(int*arr,intn){if(arr==NULL||n<=0)return;// 验证输入for(inti=0;i<n;i++){arr[i]=i*2;}}2. 检查边界条件
// 危险的内存访问voidset(int*p,intn){*p=n;// 不检查p是否为NULL}// 安全版本voidset(int*p,intn){if(p!=NULL)*p=n;}3. 处理可能的错误
FILE*file=fopen("data.txt","r");if(file==NULL){// 错误处理,而非继续执行perror("无法打开文件");return;}// 正常处理建议
- 验证所有输入:不信任任何外部输入
- 检查返回值:重要函数调用检查返回值
- 准备错误处理:为所有可能错误提供处理路径
七、综合编程准则
- 代码清晰性使用有意义的变量名 添加必要的注释 保持函数短小专注
- 错误预防编译时开启所有警告 使用静态分析工具 定期进行代码审查
- 测试策略单元测试覆盖边界情况 集成测试发现交互问题 压力测试验证稳定性
- 持续学习阅读语言标准文档 学习常见陷阱案例 参与代码审查过程
八、全书总结
《C陷阱与缺陷》通过大量真实案例揭示了C编程中的常见陷阱,从词法、语法、语义到连接、库函数、预处理器和可移植性等多个层面进行了深入分析。最终章的建议是全书精华的总结,帮助开发者建立防御性编程思维,写出更健壮可靠的C代码。
第1章. 关注由C语言词法分析的方式所造成的问题。
第2章. 将关注那些由对于语法的歧义理解所造成的错误。
第3章. 关注语义误解:在这里,语义是由符号或单词如何组成更大的单元所体现的。编程者本想表达一件事却可能实际上表达成另外一件事。
第4章. 关注连接问题:认识到一个C程序通常被分成几部分并分别编译,最后再组合在一起。这个过程被叫做连接,而且是程序和环境的联系之一。
第5章. 讨论库函数的误用:尽管严格来说,库函数不算是语言的一部分,但是库函数对于任何C程序来说都是必须的。
第6章. 讨论预处理器方面有用的东西:指出我们所写的程序并不完全是我们所运行的程序,但预处理器会事先对它处理。
第7章. 讨论可移植性问题——即一个程序在一个编译器上正常运行,但在另一个上运行出错。
第8章. 对于保守编程提出了一些建议,并给出了其他章节练习的答案。
关键收获:
- 理解比记忆更重要
- 预防比修复更有效
- 简单比复杂更可靠