文章

关键字 Extern

关键字 Extern

[TOC]

1.extern概述

在C++中,extern 关键字用于指定变量或函数的定义在另一个文件中。这样做的目的主要是为了在多个文件之间共享全局变量或者函数

以下是 extern 关键字的一些主要用途:1

  1. 链接不同文件中的全局变量extern 用于声明一个全局变量而不定义它。实际的定义将在程序的另一个文件中给出。这允许多个文件访问同一个变量。
  2. 函数声明:在C++中,所有函数默认都是 extern 的。这意味着你可以在另外一个文件中定义一个函数,并且在其他文件中声明并使用它,而不需要显式地使用 extern 关键字。然而,在C语言中,使用 extern 可以提高代码的可读性。
  3. 解决名称冲突:当你使用多个库时,可能会有名称冲突的问题。extern "C" 用来告诉C++编译器按照C语言的规则进行链接,而不是C++的规则。这通常用在包含C库头文件的时候,确保C++能够链接那些设计为C语言的函数。

2. 全局变量使用extern

需求:有两个源文件file1.cppfile2.cpp。在 file1.cpp 中有一个全局变量 int g_var,想在 file2.cpp 中也能访问该变量。

file1.cpp 中,定义这个变量:

1
2
// file1.cpp
int g_var = 42; // 定义

file2.cpp 中,使用 extern 来声明这个变量:

1
2
3
4
5
6
// file2.cpp
extern int g_var; // 声明

void func() {
    // 你现在可以在这个文件中使用 g_var 了
}

通过这种方式,g_var 成为一个连接两个文件的桥梁,file1.cpp 拥有其定义,而 file2.cpp 只是声明它,告诉编译器它的定义在别的地方。

请注意,也可以在同一个文件中使用 extern 来声明变量,然后在文件的后面部分提供定义,但这不是它的常见用法。

3. 全局函数使用extern

当一个函数在一个文件中定义,而你想在另一个文件中调用它时,可以使用 extern 关键字来声明这个函数。

这样做可以告诉编译器这个函数的定义存在于程序的其他地方

这样的组织方式可以让你的代码更加模块化,使得各个部分更容易编写、理解和维护。

3.1 C语言中

需求:假设你有两个源文件:main.cpputilities.cpp。在 utilities.cpp 中定义了一个函数 doSomething,你想在 main.cpp 中调用它。

utilities.cpp

1
2
3
4
5
6
// utilities.cpp

// 这里是函数的定义
void doSomething() {
    // 函数做一些事情
}

main.cpp 中,可以这样声明并调用 doSomething

1
2
3
4
5
6
7
8
9
10
// main.cpp

// 使用extern关键字声明函数
extern void doSomething();

int main() {
    // 调用在其他文件中定义的函数
    doSomething();
    return 0;
}

main.cpp 中,extern 关键字告诉编译器 doSomething 的定义虽然不在当前的源文件中,但它存在于程序的其它部分,并且链接器在处理程序的所有文件时会找到它的定义。因此,编译器不需要在 main.cpp 文件中找到 doSomething 函数的具体实现,只需要知道它的签名就足够了。

在上面的示例中,对于C++编译器,实际上不需要在 main.cpp 中显式地使用 extern,因为在C++中,函数默认就是外部链接的(extern)。不过,某些情况下(比如你的项目设置中有特定的编译器标志),明确地写出 extern 可以避免潜在的链接错误。

3.2 C++中

在C++中,通常,函数的声明会放在头文件中,而定义(实现)会放在源文件(.cpp文件)中。使用头文件可以让函数的声明在多个源文件中被重用,这样就不需要在每个源文件中都用extern声明一次相同的函数。

例如,假设有一个函数doSomething,需要在各处调用:

utilities.h

1
2
3
4
5
6
7
8
// 这是头文件
#ifndef UTILITIES_H
#define UTILITIES_H

// 函数声明,不需要extern,因为在C++中函数默认具有外部链接
void doSomething();

#endif // UTILITIES_H

utilities.cpp

1
2
3
4
5
6
// 这是定义函数的源文件
#include "utilities.h"

void doSomething() {
    // 实现函数
}

main.cpp

1
2
3
4
5
6
7
// 主源文件
#include "utilities.h"

int main() {
    doSomething(); // 调用在其他文件中定义的函数
    return 0;
}

