5、extern 关键字详解:声明、函数与多文件共享

extern 这个关键字,说白了就是 C 语言里的「外交官」。它负责在不同文件之间传递信息。我刚开始学 C 时,总觉得它可有可无。直到第一次做多文件项目,变量死活链接不上,编译报错一堆红色。嗯,从那以后,我再也不敢小看 extern 了。

5.1 extern 声明外部变量

先说说最基础也最常用的场景——声明外部变量。

你想想看,一个项目里通常有多个 .c 文件。比如 main.c 里定义了一个全局变量 int g_counter = 0;,那其他文件怎么访问它?直接写 extern int g_counter; 就行。

这里有个关键点:声明和定义是两码事

  • 定义:分配内存,变量真正存在。比如 int g_counter = 0;
  • 声明:告诉编译器「这个变量在其他地方定义了,你别报错」。比如 extern int g_counter;

核心原则:变量只能定义一次,但可以声明多次。

我在项目中遇到过一个问题:团队里有人把全局变量定义在了头文件里,结果多个 .c 文件都包含了这个头文件,链接时直接报「重复定义」。这就是典型的把定义当声明用了。

// file: globals.h
#ifndef GLOBALS_H
#define GLOBALS_H

extern int g_system_tick;   // ✅ 正确:这是声明
// int g_system_tick;       // ❌ 错误:这是定义,放在头文件里会出问题

#endif

// file: globals.c
#include "globals.h"
int g_system_tick = 0;      // ✅ 正确:这是定义,只出现一次

// file: main.c
#include "globals.h"
void main() {
    g_system_tick = 100;    // 通过 extern 声明,可以正常访问
}

5.2 extern 声明外部函数

函数默认就是 extern 的。你没看错,C 语言里所有函数默认都是外部可见的。

比如你在 utils.c 里写了一个 int add(int a, int b) { return a + b; },在 main.c 里直接调用 add(3, 5) 就能编译通过。因为编译器默认函数声明就是 extern int add(int, int);

但我的个人习惯是:显式写出 extern。虽然不写也能用,但显式写出来能让读代码的人一眼就明白「这个函数来自外部文件」。代码的可读性往往体现在这些细节上。

// file: utils.h
#ifndef UTILS_H
#define UTILS_H

extern int add(int a, int b);   // 显式写出 extern,更清晰
extern void print_result(int val);

#endif

// file: utils.c
#include "utils.h"

int add(int a, int b) {
    return a + b;
}

void print_result(int val) {
    printf("Result: %d\n", val);
}

小技巧:如果你想让某个函数只在当前文件内部使用,用 static 关键字。static 函数对外部文件不可见,能有效避免命名冲突。

5.3 extern "C" 的作用

这个知识点,说白了就是 C 和 C++ 之间的「翻译官」。

C++ 支持函数重载,所以编译器在编译时会对函数名进行「名字修饰」(name mangling)。比如 int add(int, int) 在 C++ 编译器内部可能被改成 _Z3addii。但 C 语言没有重载,函数名就是原名。

如果你在 C++ 代码里调用 C 语言写的库,直接链接会找不到函数——因为 C++ 编译器找的是修饰后的名字,而 C 库导出的是原名。

这时候 extern "C" 就派上用场了。它告诉 C++ 编译器:「这段代码按 C 的方式编译和链接,别做名字修饰」。

// file: c_library.h
#ifdef __cplusplus
extern "C" {
#endif

int c_function(int x);
void c_void_function(void);

#ifdef __cplusplus
}
#endif

// file: c_library.c
int c_function(int x) {
    return x * 2;
}

void c_void_function(void) {
    // do something
}

// file: main.cpp
#include "c_library.h"

int main() {
    int result = c_function(10);  // 正常调用 C 函数
    c_void_function();
    return 0;
}

我曾经踩过的坑:在嵌入式项目中,很多底层驱动库是用 C 写的。如果主程序用 C++ 开发,忘了加 extern "C",链接时就会报 undefined reference。排查了半天才发现是名字修饰的问题。所以我现在写 C 库的头文件,都会习惯性地加上 #ifdef __cplusplus 保护。

5.4 多文件共享全局变量

这是 extern 最经典的应用场景。一个项目里,多个文件需要访问同一个全局变量。

我建议的规范做法是:

  1. 在某个 .c 文件中定义全局变量(只定义一次)
  2. 在对应的 .h 文件中用 extern 声明
  3. 其他需要访问该变量的 .c 文件,包含这个 .h 文件即可
// file: system.h
#ifndef SYSTEM_H
#define SYSTEM_H

extern volatile uint32_t g_sys_tick;   // 声明
extern uint8_t g_system_status;

#endif

// file: system.c
#include "system.h"

volatile uint32_t g_sys_tick = 0;      // 定义
uint8_t g_system_status = 0;

// file: timer.c
#include "system.h"

void timer_isr(void) {
    g_sys_tick++;                       // 访问外部变量
}

// file: main.c
#include "system.h"

int main(void) {
    while(1) {
        if(g_sys_tick >= 1000) {
            g_system_status = 1;
        }
    }
}

避坑指南:我曾经在项目里看到有人把全局变量定义在头文件里,然后用 #ifndef 防止重复包含。这种做法看似聪明,实际上隐患很大——如果两个 .c 文件分别编译,每个文件都会生成一份变量副本,链接时不会报错,但运行时它们指向的是不同的内存地址。调试这种 bug 能让人崩溃。

知识体系总览

下面这张图,我把 extern 的核心知识点串起来了。你可以把它当作一个快速索引。

extern 关键字知识体系 extern 关键字 声明外部变量 只声明不定义 避免重复定义 声明外部函数 函数默认 extern 显式声明更清晰 extern "C":C/C++ 互调 禁止名字修饰 兼容 C 库函数

这张图把 extern 的三个核心用法都列出来了。左边是外部变量,中间是外部函数,右边是 C/C++ 互调。每个分支下面还有具体的注意事项。你可以把它截图保存,写代码时对照着看。

好了,关于 extern 的内容就这些。记住一句话:extern 是连接多文件的桥梁,用好了能让代码结构清晰,用不好就是链接错误的噩梦


公众号:蓝海资料掘金营,微信deep3321