如何使用 extern 在源文件之间共享变量? [英] How do I use extern to share variables between source files?

查看:26
本文介绍了如何使用 extern 在源文件之间共享变量?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

解决方案

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

了解定义一个变量和声明一个变量:

  • 一个变量被声明,当编译器被告知一个变量变量存在(这是它的类型);它不分配那个时候变量的存储.

  • 一个变量在编译器分配存储时定义变量.

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

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

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

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

虽然还有其他的方法,但是这个方法简单又好用可靠的.由file3.hfile1.cfile2.c演示:

file3.h

extern int global_variable;/* 变量声明 */

file1.c

#include "file3.h";/* 此处提供声明 */#include "prog1.h";/* 函数声明 *//* 此处定义的变量 */int global_variable = 37;/* 根据声明检查定义 */int increment(void) { return global_variable++;}

file2.c

#include "file3.h";#include "prog1.h";#include void use_it(void){printf("全局变量:%d
", global_variable++);}

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

<小时>

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

显示的完整程序使用函数,所以函数声明有蹑手蹑脚.C99 和 C11 都要求在函数之前声明或定义函数使用(而 C90 没有使用,有充分的理由).我在标题中的函数声明前使用关键字 extern为了一致性——匹配变量前面的 extern头文件中的声明.很多人不喜欢在函数前面使用extern声明;编译器不在乎——最终,我也不在乎只要您保持一致,至少在源文件中是一致的.

prog1.h

extern void use_it(void);外部整数增量(无效);

prog1.c

#include "file3.h";#include "prog1.h";#include int main(void){用它();global_variable += 19;用它();printf("增量:%d
",增量());返回0;}

  • prog1 使用 prog1.cfile1.cfile2.cfile3.hprog1.h.

文件 prog1.mk 是仅用于 prog1 的生成文件.它将适用于大约回合以来产生的大多数版本的 make千年的.它与 GNU Make 无关.

prog1.mk

# prog1 的最小生成文件程序 = 程序 1FILES.c = prog1.c file1.c file2.c文件.h = prog1.h 文件3.hFILES.o = ${FILES.c:.c=.o}CC = gccSFLAGS = -std=c11GFLAGS = -g标志 = -O3WFLAG1 = -墙WFLAG2 = -WextraWFLAG3 = -WerrorWFLAG4 = -Wstrict-prototypesWFLAG5 = -Wmissing-prototypesWFLAGS = ${WFLAG1} ${WFLAG2} ${WFLAG3} ${WFLAG4} ${WFLAG5}UFLAGS = # 仅在命令行上设置CFLAGS = ${SFLAGS} ${GFLAGS} ${OFLAGS} ${WFLAGS} ${UFLAGS}LDFLAGS =低密度脂蛋白 =全部:${PROGRAM}${PROGRAM}:${FILES.o}${CC} -o $@ ${CFLAGS} ${FILES.o} ${LDFLAGS} ${LDLIBS}prog1.o: ${FILES.h}file1.o: ${FILES.h}file2.o: ${FILES.h}# 如果存在,prog1.dSYM是macOS上的目录碎片 = a.out 核心 *~ *.dSYMRM_FR = rm -fr干净的:${RM_FR} ${FILES.o} ${PROGRAM} ${DEBRIS}

<小时>

指南

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

  • 头文件只包含变量的extern声明——从不static 或不合格的变量定义.

  • 对于任何给定的变量,只有一个头文件声明它(SPOT —单点真相).

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

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

  • 定义变量的源文件还包括要确保定义和声明一致.

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

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

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

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

不太好定义全局变量

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

file10.c

#include "prog2.h";长 l;/* 不要在可移植代码中这样做 */void inc(void) { l++;}

file11.c

#include "prog2.h";长 l;/* 不要在可移植代码中这样做 */void dec(void) { l--;}

file12.c

#include "prog2.h";#include 长 l = 9;/* 不要在可移植代码中这样做 */void put(void) { printf("l = %ld
", l);}

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

<块引用>

J.2 未定义行为

<块引用>

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

<块引用>

§6.9 外部定义¶5

<块引用>

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

<块引用>

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

然而,C 标准也在信息性附录 J 中将其列为常见扩展.

<块引用>

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

prog2.h

extern void dec(void);外部无效放置(无效);外部无效公司(无效);

prog2.c

