C++的可移植性和跨平臺開發

概述

  今天聊聊C++的可移植性問題。若是你平時使用C++進行開發,而且你對C++的可移植性問題不是很是清楚,那麼我建議你看看這個系列。即便你目前沒有跨平臺開發的須要,瞭解可移植性方面的知識對你仍是頗有幫助的。程序員

  C++的可移植性這個話題很大,包括了編譯器、操做系統、硬件體系等不少方面,每個方面都有不少內容。鑑於本人能力、精力都有限,只能介紹每個方面最容易碰到的問題,供大夥兒參考。編程

  後面我會分別從編譯器、C++語法、操做系統、第三方庫、輔助工具、開發流程等方面進行介紹。數組

編譯器

  在跨平臺的開發過程當中,不少問題都和編譯器有關。所以咱們先來聊聊編譯器相關的問題。安全

編譯器的選擇

  首先,GCC是優先要考慮支持的,由於幾乎全部操做系統平臺都有GCC可用。它基本上成了一個通用的編譯器了。若是你的代碼在A平臺的GCC可以編譯經過,以後拿到B平臺用相似版本的GCC編譯,通常也不會有太大問題。所以GCC是確定要考慮支持的。網絡

  其次,要考慮是否支持本地編譯器。所謂本地編譯器就是操做系統廠商自產的編譯器。例如:相對於Windows的本地編譯器就是Visual C++。相對於Solaris的本地編譯器就是SUNCC。若是你對性能比較敏感或者想用到某些本地編譯器的高級功能,可能就得考慮在支持GCC的同時也支持本地編譯器。多線程

編譯警告

編譯器是程序員的朋友,不少潛在的問題(包括可移植性),編譯器都是能夠發現並給出警告的,若是你平時注意這些警告信息,能夠減小不少麻煩。所以我強烈建議:併發

1把編譯器的警告級別調高;ide

2不要輕易忽略編譯器的警告信息。函數

交叉編譯器

  交叉編譯器的定義參見「維基百科」。通俗地說,就是在A平臺上編譯出運行在B平臺上的二進制程序。假設你要開發的應用是運行在Solaris上,可是你手頭沒有可以運行SolarisSPARC機器,這時候交叉編譯器就能夠派上用場了。通常狀況下都使用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(...)的行爲。

硬件體系相關

  此次聊的話題主要是和硬件體系有關的。好比你的程序須要支持不一樣類型的CPUx86SPARCPowerPC),或者是同種類型不一樣字長的CPU(好比x86x86-64),這時候你就須要關心一下硬件體系的問題。

基本類型的大小

  C++中基本類型的大小(佔用的字節數)會隨着CPU字長的變化而變化。因此,假如你要表示一個int佔用的字節數,千萬不要直接寫「4」(順便說一下,直接寫「4」還犯了Magic Number的大忌,詳見這裏),而應該寫「sizeof(int)」;反過來,若是你要定義一個大小必須爲4字節的有符號整數,也不要直接用int,要用預先typedef好的定長類型(好比boost庫的int32_tACE庫的ACE_INT32、等)。

  差點忘了,指針的大小也有上述的問題,也要當心。

字節序

  若是你沒據說過「字節序」這玩意兒,請看「維基百科」。通俗地打個比方,在一個大尾序的機器上有一個4字節的整數0x01020304,經過網絡或者文件傳到一臺小尾序的機器上就會變成0x04030201;聽說還有一種中尾序的機器(不過我沒接觸過),上述整數會變成0x02010403

  若是你編寫的應用程序中涉及網絡通信,必定要在記得進行主機序和網絡序的翻譯;若是涉及跨機器傳輸二進制文件,也要記得進行相似的轉換。

內存對齊

  若是你不曉得「內存對齊」是什麼東東,請看「維基百科」。簡單來講,出於CPU處理上的性能考慮,結構體中的數據不是緊挨着的,而是要空開一些間隔。這樣的話,結構體中每一個數據的地址正好都是某個字長的整數倍。

  因爲C++標準中沒有定義內存對齊的細節,所以,你的代碼也不能依賴對齊的細節。凡是計算結構體大小的地方,都老老實實寫上sizeof()

  有些編譯器支持#pragma pack預處理語句(能夠用來修改對齊字長),不過這種語法不是全部編譯器都支持,要慎用。

