找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2524

积分

0

好友

330

主题
发表于 12 小时前 | 查看: 0| 回复: 0

在嵌入式或C语言开发中,我们常常会遇到 extern 这个关键字。它的字面意思是“外部的”,用来修饰变量,表示该变量在别处定义。那么,这个“外部”究竟指向哪里?什么情况下必须使用 extern?为什么函数通常又不需要它呢?

1. 变量的声明与定义

在软件开发中,我们通常在IDE中创建工程,并将源代码写入 .c 文件。程序一般从 main 函数开始执行,因此我们把包含 main 函数的文件命名为 main.c。假设所有代码都写在这个文件里:

main.c初始代码截图,包含LED点亮和IO状态读取逻辑

main.c 初始代码示例

如上图所示,这段代码的功能是通过IO口 P2_0 输出高电平来点亮一个LED灯,然后通过全局变量 IO_Status 反馈这个IO口的电平状态。

然而,直接编译这段代码会出错,因为编译器找不到 IO_Status 变量的定义,会提示其为未定义的标识符。

编译错误截图:提示‘IO_Status’未定义

编译错误:未定义标识符

这个错误与编译器的工作原理有关。编译过程首先进行预处理,例如展开头文件 #include <REGX52.H>。之后,从 main 函数的第一行开始编译。代码中的 P2_0 在头文件 REGX52.H 中已有定义,所以语句 P2_0=1; 可以顺利编译。

但当编译到 IO_Status=P2_0; 时,编译器在当前作用域内找不到变量 IO_Status 的定义,因此报错。

解决这个问题的方法是将变量的定义放在调用它的函数之前,也就是 main() 函数之前。

变量定义前置后的代码截图

变量定义前置

如上图所示,把变量 IO_Status 的定义移到 main() 函数上方后,再次编译即可顺利通过。

这个示例只有一个 main.c 文件,全局变量也只在这个文件中使用。但在实际的复杂项目中,通常采用模块化编程,不同的功能模块放在不同的 .c 文件中。那么,main.c 文件如何访问其他 .c 文件中定义的全局变量呢?

1.1 变量的extern声明

上面的示例调用的是同一个文件内的全局变量。假如我们将代码模块化,新增一个 LED.c 文件,而 IO_Status 变量是在这个新文件中定义的。

LED.c文件内容截图,仅包含一行变量定义

LED.c 文件内容

如上图所示,LED.c 文件中定义了 unsigned char IO_Status;。此时,main.c 文件中的函数想使用这个变量该怎么办?

如果 main.c 不对这个变量做任何说明,编译时必定会报“未定义”的错误。那么,如果在 main.c 中提前“声明”一下这个变量是否可行呢?

main.c中尝试前置声明变量导致报错的截图

main.c 中尝试前置声明变量

如上图所示,如果在 main.c 中也写上 unsigned char IO_Status;,编译时会报出“重复定义”的错误。

编译错误截图:提示‘IO_STATUS’重复定义

编译错误:重复定义

这是为什么呢?原因在于 unsigned char IO_Status; 这条语句本身既是声明也是定义。或者说,对于全局变量和数组,其默认属性就是定义

编译器在 main.c 中检测到一次定义,在 LED.c 中又检测到一次定义,一共两次定义,因此链接器(Linker)报错。这时,就需要 extern 关键字登场了。

main.c中使用extern声明外部变量的截图

使用 extern 声明外部变量

如上图所示,在 main.c 中,在 unsigned char IO_Status; 前面加上 extern 关键字,编译器就会明白这只是一个声明,其定义位于其他文件中,编译链接就能顺利通过。

2. 函数的声明与默认链接属性

前面的例子为了说明方便,没有完全模块化。更合理的做法是将点亮LED的动作 P2_0=1; 也封装到 LED.c 文件中。

LED.c文件中封装LED_ON函数的截图

LED.c 中的函数示例

如上图所示,我们将点亮LED的功能封装为 LED_ON() 函数,并通过 return 语句返回IO状态。然后在 main() 函数中调用这个函数。

main.c中调用LED_ON函数但未声明的截图

