- [1. C/C++跨平台开发时有哪些值得注意的事项?](#1. C/C++跨平台开发时有哪些值得注意的事项? "#1-cc%E8%B7%A8%E5%B9%B3%E5%8F%B0%E5%BC%80%E5%8F%91%E6%97%B6%E6%9C%89%E5%93%AA%E4%BA%9B%E5%80%BC%E5%BE%97%E6%B3%A8%E6%84%8F%E7%9A%84%E4%BA%8B%E9%A1%B9")
- [1.1. 你知道如何选择C++标准的版本吗?](#1.1. 你知道如何选择C++标准的版本吗? "#11-%E4%BD%A0%E7%9F%A5%E9%81%93%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9c%E6%A0%87%E5%87%86%E7%9A%84%E7%89%88%E6%9C%AC%E5%90%97")
- [1.1.1. C++版本说明](#1.1.1. C++版本说明 "#111-c%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E")
- [1.1.2. 如何选择版本](#1.1.2. 如何选择版本 "#112-%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9%E7%89%88%E6%9C%AC")
- [1.1.3. 最佳实践](#1.1.3. 最佳实践 "#113-%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5")
- [1.2. 源代码要如何保存,跨平台和跨IDE时才不会出现中文乱码?](#1.2. 源代码要如何保存,跨平台和跨IDE时才不会出现中文乱码? "#12-%E6%BA%90%E4%BB%A3%E7%A0%81%E8%A6%81%E5%A6%82%E4%BD%95%E4%BF%9D%E5%AD%98%E8%B7%A8%E5%B9%B3%E5%8F%B0%E5%92%8C%E8%B7%A8ide%E6%97%B6%E6%89%8D%E4%B8%8D%E4%BC%9A%E5%87%BA%E7%8E%B0%E4%B8%AD%E6%96%87%E4%B9%B1%E7%A0%81")
- [1.2.1. 中文乱码问题与原因分析](#1.2.1. 中文乱码问题与原因分析 "#121-%E4%B8%AD%E6%96%87%E4%B9%B1%E7%A0%81%E9%97%AE%E9%A2%98%E4%B8%8E%E5%8E%9F%E5%9B%A0%E5%88%86%E6%9E%90")
- [1.2.2. 解决策略](#1.2.2. 解决策略 "#122-%E8%A7%A3%E5%86%B3%E7%AD%96%E7%95%A5")
- [1.3. 如何优雅的隔离平台的差异?](#1.3. 如何优雅的隔离平台的差异? "#13-%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E7%9A%84%E9%9A%94%E7%A6%BB%E5%B9%B3%E5%8F%B0%E7%9A%84%E5%B7%AE%E5%BC%82")
- [1.3.1. 用宏定义隔离平台的差异](#1.3.1. 用宏定义隔离平台的差异 "#131-%E7%94%A8%E5%AE%8F%E5%AE%9A%E4%B9%89%E9%9A%94%E7%A6%BB%E5%B9%B3%E5%8F%B0%E7%9A%84%E5%B7%AE%E5%BC%82")
- [1.3.2. 最佳实践](#1.3.2. 最佳实践 "#132-%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5")
- [1.4. 接口的参数和返回值可以是任意数据类型吗?](#1.4. 接口的参数和返回值可以是任意数据类型吗? "#14-%E6%8E%A5%E5%8F%A3%E7%9A%84%E5%8F%82%E6%95%B0%E5%92%8C%E8%BF%94%E5%9B%9E%E5%80%BC%E5%8F%AF%E4%BB%A5%E6%98%AF%E4%BB%BB%E6%84%8F%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%90%97")
- [1.4.1. 平台差异](#1.4.1. 平台差异 "#141-%E5%B9%B3%E5%8F%B0%E5%B7%AE%E5%BC%82")
- [1.4.2. 解决策略](#1.4.2. 解决策略 "#142-%E8%A7%A3%E5%86%B3%E7%AD%96%E7%95%A5")
- [1.5. 如何优雅的实现跨平台的文件系统操作?](#1.5. 如何优雅的实现跨平台的文件系统操作? "#15-%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E7%9A%84%E5%AE%9E%E7%8E%B0%E8%B7%A8%E5%B9%B3%E5%8F%B0%E7%9A%84%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E6%93%8D%E4%BD%9C")
- [1.5.1. 平台的差异](#1.5.1. 平台的差异 "#151-%E5%B9%B3%E5%8F%B0%E7%9A%84%E5%B7%AE%E5%BC%82")
- [1.5.2. 解决的策略](#1.5.2. 解决的策略 "#152-%E8%A7%A3%E5%86%B3%E7%9A%84%E7%AD%96%E7%95%A5")
- [1.5.3. 路径操作和文件系统的操作](#1.5.3. 路径操作和文件系统的操作 "#153-%E8%B7%AF%E5%BE%84%E6%93%8D%E4%BD%9C%E5%92%8C%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E7%9A%84%E6%93%8D%E4%BD%9C")
- [1.1. 你知道如何选择C++标准的版本吗?](#1.1. 你知道如何选择C++标准的版本吗? "#11-%E4%BD%A0%E7%9F%A5%E9%81%93%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9c%E6%A0%87%E5%87%86%E7%9A%84%E7%89%88%E6%9C%AC%E5%90%97")
- [2. 待讨论的命题](#2. 待讨论的命题 "#2-%E5%BE%85%E8%AE%A8%E8%AE%BA%E7%9A%84%E5%91%BD%E9%A2%98")
开发跨平台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. 最佳实践
代码实现:
用宏定义隔离平台的差异,实现代码通常会写成如下这样:
C++
#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
类,由于代码较长,这里不详细列出源码,大家可以前往开源项目查看:gitee.com/spencer_luo...和gitee.com/spencer_luo...。
此项目永久开源,大家放心查阅,我们可以简单看一下它的使用方法。
C++
#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;
}
执行结果如下:
bash
/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,未来还会折腾更多东西,不死不休。
感谢大家的关注,期待与你一起成长。
【SunLogging】
扫码二维码,关注微信公众号,阅读更多精彩内容