在这种组织方式下,utilities.h头文件中的声明告诉编译器函数的签名,utilities.cpp包含该函数的定义(实现),而main.cpp通过包含utilities.h来知道函数的存在。当编译器编译main.cpp时,它会在utilities.h中找到doSomething的声明,然后在链接程序的时候,链接器会在utilities.cpp编译生成的目标文件中找到该函数的定义。

这样,你就可以避免在每个需要调用该函数的源文件中都重复extern声明。头文件的方式是更加清晰和可维护的方式来共享在多个源文件中使用的函数声明。

4. extern “C”

extern "C" 是由C++语言提供的一个关键字,用于指示编译器以C语言的方式来处理特定的代码部分,主要涉及函数的链接(Linkage)。

extern "C" 可以确保在C++代码中声明的函数或在C++库中的函数,被当作C语言函数来处理,这对于混合编程(C与C++混用)和跨语言接口(如C++库被C或其他语言调用)非常重要。它确保了函数名在链接时不会被C++编译器改变,从而使不同编程语言之间的接口调用成为可能。2

4.1 C++代码调用C语言代码

假设我们有一个C语言编写的库,包含一个函数c_function,我们想在C++代码中调用它。

C语言库 (c_library.h)

1
2
/* C语言头文件 */
void c_function();

在C++中直接包含这个头文件会导致链接问题,因为C++编译器会对函数名进行名字修饰(Name mangling,是编译器解决程序实体名字唯一性问题的技术)。为了解决这个问题,我们使用extern "C"

C++代码

extern "C" {
    #include "c_library.h"
}

int main() {
    c_function();  // 调用C语言函数
    return 0;
}

在这个例子中,extern "C"告诉C++编译器,c_function是用C语言的链接方式编译的,因此不应该对其名字进行名字修饰。

4.2 C语言调用C++代码

假设我们有一个C++库,里面有一个函数cpp_function,我们想在C代码中调用它。

C++库 (cpp_library.h)

1
2
3
4
5
6
7
8
9
#ifdef __cplusplus
extern "C" {
#endif

void cpp_function();

#ifdef __cplusplus
}
#endif

C++实现 (cpp_library.cpp)

#include "cpp_library.h"

void cpp_function() {
    // C++功能实现
}

在这个例子中,extern "C"告诉C++编译器,尽管cpp_function是在C++中实现的,但是应该以C语言的方式来处理它的链接。这样,C语言代码就可以链接到这个函数,而不会因为Name mangling导致问题。

C语言调用 (c_caller.c)

#include "cpp_library.h"

int main() {
    cpp_function();  // 调用C++库中的函数
    return 0;
}

在C代码中,我们可以像调用普通C函数一样调用cpp_function,因为它在C++库中以C语言方式声明和定义。

名词解释:Name mangling

C++ 支持函数重载,意味着你可以在同一个作用域内定义多个同名函数,只要它们的参数类型或数量不同。

1
2
3
4
5
6
7
void func(int x) {
 // ...
}

void func(double x) {
 // ...
}

上面定义了两个名为 func 的函数,一个接受 int 类型参数,另一个接受 double 类型参数。在 C++ 源代码中,这两个函数看起来是不同的,因为它们的参数类型不同。但是,当这段代码被编译成机器代码时,函数的名字(比如 func)会被转换成一种特殊的格式,这个过程就是 “name mangling”。

编译器进行 “name mangling” 的具体方式依赖于编译器本身,但它通常会将函数的名字和它的参数类型组合起来,以生成一个唯一的标识符。例如,上面的两个 func 函数可能被转换成类似下面的名字:

  • func_int(对应 void func(int x)
  • func_double(对应 void func(double x)

这样,即使在底层机器代码中,这两个同名但参数不同的函数也能被清楚地区分开。

“name mangling” 是编译器内部的机制,通常对程序员是透明的。但是,它在进行跨语言编程、链接不同编译器生成的代码、或者进行底层调试时变得非常重要。程序员需要意识到这一点,因为它可能会影响库的链接、外部函数的调用等。

C 语言没有重载机制,但是 C++ 有重载机制,所以使用 extern "C" 时要保证内部代码不进行 name mangling ,所以内部代码需要提供一个没有重载的接口。

参考文章3

本文由作者按照 CC BY 4.0 进行授权