#include "prog2.h";#include int main(void){公司();放();十二月();放();十二月();放();}

  • prog2 使用 prog2.cfile10.cfile11.cfile12.c, prog2.h.
<小时>

警告

正如这里的评论中所指出的,以及我对类似问题的回答中所述问题,使用多个全局变量的定义会导致未定义的行为(J.2;§6.9),这是标准中表述任何事情都可能发生"的方式.可能发生的事情之一是程序的行为与您一样预计;J.5.11 大约说,你可能更幸运比你应得的".但是一个依赖于 extern 变量的多个定义的程序— 有或没有显式的 'extern' 关键字 — 不是严格意义上的一致的程序,并不能保证在任何地方都能工作.等效地:它包含一个可能会或可能不会出现的错误.

违反指南

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

faulty_header.h

int some_var;/* 不要在标题中这样做!!!*/

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

broken_header.h

int some_var = 13;/* 一个程序中只有一个源文件可以使用这个 */

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

seldom_correct.h

static int hidden_​​global = 3;/* 每个源文件都有自己的副本 */

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

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

<小时>

总结

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

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

原始答案结束

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

<小时>

后期主要添加

避免代码重复

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

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

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

file3a.h

#ifdef DEFINE_VARIABLES#define EXTERN/* 什么都没有 */#别的#define EXTERN 外部#endif/* 定义变量 */EXTERN int global_variable;

file1a.c

#define DEFINE_VARIABLES#include "file3a.h";/* 变量已定义 - 但未初始化 */#include "prog3.h";int increment(void) { return global_variable++;}

file2a.c

#include "file3a.h";#include "prog3.h";#include void use_it(void){printf("全局变量:%d
", global_variable++);}

<小时>

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

prog3.h

extern void use_it(void);外部整数增量(无效);

prog3.c

#include "file3a.h";#include "prog3.h";#include int main(void){用它();global_variable += 19;用它();printf("增量:%d
",增量());返回0;}

  • prog3 使用 prog3.cfile1a.cfile2a.cfile3a.h, prog3.h.
<小时>

变量初始化

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

file3b.h

#ifdef DEFINE_VARIABLES#define EXTERN/* 什么都没有 */#define INITIALIZER(...) = __VA_ARGS__#别的#define EXTERN 外部#define INITIALIZER(...)/* 没有 */#endif/* 定义变量 */EXTERN int global_variable INITIALIZER(37);外部结构{ int a;国际b;}oddball_struct INITIALIZER({ 41, 43 });

反转 #if#else 块的内容,修复由Denis Kniazhev

file1b.c

#define DEFINE_VARIABLES#include "file3b.h";/* 现在定义和初始化变量 */#include "prog4.h";int increment(void) { return global_variable++;}intoddball_value(void){ 返回oddball_struct.a +oddball_struct.b;}

file2b.c

#include "file3b.h";#include "prog4.h";#include void use_them(void){printf("全局变量:%d
", global_variable++);oddball_struct.a += global_variable;oddball_struct.b -= global_variable/2;}

显然,古怪结构的代码不是你通常所想的写,但它说明了这一点.第一个参数对第二个INITIALIZER 的调用是 { 41 和剩余的参数(在这个例子中是单数)是 43 }.没有 C99 或类似的支持对于宏的变量参数列表,需要的初始值设定项包含逗号是非常有问题的.

包含正确的头文件 file3b.h(而不是 fileba.h)Denis Kniazhev

<小时>

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

prog4.h

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

prog4.c

#include "file3b.h";#include "prog4.h";#include int main(void){使用它们();global_variable += 19;使用它们();printf("增量:%d
",增量());printf("Oddball: %d
",oddball_value());返回0;}

  • prog4 使用 prog4.cfile1b.cfile2b.cprog4.h, file3b.h.
<小时>

标题守卫

任何标题都应该被保护以防止重新包含,这样类型定义(枚举、结构或联合类型,或通常的类型定义)不造成问题.标准技术是包裹身体标头保护中的标头,例如:

#ifndef FILE3B_H_INCLUDED#define FILE3B_H_INCLUDED...标题的内容...#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 定义类型(特别是 structoddballoddball_struct 的类型).

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

  • file3c.c 定义全局变量.

  • file4c.c 仅使用全局变量.

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

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

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

此操作的限制是:

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

  2. 在你包含一个应该定义变量的头文件之前,您定义宏 DEFINE_VARIABLES.

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

