1149

我知道 C 中的全局变量有时有extern关键字。什么是extern变量?声明是什么样的?它的范围是什么?

这与跨源文件共享变量有关,但它是如何精确工作的呢?我在哪里使用extern

4

18 回答 18

1993

仅当您正在构建的程序由链接在一起的多个源文件组成时,使用extern才有意义,其中一些定义的变量(例如,在源文件中定义的变量file1.c需要在其他源文件中引用,例如file2.c.

了解定义变量和声明变量之间的区别很重要:

  • 当编译器被告知变量存在(这是它的类型)时,声明了一个变量;它此时不会为变量分配存储空间。

  • 当编译器为变量分配存储空间时,就定义了一个变量。

您可以多次声明一个变量(尽管一次就足够了);您只能在给定范围内定义一次。变量定义也是声明,但并非所有变量声明都是定义。

声明和定义全局变量的最佳方式

声明和定义全局变量的干净、可靠的方法是使用头文件来包含变量的extern 声明

标头包含在定义变量的一个源文件和引用该变量的所有源文件中。对于每个程序,一个源文件(并且只有一个源文件)定义变量。同样,一个头文件(并且只有一个头文件)应该声明该变量。头文件至关重要;它可以在独立的 TU(翻译单元 - 考虑源文件)之间进行交叉检查并确保一致性。

虽然还有其他方法可以做到,但这种方法简单可靠。它由file3.h和证明:file1.cfile2.c

文件3.h

extern int global_variable;  /* Declaration of the variable */

文件1.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++; }

文件2.c

#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在函数声明前面使用;编译器不在乎——最终,只要你保持一致,我也不在乎,至少在源文件中是这样。

程序1.h

extern void use_it(void);
extern int increment(void);

程序1.c

#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.hprog1.h

该文件prog1.mk只是一个makefile prog1make它适用于自千年之交以来生产的大多数版本。它没有专门与 GNU Make 绑定。

程序1.mk

# 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或不合格的变量定义。

  • 对于任何给定的变量,只有一个头文件声明它(SPOT - Single Point of Truth)。

  • 源文件从不包含extern变量的声明——源文件总是包含声明它们的(唯一)头文件。

  • 对于任何给定的变量,只有一个源文件定义了变量,最​​好也初始化它。(虽然不需要显式初始化为零,但它没有害处并且可以做一些好事,因为在程序中只能有一个特定全局变量的初始化定义)。

  • 定义变量的源文件还包括头文件,以确保定义和声明一致。

  • 函数永远不需要使用extern.

  • 尽可能避免使用全局变量——改用函数。

此答案的源代码和文本可在我 的 GitHub 上的SOQ(堆栈溢出问题)存储库中的 src/so-0143-3204 子目录中找到。

如果您不是一位经验丰富的 C 程序员,您可以(也许应该)停止阅读这里。

定义全局变量的方法不太好

使用一些(实际上,很多)C 编译器,您也可以摆脱所谓的“通用”变量定义。这里的“Common”是指 Fortran 中使用的一种技术,用于在源文件之间共享变量,使用(可能命名的)COMMON 块。这里发生的是,许多文件中的每一个都提供了变量的暂定定义。只要不超过一个文件提供了一个初始化定义,那么各个文件最终会共享一个通用的变量定义:

文件10.c

#include "prog2.h"

long l;   /* Do not do this in portable code */

void inc(void) { l++; }

文件 11.c

#include "prog2.h"

long l;   /* Do not do this in portable code */

void dec(void) { l--; }

文件 12.c

#include "prog2.h"
#include <stdio.h>

long l = 9;   /* Do not do this in portable code */

void put(void) { printf("l = %ld\n", l); }

这种技术不符合 C 标准的字母和“单一定义规则”——它是官方未定义的行为:

J.2 未定义的行为

使用了具有外部链接的标识符,但在程序中不存在该标识符的确切一个外部定义,或者未使用该标识符并且该标识符存在多个外部定义(6.9)。

