论“使用C++非内置全局变量的风险”

   一、C/C++语言中的各种变量

       C/C++语言中 的变量(variable)根据其存储持续性(生命周期)、作用域(scope,可见性)和链接性(linkage)(参考《C++Primer Plus》或博客)可以分为(static)全局变量、静态局部变量和自动变量(临时变量)以及类的成员变量。代码中使用最频繁的一般是自动变量,它的生命周期从定义的时候开始,代码块(花括符)下边界结束,它的作用域也是在其所在的代码块中,此外,它没有外部链接性(不能跨编译单元)。类的成员变量与自动变量有点类似,不同的是它的范围不是代码块,而是整个类。此外可以参考博客:BSS段,数据段,代码段,堆和栈,从编译器对内存分配的角度来看各种变量类型。

        在选择使用哪种变量的时候,一般第一考虑自动变量,然后再考虑类的成员变量,再然后考虑静态局部变量,最后考虑静态全局变量。当然每种变量都有其特定的应用范围,甚至是不可替代的功能。比如全局变量,它可以跨越编译单元,在整个程序中被使用。MFC中的the app就是这样的一个全局变量。另外,假设有一个这样的应用场景,我们的程序需要一个状态标志,它会在许多不同的编译单元和不同的类中被用到(读——获取状态,写——修改状态),这个时候我们就需要一个全局变量。

        但是,使用全局变量也会产生一些风险:

        1,名字冲突,特别是与系统自定义变量的名字冲突(如与“windows.h“中的变量名冲突)。不过这个可以很好的使用名称空间(namespace)来规避。

        2,出现bug时,不易定位,且很难调试。全局变量是在进入main()函数之前初始化,在退出main()之后进行清理工作,不方便设断点。下面引入一个项目中的实际案例进行说明。


       二、案例引入

        项目成员C为一个GUI软件要实现一个中英文切换的功能,他采用的是在代码中定一个全局的map<string, string>,将待切换中英文信息放在一个map中,需要的时候,去map中查找并转换。C采用的是MinGW编译器,支持C++11的初始化列表,故他是这样定义这个map:

<span style="font-size:14px;">#ifndef SF_RFID_STRINGLINGUIST
#define SF_RFID_STRINGLINGUIST
#include <QMap>
#include <QString>

#define TR(strEn) getTrQString(strEn, g_bIsEn)

extern bool g_bIsEn;

const QMap<QString, QString> g_mapLinguist = {
    {"Initializing...",                                               QString::fromLocal8Bit("初始化中...")},
    {"Fail to create instrument object !",                            QString::fromLocal8Bit("创建设备句柄失败!")},
    {"Fail to load instrument library !",                             QString::fromLocal8Bit("载入仪器运行库失败!")},
    {"Fail to initialize instrument !",                               QString::fromLocal8Bit("初始化仪器失败!")},
    {"Fail to load calibration data !",                               QString::fromLocal8Bit("载入校准文件失败!")},
    {"Succeed to initialize instrument !",                            QString::fromLocal8Bit("初始化仪器成功!")},
    {"Fail to reset dsp data and load dsp protocol data !",           QString::fromLocal8Bit("DSP数据重置以及载入DSP协议数据失败!")},
    {"Warning!",                                                      QString::fromLocal8Bit("警告!")},
    {"One or more list is empty!",                                    QString::fromLocal8Bit("一个或多个列表为空!")},
    {"StateDiagram",                                                  QString::fromLocal8Bit("状态机转移图")},
    {"Error protocol! Please close the program.",                     QString::fromLocal8Bit("错误的协议!请关闭程序.")},
    {": Please set the parameter of reader as below.",                QString::fromLocal8Bit(":请按照下面的提示设置读卡器参数.")},
    {"Change Chinese Language Success",                               QString::fromLocal8Bit("切换中文成功")},
    {"Please Restart Softrument RFID Tester To Active New Language",  QString::fromLocal8Bit("请重启软仪RFID测试程序以使用新的语言")},
    {"Change English Language Success",                               QString::fromLocal8Bit("切换英文成功")},
    {"None test case is selected !",                                  QString::fromLocal8Bit("没有选择测试项!")}};

const QString getTrQString(const QString strEn, bool bIsEn);

#endif // SF_RFID_STRINGLINGUIST</span>


        C看似使用了全局变量g_mapLinguist,实际上他使用的是多个static局部变量,因为,const修饰符会修改全局变量的特性,将其变为一个static局部变量。但是,本文这个不是本文的关注点,只是提一下,也不影响本文要讨论的问题。

        本人采用的是VS2013,不能完全支持C++11的初始化列表,拿到这份代码编译即报错:

”1>sf_rfid_stringlinguist.obj : error LNK2001: 无法解析的外部符号 "class QMap<class QString,class QString> g_mapLinguist" (?g_mapLinguist@@3V?$QMap@VQString@@V1@@@A)“

        通过在代码中搜索”g_mapLinguist“找到error产生的地方(对于链接错误,VS不能自动跳转到错误的地方,需要程序员自己根据关键词去搜索)。修改代码如下:

<span style="font-size:14px;">const QMap<QString, QString> g_mapLinguist ({
    { "Initializing...", QString::fromLocal8Bit("初始化中...") },
    { "Fail to create instrument object !", QString::fromLocal8Bit("创建设备句柄失败!") },
    { "Fail to load instrument library !", QString::fromLocal8Bit("载入仪器运行库失败!") },
    { "Fail to initialize instrument !", QString::fromLocal8Bit("初始化仪器失败!") },
    { "Fail to load calibration data !", QString::fromLocal8Bit("载入校准文件失败!") },
    { "Succeed to initialize instrument !", QString::fromLocal8Bit("初始化仪器成功!") },
    { "Fail to reset dsp data and load dsp protocol data !", QString::fromLocal8Bit("DSP数据重置以及载入DSP协议数据失败!") },
    { "Warning!", QString::fromLocal8Bit("警告!") },
    { "One or more list is empty!", QString::fromLocal8Bit("一个或多个列表为空!") },
    { "StateDiagram", QString::fromLocal8Bit("状态机转移图") },
    { "Error protocol! Please close the program.", QString::fromLocal8Bit("错误的协议!请关闭程序.") },
    { ": Please set the parameter of reader as below.", QString::fromLocal8Bit(":请按照下面的提示设置读卡器参数.") },
    { "Change Chinese Language Success", QString::fromLocal8Bit("切换中文成功") },
    { "Please Restart Softrument RFID Tester To Active New Language", QString::fromLocal8Bit("请重启软仪RFID测试程序以使用新的语言") },
    { "Change English Language Success", QString::fromLocal8Bit("切换英文成功") },
    { "None test case is selected !", QString::fromLocal8Bit("没有选择测试项!") } });</span>

        编译通过。但是程序运行的时候会产生两个bugs:

        一是在调用到该map的地方会奔溃;

        二是程序关闭时不是自然退出,而是奔溃;


        三、调试过程

       进入debug模式,开始调试:

        1,bug 1:

论“使用C++非内置全局变量的风险”_第1张图片

查看调用堆栈

论“使用C++非内置全局变量的风险”_第2张图片


        看蓝色那一行,从这一行开始是自定义代码。再次运行,并在该处设置断点。

论“使用C++非内置全局变量的风险”_第3张图片


        展开该map,可以清楚的看到第4、6、7、8项都产生了错误,这就是问题所在。其具体原因是VS2013不能完全支持C++11的初始化列表,虽然编译通过了,还是会产生运行时错误。后面将介绍如何修复这个bug。

        先注释这个bug,定位分析下一个bug。

        1,bug 2:

         关闭软件时,VS弹框,”触发了一个断点“。在主窗口析构函数和main()函数中设断点均不能捕获,故只有查看堆栈和反汇编。