外部.h

/*** 此标头不得包含标头保护(如  不得).** 每次调用时,都会重新定义宏 EXTERN、INITIALIZE** 基于当前是否定义了宏 DEFINE_VARIABLES.*/#undef 外部#undef 初始化#ifdef DEFINE_VARIABLES#define EXTERN/* 什么都没有 */#define INITIALIZE(...) = __VA_ARGS__#别的#define EXTERN 外部#define INITIALIZE(...)/* 什么都没有 */#endif/* 定义变量 */

file1c.h

#ifndef FILE1C_H_INCLUDED#define FILE1C_H_INCLUDED结构古怪{一个;国际b;};extern void use_them(void);外部整数增量(无效);extern intoddball_value(void);#endif/* FILE1C_H_INCLUDED */

file2c.h

/* 标准序言 */#if 已定义(DEFINE_VARIABLES)&&!defined(FILE2C_H_DEFINITIONS)#undef FILE2C_H_INCLUDED#万一#ifndef FILE2C_H_INCLUDED#define FILE2C_H_INCLUDED#include "external.h";/* 支持宏 EXTERN, INITIALIZE */#include "file1c.h";/* structoddball 的类型定义 */#if !defined(DEFINE_VARIABLES) ||!defined(FILE2C_H_DEFINITIONS)/* 全局变量声明/定义 */EXTERN int global_variable INITIALIZE(37);EXTERN structoddballoddball_struct INITIALIZE({ 41, 43 });#endif/* !DEFINE_VARIABLES ||!FILE2C_H_DEFINITIONS *//* 标准尾声 */#ifdef DEFINE_VARIABLES#define FILE2C_H_DEFINITIONS#endif/* 定义变量 */#endif/* FILE2C_H_INCLUDED */

file3c.c

#define DEFINE_VARIABLES#include "file2c.h";/* 现在定义和初始化变量 */int increment(void) { return global_variable++;}intoddball_value(void){ 返回oddball_struct.a +oddball_struct.b;}

file4c.c

#include "file2c.h";#include void use_them(void){printf("全局变量:%d
", global_variable++);oddball_struct.a += global_variable;oddball_struct.b -= global_variable/2;}

file5c.c

#include "file2c.h";/* 声明变量 */#define DEFINE_VARIABLES#include "file2c.h";/* 现在定义和初始化变量 */int increment(void) { return global_variable++;}intoddball_value(void){ 返回oddball_struct.a +oddball_struct.b;}

file6c.c

#define DEFINE_VARIABLES#include "file2c.h";/* 现在定义和初始化变量 */#include "file2c.h";/* 声明变量 */int increment(void) { return global_variable++;}intoddball_value(void){ 返回oddball_struct.a +oddball_struct.b;}

<小时>

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

prog5.c

#include "file2c.h";#include int main(void){使用它们();global_variable += 19;使用它们();printf("增量:%d
",增量());printf("Oddball: %d
",oddball_value());返回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

/* 标准序言 */#if 已定义(DEFINE_VARIABLES)&&!defined(FILE2D_H_DEFINITIONS)#undef FILE2D_H_INCLUDED#万一#ifndef FILE2D_H_INCLUDED#define FILE2D_H_INCLUDED#include "external.h";/* 支持宏 EXTERN, INITIALIZE */#include "file1c.h";/* structoddball 的类型定义 */#if !defined(DEFINE_VARIABLES) ||!defined(FILE2D_H_DEFINITIONS)/* 全局变量声明/定义 */EXTERN int global_variable INITIALIZE(37);EXTERN structoddballoddball_struct INITIALIZE({ 41, 43 });#endif/* !DEFINE_VARIABLES ||!FILE2D_H_DEFINITIONS *//* 标准尾声 */#ifdef DEFINE_VARIABLES#define FILE2D_H_DEFINITIONS#undef DEFINE_VARIABLES#endif/* 定义变量 */#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

/*** 此标头不得包含标头保护(如  不得).** 每次包含时,宏 HEADER_DEFINING_VARIABLES 应该** 用名称(在引号中 - 或可能是尖括号中)定义** 要包含的头文件,当宏定义变量时** DEFINE_VARIABLES 已定义.另见:external.h(使用** DEFINE_VARIABLES 并定义宏 EXTERN 和 INITIALIZE** 适当).**** #define HEADER_DEFINING_VARIABLES "file2c.h";** #include "externdef.h";*/#if 定义(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