§6.9 外部定义¶5

外部定义是一个外部声明,它也是函数(内联定义除外)或对象的定义。如果用外部链接声明的标识符在表达式中使用(而不是作为结果为整数常量的sizeofor _Alignof运算符的操作数的一部分),则在整个程序的某处,该标识符应有一个外部定义;否则,不得超过一个。161)

161)因此,如果用外部链接声明的标识符未在表达式中使用,则不需要对其进行外部定义。

但是,C 标准还在信息性附录 J 中将其列为通用扩展之一。

J.5.11 多个外部定义

一个对象的标识符可能有多个外部定义,无论是否显式使用关键字 extern;如果定义不一致,或者不止一个被初始化,则行为未定义(6.9.2)。

由于并不总是支持这种技术,因此最好避免使用它,尤其是在您的代码需要可移植的情况下。使用这种技术,您也可能会出现无意的双关语。

如果上述文件之一声明l为 adouble而不是 a long,则 C 的类型不安全链接器可能不会发现不匹配。如果您使用的是 64 位long和的机器double,您甚至不会收到警告;在具有 32-bitlong和 64-bit的机器上double,您可能会收到有关不同大小的警告——链接器将使用最大的大小,就像 Fortran 程序将采用任何常见块的最大大小一样。

请注意,于 2020-05-07 发布的 GCC 10.1.0 将默认编译选项更改为 use -fno-common,这意味着默认情况下,上面的代码不再链接,除非您使用-fcommon(或使用属性等)覆盖默认值 -见链接)。


接下来的两个文件完成了源代码prog2

程序2.h

extern void dec(void);
extern void put(void);
extern void inc(void);

程序2.c

#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 变量的多个定义的程序——无论是否有明确的“extern”关键字——并不是一个严格符合的程序,也不能保证在任何地方都可以工作。等效地:它包含一个可能会或可能不会显示自己的错误。

违反准则

当然,有很多方法可以破坏这些准则。有时,违反准则可能有充分的理由,但这种情况极不寻常。

faulty_header.h

int some_var;    /* Do not do this in a header!!! */

注意 1:如果标题定义了没有extern关键字的变量,则包含标题的每个文件都会创建变量的暂定定义。如前所述,这通常会起作用,但 C 标准不保证它会起作用。

破头文件.h

int some_var = 13;    /* Only one source file in a program can use this */

注2:如果头文件定义并初始化变量,那么在给定程序中只有一个源文件可以使用头文件。由于标头主要用于共享信息,因此创建一个只能使用一次的标头有点愚蠢。

很少正确的.h

static int hidden_global = 3;   /* Each source file gets its own copy  */

注意 3:如果头文件定义了一个静态变量(有或没有初始化),那么每个源文件都会以自己的“全局”变量私有版本结束。

例如,如果变量实际上是一个复杂的数组,这可能会导致极端的代码重复。偶尔,它可能是实现某些效果的明智方式,但这是非常不寻常的。


概括

使用我首先展示的标题技术。它可以在任何地方可靠地工作。请特别注意,声明 的标头global_variable包含在每个使用它的文件中——包括定义它的文件。这确保了一切都是自洽的。

声明和定义函数也会出现类似的问题——类似的规则适用。但问题是关于变量的,所以我只保留了变量的答案。

原始答案结束

如果您不是经验丰富的 C 程序员,您可能应该停止阅读此处。


后期主要增加

避免代码重复

关于此处描述的“标头中的声明,源中的定义”机制有时(并且合法地)提出的一个问题是有两个文件要保持同步 - 标头和源。随后通常会观察到可以使用宏,以便标头具有双重职责——通常声明变量,但是当在包含标头之前设置特定宏时,它会改为定义变量。

另一个问题可能是需要在多个“主程序”中的每一个中定义变量。这通常是一个虚假的问题。您可以简单地引入一个 C 源文件来定义变量并将生成的目标文件与每个程序链接起来。

