跨平台C、C++代码注意的事项及如何编写跨平台的C/C++代码

    在我们的开发中,跨平台的需求越来越强烈,如何保持C/C++代码能在多个平台上编译,是一个比较值得研究的问题。关于跨平台的文章网上很多,跨平台的库网上也很多。那么我从自己的跨平台开发经验谈一谈自己的心得,希望对大家能够起到一定的作用。主要涉及到Windows和linux两个操作系统。

1、  关于路径和头文件路径分隔符的问题

在Windows中,正斜杠和反斜杠都可以,但是在Linux中,只能是/。

在Windows中,路径大小写无所谓,在Linux中严格区分大小写。

2、  char的问题

如果考虑跨平台,需要明确指定是signed或者unsigned,因为不同平台直接声明char,会导致signed或者unsigned的不确定性。

3、  关于宽字符的问题。

在Windows中,wchar_t占两个字节,Linux中占四个字节,但是在Linux可以指定两个字节,这样也会造成一个问题,就是某些第三方库中wchar_t可能只指定四个字节的,这样就会导致不兼容。

4、  Linux里面没有stricmp函数,在Linux下面是strcasecmp函数比较字符串。

5、  与平台相关的调用尽量用宏隔离开来,一般用不同的目录代表不同平台,BOOST、OGRE等是这样做,也可以再一个类或者文件中,这样会导致到处都是操作系统和编译器相关宏的定义。

6、  关于头文件包含

在Windows下某些C标准库的头文件不用显式包含,但是在linux下需要显式包含。所以在.c和.cpp文件中尽量包含这个文件中需要的头文件。

7、  注意机器大尾端和小尾端的区别

大小尾端对文件的读写会有很大影响,要编写跨平台c++程序,大小尾端是必须要考虑的问题。比如,你在大尾端机器上写了一个文件,然后在小尾端机器上读取,那么结果肯定是错误的,所以,我们设计文件格式时,都需要规定文件是大尾端存储还是小尾端存储,或者一个文件中规定某些部分是大尾端某些部分是小尾端。

8、  尽量只使用STL较早出现的函数或类

较早出现的东西相对来说比较稳定,STL的各个实现基本上都会有实现,这样跨平台的时候可以兼容多个平台。

9、  使用std::exception时需要注意,LINUX下是不支持抛出异常的,如果继承自标准库的异常类写自己的异常类的时候,在Linux下,子类的析构函数中就需要表明不抛出异常,所以析构函数后面加上throw()就可以了。

10、当继承模板类时。需要谨慎

在自己的代码中,需要继承模板类时,如果需要访问基类模板类的成员函数或者成员变量,前面加上this->。另外,构造函数需要用到基类进行构造时。基类的类型需要需要用该类的类型参数初始化,否则在linux下会提示找不到基类的这个名字。

11、尽量使用标准C和C++的函数以及STL,使用C语言中定义的类型。

12、头文件重复包含的问题

         尽量用保卫宏去实现防止头文件的重复包含,很多代码在Windows下直接用#pragma once,这不能保证跨平台需要。

13、关于结构体对齐的问题。

         CPU为了简化内存和CPU之间的处理以及加快CPU从内存中取数据的速度,往往都会做一定的对齐,即结构体的各个成员并不是紧凑存储的,往往在成员中间填充一些字节。所以,我们一般不推荐用结构体直接读取和写入数据,这样在不同系统或者计算机之间进行移植时,会出现错误的结果。

14、注意BOM的陷阱(字节顺序标记)

         如果你在Windows用记事本创建一个源文件,那么Windows会在文件最前面加上一个BOM标记,即所谓的字节顺序标记,这样的源码在Windows下没问题,但是在Linux下就编译不过,所以需要用其他的文本编辑器或者直接在VS里面创建源文件。Linux下gcc/g++不认带BOM标记的源文件。

