C语⾔编程常见问题解答之可移植性
C语⾔编程常见问题解答之可移植性
可移植性并不是指所写的程序不作修改就可以在任何计算机上运⾏,⽽是指当条件有变化时,程序⽆需作很多修改就可运⾏。
你不要把“我不会遇到这种情况”这句话说得太早。直到MS—Windows出现之前,许多MS—DOS程序员还不怎么关⼼可移植性问题。然后,突然之间,他们的程序不得不在⼀个看起来不同的操作系统上运⾏。当Power PC流⾏起来后,Mac 机的程序员不得不去应付⼀个新的处理器。任何⼀个在同版本的UNIX下维护过程序的⼈所了解的可移植性的知识,恐怕都⾜以写成⼀本书,更别说写成⼀章了。
假设你⽤基本ALBATR—OS(Anti-lock Braking and Tire Rotation operating system)的Tucker C来编写防抱死刹车软件,这听起来好象是⼀个最典型的不可移植软件。即便如此,可移植性仍然很重要:你可能需要把它从Tucker C的7.55c版本升级到8.O版本,或者从ALBATR—OS的3.o版本升级到3.2a版本,以修改软件中的某些错误;你也可能会出于仿真测试或宣传的⽬的,⽽把它(或其中⼀部分)移植到MS-Windows或UNIX⼯作站上;更为可能的是,在它尚未最终完⼯之前,你会把它从⼀个程序员⼿中交到另⼀个程序员⼿中。
可移植性的本意是按照意料之中的⽅式做事情,其⽬的不在于简化编译程序的⼯作,⽽在于使改写(重写!)程序的⼯作变得容易。如果你就是接过别⼈的程序的“倒霉蛋”,那么原程序中的每⼀处出乎意料之外的地⽅都会花去你的时间,并且将来可能会引起微妙的错误。如果你是原程序的编写者,你应该注意不要使你的程序中出现出乎接⼿者意料之外的代码。你应该尽量使程序容易理解,这样就不会有⼈抱怨你的程序难懂了。此外,⼏个⽉以后,下⼀个“倒霉蛋”
很可能就会是你⾃⼰了,⽽这时你可能已经忘记了当初为什么⽤这样复杂的⼀种⽅式来写⼀个for循环。
使程序可移植的本质⾮常简单:如果做某些事情有⼀种既简单⼜标准的⽅法,就按这种⽅法做。
使程序可移植的第⼀步就是使⽤标准库函数,并且把它们和ANSI/ISO C标准中定义的头⽂件放在⼀起使⽤,详见第11章“标准库函数”。
第⼆步是尽可能使所写的程序适⽤于所有的编译程序,⽽不是仅仅适⽤于你现在所使⽤的编译程序。如果你的⼿册提醒你某种功能或某个函数是你的编译程序或某些编译程序所特有的。你就应该谨慎地使⽤它。有许多关于c语⾔编程的好书中都提出了⼀些关于如何保持良好的可移植性的建议。特别地,当你不清楚某个东西是否会起作⽤时,不要马上写⼀个测试程序来看看你的编译程序是否会接受它,因为即使这个版本的编译程序接受它,也不能说明这个程序就有很好的可移植性(C++程序员⽐c程序员应该更重视这个问题)。此外,⼩的测试程序很可能会漏掉要测试的性能或问题的某些⽅⾯。
第三步是把不可移植的代码分离出来。如果你⽆法确定某段程序是否可移植,你就应该尽快注释出这⼀点。如果有⼀些⼤的程序段(整个函数或更多)依赖于它们的运⾏环境或编译⽅式,你就应该把其中不可移植的代码分离到⼀些独⽴的“.c”⽂件中。如果只在⼀些⼩的程序段中存在可移植性问题,你可以使⽤#ifdef预处理指令。例如,在MS-DOS中⽂件名的形式
为“\tools\readme”,⽽在UNIX中⽂件名的形式为“/tools/readme”。如果你的程序需要把这样的
⽂件名分解为独⽴的部分,你就需要查正确的分隔符。如果有这样⼀段代码
#ifdef unix
#define FILE_SEP_CHAR/
#endif
#ifdef __MSDOS__
define FILE SEP CHAR//
#endif
你就可以通过把FILE_SEP_CHAR传递给strchr()或strtok()来出⽂件名中的路径部分。尽管这⼀步还⽆法出⼀个MS-DOS⽂件的驱动器名,但它已经是⼀个正确的开头了。
最后,出潜在的可移植性问题的最好⽅法之⼀就是请别⼈来查!如果可以的话,最好请别⼈来检查⼀下你的程序。他或许知道⼀些你不知道的东西,或许能发现⼀些你从未想过的问题(有些名称中含lint的⼯具和有些编译程序选项可以帮助你出⼀些问题,但你不要指望它们能出⼤的问题)。
15.1 编译程序中的C++扩充功能可以⽤在C程序中吗?
不可以,它们只能⽤在真正的C++程序中。
C++中的⼀些突出性能已被ANSI/ISO C标准委员会所接受,它们不再是“C++扩充功能”,⽽已经成为C的⼀部分。例如,函数原型和const关键字就被补充到C中,因为它们确实⾮常有⽤。
有⼀些C++性能,例如内联(inline)函数和⽤const代替#define的⽅法,有时被称为“⾼级C”性能。有些C和C++共⽤的
编译程序提供了⼀些这样的性能,你可以使⽤它们吗?
有些程序员持这样⼀种看法:如果要写C代码,就只写C代码,并且使它能被所有的C编译程序接受。如
果想使⽤C++性能,那么就转到C++上。你可以循序渐进,每次⽤⼀点新的技巧;也可以⼀步到位,⽤⼤量的内联函数,异常处理和转换运算符编写模块化的抽象基类。当你跨过这⼀步之后,你的程序就是现在的C++程序了,并且你不要指望C编译程序还会接受它。
笔者的看法是:你的⼯作是从⼀个新的C标准开始的,这个标准中包含⼀些C++性能和⼀些崭新的性能。在以后的⼏年中,⼀些编译程序的开发商会去实现这些新的性能的⼀部分,但这并不能保证所有的编译程序都会去实现这些性能,也不能保证下⼀个C标准会纳⼊这些性能。你应该保持对事态发展的关注,当⼀项新的性能看上去已经真正流⾏起来,并且不仅仅出现在你现在所使⽤的编译程序中,⽽是出现在所有你可能⽤到的编译程序中时,你就可以考虑使⽤它了。例如,如果过去有⼈⾮要等到1989年才开始使⽤函数原型,那么这其实就不是⼀种明智之举;另⼀⽅⾯,在保证可移植性的前提下,过去也没有⼀个开始使⽤noalias关键字的最佳时机。
请参见:
15.2 C++和C有什么区别?
15.2 C++和C有什么区别?
这个问题要从C程序员和C++程序员两个⾓度去分析。
对C程序员来说,C++是⼀种古怪的难以掌握的语⾔。⼤多数C++库⽆法通过C编译程序连接到c程序中(在连接时编译程序必须创建模型或“虚拟表”,⽽C编译程序不提供这种⽀持)。即使⽤c++编译程序来连接程序,c程序仍然⽆法调⽤许多
c语言编程软件是系统软件吗C++函数。除⾮⾮常⼩⼼地编写c++程序,否则C++程序总会⽐类似的c程序慢⼀些,并且⼤⼀些。C++编译程序中的错误也⽐C编译程序中的多。C++程序更难于从⼀种编译程序移植到另⼀种编译程序上。最后⼀点,C++是⼀种庞⼤的难以学会的语⾔,它的定义⼿册(1990)超过400页,⽽且每年还要加⼊⼤量的内容。另⼀⽅⾯,c语⾔是⼀种既漂亮⼜简炼的语⾔,并且这⼏年来没有什么改动(当然不可能永远不会有改动,见14.1)。C编译程序⼯作良好,并且越来越好。好的c程序可以很⽅便地在好的C编译程序之间移植。虽然在C中做⾯向对象的设计并不容易,但也不是⾮常困难。如果需要的话,你(⼏乎)总是可以⽤c++编译程序来⽣成C程序。
对于C++程序员来说,c是⼀个好的开端。在C++中你不会重犯在C中犯过的许多错误,因为编译程序不会给你这个机会。C的有些技巧,如果使⽤稍有不当,就会带来很⼤的危险。
另⼀⽅⾯,c++是⼀种优秀的语⾔。只需应⽤少数原则,稍作⼀点预先的设计⼯作,就能写出安全、⾼效并且⾮常容易理解和维护的C++程序。⽤有些⽅法写C++程序,能使C++程序⽐类似的C程序更快并且更⼩。⾯向对象的设计在C++中⾮常容易,但你不⼀定要按这种⽅式⼯作。编译程序⽇臻完善,标准也
逐渐确⽴起来。如果需要的话,你随时可以返回到C中。  那么,c和C++之间有什么具体的区别呢?C的有些成分在c++中是不允许使⽤的,例如⽼式的函数定义。⼤致来
说,C++只是⼀种增加了⼀些新性能的C:
·新的注释规则(见15.3);
·带有真正的true和false值的布尔类型,与现有的c或c++程序兼容(你可以把贴在显⽰器上的写
着“O=false,1=true”的纸条扔掉了。它仍然有效,但已不是必须的了)。
·内联函数⽐#define宏定义更加安全,功能也更强,⽽速度是⼀样的。
·如果需要的话,可以确保变量的初始化,不再有⽤的变量会被⾃动清除。
·类型检查和内存管理的功能更好,更安全,更强⼤。
·封装(encapsulation)——使新的类型可以和它们的所有操作⼀起被定义。c++中有⼀种complex类型,其操作和语法规则与float和double相同,但它不是编译程序所固有的,⽽是在C++中实现的,并且所使⽤的是每⼀个C++程序员都能使⽤的那些性能。
·
访问权控制(access contr01)——使得只能通过⼀个新类型所允许的操作来使⽤该类型。
·继承和模板(inheritance and templates)——两种编写程序的辅助⽅法,提供了函数调⽤之外的代码复⽤⽅式。
·异常处理(exceptions)——使⼀个函数可以向它的调⽤者之外的函数报告问题。
·⼀种新的I/O处理⽅法——⽐printf()更安全并且功能更强,能把格式和要写⼊的⽂件的类型分离开。
·⼀个数据类型丰富的库——你永远不需要⾃⼰编写链表或⼆叉树了(这⼀点是千真万确的!)。
那么,c和c++哪⼀个更好呢?这取决于多种因素,例如你做什么⼯作,你和谁⼀起⼯作,你有多少时间能⽤于学习,你需要并且能够使⽤的⼯具是什么,等等。有些C++程序员永远不会再返回到C,也有⼀些c程序员是从C++返回到C的,并且乐于使⽤C。有些程序员虽然也在使⽤⼀些C++性能和⼀种C++编译程序,但他们并没有真正理解C++,因此他
们被称为“⽤c++编写C程序”的⼈。还有⼀些⼈⽤C(和C++)编写FORTRAN程序,他们永远不会理解C或C++。
优秀的语⾔并不能保证产⽣优秀的程序。只有优秀的程序员才会理解他所⽤的语⾔,并且不管他⽤的是什么样的语⾔,他
都能⽤它编写出优秀的程序。
请参见:
15.1 编译程序中的C++扩充功能可以⽤在C程序中吗?
15.3 在c程序中可以⽤“∥”作注释吗?
不⾏。有些C编译程序可能⽀持使⽤“∥”,但这并不说明可以在C程序中使⽤“∥”。
在c中,注释以“/*”开始,以“*/”结束。c的这种注释风格在c++中仍然有效,但c++中还有另⼀种注释规则,即
从“∥”到⾏尾之间的内容(包括“∥”)都被认为是注释。例如,在C中你可以这样写:
i+=1;/*add one to i*/
这种写法在C++中也是有效的,⽽且下⾯这⾏语句也同样有效:
i+=1;∥add one to i
C++的这种新的注释⽅法有这样⼀种好处,即你不⽤记着去结束⼀⾏注释,⽽在注释c程序时你可能会忘记去结束⼀段注释:
i+=1;/*add one to i
printf(Dont worry,nothing will be); /*oops*/
printf(lost\n”);
在这个例⼦中只有⼀段注释,它从第⼀⾏开始,到第⼆⾏的⾏尾结束。要打印Dont worry等内容的那个printf()函数被注释掉了。
为什么c++的这种性能⽐其它性能更容易被c编译程序接受呢?因为有些编译程序的预处理程序是⼀个独⽴的程序,如果c和C++编译程序使⽤相同的预处理程序,C编译程序可能就会让这个预处理程序来处理这种新的C++注释。
C++的这种注释风格最终很可能会被c采⽤。如果有⼀天,你发现所有的C编译程序都⽀持“∥”注释符,那么你就可以⼤胆地在程序中使⽤它了。在此之前,你最好还是⽤“/*”和“*/”来注释C程序。
请参见:
第5章“编译预处理”开头部分的介绍
5.2 预处理程序有什么作⽤?
15.1 编译程序中的C++扩充功能可以⽤在C程序中吗?
15.4 char,short,int和long类型分别有多长?
其长度分别为⼀字节,⾄少两字节,⾄少两字节和⾄少4字节。除此之外,不要再依赖任何约定。
char类型的长度被定义为⼀个8位字节,这很简单。
short类型的长度⾄少为两字节。在有些计算机上,对于有些编译程序,short类型的长度可能为4字节,或者更长。
int类型是⼀个整数的“⾃然”⼤⼩,其长度⾄少为两字节,并且⾄少要和short类型⼀样长。在16位计算机上,int类型的长度可能为两字节;在32位计算机上,可能为4字节;当64位计算机流⾏起来后,int类型的长度可能会达到8字节。这⾥说的都是“可能”,例如,早期的Motorala 68000是⼀种16/32位的混合型计算机,依赖于不同的命令⾏选项,⼀个68000编译程序能产⽣两字节长或4字节长的int类型。
long类型⾄少和int类型⼀样长(因此,它也⾄少和short类型⼀样长)。long类型的长度⾄少为4字节。32位计算机上的编译程序可能会使short,int和long类型的长度都为4字节——也可能不会。
如果你需要⼀个4字节长的整型变量,你不要想当然地以为int或long类型能满⾜要求,⽽要⽤typedef把⼀种固有的类型(⼀种确实存在的类型)定义为你所需要的类型,并在它的前后加上相应的#ifdef指令:
#ifdef FOUR_BYTE_LONG
typedef long int4;
#endif
如果你需要把⼀个整型变量以字节流的⽅式写到⽂件中或⽹络上,然后再从不同的计算机上读出来,你可能就会⽤到这样的类型(如果你要这样做,请参见15.5)。
如果你需要⼀个两字节长的整型变量,你可能会遇到⼀些⿇烦!因为并不⼀定有这样的类型。但是,你总是可以把⼀个较⼩的值存放到⼀个由两个char类型组成的数组中,见15.5。
请参见:
10.6 16位和32位的数是怎样存储的?
15.5 ⾼位优先(big—endian)与低位优先(1ittle—endian)的计算机有什么区别?
15.5⾼位优先(big—endian)与低位优先(little-endian)的计算机有什么区别?
⾼位优先与低位优先的区别仅仅在于⼀个字的哪⼀端是⾼位字节。换句话说,两者的区别在于你是喜欢从左向右数,还是喜欢从右向左数。但是,哪种⽅式都不见得⽐另⼀种⽅式更好。⼀个可移植的C程序必须能同时适⽤于这两种类型的计算机。
假设你的程序运⾏在short类型为两字节长的计算机上,并且把值258(⼗进制)存放到地址s3000H处的⼀个short类型中。因为short类型的长度为两字节,所以该值的⼀个字节存放在3000H处,另⼀个字节存放在3001H处。258(⼗进制)即0102H,所以该值的⼀个字节的内容为1,另⼀个字节的内容为2。那么,究竟内容为1和2的字节分别是哪⼀个呢?
其答案因机器的不同⽽不同。在⾼位优先的计算机上,⾼位字节就是低地址字节(“⾼位字节”指的是其值变化后使整个字的值变化最⼤的那个字节,例如,在值0102H中,01H就是⾼位字节,⽽02H是低位字节)。在⾼位优先的计算机上,字节中的内容如下所⽰:
地址  2FFEH  2FFFH  3000H  3001H  3002H  3003H
值    01H    02H
这种图⽰⽅式很直观——地址就象是尺⼦上的刻度值,低地址在左,⾼地址在右。
在低位优先的计算机上,字节中的内容如下所⽰:
地址  3003H  3002H  3001H  3000H  2FFFH  2FFEH
值    01H    02H
这种图⽰⽅式同样很直观——低位字节存放在低地址中。
不幸的是,有些计算机采⽤⾼位优先的存储⽅式,⽽另⼀些计算机却采⽤低位优先的存储⽅式。例如,IBM兼容机和Macintosh机对⾼位字节和低位字节的处理⽅法就不同。
为什么这种区别会产⽣影响呢?试想⼀下,如果⽤fwrite()直接把⼀个short类型的值按两字节存到⽂件或⽹络上,不考虑格式和是否可读,⽽只是存为紧凑的⼆进制形式,会引起什么后果呢?如果在⾼位优先的计算机上存⼊这个值,⽽在低位优先的计算机上读出该值(或者反过来),那么存⼊的是0102H(258),读出的就是0201H(513)。
解决这个问题的办法是选择⼀种存储(和读取)⽅式,并且⾃始⾄终使⽤这种⽅式,⽽不是按存⼊内存的⽅式来存储short或int类型的值。例如,有些标准指定了“⽹络字节顺序(network byte order)”,它是⼀种
⾼位优先顺序(即⾼位字节存放在低地址中)。例如,如果s是⼀个short类型值⽽a是⼀个由两个char类型组成的数组,那么下⾯这段代码
a[0]=(s>>4)& Oxf;
a[1]=s&0xf;
将把s的值按⽹络字节顺序存⼊a的两个字节中。不管程序是运⾏在⾼位优先或低位优先的计算机上,s的值都会存成这种形式。
你可能会注意到,笔者⼀直没有提到哪种计算机是⾼位优先或低位优先的计算机。这样做是有⽬的的——如果可移植性是重要的,你就应该按这两种类型的计算机都能接受的⽅式编写程序;如果效率是重要的,通常你仍然要按这两种类型的计算机都能接受的⽅式编写程序。
例如,在⾼位优先的计算机上可以⽤⼀种更好的⽅法去实现上例中的那段代码,即使你使⽤了上例中的代码,⼀个好的编译程序仍然会利⽤那种更好的实现来产⽣机器代码。
注意:“big-endian和little-endian这两个名称来源于Jonathan Swift所写的《格列佛游记>>(Gullivers Travels)⼀书。在格列佛第三次出海时,他遇到了这样⼀⼈,他们对煮熟了的鸡蛋的吃法争论不休:有的要先吃⼤头,有的要先吃⼩头。
“⽹络字节顺序”只适⽤于int,short和long类型。char类型的值按定义只有⼀字节长,因此字节顺序与它⽆关。对于float和double类型的值,没有⼀种标准的存储⽅式。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。