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


987

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

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

Answers:


1750

使用extern仅是相关的,当程序你正在构建由链接在一起的多个源文件,其中的一些变量的定义,例如,在源文件中file1.c需要在其他源文件中引用,如file2.c

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

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

  • 一个变量被定义时,编译器分配该变量的存储。

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

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

声明和定义全局变量的干净,可靠的方法是使用头文件包含变量的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

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

prog1.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.cfile1.cfile2.cfile3.hprog1.h

该文件仅prog1.mk是一个makefile prog1make自从大约千年之交以来,它将与大多数版本的产品一起使用。它并不专门与GNU Make绑定。

prog1.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 —单点真相)。

  • 源文件从不包含extern变量声明-源文件始终包含声明它们的(唯一)标头。

  • 对于任何给定的变量,恰好一个源文件定义了该变量,最好也将其初始化。(尽管不需要显式地初始化为零,但它无害且可以带来一些好处,因为在程序中只能存在一个对特定全局变量的初始化定义)。

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

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

  • 尽可能避免使用全局变量,而应使用函数。

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

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

定义全局变量的方法不是很好

使用某些(实际上,很多)C编译器,您也可以摆脱所谓的“通用”变量定义。这里的“公用”是指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

一个外部定义为外部声明,这也是一个功能(比内联定义其他)或对象的定义。如果在表达式中使用了用外部链接声明的标识符(不是作为结果为整数常数的a sizeof_Alignof运算符的操作数的一部分),则在整个程序中的某个位置应有一个标识符的外部定义;否则,不得超过一个。161)

161)因此,如果在表达式中未使用通过外部链接声明的标识符,则无需为其定义外部。

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

J.5.11多个外部定义

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

由于并不总是支持此技术,因此最好避免使用它,尤其是在您的代码需要可移植的情况下。使用这种技术,您还可能最终会意外地进行类型调整。

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

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


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

prog2.h

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

prog2.c

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

int main(void)
{
    inc();
    put();
    dec();
    put();
    dec();
    put();
}
  • prog2用途prog2.cfile10.cfile11.cfile12.cprog2.h

警告

如此处评论中所述,以及如我对类似问题的回答所述,对全局变量使用多个定义会导致未定义行为(J.2;第6.9节),这是标准所说的“可能发生的一切”的方式。可能发生的事情之一是程序的行为符合预期。J.5.11大概说,“您可能比应有的机会更幸运”。但是,依赖于extern变量的多个定义(带有或不带有显式的'extern'关键字)的程序并不是严格符合要求的程序,不能保证在任何地方都能正常工作。等效地:它包含一个可能会或可能不会显示自己的错误。

违反准则

当然,有很多方法可以打破这些准则。有时,有充分的理由违反准则,但是这种情况极为罕见。

faulty_header.h

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

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

broken_header.h

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

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

seldom_correct.h

c 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++; }

file2a.c

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

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

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

程序3

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

程序3

#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.cfile1a.cfile2a.cfile3a.hprog3.h

变量初始化

如图所示,此方案的问题在于它不提供全局变量的初始化。使用C99或C11以及宏的变量参数列表,您也可以定义一个宏来支持初始化。(使用C89且不支持宏中的变量参数列表,没有简单的方法来处理任意长的初始化程序。)

file3b.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块,定影错误鉴定 丹尼斯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; }

file2b.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,其余参数(在此示例中为单数)为43 }。如果没有C99或对宏的变量参数列表的类似支持,则需要包含逗号的初始化程序将非常有问题。

丹尼斯·克尼亚热夫file3b.hDenis Kniazhevfileba.h)包含(而不是) 正确的标题


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

prog4.h

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

prog4.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.cfile1b.cfile2b.cprog4.hfile3b.h

标头护卫

应保护所有标头免于重新包含,以便类型定义(枚举,结构或联合类型,或通常为typedef)不会引起问题。标准技术是将标头的主体包装在标头防护中,例如:

#ifndef FILE3B_H_INCLUDED
#define FILE3B_H_INCLUDED

...contents of header...

#endif /* FILE3B_H_INCLUDED */

标头可能会间接包含两次。例如,如果file4b.h包含file3b.h未显示的类型定义,并且file1b.c需要同时使用header file4b.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.cfile6c.c直接包含该头file2c.h几次,但这是表明该机制的工作最简单的方法。这意味着,如果标头被间接包含两次,那也是安全的。