/* 声明变量 */#include "file2d.h";/* 定义变量 */#define HEADER_DEFINING_VARIABLES "file2d.h";#include "externdef.h";/* 声明变量 - 再次 */#include "file2d.h";/* 定义变量 - 再次 */#define HEADER_DEFINING_VARIABLES "file2d.h";#include "externdef.h";int increment(void) { return global_variable++;}intoddball_value(void){ 返回oddball_struct.a +oddball_struct.b;}

file8c.h

/* 标准序言 */#if 已定义(DEFINE_VARIABLES)&&!defined(FILE8C_H_DEFINITIONS)#undef FILE8C_H_INCLUDED#万一#ifndef FILE8C_H_INCLUDED#define FILE8C_H_INCLUDED#include "external.h";/* 支持宏 EXTERN, INITIALIZE */#include "file2d.h";/* 结构古怪 */#if !defined(DEFINE_VARIABLES) ||!defined(FILE8C_H_DEFINITIONS)/* 全局变量声明/定义 */EXTERN struct oddball another INITIALIZE({ 14, 34 });#endif/* !DEFINE_VARIABLES ||!FILE8C_H_DEFINITIONS *//* 标准尾声 */#ifdef DEFINE_VARIABLES#define FILE8C_H_DEFINITIONS#endif/* 定义变量 */#endif/* FILE8C_H_INCLUDED */

file8c.c

/* 定义变量 */#define HEADER_DEFINING_VARIABLES "file2d.h";#include "externdef.h";/* 定义变量 */#define HEADER_DEFINING_VARIABLES "file8c.h";#include "externdef.h";int increment(void) { return global_variable++;}intoddball_value(void){ 返回oddball_struct.a +oddball_struct.b;}

<小时>

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

prog8.c

#include "file2d.h";#include int main(void){使用它们();global_variable += 19;使用它们();printf("增量:%d
",增量());printf("Oddball: %d
",oddball_value());返回0;}

file9c.c

#include "file2d.h";#include void use_them(void){printf("全局变量:%d
", global_variable++);oddball_struct.a += global_variable;oddball_struct.b -= global_variable/2;}

  • prog8 使用 prog8.cfile7c.cfile9c.c.

  • prog9 使用 prog8.cfile8c.cfile9c.c.

<小时>

然而,这些问题在实践中相对不太可能发生,尤其是如果您将标准建议用于

避免使用全局变量

<小时>

这次展览有遗漏什么吗?

_Confession_:此处概述的避免重复代码"方案是开发是因为这个问题影响了我处理的一些代码(但不拥有),并且是对第一部分中概述的计划的一个小问题the answer. However, the original scheme leaves you with just twoplaces to modify to keep variable definitions and declarationssynchronized, which is a big step forward over having exernal variabledeclarations scattered throughout the code base (which really matterswhen there are thousands of files in total). However, the code in thefiles with the names `fileNc.[ch]` (plus `external.h` and `externdef.h`)shows that it can be made to work. Clearly, it would not be hard tocreate a header generator script to give you the standardized templatefor a variable defining and declaring header file.

NB These are toy programs with just barely enough code to make themmarginally interesting. There is repetition within the examples thatcould be removed, but isn't to simplify the pedagogical explanation.(For example: the difference between prog5.c and prog8.c is the nameof one of the headers that are included. It would be possible toreorganize the code so that the main() function was not repeated, butit would conceal more than it revealed.)

I know that global variables in C sometimes have the extern keyword. What is an extern variable? What is the declaration like? What is its scope?

This is related to sharing variables across source files, but how does that work precisely? Where do I use extern?

解决方案

Using extern is only of relevance when the program you're building consists of multiple source files linked together, where some of the variables defined, for example, in source file file1.c need to be referenced in other source files, such as file2.c.

It is important to understand the difference between defining a variable and declaring a variable:

  • A variable is declared when the compiler is informed that a variable exists (and this is its type); it does not allocate the storage for the variable at that point.

  • A variable is defined when the compiler allocates the storage for the variable.

You may declare a variable multiple times (though once is sufficient); you may only define it once within a given scope. A variable definition is also a declaration, but not all variable declarations are definitions.

Best way to declare and define global variables

