C++成员函数指针揭秘

本文深入探讨了C++中的成员函数指针,包括其语法、与其他语言机制的混合使用,特别是与继承、虚函数的交互。文章详细分析了Microsoft和GCC的实现方式,强调了成员函数指针在多继承和虚函数场景下的this指针调整问题,并通过实例揭示了使用成员函数指针时可能遇到的语言限制和陷阱,提醒开发者避免不必要的类型转换和静态_cast操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

C++语言支持指向成员函数的指针这一语言机制。就像许多其它C++语言机制一样,它也是一把双刃剑,用得好,能够提高程序的灵活性、可扩展性等等,但是也存在一些不易发现的陷阱,我们在使用它的时候需要格外注意, 尤其是在我们把它和c++其它的语言机制合起来使用的时候更是要倍加当心。

关键字:成员函数指针,继承,虚函数,this指针调整,static_cast

2018.4 注:此文多年前写成,有些细节可能已经发生了变化,但是对理解c++对象模型和实现仍然有参考价值。

语法

C++成员函数指针(pointer to member function)的用法和C语言的函数指针有些相似.

下面的代码说明了成员函数指针的一般用法:

class ClassName {
  public: int foo(int); }

int (ClassName::*pmf)(int) = &ClassNmae::foo;

ClassName  c;   //.*的用法,经由对象调用
(c.*pmf)(5);      // A

ClassName *pc = &c;  //->*的用法,经由对象指针调用
(Pc->*pmf)(6);   // B

使用typedef可以让代码变得略微好看一点:

typedef int (ClassName::*PMF)(int);
PMF pmf = &ClassName::foo;

注意获取一个成员函数指针的语法要求很严格:
1. 不能使用括号:例如&(ClassName::foo)不对。
2. 必须有限定符:例如&foo不对。即使在类ClassName的作用域内也不行。
3. 必须使用取地址符号:例如直接写ClassName::foo不行。(虽然普通函数指针可以这样)
所以,必须要这样写:&ClassName::foo

C++成员函数的调用需要至少3个要素:this指针,函数参数(也许为空),函数地址。上面的调用中,->和.运算符之前的对象/指针提供了this(和真正使用this并不完全一致,后面会讨论),参数在括号内提供,pmf则提供了函数地址。

注意这里成员函数指针已经开始显示它“异类”的天性了。上面代码中注释A和B处两个表达式,产生了一个在C++里面没有类型(type)的“东西”(这是C++语言里面唯一的例外,其它任何东西都是有类型的),这就是.和->运算符:

(c.*pmf)
(Pc->*pmf)

这两个运算符求值生成的“东西”我们只知道可以把它拿来当函数调用一样使唤,别的什么也不能干,甚至都不能把它存在某个地方。就因为这个原因,Andrei Alexandrescu 在他那本著名的《Modern c++ design》里面就说,成员函数指针和这两个操作符号是“curiously half-baked concept in c++”。(5.9节)

C++里面引入了“引用”(reference)的概念,可是却不存在“成员函数的引用”,这也是一个特殊的地方。(当然,我们可以使用“成员函数指针”的引用,呵呵)

与其它语言机制的混合使用

C++是一种Multi-Paradigm的语言,各种语言机制混合使用也是平常的事。这里我们只提几种会影响到成员函数指针实现和运行的语言机制。

继承

根据C++语言规定,成员函数指针具有contravariance特性,就是说,基类的成员函数指针可以赋值给继承类的成员函数指针,C++语言提供了默认的转换方式,但是反过来不行。

虚函数

首先要说明,指向虚拟成员函数(virtual function member)的指针也能正确表现出虚拟函数的特性。举例说明如下:

class B { 
  public virtual int foo(int) {
    /* B's implementation */
    return 0; 
  } 
};
class D : public B 
{ 
   public virtual int foo(int) {
      /* D's implementation */ 
      return 0; 
   } 
};

int foo(){
  int (B::*pmf)(int) = &B::foo;
  D d;
  B* pb = &d;
  (d.*pmf)(0);     //这里执行D::foo
  (pb->*pmf)(0);   //这里执行D::foo,多态
}

