C++ 方法指针语法的友好指南

一旦您理解了一般原则,C++ 方法指针就不再那么令人生畏了。
110 位读者喜欢这篇文章。

如果您正在寻找性能、复杂性或解决问题的多种可能方案,那么在极端情况下,C++ 始终是一个不错的选择。当然,功能通常伴随着复杂性,但一些 C++ 的特性几乎难以辨认。在我看来,C++ 方法指针 可能是我见过的最复杂的表达式,但我将从一些更简单的东西开始。

本文中的示例可在我的 GitHub 存储库 中找到。

C:指向函数的指针

让我们从一些基础知识开始:假设您有一个函数,它接受两个整数作为参数并返回一个整数

int sum(int a, intb){
    return a+b;
}

在纯 C 语言中,您可以创建一个指向此函数的指针,将其分配给您的 sum(...) 函数,并通过解引用来调用它。函数的签名(参数、返回类型)必须与指针的签名一致。除此之外,函数指针的行为类似于普通指针

int (*funcPtrOne)(int, int);

funcPtrOne = ∑

int resultOne = funcPtrOne(2, 5);

如果您将指针作为参数并返回指针,则会变得更难看

int *next(int *arrayOfInt){
    return ++arrayOfInt;
}

int *(*funcPtrTwo)(int *intPtr);

funcPtrTwo = &next;

int resultTwo = *funcPtrTwo(&array[0]);

C 语言中的函数指针存储子例程的地址。

指向方法的指针

让我们进入 C++:好消息是,您可能不需要使用指向方法的指针,除非在少数罕见的情况下,例如以下情况。首先,定义一个具有您已经知道的成员函数的类

class MyClass
{
public:

    int sum(int a, int b) {
        return a+b;
    }

};

1. 定义一个指向特定类类型方法的指针

声明一个指向 MyClass 类型方法的指针。此时,您不知道要调用的确切方法。您只声明了一个指向某些任意 MyClass 方法的指针。当然,签名(参数、返回类型)与您稍后要调用的 sum(…) 方法匹配

int (MyClass::*methodPtrOne)(int, int);

2. 分配特定方法

与 C(或 静态成员函数)相反,方法指针不指向绝对地址。C++ 中的每个类类型都有一个虚拟方法表 (vtable),用于存储每个方法的地址偏移量。方法指针指向 vtable 中的特定条目,因此它也只存储偏移值。此原理也实现了动态调度

由于 sum(…) 方法的签名与您的指针声明匹配,因此您可以将签名分配给它

methodPtrOne = &MyClass::sum;

3. 调用方法

如果您想使用指针调用方法,则必须提供类类型的实例

MyClass clsInstance;
int result = (clsInstance.*methodPtrOne)(2,3);

您可以使用 . 运算符访问实例,使用 * 解引用指针,从而通过提供两个整数作为参数来调用该方法。很丑陋,对吧?但您仍然可以更进一步。

在类中使用方法指针

假设您正在创建一个具有 客户端/服务器 原则架构的应用程序,其中包含后端和前端。您暂时不关心后端;相反,您将专注于前端,它基于 C++ 类。前端的完整初始化依赖于后端提供的数据,因此您需要额外的初始化机制。此外,您希望以通用方式实现此机制,以便将来可以使用其他初始化方法(可能是动态的)来扩展前端。

首先,定义一个数据类型,它可以存储指向初始化方法 (init) 的方法指针以及描述何时应调用此方法的信息 (ticks)

template<typename T>
struct DynamicInitCommand {
    void (T::*init)();     // Pointer to additional initialization method
    unsigned int ticks;    // Number of ticks after init() is called
};

以下是 Frontend 类的外观

class  Frontend
{
public:

    Frontend(){
        DynamicInitCommand<Frontend> init1, init2, init3;

        init1 = { &Frontend::dynamicInit1, 5};
        init2 = { &Frontend::dynamicInit2, 10};
        init3 = { &Frontend::dynamicInit3, 15};

        m_dynamicInit.push_back(init1);
        m_dynamicInit.push_back(init2);
        m_dynamicInit.push_back(init3);
    }
    
    

    void  tick(){
        std::cout << "tick: " << ++m_ticks << std::endl;
        
        /* Check for delayed initializations */
        std::vector<DynamicInitCommand<Frontend>>::iterator  it = m_dynamicInit.begin();

        while (it != m_dynamicInit.end()){
            if (it->ticks < m_ticks){
                 
                if(it->init)
                    ((*this).*(it->init))(); // here it is

                it = m_dynamicInit.erase(it);

            } else {
                it++;
            }
        }
    }
    
    unsigned  int  m_ticks{0};
    
private:

    void  dynamicInit1(){
        std::cout << "dynamicInit1 called" << std::endl;
    };

    void  dynamicInit2(){
        std::cout << "dynamicInit2 called" << std::endl;
    }

    void  dynamicInit3(){
        std::cout << "dynamicInit3 called" << std::endl;
    }

    unsigned  int  m_initCnt{0};
    std::vector<DynamicInitCommand<Frontend> > m_dynamicInit;
};

在实例化 Frontend 后,后端会以固定的时间间隔调用 tick() 方法。例如,您可以每 200 毫秒调用一次

int  main(int  argc, char*  argv[]){
    Frontend frontendInstance;

    while(true){
        frontendInstance.tick(); // just for simulation purpose
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

Frontend 有三个额外的初始化方法,必须根据 m_ticks 的值来调用。关于在哪个 tick 调用哪个初始化方法的信息存储在向量 m_dynamicInit 中。在构造函数 (Frontend()) 中,将此信息附加到向量,以便在五个、十个和十五个 tick 后调用其他初始化函数。当后端调用 tick() 方法时,值 m_ticks 会递增,并且您迭代向量 m_dynamicInit 以检查是否必须调用初始化方法。

如果是这种情况,则必须通过引用 this 来解引用方法指针

((*this).*(it->init))()

总结

如果您不熟悉方法指针,它们可能会变得有点复杂。我做了很多试验和错误,并且花了很多时间才找到正确的语法。但是,一旦您理解了一般原则,方法指针就不再那么可怕了。

这是迄今为止我在 C++ 中发现的最复杂的语法。您知道更糟糕的东西吗?请在评论中发布!

接下来阅读什么
标签
User profile image.
Stephan 是一位技术爱好者,他欣赏开源,因为它可以深入了解事物的运作方式。Stephan 在工业自动化软件这一主要专有领域担任全职支持工程师。如果可能,他会从事基于 Python 的开源项目、撰写文章或骑摩托车。

3 条评论

当谈到 C/C++ 函数指针语法时,我真的很喜欢 Go 博客上名为“Go 的声明语法”的文章。
它深入探讨了 Go 的声明语法设计选择的细节,其中大量参考了 C 风格的语法,尤其是函数指针。我可以轻松地推荐这篇博文,即使对于那些对 Go 本身不太感兴趣的人也是如此。它提供了一个非常好的视角。

关于以这些不同方式使用指针的“如何”的精彩文章。请考虑在此基础上添加“为什么”人们可能想要/需要使用这些情况下的指针。那部分仍然让我困惑。

幸运的是,通常不需要方法指针。假设您有一个中心类(例如 Qt 中的 MainWindow),并且您想根据程序流程调用此中心类的某些方法。这就是我想出这个主题的方式。

回复 ,作者 PeteDD

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.