一个典型的方案是这样工作的,使用如下所示的原始全局变量file3.h

文件3a.h

#ifdef DEFINE_VARIABLES
#define EXTERN /* nothing */
#else
#define EXTERN extern
#endif /* DEFINE_VARIABLES */

EXTERN int global_variable;

文件1a.c

#define DEFINE_VARIABLES
#include "file3a.h"  /* Variable defined - but not initialized */
#include "prog3.h"

int increment(void) { return global_variable++; }

文件2a.c

#include "file3a.h"
#include "prog3.h"
#include <stdio.h>

void use_it(void)
{
    printf("Global variable: %d\n", global_variable++);
}

接下来的两个文件完成了源代码prog3

程序3.h

extern void use_it(void);
extern int increment(void);

程序3.c

#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 并且不支持宏中的变量参数列表,没有简单的方法来处理任意长的初始值设定项。)

文件3b.h

#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确定的错误

文件1b.c

#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; }

文件2b.c

#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;
}

显然,古怪结构的代码不是你通常写的,但它说明了这一点。第二次调用INITIALIZERis的第一个参数{ 41,其余参数(在此示例中为单数) is 43 }。如果没有 C99 或对宏的可变参数列表的类似支持,需要包含逗号的初始化程序就会非常有问题。

每个 Denis Kniazhevfile3b.h包括(而不是)正确的标题fileba.h


接下来的两个文件完成了源代码prog4

程序4.h

extern int increment(void);
extern int oddball_value(void);
extern void use_them(void);

程序4.c

#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需要同时使用 headerfile4b.hfile3b.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它只是使用全局变量。

  • file5c.c这表明您可以声明然后定义全局变量。

  • file6c.c这表明您可以定义然后(尝试)声明全局变量。

在这些示例中,file5c.c直接file6c.c多次包含标头 file2c.h,但这是表明该机制有效的最简单方法。这意味着如果标头被间接包含两次,它也是安全的。

这个工作的限制是:

  1. 定义或声明全局变量的标头本身可能不定义任何类型。

  2. 在包含应定义变量的标头之前,立即定义宏 DEFINE_VARIABLES。

  3. 定义或声明变量的标头具有程式化的内容。

外部.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 */

文件1c.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 */

文件2c.h


/* 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 */

文件3c.c

#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; }

文件4c.c

#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;
}

文件5c.c


#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; }

文件6c.c


#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

prog5.c

#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

文件2d.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吗?” 如果您从标头中省略它并使用 and 包装任何定义 #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没有)。#undef DEFINE_VARIABLESfile2d.h

文件7c.c

/* 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; }

文件8c.h

/* 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 */

文件8c.c

