计算机系统篇之链接(11):为什么要避免在 C/C++ 中使用全局变量
Author: stormQ
Created: Friday, 08. May 2020 10:20PM
Last Modified: Thursday, 05. November 2020 11:03PM
本文从符合解析的角度解释了 C/C++ 中避免使用全局变量的原因,并介绍了如何防止误用全局变量的手段。
避免使用全局变量的原因之一是全局变量的不正确使用容易导致难以发现的程序错误。
不正确使用全局变量会引发的典型错误:
1)全局变量的值被同名的全局变量修改
$ cat test1.c
int g_val_1 = 0;
int g_val_2;
void func()
{
g_val_1 = 1;
g_val_2 = 2;
}
$ cat main.c
#include <stdio.h>
int g_val_1;
int g_val_2 = 3;
void func();
int main()
{
func();
printf("g_val_1=%d, g_val_2=%d\n", g_val_1, g_val_2);
return 0;
}
$ gcc -c test1.c -o test1.o
$ gcc -c main.c -o main.o
$ gcc -o main test1.o main.o
$ ./main
g_val_1=1, g_val_2=2
main.c 中定义的全局变量 g_val_2 的值被 test1.c 中定义的 func() 函数由 3 改为 2 了。
2)全局变量的值被不同名的全局变量修改
$ cat test2.c
double g_val_1;
void func()
{
g_val_1 = 1.1;
}
$ cat main2.c
#include <stdio.h>
int g_val_0 = 1;
int g_val_1 = 2;
int g_val_2 = 3;
void func();
int main()
{
func();
printf("g_val_0=%d, g_val_1=%d, g_val_2=%d\n", g_val_0, g_val_1, g_val_2);
return 0;
}
$ gcc -o main2 test2.c main2.c
/usr/bin/x86_64-linux-gnu-ld: Warning: alignment 4 of symbol `g_val_1' in /tmp/ccIc3noO.o is smaller than 8 in /tmp/ccXjyx8c.o
/usr/bin/x86_64-linux-gnu-ld: Warning: size of symbol `g_val_1' changed from 8 in /tmp/ccXjyx8c.o to 4 in /tmp/ccIc3noO.o
$ ./main2
g_val_0=1, g_val_1=-1717986918, g_val_2=1072798105
main2.c 中定义的全局变量 g_val_2 的值被意外地修改了。
要想正确地使用全局变量,需要遵循以下原则:
原则 1:尽量使用静态局部变量而非全局变量。
原则 2:每个定义的全局变量必须显式地初始化。
原则 3:每个引用的全局变量必须用extern
关键字声明。
原则 4:利用编译器检查全局变量的不正确使用。
1) 为什么要遵循原则 1
遵循原则 1 的理由很简单,不用全局变量就不会产生全局变量可能引发的错误。
如果一个目标模块中多个函数都会引用同一个变量,并且其他目标模块不会引用该变量,那么将该变量声明为静态局部变量。如果一个变量除了定义它的目标模块以外还会被其他目标模块引用,那么本原则就不适用了。
注:一个目标模块指的是一个源文件。
2) 为什么要遵循原则 2
遵循原则 2 的理由:编译器和链接器对未初始化的全局变量的处理规则会导致影响程序正确性的行为,但它们不会报任何错误。要想真正理解这一点,需要先理解符号解析
和gcc/g++ 处理 .c 和 .cpp 源文件时的区别
,可以参考作者的另一篇文章——计算机系统篇之链接(4):符号解析
。
3) 为什么要遵循原则 3
遵循原则 3 的理由:对于 C++ 程序(以 .cpp 或 .cc 结尾的源文件),对全局变量的引用如果不用extern
关键字修饰,链接器会直接报错。对于 C 程序(以 .c 结尾的源文件),对全局变量的引用如果不用extern
关键字修饰,虽然不会报错,但容易引发上述典型错误。因此,无论是 C 程序还是 C++ 程序,用extern
关键字修饰对全局变量的引用都是一个好习惯。
4) 为什么要遵循原则 4
遵循原则 4 的理由:对于 C 程序,如果使用 gcc 编译,容易引发上述典型错误。因为使用 gcc 编译 C 程序时,实际调用的编译器是c1
,而编译器c1
的默认行为是将未初始化的全局变量放到COMMON
块中,从而允许链接器合并临时定义,可以理解为弱符号,所以容易引发上述典型错误。因此,对于该情形,在编译时添加选项-fno-common
从而指示编译器c1
将未初始化的全局变量放到.bss
section 中,从而不允许链接器合并临时定义,可以理解为强符号。对于 C++ 程序,无论是 gcc 还是 g++ 都不会引发上述典型错误。因为对于 C++ 程序,无论是用 gcc 编译还是用 g++编译,实际调用的编译器都是cc1plus
,而编译器cc1plus
的默认行为是将未初始化的全局变量放到.bss
section 中。
下一篇:计算机系统篇之链接(12):Chapter 7 Linking 章节习题与解答