C/C++跨平臺SDK開發的注意事項
開發跨平臺SDK如同在多個操作系統的夾縫中走鋼絲:你需要同時討好Linux的嚴謹、Windows的霸道、macOS的優雅,甚至嵌入式系統的固執。以下是歷經實戰后的經驗沉淀,以及幾個值得深思的命題。
1. C/C++跨平臺開發時有哪些值得注意的事項?
1.1. 你知道如何選擇C++標準的版本嗎?
1.1.1. C++版本說明
對于C++跨平臺開發來說,選擇一個合適的C++版本是最為重要的一件事情。C++跨平臺開發最重要的難點之一是解決平臺的差異性。C++不同的版本支持的特性不同,版本越新支持的特性越多,很多平臺的差異可能在新的標準版本里C++語言層面就幫我們解決了。比如:C++11的chrono模塊提供了跨平臺的時間處理相關的工具,C++17的filesystem模塊提供了跨平臺的文件系統相關操作。
1.1.2. 如何選擇版本
問題: 在實際項目開發中,C++版本的選擇是越高越好嗎?
解答: 答案肯定是否定的,要視情況而定。
- 基于編譯器的考慮: 通常我們所說的C++版本,是指C++標準委員會推出的C++大版本,如C++11/C++14/C++17/C++20/C++23等。而這些版本是要由C++編譯器來支持的,C++編譯器本身也是一個軟件,是軟件就可能有Bug。C++編譯器對這些C++版本的支持也是在持續迭代優化的。越新的C++版本因為支持的時間越短,因此存在Bug的可能性越大;而越老的版本因為編譯器支持的時間更長,所以越穩定。
- 基于應用場景的考慮: 如果是應用層的項目,可以選擇最新的C++版本。如果是SDK,SDK本身可能要支持更多的C++版本,建議選擇低版本的C++。
1.1.3. 最佳實踐
- 如果是開發新的應用層項目,建議選擇較新的穩定版本的C++;結合實際情況,建議選擇最新版本的前一到兩個大版本,如現在(2025年02月)的最新版本是C++23,建議選擇C++17或C++20。
- 如果是開發底層的SDK項目,SDK本身就希望能支持更多的C++版本,建議選擇低版本的C++(如C++11),以覆蓋盡可能多的用戶。
- 如果是復雜的老項目:建議維持原有版本,非必要不做升級。
1.2. 源代碼要如何保存,跨平臺和跨IDE時才不會出現中文亂碼?
1.2.1. 中文亂碼問題與原因分析
C/C++跨平臺開發時,通常需要在多個平臺下開發、編譯和調試,不同的平臺可能會用不同的開發工具。如:
- Windows:
Visual Studio XXXX(XXX表示版本系列,如:2017、2019、2022) - Linux:
Vim/VSCode+GCC編譯器 - macOS:
Xcode
中文亂碼的現象和原因:
不同平臺編輯和查看代碼時,你可能經常會遇到的一個問題是中文亂碼(代碼注釋或常量字符串的中文亂碼)。如:Windows下顯示正常,Linux(macOS)下顯示為亂碼;或Linux(macOS)下顯示正常,Windows下顯示為亂碼。
而亂碼的本質是文件編碼方式不一致:
- Vim、VSCode、XCode保存的文件,默認編碼是
UTF-8(無BOM標記)。 Visual Studio XXXX系列保存的文件,Visual Studio 2022默認是UTF-8 BOM(帶BOM標記),2022之前的版本是操作系統的本地編碼,中文環境下默認是GBK。
解決思路和方法:
所以,解決問題的思路就是:所有源碼文件都統一使用相同的編碼格式保存。所有的編輯器、編譯器、IDE都要統一編碼格式,如統一使用UTF-8編碼。
1.2.2. 解決策略
所有源碼文件都以UTF-8 BOM的格式保存,任意平臺的任意IDE都采用相同的格式保存。
因為到目前為止(2025年02月),各個平臺和IDE對UTF-8 BOM格式的支持都很好。
1.3. 如何優雅的隔離平臺的差異?
1.3.1. 用宏定義隔離平臺的差異
C++跨平臺開發,最重要的一件事情就是:抹平平臺的差異。不同平臺的系統調用接口、文件系統的目錄結構等都有所差異,為了實現不同平臺的無縫對接,需要對這些差異進行隔離,最常用的方法就是通過預定義宏來實現。
通常有兩種方式來實現平臺差異的隔離:
- 操作系統預定義宏,如
_WIN32、__linux__。 - 編譯器預定義宏,如:
_MSC_VER、__clang__。
操作系統預定義宏的通用性比編譯器預定義宏更好,通常會采用此種方式。除非我們確實需要使用某個指定編譯器的特性時,才使用編譯器預定義宏。
1.3.2. 最佳實踐
代碼實現:
用宏定義隔離平臺的差異,實現代碼通常會寫成如下這樣:
#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
代碼優化:
但這種包含很多宏定義的代碼可讀性是非常差的,特別是宏定義之間的邏輯代碼如果也包含很多if...else...判斷時,要看懂代碼的邏輯分支是非常痛苦的。
解決策略是:
將這種平臺差異的邏輯代碼通過源碼文件隔離開來。
案例演示:
比如我們有這樣一個需求:
跨平臺C++項目中想使用
localtime和gmtime這兩個函數的功能。但這兩個函數是線程不安全的,想要使用這兩個函數的線程安全版本,但Windows和Linux(及類Unix系統)平臺的函數名和使用方式是不同的。
- Windows是
localtime_s和gmtime_s。- Linux是
localtime_r和gmtime_r。
我們可以定義一個頭文件time_util.h,聲明兩個自定義的函數,對這兩個函數進行封裝;然后再定義兩個源文件time_util_win.cpp和time_util_unix.cpp分別進行Windows和Linux(及類Unix系統)下的實現。
1.4. 接口的參數和返回值可以是任意數據類型嗎?
1.4.1. 平臺差異
C/C++有多種內置的整數類型,如:short、int、long、long long,它們在不同的平臺下,所占用的字節大小和表達的數據范圍可能是不一樣的。我們在進行跨平臺C++ SDK開發時,要避免這個問題,應采用定長的數據類型。
1.4.2. 解決策略
在進行跨平臺C/C++ SDK開發時,函數的參數和返回值要使用基本數據類型或指針類型。而基本數據類型要采用<stdint.h>或<cstdint>頭文件里的標準整型數據替代內置的數據類型。這些數據類型在不同平臺下占用的大小相同。
以下數據類型可以在不同平臺下表現一致,對應的大小如下:
| 數據類型 | 大小 |
|---|---|
| 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. 如何優雅的實現跨平臺的文件系統操作?
1.5.1. 平臺的差異
- Linux的路徑分隔符是
/,Windows的默認路徑分隔符是\,但也支持/。 - Linux(類Unix)平臺,文件系統嚴格區分文件名的大小寫。而Windows平臺,文件系統不區分文件名的大小寫。
1.5.2. 解決的策略
- 代碼中涉及文件或目錄的路徑時,統一使用
/分隔符。 - 頭文件、路徑(文件名和目錄名)、控制臺命令等均要嚴格區分大小寫。
1.5.3. 路徑操作和文件系統的操作
C++17及之后:
- STL標準庫提供了
std::filesystem::path類,可以方便的進行路徑相關的操作。 - STL標準庫提供了
std::filesystem類,可以方便的進行文件相關的操作。
C++17之前:
可以將這些常用的操作自己封裝成一系列工具函數,也可以使用開源的第三方庫,如boost::filesystem。
這里推薦一個我自己實現的輕量級的跨平臺filepath類和fileutil類,由于代碼較長,這里不詳細列出源碼,大家可以前往開源項目查看: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。
此項目永久開源,大家放心查閱,我們可以簡單看一下它的使用方法。
#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() ? "存在" : "不存在") << ", 是一個"
<< (path1.isfile() ? "文件" : "文件夾") << std::endl;
std::cout << "父目錄: " << path1.dirname() << std::endl;
std::cout << "文件名: " << path1.basename() << std::endl;
std::cout << "擴展名: " << path1.extension() << std::endl;
auto path2 = cutl::path(path1.dirname()).join("LICENSE");
std::cout << "LICENSE文件的路徑: " << path2 << std::endl;
}
執行結果如下:
/home/spencer/workspace/common_util/README.md存在, 是一個文件
父目錄: /home/spencer/workspace/common_util
文件名: README.md
擴展名: .md
LICENSE文件的路徑: /home/spencer/workspace/common_util/LICENSE
2. 待討論的命題
- 除了以上這些,你還遇到過哪些跨平臺開發中的坑?
- 如何平衡抽象層帶來的性能損耗與可維護性?
- 當某個平臺的特殊需求威脅架構設計時,是妥協還是拒絕支持?
請在評論區分享你的血淚史——每個跨平臺開發者的傷疤,都是后來者的路標。
SDK開發的更多詳細內容:
大家好,我是陌塵。
IT從業10年+, 北漂過也深漂過,目前暫定居于杭州,未來不知還會飄向何方。
搞了8年C++,也干過2年前端;用Python寫過書,也玩過一點PHP,未來還會折騰更多東西,不死不休。
感謝大家的關注,期待與你一起成長。

浙公網安備 33010602011771號