C++ 的名字修饰(符号修饰)

【关于C和C++混合编程中编译和链接的问题】https://www.bilibili.com/video/BV1Zm4y1E7nm?vd_source=5a0790755035f26a67935abfbfcdfd5b extern “c"的用法:c与c++的互相调用 - 青山牧云人 - 博客园 C++–名字修饰_c++的函数名修饰是在哪个阶段进行的-CSDN博客 C/C++ 函数签名与名字修饰(符号修饰)-CSDN博客 C/C++中的 extern 和extern“C“关键字的理解和使用(对比两者的异同)_c extern c-CSDN博客

符号修饰

在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。

名字修饰(Name Mangling)是一种在编译过程中,将函数、变量的名称重新改编的机制,简单来说就是编译器为了区分各个函数,将函数通过一定算法,重新修饰为一个全局唯一的名称。

由于C++和C的名字修饰规则不同(不同操作系统下的编译器的名字修饰方式也会不同),导致C++可以支持函数重载,而C不支持函数重载

可以简单理解,c++中支持函数重载,c语言不支持函数重载,这造成了c++和c语言的函数名解析不同(名字修饰/符号修饰)。c语言函数名就是函数名,c++的函数名是函数名+参数组合起来的。

C与C++符号修饰的实例

extern “c"的用法:c与c++的互相调用 - 青山牧云人 - 博客园 (直接copy自该博客)

比如,函数void func(double a) 在CC++中的编译阶段函数名称会被解析成什么呢?

C语言中,由于没有名称修饰,所以在编译时函数名称仍然是func,不会因为参数类型或数量而改变。

C++中,由于名称修饰的存在,函数名称在编译阶段会被编译器转换成一个包含函数原型信息的唯一标识符。通常会涉及函数返回类型、参数类型以及参数数量。以GCC(GNU Compiler Collection)为例,func(double a)会被转换成_Z4funcd ,这里:

  • _Z:是GCC用来表示修饰名称的前缀
  • 4:表示函数名称func的的字符数
  • d:是double类型的编码

因此,用c++的方式去寻找c语言的符号是无法寻找到的extern "C"为何可以做到?

extern "C"的作用就是修改了符号表的生成方式,将c++符号的生成方式换成了c的生成方式。

即c库中生成的符号是c编译器的符号, 因此c语言可以直接链接。而c++程序需要使用extern "C"让编译器使用c的符号命名方式去进行链接,这样才能找到对应的符号。

extern “C”

C/C++中的 extern 和extern“C“关键字的理解和使用(对比两者的异同)_c extern c-CSDN博客

看这一篇就够了 看实例

  1. cpp调用c,cpp编译时会有C++的符号修饰,导致链接C库的函数找不到,需要用extern “C"告诉g++编译器以C语言的风格进行编译
  2. c调用cpp(不涉及C++的类和成员函数),设计一个C的接口,接口的实现在cpp中,接口函数内部去使用C++的特性
  3. c中调用c++成员函数,需要一个接口函数

arm-none-eabi的符号修饰

在嵌入式开发中,arm-none-eabi 工具链(针对 ARM 架构的裸机 / 嵌入式交叉编译工具)的符号修饰(Name Mangling) 主要由编译器(通常是 GCC)根据语言特性(C/C++)和目标 ABI(应用程序二进制接口)决定,目的是生成唯一的符号名,以支持函数重载、命名空间、类型信息等特性。

1. C 语言的符号修饰

C 语言不支持函数重载和命名空间,因此符号修饰非常简单,通常仅在函数名前添加下划线(_),或直接使用原函数名,具体取决于目标 ABI(如 ARM 的 APCS、EABI 等)。

  • 示例:
    C 函数 void foo(int a) 经过 arm-none-eabi-gcc 编译后,符号通常为 foo 这也是为什么在汇编中调用 C 函数时,通常需要引用带下划线的符号

2. C++ 的符号修饰(命名粉碎)

C++ 支持函数重载、类、命名空间、模板等特性,因此需要更复杂的符号修饰规则(称为 “命名粉碎”),以确保每个重载函数 / 方法生成唯一的符号。arm-none-eabi-g++ 遵循 GCC 的 C++ 符号修饰规则,基本格式为:
_Z[命名空间/类前缀][函数名长度][函数名][参数类型编码]

  • 关键规则解析:
    • _Z:固定前缀,标识 C++ 符号。
    • 命名空间 / 类:若函数属于命名空间或类,会在前缀中编码(如 N 表示命名空间开始,E 表示结束)。
    • 函数名长度:紧跟在命名空间 / 类之后,用数字表示函数名的字符数。
    • 参数类型编码:用简写字母表示参数类型(如 i=int,f=float,p= 指针,v=void 等)。
  • 示例:
    1. 全局函数 void foo(int)
      修饰后符号:_Z3fooi
      解析:_Z(C++ 符号) + 3foo 长度为 3) + foo(函数名) + i(参数 int)。
    2. 重载函数 void foo(float)
      修饰后符号:_Z3foof(参数 f 表示 float,与上例区分)。
    3. 类成员函数 class A { void bar(double); }
      修饰后符号:_ZN1A3barEd
      解析:_Z + N(类 / 命名空间开始) + 1A(类名 A 长度为 1) + 3bar(函数名 bar 长度为 3) + E(类结束) + d(参数 double)。
    4. 命名空间内函数 namespace ns { int add(int, int); }
      修饰后符号:_ZN2ns3addii
      解析:_Z + N(命名空间开始) + 2ns(命名空间 ns 长度为 2) + 3add(函数名 add 长度为 3) + E(命名空间结束) + ii(两个 int 参数)。

3. C 和 C++ 混合编程的符号兼容

当 C++ 代码调用 C 函数(或反之)时,需使用 extern "C" 声明,以禁用 C++ 的符号修饰,确保链接时匹配 C 风格的符号。

  • 示例:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    // C头文件(需兼容C++)
    #ifdef __cplusplus
    extern "C" {
    #endif
    
    void c_function(int a);  // C函数,符号为 _c_function
    
    #ifdef __cplusplus
    }
    #endif
    
    // C++代码中调用
    void cpp_func() {
        c_function(10);  // 正确引用C符号 _c_function
    }
    ```
experience
使用 Hugo 构建
主题 StackJimmy 设计