[Itanium C++ ABI] 成员指针

本文深入解析C++中数据成员指针、非虚函数成员指针及虚函数成员指针的概念与内部实现机制,通过示例代码说明不同类型的成员指针如何指向类的数据成员和成员函数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

总结

  • 数据成员指针,其值是数据成员相对于对象起始地址的偏移,空指针用-1表示;
  • 非虚函数成员指针,ptr部分内容为函数指针,指向一个全局函数,该函数的第一个参数为this指针;adj部分始终为0;
  • 虚成员函数指针,ptr部分内容为虚函数对应的函数指针在虚函数表中的偏移地址加1;adj部分为调节this指针的偏移,目的是指向相应的虚函数表;

数据成员指针

对于普通指针变量来说,其值是它所指向的地址,0表示空指针;
对于数据成员指针变量来说,其值是数据成员相对于对象起始地址的偏移,空指针用-1表示;

struct X {
    int a;
    int b;
};

#define VALUE_OF_PTR(p)     (*(long*)&p)

int main() {
    int X::*p = 0;
    cout << VALUE_OF_PTR(p) << endl;

    p = &X::a;
    cout << VALUE_OF_PTR(p) << endl;

    p = &X::b;
    cout << VALUE_OF_PTR(p) << endl;

    return 0;
}

函数成员指针

其size为普通函数指针的两倍(x64下为16字节),分为:ptr和adj两部分;

  • 非虚函数成员指针

ptr部分内容为函数指针(指向一个全局函数,该函数的第一个参数为this指针),adj部分始终为0

extern "C" int printf(const char*, ...);

struct B {
    void foo() {  printf("B::foo(): this = 0x%p\n", this); }
};

struct D : public B {
    void bar() { printf("D::bar(): this = 0x%p\n", this); }
};

void (B::*pbfoo)() = &B::foo; // ptr: points to _ZN1B3fooEv, adj: 0
void (D::*pdfoo)() = &D::foo; // ptr: points to _ZN1B3fooEv, adj: 0
void (D::*pdbar)() = &D::bar; // ptr: points to _ZN1D3barEv, adj: 0

extern "C" void _ZN1B3fooEv(B*);
extern "C" void _ZN1D3barEv(D*);

#define PART1_OF_PTR(p)     (((long*)&p)[0])
#define PART2_OF_PTR(p)     (((long*)&p)[1])

int main() {
    printf("%p\n", _ZN1B3fooEv);
    printf("%p\n\n", _ZN1D3barEv);

    printf("&B::foo->ptr: 0x%lx\n", PART1_OF_PTR(pbfoo));
    printf("&B::foo->adj: 0x%lx\n", PART2_OF_PTR(pbfoo));

    printf("&D::foo->ptr: 0x%lx\n", PART1_OF_PTR(pdfoo));
    printf("&D::foo->adj: 0x%lx\n", PART2_OF_PTR(pdfoo));

    printf("&D::bar->ptr: 0x%lx\n", PART1_OF_PTR(pdbar));
    printf("&D::bar->adj: 0x%lx\n\n", PART2_OF_PTR(pdbar));


    D* d = new D();
    printf("%p\n", (long *)d);

    d->foo();
    _ZN1B3fooEv(d); // equal to d->foo()

    d->bar();
    _ZN1D3barEv(d); // equal to d->bar()

    return 0;
}

  • 函数成员指针

ptr部分内容为虚函数对应的函数指针在虚函数表中的偏移地址加1
adj部分为调节this指针的偏移字节数;

extern "C" int printf(const char*, ...);

struct A {
    virtual void foo() { printf("A::foo(): this = 0x%p\n", this); }
};
struct B {
    virtual void bar() { printf("B::bar(): this = 0x%p\n", this); }
};
struct C : public A, public B {
    virtual void quz() { printf("C::quz(): this = 0x%p\n", this); }
};

void (A::*pafoo)() = &A::foo;   // ptr: 1, adj: 0

