- 1. C/C++跨平臺(tái)開發(fā)時(shí)有哪些值得注意的事項(xiàng)?
- 2. 待討論的命題
開發(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)目中想使用
localtime和gmtime這兩個(gè)函數(shù)的功能。但這兩個(gè)函數(shù)是線程不安全的,想要使用這兩個(gè)函數(shù)的線程安全版本,但Windows和Linux(及類Unix系統(tǒng))平臺(tái)的函數(shù)名和使用方式是不同的。
- Windows是
localtime_s和gmtime_s。- Linux是
localtime_r和gmtime_r。
我們可以定義一個(gè)頭文件time_util.h,聲明兩個(gè)自定義的函數(shù),對(duì)這兩個(gè)函數(shù)進(jìn)行封裝;然后再定義兩個(gè)源文件time_util_win.cpp和time_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.h和https://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. 待討論的命題
- 除了以上這些,你還遇到過哪些跨平臺(tái)開發(fā)中的坑?
- 如何平衡抽象層帶來(lái)的性能損耗與可維護(hù)性?
- 當(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)容:
08. 原理篇:靜態(tài)庫(kù)、動(dòng)態(tài)庫(kù)與運(yùn)行庫(kù)
09. 跨平臺(tái):C++標(biāo)準(zhǔn)的版本
12. 跨平臺(tái):基礎(chǔ)數(shù)據(jù)類型的定義
大家好,我是陌塵。
IT從業(yè)10年+, 北漂過也深漂過,目前暫定居于杭州,未來(lái)不知還會(huì)飄向何方。
搞了8年C++,也干過2年前端;用Python寫過書,也玩過一點(diǎn)PHP,未來(lái)還會(huì)折騰更多東西,不死不休。
感謝大家的關(guān)注,期待與你一起成長(zhǎng)。