C++的可移植性和跨平台開發(長文)

來源:互聯網
上載者:User

概述

  今天聊聊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失敗會返回null 指標。我當年用的Borland C++ 3.1似乎就是這樣的,現在這種編譯器應該不多見了。如果你目前用的編譯器還有這種行為,那你就慘了。你可以考慮重載new操作符來拋出 bad_alloc異常,便於進行異常處理。
稍微新式一點的編譯器,就不是僅僅返回null 指標了。當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、返回null 指標;
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,所以大伙兒還是要盡最大努力確保所有線程都能夠自然死亡。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.