在我们的开发中,跨平台的需求越来越强烈,如何保持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
其实,这些只是冰山一角,在跨平台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上。