概念
C++为支持函数重载、命名空间、类以及期待特性, 存在name mangling
(命名修饰)机制
例如重载函数, 编译阶段C++编译器会通过添加参数类型、参数个数等额外信息对函数重命名, 生成唯一符号, 以区分同名函数
链接阶段, 链接器会按照符号名来解析不同目标文件和库文件中所引用符号, 以正确区分和链接函数
- 示例, C++中同名函数经过C++
name mangling
后生成唯一修饰名
graph LR;
A[/"int Sum(int x, int y)"/]
A1[/"_Z6Sumii"/]
B[/"double Sum(double x, double y)"/]
B1[/"_Z6Sumdd"/]
C(name mangling)
A-->C-->A1
B-->C-->B1
函数处理
函数和变量在本质上都是地址助记符, 在链接过程中称为symbol
(符号)
C语言
C语言不支持重载, 故无命名修饰机制, 每个函数都有唯一未修饰名称, 链接器可直接使用名称解析符号
- 示例, c_module.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);
}
由于C语言编译器不会对函数名增加任何处理, 因此函数名必须唯一
错误情况
- 示例, error.c中有两个同名函数
#include <stdio.h>
int Add(int x) {
return x + 1;
}
double Add(double x) {
return x + 0.1;
}
编译时报错定义类型冲突
C++
- 示例, cpp_module.cpp生成目标文件
// 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++编译器通过命名修饰机制, 对于同名函数, 只要参数类型、参数个数或返回值类型不一致也可通过编译
C/C++混合
- 示例, math_module.c、main.cpp生成可执行文件
// 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;
}
库链接
日常编程中更多是通过库进行调用, 当C/C++混合编程时可能会出现链接错误
未定义错误
(1) 用C语言编译器将math_module.c生成math_module.o
(2) 使用C++编译器将main.cpp生成目标文件
(3) 链接math_module.o、main.o为可执行文件, 出现符号未定义错误
(4) 查看math_module.o与main.o符号表, 发现同一个函数名出现两个不同符号
原因分析
(1) main.cpp 预处理时, 内容展开
生成main.o时, C++编译器对main.cpp中两个原本C语言函数名Add
、GetSquareArea
使用name mangling
, 生成全新函数名_Z3Addii
、_Z13GetSquareAread
+ #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;
}
(2) math_module.o由C编译器编译生成, 没有经过命令修饰, 函数名未改变
(3) 链接时main.o按照_Z3Addii
符号名到各模块查找函数引用, 结果math_module.o里符号名是Add
、GetSquareArea
, 无法匹配, 自然出现函数未定义错误
这种情况需通过extern "C"
处理
extern “C”
定义
C++编译器中提供 extern "C"
/ extern "C" {}
机制, 表示其后续或范围内函数名屏蔽name mangling
机制, 按C语言风格处理, 保持原本名称
通常用于C++代码中调用C语言动态库, 以及C余元调用C++动态库
语法
作用函数
函数名前添加extern "C"
, 无论使用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中两个C语言函数编译时不受命名修饰影响, 依然保持原名称, 和math_module.o中符号一致, 链接错误问题解决
C语言调用C++动态库
C++动态库需要在导出函数名前添加extern "C"
或用 extern "C" {}
包裹, 否则C语言无法调用