MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ extern "C"在跨语言编程中的意义

2022-09-301.7k 阅读

C++ 中 extern "C" 的基本概念

在 C++ 编程中,extern "C" 是一种特殊的声明语法,它主要用于解决 C++ 与 C 语言之间的链接兼容性问题。C++ 作为一种面向对象的编程语言,其编译器在处理函数和变量声明时,为了支持函数重载等特性,会对函数名进行“修饰”(name mangling)。这种修饰会改变函数名原本的形式,在目标文件中以一种复杂的编码形式存储。而 C 语言则没有函数重载的概念,其函数名在目标文件中以简单的原始形式存储。

当我们想要在 C++ 代码中调用 C 语言编写的函数,或者在 C 语言代码中调用 C++ 编写的函数时,由于函数名修饰规则的不同,链接器可能无法正确找到对应的函数,从而导致链接错误。extern "C" 的作用就是告诉 C++ 编译器,按照 C 语言的方式来处理函数和变量的声明与链接,即不进行函数名修饰。

下面来看一个简单的代码示例,展示 C++ 中函数名修饰的现象以及 extern "C" 的作用。

// 示例1:C++ 函数名修饰
void myFunction(int a) {
    // 函数体
}

int main() {
    myFunction(10);
    return 0;
}

在上述代码中,C++ 编译器会对 myFunction 函数名进行修饰。假设使用 GCC 编译器,通过 nm 命令查看目标文件(先编译:g++ -c main.cpp,然后 nm main.o),会发现函数名不再是简单的 myFunction,而是类似 _Z9myFunctioni 这样经过修饰的名称。其中 _Z 是 GCC 修饰名的前缀,9 表示函数名 myFunction 的长度,i 表示函数接受一个 int 类型的参数。

// 示例2:使用 extern "C"
extern "C" void myCFunction(int a) {
    // 函数体
}

int main() {
    myCFunction(10);
    return 0;
}

在这个示例中,使用 extern "C" 声明的 myCFunction 函数,其函数名在目标文件中不会被修饰,保持为 myCFunction 原始形式。通过 nm 命令查看目标文件(同样先编译:g++ -c main.cpp,然后 nm main.o),会看到函数名就是 myCFunction,这与 C 语言函数名的存储方式一致。

C++ 调用 C 函数

在实际开发中,经常会遇到 C++ 代码需要调用 C 语言编写的库函数的情况。许多底层库,如 OpenGL、OpenCV 等,在早期都是用 C 语言编写的,并且为了保持兼容性,它们依然提供 C 风格的接口。

假设我们有一个用 C 语言编写的 math.c 文件,其中定义了一个简单的加法函数 add

// math.c
int add(int a, int b) {
    return a + b;
}

我们将其编译为静态库 libmath.a(编译命令:gcc -c math.c,然后 ar rcs libmath.a math.o)。

现在,在 C++ 代码中调用这个函数:

// main.cpp
#include <iostream>

// 使用 extern "C" 声明 C 函数
extern "C" {
    int add(int a, int b);
}

int main() {
    int result = add(3, 5);
    std::cout << "The result of 3 + 5 is: " << result << std::endl;
    return 0;
}

在上述 C++ 代码中,通过 extern "C" 声明了 add 函数,告诉 C++ 编译器按照 C 语言的链接方式来处理这个函数。这样,在编译链接时(编译命令:g++ main.cpp -L. -lmath,其中 -L. 表示在当前目录查找库,-lmath 表示链接 libmath.a 库),C++ 代码就能正确找到并调用 C 语言编写的 add 函数。

如果不使用 extern "C" 声明,编译时会报链接错误,提示找不到 add 函数,因为 C++ 编译器会按照自己的函数名修饰规则去寻找函数,而 C 语言库中的函数名并没有经过这样的修饰。

C 调用 C++ 函数

虽然相对较少,但有时也会出现 C 语言代码需要调用 C++ 编写的函数的情况。比如,我们可能在 C++ 中实现了一些复杂的算法,希望在 C 语言项目中复用这些功能。

