我知道 C 中的全局变量有时会有extern
关键字。什么是extern
变量?宣言是什么样的?它的范围是什么?
这与跨源文件共享变量有关,但这是如何工作的?我在哪里使用extern
?
当你正在构建的程序由链接在一起的多个源文件组成时,使用extern
只是相关的,例如,在源文件file1.c
定义的一些变量需要在其他源文件中引用,例如file2.c
。
了解定义变量和声明变量之间的区别非常重要:
您可以多次声明变量(尽管一次就足够了); 您只能在给定范围内定义一次。变量定义也是一个声明,但并非所有变量声明都是定义。
声明和定义全局变量的干净,可靠的方法是使用头文件来包含变量的extern
声明 。
标头包含在定义变量的一个源文件和引用该变量的所有源文件中。对于每个程序,一个源文件(和一个源文件)定义该变量。同样,一个头文件(只有一个头文件)应声明该变量。头文件至关重要; 它可以在独立的 TU(翻译单元 - 思考源文件)之间进行交叉检查,并确保一致性。
虽然还有其他方法,但这种方法简单可靠。它由file3.h
, file1.c
和file2.c
演示:
extern int global_variable; /* Declaration of the variable */
#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++; }
#include "file3.h"
#include "prog1.h"
#include <stdio.h>
void use_it(void)
{
printf("Global variable: %d\n", global_variable++);
}
这是声明和定义全局变量的最佳方式。
接下来的两个文件完成了prog1
的源代码:
显示的完整程序使用函数,因此函数声明已经悄悄进入. C99 和 C11 都要求在使用之前声明或定义函数(而 C90 没有,有充分的理由)。我在头文件中的函数声明前面使用关键字extern
来保持一致性 - 在头文件中匹配变量声明前面的extern
。很多人不喜欢在函数声明前使用extern
; 编译器并不关心 - 最终,只要你是一致的,我也不会,至少在源文件中。
extern void use_it(void);
extern int increment(void);
#include "file3.h"
#include "prog1.h"
#include <stdio.h>
int main(void)
{
use_it();
global_variable += 19;
use_it();
printf("Increment: %d\n", increment());
return 0;
}
prog1
使用prog1.c
, file1.c
, file2.c
, file3.h
和prog1.h
。 文件prog1.mk
只是prog1
的 makefile。它将与多数版本的工作, make
千年以来的绕转动产生的。它与 GNU Make 没有特别的联系。
# Minimal makefile for prog1
PROGRAM = prog1
FILES.c = prog1.c file1.c file2.c
FILES.h = prog1.h file3.h
FILES.o = ${FILES.c:.c=.o}
CC = gcc
SFLAGS = -std=c11
GFLAGS = -g
OFLAGS = -O3
WFLAG1 = -Wall
WFLAG2 = -Wextra
WFLAG3 = -Werror
WFLAG4 = -Wstrict-prototypes
WFLAG5 = -Wmissing-prototypes
WFLAGS = ${WFLAG1} ${WFLAG2} ${WFLAG3} ${WFLAG4} ${WFLAG5}
UFLAGS = # Set on command line only
CFLAGS = ${SFLAGS} ${GFLAGS} ${OFLAGS} ${WFLAGS} ${UFLAGS}
LDFLAGS =
LDLIBS =
all: ${PROGRAM}
${PROGRAM}: ${FILES.o}
${CC} -o $@ ${CFLAGS} ${FILES.o} ${LDFLAGS} ${LDLIBS}
prog1.o: ${FILES.h}
file1.o: ${FILES.h}
file2.o: ${FILES.h}
# If it exists, prog1.dSYM is a directory on macOS
DEBRIS = a.out core *~ *.dSYM
RM_FR = rm -fr
clean:
${RM_FR} ${FILES.o} ${PROGRAM} ${DEBRIS}
规则只能由专家打破,并且只有充分的理由:
extern
变量声明 - 从不static
或非限定变量定义。 extern
声明 - 源文件始终包含声明它们的(唯一)标头。 extern
声明变量。 这个答案的源代码和文本可以在src / so-0143-3204子目录中的 GitHub 上的SOQ (Stack Overflow Questions)存储库中找到。
如果你不是一位经验丰富的 C 程序员,你可以(也许应该)在这里停止阅读。
对于一些(实际上很多)C 编译器,你也可以放弃所谓的变量的 “通用” 定义。这里的 “Common” 是指 Fortran 中使用的一种技术,用于在源文件之间共享变量,使用(可能命名的)COMMON 块。这里发生的是,许多文件中的每一个都提供了变量的暂定定义。只要不超过一个文件提供初始化定义,那么各种文件最终会共享变量的常见单一定义:
#include "prog2.h"
int i; /* Do not do this in portable code */
void inc(void) { i++; }
#include "prog2.h"
int i; /* Do not do this in portable code */
void dec(void) { i--; }
#include "prog2.h"
#include <stdio.h>
int i = 9; /* Do not do this in portable code */
void put(void) { printf("i = %d\n", i); }
这种技术不符合 C 标准的字母和 “一个定义规则” - 它是官方未定义的行为:
使用具有外部链接的标识符,但是在程序中,不存在标识符的正好一个外部定义,或者不使用标识符,并且存在标识符的多个外部定义(6.9)。
外部定义是外部声明,它也是函数(内联定义除外)或对象的定义。如果在表达式中使用通过外部链接声明的标识符(除了作为
sizeof
或_Alignof
运算符的操作数的一部分,其结果是整数常量),则整个程序中的某个地方应该只有一个标识符的外部定义; 否则,不得超过一个。 161)161)因此,如果在表达式中未使用通过外部链接声明的标识符,则不需要外部定义。
但是,C 标准还在信息性附件 J 中列出了它作为通用扩展之一 。
对象的标识符可能有多个外部定义,有或没有明确使用关键字 extern; 如果定义不一致,或者初始化了多个,则行为未定义(6.9.2)。
由于并不总是支持此技术,因此最好避免使用它, 尤其是在代码需要可移植的情况下 。使用这种技术,您最终也可能会遇到无意的类型惩罚。如果其中一个文件将i
声明为double
而不是int
,则 C 的类型不安全链接器可能不会发现不匹配。如果你在 64 位int
和double
的机器上,你甚至都不会收到警告; 在具有 32 位int
和 64 位double
,您可能会收到有关不同大小的警告 - 链接器将使用最大的大小,正如 Fortran 程序将占用任何公共块的最大大小一样。
接下来的两个文件完成了prog2
的源prog2
:
extern void dec(void);
extern void put(void);
extern void inc(void);
#include "prog2.h"
#include <stdio.h>
int main(void)
{
inc();
put();
dec();
put();
dec();
put();
}
prog2
使用prog2.c
, file10.c
, file11.c
, file12.c
, prog2.h
。 正如我在这里的评论中所指出的,并且在我对类似问题的回答中所述,对全局变量使用多个定义会导致未定义的行为(J.2;§6.9),这是标准的说法 “任何事情都可能发生”。可能发生的事情之一是程序的行为与您期望的一样; 和 J.5.11 大致相同,“你可能比你应得的更幸运”。但是,一个依赖于外部变量的多个定义的程序 - 有或没有明确的'extern' 关键字 - 不是一个严格符合的程序,并不保证在任何地方都可以工作。同等地:它包含一个可能会或可能不会显示自身的错误。
当然,有很多方法可以打破这些指导方针。偶尔,可能有理由违反指导方针,但这种情况极不寻常。
int some_var; /* Do not do this in a header!!! */
注 1:如果标题定义了不带extern
关键字的变量,那么包含标题的每个文件都会创建变量的暂定定义。如前所述,这通常会起作用,但 C 标准并不能保证它能够正常工作。
int some_var = 13; /* Only one source file in a program can use this */
注 2:如果标题定义并初始化变量,则给定程序中只有一个源文件可以使用标题。由于标题主要用于共享信息,因此创建一个只能使用一次的信息有点愚蠢。
static int hidden_global = 3; /* Each source file gets its own copy */
注 3:如果标头定义了一个静态变量(有或没有初始化),那么每个源文件最终都有自己的私有版本的'global' 变量。
例如,如果变量实际上是一个复杂的数组,则可能导致代码的极端重复。偶尔,它可以是一种实现某种效果的明智方式,但这是非常不寻常的。
使用我首先展示的标题技术。它可靠,无处不在。特别要注意,声明global_variable
的标头包含在使用它的每个文件中 - 包括定义它的文件。这确保了一切都是自洽的。
声明和定义功能也会出现类似的问题 - 适用类似的规则。但问题是关于变量的具体问题,所以我只保留了变量的答案。
如果您不是经验丰富的 C 程序员,您可能应该停止阅读。
延迟重大增加
有时(并且合法地)提出关于这里描述的 '标题中的声明,源中的定义' 机制的一个问题是有两个文件要保持同步 - 标题和源。这通常会随后观察到可以使用宏以使头部具有双重任务 - 通常声明变量,但是当在包含头部之前设置特定宏时,它会定义变量。
另一个问题可能是变量需要在许多 “主程序” 中定义。这通常是一个虚假的问题; 您可以简单地引入 C 源文件来定义变量并链接使用每个程序生成的目标文件。
一个典型的方案就是这样,使用file3.h
说明的原始全局变量:
#ifdef DEFINE_VARIABLES
#define EXTERN /* nothing */
#else
#define EXTERN extern
#endif /* DEFINE_VARIABLES */
EXTERN int global_variable;
#define DEFINE_VARIABLES
#include "file3a.h" /* Variable defined - but not initialized */
#include "prog3.h"
int increment(void) { return global_variable++; }
#include "file3a.h"
#include "prog3.h"
#include <stdio.h>
void use_it(void)
{
printf("Global variable: %d\n", global_variable++);
}
接下来的两个文件完成了prog3
的源prog3
:
extern void use_it(void);
extern int increment(void);
#include "file3a.h"
#include "prog3.h"
#include <stdio.h>
int main(void)
{
use_it();
global_variable += 19;
use_it();
printf("Increment: %d\n", increment());
return 0;
}
prog3
使用prog3.c
, file1a.c
, file2a.c
, file3a.h
, prog3.h
。 所示的该方案的问题在于它不提供全局变量的初始化。使用 C99 或 C11 以及宏的可变参数列表,您可以定义一个宏来支持初始化。 (使用 C89 并且不支持宏中的变量参数列表,没有简单的方法来处理任意长的初始化器。)
#ifdef DEFINE_VARIABLES
#define EXTERN /* nothing */
#define INITIALIZER(...) = __VA_ARGS__
#else
#define EXTERN extern
#define INITIALIZER(...) /* nothing */
#endif /* DEFINE_VARIABLES */
EXTERN int global_variable INITIALIZER(37);
EXTERN struct { int a; int b; } oddball_struct INITIALIZER({ 41, 43 });
反转#if
和#else
块的内容,修复Denis Kniazhev识别的 bug
#define DEFINE_VARIABLES
#include "file3b.h" /* Variables now defined and initialized */
#include "prog4.h"
int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }
#include "file3b.h"
#include "prog4.h"
#include <stdio.h>
void use_them(void)
{
printf("Global variable: %d\n", global_variable++);
oddball_struct.a += global_variable;
oddball_struct.b -= global_variable / 2;
}
很明显,古怪结构的代码并不是你通常写的,但它说明了这一点。第二次调用INITIALIZER
的第一个参数是{ 41
,其余参数(本例中为单数)为43 }
。如果没有 C99 或对宏的变量参数列表的类似支持,那么需要包含逗号的初始化器非常有问题。
每个Denis Kniazhev file3b.h
包含正确的头文件file3b.h
(而不是fileba.h
)
接下来的两个文件完成了prog4
的源prog4
:
extern int increment(void);
extern int oddball_value(void);
extern void use_them(void);
#include "file3b.h"
#include "prog4.h"
#include <stdio.h>
int main(void)
{
use_them();
global_variable += 19;
use_them();
printf("Increment: %d\n", increment());
printf("Oddball: %d\n", oddball_value());
return 0;
}
prog4
使用prog4.c
, file1b.c
, file2b.c
, prog4.h
, file3b.h
。 应该保护任何标头不被重新包含,因此类型定义(枚举,结构或联合类型或通常的 typedef)不会导致问题。标准技术是将标题的主体包装在标题保护中,例如:
#ifndef FILE3B_H_INCLUDED
#define FILE3B_H_INCLUDED
...contents of header...
#endif /* FILE3B_H_INCLUDED */
标题可能间接包含两次。例如,如果file4b.h
包含file3b.h
用于未显示的类型定义,并且file1b.c
需要同时使用头文件file4b.h
和file3b.h
,那么您需要解决一些棘手的问题。显然,您可以修改标题列表以仅包含file4b.h
。但是,您可能不了解内部依赖关系 - 理想情况下,代码应该继续工作。
此外,它开始变得棘手,因为您可能在包含file4b.h
之前包含file3b.h
以生成定义,但file3b.h
上的正常标头保护会阻止标头被重新包含。
因此,您需要将file3b.h
的主体最多包含一次用于声明,最多只包含一次用于定义,但您可能需要在单个转换单元中(TU - 源文件和它使用的标头的组合) 。
但是,它可以在不太不合理的约束下完成。让我们介绍一组新的文件名:
external.h
用于 EXTERN 宏定义等。 file1c.h
用于定义类型(特别是struct oddball
, oddball_struct
的类型)。 file2c.h
用于定义或声明全局变量。 file3c.c
,定义全局变量。 file4c.c
, file4c.c
使用全局变量。 file5c.c
表明您可以声明然后定义全局变量。 file6c.c
表明您可以定义然后(尝试)声明全局变量。 在这些示例中, file5c.c
和file6c.c
直接包含头文件file2c.h
,但这是显示该机制有效的最简单方法。这意味着如果标题间接包含两次,那么它也是安全的。
这项工作的限制是:
/*
** 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 */
#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 */
/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE2C_H_DEFINITIONS)
#undef FILE2C_H_INCLUDED
#endif
#ifndef FILE2C_H_INCLUDED
#define FILE2C_H_INCLUDED
#include "external.h" /* Support macros EXTERN, INITIALIZE */
#include "file1c.h" /* Type definition for struct oddball */
#if !defined(DEFINE_VARIABLES) || !defined(FILE2C_H_DEFINITIONS)
/* Global variable declarations / definitions */
EXTERN int global_variable INITIALIZE(37);
EXTERN struct oddball oddball_struct INITIALIZE({ 41, 43 });
#endif /* !DEFINE_VARIABLES || !FILE2C_H_DEFINITIONS */
/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE2C_H_DEFINITIONS
#endif /* DEFINE_VARIABLES */
#endif /* FILE2C_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; }
#include "file2c.h"
#include <stdio.h>
void use_them(void)
{
printf("Global variable: %d\n", global_variable++);
oddball_struct.a += global_variable;
oddball_struct.b -= global_variable / 2;
}
#include "file2c.h" /* Declare variables */
#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; }
#define DEFINE_VARIABLES
#include "file2c.h" /* Variables now defined and initialized */
#include "file2c.h" /* Declare variables */
int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }
下一个源文件为prog5
, prog6
和prog7
完成源(提供主程序):
#include "file2c.h"
#include <stdio.h>
int main(void)
{
use_them();
global_variable += 19;
use_them();
printf("Increment: %d\n", increment());
printf("Oddball: %d\n", oddball_value());
return 0;
}
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
)中,则只会遇到问题。除了 “不要这样做” 之外,没有一种简单的方法。
您可以通过将file2c.h
修改为file2d.h
来部分解决此问题:
/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE2D_H_DEFINITIONS)
#undef FILE2D_H_INCLUDED
#endif
#ifndef FILE2D_H_INCLUDED
#define FILE2D_H_INCLUDED
#include "external.h" /* Support macros EXTERN, INITIALIZE */
#include "file1c.h" /* Type definition for struct oddball */
#if !defined(DEFINE_VARIABLES) || !defined(FILE2D_H_DEFINITIONS)
/* Global variable declarations / definitions */
EXTERN int global_variable INITIALIZE(37);
EXTERN struct oddball oddball_struct INITIALIZE({ 41, 43 });
#endif /* !DEFINE_VARIABLES || !FILE2D_H_DEFINITIONS */
/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE2D_H_DEFINITIONS
#undef DEFINE_VARIABLES
#endif /* DEFINE_VARIABLES */
#endif /* FILE2D_H_INCLUDED */
如果标题包含#undef DEFINE_VARIABLES
,问题就变成 ' 如果从标题中省略它并使用#define
和#undef
包装任何定义调用:
#define DEFINE_VARIABLES
#include "file2c.h"
#undef DEFINE_VARIABLES
在源代码中(所以标题永远不会改变DEFINE_VARIABLES
的值),那么你应该是干净的。记住编写额外的行只是一件麻烦事。另一种选择可能是:
#define HEADER_DEFINING_VARIABLES "file2c.h"
#include "externdef.h"
/*
** 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 */
这有点令人费解,但似乎是安全的(使用file2d.h
,在file2d.h
没有#undef DEFINE_VARIABLES
file2d.h
)。
/* Declare variables */
#include "file2d.h"
/* Define variables */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"
/* Declare variables - again */
#include "file2d.h"
/* Define variables - again */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"
int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }
/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE8C_H_DEFINITIONS)
#undef FILE8C_H_INCLUDED
#endif
#ifndef FILE8C_H_INCLUDED
#define FILE8C_H_INCLUDED
#include "external.h" /* Support macros EXTERN, INITIALIZE */
#include "file2d.h" /* struct oddball */
#if !defined(DEFINE_VARIABLES) || !defined(FILE8C_H_DEFINITIONS)
/* Global variable declarations / definitions */
EXTERN struct oddball another INITIALIZE({ 14, 34 });
#endif /* !DEFINE_VARIABLES || !FILE8C_H_DEFINITIONS */
/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE8C_H_DEFINITIONS
#endif /* DEFINE_VARIABLES */
#endif /* FILE8C_H_INCLUDED */
/* Define variables */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"
/* Define variables */
#define HEADER_DEFINING_VARIABLES "file8c.h"
#include "externdef.h"
int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }
接下来的两个文件完成了prog8
和prog9
的源代码:
#include "file2d.h"
#include <stdio.h>
int main(void)
{
use_them();
global_variable += 19;
use_them();
printf("Increment: %d\n", increment());
printf("Oddball: %d\n", oddball_value());
return 0;
}
#include "file2d.h"
#include <stdio.h>
void use_them(void)
{
printf("Global variable: %d\n", global_variable++);
oddball_struct.a += global_variable;
oddball_struct.b -= global_variable / 2;
}
prog8
使用prog8.c
, file7c.c
, file9c.c
。 prog9
使用prog8.c
, file8c.c
, file9c.c
。 但是,这些问题在实践中相对不太可能发生,特别是如果您采用标准建议
这个博览会是否遗漏了什么?
忏悔 :这里概述的 “避免重复代码” 方案的开发是因为该问题影响了我工作的一些代码(但不拥有),并且是对答案第一部分概述的方案的一个琐碎关注。但是,原始方案只留下两个地方进行修改以保持变量定义和声明同步,这是将 exernal 变量声明分散在整个代码库中的一大步(当总共有数千个文件时真正重要) 。但是,名为fileNc.[ch]
(加上external.h
和externdef.h
)的文件中的代码表明它可以工作。显然,创建一个标题生成器脚本来为定义和声明头文件的变量提供标准化模板并不困难。
NB这些玩具程序只有几乎没有足够的代码来使它们略微有趣。在示例中可以删除重复,但不是为了简化教学解释。 (例如: prog5.c
和prog8.c
之间的区别是包含的一个头的名称。可以重新组织代码,以便不重复main()
函数,但它会隐藏更多比它透露的。)
extern
变量是在另一个翻译单元中定义的变量的声明(由于 sbi 用于校正)。这意味着变量的存储空间分配在另一个文件中。
假设你有两个.c
-files test1.c
和test2.c
。如果你定义一个全局变量int test1_var;
在test1.c
,你想在test2.c
访问这个变量,你必须使用extern int test1_var;
在test2.c
。
完整样本:
$ cat test1.c
int test1_var = 5;
$ cat test2.c
#include <stdio.h>
extern int test1_var;
int main(void) {
printf("test1_var = %d\n", test1_var);
return 0;
}
$ gcc test1.c test2.c -o test
$ ./test
test1_var = 5
Extern 是用于声明变量本身位于另一个转换单元中的关键字。
因此,您可以决定在翻译单元中使用变量,然后从另一个变量访问它,然后在第二个变量中将其声明为 extern,并且链接器将解析该符号。
如果你没有将它声明为 extern,你将获得两个名为相同但根本不相关的变量,以及变量的多个定义的错误。