计算机系统篇之链接(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) 为什么要遵循原则 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将未初始化的全局变量放到.bsssection 中,从而不允许链接器合并临时定义,可以理解为强符号。对于 C++ 程序,无论是 gcc 还是 g++ 都不会引发上述典型错误。因为对于 C++ 程序,无论是用 gcc 编译还是用 g++编译,实际调用的编译器都是cc1plus,而编译器cc1plus的默认行为是将未初始化的全局变量放到.bsssection 中。


下一篇:计算机系统篇之链接(12):Chapter 7 Linking 章节习题与解答

上一篇:计算机系统篇之链接(10):.bss、.data 和 .rodata sections 之间的区别

首页