概述
本日聊聊C++的可移植性题目。如果你日常平凡运用C++举行开辟,而且你对C++的可移植性题目不是非常清晰,那末我发起你看看这个系列。纵然你如今没有跨平台开辟的须要,相识可移植性方面的学问对你照样很有协助的。
C++的可移植性这个话题很大,包括了编译器、操纵体系、硬件体系等很多方面,每一个方面都有很多内容。鉴于本人才能、精神都有限,只能引见每一个方面最随意马虎遇到的题目,供大伙儿参考。
背面我会离别从编译器、C++语法、操纵体系、第三方库、辅助工具、开辟流程等方面举行引见。
编译器
在跨平台的开辟过程当中,很多题目都和编译器有关。因而我们先来聊聊编译器相干的题目。
编译器的挑选
起首,GCC是优先要斟酌支撑的,因为险些一切操纵体系平台都有GCC可用。它基础上成了一个通用的编译器了。如果你的代码在A平台的GCC能够编译经由过程,今后拿到B平台用类似版本的GCC编译,平常也不会有太大题目。因而GCC是肯定要斟酌支撑的。
其次,要斟酌是不是支撑当地编译器。所谓当地编译器就是操纵体系厂商自产的编译器。比如:相关于Windows的当地编译器就是Visual C++。相关于Solaris的当地编译器就是SUN的CC。如果你对机能比较敏感或许想用到某些当地编译器的高等功用,能够就得斟酌在支撑GCC的同时也支撑当地编译器。
编译正告
编译器是递次员的朋侪,很多潜伏的题目(包括可移植性),编译器都是能够发明并给出正告的,如果你日常平凡注重这些正告信息,能够削减很多贫苦。因而我强烈发起:
1把编译器的正告级别调高;
2不要随意马虎疏忽编译器的正告信息。
交织编译器
交织编译器的定义拜见“维基百科”。浅显地说,就是在A平台上编译出运转在B平台上的二进制递次。假定你要开辟的运用是运转在Solaris上,然则你手头没有能够运转Solaris的SPARC机械,这时刻交织编译器就能够派上用场了。平常状况下都运用GCC来制造一个交织编译器,限于篇幅,这里就不深切聊了。有兴致的同砚能够拜见“这里”。
非常处置惩罚
上一个帖子“语法”因为篇幅有限,没来得及聊非常,如今把和非常相干的部份零丁拿出来讲一下。
警惕new分派内存失利
初期的老式编译器生成的代码,如果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,在你盘算写跨平台代码之前,要改掉这个习气。包括有SEH的代码只能在Windows平台上编译经由过程,肯定没法跨平台的。
关于catch(…)
照理说,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体系。
文件体系(FileSystem以下简称FS)
刚最先搞跨平台开辟的新手,多数都邑碰上和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,所以大伙儿照样要尽最大勤奋确保一切线程都能够天然殒命。
相干引荐:
Python和ruby之间的异同对照
Java言语简介(动力节点整顿)
以上就是C++的可移植性和跨平台开辟(长文)的细致内容,更多请关注ki4网别的相干文章!