main.c 中调用函数但未声明

如上图所示,main() 函数调用 LED_ON() 并将其返回值赋给 IO_Status。这里的 IO_Statusmain.c 中定义的全局变量,与 LED.c 无关,因此它不需要 extern

但是,如果在 main.c 中不事先声明 LED_ON() 函数,编译时可能会产生“找不到函数原型”的警告(或错误,取决于编译器标准)。

编译警告截图:提示‘LED_ON’缺少函数原型

编译警告:缺少函数原型

注意,这里可能只是警告而非错误,是因为有些编译器为了兼容早期的C语言C89标准。C89标准允许“隐式函数声明”,即遇到未声明的函数调用时,编译器会假设该函数返回 int 类型。

但如果编译器严格执行更新的C99等标准,“隐式函数声明”已被废弃,使用未声明的函数将导致编译错误。无论警告还是错误,这种编程习惯都是不安全且不推荐的。解决方法就是加上函数声明。

main.c中使用extern声明外部函数的截图

使用 extern 声明外部函数

我们在 main.c 中加上声明 extern unsigned char LED_ON();,编译即可顺利通过。看到这里,你可能会觉得函数的外部声明方法和变量是一样的。接下来,我们尝试把函数声明前的 extern 去掉。

main.c中去掉extern关键字声明函数的截图

去掉 extern 关键字声明函数

如上图所示,去掉 extern 后,编译仍然顺利通过,没有警告和错误。这是为什么?

2.1 默认链接属性(External Linkage)

C语言规定,所有函数声明默认具有“外部链接”属性。这意味着,函数声明时不加 extern 关键字,也默认可以被其他源文件(.c 文件)访问和调用。

因此,在函数声明前面加 extern 是语法允许但功能冗余的,它的唯一作用是显式地表明这种默认行为,使意图更直观。不加 extern 会让代码看起来更简洁。

那么,为什么全局变量的声明不能像函数一样,也默认为“外部链接”呢?

关键在于,这个默认外部链接属性是针对“声明”而言的,不能是“定义”。声明和定义的处理方式截然不同:声明只是告诉编译器存在这个符号;而定义则要求编译器立即为其分配内存并生成代码。

函数可以清晰地区分声明和定义:

  • 函数声明 以分号 ; 结尾。
  • 函数定义 以花括号 {} 结尾。
    编译器能通过这些语法规则明确识别。

但全局变量则不然。语句 int x; 既可以作为声明(如果 x 在其他地方定义),也可以作为定义(如果这是第一次出现)。编译器在单文件编译阶段无法仅从语法上区分其意图。

因此,对于全局变量,必须加上 extern 关键字,编译器才能明确知道这是一个声明,而非定义。这也说明了全局变量的默认链接属性是“内部链接”,只能在本文件内使用。若需跨文件访问,则必须用 extern 进行显式声明。

另外需要说明的是,只有普通函数需要声明,main 函数作为程序入口点,其定义本身隐含了声明的作用,无需额外前置声明。

3. 总结

对于全局变量(包括数组),int var; 这样的语句既是声明也是定义,会分配存储空间。如果在多个源文件中出现相同的定义,链接时就会报“重复定义”错误。因此,当需要在其他文件中引用该变量时,必须使用 extern int var; 来声明。

而函数的声明与定义有清晰的语法区别,且C语言默认函数的链接属性就是 extern(外部链接)。因此,函数声明时不需要再加 extern,当然加上也不会有错,只是显得冗余。

简而言之,全局变量需要 extern 关键字来帮助编译器明确区分“声明”与“定义”,而函数的声明与定义天生易区分,因此无需借助 extern 来表明外部链接属性。理解这一区别,有助于写出更清晰、健壮的模块化C语言代码。对于这类编译与链接的底层原理,开发者可以在云栈社区的相应板块找到更多深入的讨论和资源。




上一篇:使用 Biome GritQL 自定义规则:阻断 RHF 性能隐患的 3 个关键策略
下一篇:OpenAI年营收200亿美元仍测试广告?大模型盈利与用户体验的平衡难题
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-26 18:42 , Processed in 0.255149 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表