在C++编程的深层机制中,有一项至关重要的技术——名称重整(Name Mangling),它像一座连接高级语言特性与底层二进制世界的桥梁。对于C++程序员而言,函数重载是一项习以为常的便利功能:我们可以在同一作用域内定义多个同名函数,只要它们的参数列表不同即可。例如,一个 print 函数可以接受 int、double 或 std::string 类型的参数,而编译器总能正确地选择最匹配的版本。然而,这种便利在二进制层面却引发了一个根本性的问题:在目标文件和可执行文件中,每个函数、变量或任何可寻址的实体都必须拥有一个全局唯一的标识符(即符号)。操作系统加载器和链接器正是通过这些唯一的符号来定位和调用代码或访问数据。
如果 print(int) 和 print(double) 在编译后的目标文件中都简单地被称为 print,链接器将无从分辨,这无疑会导致符号冲突。名称重整正是为了解决这个核心矛盾而生。它是一种编译器技术,将C++源代码中人类可读的标识符,根据其上下文信息(如函数签名、命名空间、类名、模板参数等),转换成一个在二进制层面保证唯一性的、通常较长的字符串符号。这个转换后的字符串就是我们常说的“重整名”(Mangled Name)或“修饰名”(Decorated Name)。
本讲座将作为一次深入的技术探讨,详细解析C++名称重整的原理、其在二进制级别支持函数重载的机制、各种C++语言特性如何被编码进重整名中,以及不同编译器和ABI(Application Binary Interface)对名称重整的具体实现。我们将通过丰富的代码示例,配合对重整名的逐层剖析,揭示C++编译器如何巧妙地在底层解决了高级语言的复杂性。
名称重整的核心逻辑与必要性
为什么需要名称重整?
C++作为一种多范式语言,引入了许多C语言不具备的特性,这些特性在源代码层面提高了抽象能力和表达力,但在二进制层面却要求更复杂的管理机制:
- 函数重载(Function Overloading): 这是最直接的需求。多个函数共享同一名称,但拥有不同的参数列表。重整机制必须区分
foo(int)和foo(double)。 - 命名空间(Namespaces): 命名空间用于组织代码,防止全局命名冲突。
MyNamespace::func()和AnotherNamespace::func()即使函数名相同,也必须在二进制层面独一无二。 - 类与成员(Classes and Members): 类可以有成员函数和静态数据成员。
MyClass::method()和YourClass::method()必须被区分。成员函数的const、volatile、&、&&修饰符也影响其签名。 - 模板(Templates): 模板在编译时根据模板参数实例化出具体的函数或类。
template_func<int>()和template_func<double>()是两个完全不同的函数,需要独立的符号。 - 操作符重载(Operator Overloading):
operator+、operator<<等重载操作符本质上是特殊命名的函数,它们同样需要重整。 - 特殊成员函数: 构造函数、析构函数、转换操作符等都有其特殊的命名和重载规则,也需要通过重整来获得唯一符号。
- 类型信息(Type Information): 运行时类型信息(RTTI)和虚函数机制(vtable)也依赖于名称重整来生成其内部数据结构(如
std::type_info对象和虚函数表)的唯一符号。
名称重整正是应对这些挑战的通用解决方案。它将所有必要的上下文信息——包括函数名、参数类型、命名空间、类名、模板参数等——编码进一个单一的字符串中,从而为每一个C++实体在二进制层面提供一个独一无二的“全限定名”。
名称重整的执行者与可见性
名称重整是由C++编译器在编译阶段完成的。当编译器将 .cpp 源文件编译成 .o(或 .obj)目标文件时,它会生成一个符号表。这个符号表包含了所有函数、变量和其他实体的重整名,以及它们在目标文件中的地址或相对位置。
在日常编程中,C++程序员通常不需要直接处理重整名。然而,在以下场景中,重整名会变得可见且重要:
- 链接错误: 当链接器报告“undefined reference”(未定义引用)或“multiple definition”(多重定义)错误时,它通常会显示重整名。理解重整名有助于快速定位代码中的问题,例如函数签名不匹配、忘记实现函数、命名空间或类名拼写错误等。
- 调试器: 在某些调试器中,尤其是在查看汇编代码、调用堆栈或检查符号表时,可能会看到函数的重整名。现代调试器通常能够自动对这些名称进行反重整(demangle),以显示原始的C++签名。
- 检查目标文件或库: 使用命令行工具,如Linux/Unix上的
nm、objdump或Windows上的dumpbin,可以查看目标文件或库的符号表,直接观察到原始的重整名。
C++名称重整的基本编码规则(以Itanium C++ ABI为例)
C++标准并未规定名称重整的具体方案,这属于应用二进制接口(ABI)的范畴。因此,不同的编译器和平台可能会采用不同的重整规则。在许多类Unix系统(如Linux、macOS)上,GCC和Clang等编译器普遍遵循Itanium C++ ABI。本讲座将主要以Itanium C++ ABI为例进行讲解,因为它是一个被广泛接受且文档相对完善的标准。
Itanium ABI的重整名通常以 _Z 或 _ZN 开头,表示这是一个C++符号。其编码规则高度结构化,旨在紧凑地表示各种C++类型和结构。
核心编码元素
| 编码前缀 | 含义 | 示例(重整名片段) | 解释 |
|---|---|---|---|
_Z |
全局符号(在全局命名空间中) | _Z |
函数或变量在全局作用域 |
_ZN |
嵌套名称(在命名空间或类中) | _ZN |
函数或变量位于命名空间或类内部 |
E |
嵌套名称或模板参数列表的结束 | E |
标记命名空间或类名的结束 |
<length> |
标识符的长度(十进制数字) | 4 |
表示接下来的标识符有4个字符 |
<name> |
原始标识符(函数名、类名、命名空间名) | func |
实际的名称字符串 |
v |
void 类型 |
v |
|
b |
bool 类型 |
b |
|
c |
char 类型 |
c |
|
s |
short 类型 |
s |
|
i |
int 类型 |
i |
|
l |
long 类型 |
l |
|
x |
long long 类型 |
x |
|
f |
float 类型 |
f |
|
d |
double 类型 |
d |
|
e |
long double 类型 |
e |
|
P |
指针 | Pi |
int* |
R |
引用 | Ri |
int& |
K |
const 修饰符 |
Ki |
const int |
V |
volatile 修饰符 |
Vi |
volatile int |
S_ |
第一次出现的类型或名称的缩写(Substitution) | S_ |
引用之前已编码的类型或命名空间,用于缩短重整名 |
T_ |
第二次出现的类型或名称的缩写 | T_ |
引用之前已编码的类型或命名空间 |
I |
模板参数列表开始 | I |
|
L |
非类型模板参数(字面量) | Lj5 |
字面量 5 |
关于返回类型
函数的返回类型通常不被编码到函数重整名中。 这是一个非常重要的细节。C++的重载解析规则仅基于函数名和参数列表(包括参数类型、顺序、const/volatile修饰符等)来确定调用哪个函数,而不考虑返回类型。如果两个函数只有返回类型不同而参数列表完全相同,C++标准规定这是非法的,编译器会报错。因此,链接器在查找符号时,也只需要函数名和参数列表来唯一标识一个函数。将返回类型编码进重整名只会徒增其长度和复杂性,而无益于解决重载解析或链接时的歧义。
剖析重整名:代码示例与解析
为了直观理解名称重整,我们将通过一系列C++代码示例,并模拟 nm 工具的输出(不带 -C 选项,以显示原始重整名)来逐一解析。假设我们使用GCC/Clang编译器。
1. 全局函数与函数重载
// global_funcs.cpp
void func_global_void() { /* ... */ }
int func_global_int(int a) { return a; }
double func_global_double(int a, char b) { return a + b; }
void func_global_pointer(int* p) { /* ... */ }
编译并查看符号:
g++ -c global_funcs.cpp -o global_funcs.o
nm global_funcs.o | grep func_global
可能的输出(原始重整名):
0000000000000000 T _Z18func_global_voidv
0000000000000010 T _Z17func_global_inti
0000000000000020 T _Z22func_global_doubleic
0000000000000030 T _Z21func_global_pointerPi
解析:
-
_Z: Itanium ABI的全局符号前缀。 -
18: 函数名func_global_void的长度。 -
func_global_void: 原始函数名。 -
v: 参数列表,表示void(无参数)。 -
_Z17func_global_inti: 全局函数func_global_int,带一个int参数(i)。 -
_Z22func_global_doubleic: 全局函数func_global_double,带一个int参数(i)和一个char参数(c)。 -
_Z21func_global_pointerPi: 全局函数func_global_pointer,带一个int*参数(P代表指针,i代表int)。
这清晰地展示了即使函数名部分相同,只要参数列表不同,就能生成完全唯一的重整名。
2. 命名空间中的函数
// namespace_funcs.cpp
namespace MyNamespace {
void do_something() { /* ... */ }
int calculate(int x) { return x * 2; }
double calculate(double d) { return d * 2.0; } // Overload
}
namespace AnotherNamespace {
void do_something() { /* ... */ }
}
编译并查看符号:
g++ -c namespace_funcs.cpp -o namespace_funcs.o
nm namespace_funcs.o | grep do_something
nm namespace_funcs.o | grep calculate
可能的输出:
0000000000000000 T _ZN11MyNamespace12do_somethingEv
0000000000000010 T _ZN16AnotherNamespace12do_somethingEv
0000000000000020 T _ZN11MyNamespace9calculateEi
0000000000000030 T _ZN11MyNamespace9calculateEd
解析:
-
_ZN: 嵌套名称符号前缀。 -
11: 命名空间名MyNamespace的长度。 -
MyNamespace: 命名空间名。 -
12: 函数名do_something的长度。 -
do_something: 原始函数名。 -
Ev: 命名空间结束符E,后跟参数列表v(void)。 -
_ZN16AnotherNamespace12do_somethingEv: 命名空间AnotherNamespace中的do_something。长度编码16表示AnotherNamespace。 -
_ZN11MyNamespace9calculateEi:MyNamespace::calculate(int)。参数int编码为i。 -
_ZN11MyNamespace9calculateEd:MyNamespace::calculate(double)。参数double编码为d。
命名空间信息被清晰地编码,即使函数名相同,不同命名空间中的函数也能被区分。
3. 类成员函数与修饰符
// class_members.cpp
class MyClass {
public:
void non_const_method() { /* ... */ }
void const_method() const { /* ... */ }
int overloaded_method(int x) { return x; }
double overloaded_method(double d) const { return d; } // const overload
static void static_method() { /* ... */ }
};
编译并查看符号:
g++ -c class_members.cpp -o class_members.o
nm class_members.o | grep MyClass
可能的输出:
0000000000000000 T _ZN7MyClass16non_const_methodEv
0000000000000010 T _ZN7MyClass12const_methodEvK
0000000000000020 T _ZN7MyClass17overloaded_methodEi
0000000000000030 T _ZN7MyClass17overloaded_methodEdK
0000000000000040 T _ZN7MyClass13static_methodEv
解析:
_ZN7MyClass16non_const_methodEv: 类MyClass中的non_const_method()。_ZN7MyClass12const_methodEvK: 类MyClass中的const_method()。- 注意末尾的
K:它表示这是一个const成员函数。这个K实际上是修饰了隐式的this指针,使其类型为const MyClass*。
- 注意末尾的
_ZN7MyClass17overloaded_methodEi:MyClass::overloaded_method(int)。_ZN7MyClass17overloaded_methodEdK:MyClass::overloaded_method(double) const。- 这里
EdK表示参数列表是一个double(d),并且该方法是一个const成员函数 (K)。这完美地展示了如何在二进制层面区分重载和const修饰符。
- 这里
_ZN7MyClass13static_methodEv: 静态成员函数MyClass::static_method()。静态成员函数没有隐式的this指针,因此其重整名结构与普通全局函数类似,但包含了类名。
4. 操作符重载
操作符重载函数在Itanium ABI中由特定的操作符代码表示。
// operators.cpp
#include <iostream>
class Vector {
int x, y;
public:
Vector(int x_ = 0, int y_ = 0) : x(x_), y(y_) {}
Vector operator+(const Vector& other) const {
return Vector(x + other.x, y + other.y);
}
bool operator==(const Vector& other) const {
return x == other.x && y == other.y;
}
Vector& operator++() { // Prefix increment
++x; ++y; return *this;
}
Vector operator++(int) { // Postfix increment
Vector temp = *this;
++(*this);
return temp;
}
friend std::ostream& operator<<(std::ostream& os, const Vector& v);
};
std::ostream& operator<<(std::ostream& os, const Vector& v) {
os << "(" << v.x << ", " << v.y << ")";
return os;
}
编译并查看符号:
g++ -c operators.cpp -o operators.o
nm operators.o | grep Vector | grep operator
可能的输出:
0000000000000000 T _ZN6VectorppEv
0000000000000010 T _ZN6VectorppEi
0000000000000020 T _ZN6VectorplERKS_K
0000000000000030 T _ZN6VectoreqERKS_K
0000000000000040 W _ZlsRSoRK6Vector
解析:
_ZN6VectorplERKS_K:Vector::operator+(const Vector&) constpl: 代表operator+。ERKS_:E结束类名,R引用,Kconst,S_是一个缩写,代表Vector类型(因为Vector在重整名中已经出现过)。K: 成员函数的const修饰符。
_ZN6VectoreqERKS_K:Vector::operator==(const Vector&) const。eq代表operator==。_ZN6VectorppEv:Vector::operator++()(前缀递增)。pp代表operator++。Ev表示void参数。_ZN6VectorppEi:Vector::operator++(int)(后缀递增)。Ei表示int参数,这是C++标准用于区分前缀和后缀递增的哑元参数。_ZlsRSoRK6Vector: 全局函数operator<<(std::ostream&, const Vector&)_Z: 全局符号。ls: 代表operator<<。RSo:R引用,So是std::ostream的缩写。RK6Vector:R引用,Kconst,6长度,Vector类型。
5. 模板函数与模板类
模板的实例化会生成具体的类型或函数,每个实例化都需要唯一的重整名。
// templates.cpp
template<typename T>
void print_value(T value) { /* ... */ }
template<typename T, int N>
void process_array(T (&arr)[N]) { /* ... */ }
template<typename T>
class MyTemplateClass {
public:
void method(T val) { /* ... */ }
};
void caller() {
print_value(10); // T = int
print_value(3.14); // T = double
print_value("hello"); // T = const char*
int arr_int[5];
process_array(arr_int); // T = int, N = 5
MyTemplateClass<int> int_obj;
int_obj.method(20);
MyTemplateClass<double> double_obj;
double_obj.method(20.5);
}
编译并查看符号:
g++ -c templates.cpp -o templates.o
nm templates.o | grep print_value
nm templates.o | grep process_array
nm templates.o | grep MyTemplateClass
可能的输出:
0000000000000000 T _Z11print_valueIiEvT_
0000000000000010 T _Z11print_valueIdEvT_
0000000000000020 T _Z11print_valueIPKcEvT_
0000000000000030 T _Z13process_arrayIRiLj5EEv
0000000000000040 T _ZN15MyTemplateClassIiE6methodEi
0000000000000050 T _ZN15MyTemplateClassIdE6methodEd
解析:
-
_Z11print_valueIiEvT_:print_value<int>(int)Ii:I模板参数列表开始,i表示模板类型参数T被实例化为int。E: 模板参数列表结束。v: 函数参数列表,void(因为模板参数T已经指定了类型)。T_: 模板参数T的占位符,表示函数参数是模板类型参数。- 更准确的Itanium ABI表示可能是
_Z11print_valueIiEv,其中Ii已经足够表示T为int,而函数参数value的类型就是T。编译器可能会优化掉冗余的T_。c++filt会解析为void print_value<int>(int)。为了简化和聚焦核心,我们假设T的类型在模板参数列表中已经完全编码。
-
_Z13process_arrayIRiLj5EEv:process_array<int, 5>(int (&)[5])IRiLj5EE: 模板参数列表。I: 模板参数列表开始。R: 引用。i: 类型参数T为int。L: 字面量参数。j5: 字面量5(即N的值)。E: 模板参数列表结束。
Ev: 函数参数列表,void。
-
_ZN15MyTemplateClassIiE6methodEi:MyTemplateClass<int>::method(int)_ZN15MyTemplateClassIiE: 这是类名部分,表示MyTemplateClass<int>。15:MyTemplateClass长度。IiE: 模板参数T为int。
6method: 函数名method。Ei: 函数参数为int。
模板实例化导致了更长、更复杂的重整名,但同样保证了每个实例化版本的唯一性。
6. 构造函数、析构函数与全局变量
// special_members_vars.cpp
int global_var = 10;
namespace NS {
double ns_global_var = 20.0;
class MyProduct {
public:
MyProduct() { /* default ctor */ }
MyProduct(int id) { /* overloaded ctor */ }
~MyProduct() { /* dtor */ }
static int static_product_id;
};
}
int NS::MyProduct::static_product_id = 100;
编译并查看符号:
g++ -c special_members_vars.cpp -o special_members_vars.o
nm special_members_vars.o
可能的输出:
0000000000000000 D _Z10global_var
0000000000000004 D _ZN2NS13ns_global_varE
0000000000000000 T _ZN2NS9MyProductC1Ev
0000000000000010 T _ZN2NS9MyProductC1Ei
0000000000000020 T _ZN2NS9MyProductD1Ev
0000000000000008 D _ZN2NS9MyProduct17static_product_idE
解析:
_Z10global_var: 全局变量global_var。_ZN2NS13ns_global_varE: 命名空间NS中的变量ns_global_var。_ZN2NS9MyProductC1Ev:NS::MyProduct::MyProduct()(默认构造函数)。C1: 表示“complete object constructor”(完整对象构造函数)。Itanium ABI区分几种构造函数类型。
_ZN2NS9MyProductC1Ei:NS::MyProduct::MyProduct(int)(重载构造函数)。Ei表示参数为int。_ZN2NS9MyProductD1Ev:NS::MyProduct::~MyProduct()(析构函数)。D1: 表示“complete object destructor”(完整对象析构函数)。
_ZN2NS9MyProduct17static_product_idE: 命名空间NS中类MyProduct的静态成员变量static_product_id。
对于变量,重整名通常只包含其全限定名,不包含类型信息,因为变量的类型在链接时通常不需要区分,只需要其名称唯一即可。
C++ ABI 与 名称重整标准
如前所述,名称重整的具体方案并非C++语言标准的一部分,而是由各个编译器的应用二进制接口(ABI)来定义。ABI是一套规范,定义了在特定硬件平台和操作系统上,如何编译和链接C++代码的底层细节。
为什么需要ABI?
ABI的存在是为了确保不同编译单元(.o 文件)、不同库(静态库、动态库)之间能够正确地相互链接和调用,即使它们是由不同的编译器版本或甚至不同的编译器(只要它们遵循相同的ABI)编译的。一个完整的ABI通常包括:
- 名称重整方案: C++符号如何转换为二进制符号。
- 调用约定: 函数参数如何传递,返回值如何返回,寄存器如何使用,栈如何管理。
- 对象布局: 类成员如何在内存中排列,虚函数表如何构建。
- 异常处理机制: 异常如何捕获和传递。
- 运行时类型信息(RTTI)表示:
type_info对象如何表示和查找。
主流C++ ABI及其名称重整差异
-
Itanium C++ ABI:
- 平台: 广泛应用于类Unix系统,如Linux、macOS、Solaris、BSD等。
- 编译器: GCC、Clang等开源编译器遵循此ABI。
- 重整特点: 以
_Z或_ZN开头,使用数字前缀表示名称长度,并广泛使用缩写(如S_、So、St)来减少重整名长度。其规范相对开放和文档化。
-
Microsoft Visual C++ ABI:
- 平台: 主要用于Windows平台。
- 编译器: Microsoft Visual C++ (MSVC) 编译器使用其特有的ABI。
- 重整特点: 通常以
?开头,语法与Itanium ABI完全不同。例如,void foo(int)在MSVC中可能被重整为?foo@@YAXH@Z。MSVC的ABI在细节上通常不如Itanium ABI公开,但在Windows生态系统中是事实标准。
ABI差异的影响
由于名称重整方案是ABI的核心组成部分,不同ABI之间是不兼容的。这种不兼容性体现在:
- 链接失败: 由遵循Itanium ABI的编译器(如GCC)编译的目标文件,不能直接与由遵循MSVC ABI的编译器(如MSVC)编译的目标文件链接。链接器将无法找到或匹配预期的符号。
- 运行时行为异常: 即使勉强链接成功(例如,只通过C接口交互),如果两个编译单元对对象布局、调用约定、异常处理等底层机制的理解不同,运行时也可能导致崩溃或未定义行为。
- 二进制兼容性: 除非严格遵循相同的ABI,否则不能期望不同编译器或甚至同一编译器不同主要版本编译出的C++库是二进制兼容的。
因此,在构建跨平台库、混合语言项目或在大型团队中统一开发环境时,选择和遵守一致的ABI是至关重要的。
实际应用与工具
理解名称重整不仅是学术上的兴趣,在实际的开发中也具有重要的指导意义。
1. 诊断链接错误
当链接器报告“undefined reference”或“multiple definition”错误时,它通常会显示重整名。例如,看到 undefined reference to '_ZN7MyClass8method_bEi',你会立即知道链接器找不到 MyClass 类中接收一个 int 参数的 method_b 函数。这可能是:
- 函数定义缺失。
- 函数签名不匹配(例如,代码中调用的是
method_b(double),但期望的是method_b(int))。 - 命名空间或类名拼写错误。
- 忘记链接包含该函数定义的目标文件或库。
2. extern "C":禁用名称重整
C语言没有函数重载,因此也没有名称重整机制。C编译器生成的函数符号通常就是函数名本身(有时带一个下划线前缀,如 _foo)。
当C++代码需要调用C库中的函数,或C代码需要调用C++中定义的一些函数时,就必须禁用C++的名称重整,以确保C和C++编译器能够识别相同的符号。这通过 extern "C" 链接指示符来实现:
// c_header.h (C头文件,可能被C++文件包含)
#ifdef __cplusplus
extern "C" { // 告诉C++编译器,这部分代码使用C链接约定
#endif
void c_function_from_c_lib(int arg);
int get_value_from_c_lib();
#ifdef __cplusplus
}
#endif
// cpp_code.cpp (C++源文件,调用C函数,并导出C++函数给C调用)
#include "c_header.h" // 确保 extern "C" 生效
// C++代码调用C函数
void call_c_apis() {
c_function_from_c_lib(100);
int val = get_value_from_c_lib();
// ...
}
// C++函数,但希望以C链接约定导出,供C代码调用
#ifdef __cplusplus
extern "C" {
#endif
void cpp_function_for_c(double x) {
// 这个C++函数将不会被重整
// 但它不能被重载,因为C语言不支持重载
}
#ifdef __cplusplus
}
#endif
extern "C" 指示编译器对其中的符号使用C语言的链接约定,即不进行名称重整。这意味着 cpp_function_for_c 在二进制层面只有一个C风格的名字 cpp_function_for_c(或 _cpp_function_for_c),因此它不能被重载。这是C++与C语言进行互操作的桥梁。
3. 符号查看与反重整工具
nm(Linux/Unix): 用于列出目标文件、可执行文件或库中的符号表。nm your_library.a: 显示原始重整名。nm -C your_library.a: 尝试对重整名进行反重整(demangle),显示更易读的C++签名。
objdump(Linux/Unix): 更强大的工具,可以显示目标文件的更多信息,包括符号表、段信息、反汇编代码等。objdump -t your_object.o: 显示符号表。
c++filt(Linux/Unix): 专门用于反重整C++符号的命令行工具。echo _ZN7MyClass8method_aEv | c++filt会输出MyClass::method_a()。
dumpbin(Windows): Visual Studio 提供的一个命令行工具,类似于objdump。dumpbin /SYMBOLS your_object.obj: 显示符号表。
undname(Windows): Visual Studio 提供的一个命令行工具,类似于c++filt。undname ?func@@YAXH@Z: 反重整MSVC的符号。
这些工具在分析第三方库、诊断链接问题、进行逆向工程或只是好奇C++底层实现时都非常有用。
高级主题与边缘情况
1. 模板元编程与复杂重整名
当C++代码中使用了复杂的模板,特别是模板元编程(TMP)技术时,生成的重整名可能会变得异常冗长和复杂。编译器需要将所有模板参数(包括类型参数、非类型参数,甚至模板模板参数)都编码进重整名中,以保证其唯一性。过长的重整名可能会在某些工具或系统上引发问题,但现代编译器和链接器通常能很好地处理它们。
2. Lambda表达式的重整
C++11引入的Lambda表达式会在编译时生成一个匿名类的闭包(closure)类型,以及该类型的一个 operator() 成员函数。这个匿名类的名称也是由编译器生成的,并且会经过名称重整。由于其匿名性,这些重整名通常是编译器内部的特定模式,难以直接理解,但在调试器中仍然可以被识别和反重整。
3. 虚函数表(vtable)和运行时类型信息(RTTI)
C++的虚函数机制依赖于虚函数表(vtable),而RTTI则依赖于类型信息对象(std::type_info)。这些内部数据结构也需要唯一的符号来标识。编译器会为每个类生成一个 vtable 符号和一个 type_info 符号,它们同样会经过名称重整。例如,type_info 对象的重整名通常是 _ZTI<mangled_class_name>,而 vtable 则是 _ZTV<mangled_class_name>。这些都是确保C++运行时多态和类型识别的关键。
4. COMDAT 段与模板实例化
在C++中,模板函数和类的定义通常放在头文件中。这可能导致一个问题:当多个编译单元