📘 C++ 中成员函数与普通函数的地址是否唯一?
在 C++ 编程中,我们经常会使用函数指针来引用普通函数或类的成员函数。表面上看,它们都像是函数的地址,但实际上它们在底层的行为和内存模型是截然不同的。本篇文章将深入讲解这两者的区别,特别是函数地址是否唯一这个问题。
🔹 一、普通函数的地址是唯一的吗?
✅ 答案:是的,普通函数的地址是唯一的
普通函数在编译时会被编译器放入代码段(.text 段) ,所有地方使用的都是这段代码的唯一地址。
示例:
c
#include <iostream>
void hello() {
std::cout << "Hello!" << std::endl;
}
int main() {
void (*fp1)() = &hello;
void (*fp2)() = &hello;
std::cout << "fp1 = " << (void*)fp1 << std::endl;
std::cout << "fp2 = " << (void*)fp2 << std::endl;
}
输出示例:
ini
fp1 = 0x4005d6
fp2 = 0x4005d6
✅ 两个函数指针地址一致,说明函数地址是唯一的。
🔹 二、成员函数的地址是唯一的吗?
✅ 答案:成员函数代码是唯一的,但指针形式可能更复杂,不总是简单地址。
解释:
- 成员函数的实现(代码)在内存中只有一份,也位于代码段。
- 但成员函数指针 (如
void (Class::*)()
)并不直接表示函数地址,而是一个结构体或偏移量。 - 它依赖于当前对象的
this
指针来访问属性。 - 编译器在调用时会自动传入
this
。
🔸 举例说明:
csharp
class Person {
public:
int age;
void setAge(int a) {
age = a; // 实际是 this->age = a
}
};
int main() {
void (Person::*mptr)(int) = &Person::setAge;
Person p1, p2;
(p1.*mptr)(10); // p1.age = 10
(p2.*mptr)(20); // p2.age = 20
}
内部原理:
arduino
// 编译器会翻译为:
Person::setAge(&p1, 10); // this = &p1
函数里访问 age
实际上是:
ini
this->age = a;
🔹 三、成员函数指针 vs 普通函数指针
类型 | 是否唯一 | 内部结构 | 访问依赖 | 调用方式 |
---|---|---|---|---|
普通函数指针 | ✅ 唯一地址 | 简单地址 | 无 | fp() 或 (*fp)() |
成员函数指针(非虚) | ✅ 函数唯一 指针可能结构化 | 函数地址 + this偏移(可能) | 需要对象 | (obj.*fptr)() |
成员函数指针(虚函数、多继承) | ❌ 结构更复杂 | 包含虚表偏移、this调整等 | 需要对象 + 虚表 | (obj.*fptr)() |
🔹 四、为什么成员函数能访问对象属性?
✅ 关键在于:this
指针
- 每次调用成员函数,编译器自动传入对象的地址(即
this
)。 - 所以虽然函数代码是共享的,但它总是通过
this->
来访问当前对象的成员变量。
类比说明:
成员函数像是一把剪刀,剪刀是共享的(函数唯一),但你每次使用它剪不同的布(不同对象),你告诉它剪哪块布(this),结果就不同。
🔹 五、内存模型对比(示意)
假设类如下:
csharp
class MyClass {
public:
int x;
void foo();
};
创建两个对象:
css
MyClass a, b;
内存示意:
lua
堆或栈:
+---------+ +---------+
| a 对象 | ----> | x: 10 |
+---------+ +---------+
| b 对象 | ----> | x: 20 |
+---------+ +---------+
代码段:
+------------------+
| MyClass::foo() | ← 所有对象共享这段代码
+------------------+
✅ 总结一句话:
普通函数的指针是全局唯一的地址;成员函数的代码是唯一的,但它通过
this
指针访问不同对象的成员变量,成员函数指针本身可能有额外信息来支持多态或继承。