【问题标题】:How does C++ identify which function to use (class member function pointers related)C++如何识别使用哪个函数(类成员函数指针相关)
【发布时间】:2013-09-30 06:20:19
【问题描述】:

我有一个关于类成员函数的问题。这涉及继承。我在下面写了以下代码,但我并不太明白它是如何工作的(我只能猜测):

#include <iostream>

using namespace std;

class Base
{
};

typedef void(Base::*handler)();

#define selector(_SELECTOR) static_cast<handler>(&_SELECTOR)

class Boo : public Base
{
public:
    void callingFunc()
    {
        cout << "Hey there" << endl;
    }

};


class Foo
{   
public:
    void setCallback( Base * instance,  void (Base::*funcToCall)(void) )
    {
        this->instance = instance;
        this->funcToCall = funcToCall;
    }

    void doCall()
    {
        (instance->*funcToCall)();
    }

private:
    Base* instance;
    void (Base::*funcToCall)(void);
};

void main()
{
    Foo * foo = new Foo();
    Boo * boo = new Boo();

    foo->setCallback(boo, selector(Boo::callingFunc) );
    foo->doCall();  // outputs "Hey there"
}

此代码有效,但我想知道详细原因。 doCall() 似乎将 funcToCall 从 (void (Base::*)(void)) 向上转换为 (void (Boo::*)(void)) 类型。它似乎也将我的instance 变量转换为Boo!它似乎神奇地知道我给的funcToCall 属于Boo 并相应地转换。

这是怎么做到的?这是在运行时还是编译时完成的?当我调用(instance-&gt;*funcToCall)(); 时,它是否只是尝试查找函数的名称?

不要对typedef 吹毛求疵。我知道有些东西需要typedef 才能阅读。这只是测试代码。

编辑:我更多地使用了代码,但它看起来很陌生。我添加了一个新类,结果如下:

class Goo : public Base
{
public:
    void callingFunc()
    {
        cout << "Yo there" << endl;
    }
};

void main()
{
    Foo * foo = new Foo();
    Boo * boo = new Boo();
    Goo * goo = new Goo();

    foo->setCallback(goo, selector(Boo::callingFunc) );
    foo->doCall(); // outputs "Hey there" not "Yo there"
}

在这一点上,这有点道理,但也有点没有意义。我的意思是,很明显它会从 Boo 中调用“Hey There”,但为什么代码不会爆炸?看起来很危险。

编辑 2: 发现了一些真正令人不安和令人不安的事情。我调整了代码,所以接受一个计数器,这样我就有一个变量来检查正在发生的事情。

#include <iostream>

using namespace std;

class Base
{
};

typedef void(Base::*handler)();
#define selector(_SELECTOR) static_cast<handler>(&_SELECTOR)

class Boo : public Base
{
public:
    Boo() : counter(0) {}
    void callingFunc()
    {
        cout << "Hey there " << counter << " at " << &counter << endl;
        counter += 1;
    }

    int counter;

};

class Goo : public Base
{
public:
    Goo() : counter(0) {}
    void callingFunc()
    {
        cout << "Yo there " << counter << " at " << &counter << endl;
        counter += 1;
    }

    int counter;
};

class Foo
{
public:
    void setCallback( Base * instance,  void (Base::*funcToCall)(void) )
    {
        this->instance = instance;
        this->funcToCall = funcToCall;
    }

    void doCall()
    {
        (instance->*funcToCall)();
    }

private:
    Base* instance;
    void (Base::*funcToCall)(void);
};

void main()
{
    Foo * foo = new Foo();
    Boo * boo = new Boo();
    Base * base = new Base();
    Goo * goo = new Goo();

    // first run
    foo->setCallback(goo, selector(Boo::callingFunc) );
    foo->doCall(); // "Hey there 0 at 0044BC60"

    foo->setCallback(boo, selector(Boo::callingFunc) );
    foo->doCall(); // "Hey there 0 at 0044BC00"

    //second run
    foo->setCallback(goo, selector(Boo::callingFunc) );
    foo->doCall(); // "Hey there 1 at 0044BC60"

    foo->setCallback(boo, selector(Boo::callingFunc) );
    foo->doCall(); // "Hey there 1 at 0044BC00"

    // attempt with base
    foo->setCallback(base, selector(Boo::callingFunc) );
    foo->doCall(); // "Hey there *rubbish number* at  at 0044BC30"

}

现在我很确定函数回调是一个运行时的东西(显然是因为它没有给出编译错误,但我不确定,因为通常情况并非如此)。如果它是运行时的东西,那么它是有道理的,因为它几乎像脚本语言一样工作(按名称查找变量,如果存在则更新它等)。

