ChatGPT解决这个技术问题 Extra ChatGPT

如何使用 extern 在源文件之间共享变量?

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

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


J
Jonathan Leffler

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

understand the difference between defining a variable and declaring a variable 很重要:

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

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

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

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

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

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

虽然还有其他方法可以做到,但这种方法简单可靠。 file3.hfile1.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.h 和 prog1.h。

文件 prog1.mk 是仅用于 prog1 的生成文件。它适用于自世纪之交以来生产的大多数 make 版本。它没有专门与 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}

指导方针

只有专家才能打破的规则,并且有充分的理由:

头文件只包含变量的外部声明——从不包含静态或不合格的变量定义。

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

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

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

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

函数永远不需要使用 extern 声明变量。

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

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

如果您不是一位经验丰富的 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

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

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

但是,C 标准也在信息性附录 J 中将其列为 Common extensions 之一。

J.5.11 多个外部定义

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

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

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

请注意,2020-05-07 发布的 GCC 10.1.0 将默认编译选项更改为使用 -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。

警告

正如这里的评论中所述,以及我对类似 question 的回答中所述,对全局变量使用多个定义会导致未定义的行为(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;
}

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

每个 Denis Kniazhev 包含正确的标题 file3b.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 需要同时使用标头 file4b.hfile3b.h,那么您需要解决一些更棘手的问题。显然,您可以修改标题列表以仅包含 file4b.h。但是,您可能不知道内部依赖关系——理想情况下,代码应该继续工作。

此外,它开始变得棘手,因为您可能会在包含 file3b.h 之前包含 file4b.h 以生成定义,但是 file3b.h 上的正常标头保护会阻止重新包含标头。

因此,您最多需要在声明中包含一次 file3b.h 的主体,对定义最多包含一次,但您可能需要在单个翻译单元中同时包含这两者(TU — 源文件及其使用的标头的组合) .

变量定义的多重包含

但是,它可以在不太不合理的约束下完成。让我们介绍一组新的文件名:

external.h 用于 EXTERN 宏定义等。

file1c.h 来定义类型(特别是 structoddball,oddball_struct 的类型)。

file2c.h 来定义或声明全局变量。

file3c.c 定义了全局变量。

file4c.c 仅使用全局变量。

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

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

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

这个工作的限制是:

定义或声明全局变量的标头本身可能不定义任何类型。在包含应定义变量的标头之前,立即定义宏 DEFINE_VARIABLES。定义或声明变量的标头具有程式化的内容。

外部.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; }

下一个源文件完成了 prog5prog6prog7 的源代码(提供了一个主程序):

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?”如果您从标头中省略它并使用 #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.hfile2d.h 中没有 #undef DEFINE_VARIABLES)。

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

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

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() 函数,但它会隐藏更多比它揭示的要多。)


@litb:有关通用定义,请参见附件 J.5.11 - 它是通用扩展。
@litb:我同意应该避免它——这就是为什么它出现在“定义全局变量的不太好的方法”一节中。
事实上,它是一个常见的扩展,但程序依赖它是未定义的行为。我只是不清楚你是否说这是 C 自己的规则所允许的。现在我看到你说这只是一个常见的扩展,如果你需要你的代码是可移植的,请避免它。所以我可以毫无疑问地支持你。真的很好的答案恕我直言:)
如果你停在顶部,它会让简单的事情变得简单。当您进一步阅读时,它会处理更多的细微差别、复杂性和细节。我刚刚为经验不足的 C 程序员或已经了解该主题的 C 程序员添加了两个“早期停止点”。如果您已经知道答案,则无需阅读全部内容(但如果您发现技术故障,请告诉我)。
@supercat:我突然想到,您可以使用 C99 数组文字来获取数组大小的枚举值,例如 (foo.h): #define FOO_INITIALIZER { 1, 2, 3, 4, 5 } 定义数组的初始化程序,enum { FOO_SIZE = sizeof((int [])FOO_INITIALIZER) / sizeof(((int [])FOO_INITIALIZER)[0]) }; 获取大小数组,extern int foo[]; 声明数组。显然,定义应该只是 int foo[FOO_SIZE] = FOO_INITIALIZER;,尽管大小实际上不必包含在定义中。这会得到一个整数常量 FOO_SIZE
j
jww

extern 变量是在另一个翻译单元中定义的变量的声明(感谢 sbi 的更正)。这意味着变量的存储空间分配在另一个文件中。

假设您有两个 .c 文件 test1.ctest2.c。如果您在 test1.c 中定义了一个全局变量 int test1_var;,并且您想在 test2.c 中访问该变量,您必须在 test2.c 中使用 extern int test1_var;

完整样本:

$ 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 int test1_var; 更改为 int test1_var;,链接器 (gcc 5.4.0) 仍然通过。那么,在这种情况下真的需要 extern 吗?
@radiohead:在我的 answer 中,您会发现删除 extern 是一个常见的扩展,它通常可以工作 - 特别适用于 GCC(但 GCC 远不是唯一支持它的编译器;它很普遍在 Unix 系统上)。您可以在我的答案中查找“J.5.11”或“不太好的方法”部分(我知道 - 它 很长)以及附近的文字解释它(或尝试这样做) .
extern 声明当然不必在另一个翻译单元中定义(通常不是)。事实上,声明和定义可以是一回事。
A
Arkaitz Jimenez

Extern 是您用来声明变量本身位于另一个翻译单元中的关键字。

因此,您可以决定在翻译单元中使用一个变量,然后从另一个翻译单元访问它,然后在第二个翻译单元中将其声明为 extern,并且该符号将由链接器解析。

如果您不将其声明为 extern,您将获得 2 个名称相同但根本不相关的变量,以及该变量的多个定义错误。


换句话说,使用 extern 的翻译单元知道这个变量、它的类型等,因此允许底层逻辑中的源代码使用它,但它不分配变量,另一个翻译单元会这样做。如果两个翻译单元都正常声明变量,则变量实际上会有两个物理位置,在编译代码中具有相关的“错误”引用,并且导致链接器的歧义。
B
Buggieboy

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

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

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


更一般地说,声明是一个承诺,即名称将在链接时解析为一个确切的定义。 extern 声明一个变量而不定义。
L
Lucian
                 declare | define   | initialize |
                ----------------------------------

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

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


B
BenB

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

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

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


没有声明内存。有关更多详细信息,请参阅此问题的答案:stackoverflow.com/questions/1410563
C
Community

添加 extern 会将变量 定义 转换为变量 声明。请参阅 this thread,了解声明和定义之间的区别。


int fooextern int foo(文件范围)有什么区别?两者都是声明,不是吗?
@ user14284:它们都是声明,只是在每个定义也是声明的意义上。但我链接到对此的解释。 (“请参阅此线程,了解声明和定义之间的区别。”)您为什么不简单地点击链接阅读?
A
Alex Lockwood

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


A
Anup H

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

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


b
bobby

在 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>


C
Ciro Santilli Путлер Капут 六四事

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 Update ELF spec“符号表”一章解释:

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

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

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

在 GCC 4.8 上测试。

C++17 内联变量

在 C++17 中,您可能希望使用内联变量而不是外部变量,因为它们易于使用(可以在标头上定义一次)且功能更强大(支持 constexpr)。请参阅:What does 'const static' mean in C and C++?


这不是我的反对票,所以我不知道。不过,我会提出意见。尽管查看 readelfnm 的输出可能会有所帮助,但您还没有解释如何使用 extern 的基础知识,也没有完成具有实际定义的第一个程序。您的代码甚至没有使用 notExtern。还有一个命名问题:虽然 notExtern 在这里定义而不是用 extern 声明,但它是一个外部变量,如果这些翻译单元包含合适的声明(需要 extern int notExtern; !)。
@JonathanLeffler 感谢您的反馈!标准行为和使用建议已经在其他答案中完成,所以我决定稍微展示一下实现,因为这真的帮助我掌握了正在发生的事情。不使用 notExtern 很丑,修复它。关于命名法,如果你有更好的名字,请告诉我。当然,对于实际程序来说,这不是一个好名字,但我认为它很适合这里的教学角色。
至于名称,这里定义的变量的 global_def 和其他模块中定义的变量的 extern_ref 呢?它们会有适当清晰的对称性吗?您仍然在定义它的文件中以 int extern_ref = 57; 或类似名称结尾,因此名称不是很理想,但在单个源文件的上下文中,这是一个合理的选择。在我看来,在标题中包含 extern int global_def; 并不是什么大问题。当然,完全取决于你。
J
Jonathan Leffler

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

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


J
Jonathan Leffler

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

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


J
Jonathan Leffler

extern 仅表示变量在别处定义(例如,在另一个文件中)。


J
Jonathan Leffler

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

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


请注意,extern 声明应该在标头中,而不是在 first.c 中,这样如果类型更改,声明也会更改。此外,声明变量的标头应包含在 second.c 中,以确保定义与声明一致。标题中的声明是将所有内容粘合在一起的粘合剂;它允许单独编译文件,但确保它们具有全局变量类型的一致视图。
u
user50619

使用 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

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


m
muusbolla

我用来允许头文件包含外部引用或对象的实际实现的一个非常简短的解决方案。实际包含该对象的文件只是执行 #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

i
i486

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