15、注意调用函数时的形参类型和函数声明中参数列表的类型不匹配。这里特指有无const或者是否是引用参数。在Windows下的cl编译器没问题,linux下GCC/G++会报错。

16、注意两个尖括号不要连着写。例如std::vector> vec;在Windows下这么写完全没问题,那么在linux下就是编译不过,所以linux下可以在连续两个尖括号符号之间留一个空格,即std::vector  > vec;


 

其实,这些只是冰山一角,在跨平台C/C++开发上还需要做更多的探索。


怎样才能实现一套C/C++代码跨平台编译呢?应该注意哪些问题,下面对其进行总结。

         平台包括: windows7(32bit and 64bit), windows 8.1(32bit and 64bit, desktop, app store, metro ),windows phone, Mac(32bit and 64bit), iOS(32bit and 64bit, x86, x64, armv6,armv7-a, arm64), Linux(32bit and 64bit, x86, x64, mips, armv8-a), Android(32bitand 64bit, x86, x64, armv6, armv7-a, armv8-a).

         遇到的问题包括:

         (1).数据类型在不同平台有可能长度不一致;

         (2).重定义问题,在有些平台某个功能有多种语言实现,如C++,SIMD,汇编等;

         (3).在有些平台,库的前后位置放置不正确,如Linux, Mac链接时与库位置有关;

         (4).对于intel SIMD指令,在Linux/Mac和windows下,有些包含的头文件名字不一致,如在windows下用"intrin.h",在linux下用"x86intrin.h",而在mac下用"nmmintrin.h";

         (5). 在特定系统中有些与性能相关的实现需要关闭,如SIMD在中标麒麟或湖南麒麟Linux下是不支持的;

         (6). SIMD个别用法不一致不能使用统一方法导致编译出错,如__m128i在linux下不能直接提取子元素,而在windows下是可以的;

         (7). 在有些平台只有函数声明没有实现,有时会报编译错误;

         (8). 在用命令行编译和链接时,要保证编译和链接的配置参数保持完全一致;

         (9). 有些平台缺少必要的软件或插件导致编译错误;

         (10). 对于SIMD,有些机子配置偏低,支持SIMD指令有限;

         (11). 在移动平台,对armv6, armv7-a, armv8-a,它们支持的参数是不一致的,要区别对待,如-msoft-float参数,在armv6和armv7-a下是支持的,在armv8-a下是不支持的;

         (12).在有些平台,自动获取CPU架构及位数有误。

         下面是从网上汇总的一些关于跨平台开发时的一些注意事项:

         以下内容参考(http://loopjump.com/ten_rules_for_cross_platform_cpp/http://www.cppblog.com/johndragon/archive/2007/04/18/22204.html?opt=admin):

         (1).不要先开发后移植:当增加了一个功能或者修复了某个bug,开始就需要考虑所有的目标平台,并使得代码能够在所有的平台上正常工作。

         (2).将GUI代码作为不可复用代码

         (3).使用标准C/C++类型,不要使用特定于平台的类型。

         (4).只使用内置的 #ifdef 编译标志,不要自己发明轮子。

         (5).开发一个简单的可重用的跨平台的基础库,来隐藏每个平台的代码。

         (6).在所有的API中都使用Unicode(特别是UTF-8)。现在,Unicode在各个平台所有应用上都是100%支持的,UTF-8的一个好的性质是它向后兼容ASCII码。

         (7).不要使用第三方应用程序框架或者运行时环境来使你的代码跨平台。

         (8).程序中不要硬性编码与平台相关的任何常量,比如行分隔符、文件分隔符、路径分隔符等。

         (9).不同的操作系统,不同的机器,系统支持的颜色和屏幕的大小和分辨率都不同,要注意。

         (10).注意内存对齐。

         (11).注意文件名的大小写。

         (12).保证每个代码文件(.cpp, .h)的结尾都有单独的没有任何内容的一行,这在CC/GCC/G++编译器下能减少很多警告。

         (13).#include 包含头文件的时候一定要注意文件名的大小写。

         (14).头文件的路径分隔推荐使用”/”,而不是windows下常用的”\”,前者基本可以在所有系统上使用,后者似乎是windows独家支持的。

         (15).尽量使用标准C++的原子数据类型,避免使用被重新定义过的类型。或者自己重新重定义一套。

         (16).避免使用与编译器相关的一些特性。尽量所有代码都按照c++标准来编写。

         (17).尽量把操作系统相关的东西都封装成统一的调用接口,这样在移植代码的时候,可以做到重新编译即可运行。

         (18).尽量不要使用内嵌汇编。

         (19).在编译时尽量打开所有的警告选项,这样编译器可以尽可能多的发现平台相关的语句,并给出警告

         以下内容摘自《C++跨平台开发技术指南》 :

         (1).把所有的平台都放在同样重要的位置:必须在产品开发和发布的每一个阶段,都要在所有支持的平台上完全达到功能和质量的要求。

         (2).使用公共的代码:能被不同平台共享的代码越多,跨平台的项目就越容易成功。所有平台上公用的功能应该被标识出来避免它们在平台相关的代码里重复出现。并且它们的编译、测试和部署应该贯穿在整个产品生命期中。如果代码不能公用的话,应该把它隐藏到一个统一的API抽象之中去。这些封装平台相关功能的API对使用它的应用程序来说应该是透明的。

         (3).要求开发人员用不同的编译器编译代码:它可以帮助你避免编译器相关的特性、标志和宏。它把对C/C++标准不同解读的影响减到最小,并帮助你避免使用未经证明的语言特性。每种编译器都会产生不同的错误和警告,这会帮助你编写更强壮的代码,让开发过程变得简单。不同的编译器会生成不同的代码,这可以帮助你发现一些很难发现的问题。

         (4).要求开发人员在不同的平台上编译代码

         (5).测试所有的平台

         (6).关注编译警告

         (7).使用最适合平台的编译器

         (8).尽量使用本地IDE。

         (9).在Windows上安装和使用Cygwin:Cygwin是一个”windows 上的Linux环境”。

         (10).使用跨平台的Make系统。

         (11).使用跨平台的bug报告和跟踪系统:跨平台工具包里非常重要的一个组件就是bug报告和跟踪系统(bug系统)。工程师和测试人员利用bug系统来报告软件开发测试中遇到的缺陷和问题。通常一个bug系统应该允许报告的人描述遇到的问题,界定发现bug时系统的环境和状态,以及重现bug的步骤。它还必须允许跟踪所有包含的问题。

         (12).设置Tinderbox:Tinderbox最初由Netscape开发,现在则作为一个开源软件由Mozilla项目负责维护。Tinderbox是设计用来管理在开发过程中,特别是涉及大量身处不同地方的开发人员的大型跨平台软件开发中的复杂性。Tinderbox的目标非常明确:在源码仓库发生任何修改时,以集中的方式立刻通知整个团队。通过持续地在每个所支持的平台上编译代码来告知目前源码仓库的“健康状况”。对于每个编译循环,每一次通过或失败都会报告到一个集中的地方。这让开发人员能够决定什么时候应该更新他们的本地代码树,从而避免获取到不能正确编译(或运行)的代码。结合以上两点,Tinderbox可以知晓是谁或者什么修改影响了代码树的“健康”,这些信息可以帮助我们尽可能迅速准确地解决问题。

         (13).用CVS或Subversion来管理源代码。

         (14).使用patch

         (15).为本地安装程序提供支持。

         (16).使用标准API。

         (17).考虑使用NSPR这样的平台抽象库:NSPR为一些常见的移植性问题提供了解决方案。

         (18).慎用浮点数:对于浮点数来说,硬件、编译器和编写代码的方式都可以影响结果的精确性。

         (19).显示地注明char类型的符号:不用char类型变量而用int类型变量来存放整型值。如果开始时值是以char保存的话,根据需要将它赋值给一个有符号或者无符号的整型变量。即使如果一定要用char类型变量来存放整数,也不要放任不管。

         (20).避免序列化二进制数据:在跨平台的应用程序里,数据序列化(就是把应用程序数据写到可持久保存的地方)需要额外的考量,主要是因为在一个平台上的程序所写下的数据要能在其他平台的程序的实例上被识别读取。

         (21).避免类型长度和组织带来的问题:整型是非常依赖于CPU和编译器的。

         (22).从Model里把用户界面分离出来:跨平台开发里最困难的地方之一就是应用程序的GUI部分。平台之间的外观、行为和创建UI所用的工具都非常的不同。

         (23). 开发一个跨平台用户界面的策略:两个最流行的跨平台桌面GUI工具包:wxWidgets和Qt。


跨平台的代码最好是在写的时候就已兼顾到多平台,即编写和调试分别在两个平台上同时进行。如果是先在一个平台开发后再来做移植,工作量可能会大很多。这种移植可能会用到很多重构方法,假如你没有很好的单元测试流程,那么大规模的重构将很有可能引入bug。

  在两个平台同时开发并不困难。首先,你最好能找到在这些平台都可以使用的工具,vim+makefile是个不错的选择,缺点是gdb的调试不是很方便。因此我通常会创建一个“_IDE”的目录,里面主要放一些工程文件(vs和xcode的),使用这些IDE调试会方便不少。

  跨平台开发选择一门跨平台的语言是必须的。像java或python这类可以自成一派的语言,几乎不用考虑不同平台的差异。这里主要讨论的是C/C++,它本身是非常具有可移植性的,只是由于一些历史原因,它的标准还没有完全普及。庆幸的是,作为一门可以直接跟系统打交道的语言,差异大多在不同系统提供的api上。


1. 操作系统扩展的C标准函数

  文件I/O操作是比较有代表性的,最初C的标准库文件寻址最大是4G,现在已有超过4G的文件,所以文件寻址从32位变成了64位,windwos上有了这一批api出现。
[cpp]  view plain  copy
  1. _ftelli64  
  2. _lseeki64  
  这些带下划线的都不可移植的。像这种功能不变,函数名有变的情况,通常我们直接用宏的方式重新命名。
[cpp]  view plain  copy
  1. #ifdef WIN32  
  2. #define _LSEEK _lseeki64  
  3. #else  
  4. #define _LSEEK lseek  
  5. #endif  
  另一些函数是被增强了。比如微软有很多安全版本的字符串处理函数。如果确实需要,最好显示地调用,而不是采用安全模板重载(Secure Template Overloads)。


2. 自己扩展C标准库

  广泛使用的C标准是C89,一些现在常用的函数并有包含其中,比如bool类型。于是多数人会自己定义一个BOOL类型的TRUE和FALSE。
[cpp]  view plain  copy
  1. #ifdef TRUE   
  2. #undef TRUE  
  3. #define TRUE 1  
  4. #endif  
  5.   
  6. #ifdef FALSE   
  7. #undef FALSE  
  8. #define FALSE 0  
  9. #endif  

  TRUE和FALSE是非常容易使用到的名字,先判断是否已经定义。另外,这类定义最好不要被其它人可见,因为大部分系统api都以返回0表示成功,如果你返回TRUE很有可能被别人认为是返回0.


3.  CPU对C的影响

  这种影响主要是CPU总线位数和对齐方式。
  int这种C标准类型,在16位和32位上长度是不一样的,64位在编译器上表示不同。解决这类问题,我们通常是给这些类型重新命名。
#ifdef WIN32
[cpp]  view plain  copy
  1. typdef __int64 int64_t   
  2. se  
  3.  typdef long long int64_t  
  4. dif  
  这些重定义一般都是放在stdint.h中。这个在*nix世界中已成为标准,最近的VS2010也加入了这个头文件。

  对齐方式情况比较复杂一些,但它通常不会影响计算,会影响到写入磁盘和内存里读数据。操作内存,只要不是把指针强制转换,一般不会有什么问题。而如果程序在一个小端对齐的环境中写的文件,再由大端对齐中读取,这样就会出现错误的结果。
  通常,我们会规定文件上的对齐方式,然后在程序中再做判断。比如:

[cpp]  view plain  copy
  1. int flipEndian(int x)  
  2. {  
  3. #if defined (__ppc__) || defined (__ppc64__)  
  4.     int y = x;  
  5.     ... do somthing   
  6.     x = y;  
  7. #endif  
  8.     return x;  
  9. }  

  在写入和读取的地方都应用这些函数,就可以比较轻松的应付了。


4. 不同系统提供的api不同

  api是操作系统自己的规范,当有两种及其以上的规范存在时,我们可以选择其中的一种规范(自己再实现一个规范当然不错,但没必要再把问题复杂化)。
  比如我们需要让线程休眠几秒种,windows和bsd上都有sleep函数,它们不仅写法有点差异,参数类型也不一样。这时我们只需要选择一种标准即可。
[cpp]  view plain  copy
  1. #ifdef WIN32  
  2. #include   
  3. #define sleep(t) Sleep((t)*1000)  
  4. #else  
  5. #include   
  6. #endif  

  我个人比较倾向于用bsd那一套,因为它们大多遵守POSIX规范,更通用一些。

  有些api比较复杂,用宏定义做不了,这时可以用inline实现更为复杂的表达。比如下面的
[cpp]  view plain  copy
  1. #ifdef WIN32  
  2. typedef void* pthread_t;  
  3. typedef void* pthread_attr_t;  
  4.   
  5. typedef void *(WINAPI* thread_proc_t)(void *);  
  6.   
  7. __inline int pthread_create(pthread_t * __restrict pHandle,  
  8.                           const pthread_attr_t * __restrict attr,  
  9.                           thread_proc_t threadStart,  
  10.                           void* arg)   
  11. {  
  12.     DWORD tid;  
  13.     *pHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)threadStart, arg, CREATE_SUSPENDED, &tid);  
  14.     return tid;  
  15. }  
  16.   
  17. #else  
  18. #define WINAPI  
  19. #endif  

  这种移植是以牺牲少部份功能为代价的,要确保你用到的是两个平台功能的交集。


