如果您正在寻找性能、复杂性或解决问题的多种可能方案,那么在极端情况下,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++ 中发现的最复杂的语法。您知道更糟糕的东西吗?请在评论中发布!
 
 
 
 
 
 
 

3 条评论