移位操做

  對於有符號整數的右移操做,有些系統默認使用算數右移(最高的符號位不變),有些默認使用邏輯右移(最高的符號位補0)。因此,不要對有符號整數進行右移操做。順便說一下,即便沒有移植性問題,代碼中也儘可能少用移位運算符。那些企圖用移位運算來提升性能的同窗更要注意了,這麼幹不但可讀性不好,並且吃力不討好。只要不太弱智的編譯器,都會自動幫你搞定這種優化,無須程序員操心。

操做系統

  上一個帖子提到了「硬件體系」相關的話題,今天來講說和操做系統相關的話題。C++跨平臺開發中和OS相關的雜事挺多,因此今天會囉嗦比較長的篇幅,請列位看官見諒 :-)

  爲了避免繞口,如下把Linux和各類Unix統稱爲Posix系統。

文件系統(FileSystem如下簡稱FS)

  剛開始搞跨平臺開發的新手,多半都會碰上和FS相關的問題。因此先來聊一下FS。概括下來,開發中容易碰上的FS差別主要有以下幾個:目錄分隔符的差別;大小寫敏感的差別;路徑中禁用字符的差別。

  爲了應對上述差別,你要注意以下幾點:

1、文件和目錄命名要規範

  在給文件和目錄命名時,儘可能只使用字母和數字。不要在同一個目錄下放兩個名稱類似(名稱中只有大小寫不一樣,例如foo.cppFoo.cpp)的文件。不要使用某些OS的保留字(例如auxconnulprn)做文件名或目錄名。

  補充一下,剛纔說的命名,包括了源代碼文件、二進制文件和運行時建立的其它文件。

2#include語句要規範

  當你寫#include語句時,要注意使用正斜線「/」(比較通用)而不要使用反斜線「\」(僅在Windows可用)。#include語句中的文件和目錄名要和實際名稱保持大小寫徹底一致。

3、代碼中涉及FS操做,儘可能使用現成的庫

  已經有不少成熟的、用於FS的第三方庫(好比boost::filesystem)。若是你的代碼涉及到FS的操做(好比目錄遍歷),儘可能使用這些第三方庫,能夠幫你省很多事情。

文本文件的回車CR/換行LF

  因爲幾個知名的操做系統對回車/換行的處理不一致,致使了這個煩人的問題。目前的局面是:Windows同時使用CRLFLinux和大部分的Unix使用LF;蘋果的Mac系列使用CR

  對於源代碼管理,好在不少版本管理軟件(好比CVSSVN)都會智能地處理這個問題,讓你從代碼庫取回本地的源碼能適應本地的格式。

  若是你的程序須要在運行時處理文本文件,要留意本文方式打開和二進制方式打開的區別。另外,若是涉及跨不一樣系統傳輸文本文件,要考慮進行適當的處理。

  ★文件搜索路徑(包括搜索可執行文件和動態庫)

  在Windows下,若是要執行文件或者加載動態庫,通常會搜索當前目錄;而Posix系統則不盡然。因此若是你的應用涉及到啓動進程或加載動態庫,就要當心這個差別。

  ★環境變量

  對於上述提到的搜索路徑問題,有些同窗想經過修改PATHLD_LIBRARY_PATH來引入當前路徑。假如使用這種方法,建議你只修改進程級的環境變量,不要修改系統級的環境變量(修改系統級有可能影響到同機的其它軟件,產生反作用)。

  ★動態庫

  若是你的應用程序使用動態庫,強烈建議動態庫導出標準C風格的函數(儘可能不要導出類)。若是在Posix系統中加載動態庫,切記慎用RTLD_GLOBAL標誌位。這個標誌位會Enable全局符號表,有可能會致使多個動態庫之間的符號名衝突(一旦碰到這種事,會出現匪夷所思的運行時錯誤,極難調試)。

  ★服務/看守進程

  若是你不清楚服務和看守進程的概念,請看維基百科(這裏和這裏)。爲了敘述方便,如下統稱服務。

  因爲C++開發的模塊大部分是後臺模塊,常常會碰到服務的問題。編寫服務須要調用好幾個系統相關的API,致使了與操做系統的緊密耦合,很難用一套代碼搞定。所以比較好的辦法是抽象出一個通用的服務外殼,而後把業務邏輯代碼做爲動態庫掛載到它下面。這樣的話,至少保證了業務邏輯的代碼只須要一套;服務外殼的代碼雖然須要兩套(一個用於Windows、一個用於Posix),但他們是業務無關的,能夠很方便地重用。

  ★默認棧大小

  不一樣的操做系統,棧的默認大小差異很大,從幾十KB(聽說Symbian只有12K,真摳門)到幾MB不等。所以你事先要打聽一下目標系統的默認棧大小,若是碰上像Symbian這樣摳門的,能夠考慮用編譯器選項調大。固然,養成「不在棧上定義大數組/大對象」的好習慣也很重要,不然再大的棧也會被撐爆的。

