桓楠百科网

编程知识、经典语录与百科知识分享平台

C语言进阶教程:C语言与C++语言交互 (extern "C")

C++ 语言是在 C 语言基础上发展起来的,它既兼容大部分 C 语言的特性,又引入了许多新的概念,如类、对象、模板、异常处理、命名空间等。这使得 C++ 编译器在处理函数和变量名时,会进行一种称为“名字修饰”(Name Mangling 或 Name Decoration)的过程,以便支持函数重载、命名空间等特性。而 C 语言编译器则不会进行这种修饰。

当需要在 C++ 项目中使用 C 语言编写的库,或者在 C 项目中使用 C++ 编写的库时,这种名字修饰的差异就会导致链接错误。extern "C" 就是为了解决这个问题而引入的机制。

名字修饰 (Name Mangling)

我们先来看一个简单的例子,理解名字修饰。

C++ 代码 (mangle_test.cpp):

 // mangle_test.cpp
 void myFunction(int x) {}
 void myFunction(double x) {}
 
 namespace MyNamespace {
     void myFunction(int x) {}
 }
 
 class MyClass {
 public:
     void memberFunction(int x) {}
 };

如果我们用 C++ 编译器编译(例如 GCC/G++),然后查看导出的符号(例如使用 nm 命令 nm mangle_test.o | c++filt),可能会看到类似这样的结果(具体修饰规则因编译器而异):

  • myFunction(int) 可能被修饰成类似 _Z10myFunctioni
  • myFunction(double) 可能被修饰成类似 _Z10myFunctiond
  • MyNamespace::myFunction(int) 可能被修饰成类似 _ZN11MyNamespace10myFunctionEi
  • MyClass::memberFunction(int) 可能被修饰成类似 _ZN7MyClass14memberFunctionEi

这些修饰后的名字包含了函数的参数类型、所在的命名空间或类等信息,使得链接器能够区分同名但签名不同的函数。

C 代码 (c_function.c):

 // c_function.c
 void cFunction(int x) {}

C 编译器编译后,cFunction 的符号名通常就是 cFunction_cFunction(取决于平台和编译器),没有复杂的修饰。

extern "C"的作用

extern "C" 的主要作用是告诉 C++ 编译器,其声明的函数或变量应该按照 C 语言的方式来编译和链接,即不进行名字修饰。

extern "C" 可以用于:

  1. 在 C++ 代码中声明 C 语言函数:让 C++ 代码能够正确链接到 C 语言编译的库。
  2. 在 C++ 代码中定义能被 C 语言调用的函数:让 C++ 编写的函数能够被 C 代码调用。

语法

extern "C" 可以修饰单个函数声明/定义,也可以修饰一个代码块。

修饰单个函数:

 extern "C" void c_style_function(int params);
 
 extern "C" int c_style_variable;

修饰代码块:

 extern "C" {
     // 所有在这里声明或定义的函数和变量都按C方式处理
     void func1(int);
     int var1;
     // ...
 }

场景一:C++ 调用 C 代码

假设我们有一个用 C 语言编写的库 my_c_lib.hmy_c_lib.c

my_c_lib.h:

 #ifndef MY_C_LIB_H
 #define MY_C_LIB_H
 
 // C库的头文件,提供给C和C++使用者
 
 // 为了C++编译器能正确处理,使用条件编译
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 void print_message_from_c(const char* message);
 int add_integers_from_c(int a, int b);
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif // MY_C_LIB_H

my_c_lib.c:

 #include "my_c_lib.h"
 #include <stdio.h>
 
 void print_message_from_c(const char* message) {
     printf("C_Lib: %s\n", message);
 }
 
 int add_integers_from_c(int a, int b) {
     return a + b;
 }

C++ 调用代码 (main.cpp):

 #include "my_c_lib.h" // 包含C库的头文件
 #include <iostream>
 
 int main() {
     print_message_from_c("Hello from C++!");
 
     int sum = add_integers_from_c(10, 20);
     std::cout << "C++_Main: Sum from C_Lib = " << sum << std::endl;
 
     return 0;
 }

编译链接 (GCC/G++):

 # 编译C库 (生成目标文件)
 gcc -c my_c_lib.c -o my_c_lib.o
 
 # 编译C++主程序 (生成目标文件)
 g++ -c main.cpp -o main.o
 
 # 链接C和C++的目标文件
 g++ main.o my_c_lib.o -o program
 
 # 运行
 ./program

