头文件定义全局变量的探究

编译环境

  本篇文章中编译环境

5.15.34-amd64-desktop #2 SMP Mon May 16 16:31:30 CST 2022 x86_64 GNU/Linux
gcc version 8.3.0 (Uos 8.3.0.3-3+rebuild) 
gcc version 8.3.0 (Uos 8.3.0.3-3+rebuild) 

问题的引入

  今天在自己编程的时候,遇到了一个问题,让我更加深入的了解了一些C++的语法,让我受益匪浅,这是今天遇到的问题
avatar
  我在一个头文件中定义了一个全局变量的字符串指针变量,有多个模块都会引用这个头文件,在编译的时候遇到了这个错误。从这一个问题上我又思考到了别的问题,在这里一并记录,以便后期理解。本篇笔记都是用C++进行的测试


在头文件中定义全局变量是对的吗?

  既然思考,我便从头开始思考这个问题,为此我开始写demo做测试,首先是创建几个文件public.h a.cpp a.h main.cpp。a.h和main.cpp都包含public.h,而public.h文件中有一个int num变量。

//public.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
int num = 0; //全局变量 这个变量赋不赋值编译结果都一样

//a.h 
#pragma once
#include "public.h"

//main.cpp
#include "public.h"
int main()
{
    printf("this is main\n");
    return 0;
}

  demo就这么简单,然后编译看看结果,如下,没有编译成功,因为num变量重复定义。

