【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 - 宏函数参数

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

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