解释:

  • my_c_lib.h 中,#ifdef __cplusplus 是一个预处理指令。__cplusplus 是 C++ 编译器会自动定义的一个宏。因此,当这个头文件被 C++ 编译器包含时,extern "C" { ... } 块会生效,告诉 C++ 编译器 print_message_from_cadd_integers_from_c 是 C 风格的函数,链接时应该查找未修饰的符号名。
  • 当这个头文件被 C 编译器包含时,__cplusplus 未定义,extern "C" 部分被忽略,C 编译器按其默认方式处理。
  • 这样,同一个头文件可以同时被 C 和 C++ 代码使用。

场景二:C 调用 C++ 代码

假设我们有一个用 C++ 编写的库,希望提供一些接口给 C 语言调用。

my_cpp_lib.h (供 C++ 内部使用和导出 C 接口):

#ifndef MY_CPP_LIB_H
#define MY_CPP_LIB_H

#include <string>

// C++ 内部类
class MyCppClass {
public:
    MyCppClass(const std::string& name);
    void greet();
private:
    std::string _name;
};

// 导出给C语言的接口,必须用 extern "C"
#ifdef __cplusplus
extern "C" {
#endif

// 使用不透明指针来隐藏C++对象的实现细节
typedef struct MyCppObjectHandle MyCppObjectHandle;

MyCppObjectHandle* create_my_cpp_object(const char* name);
void use_my_cpp_object_greet(MyCppObjectHandle* handle);
void destroy_my_cpp_object(MyCppObjectHandle* handle);

#ifdef __cplusplus
}
#endif

#endif // MY_CPP_LIB_H

my_cpp_lib.cpp:

#include "my_cpp_lib.h"
#include <iostream>
#include <vector> // 只是为了演示C++特性

// C++类实现
MyCppClass::MyCppClass(const std::string& name) : _name(name) {
    std::cout << "C++_Lib: MyCppClass constructor for '" << _name << "'" << std::endl;
}

void MyCppClass::greet() {
    std::cout << "C++_Lib: Hello from '" << _name << "'! Using std::vector size: " << std::vector<int>().capacity() << std::endl;
}

// C接口的实现
// extern "C" 再次确保这些函数是C链接方式
extern "C" {

// 定义不透明指针的实际类型
struct MyCppObjectHandle {
    MyCppClass* instance;
};

MyCppObjectHandle* create_my_cpp_object(const char* name) {
    if (!name) return nullptr;
    MyCppObjectHandle* handle = new MyCppObjectHandle();
    if (!handle) return nullptr;
    try {
        handle->instance = new MyCppClass(name);
    } catch (const std::bad_alloc&) {
        delete handle;
        return nullptr;
    }
    return handle;
}

void use_my_cpp_object_greet(MyCppObjectHandle* handle) {
    if (handle && handle->instance) {
        handle->instance->greet();
    }
}

void destroy_my_cpp_object(MyCppObjectHandle* handle) {
    if (handle) {
        delete handle->instance; // 调用C++析构函数
        delete handle;           // 释放句柄本身
    }
}

} // extern "C"

C 调用代码 (main_c_caller.c):

#include "my_cpp_lib.h" // 包含C++库提供的C接口头文件
#include <stdio.h>

int main() {
    printf("C_Main: Calling C++ library.\n");

    MyCppObjectHandle* obj1 = create_my_cpp_object("ObjectOne");
    if (obj1) {
        use_my_cpp_object_greet(obj1);
    } else {
        printf("C_Main: Failed to create ObjectOne.\n");
    }

    MyCppObjectHandle* obj2 = create_my_cpp_object("ObjectTwo");
    if (obj2) {
        use_my_cpp_object_greet(obj2);
    }

    // 清理资源
    destroy_my_cpp_object(obj1);
    destroy_my_cpp_object(obj2);

    printf("C_Main: Finished using C++ library.\n");
    return 0;
}

编译链接 (GCC/G++):

# 编译C++库 (生成目标文件)
g++ -c my_cpp_lib.cpp -o my_cpp_lib.o

# 编译C主程序 (生成目标文件)
gcc -c main_c_caller.c -o main_c_caller.o

