【C++】跨平台开发注意事项【上】

简介: 将 Windows 平台上适用 C++ 代码移植到 Linux 下需要注意的事项

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 电脑上无法在同一个路径下创建名称分别为 aA 的两个文件或目录。

然而 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 相关的一些运行库和头文件,如 cmathstring.hmemory.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 - 宏函数参数

宏函数使用时,参数个数需要与定义保持一致。

目录
相关文章
|
2月前
|
编译器 Linux C++
【C++ 跨平台开发 】掌握 C++ 跨平台关键宏的使用
【C++ 跨平台开发 】掌握 C++ 跨平台关键宏的使用
54 3
|
3月前
|
存储 算法 C语言
【编程陷阱】编写出色C++代码:遵循的注意事项和最佳实践
【编程陷阱】编写出色C++代码:遵循的注意事项和最佳实践
35 0
|
5月前
|
C++
C和C++动态内存分配及内存注意事项(重要)
C和C++动态内存分配及内存注意事项(重要)
34 0
|
6月前
|
NoSQL Linux MongoDB
C++库封装mongodb(跨平台开发)
我的初衷是在Linux平台下只提供动态库和头文件,windows平台下提供静态库和头文件给开发者,这个库mongo-proxy对外提供了一些对mongodb的连接,增删改查,创建索引,聚合等操作的封装,开发者只需要关心接口如何调用,而不需要关心接口是如何实现的,也不需要关心mongo-c-driver的相关依赖,这里我抽象出mongo_proxy类,
|
7月前
|
Java 程序员 Android开发
C++ 程序员,安卓开发注意事项
C++ 程序员,安卓开发注意事项
|
9月前
|
存储 Linux 编译器
【C++】跨平台开发注意事项【下】
在 Windows 平台上适用的 C++ 代码移植到 Linux 下的注意事项
78 0
【C++】跨平台开发注意事项【下】
|
11月前
|
算法 安全 C语言
c++的lambda使用注意事项,可能导致的崩溃问题分析
c++的lambda使用注意事项,可能导致的崩溃问题分析
|
12月前
|
编译器 Linux API
C++中的可移植性和跨平台开发
在当今软件开发行业中,跨平台开发已经成为了一种非常流行的方式。C++作为一门强大的编程语言,也被广泛应用于跨平台开发中。然而,由于不同操作系统的差异和限制,C++在不同的平台上的表现可能会有所不同。为了解决这个问题,我们需要优化C++代码的可移植性,以便在不同的平台上实现相同的功能
134 0
|
存储 IDE 编译器
Android C++系列:函数返回值注意事项
函数返回值就是使用return语句终止正在执行的函数,看是很简单的问题有什么说的呢?因为越是简单的问题里面越是有一些不易发现的坑。
73 0
|
编译器 Android开发 C++
Android C++系列:数组在函数中注意事项
数组作为函数形参传递的是数组首元素的地址本来是很简单的知识点,但是在具体使用中还会有一些坑需要注意。
109 0