extern int global_variable; /* Declaration of the variable */
file1.c
#include "file3.h" /* Declaration made available here */
#include "prog1.h" /* Function declarations */
/* Variable defined here */
int global_variable = 37; /* Definition checked against declaration */
int increment(void) { return global_variable++; }
由于并不总是支持此技术,因此最好避免使用它,尤其是在代码需要可移植的情况下 . 使用这种技术,您最终也可能会遇到无意的类型惩罚 . 如果其中一个文件声明 i 为 double 而不是 int ,C 's type-unsafe linkers probably would not spot the mismatch. If you'在具有64位 int 和 double 的机器上,则您甚至不会收到警告;在具有32位 int 和64位 double 的计算机上,您可能会收到有关不同大小的警告 - 链接器将使用最大的大小,正如Fortran程序将占用任何公共块的最大大小一样 .
正如我在这里的评论中所指出的,并且在我对类似question的回答中所述,对全局变量使用多个定义会导致未定义的行为(J.2;§6.9),这是标准的's way of saying 458913 . One of the things that can happen is that the program behaves as you expect; and J.5.11 says, approximately, 458914 . But a program that relies on multiple definitions of an extern variable — with or without the explicit ' extern'关键字 - 不是严格的符合计划,并不保证在任何地方工作 . 同等地:它包含一个可能会或可能不会显示自身的错误 .
#define DEFINE_VARIABLES
#include "file3a.h" /* Variable defined - but not initialized */
#include "prog3.h"
int increment(void) { return global_variable++; }
/*
** This header must not contain header guards (like <assert.h> must not).
** Each time it is invoked, it redefines the macros EXTERN, INITIALIZE
** based on whether macro DEFINE_VARIABLES is currently defined.
*/
#undef EXTERN
#undef INITIALIZE
#ifdef DEFINE_VARIABLES
#define EXTERN /* nothing */
#define INITIALIZE(...) = __VA_ARGS__
#else
#define EXTERN extern
#define INITIALIZE(...) /* nothing */
#endif /* DEFINE_VARIABLES */
file1c.h
#ifndef FILE1C_H_INCLUDED
#define FILE1C_H_INCLUDED
struct oddball
{
int a;
int b;
};
extern void use_them(void);
extern int increment(void);
extern int oddball_value(void);
#endif /* FILE1C_H_INCLUDED */
#define DEFINE_VARIABLES
#include "file2c.h" /* Variables now defined and initialized */
int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }
/*
** This header must not contain header guards (like <assert.h> must not).
** Each time it is included, the macro HEADER_DEFINING_VARIABLES should
** be defined with the name (in quotes - or possibly angle brackets) of
** the header to be included that defines variables when the macro
** DEFINE_VARIABLES is defined. See also: external.h (which uses
** DEFINE_VARIABLES and defines macros EXTERN and INITIALIZE
** appropriately).
**
** #define HEADER_DEFINING_VARIABLES "file2c.h"
** #include "externdef.h"
*/
#if defined(HEADER_DEFINING_VARIABLES)
#define DEFINE_VARIABLES
#include HEADER_DEFINING_VARIABLES
#undef DEFINE_VARIABLES
#undef HEADER_DEFINING_VARIABLES
#endif /* HEADER_DEFINING_VARIABLES */
在C中,文件中的变量表示example.c具有本地范围 . 编译器期望变量将在同一个文件example.c中定义它,并且当它找不到相同时,它会抛出一个错误 . 另一方面,一个函数默认具有全局范围 . 因此,您不必明确提及编译器"look dude...you might find the definition of this function here" . 对于包含包含其声明的文件的函数就足够了 . (实际上称为头文件的文件) . 例如,考虑以下2个文件: example.c
#include<stdio.h>
extern int a;
main(){
printf("The value of a is <%d>\n",a);
}
example1.c
int a = 5;
现在,当您将两个文件一起编译时,请使用以下命令:
步骤1)cc -o ex example.c example1.c step 2)./ ex
您将获得以下输出:a的值为<5>
1542
GCC ELF Linux implementation
main.c :
#include <stdio.h>
int not_extern_int = 1;
extern int extern_int;
void main() {
printf("%d\n", not_extern_int);
printf("%d\n", extern_int);
}
编译和反编译:
gcc -c main.c
readelf -s main.o
输出包含:
Num: Value Size Type Bind Vis Ndx Name
9: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 not_extern_int
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND extern_int
#ifdef MAIN_C
#define GLOBAL
/* #warning COMPILING MAIN.C */
#else
#define GLOBAL extern
#endif
GLOBAL unsigned char testing_mode; // example var used in several C files
15 回答
我喜欢将外部变量视为您对编译器的承诺 .
遇到extern时,编译器只能找到它的类型,而不是它“存在”的位置,因此它无法解析引用 .
你告诉它,“相信我 . 在链接时,这个引用将是可解析的 . ”
使用
extern
仅在您正在构建的程序由链接在一起的多个源文件组成时才有意义,其中一些定义的变量(例如,源文件file1.c
中)需要在其他源文件中引用,例如file2.c
.重要的是understand the difference between defining a variable and declaring a variable:
当编译器被告知变量存在时(和它的类型),变量是 declared ;它不会在该点为变量分配存储空间 .
当编译器为变量分配存储时,变量为 defined .
您可以多次声明变量(尽管一次就足够了);您只能在给定范围内定义一次 . 变量定义也是一个声明,但并非所有变量声明都是定义 .
声明和定义全局变量的最佳方式
声明和定义全局变量的干净,可靠的方法是使用头文件来包含变量的
extern
声明 .标头包含在定义变量的一个源文件和引用该变量的所有源文件中 . 对于每个程序,一个源文件(和一个源文件)定义该变量 . 同样,一个头文件(只有一个头文件)应声明该变量 . 头文件至关重要;它可以在独立的TU(翻译单元 - 思考源文件)之间进行交叉检查,并确保一致性 .
虽然还有其他方法,但这种方法简单可靠 . 它由
file3.h
,file1.c
和file2.c
证明:file3.h
file1.c
file2.c
这是声明和定义全局变量的最佳方式 .
接下来的两个文件完成了
prog1
的源代码:显示的完整程序使用函数,因此函数声明已经悄悄进入.C99和C11都要求在使用之前声明或定义函数(而C90没有,出于好的理由) . 我在标头中的函数声明前面使用关键字
extern
来保持一致性 - 以匹配标头中变量声明前面的extern
. 很多人不喜欢在函数声明前使用extern
;编译器至少在源文件中不一致 .prog1.h
prog1.c
prog1
使用prog1.c
,file1.c
,file2.c
,file3.h
和prog1.h
.文件
prog1.mk
只是prog1
的生成文件 . 它将适用于大约在千禧年之后产生的大多数版本的make
. 它与GNU Make没有特别的联系 .prog1.mk
指南
规则只能由专家打破,并且只有充分的理由:
头文件仅包含
extern
变量声明 - 从不static
或非限定变量定义 .对于任何给定的变量,只有一个头文件声明它(SPOT - 单点真相) .
源文件永远不会包含
extern
变量声明 - 源文件始终包含声明它们的(唯一)标头 .对于任何给定变量,只有一个源文件定义变量,最好也初始化它 . (虽然不需要显式地初始化为零,但它没有任何损害并且可以做一些好事,因为在程序中只能有一个特定全局变量的初始化定义) .
定义变量的源文件还包括标头,以确保定义和声明是一致的 .
函数永远不需要使用
extern
声明变量 .尽可能避免全局变量 - 改为使用函数 .
这个答案的源代码和文本可以在src / so-0143-3204子目录中的GitHub上的SOQ(Stack Overflow Questions)存储库中找到 .
如果你不是一个经验丰富的C程序员,你可以(也许应该)在这里停止阅读 .
不是定义全局变量的好方法
对于一些(实际上很多)C编译器,你可以放弃所谓的变量的“通用”定义 . 这里的“Common”是指Fortran中使用的一种技术,用于在源文件之间共享变量,使用(可能命名的)COMMON块 . 这里发生的是,许多文件中的每一个都提供了一个暂定的定义变量 . 只要不超过一个文件提供初始化定义,那么各种文件最终会共享变量的常见单一定义:
file10.c
file11.c
file12.c
这种技术不符合C标准的字母和“一个定义规则” - 它是官方未定义的行为:
但是,C标准也将其列为资料性附件J中的一个Common extensions .
由于并不总是支持此技术,因此最好避免使用它,尤其是在代码需要可移植的情况下 . 使用这种技术,您最终也可能会遇到无意的类型惩罚 . 如果其中一个文件声明
i
为double
而不是int
,C 's type-unsafe linkers probably would not spot the mismatch. If you'在具有64位int
和double
的机器上,则您甚至不会收到警告;在具有32位int
和64位double
的计算机上,您可能会收到有关不同大小的警告 - 链接器将使用最大的大小,正如Fortran程序将占用任何公共块的最大大小一样 .接下来的两个文件完成
prog2
的源:prog2.h
prog2.c
prog2
使用prog2.c
,file10.c
,file11.c
,file12.c
,prog2.h
.警告
正如我在这里的评论中所指出的,并且在我对类似question的回答中所述,对全局变量使用多个定义会导致未定义的行为(J.2;§6.9),这是标准的's way of saying 458913 . One of the things that can happen is that the program behaves as you expect; and J.5.11 says, approximately, 458914 . But a program that relies on multiple definitions of an extern variable — with or without the explicit ' extern'关键字 - 不是严格的符合计划,并不保证在任何地方工作 . 同等地:它包含一个可能会或可能不会显示自身的错误 .
违反准则
当然,有很多方法可以打破这些指导方针 . 偶尔,有可能有理由违反指导方针,但这种情况极不寻常 .
faulty_header.h
注1:如果 Headers 定义了不带
extern
关键字的变量,那么包含 Headers 的每个文件都会创建变量的暂定定义 . 如前所述,这通常会起作用,但C标准并不能保证它能够正常工作 .broken_header.h
注2:如果标头定义并初始化变量,则给定程序中只有一个源文件可以使用标头 . 由于 Headers 主要用于共享信息,因此创建一个只能使用一次的信息有点愚蠢 .
seldom_correct.h
注3:如果标头定义了一个静态变量(有或没有初始化),那么每个源文件最终都有自己的私有版本的'global'变量 .
例如,如果变量实际上是一个复杂的数组,则可能导致代码的极端重复 . 偶尔,它可以是实现某种效果的合理方式,但这是非常不寻常的 .
摘要
使用我首先展示的 Headers 技术 . 它可靠,无处不在 . 请特别注意,声明
global_variable
的标头包含在使用它的每个文件中 - 包括定义它的文件 . 这确保了一切都是自洽的 .声明和定义功能也会出现类似的问题 - 适用类似的规则 . 但问题是关于变量的具体问题,所以我只保留了变量的答案 .
原始答案结束
如果您不是经验丰富的C程序员,您可能应该停止阅读 .
延迟重大增加
避免代码重复
有时(并且合法地)提出关于这里描述的' Headers 中的声明,源中的定义'机制的一个问题是有两个文件要保持同步 - Headers 和源 . 这通常会随后观察到可以使用宏来使头部具有双重功能 - 通常声明变量,但是在包含标头之前设置特定的宏时,它会定义变量 .
另一个问题可能是需要在许多“主程序”中定义变量 . 这通常是一个虚假的问题;您可以简单地引入C源文件来定义变量并链接用每个程序生成的目标文件 .
一个典型的方案就是这样,使用
file3.h
中说明的原始全局变量:file3a.h
file1a.c
file2a.c
接下来的两个文件完成了
prog3
的源代码:prog3.h
prog3.c
prog3
使用prog3.c
,file1a.c
,file2a.c
,file3a.h
,prog3.h
.变量初始化
所示的该方案的问题在于它不提供全局变量的初始化 . 使用C99或C11以及宏的可变参数列表,您可以定义一个宏来支持初始化 . (使用C89并且不支持宏中的变量参数列表,没有简单的方法来处理任意长的初始化器 . )
file3b.h
反转#if和#else块的内容,修复Denis Kniazhev识别的bug
file1b.c
file2b.c
很明显,古怪结构的代码并不是你通常写的,但它说明了这一点 . 第二次调用
INITIALIZER
的第一个参数是{ 41
,其余参数(本例中为单数)是43 }
. 如果没有C99或对宏的可变参数列表的类似支持,那么需要包含逗号的初始化器非常有问题 .每个Denis Kniazhev都包含正确的头文件3b.h(而不是fileba.h)
接下来的两个文件完成
prog4
的源:prog4.h
prog4.c
prog4
使用prog4.c
,file1b.c
,file2b.c
,prog4.h
,file3b.h
.Header Guards
应保护任何标头不被重新包含,因此类型定义(枚举,结构或联合类型或一般的typedef)不会导致问题 . 标准技术是将 Headers 的主体包裹在 Headers 保护中,例如:
Headers 可能间接包含两次 . 例如,如果
file4b.h
包含file3b.h
用于未显示的类型定义,并且file1b.c
需要同时使用 Headersfile4b.h
和file3b.h
,那么您需要解决一些棘手的问题 . 显然,您可以修改 Headers 列表以仅包含file4b.h
. 但是,您可能不了解内部依赖关系 - 理想情况下,代码应该继续工作 .此外,它开始变得棘手,因为您可能在包含
file3b.h
之前包含file4b.h
来生成定义,但file3b.h
上的正常标头保护会阻止标头被重新包含 .因此,您需要最多包含
file3b.h
的主体一次用于声明,最多只包含一次用于定义,但您可能需要在单个转换单元(TU - 源文件和它使用的标头的组合)中 .包含变量定义的多个包含
但是,它可以在不太合理的约束下完成 . 我们来介绍一组新的文件名:
external.h
用于EXTERN宏定义等 .file1c.h
定义类型(特别是struct oddball
,oddball_struct
的类型) .file2c.h
定义或声明全局变量 .file3c.c
定义了全局变量 .file4c.c
只使用全局变量 .file5c.c
表明您可以声明然后定义全局变量 .file6c.c
表明您可以定义然后(尝试)声明全局变量 .在这些示例中,
file5c.c
和file6c.c
多次直接包含 Headersfile2c.h
,但这是显示该机制有效的最简单方法 . 这意味着如果 Headers 间接包含两次,那么它也是安全的 .这项工作的限制是:
定义或声明全局变量的标头本身可能不定义任何类型 .
在包含应定义变量的标头之前,立即定义宏DEFINE_VARIABLES .
定义或声明变量的 Headers 具有程式化的内容 .
external.h
file1c.h
file2c.h
file3c.c
file4c.c
file5c.c
file6c.c
下一个源文件为
prog5
,prog6
和prog7
完成源(提供主程序):prog5.c
prog5
使用prog5.c
,file3c.c
,file4c.c
,file1c.h
,file2c.h
,external.h
.prog6
使用prog5.c
,file5c.c
,file4c.c
,file1c.h
,file2c.h
,external.h
.prog7
使用prog5.c
,file6c.c
,file4c.c
,file1c.h
,file2c.h
,external.h
.该方案避免了大多数问题 . 如果定义变量的头(例如
file2c.h
)包含在定义变量的另一个头(例如file7c.h
)中,则只会遇到问题 . 除了"don't do it"之外,还有一个简单的方法 .您可以通过将
file2c.h
修改为file2d.h
来部分解决此问题:file2d.h
如果 Headers 包含
#undef DEFINE_VARIABLES
,则问题变为'?'如果从 Headers 中省略它并用#define
和#undef
包装任何定义调用:在源代码中(所以 Headers 永远不会改变
DEFINE_VARIABLES
的值),那么你应该是干净的 . 只记得编写额外的行是一件麻烦事 . 另一种选择可能是:externdef.h
这有点令人费解,但似乎是安全的(使用
file2d.h
,file2d.h
中没有#undef DEFINE_VARIABLES
) .file7c.c
file8c.h
file8c.c
接下来的两个文件完成了
prog8
和prog9
的来源:prog8.c
file9c.c
prog8
使用prog8.c
,file7c.c
,file9c.c
.prog9
使用prog8.c
,file8c.c
,file9c.c
.但是,这些问题在实践中相对不太可能发生,特别是如果您采用标准建议
避免全局变量
这个博览会是否遗漏了什么?
忏悔:这里概述的'avoiding duplicated code'方案是因为问题影响了我工作的一些代码(但不拥有),并且是对答案第一部分概述的方案的一个琐碎关注 . 但是,原始方案只留下两个地方进行修改以保持变量定义和声明同步,这是将exernal变量声明分散在整个代码库中的一大步(当总共有数千个文件时真正重要) . 但是,名称为
fileNc.[ch]
(加上external.h
和externdef.h
)的文件中的代码表明它可以正常工作 . 显然,创建一个 Headers 生成器脚本来为定义和声明头文件的变量提供标准化模板并不困难 .注意:这些玩具程序只有几乎没有足够的代码来使它们略微有趣 . 在示例中可以删除重复,但不是为了简化教学解释 . (例如:
prog5.c
和prog8.c
之间的区别是包含的其中一个 Headers 的名称 . 可以重新组织代码,以便不重复main()
函数,但它隐藏的内容比它显示的要多 . )在C中,文件中的变量表示example.c具有本地范围 . 编译器期望变量将在同一个文件example.c中定义它,并且当它找不到相同时,它会抛出一个错误 . 另一方面,一个函数默认具有全局范围 . 因此,您不必明确提及编译器"look dude...you might find the definition of this function here" . 对于包含包含其声明的文件的函数就足够了 . (实际上称为头文件的文件) . 例如,考虑以下2个文件:
example.c
example1.c
现在,当您将两个文件一起编译时,请使用以下命令:
步骤1)cc -o ex example.c example1.c step 2)./ ex
您将获得以下输出:a的值为<5>
GCC ELF Linux implementation
main.c
:编译和反编译:
输出包含:
System V ABI Update ELF spec "Symbol Table"章解释说:
这基本上是C标准给
extern
变量的行为 .从现在开始,链接器的作用是创建最终程序,但
extern
信息已经从源代码中提取到目标文件中 .在GCC 4.8上测试 .
extern告诉编译器要相信这个变量的内存是在别处声明的,所以它不会尝试分配/检查内存 .
因此,您可以编译引用extern的文件,但如果未在某处声明该内存,则无法链接 .
对全局变量和库有用,但很危险,因为链接器不进行类型检查 .
extern keyword is used with the variable for its identification as a global variable.
extern
仅表示变量在别处定义(例如,在另一个文件中) .Extern是用于声明变量本身位于另一个转换单元中的关键字 .
因此,您可以决定在翻译单元中使用变量,然后从另一个变量中访问它,然后在第二个变量单元中将其声明为extern,符号将由链接器解析 .
如果你没有将它声明为extern,你将获得两个名为相同但根本不相关的变量,以及变量的多个定义的错误 .
使用xc8时,你必须要小心,尽可能将变量声明为每个文件中的相同类型,错误地,在一个文件中声明
int
,在另一个文件中声明char
. 这可能导致变量腐败 .这个问题在15年前的微芯片论坛中得到了很好的解决/ *见"http:www.htsoft.com" / / "forum/all/showflat.php/Cat/0/Number/18766/an/0/page/0#18766"
但这个链接似乎不再起作用......
所以我会尽快解释一下;制作一个名为global.h的文件 .
在其中声明如下
现在在main.c文件中
这意味着在main.c中,变量将被声明为
unsigned char
.现在在其他文件中只包含global.h会将它声明为该文件的extern .
但它将被正确声明为
unsigned char
.旧的论坛帖子可能更清楚地解释了这一点 . 但是,当使用允许您在一个文件中声明变量然后在另一个文件中将其声明为另一个类型的编译器时,这是一个真正的潜力
gotcha
. 与此相关的问题是,如果你说在另一个文件中声明testing_mode为int,它会认为它是16位var并覆盖ram的其他部分,可能会破坏另一个变量 . 很难调试!对extern的正确解释是你告诉编译器一些东西 . 您告诉编译器,尽管现在不存在,但是声明的变量将以某种方式由链接器找到(通常在另一个对象(文件)中) . 无论你是否有一些外部声明,链接器将是幸运的人找到所有东西并把它放在一起 .
首先,
extern
关键字不用于定义变量;而是用于声明变量 . 我可以说extern
是一个存储类,而不是数据类型 .extern
用于让其他C文件或外部组件知道此变量已在某处定义 . 示例:如果要构建库,则无需在库本身的某处强制定义全局变量 . 该库将直接编译,但在链接文件时,它会检查定义 .extern
变量是在另一个翻译单元中定义的变量的声明(由于sbi用于校正) . 这意味着变量的存储空间分配在另一个文件中 .假设你有两个
.c
-filestest1.c
和test2.c
. 如果在test1.c
中定义全局变量int test1_var;
并且您想在test2.c
中访问此变量,则必须在test2.c
中使用extern int test1_var;
.完整样本:
extern
允许程序的一个模块访问程序的另一个模块中声明的全局变量或函数 . 您通常在头文件中声明了外部变量 .如果您不希望程序访问您的变量或函数,则使用
static
,它告诉编译器此变量或函数不能在此模块之外使用 .添加
extern
会将变量定义转换为变量声明 . 有关声明和定义之间的区别,请参见this thread .使用
extern
,因此一个first.c
文件可以完全访问另一个second.c
文件中的全局参数 .extern
可以在first.c
文件中声明,也可以在first.c
包含的任何头文件中声明 .