本文参考Loup&卡普的文档
在大多情况下软件需要支持多个运行环境,如:Windows、Linux...Windows
平台上的MSVC
编译器比较宽松,部分错误编译器会自动纠正或者忽略,但是Linux
下gcc/g++
编译器相对严格,且运行库,环境同。Windows
下可编译的代码,直接在Linux
下编译会产生很多问题,我们通过制定一定的跨平台代码编写规范来杜绝这些问题。
代码中涉及路径时候,建议使用/
作为分隔符,代替\\
Windows
下路径分隔符为\\
,Linux
下路径分隔符为/
,例如:
//windows path
std::string win_path = "D:\\test\\1.txt";
//Linux path
std::string linux_path = "/home/user/test/1.txt";
很多函数或api
在两种系统下支持/
作为路径分隔符。
实例
std::filesystem::exists("D:/test/1.txt");
bool isExit = QFile::exists("D:/test/1.txt");
QFile log("D:/test/1.txt");
不同平台宏隔离建议使用_WIN32
与 __linux__
。
以下是一些标识Windows的宏:
WIN32 WIN64 _WIN32 _NT _WIN64
建议使用_WIN32
,此宏在操作系统为x86
和x64
系统中都会定义。编译x86
工程或32位操作系统下会额外定义WIN32
,64位操作系统下会额外定义_WIN64
。
linux
内核的操作系统建议使用__linux__
宏,所用使用linux
内核的系统都会默认定义此宏。
不同编译器也提供了编译器宏,所以我们可以使用编译器宏来区分环境。
//实例
#if defined(_MSC_VER)
std::string port("COM3")
#elif defined(__GUNC__)
std::string port("/dev/ttyUSB1")
#endif
特定系统的代码使用对应的宏,不需要检测别的系统的宏是否未定义:
以下代码是错误示范:
//实例
#ifnedf _WIN32
//code for linux
#endif
#ifndef __linux__
//code for windows
#endif
如果软件有超过2个平台的跨平台需求,若使用非此即彼的f方式定义,在跨第三个平台时候,就需要改动大量代码,可能因修改疏漏导致未知错误。应如下修改:
#ifdef __linux__
//code for linux
#endif
#ifdef _WIN32
//code for windows
#endif
包含头文件时,文件名称需使用正确的字母大小写
Windows
平台,文件系统不区分文件名的大小写,额外包括目录、控制台、PowerShell
的命令都是不区分大小的。但Linux
下的文件系统,命令等区分大小写。例如,我们无法在Windows
同一级目录下创建名为A
和a
的目录,而Linux
下可行。
以下代码是错误示范:
//qt头文件
#include
修改为:
#include
Qt
头文件一般是Q
前缀和紧跟第一个字母大写。多单词则为驼峰,如:QCoreApplication
。
引入函数或类时包含对应的头文件,个别头文件Visual Studio
会提供,但Linux
下不行。
如math.h
,memory.h
,string.h
等。这些相关头文件在Visual Studio
的外部依赖项会提供。
所以如果使用了智能指针shared_ptr
,unique_ptr
需要手动包含一下
,使用sqrt
等数学公式的时候手动包含
,使用memecpy
时候,需要手动包含一下
使用了其他类或函数也需要手动包括对应头文件。
使用平台专属类、函数时候,头文件和实现需要添加宏隔离
Windows
平台专有的头文件,如Windows
等,添加时,需要加入宏隔离。
//windows平台
#if _WIN32
#include "a.h"
#else
#include
#endif
#if _MSC_VER >1400
#define fgetc _fgetc_nolock
#endif
VC
为使字符串操作安全提供_s
后缀的函数,为VC
专属函数,如sprintf_s
等,需要添加宏隔离。
#ifdef _MSC_VER
#define SPRINTF sprintf_s
#else
#define SPRINTF sprintf
#endif
SPRINTF(pBuf,"Found thermo database file %s\n",strFn.c_str());
知识扩展:
_MSC_VER
是微软内部的一个版本,下表为Visual Studio
以及VC++
的对应表。
_MSC_VER | Visual Studio | VC++ |
---|---|---|
1910 | VS2017 | VC 15.0 |
1900 | VS2015 | VC 14.0 |
1800 | VS2013 | VC 12.0 |
1700 | VS2012 | VC 11.0 |
精简非必要的头文件使用
①:如非必要,尽量不包含多余的头文件,包含的头文件会在预编译阶段展开,除影响编译速度外,增加额外需要的链接库,影响软件最终体积。
②:尽量使用前置声明,避免不必要的头文件展开影响编译时间。
如:头文件中使用类声明,源文件中包含头文件。
//示例
class TestClass;
//源文件中包含
#include "../../test/TestClass.h"
注:前置声明仅支持指针和引用的声明,在头文件中相关的操作可能会失败,如:
class TestClass;
class Test
{
~Test()
{
if(!ptr)
{
delete ptr;//此处delete可能会失败,造成内纯泄露,建议将实现移动到cpp文件中
ptr = nullptr;
}
}
private:
TestClass* ptr{nullptr};
}
Windows
平台上MSVC
编译器忽略或自动纠正的语法错误。
使用模板时,需要显式声明模板具体类型。
错误示例:
QList a = GetListString();
vec.push_back(std::make_pair("key"),value));
需要更正为:
QList<QString> a = GetListString();
vec.push_back(std::make_pair<string,double>("key"),value));
不适用冗余的宏扩展
##
用户合成一个标识符
Linux
环境下报错为毗邻‘##’无法构建一个有效的标识符
‘,所以要去掉##
不在类声明中和非静态函数调用时,使用多余的命名空间限定符。
①类声明中冗余的命名空间限定符,MSVC
会忽略。
//错误实例
class TestClass
{
QString TestClass::getName(const QString &func);
}
//需要去掉`TestClass::`
②调用非静态函数时的命名空间限定符
//错误实例
return QJsonDocument::QJsonDocument(jsobj).toJson(QJsonDocument::Compact);
//此处构建出来的QJsonDocument对象调用了非静态函数需要去掉多余的QJsonDocument::
函数调用传参时,不在调用同时创建局部变量并使用其地址。
在函数调用传实参数处,构建局部对象并取用其地址,Linux
下会编译报错:
taking address of rvalue
//错误示范
FUN("TEST",&TestClass("test"))
//正确示范
TestClass obj("test");
FUN("TEST",obj);
使用常量限定类型到非常量限定类型指针传递时,需要转化
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
关键字修饰字修饰。
C
中的常量字符串类型为char*
,而C++
中常量字符串类型为 const char*
宏函数使用时,参数个数需要与定义时保持一致。
MSVC
有较高的容错,暂时未出现问题,宏函数参数数量与定义不一致。有些含函数要求入参,实际只传入一个,或要求一个入参,实际缺传入两个的问题。
不使用局部变量初始化引用
Linux
下g++
编译器报错为无法绑定非常量右值。
不使用布尔类型(false
)到空指针(nullptr
)的隐式转化。
Visual Studio
有较高的容错,布尔值false
在某些VC
版本下可以与nullptr
等价使用。Linux
下编译报错cannot convert 'bool' to userclass* in return
,无法在返回时转化bool
类型为userclass*
类型。
//示例1
virtual userClass* sourceName(){return false;}
//示例2
userClass* func(userClass* sid)
{
NOT_NULL(sid)
}
//NOT NULL 定义为:
#define NOT_NULL(aPointer) \
{ \
if(nullptr==aPointer) \
{ \
return false; \
} \
}
Linux
下宏函数使用...
和__VA_ARGS__
会编译不通过。
#define FUN(funName,...)\
{\
muFun(funName,__VA__ARGS__);\
}
Linux
下编译报错为expected primary-expression before'{'token。
可改为:
template<typename... Args>
bool FUN (const std::string& funName,Args...args)
{
return myFun(funName,std::forward<Args>(args)...);
}
std::is_convertible
在编译时候会产生歧义。
Windows
平台下动态库导出,一般使用__declspec(dllexport)
标识,如下实例:
class __declspec(dllexport) sonClass:FatherClass
但是此标识在Linux
中无法识别,需要添加宏隔离如下实例:
#if defined(_WIN32)||defined(__MSC_VER)
class __declspec(dllexport) sonClass:pubulic FatherClass
#elif defined(__linux__)||define(__GNUC__)
class __attribute__(xxxx):pubulic FatherClass
最好的方式就是将跨平台的宏预先定义在一个头文件中,简化代码:
#if define(_WIN32)||defined(__MSC_VER)
xxxx
#else define(__linux__)||defined(__GNUC__)
xxxx
#endif
知识扩充:
Windows
平台下,编译静态库后,再使用此静态库并导出为动态库时候,不需要特殊处理,但是这种情况,Linux
平台下编译静态库时,需要添加额外的编译选项-fPIC
,需要在CMakeList.txt
中添加分支持特殊处理:
if(UNIX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC")
#endif()