# 链接C和C++的目标文件
# 注意:因为C++库可能使用了C++标准库,链接时通常需要使用g++
g++ main_c_caller.o my_cpp_lib.o -o program_c_calls_cpp

# 运行
./program_c_calls_cpp

解释:

  • my_cpp_lib.h 中,我们为 C 语言使用者提供了 extern "C" 保护的函数声明。
  • 由于 C 语言没有类的概念,我们不能直接将 C++ 类暴露给 C。因此,使用了不透明指针 (MyCppObjectHandle*) 的技巧。C 代码只知道这是一个句柄,具体它指向什么以及如何操作由 C++ 库内部实现。
  • create_my_cpp_object 函数在内部创建 C++ 对象,并返回一个包含该对象指针的句柄。
  • use_my_cpp_object_greet 函数接收这个句柄,并在内部将其转换回 C++ 对象指针来调用成员函数。
  • destroy_my_cpp_object 函数负责释放 C++ 对象和句柄。
  • my_cpp_lib.cpp 中,C 接口函数的定义也需要放在 extern "C" 块内(或者每个函数单独用 extern "C" 修饰),以确保它们以 C 链接方式导出。
  • 链接时使用 g++ 是因为 C++ 代码 (my_cpp_lib.o) 可能依赖于 C++ 标准库(例如 iostream, string, new/delete 操作等),g++ 链接器会自动处理这些依赖。

注意事项

  1. 仅限 C 兼容特性:通过 extern "C" 暴露给 C 的接口,其参数和返回值类型必须是 C 语言兼容的。这意味着不能直接使用 C++ 特有的类型,如类、引用、模板、异常等作为接口的一部分。
  2. 可以使用基本数据类型(int, char*, double 等)。
  3. 可以使用结构体(但结构体成员也必须是 C 兼容的,不能有 C++ 特有的成员如构造函数、成员函数等)。
  4. 可以使用指针(包括函数指针)。
  5. 异常处理:C 语言没有异常处理机制。如果 extern "C" 修饰的 C++ 函数可能会抛出异常,这个异常不能跨越 extern "C" 边界传播到 C 代码中。必须在 C++ 函数内部捕获所有异常,并将其转换为 C 能够理解的错误码或状态。
     extern "C" int cpp_function_for_c(int input) {
         try {
             // C++ code that might throw
             if (input < 0) throw std::runtime_error("Negative input");
             return input * 2;
         } catch (const std::exception& e) {
             fprintf(stderr, "C++ Exception: %s\n", e.what());
             return -1; // Return an error code
         } catch (...) {
             fprintf(stderr, "Unknown C++ Exception\n");
             return -2; // Return a generic error code
         }
     }
  1. 头文件管理:使用 #ifdef __cplusplusextern "C" 是管理跨语言头文件的标准做法。
  2. C++特性封装:当 C 调用 C++ 时,应将 C++ 的复杂性(如类、模板、STL容器)封装在 C++ 实现内部,只通过 C 兼容的接口(通常是不透明指针和一组操作函数)暴露给 C。
  3. 链接器:当项目中同时包含 C 和 C++ 代码时,通常推荐使用 C++ 编译器/链接器(如 g++ 或 MSVC 的 cl.exe 链接 C++ 项目)进行最终链接,因为它能更好地处理 C++ 运行时库和依赖。
  4. extern "C" 只影响链接名,不改变语义:它告诉编译器如何命名函数以供链接器查找,但函数本身仍然是 C++ 函数(如果定义在 .cpp 文件中并用 C++ 编译器编译),可以访问 C++ 特性。

总结

extern "C" 是 C 和 C++ 混合编程中不可或缺的工具。它通过控制名字修饰,使得两种语言编写的代码能够相互调用。

  • C++ 调用 C:在 C++ 代码中,使用 extern "C" 声明 C 函数,以确保链接器查找 C 风格的符号名。
  • C 调用 C++:在 C++ 代码中,使用 extern "C" 定义供 C 调用的函数,以 C 风格导出符号名。同时,需要注意接口设计,保持 C 兼容性,通常通过不透明指针封装 C++ 对象。

正确使用 extern "C" 并理解其背后的名字修饰和调用约定差异,是实现健壮、可维护的 C/C++ 混合项目的关键。

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言