【C++特殊工具与技术】类成员的指针

在 C++ 编程中,指针是操作内存的核心工具。除了我们熟悉的指向对象或变量的普通指针外,还有一种特殊的指针类型 ——类成员指针(Pointer to Class Members)。它允许我们在不绑定具体对象的情况下,指向类的成员(数据成员或成员函数),并在运行时动态访问这些成员。这种特性在框架设计、反射机制、状态机等场景中有着不可替代的作用。


目录

一、为什么需要类成员指针?

二、类成员指针的声明

2.1 数据成员指针的声明

2.2 成员函数指针的声明

2.3 为成员指针使用类型别名

三、类成员指针的使用

3.1 使用数据成员指针

3.2 使用成员函数指针

3.3 成员函数指针表:批量管理成员函数

3.4 注意:静态成员与成员指针

四、类成员指针的高级应用

4.1 模板与成员指针的结合

4.2 成员指针与虚函数

4.3 成员指针的大小与存储

五、类成员指针的局限与替代方案

六、总结


一、为什么需要类成员指针?

在正式讲解语法前,我们先思考一个问题:普通指针为什么无法直接指向类的成员?

假设我们有一个类Person

class Person {
public:
    std::string name;
    void greet() { std::cout << "Hello, " << name << "\n"; }
};

如果尝试用普通指针指向namegreet(),会遇到什么问题?

  • 普通指针存储的是内存地址,但类的成员(尤其是非静态成员)的地址依赖于具体的对象实例。不同对象的name成员存储在不同的内存位置,普通指针无法抽象出 “类成员” 的通用位置。
  • 成员函数的调用需要隐含的this指针,普通函数指针无法处理这种隐含参数。

类成员指针的价值:它抽象了类成员的 “相对位置”(相对于类对象的起始地址的偏移量),使得我们可以用统一的方式操作不同对象的同一成员。例如,用同一个成员指针,既可以访问对象 A 的name,也可以访问对象 B 的name

二、类成员指针的声明

类成员指针的声明需要明确两个关键信息:

  1. 指向的成员所属的类(通过类名限定);
  2. 指向的成员的类型(数据成员的类型或成员函数的签名)。

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++ 提供了typedefusing来简化类型别名。

示例 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::vectorstd::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;
}

注意:成员指针的大小因编译器和类结构而异,不可依赖具体数值。

五、类成员指针的局限与替代方案

虽然成员指针功能强大,但也存在一些局限性:

  1. 语法复杂:尤其是成员函数指针的声明和使用,对新手不够友好;
  2. 运行时开销:通过成员指针调用函数比直接调用略慢(需要计算偏移量或虚表指针);
  3. 类型严格性:成员指针的类型必须与类和成员的类型完全匹配,缺乏灵活性。

替代方案

  • 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限定符,必须匹配函数签名
类型别名使用usingtypedef简化复杂类型声明
成员指针使用通过.*(对象 / 引用)或->*(指针)操作符绑定具体对象
函数指针表存储多个成员函数指针,实现批量调用或动态调度
静态成员指针与普通指针语法一致,不依赖对象实例
虚函数与成员指针调用时动态绑定,与直接调用虚函数行为一致

掌握类成员指针后,可以更灵活地设计 C++ 程序,尤其是在需要动态访问类成员的场景中(如自定义序列化库、事件驱动框架)。当然,在实际开发中也需权衡语法复杂度和运行时效率,选择最适合的技术方案。

思考题:如何用成员指针实现一个通用的 “对象属性遍历” 函数,输出对象所有数据成员的名称和值?(提示:结合元编程或预处理器宏)


评论 35
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

byte轻骑兵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值