void (B::*pbbar)() = &B::bar;   // ptr: 1, adj: 0

void (C::*pcfoo)() = &C::foo;   // ptr: 1, adj: 0
void (C::*pcquz)() = &C::quz;   // ptr: 9, adj: 0
void (C::*pcbar)() = &C::bar;   // ptr: 1, adj: 8

#define PART1_OF_PTR(p)     (((long*)&p)[0])
#define PART2_OF_PTR(p)     (((long*)&p)[1])

int main() {
    printf("&A::foo->ptr: 0x%lX, ", PART1_OF_PTR(pafoo));   // 1
    printf("&A::foo->adj: 0x%lX\n", PART2_OF_PTR(pafoo));   // 0

    printf("&B::bar->ptr: 0x%lX, ", PART1_OF_PTR(pbbar));   // 1
    printf("&B::bar->adj: 0x%lX\n", PART2_OF_PTR(pbbar));   // 0

    printf("&C::foo->ptr: 0x%lX, ", PART1_OF_PTR(pcfoo));   // 1

    printf("&C::quz->ptr: 0x%lX, ", PART1_OF_PTR(pcquz));   // 9
    printf("&C::quz->adj: 0x%lX\n", PART2_OF_PTR(pcquz));   // 0

    printf("&C::bar->ptr: 0x%lX, ", PART1_OF_PTR(pcbar));   // 1
    printf("&C::bar->adj: 0x%lX\n", PART2_OF_PTR(pcbar));   // 8
    return 0;
}
  • A和B都没有基类,但是都有虚函数,因此各有一个虚函数指针(以及虚函数表);
  • C同时继承了A和B,因此会有两个虚函数指针(以及两张虚函数表);
  • C本身也有虚函数,但是为了节省空间,C会与主基类A公用一个虚函数指针、虚函数表;
  • C没有重写继承自A和B的虚函数,因此在C的虚函数表中存在A::foo和B::bar函数指针;
  • 如果C中重写了foo(),则C的虚函数表中A::foo会被替换为C::foo;(因此,C的虚函数表是单独分配的,而非共用)
  • A::foo(C::foo)、B::Bar(C::bar)都在虚函数表中偏移地址为0的位置,因此ptr为1(0+1=1),之所以加1是为了用0表示空指针;
  • C::quz在偏移为8的位置,因此ptr为9(8+1=9);
  • 当我们使用pc调用C::bar()时,如:“(pc->*pcbar)()”,实际上调用的是B::bar()(即_ZN1B3barEv(pc))
    • pc指向的C中包含两个虚函数指针,其中第一个是来自A的虚函数表指针;
    • 为了调用B中的虚函数,就必须知道B的虚函数表;
    • B的虚函数表指针距离pc指向的起始位置为8,这也就是为什么adj == 8;