多線程

  最近一個多月寫的帖子比較雜,致使本系列又很久沒更新了。結果又有網友在評論中催我了,搞得我有點囧。今天趕忙把多線程篇補上。上次聊操做系統 的時候,因爲和OS有關的話題比較瑣碎,雜七雜八說了一大堆。當時一看篇幅有點長,就把多進程和多線程的部分給留到後面了。

  ★編譯器

關於C運行庫選項

  先來講一個很基本的問題:關於C運行庫(後面簡稱CRTC 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

  wxWidgetsQT都是GUI界面庫,可是它們也都內置和對線程的支持。wxWidgets線程的簡介能夠看「這裏 」,關於QT線程的簡介能夠看「這裏 」。這兩個庫對線程的支持差很少,都提供了諸如mutexconditionsemaphore等經常使用的機制。不過特性沒有ACE豐富。

  ◇如何權衡

  對於開發GUI軟件並已經用上了wxWidgets或者QT,那你能夠直接用它們內置的線程庫(前提是你只用到基本的線程功能)。因爲它們內置的線程庫,特性稍嫌單薄。萬一你須要某高級的線程功能,那得考慮替換成boost::threadACE

  至於boost::threadACE的取捨,主要得看軟件的需求了。若是你要支持的平臺挺多挺雜,那建議選用ACE,以避免碰上編譯器不支持的問題。若是你只須要支持少數幾個主流的平臺(好比WindowsLinuxMac),那建議用boost::thread。畢竟主流操做系統上的編譯器,對boost的支持仍是蠻好的。

  ★編程上的注意事項

  其實多線程開發,須要注意的地方挺多的,我只能大體列幾個印象比較深的注意事項。

  ◇關於volatile

  說到多線程編程可能碰到的陷阱,那就不得不提到volatile 關鍵字。若是你對它還不甚瞭解,先看「這裏 」掃盲一下。因爲C++ 98C++ 03標準都沒有定義多線程的內存模型,而標準中也就volatile 和線程沾點兒邊。結果致使C++社區中有至關多的口水都集中在volatile 身上(其中有很多C++大牛的口水)。有鑑於此,我這裏就再也不多囉嗦了。推薦幾個大牛的文章:Andrei Alexandrescu 的文章「這裏 」、還有Hans Boehm的文章「這裏 」和「這裏 」。大夥兒自個兒去拜讀一下。

  ◇關於原子操做

  有些同窗光知道多個線程的競爭寫 須要加鎖,殊不知道多個讀 單個寫 也須要保護。好比有某個整數int nCount = 0x01020304;在併發狀態下,一個寫線程去修改它的值nCount = 0x05060708;另外一個讀線程去獲取該值。那麼讀線程有沒有可能讀取到一個「壞」的(好比0x05060304)數據捏?

  數據是否壞掉,取決於對nCount的讀和寫是否屬於原子操做。而這就依賴於不少硬件相關的因素了(包括CPU的類型、CPU的字長、內存對齊的字節數等)。在某些狀況下,確實可能出現數據壞掉。

  因爲咱們討論的是跨平臺的開發,天曉得未來你的代碼會在啥樣的硬件環境下執行。因此在處理相似問題的時候,仍是要用第三方庫提供的原子操做類/函數(好比ACEAtomic_Op)來確保安全。

  ◇關於對象的析構

在以前的系列帖子「C++對象是怎麼死的? 」裏面,已經分別介紹了Win32平臺和Posix平臺下線程的非天然死亡問題。

因爲上述幾個跨平臺的線程庫底層仍是要調用操做系統自帶的線程API,因此大夥兒仍是要盡最大努力確保全部線程都可以天然死亡。

相關文章
相關標籤/搜索