一、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>
本人采用的是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>
一是在调用到该map的地方会奔溃;
二是程序关闭时不是自然退出,而是奔溃;
三、调试过程
进入debug模式,开始调试:
1,bug 1:
查看调用堆栈
看蓝色那一行,从这一行开始是自定义代码。再次运行,并在该处设置断点。
展开该map,可以清楚的看到第4、6、7、8项都产生了错误,这就是问题所在。其具体原因是VS2013不能完全支持C++11的初始化列表,虽然编译通过了,还是会产生运行时错误。后面将介绍如何修复这个bug。
先注释这个bug,定位分析下一个bug。
1,bug 2:
关闭软件时,VS弹框,”触发了一个断点“。在主窗口析构函数和main()函数中设断点均不能捕获,故只有查看堆栈和反汇编。
从调用堆栈可以清楚看到,程序是在退出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>
<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驱动开发详解》一书时发现:全局变量可以用于多线程直接的通信。在多线程编程时,函数有“可重入”和“不可重入”之分。所谓“可重入”,是指函数的执行结果不和执行顺序有关。反之,如果执行结果和执行顺序有关,则称这个函数是不可重入的。
在函数中使用全局变量,如果不对它进行同步保护,则该函数是不可重入的。故,在大多数情况下,为了考虑多线程,应尽量避免使用全局变量。