C++ 成员指针详解
什么是成员指针?
C++ 中的成员指针(Pointer to Member)是一种特殊的指针,它指向类中的非静态成员,而不是指向内存中的特定地址。与常规指针不同,常规指针存储的是一个对象的内存地址,而成员指针存储的是一个成员相对于其所属对象的偏移量(对于数据成员)或一个标识/地址(对于成员函数),使得可以通过一个对象(或指向对象的指针)来访问该成员。
成员指针分为两种:
- 指向数据成员的指针 (Pointer to Data Member): 指向类中的非静态数据成员。
- 指向成员函数的指针 (Pointer to Member Function): 指向类中的非静态成员函数。
-
为什么成员指针指向类的“非静态”成员,而不是“静态”成员?
首先要知道静态成员(Static Members)属于类本身,而不是类的任何特定对象实例。无论创建了多少个类的对象,或者即使没有创建任何对象,静态成员都只有一份。
静态数据成员在内存中有固定的存储位置,这个位置在程序生命周期中(通常)是不变的,与任何对象实例的地址无关。
静态成员函数不依赖于特定的对象实例,因此它们没有 this 指针。
可以直接通过类名来访问(如 ClassName::static_member),或者通过对象实例访问(如 object.static_member,但这只是语法糖,实际上还是访问的类共享的那一份)。
因为静态成员有固定的内存地址(数据成员)或固定的调用入口(成员函数,类似于普通全局函数),所以我们可以使用常规指针来指向它们。
class MyClass { public: static int static_data; static void static_func() { /* ... */ } int non_static_data; }; int MyClass::static_data = 10; // 指向静态数据成员 (使用常规指针) int* ptr_to_static_data = &MyClass::static_data; // 指向静态成员函数 (使用常规函数指针) void (*ptr_to_static_func)() = &MyClass::static_func;
这里 ptr_to_static_data 存储的是 MyClass::static_data 变量的实际内存地址。ptr_to_static_func 存储的是 MyClass::static_func 函数的入口地址。
非静态数据成员是每个对象实例独有的。如果创建了 MyClass 的两个对象 obj1 和 obj2,那么 obj1.non_static_data 和 obj2.non_static_data 是存储在不同内存位置的两个不同变量(取决于MyClass的两个对象副本在内存中的位置)。
非静态成员函数在被调用时,需要一个特定的对象实例来操作,这个实例通过隐式的 this 指针传递给函数。
成员指针的设计初衷就是为了能够指向这些依赖于对象实例的非静态成员。它本身不绑定到任何特定对象的成员,而是描述了“如何在一个给定对象中找到某个成员”。
因此成员指针的特殊机制是为了处理非静态成员,这些成员的位置和行为与具体的对象实例紧密相关。
-
为什么成员指针存储的是“偏移量”或“标识/地址”,而非内存中特定的地址?
针对常规指针来说,比如
int* p
存储的是一个绝对内存地址。例如,int x = 10; int* p_x = &x;
如果变量 x 存储在内存地址 0x7FFF1234,那么指针 p_x 的值就是 0x7FFF1234。解引用*p_x
会直接访问这个内存地址上的数据。常规指针指向的是一个已经存在的、位于特定内存位置的数据或函数。而成员指针
int MyClass::*pmd
并不存储特定对象的特定成员的绝对地址,因为成员指针定义的是一个“类中非静态成员的柄”,它应该能用于该类的任何对象,并指向该类的某个对象的特定成员。如果一个成员指针一开始存储了特定对象的特定成员的绝对地址,这需要先存在一个类的对象并且它就只能用于指向该对象的特定成员,这违背了成员指针通用性,更退一步讲,这不符合设计需求,在编译时就行不通。为了满足成员指针设计之初的通用性,成员指针在定义时不关心类的某一个具体的对象,对象只作为类的一个副本。类在定义之初就确定了它的所有对象的结构,因此成员指针要存储指向类的数据成员的地址,只需要知道类的数据成员在类中的位置就可以了,即字节偏移量。
class Point { public: int x; // 假设在类开始处,偏移量 0 int y; // 假设 int 占 4 字节,y 的偏移量是 4 }; // 声明并初始化一个指向 Point::y 的成员指针 int Point::*ptr_y = &Point::y; // ptr_y 存储的不是任何具体 Point 对象的 y 成员的地址, // 而是类似 "4" 这样的偏移值。 Point p1; // 假设 p1 对象在内存地址 0x1000 处 // p1.x 就在 0x1000 // p1.y 就在 0x1000 + 4 = 0x1004 Point p2; // 假设 p2 对象在内存地址 0x2000 处 // p2.x 就在 0x2000 // p2.y 就在 0x2000 + 4 = 0x2004 // 当使用成员指针访问时: // int val_p1_y = p1.*ptr_y; // 编译器大致执行: // 1. 获取 p1 的起始地址 (0x1000) // 2. 获取 ptr_y 中存储的偏移量 (4) // 3. 计算实际地址:0x1000 + 4 = 0x1004 // 4. 从 0x1004 读取 int 值 // int val_p2_y = p2.*ptr_y; // 编译器大致执行: // 1. 获取 p2 的起始地址 (0x2000) // 2. 获取 ptr_y 中存储的偏移量 (4) // 3. 计算实际地址:0x2000 + 4 = 0x2004 // 4. 从 0x2004 读取 int 值
这个“偏移量”使得成员指针可以应用于任何 Point 对象。sizeof(int Point::*) 的大小可能与 sizeof(int*) 不同,因为它存储的类中数据成员的偏移量,其表示方式和大小由编译器决定。
相比数据成员指针,成员函数指针涉及更多复杂的内容,包括this指针的传递、虚函数、多重继承等。
对于非虚成员函数,函数本身的代码只有一份,位于内存的某个固定地址。成员函数指针可能存储这个函数的实际地址。当通过 (obj.*pmf)() 调用时,编译器不仅跳转到该函数地址,还会巧妙地将 obj 的地址作为 this 指针传递给函数。常规函数指针 void (*fp)() 只是一个地址。成员函数指针 void (MyClass::*mfp)() 虽然也可能包含一个地址,但它的类型不同,因为它隐含了需要一个 MyClass 对象来调用的上下文。
对于虚成员函数,情况就完全不同了。虚函数的特点是实际调用的函数版本取决于对象的动态类型(运行时类型)。成员函数指针不能简单地存储某个特定版本虚函数的地址。它通常存储的是一个**“标识”,这个标识允许在调用时通过对象的虚函数表 (vtable)** 来查找正确的函数地址。这个标识可能是虚函数在 vtable 中的索引 (index/slot number)。
class Base { public: virtual void draw() { /* Base draw */ } // 假设在 vtable 索引 0 }; class Derived : public Base { public: void draw() override { /* Derived draw */ } // 也在 vtable 索引 0 (覆盖) }; void (Base::*ptr_draw)() = &Base::draw; // ptr_draw 存储的不是 Base::draw 或 Derived::draw 的直接地址, // 而是类似 "vtable_index_0" 这样的信息。 Base b; // b 有一个 vptr 指向 Base 类的 vtable Derived d; // d 有一个 vptr 指向 Derived 类的 vtable // (b.*ptr_draw)(); // 1. 获取 b 的 vptr。 // 2. 通过 vptr 找到 Base 的 vtable。 // 3. 使用 ptr_draw 中的 "vtable_index_0" 从 vtable 中取出 Base::draw 的实际地址。 // 4. 调用该地址,并将 &b 作为 this 指针。 // (d.*ptr_draw)(); // 1. 获取 d 的 vptr。 // 2. 通过 vptr 找到 Derived 的 vtable。 // 3. 使用 ptr_draw 中的 "vtable_index_0" 从 vtable 中取出 Derived::draw 的实际地址。 // 4. 调用该地址,并将 &d 作为 this 指针。
因此,对于虚函数,成员函数指针存储的是一种间接的“标识”,而不是直接的函数地址,这样才能实现多态。
在更复杂的情况下,如多重继承、虚继承等,成员函数指针的使用会变得更加复杂。
如何使用成员指针?
使用成员指针主要分为三个步骤:声明、获取地址和解引用。
-
声明成员指针
声明成员指针的语法与声明普通指针有所不同,需要指定成员所属的类。其中又分为
-
指向数据成员的指针 (Pointer to Data Member):
// 语法: Type ClassName::*pointer_name; // 例子: class MyClass { public: int data_member; double another_data_member; }; int MyClass::*ptr_to_data; // 声明一个指向 MyClass 中 int 类型数据成员的指针 double MyClass::*ptr_to_another_data; // 声明一个指向 MyClass 中 double 类型数据成员的指针
-
指向成员函数的指针 (Pointer to Member Function):
// 语法: ReturnType (ClassName::*pointer_name)(ParameterTypes); // 例子: class MyClass { public: void member_function1() { /* ... */ } int member_function2(int x) { return x; } void const_member_function() const { /* ... */ } }; void (MyClass::*ptr_to_func1)(); // 声明一个指向 MyClass 中无参、返回 void 的成员函数的指针 int (MyClass::*ptr_to_func2)(int); // 声明一个指向 MyClass 中带 int 参数、返回 int 的成员函数的指针 void (MyClass::*ptr_to_const_func)() const; // 声明一个指向 MyClass 中 const 成员函数的指针
需要注意的是如果成员函数是 const 的,那么在成员函数指针的声明中也需要 const。
-
-
获取成员的地址
使用取地址运算符 & 并配合类名作用域解析运算符 :: 来获取非静态成员的地址。
-
获取数据成员的地址:
ptr_to_data = &MyClass::data_member; ptr_to_another_data = &MyClass::another_data_member;
-
获取成员函数的地址:
ptr_to_func1 = &MyClass::member_function1; ptr_to_func2 = &MyClass::member_function2; ptr_to_const_func = &MyClass::const_member_function;
需要注意的是对于重载的成员函数,必须明确指定函数签名才能获取正确的函数地址,或者通过显式类型转换。
-
-
解引用成员指针
解引用成员指针需要一个类的对象或指向类对象的指针,并使用特殊的成员指针访问运算符:.* (点星号) 和 ->* (箭头星号)。
-
.* (成员指针访问运算符 - 用于对象):
MyClass obj; obj.data_member = 10; // 访问数据成员 int value = obj.*ptr_to_data; // value 将会是 10 obj.*ptr_to_data = 20; // 修改 obj.data_member 的值为 20 // 调用成员函数 (obj.*ptr_to_func1)(); // 调用 obj.member_function1() int result = (obj.*ptr_to_func2)(5); // 调用 obj.member_function2(5)
需要注意的是 (obj.ptr_to_func1)() 中的括号是必需的,因为函数调用运算符 () 的优先级高于 .。
-
->* (成员指针访问运算符 - 用于指向对象的指针):
MyClass* ptr_obj = new MyClass(); ptr_obj->data_member = 30; // 访问数据成员 int value_ptr = ptr_obj->*ptr_to_data; // value_ptr 将会是 30 ptr_obj->*ptr_to_data = 40; // 修改 ptr_obj->data_member 的值为 40 // 调用成员函数 (ptr_obj->*ptr_to_func1)(); // 调用 ptr_obj->member_function1() int result_ptr = (ptr_obj->*ptr_to_func2)(8); // 调用 ptr_obj->member_function2(8) delete ptr_obj;
同样,调用成员函数时括号是必需的。
-
代码示例:
#include <iostream>
#include <string>
class Dog {
public:
std::string name;
int age;
Dog(std::string n, int a) : name(n), age(a) {}
void bark() {
std::cout << name << " says: Woof! Woof!" << std::endl;
}
void celebrate_birthday(int years) {
age += years;
std::cout << name << " is now " << age << " years old." << std::endl;
}
void print_age() const {
std::cout << name << "'s age: " << age << std::endl;
}
};
int main() {
// 指向数据成员的指针
std::string Dog::*ptr_name = &Dog::name;
int Dog::*ptr_age = &Dog::age;
// 指向成员函数的指针
void (Dog::*ptr_bark)();
ptr_bark = &Dog::bark;
void (Dog::*ptr_birthday)(int);
ptr_birthday = &Dog::celebrate_birthday;
void (Dog::*ptr_print_age)() const;
ptr_print_age = &Dog::print_age;
Dog my_dog("Buddy", 3);
Dog* another_dog = new Dog("Lucy", 5);
// 使用 .* 访问数据成员
std::cout << "Dog's name (via .*): " << my_dog.*ptr_name << std::endl;
my_dog.*ptr_age = 4;
std::cout << "Dog's new age (via .*): " << my_dog.age << std::endl;
// 使用 ->* 访问数据成员
std::cout << "Another dog's name (via ->*): " << another_dog->*ptr_name << std::endl;
another_dog->*ptr_age = 6;
std::cout << "Another dog's new age (via ->*): " << another_dog->age << std::endl;
// 使用 .* 调用成员函数
(my_dog.*ptr_bark)();
(my_dog.*ptr_birthday)(1);
(my_dog.*ptr_print_age)();
// 使用 ->* 调用成员函数
(another_dog->*ptr_bark)();
(another_dog->*ptr_birthday)(2);
(another_dog->*ptr_print_age)();
// 将成员函数指针与 const 对象一起使用
const Dog const_dog("Max", 2);
// (const_dog.*ptr_bark)(); // 错误! bark() 不是 const 成员函数
(const_dog.*ptr_print_age)(); // 正确! print_age() 是 const 成员函数
delete another_dog;
return 0;
}
为什么需要成员指针?
成员指针提供了在运行时动态选择类成员的能力。这在某些设计模式和通用编程场景中非常有用,例如:
-
回调机制和委托:
可以将成员函数指针作为回调函数传递,使得一个类可以将操作委托给另一个类的特定成员函数。举例说明,假设有一个 EventManager 类,它负责在特定事件发生时通知其他对象。还有一个 Logger 类,它希望在这些特定事件发生时记录相关的消息。EventManager 不应该直接知道 Logger 的所有细节,它只需要知道有一个“回调函数”可以在特定事件发生时被调用,即是将 EventManager 类的操作委托给另一个类 Logger 来做。
从实现步骤上来看,
- 首先需要定义一个成员指针类型,该类型代表指向 Logger 类中某个特定签名成员函数的指针。
- Logger 类中包含一个或多个可以作为回调的成员函数。
- EventManager 类中
- 存储一个指向 Logger 对象的指针。
- 存储一个指向 Logger 类成员函数的指针(即定义的回调类型)。
- 提供一个方法来注册监听器(即 Logger 对象和其回调函数)。
- 提供一个方法来触发事件,并在触发时调用已注册的回调函数。
#include <iostream> #include <string> #include <vector> // 为了演示可以有多个监听器,尽管下面例子只用一个简化版 // 前向声明 Logger 类 class Logger; // 1. 定义成员函数指针类型 // 这个类型 `EventHandlerCallback` 是一个指向 Logger 类成员函数的指针, // 该成员函数接受一个 const std::string& 参数并且返回 void。 typedef void (Logger::*EventHandlerCallback)(const std::string&); // 2. Logger 类 - 事件的接收者和处理者 class Logger { public: std::string logger_name; Logger(const std::string& name) : logger_name(name) {} // 一个可以作为回调的成员函数 void log_event_message(const std::string& message) { std::cout << "[" << logger_name << " - Event Log]: " << message << std::endl; } // 另一个不同签名或用途的成员函数,这里我们不用作回调,仅作示例 void log_system_status(const std::string& status) { std::cout << "[" << logger_name << " - System Status]: " << status << std::endl; } }; // 3. EventManager 类 - 事件的发送者 class EventManager { private: // 为了简化,我们只注册一个监听器。实际应用中可能会用一个列表。 Logger* listener_object; // 指向监听器对象 EventHandlerCallback callback_function; // 指向监听器对象的成员回调函数 public: EventManager() : listener_object(nullptr), callback_function(nullptr) {} // 注册监听器和回调函数 void register_listener(Logger* obj, EventHandlerCallback callback) { listener_object = obj; callback_function = callback; std::cout << "EventManager: Listener registered." << std::endl; } // 触发事件 void trigger_event(const std::string& event_data) { std::cout << "EventManager: Event triggered with data -> " << event_data << std::endl; if (listener_object && callback_function) { // 如果监听器和回调函数都已设置,则执行回调 // 使用 ->* 运算符来通过对象指针调用成员函数指针 (listener_object->*callback_function)(event_data); } else { std::cout << "EventManager: No listener registered to handle the event." << std::endl; } } }; int main() { // 创建 Logger 对象(事件的接收者) Logger my_file_logger("FileLogger"); Logger my_console_logger("ConsoleLogger"); // 创建 EventManager 对象(事件的发送者) EventManager event_manager; // ------ 场景1: 将事件委托给 my_file_logger 的 log_event_message 方法 ------ std::cout << "--- Scenario 1: Logging to FileLogger ---" << std::endl; // 注册监听: // &my_file_logger: 获取 my_file_logger 对象的地址 // &Logger::log_event_message: 获取 Logger 类中 log_event_message 成员函数的地址 event_manager.register_listener(&my_file_logger, &Logger::log_event_message); // 触发事件,这将调用 my_file_logger.log_event_message("User logged in") event_manager.trigger_event("User logged in"); event_manager.trigger_event("Data saved successfully"); std::cout << "\n--- Scenario 2: Re-registering to ConsoleLogger ---" << std::endl; // ------ 场景2: 现在将事件委托给 my_console_logger 的 log_event_message 方法 ------ // 动态改变委托对象和行为 event_manager.register_listener(&my_console_logger, &Logger::log_event_message); // 触发事件,这将调用 my_console_logger.log_event_message("Application started") event_manager.trigger_event("Application started"); // 如果我们想调用 Logger 的另一个方法,比如 log_system_status, // 并且它的签名与 EventHandlerCallback 不同,我们就需要一个新的 typedef // 或者使 EventManager 更通用(例如使用模板或 std::function)。 // 但在这个例子中,我们专注于单一回调类型。 std::cout << "\n--- Scenario 3: No listener (for demonstration) ---" << std::endl; EventManager another_event_manager; // 新的管理器,没有注册监听器 another_event_manager.trigger_event("Test event with no listener"); return 0; }
回调 (callback) 体现在 Logger::log_event_message 函数是回调函数。它由 EventManager 在特定事件(trigger_event被调用)发生时“回调”。EventManager 本身不执行日志记录的逻辑,它只是调用了预先注册的函数。
委托 (Delegation) 体现在 EventManager 将“如何响应事件”的具体行为委托给了 Logger 对象。EventManager 负责检测和分发事件,而 Logger 负责实际的处理逻辑。通过成员函数指针,EventManager 可以在不知道 Logger 具体类型(如果使用基类指针和虚函数,或者模板)或具体方法名称(在编译时固定,但运行时通过指针选择)的情况下,将任务委托出去。
-
状态机:
在状态机中,可以使用成员函数指针来表示不同状态下的行为或转换。举例说明,假设有一个游戏角色,他可以处于以下几种状态:
- 站立 (Idle)
- 行走 (Walking)
- 跳跃 (Jumping)
- 攻击 (Attacking)
每种状态下,角色有不同的行为(例如,更新动画、处理输入等)。可以使用成员函数指针来指向当前状态对应的行为函数。
#include <iostream> #include <string> #include <map> // 用于存储状态到名称的映射,方便打印 // 前向声明 GameCharacter 类 class GameCharacter; // 定义成员函数指针类型 // 这个类型 `CharacterStateBehavior` 是一个指向 GameCharacter 类成员函数的指针, // 该成员函数不接受参数且返回 void。 typedef void (GameCharacter::*CharacterStateBehavior)(); // 定义状态的枚举 enum class CharacterState { IDLE, WALKING, JUMPING, ATTACKING }; // 游戏角色类 class GameCharacter { private: std::string name; CharacterState current_state_enum; // 当前状态的枚举值 CharacterStateBehavior current_behavior; // 指向当前状态行为的成员函数指针 // 状态到名称的映射,用于打印 std::map<CharacterState, std::string> state_names; public: GameCharacter(const std::string& character_name) : name(character_name) { // 初始化状态名称映射 state_names[CharacterState::IDLE] = "Idle"; state_names[CharacterState::WALKING] = "Walking"; state_names[CharacterState::JUMPING] = "Jumping"; state_names[CharacterState::ATTACKING] = "Attacking"; // 初始状态设置为站立 set_state(CharacterState::IDLE); } // 各种状态对应的行为函数 void behave_idle() { std::cout << name << " is standing still, breathing gently." << std::endl; // 在实际游戏中,这里可能会播放站立动画,检查特定输入等 } void behave_walking() { std::cout << name << " is walking around the world." << std::endl; // 播放行走动画,移动角色位置等 } void behave_jumping() { std::cout << name << " jumps high into the air!" << std::endl; // 处理跳跃物理,播放跳跃动画等 } void behave_attacking() { std::cout << name << " swings their mighty weapon!" << std::endl; // 播放攻击动画,检测碰撞,造成伤害等 } // 更新角色状态(执行当前状态的行为) void update() { if (current_behavior) { // 使用 ->* 运算符通过 this 指针调用成员函数指针 (this->*current_behavior)(); } else { std::cout << name << " has no defined behavior for the current state." << std::endl; } } // 改变角色状态 void set_state(CharacterState new_state) { current_state_enum = new_state; std::cout << "\n" << name << " transitions to state: " << state_names[current_state_enum] << std::endl; // 根据新状态,更新成员函数指针 current_behavior switch (new_state) { case CharacterState::IDLE: current_behavior = &GameCharacter::behave_idle; break; case CharacterState::WALKING: current_behavior = &GameCharacter::behave_walking; break; case CharacterState::JUMPING: current_behavior = &GameCharacter::behave_jumping; break; case CharacterState::ATTACKING: current_behavior = &GameCharacter::behave_attacking; break; default: current_behavior = nullptr; // 或者一个默认行为 std::cout << "Warning: Unknown state, behavior not set." << std::endl; break; } } // 获取当前状态(用于外部查询) CharacterState get_current_state() const { return current_state_enum; } std::string get_current_state_name() const { auto it = state_names.find(current_state_enum); if (it != state_names.end()) { return it->second; } return "Unknown"; } }; int main() { GameCharacter player("Hero"); // 模拟游戏循环或事件驱动 player.update(); // 初始状态是 Idle // 模拟输入或事件导致状态转换 player.set_state(CharacterState::WALKING); player.update(); player.update(); // 保持行走状态 player.set_state(CharacterState::JUMPING); player.update(); // 在实际游戏中,跳跃状态可能会在一段时间后自动转换回站立或行走 // 这里为了演示,我们手动转换 player.set_state(CharacterState::IDLE); player.update(); player.set_state(CharacterState::ATTACKING); player.update(); // 攻击后可能回到站立状态 player.set_state(CharacterState::IDLE); player.update(); return 0; }
-
数据驱动编程:
可以根据配置或数据动态地访问或修改对象的特定数据成员。举例说明,假设有一个游戏对象(GameObject),它有诸如生命值(health)、法力值(mana)、名称(name)和可见性(is_visible)等属性。我们希望能够通过外部数据(例如,一个模拟的配置文件)来动态地设置这些属性的值,而不需要在代码中硬编码针对每个属性的 if-else 或 switch 语句。成员数据指针在这里可以发挥关键作用。
#include <iostream> #include <string> #include <vector> #include <map> #include <stdexcept> // 用于 std::runtime_error, std::stoi 等的异常 #include <sstream> // 用于字符串转换 // 游戏对象类 class GameObject { public: int health; float mana; std::string name; bool is_visible; GameObject(const std::string& n, int h, float m, bool v) : name(n), health(h), mana(m), is_visible(v) {} void print() const { std::cout << " Name: " << name << ", Health: " << health << ", Mana: " << mana << ", Visible: " << (is_visible ? "true" : "false") << std::endl; } }; // 为 GameObject 的成员定义成员指针类型别名 using IntMemberPtr = int GameObject::*; using FloatMemberPtr = float GameObject::*; using StringMemberPtr = std::string GameObject::*; using BoolMemberPtr = bool GameObject::*; // 辅助类,用于通过配置来修改 GameObject 的属性 class ConfigurableGameObject { private: GameObject& obj_ref; // 对被配置的 GameObject 对象的引用 // 静态映射:从属性字符串名称到对应的成员指针 // 这些映射对于所有 ConfigurableGameObject 实例是共享的 static std::map<std::string, IntMemberPtr> int_member_pointers; static std::map<std::string, FloatMemberPtr> float_member_pointers; static std::map<std::string, StringMemberPtr> string_member_pointers; static std::map<std::string, BoolMemberPtr> bool_member_pointers; // 用于确保静态映射只被初始化一次的标志 static bool maps_initialized; // 初始化静态映射的静态方法 static void initialize_member_pointer_maps() { if (!maps_initialized) { int_member_pointers["health"] = &GameObject::health; // 获取 health 成员的指针 float_member_pointers["mana"] = &GameObject::mana; // 获取 mana 成员的指针 string_member_pointers["name"] = &GameObject::name; // 获取 name 成员的指针 bool_member_pointers["is_visible"] = &GameObject::is_visible; // 获取 is_visible 成员的指针 maps_initialized = true; std::cout << "[ConfigurableGameObject] Member pointer maps initialized." << std::endl; } } public: // 构造函数,接收一个 GameObject 的引用 ConfigurableGameObject(GameObject& obj) : obj_ref(obj) { initialize_member_pointer_maps(); // 确保映射已初始化 } // 根据属性名(字符串)和属性值(字符串)来设置对象的属性 bool set_property(const std::string& property_name, const std::string& string_value) { std::cout << " Attempting to set property '" << property_name << "' to value '" << string_value << "'..." << std::endl; // 尝试匹配并设置 int 类型的成员 auto it_int = int_member_pointers.find(property_name); if (it_int != int_member_pointers.end()) { try { // it_int->second 是一个 IntMemberPtr (即 int GameObject::*) // obj_ref.*(it_int->second) 使用成员指针访问对象的成员 obj_ref.*(it_int->second) = std::stoi(string_value); // 字符串转整数 std::cout << " Set int property '" << property_name << "' successfully." << std::endl; return true; } catch (const std::exception& e) { std::cerr << " Error: Failed to convert '" << string_value << "' to int for property '" << property_name << "'. " << e.what() << std::endl; return false; } } // 尝试匹配并设置 float 类型的成员 auto it_float = float_member_pointers.find(property_name); if (it_float != float_member_pointers.end()) { try { obj_ref.*(it_float->second) = std::stof(string_value); // 字符串转浮点数 std::cout << " Set float property '" << property_name << "' successfully." << std::endl; return true; } catch (const std::exception& e) { std::cerr << " Error: Failed to convert '" << string_value << "' to float for property '" << property_name << "'. " << e.what() << std::endl; return false; } } // 尝试匹配并设置 std::string 类型的成员 auto it_string = string_member_pointers.find(property_name); if (it_string != string_member_pointers.end()) { obj_ref.*(it_string->second) = string_value; // 直接赋值 std::cout << " Set string property '" << property_name << "' successfully." << std::endl; return true; } // 尝试匹配并设置 bool 类型的成员 auto it_bool = bool_member_pointers.find(property_name); if (it_bool != bool_member_pointers.end()) { if (string_value == "true" || string_value == "1") { obj_ref.*(it_bool->second) = true; std::cout << " Set bool property '" << property_name << "' to true successfully." << std::endl; return true; } else if (string_value == "false" || string_value == "0") { obj_ref.*(it_bool->second) = false; std::cout << " Set bool property '" << property_name << "' to false successfully." << std::endl; return true; } else { std::cerr << " Error: Invalid boolean value '" << string_value << "' for property '" << property_name << "'. Use 'true', 'false', '1', or '0'." << std::endl; return false; } } std::cerr << " Error: Property '" << property_name << "' not found or type not supported for setting." << std::endl; return false; } // 获取属性值(以字符串形式) std::string get_string_property(const std::string& property_name) const { auto it_int = int_member_pointers.find(property_name); if (it_int != int_member_pointers.end()) return std::to_string(obj_ref.*(it_int->second)); auto it_float = float_member_pointers.find(property_name); if (it_float != float_member_pointers.end()) return std::to_string(obj_ref.*(it_float->second)); auto it_string = string_member_pointers.find(property_name); if (it_string != string_member_pointers.end()) return obj_ref.*(it_string->second); auto it_bool = bool_member_pointers.find(property_name); if (it_bool != bool_member_pointers.end()) return (obj_ref.*(it_bool->second) ? "true" : "false"); throw std::runtime_error("Property '" + property_name + "' not found for string conversion."); } }; // 初始化静态成员变量 // 注意:静态成员变量需要在类定义之外进行定义(分配存储空间) bool ConfigurableGameObject::maps_initialized = false; std::map<std::string, IntMemberPtr> ConfigurableGameObject::int_member_pointers; std::map<std::string, FloatMemberPtr> ConfigurableGameObject::float_member_pointers; std::map<std::string, StringMemberPtr> ConfigurableGameObject::string_member_pointers; std::map<std::string, BoolMemberPtr> ConfigurableGameObject::bool_member_pointers; // 模拟从配置文件加载数据 // 返回一个包含 (属性名, 属性字符串值) 对的列表 std::vector<std::pair<std::string, std::string>> load_configuration_from_file() { std::cout << "\n[ConfigurationLoader] Loading configuration data..." << std::endl; return { {"name", "Configured Hero"}, // 字符串类型 {"health", "150"}, // 整数类型 {"mana", "75.5"}, // 浮点数类型 {"is_visible", "false"}, // 布尔类型 {"armor", "25"}, // 一个不存在于 GameObject 中的属性,用于测试 {"health", "invalid_int"} // 无效的整数值,用于测试错误处理 }; } int main() { // 创建一个游戏对象 GameObject player("Default Player", 100, 50.0f, true); // 创建一个配置器,关联到 player 对象 ConfigurableGameObject player_configurator(player); std::cout << "Initial state of player:" << std::endl; player.print(); // 加载配置数据 auto configurations = load_configuration_from_file(); std::cout << "\n[MainLogic] Applying loaded configurations to player..." << std::endl; // 遍历配置数据并应用到 player 对象 for (const auto& config_item : configurations) { player_configurator.set_property(config_item.first, config_item.second); } std::cout << "\nState of player after applying configurations:" << std::endl; player.print(); std::cout << "\n[MainLogic] Demonstrating getting properties..." << std::endl; try { std::cout << " Player Name (read back): " << player_configurator.get_string_property("name") << std::endl; std::cout << " Player Health (read back): " << player_configurator.get_string_property("health") << std::endl; std::cout << " Player Mana (read back): " << player_configurator.get_string_property("mana") << std::endl; std::cout << " Player Visibility (read back): " << player_configurator.get_string_property("is_visible") << std::endl; } catch (const std::exception& e) { std::cerr << " Error getting property: " << e.what() << std::endl; } return 0; }
数据驱动编程体现在,不需要为每个可能的属性编写单独的设置代码,比如 if (prop_name == "health") obj.health = ...; else if ... 相反,set_property 方法是通用的。
配置驱动体现在,对象的哪些属性被修改以及修改成什么值,完全由外部数据(configurations 列表)决定。如果配置文件改变了,程序的行为也会相应改变,而不需要重新编译核心逻辑代码。
如果要让 GameObject 的新成员也支持数据驱动配置,主要步骤是:
- 在 GameObject 中添加新成员。
- 为新成员的类型定义相应的成员指针类型别名(如果尚不存在)。
- 在 ConfigurableGameObject 中添加一个新的静态 std::map 来存储新类型成员的指针。
- 在 initialize_member_pointer_maps() 中添加一行来填充这个新映射。
- 在 set_property() (和 get_string_property()) 中添加逻辑来处理这种新类型的成员。
-
通用算法:
编写可以操作不同类对象的特定成员的通用算法。例如,一个排序算法可以根据用户提供的指向比较成员的指针进行排序。 -
实现某些设计模式:
例如命令模式 (Command Pattern),可以将接收者对象和其成员函数(动作)封装成一个命令对象。