我仍然需要有人来确认这一点。它确实看起来既强大又危险。我真的有一段时间没有看到这样的东西了。我现在太忙了,无法尝试打开组装中的东西来破译到底发生了什么。另外,我不擅长阅读它^^;;

EDIT 3 谢谢大家,现在一切都说得通了。 villekulla 的回答让我相信,因为我的 Boo 和 Goo 类的结构相同,所以它能够以相同的方式访问“计数器”变量(如果您了解类和结构的内存是如何分配的,这应该很明显)。所以我在 Goo 插入了一个 'char' 变量:

class Goo : public Base
{
public:
    Goo() : counter(0) {}
    void callingFunc()
    {
        cout << "Yo there " << counter << " at " << &counter << endl;
        counter += 1;
    }

    char hey;
    int counter;
};

调用:

foo->setCallback(goo, selector(Boo::callingFunc) );
foo->doCall();

两次会产生乱码,因为它正在抓取计数器应该在的字符(确认未定义的行为,如所述)。没有编译错误是因为……嗯……就编译而言,代码没有什么大错。

再次感谢!

【问题讨论】:

    标签: c++ function class pointers member


    【解决方案1】:

    这是未定义的行为。

    考虑为 Foo 和 Goo 添加一个成员,并调整 callFunc 以使用该成员:

    class Boo : public Base
    {
    public:
      Boo()
        : m("Boo")
      {}
      void callingFunc()
      {
         cout << "Hey there, I'am " << boo << endl;
      }
      const char* m;
    };
    
    class Goo : public Base
    {
    public:
      Goo()
        : m("Goo")
      {}
      void callingFunc()
      {
         cout << "Yo there, I'am " << goo << endl;
      }
      const char* m;
    };
    

    在这种情况下

    foo->setCallback(boo, selector(Boo::callingFunc) );
    

    你得到输出

    嘿,我是嘘

    在这种情况下

    foo->setCallback(goo, selector(Boo::callingFunc) );
    

    你得到输出

    嘿,我是咕

    你清楚地看到 Boo::callingFunc 得到了一些 Goo 的实例......

    这是数以百万计的例子之一,C++ 如何帮助你打自己的脚......只做标准允许的事情:/

    你的例子没有爆炸,因为 callFunc 和 Goo/Foo 是微不足道的。如果不幸的是它永远不会爆炸,它“只是”会引入奇怪的错误(Foo::callingFunc 处理 Goo::callingFunc 的数据)。

    由于您没有使用虚函数,所有函数调用地址在获取地址时在编译时解析(在 foo->setCallback(boo, selector(Boo::callingFunc) 行中); em>

    【讨论】:

    • 啊,在您回复和更新之前,我做了类似的事情,然后才开始阅读。你似乎是正确的关于脚下射击的事情。我实际上想知道它是如何工作的。就像在这种特殊情况下 C++ 是否按名称查找我的函数/变量?
    • 一切都在编译时解决(我更新了我的答案)。
    • 好吧,这更有意义。感谢您指出这一行并暗示地址的事情。我猜它设法绕过类型检查并继续分配地址。如果我错了,请纠正我,但我认为由于我的变量是如何排列和命名的,它能够以完全相同的方式访问两个类的“计数器”变量(知道如何为类分配内存)。编辑:啊,我在 Goo 的“计数器”变量之前添加了一个随机字符变量,它按预期输出乱码!我会更新我的帖子。
    【解决方案2】:

    如果您考虑如何在大多数现代 C++ 编译器中实现方法调用,可能会更清楚。非虚拟实例方法实际上是一个静态函数,它以这个指针作为第一个参数。 例如:

    class A {
      void f();
    }
    

    等价于

    class A {
      static void f( A* this );
    }
    

    所以当你传递一个指向实例方法的指针时,你可以把它想象成你传递一个指向静态函数的指针,第一个参数是 funcToCall(Base* this)。问题是 funcToCall 的真正实现认为你的实例指针的数据尊重 Foo 类,如果你的类有非平凡的数据,这可能会导致很多麻烦。 这解释了为什么您的代码有效 - 它根本不使用实例数据。

    为了实验,你可以试试下面的代码:

    Boo* boo = 0;
    boo->funcToCall();
    

    如果您没有捕捉到调试断言,它会起作用,因为您的方法不使用任何实例数据。

    【讨论】:

    • 我只是模糊地知道 C++ 编译器是如何工作的,但我确实了解它们通常如何尝试分配内存、运行代码、更改堆栈指针、跳转等。但是是的,你是对的,它确实变成了只有当您了解所有这些时才会非常清楚......
    猜你喜欢
    • 1970-01-01
    • 2014-10-03
    • 1970-01-01
    • 2018-09-12
    • 1970-01-01
    • 1970-01-01
    • 2010-11-02
    • 2012-02-29
    • 1970-01-01
    相关资源
    最近更新 更多