在 C++ 编程中,指针是操作内存的核心工具。除了我们熟悉的指向对象或变量的普通指针外,还有一种特殊的指针类型 ——类成员指针(Pointer to Class Members)。它允许我们在不绑定具体对象的情况下,指向类的成员(数据成员或成员函数),并在运行时动态访问这些成员。这种特性在框架设计、反射机制、状态机等场景中有着不可替代的作用。
目录
一、为什么需要类成员指针?
在正式讲解语法前,我们先思考一个问题:普通指针为什么无法直接指向类的成员?
假设我们有一个类Person
:
class Person {
public:
std::string name;
void greet() { std::cout << "Hello, " << name << "\n"; }
};
如果尝试用普通指针指向name
或greet()
,会遇到什么问题?
- 普通指针存储的是内存地址,但类的成员(尤其是非静态成员)的地址依赖于具体的对象实例。不同对象的
name
成员存储在不同的内存位置,普通指针无法抽象出 “类成员” 的通用位置。 - 成员函数的调用需要隐含的
this
指针,普通函数指针无法处理这种隐含参数。
类成员指针的价值:它抽象了类成员的 “相对位置”(相对于类对象的起始地址的偏移量),使得我们可以用统一的方式操作不同对象的同一成员。例如,用同一个成员指针,既可以访问对象 A 的name
,也可以访问对象 B 的name
。
二、类成员指针的声明
类成员指针的声明需要明确两个关键信息:
- 指向的成员所属的类(通过类名限定);
- 指向的成员的类型(数据成员的类型或成员函数的签名)。
2.1 数据成员指针的声明
语法:类型 类名::*指针变量名;
例如,指向Person
类中std::string
类型数据成员的指针:
std::string Person::*p_name; // 声明一个指向Person类string类型数据成员的指针
初始化:数据成员指针需要绑定具体的类成员,语法为&类名::成员名
:
p_name = &Person::name; // 指针p_name指向Person类的name成员
注意:数据成员指针不存储实际的内存地址,而是存储成员相对于类对象起始地址的偏移量(offset)。这个偏移量是编译期确定的常量(对于非虚、非静态成员)。
2.2 成员函数指针的声明
语法:返回类型 (类名::*指针变量名)(参数列表) cv限定符 引用限定符;
参数说明:
cv限定符
:const
/volatile
,用于匹配类的常成员函数;引用限定符
:&
/&&
,用于匹配左值 / 右值成员函数(C++11 引入)。
例如,指向Person
类中greet
成员函数的指针(无参数、返回void
):
void (Person::*p_greet)() = &Person::greet; // 声明并初始化
如果成员函数是const
的:
class Person {
public:
void greet() const { // const成员函数
std::cout << "Hello, " << name << "\n";
}
};
void (Person::*p_greet_const)() const = &Person::greet; // 必须匹配const限定符
如果成员函数有参数:
class Calculator {
public:
int add(int a, int b) { return a + b; }
};
int (Calculator::*p_add)(int, int) = &Calculator::add; // 参数列表必须完全匹配
2.3 为成员指针使用类型别名
直接声明成员指针的语法较为复杂,尤其是成员函数指针。C++ 提供了typedef
和using
来简化类型别名。
示例 1:数据成员指针的别名
using NamePtr = std::string Person::*; // 类型别名NamePtr表示指向Person类string成员的指针
NamePtr p_name = &Person::name; // 使用别名声明指针
示例 2:成员函数指针的别名
// 使用using定义成员函数指针类型
using GreetFunc = void (Person::*)();
GreetFunc p_greet = &Person::greet;
// 对于带参数和const限定的函数
using AddFunc = int (Calculator::*)(int, int);
AddFunc p_add = &Calculator::add;
技巧:使用类型别名可以让代码更易读,尤其是在模板元编程或函数指针表中。
三、类成员指针的使用
声明并初始化成员指针后,需要结合具体的对象实例来访问成员。C++ 提供了两个特殊操作符:
.*
:通过对象或引用访问成员指针指向的成员;->*
:通过对象指针访问成员指针指向的成员。
3.1 使用数据成员指针
场景:动态访问不同对象的同一数据成员。
示例代码:
#include <iostream>
#include <string>
class Person {
public:
std::string name;
int age;
};
int main() {
Person alice{"Alice", 25};
Person bob{"Bob", 30};
// 声明数据成员指针并初始化
std::string Person::*p_name = &Person::name;
int Person::*p_age = &Person::age;
// 通过对象访问
std::cout << "Alice's name: " << alice.*p_name << "\n"; // 输出:Alice's name: Alice
std::cout << "Alice's age: " << alice.*p_age << "\n"; // 输出:Alice's age: 25
// 通过对象指针访问
Person* p_bob = &bob;
std::cout << "Bob's name: " << p_bob->*p_name << "\n"; // 输出:Bob's name: Bob
std::cout << "Bob's age: " << p_bob->*p_age << "\n"; // 输出:Bob's age: 30
return 0;
}
alice.*p_name
等价于alice.name
;p_bob->*p_name
等价于p_bob->name
;- 数据成员指针可以指向任何对象的对应成员,只要对象类型与指针声明的类一致。
3.2 使用成员函数指针
场景:动态调用不同对象的同一成员函数,或实现 “函数回调” 的灵活控制。
示例代码:
#include <iostream>
#include <string>
class Person {
public:
std::string name;
void greet() const { std::cout << "Hello, " << name << "!\n"; }
void introduce(int age) { std::cout << name << ", " << age << " years old.\n"; }
};
int main() {
Person alice{"Alice"};
Person* p_bob = new Person{"Bob"};
// 声明成员函数指针并初始化
void (Person::*p_greet)() const = &Person::greet;
void (Person::*p_introduce)(int) = &Person::introduce;
// 通过对象调用成员函数
(alice.*p_greet)(); // 输出:Hello, Alice!
// 通过对象指针调用成员函数
(p_bob->*p_greet)(); // 输出:Hello, Bob!
// 调用带参数的成员函数
(alice.*p_introduce)(25); // 输出:Alice, 25 years old.
(p_bob->*p_introduce)(30); // 输出:Bob, 30 years old.
delete p_bob;
return 0;
}
- 成员函数指针的调用必须通过
.*
或->*
操作符,且需要绑定具体的对象(提供this
指针); - 函数参数必须与指针声明的参数列表完全匹配;
const
成员函数的指针必须匹配const
限定符(否则编译错误)。
3.3 成员函数指针表:批量管理成员函数
在实际开发中,我们可能需要将多个成员函数指针存储在一个容器(如数组、std::vector
或std::map
)中,形成函数指针表。这种技术常用于:
- 实现状态机(根据当前状态调用对应的处理函数);
- 实现反射机制(通过字符串映射到成员函数);
- 简化重复的函数调用逻辑。
示例:通过函数指针表实现计算器
#include <iostream>
#include <vector>
#include <functional> // 用于std::function
class Calculator {
public:
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return b != 0 ? a / b : 0; }
};
int main() {
Calculator calc;
using CalcFunc = int (Calculator::*)(int, int); // 成员函数指针类型别名
// 函数指针表:存储4个成员函数指针
std::vector<CalcFunc> func_table = {
&Calculator::add,
&Calculator::sub,
&Calculator::mul,
&Calculator::div
};
// 测试数据
int a = 10, b = 5;
// 遍历函数指针表,调用每个函数
for (auto func : func_table) {
int result = (calc.*func)(a, b); // 通过对象调用成员函数
std::cout << "Result: " << result << "\n";
}
return 0;
}
输出结果:
扩展:结合std::map
实现命名函数表
如果需要通过函数名(如字符串)动态查找函数,可以使用std::map
:
#include <map>
#include <string>
// 在Calculator类中添加函数名映射
std::map<std::string, CalcFunc> named_func_table = {
{"add", &Calculator::add},
{"sub", &Calculator::sub},
{"mul", &Calculator::mul},
{"div", &Calculator::div}
};
// 使用示例
std::string op = "mul";
auto it = named_func_table.find(op);
if (it != named_func_table.end()) {
int result = (calc.*it->second)(a, b); // 调用mul函数
std::cout << "Mul result: " << result << "\n"; // 输出50
}
3.4 注意:静态成员与成员指针
类的静态成员(static
成员)不属于任何对象实例,因此指向静态成员的指针与普通指针语法一致:
- 静态数据成员指针:
类型* 指针变量名 = &类名::静态成员
;- 静态成员函数指针:
返回类型(*指针变量名)(参数列表) = &类名::静态成员函数
。
示例:静态成员指针的使用
class Math {
public:
static int pi; // 静态数据成员
static int square(int x) { return x * x; } // 静态成员函数
};
int Math::pi = 3; // 静态成员定义
int main() {
// 静态数据成员指针
int* p_pi = &Math::pi;
std::cout << "pi: " << *p_pi << "\n"; // 输出:pi: 3
// 静态成员函数指针
int (*p_square)(int) = &Math::square;
std::cout << "square(5): " << p_square(5) << "\n"; // 输出:25
return 0;
}
关键区别:静态成员指针不涉及
类名::*
语法,因为静态成员的地址是全局的,不依赖对象实例。
四、类成员指针的高级应用
4.1 模板与成员指针的结合
模板可以让成员指针的使用更通用。例如,实现一个通用函数,通过成员指针获取对象的某个属性:
示例:通用属性获取函数
#include <iostream>
#include <string>
// 模板函数:通过数据成员指针获取对象的属性
template <typename T, typename Class>
T get_property(const Class& obj, T Class::*member_ptr) {
return obj.*member_ptr;
}
class Person {
public:
std::string name;
int age;
};
int main() {
Person alice{"Alice", 25};
// 获取name属性
std::string name = get_property(alice, &Person::name);
std::cout << "Name: " << name << "\n"; // 输出:Name: Alice
// 获取age属性
int age = get_property(alice, &Person::age);
std::cout << "Age: " << age << "\n"; // 输出:Age: 25
return 0;
}
这个模板函数可以适配任何类的任何数据成员,只要成员类型匹配。
4.2 成员指针与虚函数
如果类中包含虚函数,成员函数指针如何处理动态绑定?
结论:成员函数指针的类型由声明时的函数签名决定,但调用时会根据对象的实际类型动态绑定虚函数。
示例:虚函数与成员指针
#include <iostream>
class Animal {
public:
virtual void speak() { std::cout << "Animal speaks\n"; }
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Dog barks\n"; }
};
int main() {
Animal* animal = new Dog(); // 基类指针指向派生类对象
// 成员函数指针指向基类的虚函数
void (Animal::*p_speak)() = &Animal::speak;
// 调用成员函数指针:动态绑定到派生类的实现
(animal->*p_speak)(); // 输出:Dog barks
delete animal;
return 0;
}
即使p_speak
声明为指向Animal::speak
的指针,但由于animal
实际指向Dog
对象,调用时会执行Dog::speak
。与直接调用虚函数的行为一致。
4.3 成员指针的大小与存储
在 32 位系统中,普通指针占 4 字节,而成员指针的大小可能更大(取决于类的结构):
- 对于非虚、非继承的类,数据成员指针的大小等于普通指针(存储偏移量);
- 对于包含虚函数或多重继承的类,成员指针可能需要存储额外信息(如虚表指针偏移量),此时大小可能为 8 字节(32 位系统)或 16 字节(64 位系统)。
示例:成员指针的大小测试
#include <iostream>
class A {
public:
int x;
};
class B : public A {
public:
int y;
};
class C {
public:
virtual void func() {}
};
int main() {
int A::*p_a = &A::x; // 获取public成员的指针(合法)
int B::*p_b = &B::y; // 获取public成员的指针(合法)
void (C::*p_c)() = &C::func; // 获取public虚函数的指针(合法)
std::cout << "Size of p_a: " << sizeof(p_a) << "\n"; // 输出指针大小(64位系统通常为8)
std::cout << "Size of p_b: " << sizeof(p_b) << "\n"; // 输出指针大小(64位系统通常为8)
std::cout << "Size of p_c: " << sizeof(p_c) << "\n"; // 输出指针大小(64位系统通常为16,含虚表信息)
return 0;
}
注意:成员指针的大小因编译器和类结构而异,不可依赖具体数值。
五、类成员指针的局限与替代方案
虽然成员指针功能强大,但也存在一些局限性:
- 语法复杂:尤其是成员函数指针的声明和使用,对新手不够友好;
- 运行时开销:通过成员指针调用函数比直接调用略慢(需要计算偏移量或虚表指针);
- 类型严格性:成员指针的类型必须与类和成员的类型完全匹配,缺乏灵活性。
替代方案:
std::function
:C++11 引入的std::function
可以封装任何可调用对象(包括成员函数),结合std::bind
可以更灵活地绑定对象和成员指针;- 虚函数与多态:如果需要动态调用不同类的同名函数,虚函数可能更直观;
- 反射库:部分第三方库(如 Boost.Reflection)提供了更高级的反射机制,可通过字符串直接访问成员。
示例:使用std::function
封装成员函数
#include <iostream>
#include <functional>
class Person {
public:
std::string name;
void greet() const { std::cout << "Hello, " << name << "!\n"; }
};
int main() {
Person alice{"Alice"};
// 使用std::function和std::bind封装成员函数
std::function<void()> greet_alice = std::bind(&Person::greet, &alice);
greet_alice(); // 输出:Hello, Alice!
return 0;
}
六、总结
类成员指针是 C++ 中非常强大但容易被忽视的特性,它允许我们在运行时动态访问类的成员,为框架设计、反射机制等复杂场景提供了底层支持。本文从声明、使用、高级应用等多个维度展开,总结了以下核心要点:
知识点 | 关键细节 |
---|---|
数据成员指针声明 | 类型 类名::*指针名 ,存储成员相对于对象的偏移量 |
成员函数指针声明 | 返回类型 (类名::*指针名)(参数列表) cv限定符 ,必须匹配函数签名 |
类型别名 | 使用using 或typedef 简化复杂类型声明 |
成员指针使用 | 通过.* (对象 / 引用)或->* (指针)操作符绑定具体对象 |
函数指针表 | 存储多个成员函数指针,实现批量调用或动态调度 |
静态成员指针 | 与普通指针语法一致,不依赖对象实例 |
虚函数与成员指针 | 调用时动态绑定,与直接调用虚函数行为一致 |
掌握类成员指针后,可以更灵活地设计 C++ 程序,尤其是在需要动态访问类成员的场景中(如自定义序列化库、事件驱动框架)。当然,在实际开发中也需权衡语法复杂度和运行时效率,选择最适合的技术方案。
思考题:如何用成员指针实现一个通用的 “对象属性遍历” 函数,输出对象所有数据成员的名称和值?(提示:结合元编程或预处理器宏)