今天聊聊C++的可移植性问题。若是你平时使用C++进行开发,而且你对C++的可移植性问题不是很是清楚,那么我建议你看看这个系列。即便你目前没有跨平台开发的须要,了解可移植性方面的知识对你仍是颇有帮助的。程序员
C++的可移植性这个话题很大,包括了编译器、操做系统、硬件体系等不少方面,每个方面都有不少内容。鉴于本人能力、精力都有限,只能介绍每个方面最容易碰到的问题,供大伙儿参考。编程
后面我会分别从编译器、C++语法、操做系统、第三方库、辅助工具、开发流程等方面进行介绍。数组
在跨平台的开发过程当中,不少问题都和编译器有关。所以咱们先来聊聊编译器相关的问题。安全
首先,GCC是优先要考虑支持的,由于几乎全部操做系统平台都有GCC可用。它基本上成了一个通用的编译器了。若是你的代码在A平台的GCC可以编译经过,以后拿到B平台用相似版本的GCC编译,通常也不会有太大问题。所以GCC是确定要考虑支持的。网络
其次,要考虑是否支持本地编译器。所谓本地编译器就是操做系统厂商自产的编译器。例如:相对于Windows的本地编译器就是Visual C++。相对于Solaris的本地编译器就是SUN的CC。若是你对性能比较敏感或者想用到某些本地编译器的高级功能,可能就得考虑在支持GCC的同时也支持本地编译器。多线程
编译器是程序员的朋友,不少潜在的问题(包括可移植性),编译器都是能够发现并给出警告的,若是你平时注意这些警告信息,能够减小不少麻烦。所以我强烈建议:并发
1把编译器的警告级别调高;ide
2不要轻易忽略编译器的警告信息。函数
交叉编译器的定义参见“维基百科”。通俗地说,就是在A平台上编译出运行在B平台上的二进制程序。假设你要开发的应用是运行在Solaris上,可是你手头没有可以运行Solaris的SPARC机器,这时候交叉编译器就能够派上用场了。通常状况下都使用GCC来制做一个交叉编译器,限于篇幅,这里就不深刻聊了。有兴趣的同窗能够参见“这里”。工具
上一个帖子“语法”因为篇幅有限,没来得及聊异常,如今把和异常相关的部分单独拿出来讲一下。
早期的老式编译器生成的代码,若是new失败会返回空指针。我当年用的Borland C++ 3.1彷佛就是这样的,如今这种编译器应该很少见了。若是你目前用的编译器还有这种行为,那你就惨了。你能够考虑重载new操做符来抛出 bad_alloc异常,便于进行异常处理。
稍微新式一点的编译器,就不是仅仅返回空指针了。当new操做符发现内存告急,按照标准的规定(参见C++ 03标准18.4.2章节),它应该去调用new_handler函数(原型为typedef void (*new_handler)();)。标准建议new_handler函数干以下三件事:
1、设法去多搞点内存来;
2、抛出bad_alloc异常;
3、调用abort()或者exit()退出进程。
因为new_handler函数是能够被从新设置的(经过调用set_new_handler),因此上述的行为它均可能有。
综上所述,new分配内存失败,有可能三种可能:
1、返回空指针;
2、抛出异常;
3、进程当即终止。
若是你但愿你的代码具备较好的移植性,你就得把这三种状况都考虑到。
异常规格在我看来不是一个好东西,不信能够去看看《C++ Coding Standards - 101 Rules, Guidelines & Best Practices》的第75条。(具体有哪些坏处之后专门开一个C++异常和错误处理的帖子来聊)言归正传,按照标准(参见03标准18.6.2章节),若是一个函数抛到外面的异常没有包含在该函数的异常规范中,那么应该调用unexcepted()。可是并不是全部编译器生成的代码都遵照标准(好比某些版本的VC编译器)。若是你的须要支持的编译器在异常规范上的行为不一致,那就得考虑去掉异常规范声明。
此处说的模块是指动态库。若是你的程序包含有多个动态库,不要把异常抛到模块的导出函数以外。毕竟如今C++尚未ABI标准(估计未来也未必会有),跨模块抛出异常会有不少不可预料的行为。
若是你历来没有据说过SEH,那就当我没说,跳过这段。若是你之前习惯于用SEH,在你打算写跨平台代码以前,要改掉这个习惯。包含有SEH的代码只能在Windows平台上编译经过,确定没法跨平台的。
照理说,catch(...)语句只可以捕获C++的异常类型,对于访问违例、除零错等非C++异常是无能为力的。可是某些状况下(好比某些VC编译器),诸如访问违例、除零错也能够被catch(...)捕获。因此,你若是但愿代码移植性好,就不能在程序逻辑中依赖上述catch(...)的行为。
此次聊的话题主要是和硬件体系有关的。好比你的程序须要支持不一样类型的CPU(x86、SPARC、PowerPC),或者是同种类型不一样字长的CPU(好比x86和x86-64),这时候你就须要关心一下硬件体系的问题。
C++中基本类型的大小(占用的字节数)会随着CPU字长的变化而变化。因此,假如你要表示一个int占用的字节数,千万不要直接写“4”(顺便说一下,直接写“4”还犯了Magic Number的大忌,详见这里),而应该写“sizeof(int)”;反过来,若是你要定义一个大小必须为4字节的有符号整数,也不要直接用int,要用预先typedef好的定长类型(好比boost库的int32_t、ACE库的ACE_INT32、等)。
差点忘了,指针的大小也有上述的问题,也要当心。
若是你没据说过“字节序”这玩意儿,请看“维基百科”。通俗地打个比方,在一个大尾序的机器上有一个4字节的整数0x01020304,经过网络或者文件传到一台小尾序的机器上就会变成0x04030201;听说还有一种中尾序的机器(不过我没接触过),上述整数会变成0x02010403。
若是你编写的应用程序中涉及网络通信,必定要在记得进行主机序和网络序的翻译;若是涉及跨机器传输二进制文件,也要记得进行相似的转换。
若是你不晓得“内存对齐”是什么东东,请看“维基百科”。简单来讲,出于CPU处理上的性能考虑,结构体中的数据不是紧挨着的,而是要空开一些间隔。这样的话,结构体中每一个数据的地址正好都是某个字长的整数倍。
因为C++标准中没有定义内存对齐的细节,所以,你的代码也不能依赖对齐的细节。凡是计算结构体大小的地方,都老老实实写上sizeof()。
有些编译器支持#pragma pack预处理语句(能够用来修改对齐字长),不过这种语法不是全部编译器都支持,要慎用。
对于有符号整数的右移操做,有些系统默认使用算数右移(最高的符号位不变),有些默认使用逻辑右移(最高的符号位补0)。因此,不要对有符号整数进行右移操做。顺便说一下,即便没有移植性问题,代码中也尽可能少用移位运算符。那些企图用移位运算来提升性能的同窗更要注意了,这么干不但可读性不好,并且吃力不讨好。只要不太弱智的编译器,都会自动帮你搞定这种优化,无须程序员操心。
上一个帖子提到了“硬件体系”相关的话题,今天来讲说和操做系统相关的话题。C++跨平台开发中和OS相关的杂事挺多,因此今天会啰嗦比较长的篇幅,请列位看官见谅 :-)
为了避免绕口,如下把Linux和各类Unix统称为Posix系统。
刚开始搞跨平台开发的新手,多半都会碰上和FS相关的问题。因此先来聊一下FS。概括下来,开发中容易碰上的FS差别主要有以下几个:目录分隔符的差别;大小写敏感的差别;路径中禁用字符的差别。
为了应对上述差别,你要注意以下几点:
1、文件和目录命名要规范
在给文件和目录命名时,尽可能只使用字母和数字。不要在同一个目录下放两个名称类似(名称中只有大小写不一样,例如foo.cpp与Foo.cpp)的文件。不要使用某些OS的保留字(例如aux、con、nul、prn)做文件名或目录名。
补充一下,刚才说的命名,包括了源代码文件、二进制文件和运行时建立的其它文件。
2、#include语句要规范
当你写#include语句时,要注意使用正斜线“/”(比较通用)而不要使用反斜线“\”(仅在Windows可用)。#include语句中的文件和目录名要和实际名称保持大小写彻底一致。
3、代码中涉及FS操做,尽可能使用现成的库
已经有不少成熟的、用于FS的第三方库(好比boost::filesystem)。若是你的代码涉及到FS的操做(好比目录遍历),尽可能使用这些第三方库,能够帮你省很多事情。
★文本文件的回车CR/换行LF
因为几个知名的操做系统对回车/换行的处理不一致,致使了这个烦人的问题。目前的局面是:Windows同时使用CR和LF;Linux和大部分的Unix使用LF;苹果的Mac系列使用CR。
对于源代码管理,好在不少版本管理软件(好比CVS、SVN)都会智能地处理这个问题,让你从代码库取回本地的源码能适应本地的格式。
若是你的程序须要在运行时处理文本文件,要留意本文方式打开和二进制方式打开的区别。另外,若是涉及跨不一样系统传输文本文件,要考虑进行适当的处理。
★文件搜索路径(包括搜索可执行文件和动态库)
在Windows下,若是要执行文件或者加载动态库,通常会搜索当前目录;而Posix系统则不尽然。因此若是你的应用涉及到启动进程或加载动态库,就要当心这个差别。
★环境变量
对于上述提到的搜索路径问题,有些同窗想经过修改PATH和LD_LIBRARY_PATH来引入当前路径。假如使用这种方法,建议你只修改进程级的环境变量,不要修改系统级的环境变量(修改系统级有可能影响到同机的其它软件,产生反作用)。
★动态库
若是你的应用程序使用动态库,强烈建议动态库导出标准C风格的函数(尽可能不要导出类)。若是在Posix系统中加载动态库,切记慎用RTLD_GLOBAL标志位。这个标志位会Enable全局符号表,有可能会致使多个动态库之间的符号名冲突(一旦碰到这种事,会出现匪夷所思的运行时错误,极难调试)。
★服务/看守进程
若是你不清楚服务和看守进程的概念,请看维基百科(这里和这里)。为了叙述方便,如下统称服务。
因为C++开发的模块大部分是后台模块,常常会碰到服务的问题。编写服务须要调用好几个系统相关的API,致使了与操做系统的紧密耦合,很难用一套代码搞定。所以比较好的办法是抽象出一个通用的服务外壳,而后把业务逻辑代码做为动态库挂载到它下面。这样的话,至少保证了业务逻辑的代码只须要一套;服务外壳的代码虽然须要两套(一个用于Windows、一个用于Posix),但他们是业务无关的,能够很方便地重用。
★默认栈大小
不一样的操做系统,栈的默认大小差异很大,从几十KB(听说Symbian只有12K,真抠门)到几MB不等。所以你事先要打听一下目标系统的默认栈大小,若是碰上像Symbian这样抠门的,能够考虑用编译器选项调大。固然,养成“不在栈上定义大数组/大对象”的好习惯也很重要,不然再大的栈也会被撑爆的。
最近一个多月写的帖子比较杂,致使本系列又很久没更新了。结果又有网友在评论中催我了,搞得我有点囧。今天赶忙把多线程篇补上。上次聊操做系统 的时候,因为和OS有关的话题比较琐碎,杂七杂八说了一大堆。当时一看篇幅有点长,就把多进程和多线程的部分给留到后面了。
★编译器
◇关于C运行库选项
先来讲一个很基本的问题:关于C运行库(后面简称CRT:C Run-Time)的设置。原本不想聊这么低级的问题,但周围有好几我的都在这个地方吃过亏,因此仍是讲一下。
大部分C++编译器都会自带有CRT(可能还不止一个)。某些编译器自带的CRT可能会根据线程的支持分为单线程CRT和多线程CRT两类。当你要进行多线程开发的时候,别忘了确保相关的C++工程项目使用的是多线程的CRT。不然会死得很难看。
尤为当你使用Visual C++建立工程项目,更加要当心。若是新建的工程项目是不含MFC的(包括Console工程和Win32工程),那工程的默认设置会是使用“单线程CRT”,以下图所示:
◇关于优化选项
“优化选项”是另外一个很关键的编译器相关话题。有些编译器提供号称很牛X的优化选项,可是某些优化选项可能会有潜在的风险。编译器可能自做主张打乱执行指令的顺序,从而致使出乎意料的线程竞态问题(Race Condition,详细解释看“这里 ”)。刘未鹏同窗在“C++多线程内存模型 ”里举了几个典型的例子,大伙儿能够去瞧一瞧。
建议只使用编译器常规的速度优化选项便可。其它那些花哨的优化选项,增长的效果未必明显,可是潜在的风险不小。实在不值得冒险。
以GCC为例:建议用-O2 选项便可(其实-O2 是一堆选项的集合),不必冒险用-O3 (除非你有很充足的理由)。除了-O2 和-O3 以外,GCC还有一大坨(估计有上百个)其它的优化选项。若是你企图用当中的某个选项,必定要先把它的特性、可能的反作用都摸清楚,不然未来死都不知道怎么死的。
★线程库的选择
因为当前的C++ 03标准几乎没有涉及线程相关的内容(即便未来C++ 0x包含了线程的标准库,编译器厂商的支持在短时间内也未必全面),因此在将来很长的一段时间,跨平台的多线程支持仍是要依赖第三方库。因此线程库的选择是大大滴重要。下面大体介绍一下几个知名的跨平台线程库。
◇ACE
先说一下ACE这个历史悠久的库。若是你以前从未接触过它,先看“这里 ”扫盲。从ACE的全称(Adaptive Communication Environment)来看,它应该是以“通信”为主业。不过ACE对“多线程”这个副业的支持仍是很是全面的,好比互斥锁(ACE_Mutex)、条件变量(ACE_Condition)、信号量(ACE_Semaphore)、栅栏(ACE_Barrier)、原子操做(ACE_Atomic_Op)等等。对某些类型好比ACE_Mutex还细分为线程读写锁(ACE_RW_Thread_Mutex)、线程递归锁(ACE_Recursive_Thread_Mutex)等等。
除了支持很全面,ACE还有另外一个很明显的优势,就是对各类操做系统平台及其自带的编译器支持很好。包括一些老式的编译器(好比VC6),它也可以支持(此处所说的支持 ,不光是能编译经过,并且要能稳定运行)。这个优势对于跨平台开发那是至关至关滴明显。
那缺点捏?因为ACE开工的年头很早(大概是上世纪九十年代中期),那会儿不少C++的老特性都还没出来(更别提新特性了),因此感受ACE整个的风格比较老气,远不如boost那么时髦前卫。
◇boost::thread
boost::thread正好和ACE造成鲜明对照。这玩意貌似从boost 1.32版本开始引入,年头比ACE短。不过得益于boost里一帮大牛的支持,发展仍是蛮快的。到目前的boost 1.38版本,也可以支持许多特性了(不过彷佛没ACE多)。鉴于不少C++标准委员会的成员云集在boost社区中,随着时间的推移,boost::thread终将成为C++线程的明日之星,前途无量啊!
boost::thread的缺点就是支持的编译器不够多,尤为是一些老式 编译器(不少boost的子库都有此问题,多半由于用了一些高级的模板语法)。这对于跨平台而言一个比较明显的问题。
◇wxWidgets 和QT
wxWidgets和QT都是GUI界面库,可是它们也都内置和对线程的支持。wxWidgets线程的简介能够看“这里 ”,关于QT线程的简介能够看“这里 ”。这两个库对线程的支持差很少,都提供了诸如mutex、condition、semaphore等经常使用的机制。不过特性没有ACE丰富。
◇如何权衡
对于开发GUI软件并已经用上了wxWidgets或者QT,那你能够直接用它们内置的线程库(前提是你只用到基本的线程功能)。因为它们内置的线程库,特性稍嫌单薄。万一你须要某高级的线程功能,那得考虑替换成boost::thread或ACE。
至于boost::thread和ACE的取舍,主要得看软件的需求了。若是你要支持的平台挺多挺杂,那建议选用ACE,以避免碰上编译器不支持的问题。若是你只须要支持少数几个主流的平台(好比Windows、Linux、Mac),那建议用boost::thread。毕竟主流操做系统上的编译器,对boost的支持仍是蛮好的。
★编程上的注意事项
其实多线程开发,须要注意的地方挺多的,我只能大体列几个印象比较深的注意事项。
◇关于volatile
说到多线程编程可能碰到的陷阱,那就不得不提到volatile 关键字。若是你对它还不甚了解,先看“这里 ”扫盲一下。因为C++ 98和C++ 03标准都没有定义多线程的内存模型,而标准中也就volatile 和线程沾点儿边。结果致使C++社区中有至关多的口水都集中在volatile 身上(其中有很多C++大牛的口水)。有鉴于此,我这里就再也不多啰嗦了。推荐几个大牛的文章:Andrei Alexandrescu 的文章“这里 ”、还有Hans Boehm的文章“这里 ”和“这里 ”。大伙儿自个儿去拜读一下。
◇关于原子操做
有些同窗光知道多个线程的竞争写 须要加锁,殊不知道多个读 单个写 也须要保护。好比有某个整数int nCount = 0x01020304;在并发状态下,一个写线程去修改它的值nCount = 0x05060708;另外一个读线程去获取该值。那么读线程有没有可能读取到一个“坏”的(好比0x05060304)数据捏?
数据是否坏掉,取决于对nCount的读和写是否属于原子操做。而这就依赖于不少硬件相关的因素了(包括CPU的类型、CPU的字长、内存对齐的字节数等)。在某些状况下,确实可能出现数据坏掉。
因为咱们讨论的是跨平台的开发,天晓得未来你的代码会在啥样的硬件环境下执行。因此在处理相似问题的时候,仍是要用第三方库提供的原子操做类/函数(好比ACE的Atomic_Op)来确保安全。
◇关于对象的析构
在以前的系列帖子“C++对象是怎么死的? ”里面,已经分别介绍了Win32平台和Posix平台下线程的非天然死亡问题。
因为上述几个跨平台的线程库底层仍是要调用操做系统自带的线程API,因此大伙儿仍是要尽最大努力确保全部线程都可以天然死亡。