C++:优雅的构造全局变量

首先声明一点,标题中说的全局变量是指的这样的需求:我们需要一个对象,能够全局的使用它。

简单构造方式

一般我们首先想到是使用全局的静态变量,即全局变量、namespace作用域变量、以及在classes内以及在file作用域声明为static的变量,这样我们就可以在文件其他地方,甚至是其他编译模块使用它(当然不包括file作用域声明为static的变量)。如下:

golobal.hpp:

//全局变量 声明 
extern int a;

//file作用域变量声明为static 声明 错误!!!其实是定义 
//static int b;

//namespace作用域变量 声明 
namespace Test{
	extern int c;
};

//classes内声明为static变量 声明 
class Test_C{
public:
	static int d;
};

golobal.cpp:

#include "Golobal.hpp" 

//全局变量 定义 
int a = 3;

//file作用域变量声明为static 定义 
static int b = 3;

//namespace作用域变量 定义 
int Test::c= 3;

//classes内声明为static变量 定义 
int Test_C::d = 3;

main.cpp

#include <iostream>
#include "Golobal.hpp"
using namespace std;

int main(int argc, char *argv[])
{
	cout<<"a:"<<a<<" c:"<<Test::c<<" d:"<<Test_C::d;
	return 0;
}

运行结果:

问题

但是我们得意识到一个问题,那就是C++中关于不同编译模块中全局的静态变量初始化顺序是不确定的,也就是说如果两个全局的静态变量定义在不同的文件,而且其中一个变量A的初始化需要依赖于另一个变量B的初始化(这同时也意味着你的设计并不是太好,但有时我们必须这样实现),那么就有可能出现莫名其妙的问题,这是因为编译器不能保证A和B初始化顺序,那么在A初始化的时候B有可能没有初始化,从而使用了未初始化的B,这是非常危险的,举一个很简单的例子:

int a = 3;
int b = a;

static int c = b;
void main(void){
    use(c);
    return;
}

以上代码放在同一个文件中是没有任何问题的,能够保证b的值为3,但如果a和b的定义放在不同的实现文件中,就无法保证b的值一定是a初始化后的值,也就是3(当然你的编译器或者能简单的得到正确的答案,但千万别写这种依赖于编译顺序的代码),我们可以这样来分析,int a=3;其实是分为两步的,定义和初始化,定义则为a这个符号形式上分配了一块内存,拥有了虚拟内存地址和占用大小(假设在0x10000),这些分配的操作是在编译期就进行的(当然是形式上,具体运行期还需要进行装载到真正的内存的操作),这样在程序链接期,其他编译模块都可以在声明后使用这个全局变量而不会报未定义的错误,而初始化则在编译链接期生成了类似copy 0x10000 0x03(把立即数3写入0x10000地址内存里)的指令,这条指令执行后这个内存才会是3,否则就是个随机的值,然后执行这条指令需要等到程序运行期,同理b也是如此,这样b=a这条赋值语句如果运行期在copy 0x10000 0x03之后运行,那么我们就能得到正确的初始值,否则就只能得到一个未初始化的值。

优化

那如何得到保证过初始化的值呢?这里我们利用C++的一个保证:函数内局部的静态对象(在函数内声明为static的对象)会在该函数被调用期间首次遇到该对象之定义式时被初始化。我们可以这样来分析,静态对象的存储区域位于静态区,同理在编译期时,如果存在该函数的调用,那么函数内局部静态对象的符号就已经和一段静态区域的内存绑定映射了,只是它的作用域位于函数内,而我们知道调用函数,其实就是调用子程序,也就是函数内的代码,那么C++的这个保证也就意味着我们在函数内第一次定义该局部静态对象时,如果未初始化,则该对象就会映射到一块区域值为0的内存,也就是我们说的默认初始化值为0(整形静态变量),如果我们指定了初始化,则默认会在函数内第一次定义式后续的指令之前插入一条初始化指令,以保证该对象被初始化,这样当我们调用函数时,就能保证得到初始化的值了。

所以较好的构造全局对象的方法应该是,将全局性的静态变量定义为局部性的静态变量,并通过一个函数返回,如果想改变该对象的值,则返回该对象的引用,否则直接按值返回就行啦。至于这个函数你即可以按照C的方式定义,也可以定义为类中的一个静态函数。如下:

Golobal.hpp:
int& a();
	
class Test_C{
public:
	static int b();	
};

Golobal.cpp:
#include "Golobal.hpp" 
int& a(){
	static int a = 3;
	return a;	
}
int Test_C::b(){
	static int b = a();
	return b;
}

main.cpp:
#include <iostream>
#include "Golobal.hpp"
using namespace std;

int main(int argc, char *argv[])
{
	cout<<"a:"<<a()<<" b:"<<Test_C::b()<<endl;
	int& c = a();
	c = 4;
	cout<<"a:"<<a()<<" b:"<<Test_C::b()<<endl;
	return 0;
}

运行结果:

另一个好处

其实这样使用全局变量还有一种好处,那就是当你没有在程序中调用该函数时,那么就不会存在该局部静态变量的构造和析构成本,还有一个好处,那就是我们可以在使用该全局变量时自定义一些操作,因为它本质上是一个函数,可能你会有点难理解这一点,举个例子,我现在有一个需求:程序有一个全局变量可以供其他任何模块使用,而且要求在第一次使用的时候利用标准输入赋值,且赋值范围为0~100,这样我就可以在程序后面的部分根据该值的不同进行不同的操作,比如1~20干啥,20~50干啥,50~100干啥,而且在后面的使用过程中不再改变该值(不再进行标准输入这一过程)。

如果我们使用本文开头说的单纯的定义全局变量的方式,除了会产生全局变量初始化顺序依赖的问题,还会有一个问题,那就是我们需要找到该程序第一次使用该变量的地方,然后加上标准输入,然而这有时候是不容易找到的,甚至在不同的情况下,其第一次使用的地方也不一样,还有一种解决方法就是判断是否已经输入的控制逻辑,如果没有输入那么就进行标准输入的逻辑,但是这样在每处使用该变量时都得加上该逻辑代码,这样岂不是非常麻烦,且不具有良好的维护性,其实到这里我相信读者已经想到利用模块编程的方法了,即定义一个模块取得该全局变量,绕来绕去,这其实就是我们上面讲的好处:在使用该全局变量时自定义一些操作。如下:

golobal.hpp
int a();

golobal.cpp
#include <iostream> 
#include <assert.h>
#include "golobal.hpp" 
int a(){
	static int a = -1;
	if (a==-1){
		std::cout<<"input a:";
		std::cin>>a;
		assert(a>=0 && a<=100);
	}
	return a;	
}

main.cpp:
#include <iostream>
#include "golobal.hpp"
using namespace std;

int main(int argc, char *argv[])
{
	a();
	cout<<"a:"<<a()<<endl;
	return 0;
}

运行结果:

猜你喜欢

转载自blog.csdn.net/g1093896295/article/details/103225655