首页 > 技术文章 > 能不能在头文件中定义全局变量?

jakelin 2021-01-25 14:33 原文

编译器驱动程序

大多数编译系统提供编译器驱动程序(compiler driver),它代表用户在需要时调用语言预处理器编译器汇编器、和链接器

我们所常说的 “编译生成可执行文件” 实际包括以下过程:

  1. 预处理器 (某些编译系统,预处理器被集成到 编译器 中)

    cpp [other arguments] main.c /tmp/main.i
    

    处理预处理指令,生成中间文件,所有的预处理器命令都是以井号(#)开头。主要任务包括:

    • 删除注释;
    • 插入被 #include 指令所包含的的文件内容;
    • 定义和替换由#define指令定义的符号;
    • 确定代码的部分内容是否应该根据一些条件编译指令进行编译;
  2. 编译器

    cc1 /tmp/main.i -Og [other arguments] -o /tmp/main.s
    

    将预处理后的中间文件翻译成一个ASCII汇编语言文件

  3. 汇编器

    as [other arguments] -o /tmp/main.o /tmp/main.s
    

    将ASCII汇编语言文件翻译成一个可重定位目标文件

  4. 链接器

    ld -o prog [system object files and args] /tmp/main.o /tmp/sum.o
    

    将一个或多个可重定位目标文件,以及必要的系统目标文件组合起来,创建一个可执行文件

编译生成可执行文件过程.png

非静态全局变量

先个下结论:可以,但非常非常非常不建议!!

  • 若头文件仅被一个源文件使用到,可以正常生成可执行文件。
  • 若头文件被多个源文件包含,可正常执行完 cpp、cc1、as,但在链接(ld)时便会报错(重复定义)。

由上面的图可以分析出,源程序代码在生成可执行文件的过程中,前三步均可看做独立完成的,仅在最后一步将多个源文件生成的目标文件链接起来。

如下代码:

a.cpp 、 b.cpp 在经过预处理器、编译器、汇编器时均认为是没有定义变量 A 的,于是都有变量定义。在链接时,便出现了二义性(重复定义)。

[root@localhost val]# g++ a.cpp b.cpp

/tmp/ccVzRqVG.o:(.bss+0x0): multiple definition of `A'

/tmp/ccjI4zgp.o:(.bss+0x0): first defined here

collect2: error: ld returned 1 exit status

// a.h
#ifndef A_H
#define A_H
int A;
void funcA();
#endif
// a.cpp
#include<iostream>
#include "a.h"
                                              
void funcA(){
	A = 10;
    std::cout << "a.cpp : " << A << std::endl;
}
// b.cpp
#include<iostream>
#include "a.h"

int main(){
	funcA();
	A =20;
    std::cout << "b.cpp : " << A << std::endl;
    return 0;
}

静态全局变量

上面的代码如若将变量 A定义为 static,编译执行没有问题。但是,静态变量的作用域仅在 ”当前源文件“ ,即两处的变量 A 不是同一个变量,是不同的文件作用域内的静态变量。

“静态全局变量” 这个称谓其实就有点怪异,静态变量在文件作用域内就是全局的,且仅在文件作用域内。

使用建议

全局变量在某一个源文件中定义,其余源文件若要使用,将外部声明 extern 写在头文件中,源文件包含这个头文件。如下:

// a.cpp
int nums;  // 全局变量定义
// out.h
extern int nums;	//外部变量声明
// b.cpp
#include "out.h" // 相当于声明了外部变量
void set_nums(int val){
	nums = val;
}

g++ a.cpp b.cpp

更加深入-全局变量

如果在 a.h 中变量定义时,定义为 “弱定义”,那么是能达到预期的目标。

int  A __attribute__((weak));

Linux gcc

注意:仅在 gcc 下正确,换做 g++ 同样报错(重复定义)。

在编译时,编译器向汇编器输出每个全局符号,或者是强(strong)或者是弱(weak),而编译器把这个信息隐含地编码在可重定位目标文件的符号表里。函数已初始化的全局变量强(strong)符号未初始化的全局变量弱(weak)符号

根据弱符号的定义,Linux链接器使用下面的规则来处理多重定义的符号名:

  • 规则1:不允许有多个同名的强符号。
  • 规则2:如果有一个强符号和多个弱符号同名,那么选择强符号。
  • 规则3:若干有多个弱符号同名,那么从这些弱符号中任意选择一个。
// bar.c
#include<stdio.h>
int x;
void f(){
//      std::cout << "begin f() x = " << x << std::endl;      
        printf("begin f() x = %d\n", x);
        x = 654321;
}
// foo.c
#include<stdio.h>
int x  = 12345; // 已初始化 强符号
void f(); 
int main(){
        f();
//      std::cout << "after f() x = " << x << std::endl;
        printf("after f() x = %d\n", x);
        return 0;
}
[root@localhost 7]# gcc foo.c bar.c 
[root@localhost 7]# ./a.out 
begin f() x = 12345
after f() x = 654321

推荐阅读