C/C++跨平臺(tái)SDK開發(fā)的注意事項(xiàng)

開發(fā)跨平臺(tái)SDK如同在多個(gè)操作系統(tǒng)的夾縫中走鋼絲:你需要同時(shí)討好Linux的嚴(yán)謹(jǐn)、Windows的霸道、macOS的優(yōu)雅,甚至嵌入式系統(tǒng)的固執(zhí)。以下是歷經(jīng)實(shí)戰(zhàn)后的經(jīng)驗(yàn)沉淀,以及幾個(gè)值得深思的命題。

1. C/C++跨平臺(tái)開發(fā)時(shí)有哪些值得注意的事項(xiàng)?

1.1. 你知道如何選擇C++標(biāo)準(zhǔn)的版本嗎?

1.1.1. C++版本說明

對(duì)于C++跨平臺(tái)開發(fā)來(lái)說,選擇一個(gè)合適的C++版本是最為重要的一件事情。C++跨平臺(tái)開發(fā)最重要的難點(diǎn)之一是解決平臺(tái)的差異性。C++不同的版本支持的特性不同,版本越新支持的特性越多,很多平臺(tái)的差異可能在新的標(biāo)準(zhǔn)版本里C++語(yǔ)言層面就幫我們解決了。比如:C++11的chrono模塊提供了跨平臺(tái)的時(shí)間處理相關(guān)的工具,C++17的filesystem模塊提供了跨平臺(tái)的文件系統(tǒng)相關(guān)操作。

1.1.2. 如何選擇版本

問題: 在實(shí)際項(xiàng)目開發(fā)中,C++版本的選擇是越高越好嗎?

解答: 答案肯定是否定的,要視情況而定。

  • 基于編譯器的考慮: 通常我們所說的C++版本,是指C++標(biāo)準(zhǔn)委員會(huì)推出的C++大版本,如C++11/C++14/C++17/C++20/C++23等。而這些版本是要由C++編譯器來(lái)支持的,C++編譯器本身也是一個(gè)軟件,是軟件就可能有Bug。C++編譯器對(duì)這些C++版本的支持也是在持續(xù)迭代優(yōu)化的。越新的C++版本因?yàn)橹С值臅r(shí)間越短,因此存在Bug的可能性越大;而越老的版本因?yàn)榫幾g器支持的時(shí)間更長(zhǎng),所以越穩(wěn)定。
  • 基于應(yīng)用場(chǎng)景的考慮: 如果是應(yīng)用層的項(xiàng)目,可以選擇最新的C++版本。如果是SDK,SDK本身可能要支持更多的C++版本,建議選擇低版本的C++。

1.1.3. 最佳實(shí)踐

  • 如果是開發(fā)新的應(yīng)用層項(xiàng)目,建議選擇較新的穩(wěn)定版本的C++;結(jié)合實(shí)際情況,建議選擇最新版本的前一到兩個(gè)大版本,如現(xiàn)在(2025年02月)的最新版本是C++23,建議選擇C++17或C++20。
  • 如果是開發(fā)底層的SDK項(xiàng)目,SDK本身就希望能支持更多的C++版本,建議選擇低版本的C++(如C++11),以覆蓋盡可能多的用戶。
  • 如果是復(fù)雜的老項(xiàng)目:建議維持原有版本,非必要不做升級(jí)。

1.2. 源代碼要如何保存,跨平臺(tái)和跨IDE時(shí)才不會(huì)出現(xiàn)中文亂碼?

1.2.1. 中文亂碼問題與原因分析

C/C++跨平臺(tái)開發(fā)時(shí),通常需要在多個(gè)平臺(tái)下開發(fā)、編譯和調(diào)試,不同的平臺(tái)可能會(huì)用不同的開發(fā)工具。如:

  • Windows: Visual Studio XXXX (XXX表示版本系列,如:2017、2019、2022)
  • Linux: Vim/VSCode + GCC編譯器
  • macOS: Xcode

中文亂碼的現(xiàn)象和原因:

不同平臺(tái)編輯和查看代碼時(shí),你可能經(jīng)常會(huì)遇到的一個(gè)問題是中文亂碼(代碼注釋或常量字符串的中文亂碼)。如:Windows下顯示正常,Linux(macOS)下顯示為亂碼;或Linux(macOS)下顯示正常,Windows下顯示為亂碼。

而亂碼的本質(zhì)是文件編碼方式不一致

  • Vim、VSCode、XCode保存的文件,默認(rèn)編碼是UTF-8(無(wú)BOM標(biāo)記)。
  • Visual Studio XXXX系列保存的文件,Visual Studio 2022默認(rèn)是UTF-8 BOM(帶BOM標(biāo)記),2022之前的版本是操作系統(tǒng)的本地編碼,中文環(huán)境下默認(rèn)是GBK。

解決思路和方法:

所以,解決問題的思路就是:所有源碼文件都統(tǒng)一使用相同的編碼格式保存。所有的編輯器、編譯器、IDE都要統(tǒng)一編碼格式,如統(tǒng)一使用UTF-8編碼。

1.2.2. 解決策略

