zoukankan      html  css  js  c++  java
  • 关于C++中的非静态类成员函数指针

      昨天发现了一个问题,就是使用对类中的非静态成员函数使用std::bind时,不能像普通函数一样直接传递函数名,而是必须显式地调用&(取地址),于是引申出我们今天的问题:非静态类成员函数指针和普通函数指针有什么区别?

    一.C++中对函数到指针的隐式转换

      以前在C语言程序设计课上,老师都会说:“函数名就是指向这个函数的指针”。实际上通过查阅cppreference中的隐式转换规则,其中有这么一句关键的话道出了玄机:

    函数类型 T 的左值能隐式转换成指向该函数的指针纯右值。这不作用于非静态成员函数,因为不存在指代非静态成员函数的左值。

      这一句话表明,实际上我们在把普通函数的函数名当作右值使用的过程中,C++隐式地将其转换成了指向该函数的的指针。这也就是我们在使用std::bind的过程中可以直接传递普通函数的函数名的原因,例如:

    int foo(int a,int b)
    {
        return (a+b);
    }
    
    int main()
    {
        auto function = std::bind(foo,std::placeholders::_1,std::placeholders::_2);
        std::cout << function(3,4) << std::endl; //7
        return 0;
    }

      但是,这一条隐式转换规则不作用于非静态成员函数,因为不存在指代非静态成员函数的左值。因此我们能看出,指向非静态成员函数的指针和指向普通函数的指针是有区别的。接下来我们将会讨论类成员函数指针(除非有特别说明,下文中的“类成员”都指代“非静态类成员”)。

    二.类成员函数指针的声明和调用

      类成员函数指针的声明有些类似普通函数指针。但是区别在于,类成员函数是类成员的一部分,所以需要将类成员函数指针使用 ::* 声明成 成员指针类型。这也表示了类成员函数指针不能单独地被调用,需要 该类型的实例使用.*(对象的成员指针)指向该类型的指针使用->*(指针的成员指针) 进行调用。同时由于上文说到的C++隐式转换的规则,我们需要显示地调用 &(取地址) 获取非静态成员函数指针:

    class A
    {
    public:
        void FunA()
        {
            std::cout << "Function A" << endl;
        }
    };
    
    int main()
    {
        void(A::*p1)() = &A::FunA;
        //p1(); Error
        A a;
        A *p2 = &a;
        (a.*p1)(); // Succ
        (p2->*p1)(); // Succ
        return 0;
    }

    三.探究类成员函数指针

      我们都知道,普通函数指针实际上指向的是函数代码的起始地址。然而类成员函数指针不仅仅是类成员函数的内存起始地址,它还解决C++中涉及到的多重继承、虚函数等导致的地址偏移问题。因此,普通函数指针的内存大小是普通指针的大小(32位系统4字节,64位系统8字节),但是成员函数指针却不一定。

    1.多继承中的使用

      现在有三个类A B C,其中类C public继承 了A B两个类。它们的声明和定义如下:

    class A
    {
    public:
      int a;
      void funA()
      {
        std::cout << "function A" << std::endl;
      }
    };
    
    class B
    {
    public:
      int b;
      void funB()
      {
        std::cout << "function B" << std::endl;
      }
    };
    
    class C : public A, public B
    {
    public:
      int c;
      void funC()
      {
        std::cout << "function C" << std::endl;
      }
    };

      现在我们声明一个函数指针:

    int main()
    {
      void (C::*mfpC)() = &B::funB;
      return 0;
    }

      这个指针很特别,它是一个指向C的类成员的指针,但实际上它指向了C的基类B的成员函数B::funB。现在我们编译代码并调用gdb调试查看mfpC的信息:

    (gdb) p mfpC
    $1 = (void (C::*)(C * const)) 0x8000966 <B::funB()>, this adjustment 4
    (gdb) p sizeof(mfpC)
    $2 = 16
    (gdb) x/16xb &mfpC
    0x7ffffffee830: 0x66    0x09    0x00    0x08    0x00    0x00    0x00    0x00
    0x7ffffffee838: 0x04    0x00    0x00    0x00    0x00    0x00    0x00    0x00

      可以看见,mfpC实际占用了16个字节(运行环境为64位操作系统),这16个字节中不仅包含了它所指向的函数的起始地址,还包含了this指针的调整值。因为对于多重继承来说,如果类成员函数指针保存的是非最左基类的成员函数地址,根据C++标准,非最左基类实例的开始地址肯定不同于派生类实例的开始地址,所以需要调整this指针,使其指向非最左基类实例。

    2.有虚函数的使用

      现在修改代码,在类B中定义一个虚函数funC,并在类C中重写它:

    class A
    {
    public:
      int a;
      void funA()
      {
        std::cout << "function A" << std::endl;
      }
    };
    
    class B
    {
    public:
      int b;
      void funB()
      {
        std::cout << "function B" << std::endl;
      }
    
      virtual void funC()
      {
        std::cout << "function C in class B" << std::endl;
      }
    };
    
    class C : public A, public B
    {
    public:
      int c;
      void funC()
      {
        std::cout << "function C in class C" << std::endl;
      }
    };

      然后我们实例化一个类C的对象c和一个指向B::funC的成员函数指针mfpB,最后通过c调用它:

    int main()
    {
      C c;
      void (B::*mfpB)() = &B::funC;
      (c.*mfpB)();
      return 0;
    }

      结果显而易见,输出的不是"function C in class B",而是"function C in class C"。

      由此可见,成员函数指针虽然看起来和普通的函数指针有些类似,但实际上两者还是有很大的差别的。

  • 相关阅读:
    UrlRewrite(地址变换)技术在IIS 5.0/ASP平台上面的应用
    Asp.Net页面输出到EXCEL
    [收藏] ASP.NET缓存:方法和最佳实践
    基于.NET的全文索引引擎Lucene.NET
    [ASP.NET]支持up,down以及pageup,pagedown,home,end,Enter键盘操作的DataGrid
    无知所以羞耻
    你相信世界上有心有灵犀的事情吗?
    javascript的日期加减
    2007312
    人应该多向上
  • 原文地址:https://www.cnblogs.com/RodChong/p/9890447.html
Copyright © 2011-2022 走看看