multiple definition of `num'; /tmp/ccXSgu5P.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status

  但是我在网上看到C语言编译是不会报错的,所以也试试,把上面的cpp文件改成c文件再试一下。结果发现,如果num赋初值就会报重复定义错误,如果不赋初值就可以编译成功。 也挺神奇的。那就看看怎么回事,生成a.c和main.c的目标文件,看看里面的内容。

不赋初值的情况:
a.o     
             0000000000000000 T fun
             0000000000000004 C num

main.o  
             0000000000000000 T main
             0000000000000004 C num
                              U printf

赋初值的情况:
a.o
			0000000000000000 T fun
			0000000000000000 B num

main.o
			0000000000000000 T main
			0000000000000000 B num
						     U printf

  可以看到两种情况中num前面的类型不一样,所以应该是这个差异导致了不同的编译结果

  看了C语言的,我就也想看看C++生成的.o文件内容,如下

赋初值 和 不赋初值的情况一样
a.o
		0000000000000000 B num
		0000000000000000 T _Z3funv
		
main.o
		0000000000000000 T main
		0000000000000000 B num
        		         U puts

  所以C++编译报错和C语言num赋初值编译报错原因应该一样。【TODO 这个符号含义我也不太懂,这部分的结论都是自己猜测的,以后有机会再补充这里,读者切勿参考这段】 本篇文章还是以C++为主。

  按照常见的写法,变量的定义都是在.cpp中进行的,而如果其他文件需要使用变量则需要进行extern外部声明。如下,所以在头文件中定义全局变量是不太推荐的

参考博客 https://blog.csdn.net/nbu_dahe/article/details/117434124


//public.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
extern int num; //声明

//a.cpp
#include  "a.h"
int num; 
int fun()
{
    return 0;
}

//main.cpp
#include "public.h"

int main()
{
    extern int num;
    num = 10;
    printf("this is main num = %d\n",num);
    return 0;
}

难道不能在头文件中定义了全局变量了吗?

  C&C++的魅力就是在于没有什么事是不能做的,所以头文件中当然也可以定义全局变量,但是单纯的一个变量肯定是不行的,这里需要关键字修饰。

  但是使用什么关键字修饰呢?在这里我使用了staticconst进行了修饰,在编译时都没有报错,但是要注意没有报错不等于得到了我们想要的结果。static修饰了全局变量,其实每个头文件引用的便不是原本的变量了,我做了测试在public.h头文件中定义全局静态变量,然后在不同的源文件中使用它(赋值并输出信息)

//====================a.h=====================
#pragma once
#include "public.h"
class A
{
  public:
     A();
};



//====================a.cpp=====================
#include  "a.h"
A::A()
{
    public_data = 10;
    cout<<__FUNCTION__<<" public_data = "<<public_data<<" addr = "<<&public_data<<endl;
}



//====================b.h=====================
#pragma once
#include "public.h"
class B
{
  public:
     B();
};



//====================b.cpp=====================
#include  "b.h"
B::B()
{
    public_data = 19;
    cout<<__FUNCTION__<<" public_data = "<<public_data<<" addr = "<<&public_data<<endl;
}



//====================public.cpp=====================
#include "public.h"
void fun()
{
    cout<<__FUNCTION__<<" public_data = "<<public_data<<" addr = "<<&public_data<<endl;
}



//====================public.h=====================
#pragma once
#include <iostream>
using namespace std;
void fun();
static int public_data;




//====================main.cpp=====================
#include "public.h"
#include "a.h"
#include "b.h"
int main()
{
    public_data = 30;
    cout<<__FUNCTION__<<" public_data = "<<public_data<<" addr = "<<&public_data<<endl;
    A a;
    B b;
    fun();
    return 0;
}




//运行结果
main public_data = 30 addr = 0x404198
A    public_data = 10 addr = 0x4041a0
B    public_data = 19 addr = 0x4041a8
fun  public_data = 0  addr = 0x4041b0

  很明显从输出的结果中看,每个源文件中的public_data地址都不一样,说明它们已经不是一个变量了而是四个变量。很明显每个模块(x.h x.cpp 两个文件的组合可以称作模块)在引用public.h时都会新定义一个public_data变量,这是为什么地址不同的原因。另外由于每个变量都被static修饰导致了变量对其他模块是不可见的,所以这样并不会报重定义的错误。 所以这种定义方式的全局变量并不是我们想要的,这只是每个模块内部的全局变量。


  接着我又使用常见的做法进行了测试,为的是与上面的结果做个对比。

//这里仅仅是public.cpp 和 public.h文件做了修改,其他文件没有修改

//==================public.h===================
#pragma once
#include <iostream>
using namespace std;
void fun();
extern int public_data;


//==================public.cpp===================
#include "public.h"
int public_data = 100;
void fun()
{
    cout<<__FUNCTION__<<" public_data = "<<public_data<<" addr = "<<&public_data<<endl;
}

//运行结果
main public_data = 30 addr = 0x404068
A public_data    = 10 addr = 0x404068
B public_data    = 19 addr = 0x404068
fun public_data  = 19 addr = 0x404068

  这个结果才是我想要的,地址都是一样的,说明是同一个变量。
  除了static外,还有一个const,测试一下它是不是可以达到我们的预期。 我们在上面例子的基础上稍作修改,在public.h文件中定义const全局变量,public.cpp中去除public_data的定义,在其他的文件中删除赋值的操作,如下

//public.h 
#pragma once
#include <iostream>
using namespace std;
void fun();
const int public_data = 100;


//运行结果
main public_data = 100 addr = 0x402008
A public_data    = 100 addr = 0x402030
B public_data    = 100 addr = 0x402054
fun public_data  = 100 addr = 0x402078

  很明显,这似乎也是四个变量,在 C++ 看来,全局const定义就像使用了static说明符一样。虽然也是又多个备份,但是我们在使用const常量时一般都是读取操作,不会修改,这样用似乎也可以接受。


世界线收束,回归问题

  现在回到我的问题上来,我在头文件中定义如下的const变量为什么会报错

//在头文件中定义报错
const char * const_QStrUserDataFileName = "./user_data/";   

  说一下自己的理解,首先这样做按照自己的想法是没错的,我使用const修饰了一个全局变量只用作读取,它的效果和static修饰的效果差不多。但是这是我自己想的,实际上我修饰的是一个指针变量,什么东西沾上指针可就复杂了(珍爱生命,原理指针)。

  在这里想搞清楚还需要理解指针常量常量指针的含义。可以参考一下博客

https://blog.csdn.net/weixin_42148156/article/details/104083456?spm=1001.2014.3001.5502

  很明显,我声明的是一个常量指针,const修饰的是其后的整个 *const_QStrUserDataFileName,而不是const_QStrUserDataFileName变量本身。于是const char * const_QStrUserDataFileName表示的意思就是,指针指向的位置的值不能变(*const_QStrUserDataFileName不能被赋值),但是指针可以指向其他位置(const_QStrUserDataFileName可以被赋值)。

  这就有意思了,我定义了一个常量指针,但是这个指针是一个变量而且还定义在头文件中就和前面提到的全局变量一样了,所以会报重复定义的错误。


解决方案

  既然问题已经知道产生的原因,那么也要提供一些解决方案。这里提供的解决方案仅仅修改后是不会报错,至于合不合适还是需要具体问题具体分析。

  1. 既然我需要的是指针常量,那么我就修改一下定义方式好了

   //注意这是指针常量,指针是个常量,但是定义在头文件中g++会警告,看的膈应
   //warning: ISO C++ forbids converting a string constant to ‘char*’ [-Wwrite-strings]
   char *  const const_QStrUserDataFileName  = "./user_data/"; 

   //<<Effective C++>>中提到过这个用法,在条款02
   const char * const const_QStrUserDataFileName  = "./user_data/";
 
   //<<Effective C++>>中提到过这个用法,在条款02  都用C++,整点string用用吧
   const std::string str = "./user_data/"; 
  
   //参考博客  https://blog.csdn.net/li1914309758/article/details/79971356
   const char  const_QStrUserDataFileName[]  = "./user_data/"; 
   
  1. 使用static修饰

   static const char *  const_QStrUserDataFileName  = "./user_data/";
   
  1. 在cpp文件中定义,在h文件中extern声明

  2. 其余方法参考 https://blog.csdn.net/u012411498/article/details/96891491,感谢博主

如若有误,欢迎各位大佬指正

  • 7
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值