这项工作的限制是:

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

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

  3. 定义或声明变量的标题具有风格化的内容。

外部


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

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

file3c.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; }

file4c.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;
}

file5c.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; }

file6c.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.cfile3c.cfile4c.cfile1c.hfile2c.hexternal.h

  • prog6用途prog5.cfile5c.cfile4c.cfile1c.hfile2c.hexternal.h

  • prog7用途prog5.cfile6c.cfile4c.cfile1c.hfile2c.hexternal.h


该方案避免了大多数问题。仅当定义变量的标头(例如file2c.h)包含在另一个file7c.h定义变量的标头(例如)中时,您才遇到问题。除了“不做”以外,没有其他简便的方法。

您可以通过修改file2c.h为部分解决此问题file2d.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"

外部定义


#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)。

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

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

程序8

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

file9c.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.cfile7c.cfile9c.c

  • prog9使用prog8.cfile8c.cfile9c.c


但是,在实践中相对不太可能出现问题,尤其是如果您采用标准建议

避免全局变量


这次博览会有什么遗漏吗?

坦白:之所以开发此处概述的“避免重复代码”方案,是因为该问题会影响我正在处理的某些代码(但不属于我),并且是对答案第一部分中概述的方案的关注。但是,原始方案仅使您有两个地方可以进行修改以使变量定义和声明保持同步,这比将外部变量声明分散在整个代码库中是向前迈出了一大步(当总共有数千个文件时,这很重要) 。但是,文件中带有名称fileNc.[ch](加号external.hexterndef.h)的代码表明可以使其正常工作。显然,创建标题生成器脚本以为定义和声明标题文件的变量提供标准化模板并不困难。

注意:这些是玩具程序,仅具有几乎不足以使它们有趣的代码。示例中可以删除重复项,但这并不是为了简化教学方法。(例如:之间的差prog5.cprog8.c是所包含的报头中的一个的名称将是可能重新组织代码,使得。main()不重复的功能,但它会掩盖超过它显露出来。)


3
@litb:有关通用定义,请参见附件J.5.11-它是通用扩展。
乔纳森·莱夫勒

3
@litb:我同意应避免使用-这就是为什么它在“定义全局变量的方法不是很好”部分中的原因。
乔纳森·莱夫勒

3
确实,这是一个常见的扩展,但是对于依赖它的程序来说,这是未定义的行为。我只是不清楚您是否在说这是C自己的规则所允许的。现在,我看到您说的是这只是一个常见的扩展,如果需要代码可移植,请避免使用它。所以我可以毫无疑问地支持你。恕我直言,真的很棒的答案:)
Johannes Schaub-litb

19
如果停在顶部,则可以使简单的事情保持简单。当您进一步阅读时,它将涉及更多的细微差别,复杂性和细节。我刚刚为经验不足的C程序员(或已经知道该主题的C程序员)添加了两个“早期停止点”。如果您已经知道答案,则无需全部阅读(但如果发现技术故障,请告诉我)。
Jonathan Leffler 2014年

4
@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
Jonathan Leffler 2014年

125

extern变量是在另一个转换单元中定义的变量的声明(感谢sbi进行更正)。这意味着该变量的存储在另一个文件中分配。

假设您有两个.c文件test1.ctest2.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

21
没有“伪定义”。这是一个宣言。
sbi

3
在上面的示例中,如果将更extern int test1_var;改为int test1_var;,则链接器(gcc 5.4.0)仍然可以通过。那么,extern在这种情况下真的需要吗?
Radiohead

2
@radiohead:在我的答案中,您会发现删除的信息extern是一个常用的扩展名,尤其是与GCC一起使用(但GCC远不是唯一支持它的编译器;它在Unix系统上很普遍)。你可以看看我的回答“J.5.11”或部分“不太好办法”(我知道-它长)和附近的文本解释了它(或试图这样做)。
乔纳森·莱夫勒

当然,不必在另一个翻译单元中定义extern声明(通常不是这样)。实际上,声明和定义可以是相同的。
请记住莫妮卡

40

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

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

如果不将其声明为extern,则将获得2个名称相同但完全不相关的变量,并且会出现该变量的多个定义的错误。