论“使用C++非内置全局变量的风险”_第4张图片


        从调用堆栈可以清楚看到,程序是在退出main()函数之后产生的”触发断点“错误。须知:windows程序的main()函数变异为:_tmainCrtStartup()和WinMainCRTStarup()。再往上看,”`dynamic atexit destructor for 'g_mapLinguist''()“,意思是在析构该静态的自定义变量。此时发生了异常。结合bug1可知,应该是在创建该map时,其中有几项未创建成功,析构时造成内存泄漏。其根本原因与bug1相同。

        本文并非讨论编译器的不支持C++11,而是着重与这种全局变量产生的bug和如何定位分析这类bug。(查看堆栈)


        四、问题解决

        替代初始化列表,采用手动insert项的方式初始化map。在次就涉及到一个问题,因为这个map是全局变量,需要确定在何时调用insert函数初始化,即需要解决初始化在先,使用在后的问题。C给出的方案是在main()或mainwindow的构造函数中初始化,这个是最早执行的地方,能确保”初始化在先,使用在后的“,但是这样会使代码散落各处。我的设计方案如下:

<span style="font-size:14px;">#ifndef SF_RFID_STRINGLINGUIST
#define SF_RFID_STRINGLINGUIST
#include <QMap>
#include <QString>

#define TR(strEn) getTrQString(strEn, g_bIsEn)

extern bool g_bIsEn;
extern QMap<QString, QString> g_mapLinguist;


const QString getTrQString(const QString strEn, bool bIsEn);

#endif // SF_RFID_STRINGLINGUIST</span>

在头文件中仅进行声明,且取消const修饰符,变为真正的全局变量。在对应的源文件中进行定义和初始化,如下:

<span style="font-size:14px;">QMap<QString, QString> g_mapLinguist;

static struct InitMapLinguist
{
    InitMapLinguist()
    {
        g_mapLinguist.insert({ "Initializing..."}, QString::fromLocal8Bit("初始化中...") );
        g_mapLinguist.insert({ "Fail to create instrument object !"}, QString::fromLocal8Bit("创建设备句柄失败!") );
        g_mapLinguist.insert({ "Fail to load instrument library !"}, QString::fromLocal8Bit("载入仪器运行库失败!") );
        g_mapLinguist.insert({ "Fail to initialize instrument !"}, QString::fromLocal8Bit("初始化仪器失败!") );
        g_mapLinguist.insert({ "Fail to load calibration data !"}, QString::fromLocal8Bit("载入校准文件失败!") );
        g_mapLinguist.insert({ "Succeed to initialize instrument !"}, QString::fromLocal8Bit("初始化仪器成功!") );
        g_mapLinguist.insert({ "Fail to reset dsp data and load dsp protocol data !"}, QString::fromLocal8Bit("DSP数据重置以及载入DSP协议数据失败!") );
        g_mapLinguist.insert({ "Warning!"}, QString::fromLocal8Bit("警告!") );
        g_mapLinguist.insert({ "One or more list is empty!"}, QString::fromLocal8Bit("一个或多个列表为空!") );
        g_mapLinguist.insert({ "StateDiagram"}, QString::fromLocal8Bit("状态机转移图") );
        g_mapLinguist.insert({ "Error protocol! Please close the program."}, QString::fromLocal8Bit("错误的协议!请关闭程序.") );
        g_mapLinguist.insert({ ": Please set the parameter of reader as below."}, QString::fromLocal8Bit(":请按照下面的提示设置读卡器参数.") );
        g_mapLinguist.insert({ "Change Chinese Language Success"}, QString::fromLocal8Bit("切换中文成功") );
        g_mapLinguist.insert({ "Please Restart Softrument RFID Tester To Active New Language"}, QString::fromLocal8Bit("请重启软仪RFID测试程序以使用新的语言") );
        g_mapLinguist.insert({ "Change English Language Success"}, QString::fromLocal8Bit("切换英文成功") );
        g_mapLinguist.insert({ "None test case is selected !"}, QString::fromLocal8Bit("没有选择测试项!") );
    }
} initStruct;</span>

在此使用了一个技巧,(参考我之前将”动态创建“的博客)定义一个静态的结构体,让该结构体的构造函数执行一些需要在main()之前执行的操作。这种设计一使得代码不会分散,二是保证定义和初始化的时机,早于main()函数。

注:第一版没写了static和变量名“initStruct”。其中,不写变量名则不会创建这样的一个结构体实例,也就是说不会调用其构造函数;而static的作用是限制该变量只在该文件可见。


五、总结

        1,需要全局变量时,我们一般需要这样定义和声明:1)在某个.cpp文件中定义该全局变量,如 CategoryInfo g_stCategoryInfo;其中CategoryInfo是一个自定义的结构体;2)在其他用到该全局变量的地方声明该变量,如:extern CategoryInfo g_stCategoryInfo;。一般放在.h文件中声明,需添加extern修饰符。

        2,使用”调用堆栈“定位全局变量产生的bug。

        3,根据关键词自行搜索编译时产生的链接错误。



六、后记

        近日,我在阅读《Windows驱动开发详解》一书时发现:全局变量可以用于多线程直接的通信。在多线程编程时,函数有“可重入”和“不可重入”之分。所谓“可重入”,是指函数的执行结果不和执行顺序有关。反之,如果执行结果和执行顺序有关,则称这个函数是不可重入的。

        在函数中使用全局变量,如果不对它进行同步保护,则该函数是不可重入的。故,在大多数情况下,为了考虑多线程,应尽量避免使用全局变量。


你可能感兴趣的:(论“使用C++非内置全局变量的风险”)