【C陷阱与缺陷】第7章:可移植性陷阱解析 | 编写跨平台C程序
在底层的角度下,一个程序就是一个由符号(token)或者记号组成的序列,就像一本书(程序)也只是一个单词(token)序列。还可以把程序看作语句和声明的序列,就像可以把书看作句子的序列一样。把程序分割成符号的过程叫做词法分析。
写作本书的出发点不是要批判C语言,而是帮助C程序员绕过编程过程中的陷阱和障碍。全书分为8章,分别从词法分析、语法语义、连接、库函数、预处理器、可移植性缺陷等几个方面分析了C编程中可能遇到的问题。最后,作者用一章的篇幅给出了若干具有实用价值的建议。
(关注不迷路哈!!!)
文章目录
- 【C陷阱与缺陷】第7章:可移植性陷阱解析 | 编写跨平台C程序
- 前言
- 一、应对C语言标准变更
- 问题场景
- 解决方案
- 二、标识符名称的限制
- 重要规则
- 危险示例
- 三、整数类型的大小选择
- 类型长度规则
- 可移植方案
- 四、字符的符号性问题
- 问题本质
- 错误转换
- 正确转换
- 五、移位运算符的陷阱
- 两大问题
- 可移植写法
- 六、空指针的特殊性
- 危险操作
- 检测方法
- 七、整数除法的截断方式
- 数学关系
- 实现差异
- 可移植方案
- 八、随机数范围的处理
- 历史问题
- 可移植方案
- 九、大小写转换的实现
- 历史宏定义
- 安全方案
- 十、内存分配的特殊历史
- 老式realloc用法
- 现代用法
- 十一、综合示例:数字打印函数
- 初始版本(有问题)
- 最终可移植版本
- 十二、实战总结与建议
- 十三、读后感
前言
- C语言的可移植性是其重要优势,但不同编译环境和硬件平台的差异仍会导致潜在问题。
- 本章深入分析整数大小、字符符号性、移位运算、内存处理等可移植性陷阱,帮助开发者编写真正跨平台的C程序。
一、应对C语言标准变更
问题场景
doublesquare(doublex){returnx*x;}// C99+标准在旧编译器上可能无法编译,因为早期C标准不支持函数原型。
解决方案
- 使用函数声明:
doublesquare();// 旧式声明main(){printf("%g\n",square(3));// 可能出错}- 完整原型声明(推荐):
doublesquare(double);// 完整原型main(){printf("%g\n",square(3));// 正确:3转换为double}权衡:使用新特性提高开发效率,但会降低向后兼容性。
二、标识符名称的限制
重要规则
- C实现只需区分前6个字符(不区分大小写)。
- 连接器可能进一步限制外部名称。
危险示例
char*Malloc(unsignedn){char*p,*malloc(unsigned);p=malloc(n);// 实际调用Malloc自身(递归)if(p==NULL)panic("out of memory");returnp;}问题:Malloc与malloc仅大小写不同,但连接器可能视为同一函数。
建议:避免与标准库函数相似的名称。
三、整数类型的大小选择
类型长度规则
short≤int≤longint至少16位,long至少32位int必须能容纳任何数组下标
可移植方案
typedeflongtenmil;// 用于可能的大数值tenmil population=10000000L;建议:根据数据范围选择类型,使用typedef提高可读性和可维护性。
四、字符的符号性问题
问题本质
char可能为有符号(-128–127)或无符号(0–255)- 影响字符到整数的转换结果
错误转换
charc='\xFF';unsignedu=(unsigned)c;// 可能得到0xFFFFFFFF正确转换
unsignedcharuc=c;unsignedu=(unsigned)uc;// 总是得到0x000000FF建议:需要明确符号性时使用signed char或unsigned char。
五、移位运算符的陷阱
两大问题
- 右移填充:有符号数右移可能填充符号位(算术右移)或0(逻辑右移)
- 移位计数:必须满足0 ≤ count < 类型位数
可移植写法
// 无符号右移(总是填充0)unsignedintu=n;u>>=3;// 有符号右移(避免依赖实现)ints=n;if(s<0)s=-((-s)>>3);// 自定义算术右移elses>>=3;性能提示:用移位代替除法(如mid = (low+high)>>1)。
六、空指针的特殊性
危险操作
char*p=NULL;printf("%d\n",*p);// 未定义行为检测方法
#include<stdio.h>intmain(){char*p=NULL;printf("Location 0 contains %d\n",*p);}- 在禁止访问地址0的系统上会崩溃
- 允许访问的系统可能输出随机值
建议:始终检查指针是否为NULL后再解引用。
七、整数除法的截断方式
数学关系
q=a/b`,`r=a%b`应满足:`q*b+r==a实现差异
- 大多数实现:商向0取整,余数与除数同号
-3/2可能为-1(余-1)或-2(余1)
可移植方案
// 自定义整数除法voiddivide(inta,intb,int*q,int*r){*q=a/b;*r=a%b;if(*r<0){*r+=abs(b);*q+=(a<0)?-1:1;}}八、随机数范围的处理
历史问题
rand()返回值范围因实现而异(PDP-11: 0-32767, VAX: 0-2147483647)
可移植方案
// 生成[min, max]范围内的随机数intrandom_range(intmin,intmax){returnmin+(int)((max-min+1.0)*rand()/(RAND_MAX+1.0));}注意:检查RAND_MAX的定义。
九、大小写转换的实现
历史宏定义
#definetoupper(c)((c)-'a'+'A')// 危险:不检查输入#definetolower(c)((c)-'A'+'a')安全方案
// 使用函数版本#include<ctype.h>charc=toupper('a');// 正确检查输入// 或使用新宏名#define_toupper(c)((c)+'A'-'a')// 使用者自行确保输入正确十、内存分配的特殊历史
老式realloc用法
free(p);p=realloc(p,newsize);// 在老系统中合法现代用法
p=realloc(p,newsize);// 自动处理释放if(p==NULL){// 处理分配失败}注意:移植老代码时需要检查此类用法。
十一、综合示例:数字打印函数
初始版本(有问题)
voidprintnum(longn,void(*p)(char)){if(n<0){(*p)('-');n=-n;// 可能溢出(如LONG_MIN)}if(n>=10)printnum(n/10,p);(*p)('0'+n%10);// 可能字符集不连续}最终可移植版本
voidprintneg(longn,void(*p)(char)){if(n<=-10)printneg(n/10,p);(*p)("0123456789"[-(n%10)]);// 使用查找表}voidprintnum(longn,void(*p)(char)){if(n<0){(*p)('-');printneg(n,p);}else{printneg(-n,p);// 统一处理负数}}十二、实战总结与建议
- 类型选择: 明确数据范围,使用
typedef定义明确类型 需要大整数时使用long - 字符处理: 需要明确符号性时使用
signed/unsigned char字符转换使用查找表而非算术运算 - 移位运算: 无符号数进行位操作 检查移位计数范围
- 内存操作: 避免依赖空指针行为 使用现代内存分配模式
- 除法运算: 需要特定截断行为时自定义函数
- 兼容性考虑: 使用特性前检查编译器支持 必要时提供多种实现版本
十三、读后感
《C陷阱与缺陷》的第七章主要讲述了C语言中的可移植性陷阱。
使用C语言编写程序的重要优势之一是,C程序在不同的编译环境中具有可移植性。本章节主要讨论了几个常见的错误来源,并把重点放在语言属性上。
- C语言标准的变更所面临的问题:C程序中如果使用新的特性,虽然程序更加容易编写,但是可能在较早的编译器上无法工作,需要我们权衡与决策;
- 为了保证C程序的可移植性,谨慎地选择外部标识符的名称是很重要的……总的来说,我们很难预言未来硬件的特性,努力提高软件的可移植性,实际上是延长软件的生命期。
- 读完《C陷阱与缺陷》第七章关于可移植性陷阱的章节内容,我体会C语言中的运行速度快慢以及可移植性问题,是我们在编写程序过程中需要考虑的因素。
此外,通过学习本章内容,我更加认识到很多事情是无法达到完美的,在实际工程应用中需要我们进行适当的取舍,力求做到权衡利弊的同时满足实际供应需求。