5
换句话说,使用extern的翻译单元知道该变量,变量的类型等,因此允许底层逻辑中的源代码使用它,但它不分配变量,另一个翻译单元将执行此操作。如果两个翻译单元都正常声明该变量,则该变量将有效地存在两个物理位置,在编译后的代码中具有关联的“错误”引用,并且对于链接程序造成歧义。
mjv

26

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

遇到外部变量时,编译器只能找到其类型,而不能找到其“存在”的位置,因此它无法解析引用。

您告诉它,“相信我。在链接时,此引用将是可解决的。”


更一般地,声明是对在链接时将名称解析为一个定义的承诺。extern声明了一个变量,但没有定义。
Lie Ryan

18

extern告诉编译器信任您该变量的内存在其他地方声明,因此它不会尝试分配/检查内存。

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

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


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

15

extern变量定义加到变量声明中。请参阅此线程,以了解声明和定义之间的区别。


int fooextern int foo(文件作用域)之间有什么区别?两者都是声明,不是吗?

@ user14284:两者都是声明,仅在每个定义也是声明的意义上。但我链接到对此的解释。(“请参见此线程以了解声明和定义之间的区别。”)为什么不简单地按照链接进行阅读呢?
2012年

14
                 declare | define   | initialize |
                ----------------------------------

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

声明不会分配内存(必须为内存分配定义变量),但是定义会。这只是对extern关键字的另一种简单见解,因为其他答案确实很棒。


11

extern的正确解释是您向编译器告知了一些信息。您告诉编译器,尽管现在不存在,但声明的变量将以某种方式由链接器(通常在另一个对象(文件)中)找到。不管您是否有外部声明,链接器都将是找到所有内容并将其放在一起的幸运者。


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

example1.c

int a = 5;

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

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

您将获得以下输出:a的值为<5>


8

GCC ELF Linux实施

其他答案涵盖了语言使用方面的观点,因此现在让我们看一下如何在此实现中实现它。

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

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

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

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

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

在GCC 4.8上测试。

C ++ 17内联变量

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


3
这不是我的不赞成,所以我不知道。但是,我会提出意见。尽管查看readelf或的输出nm可能会有所帮助,但您尚未说明如何使用的基础知识extern,也没有完成带有实际定义的第一个程序。您的代码甚至都没有使用notExtern。也有一个命名法问题:尽管notExtern这里定义而不是用声明extern,但它是一个外部变量,如果这些翻译单元包含合适的声明(可能需要extern int notExtern;!),则其他源文件可以访问该外部变量。
乔纳森·莱夫勒

1
@JonathanLeffler感谢您的反馈!在其他答案中已经完成了标准的行为和用法建议,因此我决定展示一下实现,因为它确实帮助我了解了正在发生的事情。不使用notExtern是丑陋的,修复它。关于术语,让我知道您是否有更好的名字。当然,对于实际程序而言,这并不是一个好名字,但我认为它非常适合这里的教学角色。
Ciro Santilli冠状病毒审查六四事件法轮功

至于名称,global_def这里定义的变量以及extern_ref其他模块定义的变量呢?它们是否具有适当清晰的对称性?您仍然int extern_ref = 57;会在定义该文件的文件中最终得到诸如此类的名称,因此名称并不是很理想,但是在单个源文件的上下文中,这是一个合理的选择。在extern int global_def;我看来,头文件并不是一个大问题。当然,完全取决于您。
Jonathan Leffler

7

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

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


5

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

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


5

extern 仅仅意味着在其他地方(例如,在另一个文件中)定义了变量。


4

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

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


3

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

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


3
请注意,extern声明应该在标头中,而不是in中first.c,这样,如果类型更改,则声明也将更改。同样,声明变量的标头应包括在内,second.c以确保定义与声明一致。标头中的声明是将所有内容粘合在一起的粘合剂;它允许分别编译文件,但确保它们对全局变量的类型具有一致的视图。
Jonathan Leffler

2

使用xc8时,您必须谨慎地在每个文件中声明一个与同一类型相同的变量,错误地int在一个文件中声明一个,在另一个文件中声明char。这可能导致变量损坏。

这个问题在15年前的微芯片论坛上得到了很好的解决//参见“ http:www.htsoft.com” // “ forum / all / / 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的其他部分,从而可能损坏另一个变量。调试困难!


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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.