跨平台代码编写规范——参考《Loup&卡普》的文档

本文参考Loup&卡普的文档

一、概述

在大多情况下软件需要支持多个运行环境,如:Windows、Linux...Windows平台上的MSVC编译器比较宽松,部分错误编译器会自动纠正或者忽略,但是Linuxgcc/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__

3.1 Windows下的宏

以下是一些标识Windows的宏:

WIN32 WIN64 _WIN32 _NT _WIN64

建议使用_WIN32,此宏在操作系统为x86x64系统中都会定义。编译x86工程或32位操作系统下会额外定义WIN32,64位操作系统下会额外定义_WIN64

3.2 Linux下的宏

linux内核的操作系统建议使用__linux__宏,所用使用linux内核的系统都会默认定义此宏。

3.3 编译器宏

不同编译器也提供了编译器宏,所以我们可以使用编译器宏来区分环境。

//实例
#if defined(_MSC_VER)
std::string port("COM3")
#elif defined(__GUNC__)
std::string port("/dev/ttyUSB1")
#endif

3.4 注意:

特定系统的代码使用对应的宏,不需要检测别的系统的宏是否未定义:
以下代码是错误示范:

//实例
#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

四、 头文件

4.1 字母大小写

包含头文件时,文件名称需使用正确的字母大小写
Windows平台,文件系统不区分文件名的大小写,额外包括目录、控制台、PowerShell的命令都是不区分大小的。但Linux下的文件系统,命令等区分大小写。例如,我们无法在Windows同一级目录下创建名为Aa的目录,而Linux下可行。
以下代码是错误示范:

//qt头文件
#include

修改为:

#include

Qt头文件一般是Q前缀和紧跟第一个字母大写。多单词则为驼峰,如:QCoreApplication

4.2 缺少必要的头文件

引入函数或类时包含对应的头文件,个别头文件Visual Studio会提供,但Linux下不行。
math.h,memory.h,string.h等。这些相关头文件在Visual Studio的外部依赖项会提供。
所以如果使用了智能指针shared_ptr,unique_ptr需要手动包含一下,使用sqrt等数学公式的时候手动包含,使用memecpy时候,需要手动包含一下使用了其他类或函数也需要手动包括对应头文件。

4.3 平台专属头文件

使用平台专属类、函数时候,头文件和实现需要添加宏隔离
Windows平台专有的头文件,如Windows等,添加时,需要加入宏隔离。

//windows平台
#if _WIN32
#include "a.h"
#else
#include 
#endif

4.4 平台专属函数

#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

4.5 精简头文件(建议)

精简非必要的头文件使用
①:如非必要,尽量不包含多余的头文件,包含的头文件会在预编译阶段展开,除影响编译速度外,增加额外需要的链接库,影响软件最终体积。
②:尽量使用前置声明,避免不必要的头文件展开影响编译时间。
如:头文件中使用类声明,源文件中包含头文件。

//示例
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编译器忽略或自动纠正的语法错误。

5.1 模板缺少具体的类型

使用模板时,需要显式声明模板具体类型。

错误示例:
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));

5.2 冗余的宏扩展

不适用冗余的宏扩展
##用户合成一个标识符
Linux环境下报错为毗邻‘##’无法构建一个有效的标识符‘,所以要去掉##

5.3 冗余的限定符

不在类声明中和非静态函数调用时,使用多余的命名空间限定符。
①类声明中冗余的命名空间限定符,MSVC会忽略。

//错误实例
class TestClass
{
QString TestClass::getName(const QString &func);
}
//需要去掉`TestClass::`

②调用非静态函数时的命名空间限定符

//错误实例
return QJsonDocument::QJsonDocument(jsobj).toJson(QJsonDocument::Compact);
//此处构建出来的QJsonDocument对象调用了非静态函数需要去掉多余的QJsonDocument::

5.4 右值取地址

函数调用传参时,不在调用同时创建局部变量并使用其地址。
在函数调用传实参数处,构建局部对象并取用其地址,Linux下会编译报错:
taking address of rvalue

//错误示范
FUN("TEST",&TestClass("test"))
//正确示范
TestClass obj("test");
FUN("TEST",obj);

5.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());

5.6 入参使用限定类型(建议)

函数声明与实现,参数若不更改,尽量使用限定类型,即使用const关键字修饰字修饰。
C中的常量字符串类型为char*,而C++中常量字符串类型为 const char*

5.7 宏函数参数错误

宏函数使用时,参数个数需要与定义时保持一致。
MSVC有较高的容错,暂时未出现问题,宏函数参数数量与定义不一致。有些含函数要求入参,实际只传入一个,或要求一个入参,实际缺传入两个的问题。

5.8 使用局部变量初始化引用

不使用局部变量初始化引用
Linuxg++编译器报错为无法绑定非常量右值。

5.9 布尔值与指针转化

不使用布尔类型(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; 		\
		}						\
}

六、其他问题

6.1 可变长参数

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)...);
}

6.2 类型与对象歧义

std::is_convertible在编译时候会产生歧义。

6.3 动态库导出

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()

6.4 操作符typid

你可能感兴趣的:(C++,c++)