所有源碼文件都以UTF-8 BOM的格式保存,任意平臺(tái)的任意IDE都采用相同的格式保存。

因?yàn)榈侥壳盀橹?2025年02月),各個(gè)平臺(tái)和IDE對(duì)UTF-8 BOM格式的支持都很好。

1.3. 如何優(yōu)雅的隔離平臺(tái)的差異?

1.3.1. 用宏定義隔離平臺(tái)的差異

C++跨平臺(tái)開發(fā),最重要的一件事情就是:抹平平臺(tái)的差異。不同平臺(tái)的系統(tǒng)調(diào)用接口、文件系統(tǒng)的目錄結(jié)構(gòu)等都有所差異,為了實(shí)現(xiàn)不同平臺(tái)的無(wú)縫對(duì)接,需要對(duì)這些差異進(jìn)行隔離,最常用的方法就是通過預(yù)定義宏來(lái)實(shí)現(xiàn)。

通常有兩種方式來(lái)實(shí)現(xiàn)平臺(tái)差異的隔離:

  • 操作系統(tǒng)預(yù)定義宏,如_WIN32、__linux__。
  • 編譯器預(yù)定義宏,如:_MSC_VER、__clang__。

操作系統(tǒng)預(yù)定義宏的通用性比編譯器預(yù)定義宏更好,通常會(huì)采用此種方式。除非我們確實(shí)需要使用某個(gè)指定編譯器的特性時(shí),才使用編譯器預(yù)定義宏。

1.3.2. 最佳實(shí)踐

代碼實(shí)現(xiàn):

用宏定義隔離平臺(tái)的差異,實(shí)現(xiàn)代碼通常會(huì)寫成如下這樣:

#if defined(_WIN32)
    std::cout << "Windows ";
#elif defined(__APPLE__)
    std::cout << "Apple " << std::endl;
#elif defined(__ANDROID__)
    std::cout << "Android" << std::endl;
#elif defined(__linux__)
    std::cout << "Linux" << std::endl;
#elif defined(__unix__)
    std::cout << "Unix" << std::endl;
#else
    std::cout << "Unknown platform" << std::endl;
#endif

代碼優(yōu)化:

但這種包含很多宏定義的代碼可讀性是非常差的,特別是宏定義之間的邏輯代碼如果也包含很多if...else...判斷時(shí),要看懂代碼的邏輯分支是非常痛苦的。

解決策略是:

將這種平臺(tái)差異的邏輯代碼通過源碼文件隔離開來(lái)。

案例演示:

比如我們有這樣一個(gè)需求:

跨平臺(tái)C++項(xiàng)目中想使用localtimegmtime這兩個(gè)函數(shù)的功能。但這兩個(gè)函數(shù)是線程不安全的,想要使用這兩個(gè)函數(shù)的線程安全版本,但Windows和Linux(及類Unix系統(tǒng))平臺(tái)的函數(shù)名和使用方式是不同的。

  • Windows是 localtime_sgmtime_s。
  • Linux是 localtime_rgmtime_r。

我們可以定義一個(gè)頭文件time_util.h,聲明兩個(gè)自定義的函數(shù),對(duì)這兩個(gè)函數(shù)進(jìn)行封裝;然后再定義兩個(gè)源文件time_util_win.cpptime_util_unix.cpp分別進(jìn)行Windows和Linux(及類Unix系統(tǒng))下的實(shí)現(xiàn)。

1.4. 接口的參數(shù)和返回值可以是任意數(shù)據(jù)類型嗎?

1.4.1. 平臺(tái)差異

C/C++有多種內(nèi)置的整數(shù)類型,如:short、int、long、long long,它們?cè)诓煌钠脚_(tái)下,所占用的字節(jié)大小和表達(dá)的數(shù)據(jù)范圍可能是不一樣的。我們?cè)谶M(jìn)行跨平臺(tái)C++ SDK開發(fā)時(shí),要避免這個(gè)問題,應(yīng)采用定長(zhǎng)的數(shù)據(jù)類型。

1.4.2. 解決策略

在進(jìn)行跨平臺(tái)C/C++ SDK開發(fā)時(shí),函數(shù)的參數(shù)和返回值要使用基本數(shù)據(jù)類型或指針類型。而基本數(shù)據(jù)類型要采用<stdint.h><cstdint>頭文件里的標(biāo)準(zhǔn)整型數(shù)據(jù)替代內(nèi)置的數(shù)據(jù)類型。這些數(shù)據(jù)類型在不同平臺(tái)下占用的大小相同。

以下數(shù)據(jù)類型可以在不同平臺(tái)下表現(xiàn)一致,對(duì)應(yīng)的大小如下:

數(shù)據(jù)類型 大小
char 1
bool 1
float 4
double 8
int8_t 1
int16_t 2
int32_t 4
int64_t 8
uint8_t 1
uint16_t 2
uint32_t 4
uint64_t 8

1.5. 如何優(yōu)雅的實(shí)現(xiàn)跨平臺(tái)的文件系統(tǒng)操作?

