C/C++中的全局变量与符号

搬运自我的CSDN https://blog.csdn.net/u013213111/article/details/106773639

0 引言

起因是在头文件中定义了全局变量,而又有多个不同的源文件包含了这个头文件,这样显然会出现multiple definition的问题。
以下是对上述问题的探索,文中若有不对的地方烦请指正。

总的来说,一个变量是不能被多次定义的。以及,C和C++在关于multiple definition/redefinition的处理上会略有不同,原因在于tentative definition。

1 Declaration & Definition

既然谈论的是multiple definition的问题,那么有必要再明确一下何为definition。用stackoverflow的例子作为C语言的说明:

extern int x;  // declares x, without defining it
extern int x = 42;  // not frequent, declares AND defines it
int x;  // at block scope, declares and defines x
int x = 42;  // at file scope, declares and defines x
int x;  // at file scope, declares and "tentatively" defines x

注意最后int x(at file scope)的例子,就是所谓的Tentative Definition

Prior to C90, implementations varied widely with regard to forward referencing identifiers with internal linkage (see §6.2.2). The C89 committee invented the concept of tentative definition to handle this situation. A tentative definition is a declaration that may or may not act as a definition: If an actual definition is found later in the translation unit, then the tentative definition just acts as a declaration. If not, then the tentative definition acts as an actual definition. For the sake of consistency, the same rules apply to identifiers with external linkage, although they‘re not strictly necessary.
—— C99 rationale section 6.9.2

而这种tentative definition在C++中是不允许的。在C++中,int x(at file scope)毫无疑问是一个definition(同时也是declaration)。

2 C的例子

先上代码:

/*****main.h*****/
int integVal;
int increase(void);

/*****increase.c*****/
#include "main.h"
int increase(void)
{
	integVal++;	
	return 0;
}

/*****main.c*****/
#include <stdio.h>
#include "main.h"
int main()
{
	integVal = 1;
	increase();
	printf("integVal %d\r\n", integVal);
	return 0;
}

用gcc编译之后,分别用nm命令看一下main.o和increase.o的符号表:

/*****main.o*****/
         U increase
00000004 C integVal
00000000 T main
         U printf‘
         
/*****increase.o*****/
00000000 T increase
00000004 C integVal

两个文件中integVal的符号类型都是C,which means——

The symbol is common. Common symbols are uninitialized data. When linking, multiple common symbols may appear with the same name. If the symbol is defined anywhere, the common symbols are treated as undefined references.

经过链接之后,多个同名的common symbols会整合在一起,那么看一下最终生成的目标文件中,integVal是怎样的形态:

0804a020 B integVal

Aha,最终分配在了BSS段中。

3 C++的例子

同样的代码无需更改,这次改用g++进行编译(会将.c文件也作为C++处理),multiple deinition的问题来了:

main.o:(.bss+0x0): multiple definition of `integVal‘
increase.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status

原因之前已经提过,int integVal这样的写法在C++就是确凿无疑的definition。
符号表是这样的:

/*****main.o*****/
00000000 B integVal
00000000 T main
         U printf
         U _Z8increasev
/*****increase.o*****/
00000000 B integVal
00000000 T _Z8increasev

4 解决方案

只在一个源文件中定义全局变量,在其他文件中利用extern这个关键字来声明全局变量。
如果想将全局变量“声明/定义”在头文件中,可参考ucos的做法:
在ucos_ii.h文件中定义了OS_EXT宏,如果文件中也同时定义了OS_GLOBALS宏的话,那么OS_EXT宏的定义就为空,否则为extern;在头文件中声明变量的时候用OS_EXT做限定,eg.OS_EXT INT32U OSCtxSwCtr
这样就可以只在某个特定的源文件(ucos_ii.c)中定义OS_GLOBALS宏再包含ucos_ii.h,而除此之外的源文件中不定义OS_GLOBALS再包含ucos_ii.h的时候默认全局变量就是extern的了。

/*****ucos_ii.h*****/
#ifdef   OS_GLOBALS
#define  OS_EXT
#else
#define  OS_EXT  extern
#endif
OS_EXT INT32U OSCtxSwCtr; //GLOBAL VARIABLE
//...

/*****ucos_ii.c*****/
#define  OS_GLOBALS
#include <ucos_ii.h>
//...

于是将之前的代码更改为如下,g++也顺利编过:

/*****main.h*****/
#ifdef DATA_GLOBALS
#define DATA_EXT
#else
#define DATA_EXT extern
#endif
DATA_EXT int integVal;
int increase(void);

/*****main.c*****/
#include <stdio.h>
#define DATA_GLOBALS
#include "main.h"
int main()
{
	integVal = 1;
	increase();
	printf("integVal %d\r\n", integVal);
	return 0;
}

再看一下main.o和increase.o的符号表,在increase.o中integVal的符号类型为undefined未定义,自然不会冲突了。

/*****main.o*****/
00000000 B integVal
00000000 T main
         U printf
         U _Z8increasev
/*****increase.o*****/
         U integVal
00000000 T _Z8increasev

5 再谈Tentative definition

推荐一篇不错的文章What Are "Tentative" Symbols?,比较全面地解释了tentative symbols的由来。其中谈到C语言的tentative definition是为了兼容旧的代码,实际上是一种非常不好的用法,现在不应该再这样使用,并且对于全局变量的使用提出了以下建议:
1.给出全局变量的初始值,即使是0,这样可以确保不会变成tentative definition。
2.定义了全局变量的源文件应给出相应的头文件,在头文件中用extern声明这个全局变量,并且源文件也包含这个头文件。
3.其他源文件在使用全局变量时,应该通过包含相应的头文件来使用,而不是在源文件中再通过extern来声明。

Tentative definition可能导致的问题可参考CSAPP 7.6.1节。在使用gcc编译的时候,还可以使用-fno-common选项(在GCC 10中已经默认使用该选项了),这样在全局变量出现多重定义时就会给出ERROR!

扩展阅读

[1]《程序员的自我修养——链接、装载与库》,3.5节符号,4.3节COMMON块。
[2]Stackoverflow-How do I use extern to share variables between source files,最高赞回答对于全局变量的使用给出了仔细的例子和说明。

相关推荐