I - 概括总述
随着多系统的普及,不少软件面临跨平台的需求,普通 Windows PC 端可以使用的代码,需要在另一个平台下编译和运行。
本文章以 Linux 平台为例,整理了从 Windows 平台上的可编译代码到 Linux 平台上代码移植问题,以及跨平台开发的建议规范与需要注意的事项。
Windows 平台上的 MSVC 编译器容错率比较高,部分代码问题编译器会自动纠正或忽略,但是 Linux 下的 gcc/g++ 比较严格,且运行环境、库不一致,Windows 下可编译的代码,直接在 Linux 下编译会产生很多问题。如何做到隔离区分?
II - 标识与隔离
2.1 - 宏隔离
2.1.1 - 系统宏
代码中的区分平台的宏建议使用 _WIN32 与 __linux__
关于 标识 Windows 的宏,网上存在很多:
WIN32 WIN64 _WIN32 _WIN64 ...
Windows 平台建议使用 _WIN32
,此宏在操作系统为 x86 和 x64 系统中都会定义,使用 Visual Studio 2019 ,Win11 x64 电脑做开发,不添加自定义宏的情况下,只有 _WIN32
、_MSC_VER
是有效的。编译 x86 工程或 32 位系统下会额外定义 WIN32
,编译 x64 工程则会额外定义 _WIN64
Linux 平台建议使用 __linux__ 系统宏,所有使用 Linux 内核的系统都会默认定义此宏。
示例
#if defined(_WIN32)
std::string port("COM1");
#elif defined(__linux__)
std::string port("/dev/ttyUSB3");
#endif
2.1.2 - 编译器宏
也可以使用编译器宏来区分,Win 平台使用 _MSC_VER 来区分,Linux 平台使用编译器宏 __GNUC__
示例代码
#if defined (_MSC_VER)
p = HeapAlloc(hHeap, dwFlags, dwBytes);
#elif defined(__GNUC__)
p = malloc(sizeof(DataStruct));
#endif
2.1.3 - 注意事项
特定系统的代码隔离使用对应系统的宏,不使用检测另一个系统的宏未定义
示例
#ifndef _WIN32
// code for linux
#endif
//...
#ifndef __linux__
// code for windows
#endif
由于软件的跨平台需求可能不止两个系统,使用 “非此即彼” 的二分方式定义,在有第三个平台的跨平台开发需求时,代码需要做比较多的修改,有可能因修改疏漏造成未知的缺陷,应使用如下方式
#ifdef _WIN32
// code for windows
#endif
//...
#ifdef __linux__
// code for linux
#endif
建议使用平台宏和编译器宏组合的方式来判断
#if defined(_WIN32) || defined(_MSC_VER)
// code for windows
#elif defined(__linux__) || defined(__GNUC__)
// code for linux
#endif
关于更多系统预定义宏,见链接:
https://sourceforge.net/p/predef/wiki/OperatingSystems
2.2 - CMakeList 隔离
if (WIN32)
elseif (UNIX)
endif()
III - 常见平台差异
3.1 - 路径分隔符
代码中涉及文件/目录路径时,使用分隔符 / 替代 \\
Windows 下路径分隔符为 \\
,Linux 下路径分隔符为 /
, 例如
// windows path
std::string pathWin = "D:\\Repository\\MyTest\\test.cpp";
// linux path
std::string pathLinux = "/home/user/Desktop/dev/main.cpp";
多数函数或 API 方法在两种系统下均支持使用 /
作为分隔符。
示例
// 标准库
std::filesystem::exists("D:/Repository/MyTest/test.cpp");
// Qt API
QFile file("D:/Program Files/Typora/config.ini");
if (!file.open(QIODevice::Text | QIODevice::ReadOnly))
{
qDebug() << "open failed";
return false;
}
3.2 - 文件系统
包含头文件时,文件名称需使用正确的字母大小写
Windows 平台,文件系统不区分文件名的大小写,以及其目录、控制台和 PowerShell 的命令均不区分大小写。举例,在 Windows 电脑上无法在同一个路径下创建名称分别为 a
和 A
的两个文件或目录。
然而 Linux 下严格区分文件名称的大小写,在同一个目录下,允许字母相同但大小写不同的两个文件存在。
Windows 平台使用错误的大小写依然可以正确访问头文件。
错误举例
// qt 头文件
#include <qlist>
#include <qmap>
// 自定义头文件
#include "predefinedmacros.h"
需更正其大小写
// qt
#include <QList>
#include <QMap>
// 自定义头文件
#include "predefinedMacros.h"
如不清楚 Qt 中文件名的具体大小写,则可使用 Qt 的助手 (Assistant)。索引查找类名,找到后主显示区 Header 处有正确的头文件大小写。
3.3 - 开发环境
3.3.1 - 专属头文件
引入函数或类时,引入对应的头文件
此小节包含两项主要内容,
- Visual Studio 创建工程时自带的头文件需补充
- Windows / Linux 平台专属的头文件
创建 VS 解决方案时,默认会包含 VC 相关的一些运行库和头文件,如 cmath
, string.h
,memory.h
等,Linux 下编译时会因为找不到对应的头文件报错。
即使用 sqrt 等数学公式时包含 \ ,使用 memcpy 时,需要手动包含一下 \\,使用了智能指针 shared_ptr 等时,需要手动包含一下 \。
#include <cmath>
#include <cstring>
#include <memory>
另外,平台专属的头文件需要使用宏隔离开,示例
#if defined(_WIN32) || defined(_MSC_VER) || defined(_WIN64)
#include <Windows.h>
#elif defined(__linux__) || defined(__GNUC__)
#include <unistd.h>
#endif
3.3.2 - 专属函数
一些 VC 特殊功能的函数,可使用宏定义
示例
#if _MSC_VER > 1400
#define fgetc _fgetc_nolock
#endif
为了字符串操作安全, VC 提供了一套_s 后缀的接口,为 VC 专属函数,_s 表示 safe (安全),如 sprintf_s 等。可使用如下方式
示例:
#if defined(_MSC_VER) || defined(_WIN32) || defined(_WIN64)
#define SPRINTF sprintf_s
#else
#define SPRINTF sprintf
#endif
// e.g.
SPRINTF(buf, "some string to output %s\n", str.data());
_MSC_VER 是微软内部的一个版本,Visual Studio 2019 中 _MSC_VER 的定义是 1920。 下表为 Visual Studio 版本、 VC 版本 与 _MSC_VER 的对应
_MSC_VER | Visual Studio | VC++ |
---|---|---|
1910 | VS2017 | VC 15.0 |
1900 | VS2015 | VC 14.0 |
1800 | VS2013 | VC 12.0 |
1700 | VS 2012 | VC 11.0 |
IV - 编译器语法检查
Windows 平台上 MSCV 编译器忽略或者自动纠正的语法错误。
4.1 - 模板使用
使用模板时,需显示声明模板具体类型
错误示例
QList a = temp.split(_SPLIT_CHAR_);
vec.push_back(std::make_pair("key", value));
需更正为
QList<QString> a = temp.split(_SPLIT_CHAR_);
vec.push_back(std::make_pair<QString, double>("key", value));
4.2 - 宏扩展
不使用冗余的宏扩展
宏扩展 ##
用于合成一个标识符,
错误举例
#define BIND_CALLBACK(a,b,c) m_callbackFuncs[a] = std::bind(&##b, c, std::place_holders::_1);
BIND_CALLBACK("customize_callback_1", CallBack::CustomizeCallBack1, this);
Linux 下报错为 毗邻 '##' 无法构建一个有效的标识符。&
符号,CallBack::
和后边的函数 b ,为三个标识符,&
为操作符,CallBack::
为命名空间限定符,所以需要去掉链接符号 ##
#define BIND_CALLBACK(a,b,c) m_callbackFuncs[a] = std::bind(&b, c, std::place_holders::_1);
4.3 - 命名空间限定符
不在类声明中和非静态函数调用处 使用多余的命名空间限定符
1 - 类声明中冗余的命名空间限定符,MSVC 编译器会忽略。
错误示例
class CustomizedClass
{
public:
//...
QString CutomizedClass::Test(const QString & input);
}
类声明中需要去掉 CustomizedClass::
2 - 调用非静态函数是的命名空间限定符
错误示例
return QJsonDocument::QJsonDocument(jsonobj).toJson(QJsonDocument::Compact);
此处需要构造出 QJsonDocument
对象,此函数非静态函数,需要去掉多余的 QJsonDocument::
。
4.4 - 右值使用
函数调用传参时,不在调用处创建局部变量并使用其地址
函数调用处传入实参处,构造局部对象并取地址,Linux 下会报错,错误为 taking address of rvalue。
错误示例
Func("string", &DataStruct("key1","value1"));
需更改为
DataStruct dt("key1", "value1");
Func("string", &dt);
4.5 - 常量类型指针
使用常量限定类型到非常量限定类型指针传递时,需转换
const 限定类型地址赋值到非 const 限定类型指针。
错误示例
char * ptr = str.data();
发生 const char *
到 char *
的强制转换。需要添加类型转换或者更改类型。
// 1 - 更改目标类型
const char * ptr = str.data();
// 2 - 强转
char * ptr = (char*) str.data();
// 3 - 操作符
char * ptr = const_cast<char*>(str.data());
建议使用第一种
由于布尔类型可与指针发生隐式转换,尽量使用常指针,即使用关键字 const
修饰。
DataStruct(char * str); // 1
DataStruct(bool bval); // 2
DataStruct ds("string"); // 匹配 2
避免在在多个函数重载时会出现匹配错误。
4.6 - 宏函数参数
宏函数使用时,参数个数需要与定义保持一致。