• 跨平台代码编写规范——参考《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";
    
    • 1
    • 2
    • 3
    • 4
    • 5

    很多函数或api在两种系统下支持/作为路径分隔符。
    实例

    std::filesystem::exists("D:/test/1.txt");
    bool isExit = QFile::exists("D:/test/1.txt");
    QFile log("D:/test/1.txt");
    
    • 1
    • 2
    • 3

    三、宏隔离

    不同平台宏隔离建议使用_WIN32__linux__

    3.1 Windows下的宏

    以下是一些标识Windows的宏:

    WIN32 WIN64 _WIN32 _NT _WIN64
    
    • 1

    建议使用_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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.4 注意:

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

    //实例
    #ifnedf _WIN32
    //code for linux
    #endif 
    
    #ifndef __linux__
    //code for windows
    #endif 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果软件有超过2个平台的跨平台需求,若使用非此即彼的f方式定义,在跨第三个平台时候,就需要改动大量代码,可能因修改疏漏导致未知错误。应如下修改:

    #ifdef __linux__
    //code for linux
    #endif 
    
    #ifdef _WIN32
    //code for windows
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    四、 头文件

    4.1 字母大小写

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

    //qt头文件
    #include
    
    • 1
    • 2

    修改为:

    #include
    
    • 1

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    4.4 平台专属函数

    #if _MSC_VER >1400 
    #define fgetc _fgetc_nolock
    #endif
    
    • 1
    • 2
    • 3

    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());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    知识扩展:
    _MSC_VER是微软内部的一个版本,下表为Visual Studio以及VC++的对应表。

    _MSC_VERVisual StudioVC++
    1910VS2017VC 15.0
    1900VS2015VC 14.0
    1800VS2013VC 12.0
    1700VS2012VC 11.0

    4.5 精简头文件(建议)

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

    //示例
    class TestClass;
    //源文件中包含
    #include "../../test/TestClass.h"
    
    • 1
    • 2
    • 3
    • 4

    注:前置声明仅支持指针和引用的声明,在头文件中相关的操作可能会失败,如:

    class TestClass;
    class Test
    {
    ~Test()
    {
    	if(!ptr)
    	{
    	 delete ptr;//此处delete可能会失败,造成内纯泄露,建议将实现移动到cpp文件中
    	 ptr = nullptr;
    	}
    }
    private:
    	TestClass* ptr{nullptr};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    五、语法错误

    Windows平台上MSVC编译器忽略或自动纠正的语法错误。

    5.1 模板缺少具体的类型

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

    错误示例:
    QList a = GetListString();
    vec.push_back(std::make_pair("key"),value));
    
    • 1
    • 2
    • 3

    需要更正为:

    QList<QString> a = GetListString();
    vec.push_back(std::make_pair<string,double>("key"),value));
    
    • 1
    • 2

    5.2 冗余的宏扩展

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

    5.3 冗余的限定符

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

    //错误实例
    class TestClass
    {
    QString TestClass::getName(const QString &func);
    }
    //需要去掉`TestClass::`
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

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

    5.4 右值取地址

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

    //错误示范
    FUN("TEST",&TestClass("test"))
    //正确示范
    TestClass obj("test");
    FUN("TEST",obj);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    5.5 常量限定类型

    使用常量限定类型到非常量限定类型指针传递时,需要转化
    const限定类型地址赋值到非const限定类型指针。

    //错误实例
    char* ptr = str.data();
    
    • 1
    • 2

    发生const char* char*的强制转化
    需要添加类型转化或者更改类型。

    //正确方案
    //1 - 更改目标类型
    const char* ptr = str.data();
    
    //2 - 强转
    char* ptr = (char*)str.data();
    
    //3 - 操作符
    char* ptr = const_cast<char*>(str.data());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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; 		\
    		}						\
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    六、其他问题

    6.1 可变长参数

    Linux下宏函数使用...__VA_ARGS__会编译不通过。

    #define FUN(funName,...)\
    {\
    	muFun(funName,__VA__ARGS__);\
    }
    
    • 1
    • 2
    • 3
    • 4

    Linux下编译报错为expected primary-expression before'{'token。
    可改为:

    template<typename... Args>
    bool FUN (const std::string& funName,Args...args)
    {
    return myFun(funName,std::forward<Args>(args)...);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    6.2 类型与对象歧义

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

    6.3 动态库导出

    Windows平台下动态库导出,一般使用__declspec(dllexport)标识,如下实例:

    class __declspec(dllexport) sonClass:FatherClass
    
    • 1

    但是此标识在Linux中无法识别,需要添加宏隔离如下实例:

    #if defined(_WIN32)||defined(__MSC_VER)
    class __declspec(dllexport) sonClass:pubulic FatherClass
    #elif defined(__linux__)||define(__GNUC__)
    class __attribute__(xxxx):pubulic FatherClass
    
    • 1
    • 2
    • 3
    • 4

    最好的方式就是将跨平台的宏预先定义在一个头文件中,简化代码:

    #if define(_WIN32)||defined(__MSC_VER)
    xxxx
    #else define(__linux__)||defined(__GNUC__)
    xxxx
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5

    知识扩充:
    Windows平台下,编译静态库后,再使用此静态库并导出为动态库时候,不需要特殊处理,但是这种情况,Linux平台下编译静态库时,需要添加额外的编译选项-fPIC,需要在CMakeList.txt中添加分支持特殊处理:

    if(UNIX)
     set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC")
    #endif()
    
    • 1
    • 2
    • 3

    6.4 操作符typid

  • 相关阅读:
    【Android】adb无法连接设备原因及解决办法
    linux 关闭ssh后命令继续执行
    开源推荐,腾讯正式开源 Spring Cloud Tencent
    基于 Bresenham 算法画圆
    【LittleXi】【MIT6.S081-2022Fall】Lab: syscall
    梦开始的地方 —— C语言: 函数指针+函数指针数组+指向函数指针数组的指针
    C语言:(动态内存管理)
    Linux基本指令
    【JavaSE】多线程篇(五)线程专项练习题
    工业标准半导体SECS标准低频RFID读写器JY-V640性能与应用方案
  • 原文地址:https://blog.csdn.net/qq_42615475/article/details/133632042