C++ name mangling

 

概念

C++为支持函数重载、命名空间、类、模板等特性, 存在name mangling(命名修饰)机制

例如对于函数重载, C++编译器会在编译阶段通过添加参数类型、参数个数等额外信息对函数重命名, 生成唯一符号, 以区分同名函数

  • 示例, C++中同名函数经过name mangling机制后生成唯一修饰名

int Sum(int x, int y)

_Z6Sumii

double Sum(double x, double y)

_Z6Sumdd

name mangling

符号处理

函数和变量在本质上都是地址助记符, 在链接过程中称为symbol(符号)

链接阶段, 链接器会按照符号名来解析不同目标文件和库文件中所引用符号, 以正确区分和链接函数

C语言

C语言无name mangling机制, 每个函数名称必须唯一, 链接器可直接使用名称解析符号

  • 示例, C语言生成目标文件
// c_module.h
#include <stdio.h>

int AddNum(int x, int y);
void PrintValue(double num);
// c_module.c
#include "c_module.h"

int AddNum(int x, int y) {
    return x + y;
}

void PrintValue(double num) {
    printf("res = %f\n", num);
}

使用nm 查看符号表, 发现函数符号名称与源代码中一致

错误情况

  • 示例, C语言源文件函数同名情况
#include <stdio.h>

int Add(int x) {
    return x + 1;
}

double Add(double x) {
    return x + 0.1;
}

编译时报错定义类型冲突

C语言编译器不会对函数名增加任何处理, 因此函数名必须唯一

C++

普通函数

  • 示例, 源文件中存在普通函数
// test.hpp
#include <iostream>

void Test();

int Sub(int x, int y);
// test.cpp
#include "test.hpp"

void Test() {
    std::cout << "Test" << std::endl;
}

int Sub(int x, int y) {
    return x - y;
}

C++编译器会对.cpp中所有函数都进行name mangling, 查看符号表发现函数已重命名

函数重载(overload)

  • 示例, 源文件中存在同名函数
// cpp_module.hpp
#include <iostream>

int AddNum(int x, int y);
double AddNum(double x, double y);

void PrintValue(int num);
void PrintValue(double num);
// cpp_module.cpp
#include "cpp_module.hpp"

int AddNum(int x, int y) {
    return x + y;
}

double AddNum(double x, double y) {
    return x + y;
}

void PrintValue(int num) {
    printf("int = %d\n", num);
}

void PrintValue(double num) {
    printf("double = %f\n", num);
}

查看符号表, 发现同名函数符号名被重命名成唯一符号

C++编译器通过name mangling机制, 对于同名函数, 只要参数类型、参数个数或返回值类型不一致也可通过编译

C/C++混合

同步编译

  • 示例, 使用C++编译器同步编译.c、.cpp
// c_test.h
#include <stdio.h>

void Test();

int Sub(int x, int y);
// c_test.c
#include "c_test.h"

void Test() {
    printf("Test\n");
}

int Sub(int x, int y) {
    return x - y;
}
// main.cpp
#include "c_test.h"
#include <iostream>

int main() {
    Test();
    return 0;
}

由下图可知, 只要使用C++编译器, 对源文件内函数名都会执行name mangling

模块链接

C语言模块与C++模块链接过程中可能会出现符号未定义错误

  • 示例, C语言模块与C++模块链接
// math_module.h
#include <stdio.h>

int Add(int x, int y);
double GetSquareArea(double length);
// math_module.c
#include "math_module.h"

int Add(int x, int y) {
    return x + y;
}

double GetSquareArea(double length) {
    return length * length;
}
// main.cpp
#include "math_module.h"
#include <iostream>

int main() {
    int res = Add(1, 2);
    double area = GetSquareArea(3.74);
    std::cout << "Add = " << res << std::endl;
    std::cout << "SquareArea = " << area << std::endl;
    return 0;
}

未定义错误

(1) 用C语言编译器将math_module.c生成 math_module.o

(2) 使用C++编译器将main.cpp生成目标文件 main.o

(3) 链接 math_module.omain.o 为可执行文件, 出现符号未定义错误

(4) 分别查看符号表, 发现同函数在两个目标文件中符号各不相同

原因分析

(1) main.cpp 预处理时, 内容展开

+ #include <stdio.h>
+ int Add(int x, int y);
+ double GetSquareArea(double length);

#include<iostream>

int main() {
    int res = Add(1, 2);
    double area = GetSquareArea(3.74);
    std::cout << "Add = " << res << std::endl;
    std::cout << "SquareArea = " << area << std::endl;
    return 0;
}

生成main.o时, C++编译器对main.cpp中两个原本C语言函数名AddGetSquareArea进行name mangling, 生成新名_Z3Addii_Z13GetSquareAread

(2) math_module.o 由C编译器编译生成, 没有name mangling机制, 函数名未改变

(3) 链接时main.o_Z3Addii 符号名到各模块查找函数引用, 结果math_module.o里符号名是AddGetSquareArea, 无法匹配, 自然出现函数未定义错误

这种情况需通过extern "C"处理

extern “C”

C++编译器中提供 extern "C"/ extern "C" {} 机制, 表示其后续或作用域内函数屏蔽name mangling机制, 按C语言风格处理, 保持原本名称

通常用于C++代码中调用C语言动态库, 以及C语言调用C++动态库时处理

语法

作用函数

函数名前添加extern "C", 表示使用C++编译器时该函数均按C语言规则编译, 不进行name mangling

extern "C" 函数声明

作用代码块

extern "C" {}表示代码块内所有函数均调用extern "C"

extern "C" {
    void Func1();
    void Func2();
    ...
}

仅C++编译时使用

预处理宏__cplusplus仅在C++编译器中定义, 可通过该宏判断代码是否被C++编译器编译

  • 示例, 仅在代码被C++编译器编译时, 对函数添加extern "C"
#if __cplusplus
extern "C" {
#endif
    void Func1();
    void Func2();
#if __cplusplus
}
#endif

特点

作用对象

extern "C" 只能用于函数和全局变量声明, 不能用于类成员或模板

特性

extern "C" 修饰函数内不能出现C++所有特性

应用

C++调用C语言动态库

  • 示例, 处理模块链接错误

修改main.cpp, 对于所引用C语言头文件使用extern "C" {}包裹

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

#include <iostream>

int main() {
    int res = Add(1, 2);
    double area = GetSquareArea(3.74);
    std::cout << "Add = " << res << std::endl;
    std::cout << "SquareArea = " << area << std::endl;
    return 0;
}

main.cpp预处理时展开

extern "C" {
    #include <stdio.h>

    int Add(int x, int y);
    double GetSquareArea(double length);
}

#include <iostream>

int main() {
    int res = Add(1, 2);
    double area = GetSquareArea(3.74);
    std::cout << "Add = " << res << std::endl;
    std::cout << "SquareArea = " << area << std::endl;
    return 0;
}

extren C ""机制, main.cpp中两个函数名编译时不受name mangling影响, 依然保持原名称, 和math_module.o中符号一致

链接错误问题解决

C语言调用C++动态库

C++动态库若要被C语言调用, 需要在导出函数名前添加extern "C" 或用 extern "C" {}包裹, 否则C语言无法识别name mangling后函数名