C++借由虚函数提供了运行时多态特性,虚函数的实现和普通函数有很大的不同。一般编译器都是采用大家都熟悉的v-table (virtual function table)的方式。所有的虚函数地址存在一个函数表里面,类对象中存储该函数表的首地址(vptr_point)。运行时根据this指针、虚函数索引和虚函数表指针找到函数调用地址。

这里写图片描述

因为这些不同,所以成员函数指针碰上虚函数的时候,也需要作特殊的处理,才能正确表现出所期望的虚拟性质。

多继承

这里扯上多继承,是因为多继承的存在导致了成员函数指针的实现的复杂性。这是因为编译器有时候需要进行”this”指针调整。
举例说明如下:
class B1{};
class B2{};
class D: public B1, public B2{}
假设上面三个对象都不涉及到虚函数,D在内存中的典型布局如下图所示(如果有虚函数则多一个vptr指针, 差别不大):

这里写图片描述

现在假设我们经由D对象调用B2的函数,
D d;
d.fun_of_b2();

这里传给fun_of_b2的this指针不能是&d, 而应该对&d加上一个偏移,得到D内含的B2子对象的首地址处。

成员函数指针的实现必须考虑这种情况。

多继承总是不那么受欢迎。不过即使是单继承,上面的情况也会出现。考虑下面的例子:
class B{}; //non-virtual class
class D :public B{}; //virtual class

假设B是一个普通的类,没有虚拟成员函数。而D加上了虚拟成员函数。那么D的典型内存布局如下图所示:

这里写图片描述

因为D引入了vptr指针,而一般的实现都将vptr放在对象的开头,这就导致经由D对象访问B的成员函数的时候,仍然需要进行this指针的调整。
D d;
d.fun_of_b(); //this 指针也需要调整,否则fun_of_b的行为就会异常

实现

从上面一节我们可以看到,编译器要实现成员函数指针,有几个问题是绕不过去的:
1. 函数是不是虚拟函数,这个涉及到虚函数表(__vtbl)的访问。
2. 函数运行时,需不需要调整this指针,如何调整。这个涉及到C++对象的内存布局。

事实上,成员函数指针必须记住这两个信息。为什么要记住是否为虚函数就不用解释了。但是this指针调整为什么要记住呢?因为在.和->运算符求值时必须用到。 考虑上面那个多继承的例子:

int (D::*pmf)(int) = &B2::foo_of_b2;  //A
D d;                           
(d.*pmf)(0);                      //B

看看上面的代码,其实我们在A处知道需要进行this指针调整,也知道该怎么调整。但是这时候this还没出世呢,还不到调整的时候。到了B处终于有了This指针了,可是又不知道该怎样调整了。所以pmf必须记住调整方式,到了B处调用的时候,再来进行调整。

Microsoft的实现

内部表示

Microsoft VC的实现采用的是Microsoft一贯使用的Thunk技术(不知道这个名字怎么来的,不过有趣的是把它反过来拼写就变成了大牛Knuth的名字,呵呵)。

对于Mircosoft来说,成员函数指针实际上分两种,一种需要调节this指针,一种不需要调节this指针。
先分清楚那些情况下成员函数指针需要调整this指针,那些情况下不需要。回忆上一节讨论的c++对象内存布局的说明,我们可以得出结论如下:
如果一个类对象obj含有一些子对象subobj,这些子对象的首地址&subobj和对象自己的首地址&obj不等的话,就有可能需要调整this指针。因为我们有可能把subobj的函数当成obj自己的函数来使用。
根据这个原则,可以知道下列情况不需要调整this指针:
1. 继承树最顶层的类。
2. 单继承,若所有类都不含有虚拟函数,那么该继承树上所有类都不需要调整this指针。
3. 单继承,若最顶层的类含有虚函数,那么该继承树上所有类都不需要调整this指针。

下列情况可能进行this指针调整:
1. 多继承
2. 单继承,最顶的base class不含virtual function,但继承类含虚函数。那么这些继承类可能需要进行this指针调整。

Microsoft把这两种情况分得很清楚。所以成员函数的内部表示大致分

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值