我目前工作中的大多数项目是在aix上跑的,最近有个aix的c++项目要移植到linux上,而我个人喜欢使用VC作为开发工具。因为这样,需要对项目windows,aix,linux上的移植,在这个过程中作了些总结。
现假设平台与编译工具对应如下:
windows――vc
aix――xlc
linux――gcc
目录:
1.3.1. 派生类如果使用基类的函数或成员变量,应该在前面加“this->”,或在类声明中加入“using 基类函数;”
1.3.2. 使用了基类或其它模板类里定义的类型,要在类型前加typename
1. c++标准
c++的发展已经有几十年历史了,但标准是在1998年制定的,在标准之前,出现了很多编译器,各个编译器对于语法上存在一定差异。
像vc与xlc这种是属于企业级应用的工具,会追求向前兼容。以保证以前的代码能够使用,所以如果有不怎么标准的代码,编译器也会让它通过。而gcc比较偏向于研究应用的工具,会追求标准化。
这就产生了有些代码在vc或xlc上通过,而在gcc上编不过的情况。
1.1. 变量、函数重定义
1.1.1. 避免在头文件里定义变量。
假设a.h定义了一个变量int error;
b.cpp中#include “a.h”
c.cpp中#include “a.h”
则在同一个程序中,error定义了两次,编译器在链接的时候报错。
1.1.2. 避免在头文件里定义外部函数
函数的道理其实跟变量差不多,也会出现重定义。但如果一定要把函数写在头文件可以有三种方法:
a.加上inline
b.加上static
c.使用一个空的名字空间把函数包起来。
变量其实也可以用static或空名字空间来解决重定义的问题,但变量的问题比函数复杂,在头文件里使用了全局变量不但带来编译的问题,还会容易引入潜在错误,如果要使用多线程,则使到问题越发复杂。
如果一定要使用全局变量有一个比较好的方法,定义一个全局函数,函数是定义一个静态变量,函数返回该变量的一个引用,事例如下:
// a.h中
int& theError();
// a.cpp中
int& theError()
{
static int s_error;
return s_error;
}
哪个地方要使用error这个全局变量的话,#include”a.h”后直接用theError()就好了。比如你可以这样用:
theError() = -1;
另外,也不建议在头文件中引入名字空间。如果在头文件中using namespace std,使用字符串组件用“string”的名字就可以了,但在stl标准化之前(甚至之后),大量的字符串组件存在,别人也可能使用“string”这个名字。一不小心就冲突了。
1.2. 函数返回值
c语言以及c++98标准制定之前的c++,函数声明或定义如果没有返回值,则隐式定义为返回int。但c++98标准之后,规定函数必须制定返回值。
对于类似于以下的代码:
fun(int, char);
早期的gcc,以及目前的大多数编译器都可以通过,但比较新版的gcc是会报错的。
1.3. 模板
*注:以下属于编译器细节,如果不想了解或看完后还是不了解的话,可以略过:
c++标准制定前,模板相当于宏,在用到模板的地方会根据模板参数直接把模板代码翻译为非模板的代码。
如果事情是这样先翻译后编译的话,模板在各个编译器上应该是差不多的了。但c++委员会为了使到模板的错误信息(即在写模板的时候就把代码写错了)更加易于理解,以及为了在解析模板的阶段检查潜在的隐患,规定模板的查找分为两个阶段,第一阶段在看到模板的定义时,第二阶段是模板实例化时。并且标准规定“对于模板中的非依赖型名称,将会在看到的第一时间进行查找”。有了这个概念后,让我们考虑下面的例子
template<typename T>
class DD : public Base<T>
{
public:
void f() { basefield = 0; } // (1) 代码有问题,标准c++会在第一阶段查找时给出错误。
};
template<>
class Base<bool>
{
public:
enum { basefield = 42; }
};
void g(DD<bool>& d) { d.f(); }
如果你不想在第一阶段就进行查找,那么可以在basefield前加上this->或Base<T>::,Base<T>::basefield的名字是依赖型的(第一阶段时,T还没定义,basefield具体是什么,依赖于T,直到实例化时才可以知道T是什么),C++标准规定“对于依赖型的名字,会推迟到实例化的时候查找”。查找推迟后,错误是还有,不过具体错误已经清晰定位了。
c++模板的确是个挺复杂的东东,就算你不写模板代码,也免不了要跟模板打交道,比如使用stl。编写或使用模板时要注意一些规则:
1.3.1. 派生类如果使用基类的函数或成员变量,应该在前面加“this->”,或在类声明中加入“using 基类函数;”
比如:
template<typename CharType>
class CMyString : public std::basic_string<CharType>
{
public:
size_t Length()
{
return this->length(); // 这里会调用std::basic_string<CharType>::length()函数。
}
};
或
template<typename CharType>
class CMyString : public std::basic_string<CharType>
{
using std::basic_string<CharType>::length; // 声明使用基类的length函数。
public:
size_t Length()
{
// 有了std::basic_string<CharType>::length的声明,length()就会成为依赖型(依赖于CharType),直到实例化时才查找该函数的具体地址,如果没有using length的声明,length()会作为一个非依赖型名字,编译器会在全局函数里查找,如果找不到,则报错,如果找到,则调用全局函数length()。
return length(); // 这里会调用std::basic_string<CharType>::length()函数。
}
};
1.3.2. 使用了基类或其它模板类里定义的类型,要在类型前加typename
比如:
template<typename CharType> inline
void Fun(std::basic_string<CharType>& str)
{
// std::basic_string<CharType>::iterator 是一个名字,
// 且该名字依赖于模板类std::basic_string的参数CharType,
// 在CharType还没确定时,iterator有可能是一个类型名,也有可能是静态变量名,或是一个枚举名。
// 加上typename后,指明std::basic_string<CharType>::iterator是一个类型名。
typename std::basic_string<CharType>::iterator it = str.begin();
}
1.4. 标准与现实之间
当然,对于以上这些标准vc和xlc执行得不是太严格,不按照上面这样用也可以通过,但gcc是严格按照标准办事的。如果代码想要有比较强的移植性以及健壮性的话,我们自己还是按照标准写为好。
2. 操作系统
2.1. API
这个大家应该会比较清楚。
写程序免不了要使用操作系统的功能,windows与linux、unix定义了不同的API用来访问操作系统。
如果程序必须操作系统的特定功能的话,那就没得说了(不过这样的程序在设计之初就没打算过要跨平台)。
如果程序使用的是一些操作系统的公共功能的话,那么使用以下形式的代码
#ifdef WIN32
调用windows的API
#else ifdef LINUX
调用linux的API
#else
调用XXX系统的API
#endif
编译的时候,只需要把相关平台上加上这些宏就会自动调用自身平台的API了。
2.2. 硬件 32位 64位
目前windows的硬件是以32位的intel处理器为主,而大多数unix是64位的专用处理器。
对于long以及指针类型来说,在windows平台是32位,而在aix可能就是64位了。VC是使用__int64来作为64位类型的,windows上还大量使用了一个叫LARGEINT的结构体来作为64位变量的储存:
typedef struct _LARGEINT
{
ULONG LowLong;
LONG HighLong;
} LARGEINT;
typedef LARGE_INTEGER *LPLARGEINT;
3. 编译器对语言的扩展
3.1. 编译器命令与选项
因为本文主要讨论代码的可移植性方面的,由于各编译器命令和选项繁多,不一一列出了。
3.2. 函数的调用约定:
调用约定 | 语义 |
cdecl | 从右到左地将被调用函数的参数压入堆栈。在执行完毕之后,由调用函数将参数弹出堆栈。 |
stdcall | 从右到左地将被调用函数的参数压入堆栈。在执行完毕之后,由调用函数将参数弹出堆栈。 |
fastca | 将最前面的两个参数传递到 ECX 和 EDX 寄存器中,同时将所有其他参数从右到左地压入堆栈。由被调用函数负责清除执行后的堆栈。 |
为了支持这些函数属性,比如stdcall,VC中是在函数前加 “__stdcall”或“_stdcall”,而gcc是函数前加“__attribute__((stdcall))”。xlc是两种方法都支持。
3.3. dll函数符号导出
VC使用“__declspec(dllexport)”导出函数符号,使用“__declspec(dllimport)”导入。但为了得到纯C(名字完全没有修改过)函数,建议使用.def文件导出。
gcc使用__attribute__ ((visibility("hidden")))声明为不导出,使用((visibility("default")))声明为导出。如果没有声明,则默认为导出。但因为大多数函数是不导出的,建议在编译时加上-fvisibility=hidden选项,这样,只有没有做声明或声明为hidden的都不导出,这样可以加快dll的加载速度。
xlc也是默认为导出。
3.4. 宽字符
c++为宽字符制定了标准类型wchar_t,但有些编译器是以扩展的形式提供该类型的。比如VC,你要#include<wchar.h>才能使用wchar_t,而gcc是把wchar_t作为内建类型。
注意,VC提供了选项是否把wchar_t作为内建类型,但使用它之前,最好你的工程中用到的其它静态库都是使用该选项的,要不然就会链接不过。
4. Unicode,使用icu
关于unicode本身就是一个很复杂的问题,这里不作专门讨论了。
c++代码的字符类型分为单字节和宽字节,经常需要做字符编码间的转换。之前做一个xml读写的项目,需要utf16和gb2312格式间的互转,但系统自带的函数和xerser-c带的函数在windows上是可以正常工作的,在aix上却不可以转换中文字符。找了些资料,IBM的icu(International Component for Unicode)组件可以解决这个问题。
下载icu-4代码,编译后得到icuuc库组件和一个叫“include”的目录(里面是头文件)。把包含路径设到它的“include”,以及链接icuuc后,以下代码可以在多个平台上实现utf16和gb2312的转换。
// XMLString自带的转换函数在aix上不能把utf16转成gb2312。
// 改为使用icu的转换函数。
// {{
#ifndef UCNV_H
#include "unicode/ucnv.h"
#endif
inline
char* UTF16_to_gb2312(const wchar_t* strtranscode, size_type len)
{
const size_type sizeDest = (len + 1) * 2;
char* pResult = new char[sizeDest];
UErrorCode status = U_ZERO_ERROR;
UConverter *conv = ucnv_open("gb2312", &status);
assert(U_SUCCESS(status));
ucnv_fromUChars(conv,
pResult, sizeDest,
(const UChar*)strtranscode, len,
&status);
ucnv_close(conv);
return pResult;
}
inline
wchar_t* gb2312_to_UTF16(const char* strtranscode, size_type len)
{
const size_type sizeDest = (len + 1);
wchar_t* pResult = new wchar_t[sizeDest];
UErrorCode status = U_ZERO_ERROR;
UConverter *conv = ucnv_open("gb2312", &status);
assert(U_SUCCESS(status));
ucnv_toUChars(conv,
(UChar*)pResult, sizeDest,
strtranscode, len,
&status);
ucnv_close(conv);
return pResult;
}
// }}
5. 封装
对于编写跨平台的代码当然好,但为了跨平台,少不了”#ifdef WIN32”之类的代码,这种代码一但多了,会很影响阅读的。应该把这些公共功能包装起来,提供一个可以被多个平台访问的接口,特别是对于API的封装。
5.1.1. ACE
现在有很多开源的类库有API的封装,比如ACE等。但只是实现了轻量级的封装,大概是线程、进程、互斥体、锁等。ACE虽然是对系统作了个轻量级封装,但其本身却是一个重量级的库。
5.1.2. QT
API中最复杂的是窗体和图形设备相关的,Windows的窗体和图形设备API加起至少有几千个。实现这些封装可以称得上是重量级的了,比如QT,但QT是一种比较特殊的开源,不给钱而使用它开发营利性软件会引起连锁性的法律问题。
Kylix(delphi的linux版)上所使用的支持跨平台的CLX组件库就是基于QT的。Kylix支持pascal和c++。不过用c++就比较转折了,先是用c++调用pascal写的VCL,再用VCL调用c++写的CLX,CLX再调用QT。如果刚好你程序在这段出现问题想调试一下代码,就头大了。(有空打算一篇写关于跨语言的文章)