跨平台C/C++开发的总结

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

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

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


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

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


2. 自己扩展C标准库

  广泛使用的C标准是C89,一些现在常用的函数并有包含其中,比如bool类型。于是多数人会自己定义一个BOOL类型的TRUE和FALSE。
#ifdef TRUE 
#undef TRUE
#define TRUE 1
#endif

#ifdef FALSE
#undef FALSE
#define FALSE 0
#endif

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


3.  CPU对C的影响

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

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

int flipEndian(int x)
{
#if defined (__ppc__) || defined (__ppc64__)
int y = x;
... do somthing
x = y;
#endif
return x;
}

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


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

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

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

  有些api比较复杂,用宏定义做不了,这时可以用inline实现更为复杂的表达。比如下面的
#ifdef WIN32
typedef void* pthread_t;
typedef void* pthread_attr_t;

typedef void *(WINAPI* thread_proc_t)(void *);

__inline int pthread_create(pthread_t * __restrict pHandle,
const pthread_attr_t * __restrict attr,
thread_proc_t threadStart,
void* arg)
{
DWORD tid;
*pHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)threadStart, arg, CREATE_SUSPENDED, &tid);
return tid;
}

#else
#define WINAPI
#endif

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


5. 分开编译移植

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


6. 第三方工具

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


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



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