<think>我们正在讨论Linux环境下C++的函数重载机制。根据引用内容,我们可以总结以下几点: 1. 函数重载的条件:函数名相同,参数列表不同(包括参数个数、类型、顺序不同)。返回类型不同不能作为重载条件。 2. C++支持函数重载而C不支持的原因:C++使用名称修饰(name mangling)技术,将函数名和参数类型等信息组合成一个新的内部名称,使得链接器能够区分不同的函数。 3. Linux环境下,我们可以通过查看编译后的符号表来观察名称修饰的结果。 接下来,我们将详细讲解Linux C++函数重载的原理及使用。 ## 一、函数重载的基本概念 函数重载是指在同一作用域内,可以有一组具有相同函数名,但参数列表不同的函数。编译器根据调用时提供的实参类型和数量来选择最合适的函数版本。 例如: ```cpp void print(int i) { cout << "整数为: " << i << endl; } void print(double f) { cout << "浮点数为: " << f << endl; } void print(const char* s) { cout << "字符串为: " << s << endl; } ``` 这三个函数都叫`print`,但参数类型不同,因此构成重载。 ## 二、函数重载的原理:名称修饰(Name Mangling) 在C语言中,函数名在符号表中就是函数名本身,所以如果两个函数同名就会导致冲突。而C++为了支持重载,编译器会对函数名进行修饰,将参数类型等信息编码进新的函数名中。 在Linux环境下,通常使用GCC(或G++)编译器,它采用Itanium C++ ABI的名称修饰规则。我们可以通过以下步骤来观察名称修饰: 1. 编写一个包含重载函数的C++程序,例如`test.cpp`: ```cpp void func(int) {} void func(double) {} void func(int, double) {} ``` 2. 编译生成目标文件,并查看符号表: ```bash g++ -c test.cpp -o test.o nm test.o ``` 执行`nm`命令后,我们会看到类似下面的输出(具体修饰后的名称可能因编译器版本和平台略有不同): ``` 0000000000000000 T _Z4funci 000000000000000a T _Z4funcid 0000000000000014 T _Z4funcd ``` 这里,`_Z4funci`表示`func(int)`,`_Z4funcd`表示`func(double)`,`_Z4funcid`表示`func(int, double)`。修饰后的名称包含了函数名和参数类型的信息。 ## 三、函数重载的解析规则 当调用一个重载函数时,编译器会按照以下步骤进行解析: 1. 确定候选函数:与被调用函数同名的所有可见函数。 2. 确定可行函数:从候选函数中选出参数个数和类型匹配的函数(允许隐式转换)。 3. 寻找最佳匹配:如果有多个可行函数,则选择最匹配的那个(转换最少的)。 如果找不到可行函数,或者有多个可行函数但没有一个是最佳匹配(即存在二义性),则编译器会报错。 例如: ```cpp void f(int a, double b) {} void f(double a, int b) {} int main() { f(1, 2); // 错误:二义性调用,因为1可以转换为double,2可以转换为int,两个函数都可行且没有优劣之分 } ``` ## 四、函数重载的使用注意事项 1. 函数重载与作用域:重载必须在同一作用域内(例如同一个类中或同一个命名空间内)。如果在不同作用域,则会隐藏外层作用域的同名函数。 2. 避免二义性:设计重载函数时,要确保调用时不会出现二义性。 3. 返回类型:不能仅通过返回类型不同来重载函数。 4. 顶层const和底层const:参数是否为const在顶层const(直接修饰参数)时不能作为重载条件,因为实参传递时忽略顶层const。但如果是底层const(如指针或引用指向const对象),则可以区分。 ```cpp void func(int a); // 1 void func(const int a); // 2: 错误,与1重复,因为顶层const被忽略 void func(int *p); // 3 void func(const int *p); // 4: 正确,底层const不同,可以重载 ``` ## 五、在Linux环境中使用函数重载的实践 在Linux下开发C++程序时,函数重载是一个常用的特性。但需要注意以下几点: 1. 与C语言交互:当C++程序需要调用C语言编写的函数时,需要使用`extern "C"`来禁止名称修饰,以确保链接到正确的C函数。 2. 动态链接:在动态链接库中使用重载函数时,导出函数名是经过修饰的。因此,如果其他程序(尤其是非C++程序)要使用这些函数,需要避免使用重载,或者提供C接口封装。 例如,使用`extern "C"`: ```cpp extern "C" { // 这里声明的函数按照C语言方式编译(不进行名称修饰) void my_c_function(int); } ``` 总结:Linux环境下C++的函数重载依赖于编译器的名称修饰技术,使得同名函数根据参数列表的不同而拥有不同的内部名称。合理使用函数重载可以提高代码的可读性和灵活性,但要注意避免二义性,并正确处理与C语言的交互问题。 §§相关问题§§ 1. 如何在Linux下查看C++函数经过名称修饰后的符号名? 2. 函数重载时如何避免二义性? 3. 在C++中,const修饰函数参数时如何影响重载? 4. 为什么C语言不支持函数重载而C++支持? 5. 在编写动态链接库时,如何处理函数重载问题?</think>### Linux环境下C++函数重载机制详解 #### 一、函数重载基础 **定义**:函数重载允许在同一作用域内定义多个同名函数,通过**参数列表的差异**(类型/数量/顺序)区分实现。例如: ```cpp void print(int i); // 处理整型 void print(double f); // 处理浮点型 void print(const char* s); // 处理字符串 ``` **重载条件**(需同时满足): 1. 函数名相同 2. 参数列表不同(满足任一): - 参数类型不同(如 `int` vs `double`) - 参数数量不同(如 `func(int)` vs `func(int, int)`) - 参数顺序不同(如 `func(int, double)` vs `func(double, int)`) 3. 返回类型**不能**作为重载依据(仅返回类型不同会导致编译错误)[^3] > ⚠️ 注意:顶层 `const` 修饰(如 `void func(int)` 和 `void func(const int)`)**不构成重载**,编译器视为同一函数。 --- #### 二、Linux下的实现原理:名称修饰(Name Mangling) **核心机制**:C++编译器通过**名称修饰**将函数名和参数类型编码为唯一符号名,解决符号冲突问题。 **Linux实践验证**: 1. 创建测试文件 `overload.cpp`: ```cpp void func(int) {} // _Z4funci void func(double) {} // _Z4funcd void func(int, char) {} // _Z4funcic ``` 2. 编译查看符号表: ```bash g++ -c overload.cpp -o overload.o nm overload.o ``` 3. 输出结果示例: ``` 0000000000000000 T _Z4funci # func(int) 0000000000000006 T _Z4funcd # func(double) 000000000000000c T _Z4funcic # func(int, char) ``` **符号名规则**(Itanium ABI标准): - `_Z`: C++修饰前缀 - `4`: 函数名长度(`func`为4字符) - 后缀表示参数类型:`i`=int, `d`=double, `c`=char[^4] --- #### 三、重载解析规则 编译器按优先级匹配函数: 1. **精确匹配**(最高优先级) ```cpp print(42); // 调用 print(int) ``` 2. **类型提升匹配** ```cpp short s = 10; print(s); // short提升为int,调用print(int) ``` 3. **标准转换匹配** ```cpp print(3.14f); // float转换为double,调用print(double) ``` 4. **用户定义转换匹配**(最低优先级) > ⚠️ **二义性错误示例**: > ```cpp > void f(int, double); > void f(double, int); > f(1, 1); // 错误!两个函数都需一次标准转换 > ``` --- #### 四、Linux环境下的特殊注意事项 1. **与C语言交互**: ```cpp extern "C" { void c_func(); // 禁止名称修饰,确保C编译器可识别 } ``` 2. **动态链接库(.so)**: - 导出重载函数时需确保符号名一致 - 建议用 `extern "C"` 封装C接口,避免跨语言问题 3. **调试信息**: GDB调试器能自动解析修饰后的名称: ```bash (gdb) break _Z4funci # 断点打在func(int) ``` --- #### 五、最佳实践 1. **避免隐式转换二义性**: ```cpp // 推荐:明确参数类型 print(static_cast<int>(3.14)); ``` 2. **优先使用引用避免拷贝**: ```cpp void process(const std::string& s); // 高效处理字符串 ``` 3. **`nullptr` 解决空指针歧义**: ```cpp void func(int*); void func(char*); func(nullptr); // C++11起明确调用func(int*) ``` --- ### 总结 Linux下C++函数重载依赖编译器的**名称修饰技术**(如GCC的Itanium ABI),通过编码参数类型生成唯一符号名。合理使用可提升代码可读性,但需注意: - 避免二义性调用 - 处理跨语言交互时使用 `extern "C"` - 调试时理解修饰符号的命名规则 > 引用说明:名称修饰机制是C++支持函数重载的核心,不同编译器实现细节可能略有差异[^2][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值