首先,我们在 C++ 中编写一个函数 subtract,并将其导出为 C 风格的接口:

// subtract.cpp
#include <iostream>

// 被 C 调用的 C++ 函数
int subtract(int a, int b) {
    return a - b;
}

// 使用 extern "C" 导出函数
extern "C" {
    int subtract_wrapper(int a, int b) {
        return subtract(a, b);
    }
}

这里定义了一个 subtract_wrapper 函数,它通过 extern "C" 声明,以 C 风格的函数名形式暴露给外部。这个函数内部调用了 C++ 实现的 subtract 函数。

然后,在 C 语言代码 main.c 中调用这个函数:

// main.c
#include <stdio.h>

// 声明 C++ 导出的函数
extern int subtract_wrapper(int a, int b);

int main() {
    int result = subtract_wrapper(8, 3);
    printf("The result of 8 - 3 is: %d\n", result);
    return 0;
}

在 C 语言代码中,通过 extern 声明了 subtract_wrapper 函数,就可以像调用普通 C 函数一样调用它。在编译链接时(编译命令:g++ -c subtract.cppgcc main.c subtract.o -o main),C 语言代码就能成功调用 C++ 实现的减法功能。

在跨语言编程中的其他应用

  1. 与 Fortran 语言交互 Fortran 是一种在科学计算领域广泛使用的编程语言。在一些大型科学计算项目中,可能会同时使用 C++ 和 Fortran。通过 extern "C",可以实现 C++ 与 Fortran 之间的函数调用。

例如,在 Fortran 中编写一个简单的乘法函数 multiply.f90

! multiply.f90
function multiply(a, b) result(res)
    implicit none
    integer, intent(in) :: a, b
    integer :: res
    res = a * b
end function multiply

将其编译为共享库 libmultiply.so(编译命令:gfortran -shared -o libmultiply.so multiply.f90)。

在 C++ 中调用这个 Fortran 函数:

// main.cpp
#include <iostream>

// 使用 extern "C" 声明 Fortran 函数
extern "C" {
    int multiply_(int*, int*);
}

int main() {
    int a = 4, b = 5;
    int result = multiply_(&a, &b);
    std::cout << "The result of 4 * 5 is: " << result << std::endl;
    return 0;
}

在 C++ 代码中,通过 extern "C" 声明了 Fortran 函数 multiply_(Fortran 函数名在 C 接口中通常会加上下划线后缀),这样就能在 C++ 中调用 Fortran 编写的乘法函数。

  1. 与汇编语言交互 在一些对性能要求极高的场景,或者需要直接操作硬件的底层开发中,会使用汇编语言。通过 extern "C",C++ 可以与汇编代码进行交互。

假设我们有一段汇编代码 add.asm,实现两个整数相加:

; add.asm
section.text
    global add
add:
    push ebp
    mov ebp, esp
    mov eax, [ebp + 8]
    add eax, [ebp + 12]
    pop ebp
    ret

将其编译为目标文件 add.o(编译命令:nasm -f elf64 add.asm)。

在 C++ 中调用这个汇编函数:

// main.cpp
#include <iostream>

// 使用 extern "C" 声明汇编函数
extern "C" {
    int add(int a, int b);
}

int main() {
    int result = add(2, 3);
    std::cout << "The result of 2 + 3 is: " << result << std::endl;
    return 0;
}

通过 extern "C" 声明,C++ 代码能够调用汇编语言实现的加法函数。在链接时(链接命令:g++ main.cpp add.o -o main),将 C++ 代码与汇编目标文件链接在一起。

注意事项

  1. 函数声明与定义一致性 当使用 extern "C" 声明函数时,函数的声明和定义必须保持一致。特别是在函数原型方面,参数类型、个数以及返回值类型都要完全匹配。否则,在链接时可能会出现未定义行为或运行时错误。

例如,在声明时:

extern "C" void myFunction(int a);

那么在定义时也必须是:

extern "C" void myFunction(int a) {
    // 函数体
}

如果定义时参数类型或函数名有差异,链接器将无法正确匹配函数,导致链接失败。

  1. 头文件保护 在跨语言编程中,头文件的使用非常关键。特别是当一个头文件可能会被 C 和 C++ 代码同时包含时,需要使用条件编译来确保正确的声明。
#ifndef MY_HEADER_H
#define MY_HEADER_H

#ifdef __cplusplus
extern "C" {
#endif

// 函数声明
int myFunction(int a, int b);

#ifdef __cplusplus
}
#endif

#endif

在上述头文件中,通过 #ifdef __cplusplus 判断当前是否是 C++ 编译环境。如果是,就使用 extern "C" 声明函数;如果是 C 编译环境,则直接声明函数。这样可以保证头文件在 C 和 C++ 代码中都能正确使用。

  1. 数据类型兼容性 虽然 extern "C" 解决了函数名修饰和链接兼容性问题,但不同语言的数据类型在表示和内存布局上可能存在差异。例如,C++ 中的 std::string 类型在 C 语言中并没有直接对应的类型。在跨语言传递数据时,需要特别注意数据类型的兼容性。

通常,可以使用基本数据类型(如 intfloatchar 等)作为跨语言接口的数据类型。如果需要传递复杂数据结构,可能需要将其转换为简单的数组或结构体形式,并在不同语言中进行相应的解析。

例如,在 C++ 中定义一个结构体:

struct Point {
    int x;
    int y;
};

在 C 语言中可以以相同的方式定义结构体,以便在两者之间传递数据:

struct Point {
    int x;
    int y;
};

这样,在 C++ 和 C 之间传递 Point 结构体时,只要保证内存对齐等规则一致,就可以正确地传递和使用数据。

  1. 异常处理 C++ 支持异常处理机制,而 C 语言没有。当在 C++ 中调用 C 函数,或者在 C 中调用 C++ 函数时,需要注意异常处理的问题。

如果 C++ 调用 C 函数,C 函数中没有异常处理机制。如果 C 函数发生错误,通常通过返回错误码的方式通知调用者。在 C++ 中调用这样的 C 函数时,需要根据返回错误码来决定是否抛出 C++ 异常。

例如:

// C 函数,返回错误码
int divide(int a, int b, int* result) {
    if (b == 0) {
        return -1; // 错误码
    }
    *result = a / b;
    return 0;
}
// C++ 调用 C 函数并处理错误
#include <iostream>
#include <stdexcept>

extern "C" {
    int divide(int a, int b, int* result);
}

int main() {
    int result;
    int err = divide(10, 2, &result);
    if (err == -1) {
        throw std::runtime_error("Division by zero");
    }
    std::cout << "The result of 10 / 2 is: " << result << std::endl;
    return 0;
}

反之,如果 C 调用 C++ 函数,C++ 函数中抛出的异常如果没有在 C++ 代码中捕获,可能会导致程序崩溃,因为 C 语言无法处理 C++ 异常。因此,在导出给 C 调用的 C++ 函数中,要谨慎处理异常,尽量避免异常传播到 C 语言代码中。

总结

extern "C" 在 C++ 的跨语言编程中起着至关重要的作用。它解决了 C++ 与 C 语言以及其他语言(通过 C 兼容接口)之间的函数名修饰和链接兼容性问题,使得不同语言编写的代码能够相互调用,充分发挥各语言的优势。在实际应用中,需要注意函数声明与定义的一致性、头文件保护、数据类型兼容性以及异常处理等问题,以确保跨语言编程的稳定性和可靠性。通过合理使用 extern "C",开发人员可以更加灵活地整合不同语言的代码,构建出功能强大、高效的软件系统。无论是在大型项目的多语言混合编程,还是在复用现有 C 语言库等场景下,extern "C" 都是 C++ 开发者不可或缺的工具。