The clean, reliable way to declare and define global variables is to use a header file to contain an extern declaration of the variable.

The header is included by the one source file that defines the variable and by all the source files that reference the variable. For each program, one source file (and only one source file) defines the variable. Similarly, one header file (and only one header file) should declare the variable. The header file is crucial; it enables cross-checking between independent TUs (translation units — think source files) and ensures consistency.

Although there are other ways of doing it, this method is simple and reliable. It is demonstrated by file3.h, file1.c and file2.c:

file3.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
", global_variable++);
}

That's the best way to declare and define global variables.


The next two files complete the source for prog1:

The complete programs shown use functions, so function declarations have crept in. Both C99 and C11 require functions to be declared or defined before they are used (whereas C90 did not, for good reasons). I use the keyword extern in front of function declarations in headers for consistency — to match the extern in front of variable declarations in headers. Many people prefer not to use extern in front of function declarations; the compiler doesn't care — and ultimately, neither do I as long as you're consistent, at least within a source file.

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
", increment());
    return 0;
}

  • prog1 uses prog1.c, file1.c, file2.c, file3.h and prog1.h.

The file prog1.mk is a makefile for prog1 only. It will work with most versions of make produced since about the turn of the millennium. It is not tied specifically to 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}


Guidelines

Rules to be broken by experts only, and only with good reason:

  • A header file only contains extern declarations of variables — never static or unqualified variable definitions.

  • For any given variable, only one header file declares it (SPOT — Single Point of Truth).

  • A source file never contains extern declarations of variables — source files always include the (sole) header that declares them.

  • For any given variable, exactly one source file defines the variable, preferably initializing it too. (Although there is no need to initialize explicitly to zero, it does no harm and can do some good, because there can be only one initialized definition of a particular global variable in a program).

  • The source file that defines the variable also includes the header to ensure that the definition and the declaration are consistent.

  • A function should never need to declare a variable using extern.

  • Avoid global variables whenever possible — use functions instead.

The source code and text of this answer are available in my SOQ (Stack Overflow Questions) repository on GitHub in the src/so-0143-3204 sub-directory.

If you're not an experienced C programmer, you could (and perhaps should) stop reading here.

Not so good way to define global variables

With some (indeed, many) C compilers, you can get away with what's called a 'common' definition of a variable too. 'Common', here, refers to a technique used in Fortran for sharing variables between source files, using a (possibly named) COMMON block. What happens here is that each of a number of files provides a tentative definition of the variable. As long as no more than one file provides an initialized definition, then the various files end up sharing a common single definition of the variable:

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
", l); }

This technique does not conform to the letter of the C standard and the 'one definition rule' — it is officially undefined behaviour:

J.2 Undefined behavior

An identifier with external linkage is used, but in the program there does not exist exactly one external definition for the identifier, or the identifier is not used and there exist multiple external definitions for the identifier (6.9).

§6.9 External definitions ¶5

An external definition is an external declaration that is also a definition of a function (other than an inline definition) or an object. If an identifier declared with external linkage is used in an expression (other than as part of the operand of a sizeof or _Alignof operator whose result is an integer constant), somewhere in the entire program there shall be exactly one external definition for the identifier; otherwise, there shall be no more than one.161)

161) Thus, if an identifier declared with external linkage is not used in an expression, there need be no external definition for it.

However, the C standard also lists it in informative Annex J as one of the Common extensions.

J.5.11 Multiple external definitions

There may be more than one external definition for the identifier of an object, with or without the explicit use of the keyword extern; if the definitions disagree, or more than one is initialized, the behavior is undefined (6.9.2).

Because this technique is not always supported, it is best to avoid using it, especially if your code needs to be portable. Using this technique, you can also end up with unintentional type punning.

If one of the files above declared l as a double instead of as a long, C's type-unsafe linkers probably would not spot the mismatch. If you're on a machine with 64-bit long and double, you'd not even get a warning; on a machine with 32-bit long and 64-bit double, you'd probably get a warning about the different sizes — the linker would use the largest size, exactly as a Fortran program would take the largest size of any common blocks.

Note that GCC 10.1.0, which was released on 2020-05-07, changes the default compilation options to use -fno-common, which means that by default, the code above no longer links unless you override the default with -fcommon (or use attributes, etc — see the link).


The next two files complete the source for 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 uses prog2.c, file10.c, file11.c, file12.c, prog2.h.