1.5.1. 平臺(tái)的差異

  • Linux的路徑分隔符是/,Windows的默認(rèn)路徑分隔符是\,但也支持/
  • Linux(類Unix)平臺(tái),文件系統(tǒng)嚴(yán)格區(qū)分文件名的大小寫。而Windows平臺(tái),文件系統(tǒng)不區(qū)分文件名的大小寫。

1.5.2. 解決的策略

  • 代碼中涉及文件或目錄的路徑時(shí),統(tǒng)一使用/分隔符。
  • 頭文件、路徑(文件名和目錄名)、控制臺(tái)命令等均要嚴(yán)格區(qū)分大小寫

1.5.3. 路徑操作和文件系統(tǒng)的操作

C++17及之后:

  • STL標(biāo)準(zhǔn)庫(kù)提供了std::filesystem::path類,可以方便的進(jìn)行路徑相關(guān)的操作。
  • STL標(biāo)準(zhǔn)庫(kù)提供了std::filesystem類,可以方便的進(jìn)行文件相關(guān)的操作。

C++17之前:

可以將這些常用的操作自己封裝成一系列工具函數(shù),也可以使用開源的第三方庫(kù),如boost::filesystem。

這里推薦一個(gè)我自己實(shí)現(xiàn)的輕量級(jí)的跨平臺(tái)filepath類和fileutil類,由于代碼較長(zhǎng),這里不詳細(xì)列出源碼,大家可以前往開源項(xiàng)目查看:https://gitee.com/spencer_luo/common_util/blob/master/src/common_util/filepath.hhttps://gitee.com/spencer_luo/common_util/blob/master/src/common_util/fileutil.h。

此項(xiàng)目永久開源,大家放心查閱,我們可以簡(jiǎn)單看一下它的使用方法。

#include "fileutil.h"
#include <iostream>

void test_filepath()
{
    auto path1 = cutl::path("/home/spencer/workspace/common_util/README.md");
    std::cout << path1.str() << (path1.exists() ? "存在" : "不存在") << ", 是一個(gè)"
              << (path1.isfile() ? "文件" : "文件夾") << std::endl;
    std::cout << "父目錄: " << path1.dirname() << std::endl;
    std::cout << "文件名: " << path1.basename() << std::endl;
    std::cout << "擴(kuò)展名: " << path1.extension() << std::endl;
    auto path2 = cutl::path(path1.dirname()).join("LICENSE");
    std::cout << "LICENSE文件的路徑: " << path2 << std::endl;
}

執(zhí)行結(jié)果如下:

/home/spencer/workspace/common_util/README.md存在, 是一個(gè)文件
父目錄: /home/spencer/workspace/common_util
文件名: README.md
擴(kuò)展名: .md
LICENSE文件的路徑: /home/spencer/workspace/common_util/LICENSE

2. 待討論的命題

  1. 除了以上這些,你還遇到過哪些跨平臺(tái)開發(fā)中的坑?
  2. 如何平衡抽象層帶來(lái)的性能損耗與可維護(hù)性?
  3. 當(dāng)某個(gè)平臺(tái)的特殊需求威脅架構(gòu)設(shè)計(jì)時(shí),是妥協(xié)還是拒絕支持?

請(qǐng)?jiān)谠u(píng)論區(qū)分享你的血淚史——每個(gè)跨平臺(tái)開發(fā)者的傷疤,都是后來(lái)者的路標(biāo)。


SDK開發(fā)的更多詳細(xì)內(nèi)容:

01. 什么是SDK

02. SDK的設(shè)計(jì)目標(biāo)

03. 接口設(shè)計(jì)與規(guī)范

04. 接口注釋與接口文檔

05. 原理篇:字符集與字符編碼(一)

06. 原理篇:字符集與字符編碼(二)

07. 原理篇:多字節(jié)字符與寬字節(jié)字符

08. 原理篇:靜態(tài)庫(kù)、動(dòng)態(tài)庫(kù)與運(yùn)行庫(kù)

09. 跨平臺(tái):C++標(biāo)準(zhǔn)的版本

10. 跨平臺(tái):源碼的保存格式與中文亂碼問題

11. 跨平臺(tái):宏定義隔離平臺(tái)差異

12. 跨平臺(tái):基礎(chǔ)數(shù)據(jù)類型的定義

13. 跨平臺(tái):文件系統(tǒng)的操作

14. 跨平臺(tái):頭文件包含的差異

15. 跨平臺(tái):導(dǎo)出接口的定義

16. 跨平臺(tái):字節(jié)序大端與小端

附錄A-計(jì)算機(jī)術(shù)語(yǔ)中成對(duì)出現(xiàn)的單詞

附錄B: 計(jì)算機(jī)術(shù)語(yǔ)中常見的單詞縮寫


大家好,我是陌塵。

IT從業(yè)10年+, 北漂過也深漂過,目前暫定居于杭州,未來(lái)不知還會(huì)飄向何方。

搞了8年C++,也干過2年前端;用Python寫過書,也玩過一點(diǎn)PHP,未來(lái)還會(huì)折騰更多東西,不死不休。

感謝大家的關(guān)注,期待與你一起成長(zhǎng)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容