【问题标题】:How do I use extern to share variables between source files?如何使用 extern 在源文件之间共享变量?
【发布时间】:2010-11-28 19:49:08
【问题描述】:

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

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

【问题讨论】:

    标签: c global-variables extern


    【解决方案1】:

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

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

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

    【讨论】:

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

    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 声明当然不必在另一个翻译单元中定义(通常不需要)。其实声明和定义可以是一回事。
    【解决方案3】:

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

    【讨论】:

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

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

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

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

    【讨论】:

    【解决方案5】:

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

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

    file2.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 声明;编译器不在乎——最终,我也不在乎 只要你是一致的,至少在源文件中。

    prog1.h

    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 是仅用于prog1 的生成文件。 它将适用于自大约转弯以来生产的大多数版本的make 的千年。 它与 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 声明变量。

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

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

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

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

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

    file10.c

    #include "prog2.h"
    
    long l;   /* Do not do this in portable code */
    
    void inc(void) { l++; }
    

    file11.c

    #include "prog2.h"
    
    long l;   /* Do not do this in portable code */
    
    void dec(void) { l--; }
    

    file12.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 Undefined behavior

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

    §6.9 External definitions ¶5

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

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

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

    J.5.11 Multiple external definitions

    标识符的外部定义可能不止一个 一个对象,有或没有明确使用关键字 extern;如果 定义不一致,或者不止一个被初始化, 行为未定义 (6.9.2)。

    因为这种技术并不总是得到支持,所以最好避免 使用它,尤其是当您的代码需要可移植时。 使用这种技术,你也可以最终得到无意的类型 双关语。

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

    请注意,2020 年 5 月 7 日发布的 GCC 10.1.0 更改了 要使用的默认编译选项 -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

    警告

    如 cmets here 中所述,以及我对类似问题的回答中所述 question,使用多个 全局变量的定义导致未定义的行为(J.2; §6.9),这是标准的说法“任何事情都可能发生”。 可能发生的事情之一是程序的行为与您一样 预计; J.5.11 大约说,“你可能更幸运 比你应得的”。 但是一个依赖于外部变量的多个定义的程序 — 有或没有显式的 'extern' 关键字 - 不是严格的 符合程序,不保证在任何地方都可以工作。 等效地:它包含一个可能会或可能不会显示自身的错误。

    违反准则

    当然,可以通过多种方式破坏这些准则。 有时,可能有充分的理由违反准则,但 这样的场合是极不寻常的。

    faulty_header.h

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

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

    broken_header.h

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

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

    seldom_correct.h

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

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

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


    总结

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

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

    原始答案结束

    如果你不是一个有经验的 C 程序员,你可能应该停止阅读这里。


    后期主要添加

    避免代码重复

    有时(并且合法地)提出的一个担忧是 描述了“标题中的声明,源中的定义”机制 这里有两个文件要保持同步——头文件 和来源。这通常伴随着一个观察结果,即 可以使用宏,以便标头具有双重职责——通常 声明变量,但是当在 包含标头,它定义了变量。

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

    一个典型的方案是这样工作的,使用原始的全局变量 说明于file3.h

    file3a.h

    #ifdef DEFINE_VARIABLES
    #define EXTERN /* nothing */
    #else
    #define EXTERN extern
    #endif /* DEFINE_VARIABLES */
    
    EXTERN int global_variable;
    

    file1a.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的源代码:

    prog3.h

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

    prog3.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.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 块的内容,修复由 Denis Kniazhev

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

    很明显,古怪结构的代码与您通常的不同 写,但它说明了这一点。第一个参数到第二个 INITIALIZER 的调用是 { 41 和剩余的参数 (在本例中为单数)是43 }。没有 C99 或类似的支持 对于宏的变量参数列表,需要 包含逗号是很成问题的。

    每个包含正确的标题 file3b.h(而不是 fileba.hDenis Kniazhev


    接下来的两个文件完成了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 需要同时使用标头file4b.hfile3b.h,然后 你有一些更棘手的问题要解决。显然,您可以修改 仅包含 file4b.h 的标题列表。然而,你可能不是 了解内部依赖关系——理想情况下,代码应该 继续工作。

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

    因此,您最多需要包含 file3b.h 的正文一次 声明,最多一次用于定义,但您可能需要两者 在单个翻译单元中(TU — 源文件和 它使用的标题)。

    变量定义的多重包含

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

    • external.h 用于 EXTERN 宏定义等

    • file1c.h 定义类型(特别是struct oddballoddball_struct 的类型)。

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

    • file3c.c 定义了全局变量。

    • file4c.c 简单地使用全局变量。

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

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

    在这些示例中,file5c.cfile6c.c 直接包含标题 file2c.h 多次,但这是表明 机制起作用。这意味着如果标题被间接包含 两次,它也将是安全的。

    这个工作的限制是:

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

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

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

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

    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"
    

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

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

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

    接下来的两个文件完成了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;
    }
    

    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


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

    避免使用全局变量


    这个展览有什么遗漏吗?

    _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
    【解决方案6】:

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

    当遇到extern时,编译器只能找出它的类型,而不是它“居住”的地方,所以它无法解析引用。

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

    【讨论】:

    • 更一般地说,声明是一个承诺,即名称将在链接时被解析为一个精确的定义。 extern 声明了一个没有定义的变量。
    【解决方案7】:

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

    【讨论】:

      【解决方案8】:

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

      【讨论】:

        【解决方案9】:

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

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

        【讨论】:

          【解决方案10】:

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

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

          【讨论】:

            【解决方案11】:

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

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

            【讨论】:

              【解决方案12】:

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

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

              【讨论】:

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

              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
              

              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; 并不是什么大问题。当然,这完全取决于您。
              【解决方案14】:

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

              【讨论】:

                【解决方案15】:

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

                这个问题在大约 15 年前的一个微芯片论坛上得到了优雅的解决 /* 参见“http:www.htsoft.com”/ /“论坛/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 的其他部分,可能会破坏另一个变量。难以调试!

                【讨论】:

                  【解决方案16】:
                                   declare | define   | initialize |
                                  ----------------------------------
                  
                  extern int a;    yes          no           no
                  -------------
                  int a = 2019;    yes          yes          yes
                  -------------
                  int a;           yes          yes          no
                  -------------
                  

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

                  【讨论】:

                    【解决方案17】:

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

                    【讨论】:

                      猜你喜欢
                      • 2022-08-14
                      • 2011-02-08
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多