c++面试:符号修饰
在C++中,名称修饰(Name Mangling)是一种将函数或变量的名称转换为唯一标识符的过程。它主要用于解决C++中的一些复杂特性,例如重载、类的作用域、模板等。以下是关于C++名称修饰的详细介绍:
1. 名称修饰的必要性
- 函数重载:C++允许函数重载,即多个函数可以有相同的名称,但参数类型或数量不同。例如:
void print(int x);
void print(double x);
在链接阶段,链接器需要区分这些函数。如果没有名称修饰,链接器无法区分 print 函数的不同版本。
- 类的作用域:C++中的成员函数和成员变量属于类的作用域。例如:
class MyClass {
void func();
};
如果没有名称修饰,链接器无法区分 MyClass::func其他 和类中的 func 函数。
- 模板实例化:模板函数在编译时会根据模板参数生成具体的函数。例如:
template <typename T>
void print(T x);
当调用 print<int>
和 print<double>
时,编译器会生成两个不同的函数。名称修饰可以为这些实例化生成唯一的名称。
2. 名称修饰的规则
2.1函数
名称修饰的具体规则因编译器而异,但大多数编译器遵循一些通用的模式。以下是一些常见的规则:
- 普通函数:
void func();
修饰后的名称可能类似于 _Z4funcv,其中 _Z 是C++名称修饰的前缀,4func 表示函数名称,v 表示无参数(void)。
- 带参数的函数:
void func(int x);
修饰后的名称可能类似于 _Z4funci,其中 i 表示参数类型为 int。
- 重载函数:
void func(int x);
void func(double x);
修饰后的名称可能分别为 _Z4funci 和 _Z4funcd。
- 类的成员函数:
class MyClass {
void func();
};
修饰后的名称可能类似于 _ZN7MyClass4funcEv,其中 _ZN 表示类的作用域,7MyClass 表示类名,4func 表示函数名,Ev 表示无参数的成员函数。
- 静态成员函数:
class MyClass {
static void func();
};
修饰后的名称可能类似于 _ZN7MyClass4funcE,与普通成员函数类似,但没有 v(表示无参数)。
2.2 变量名称修饰
在C++中,普通变量(如局部变量、全局变量等)的名称修饰(Name Mangling)主要取决于它们的作用域和链接属性。编译器对普通变量的命名修饰行为与函数的命名修饰有所不同。以下是关于普通变量命名修饰的详细说明:
- 全局变量的命名修饰
全局变量是具有外部链接(External Linkage)的变量,它们在程序的多个地方可以被访问。因此,编译器会对全局变量进行命名修饰,以确保它们在链接时具有唯一性。
int globalVar;
在GCC编译器中,全局变量 globalVar 可能会被修饰为 _Z10globalVar 或 _globalVar,具体取决于编译器的规则。
- 局部变量的命名修饰
局部变量(Local Variables)是定义在函数或代码块内部的变量,它们的作用域仅限于定义它们的函数或代码块。由于局部变量的作用域是局部的,它们不会参与链接过程,因此编译器通常不会对局部变量进行命名修饰。
void func() {
int localVar;
}
在这个例子中,localVar 是一个局部变量,它的作用域仅限于 func() 函数内部。编译器不会对 localVar 进行命名修饰,因为它的作用域是局部的,不会与其他变量冲突。
- 类的静态成员变量的命名修饰
类的静态成员变量属于类的作用域,它们在类的所有对象之间共享。因此,编译器会对类的静态成员变量进行命名修饰,以确保它们在链接时具有唯一性。
class MyClass {
static int staticVar;
};
在GCC编译器中,MyClass::staticVar 可能会被修饰为 _ZN7MyClass10staticVarE,其中:
_ZN 是类作用域的标识符。
7MyClass 是类名,7 表示类名的长度。
10staticVar 是变量名,10 表示变量名的长度。
E 是结束标识符。
- 类的普通成员变量的命名修饰
类的普通成员变量属于类的作用域,但它们是类实例的一部分,不会直接参与链接过程。因此,编译器通常不会对普通成员变量进行命名修饰。不过,编译器会在内部为每个对象生成唯一的偏移量来访问这些成员变量。
class MyClass {
int memberVar;
};
在这个例子中,memberVar 是类 MyClass 的普通成员变量。编译器不会对 memberVar 进行命名修饰,但会在对象的内存布局中为它分配一个偏移量。
- 静态局部变量的命名修饰
静态局部变量(Static Local Variables)是定义在函数内部的静态变量,它们的作用域仅限于函数内部,但生命周期与程序的运行时间相同。由于静态局部变量的作用域是局部的,编译器通常不会对静态局部变量进行命名修饰。然而,在目标文件级别,所有符号都必须是唯一的,因此编译器需要一种方法来确保即使是相同名称的静态变量也能被唯一识别。
void func() {
static int foo = 0;
}
int main() {
static int foo = 1;
func();
}
在这个例子中,尽管main和func内部都有名为foo的静态变量,但它们会被GCC分别修饰为:
main内的foo:_ZZ4mainE3foo
func内的foo:_ZZ4funcvE3foo
- 模板实例化的变量命名修饰
模板实例化的变量(如模板类的静态成员变量)在实例化时会根据模板参数生成不同的变量。编译器会对这些变量进行命名修饰,以确保它们在链接时具有唯一性。
template <typename T>
struct MyStruct {
static T value;
};
int main() {
MyStruct<int>::value = 10;
MyStruct<double>::value = 20.5;
}
在GCC编译器中,MyStruct::value 和 MyStruct::value 可能会被修饰为 _ZN8MyStructIiE5valueE 和 _ZN8MyStructIdE5valueE,其中:
_ZN 是类作用域的标识符。
8MyStruct 是类名,8 表示类名的长度。
IiE 和 IdE 分别表示模板参数 int 和 double。
5value 是变量名,5 表示变量名的长度。
E 是结束标识符。
3. 名称修饰的缺点
- 编译器依赖性:不同编译器的名称修饰规则不同,这可能导致跨编译器链接时出现问题。
- 复杂性:名称修饰规则复杂,难以手动解析,增加了理解和调试的难度。
4. 名称修饰的工具
c++filt:这是一个常用的工具,可以将修饰后的名称转换回可读的形式。例如:
echo _ZN7MyClass4funcEv | c++filt
输出结果为:
MyClass::func()
5. 名称修饰在C++中的应用
- 链接器:链接器使用修饰后的名称来区分不同的函数和变量。
- 动态库:在动态库中,修饰后的名称用于符号解析。
- 跨语言调用:当C++代码与C代码或其他语言的代码交互时,需要了解名称修饰规则,以确保正确链接。
6. 如何避免名称修饰问题
使用C语言链接规范:在C++代码中声明与C语言代码交互的函数时,可以使用 extern “C”:
extern "C" {
void cFunction();
}
这样可以避免名称修饰,使C++函数在链接时使用C语言的命名规则。