Warning

As noted in comments here, and as stated in my answer to a similar question, using multiple definitions for a global variable leads to undefined behaviour (J.2; §6.9), which is the standard's way of saying "anything could happen". One of the things that can happen is that the program behaves as you expect; and J.5.11 says, approximately, "you might be lucky more often than you deserve". But a program that relies on multiple definitions of an extern variable — with or without the explicit 'extern' keyword — is not a strictly conforming program and not guaranteed to work everywhere. Equivalently: it contains a bug which may or may not show itself.

Violating the guidelines

There are, of course, many ways in which these guidelines can be broken. Occasionally, there may be a good reason to break the guidelines, but such occasions are extremely unusual.

faulty_header.h

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

Note 1: if the header defines the variable without the extern keyword, then each file that includes the header creates a tentative definition of the variable. As noted previously, this will often work, but the C standard does not guarantee that it will work.

broken_header.h

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

Note 2: if the header defines and initializes the variable, then only one source file in a given program can use the header. Since headers are primarily for sharing information, it is a bit silly to create one that can only be used once.

seldom_correct.h

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

Note 3: if the header defines a static variable (with or without initialization), then each source file ends up with its own private version of the 'global' variable.

If the variable is actually a complex array, for example, this can lead to extreme duplication of code. It can, very occasionally, be a sensible way to achieve some effect, but that is very unusual.


Summary

Use the header technique I showed first. It works reliably and everywhere. Note, in particular, that the header declaring the global_variable is included in every file that uses it — including the one that defines it. This ensures that everything is self-consistent.

Similar concerns arise with declaring and defining functions — analogous rules apply. But the question was about variables specifically, so I've kept the answer to variables only.

End of Original Answer

If you're not an experienced C programmer, you probably should stop reading here.


Late Major Addition

Avoiding Code Duplication

One concern that is sometimes (and legitimately) raised about the 'declarations in headers, definitions in source' mechanism described here is that there are two files to be kept synchronized — the header and the source. This is usually followed up with an observation that a macro can be used so that the header serves double duty — normally declaring the variables, but when a specific macro is set before the header is included, it defines the variables instead.

Another concern can be that the variables need to be defined in each of a number of 'main programs'. This is normally a spurious concern; you can simply introduce a C source file to define the variables and link the object file produced with each of the programs.

A typical scheme works like this, using the original global variable illustrated in 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
", global_variable++);
}


The next two files complete the source for 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
", increment());
    return 0;
}

  • prog3 uses prog3.c, file1a.c, file2a.c, file3a.h, prog3.h.

Variable initialization

The problem with this scheme as shown is that it does not provide for initialization of the global variable. With C99 or C11 and variable argument lists for macros, you could define a macro to support initialization too. (With C89 and no support for variable argument lists in macros, there is no easy way to handle arbitrarily long initializers.)

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

Reverse contents of #if and #else blocks, fixing bug identified by 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
", global_variable++);
    oddball_struct.a += global_variable;
    oddball_struct.b -= global_variable / 2;
}

Clearly, the code for the oddball structure is not what you'd normally write, but it illustrates the point. The first argument to the second invocation of INITIALIZER is { 41 and the remaining argument (singular in this example) is 43 }. Without C99 or similar support for variable argument lists for macros, initializers that need to contain commas are very problematic.

Correct header file3b.h included (instead of fileba.h) per Denis Kniazhev


The next two files complete the source for 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
", increment());
    printf("Oddball:   %d
", oddball_value());
    return 0;
}

  • prog4 uses prog4.c, file1b.c, file2b.c, prog4.h, file3b.h.

Header Guards

Any header should be protected against reinclusion, so that type definitions (enum, struct or union types, or typedefs generally) do not cause problems. The standard technique is to wrap the body of the header in a header guard such as:

#ifndef FILE3B_H_INCLUDED
#define FILE3B_H_INCLUDED

...contents of header...

#endif /* FILE3B_H_INCLUDED */

The header might be included twice indirectly. For example, if file4b.h includes file3b.h for a type definition that isn't shown, and file1b.c needs to use both header file4b.h and file3b.h, then you have some more tricky issues to resolve. Clearly, you might revise the header list to include just file4b.h. However, you might not be aware of the internal dependencies — and the code should, ideally, continue to work.