/* 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

prog8.c

#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;
}

文件9c.c

#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.


但是,这些问题在实践中相对不太可能发生,特别是如果您采用标准建议

避免全局变量


这个展览有什么遗漏吗?

_Confession_:此处概述的“避免重复代码”方案是开发的,因为该问题影响了我正在处理的一些代码(但不拥有),并且是答案第一部分中概述的方案的一个琐碎问题。然而,原始方案只留下两个地方可以修改以保持变量定义和声明同步,这比将外部变量声明分散在整个代码库中迈出了一大步(当总共有数千个文件时,这真的很重要) . 但是,文件中名为 `fileNc.[ch]` 的代码(加上 `external.h` 和 `externdef.h`)表明它可以工作。显然,创建一个标头生成器脚本来为您提供用于定义和声明头文件的变量的标准化模板并不难。

NB这些是玩具程序,代码勉强足以让它们变得有点有趣。可以删除示例中的重复,但不是为了简化教学解释。(例如: 和 之间的区别prog5.cprog8.c包含的标题之一的名称。可以重新组织代码以便main()不重复该功能,但它隐藏的内容多于显示的内容。)

于 2009-09-16T14:37:14.717 回答
134

An extern variable is a declaration (thanks to sbi for the correction) of a variable which is defined in another translation unit. That means the storage for the variable is allocated in another file.

Say you have two .c-files test1.c and test2.c. If you define a global variable int test1_var; in test1.c and you'd like to access this variable in test2.c you have to use extern int test1_var; in test2.c.

Complete sample:

$ 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
于 2009-09-16T14:12:24.703 回答
42

Extern is the keyword you use to declare that the variable itself resides in another translation unit.

So you can decide to use a variable in a translation unit and then access it from another one, then in the second one you declare it as extern and the symbol will be resolved by the linker.

If you don't declare it as extern you'll get 2 variables named the same but not related at all, and an error of multiple definitions of the variable.

于 2009-09-16T14:11:25.987 回答
29

我喜欢将 extern 变量视为您对编译器的承诺。

当遇到一个外部时,编译器只能找出它的类型,而不是它“生活”的地方,所以它无法解析引用。

你告诉它,“相信我。在链接时,这个引用将是可解析的。”

于 2009-09-16T14:50:53.223 回答
26
                 declare | define   | initialize |
                ----------------------------------

extern int a;    yes          no           no
-------------
int a = 2019;    yes          yes          yes
-------------
int a;           yes          yes          no
-------------

声明不会分配内存(必须为内存分配定义变量),但定义会。这只是 extern 关键字的另一个简单视图,因为其他答案真的很棒。

于 2019-01-09T20:50:02.393 回答
18

extern 告诉编译器相信你这个变量的内存是在别处声明的,所以它不会尝试分配/检查内存。

因此,您可以编译一个引用外部的文件,但如果该内存未在某处声明,则无法链接。

对全局变量和库很有用,但很危险,因为链接器不进行类型检查。

于 2009-09-16T14:18:57.093 回答
16

Adding an extern turns a variable definition into a variable declaration. See this thread as to what's the difference between a declaration and a definition.

于 2009-09-16T14:16:24.853 回答
12

extern 的正确解释是你告诉编译器一些事情。您告诉编译器,尽管声明的变量现在不存在,但链接器会以某种方式找到声明的变量(通常在另一个对象(文件)中)。链接器将是找到所有内容并将其组合在一起的幸运者,无论您是否有一些外部声明。

于 2012-06-20T23:43:15.577 回答
8

extern 关键字与变量一起使用,以将其标识为全局变量。

它还表示您可以在任何文件中使用使用 extern 关键字声明的变量,尽管它是在其他文件中声明/定义的。

于 2012-08-20T10:19:51.157 回答
8

在 C 中,文件中的变量 example.c 被赋予本地范围。编译器希望该变量在同一个文件 example.c 中具有其定义,当它没有找到相同的文件时,它会抛出一个错误。另一方面,一个函数默认具有全局范围。因此,您不必向编译器明确提及“看老兄……您可能会在此处找到此函数的定义”。对于包含包含其声明的文件的函数就足够了。(您实际上称为头文件的文件)。例如考虑以下 2 个文件:
example.c

#include<stdio.h>
extern int a;
main(){
       printf("The value of a is <%d>\n",a);
}

例子1.c

int a = 5;

现在,当您一起编译这两个文件时,使用以下命令:

步骤 1)cc -o ex example.c example1.c 步骤 2)./ex

你得到以下输出: a 的值是 <5>

于 2012-07-02T09:11:11.497 回答
8

GCC ELF Linux 实现

其他答案已经涵盖了语言使用方面的观点,所以现在让我们看看它是如何在这个实现中实现的。

主程序

#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

System V ABI 更新 ELF 规范“符号表”一章解释说:

SHN_UNDEF 此节表索引表示符号未定义。当链接编辑器将此对象文件与另一个定义指定符号的文件组合时,此文件对符号的引用将链接到实际定义。

这基本上是 C 标准赋予extern变量的行为。

从现在开始,链接器的工作就是制作最终程序,但extern信息已经从源代码中提取到目标文件中。

在 GCC 4.8 上测试。

C++17 内联变量

在 C++17 中,您可能希望使用内联变量而不是外部变量,因为它们易于使用(可以在标头上定义一次)且功能更强大(支持 constexpr)。请参阅:C 和 C++ 中的“const static”是什么意思?

于 2015-05-29T07:34:58.657 回答
5

extern 允许程序的一个模块访问在程序的另一个模块中声明的全局变量或函数。您通常在头文件中声明外部变量。

如果您不希望程序访问您的变量或函数,请使用staticwhich 告诉编译器此变量或函数不能在此模块之外使用。

于 2012-10-03T04:58:14.327 回答
4

首先,extern关键字不用于定义变量;而是用于声明变量。我可以说extern是存储类,而不是数据类型。

extern用于让其他 C 文件或外部组件知道该变量已在某处定义。示例:如果您正在构建库,则无需在库本身的某处强制定义全局变量。该库将直接编译,但在链接文件时,它会检查定义。

于 2012-08-09T09:21:11.537 回答
4

extern仅仅意味着一个变量是在别处定义的(例如,在另一个文件中)。

于 2016-01-27T19:47:31.420 回答
3

extern使用,因此一个first.c文件可以完全访问另一个second.c文件中的全局参数。

extern可以在文件first.c中或任何包含的头文件中声明first.c

于 2014-09-01T07:35:20.020 回答
2

使用 xc8 时,您必须小心在每个文件中将变量声明为相同类型,因为您可能错误地int在一个文件中声明某事,而在另一个文件中声明某事char。这可能导致变量损坏。

大约 15 年前,这个问题在一个微芯片论坛上被优雅地解决了 /* 参见“http:www.htsoft.com” // “forum/all/showflat.php/Cat/0/Number/18766/an/0/page/ 0#18766"

但是这个链接似乎不再有效......

所以我会很快尝试解释它;创建一个名为 global.h 的文件。

在其中声明以下内容

#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

现在在文件 main.c

#define MAIN_C 1
#include "global.h"
#undef MAIN_C

这意味着在 main.c 中,变量将被声明为unsigned char.

现在在其他文件中只包括 global.h 将把它声明为该文件的外部。

extern unsigned char testing_mode;

但它将被正确声明为unsigned char.

旧论坛帖子可能更清楚地解释了这一点。gotcha但是,当使用允许您在一个文件中声明一个变量然后在另一个文件中将其声明为不同类型的编译器时,这是一个真正的潜力。与此相关的问题是,如果您说在另一个文件中将 testing_mode 声明为 int,它会认为它是 16 位 var 并覆盖 ram 的其他部分,可能会损坏另一个变量。很难调试!

于 2018-10-09T10:01:40.120 回答
0

我用来允许头文件包含外部引用或对象的实际实现的一个非常简短的解决方案。实际包含该对象的文件只是#define GLOBAL_FOO_IMPLEMENTATION. 然后,当我向该文件添加一个新对象时,它也会显示在该文件中,而无需我复制和粘贴定义。

我在多个文件中使用这种模式。所以为了让事情尽可能地独立,我只是在每个标题中重用单个 GLOBAL 宏。我的标题如下所示:

//file foo_globals.h
#pragma once  
#include "foo.h"  //contains definition of foo

#ifdef GLOBAL  
#undef GLOBAL  
#endif  

#ifdef GLOBAL_FOO_IMPLEMENTATION  
#define GLOBAL  
#else  
#define GLOBAL extern  
#endif  

GLOBAL Foo foo1;  
GLOBAL Foo foo2;


//file main.cpp
#define GLOBAL_FOO_IMPLEMENTATION
#include "foo_globals.h"

//file uses_extern_foo.cpp
#include "foo_globals.h
于 2019-06-24T02:23:37.330 回答
0

简而言之extern,意味着变量是在其他模块中定义的,并且它的地址将在链接时知道。编译器不会在当前模块中保留内存并且知道变量类型。理解extern是好的,至少对汇编程序有一点经验。

于 2022-02-28T15:22:09.110 回答