5. 分开编译移植

  有时不同平台差异太大,上面这些小打小闹已经不能满足需要,是时候动点大手术了。
     通常,我们会声明一个共同的头文件,然后在不同平台有不同的实现。为了方便管理,通常会写在不同文件。比如同一个util.h,然后创建win和mac的两个目录,不同平台的util.c的实现放在对应的目录中即可。
一些设计模式可以派上用场。比如代理模式能减少一些冗余代码,工厂模式可以让你创建对象时更轻松一点。针对不同的需求,封装粒度也会不同。


6. 第三方工具

  boost是一个非常好用的库。它的社区很活跃,质量上乘,而且也是跨平台的。cygwin也是一个非常好用的产品,它在windows上模拟unix上的接口,几乎不用做任何修改就可以编译。缺点是必须带上一个额外的动态库。类似的产品还有很多。


  跨平台开发还有很多细节也需要注意,比如源代码文件的编码。一般使用utf-8最好。很多IDE对UTF-8编码的识别不好,可以加上BOM来帮助识别。如果你直接用VS或Xcode写,默认方式下在另一编辑器里,中文字符都会显示为乱码,而gvim在这方面就好很多。另外,文件名最好用全小写。



  上面总结的是底层代码,如果要移植C/C++的界面,先找到一套跨平台的界面框架吧,否则这基本上是不可能变成的任务。


你可能感兴趣的:(Linux,C)