Further, it starts to get tricky because you might include file4b.h before including file3b.h to generate the definitions, but the normal header guards on file3b.h would prevent the header being reincluded.

So, you need to include the body of file3b.h at most once for declarations, and at most once for definitions, but you might need both in a single translation unit (TU — a combination of a source file and the headers it uses).

Multiple inclusion with variable definitions

However, it can be done subject to a not too unreasonable constraint. Let's introduce a new set of file names:

  • external.h for the EXTERN macro definitions, etc.

  • file1c.h to define types (notably, struct oddball, the type of oddball_struct).

  • file2c.h to define or declare the global variables.

  • file3c.c which defines the global variables.

  • file4c.c which simply uses the global variables.

  • file5c.c which shows that you can declare and then define the global variables.

  • file6c.c which shows that you can define and then (attempt to) declare the global variables.

In these examples, file5c.c and file6c.c directly include the header file2c.h several times, but that is the simplest way to show that the mechanism works. It means that if the header was indirectly included twice, it would also be safe.

The restrictions for this to work are:

  1. The header defining or declaring the global variables may not itself define any types.

  2. Immediately before you include a header that should define variables, you define the macro DEFINE_VARIABLES.

  3. The header defining or declaring the variables has stylized contents.

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


The next source file completes the source (provides a main program) for prog5, prog6 and prog7:

prog5.c

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

int main(void)
{
    use_them();
    global_variable += 19;
    use_them();
    printf("Increment: %d
", increment());
    printf("Oddball:   %d
", oddball_value());
    return 0;
}

  • prog5 uses prog5.c, file3c.c, file4c.c, file1c.h, file2c.h, external.h.

  • prog6 uses prog5.c, file5c.c, file4c.c, file1c.h, file2c.h, external.h.

  • prog7 uses prog5.c, file6c.c, file4c.c, file1c.h, file2c.h, external.h.


This scheme avoids most problems. You only run into a problem if a header that defines variables (such as file2c.h) is included by another header (say file7c.h) that defines variables. There isn't an easy way around that other than "don't do it".

You can partially work around the problem by revising file2c.h into 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 */

The issue becomes 'should the header include #undef DEFINE_VARIABLES?' If you omit that from the header and wrap any defining invocation with #define and #undef:

#define DEFINE_VARIABLES
#include "file2c.h"
#undef DEFINE_VARIABLES

in the source code (so the headers never alter the value of DEFINE_VARIABLES), then you should be clean. It is just a nuisance to have to remember to write the the extra line. An alternative might be:

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

This is getting a tad convoluted, but seems to be secure (using the file2d.h, with no #undef DEFINE_VARIABLES in the file2d.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; }

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


The next two files complete the source for prog8 and prog9:

prog8.c

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

int main(void)
{
    use_them();
    global_variable += 19;
    use_them();
    printf("Increment: %d
", increment());
    printf("Oddball:   %d
", oddball_value());
    return 0;
}

file9c.c

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

void use_them(void)
{
    printf("Global variable: %d
", global_variable++);
    oddball_struct.a += global_variable;
    oddball_struct.b -= global_variable / 2;
}

  • prog8 uses prog8.c, file7c.c, file9c.c.

  • prog9 uses prog8.c, file8c.c, file9c.c.


However, the problems are relatively unlikely to occur in practice, especially if you take the standard advice to

Avoid global variables


Does this exposition miss anything?

_Confession_: The 'avoiding duplicated code' scheme outlined here was developed because the issue affects some code I work on (but don't own), and is a niggling concern with the scheme outlined in the first part of the answer. However, the original scheme leaves you with just two places to modify to keep variable definitions and declarations synchronized, which is a big step forward over having exernal variable declarations scattered throughout the code base (which really matters when there are thousands of files in total). However, the code in the files with the names `fileNc.[ch]` (plus `external.h` and `externdef.h`) shows that it can be made to work. Clearly, it would not be hard to create a header generator script to give you the standardized template for a variable defining and declaring header file.

NB These are toy programs with just barely enough code to make them marginally interesting. There is repetition within the examples that could be removed, but isn't to simplify the pedagogical explanation. (For example: the difference between prog5.c and prog8.c is the name of one of the headers that are included. It would be possible to reorganize the code so that the main() function was not repeated, but it would conceal more than it revealed.)

这篇关于如何使用 extern 在源文件之间共享变量?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