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本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
© . All rights reserved.