本章主要探讨C++类的成员函数(member function):static、nonstatic、及vritual函数调用方式的区别。
C++类的非静态成员函数(nonstatic member function)被设计为和普通非成员函数具有相同的效率。
可以思考怎样将一个成员函数转化为一个非成员函数?解决这个问题,首先需要搞清楚他们之间有何区别。
可以想象,最大的区别就是非成员函数没有隐含的this指针,因此将成员函数转化为非成员函数,必须将this指针作为参数安插到非成员函数中。可能还需要根据原始成员函数而进行一些变化。比如如何原始成员函数为const的,那么参数类型需要变成
const class_name *const this
另外,将内部函数转化成外部函数,需要处理名字重复的问题,结合类名等信息生成(mangling)一个独一无二的函数名。
我们知道如果类包含虚函数,则类会包含一个指向虚函数表的指针vptr。
当调用虚函数时,编译器将会找到该函数在虚函数表中的索引,进而确定函数真正的地址。类似如下过程
ptr->normalize();
would be internally transformed into
( * ptr->vptr[ 1 ])( ptr );
值得注意的是,如果通过对象而不是指针调用虚函数,则不存在多态而进行必要的运行时决策,这种操作和一般的成员函数的调用没有区别,而不需要通过虚函数表进行。
静态成员函数不属于任何一个对象,特性就是函数没有this指针,因此静态成员函数基本就是一个普通非成员函数。验证方式是对静态成员函数取地址,类型是:
unsigned int (*)();
而不是
unsigned int ( Point3d::* )();
注:Point3d为类名。
class Point {
public:
virtual ~Point();
virtual Point& mult( float ) = 0;
// ... other operations ...
float x() const { return _x; }
virtual float y() const { return 0; }
virtual float z() const { return 0; }
// ...
protected:
Point( float x = 0.0 );
float _x;
};
class Point2d : public Point {
public:
Point2d( float x = 0.0, float y = 0.0 )
: Point( x ), _y( y ) {}
~Point2d();
// overridden base class virtual functions
Point2d& mult( float );
float y() const { return _y; }
// ... other operations ...
protected:
float _y;
};
在C++中,多态可以理解为“以一个public base class指针,寻址出一个derived class object”。更具体地,对于下面调用
ptr->z();
会被编译器转化为类似:
( *ptr->vptr[ 4 ] )( ptr );
即使我们不知道ptr是point还是point3d类型,但是通过ptr可以通过虚函数指针vptr索引到虚函数表中的目标函数,而虚函数表中函数的位置固定,如果派生类中重写了基类的虚函数,就会重写虚函数表中那一项。理解其结构很重要。
首先需要清楚单一继承和多重继承的区别,下面就是多重继承。
多重继承带来的复杂变化可以从类的布局变化来窥探,如下图所示。
不过在看图之前,我们可以先思考如果你是设计者,派生类怎样处理多个基类的虚函数表?多重继承下怎样支持多态呢?
1.可以确定的是,派生类必须保留每个多重基类的虚函数表,每个都无法抛弃。
2.由于多重继承,派生类中无法保证每个member具有像单一继承那样的一致的相对位置,意思是单一继承下,派生类如果有新成员变量,是保存原始基类布局不变,在类尾部追加新成员,这样保证基类部分布局不变,基类部分相对类起始地址偏移一致,这样,指向派生类的基类指针在存取基类变量时无需任何调整。
但是,在多重继承下,还能是这样的吗?现在请结合下图,考虑以下操作
Base2 *pbase2 = new Derived;
pbase2->data_Base2; //(*pbase2).data_Base2
虽然pbase2具有Derived类的地址,但是第二行如果编译器不对指针进行调整,访问的将是错误的信息,可以想象如果是单一继承就没有这个问题。根本原因还是多重继承导致派生类结构无法和基类保持一致。因此,编译器就需要做一些工作来保证正确的访问。
文中列举了大量实例,在此不一一列举,关键明白产生这一问题的本质。
虚继承给给派生类带来的变化是多个派生类如果共享同一个基类,将基类放到派生类中哪个位置?该如何能够正确地访问到虚继承下的基类member。
class Point2d {
public:
Point2d( float = 0.0, float = 0.0 );
virtual ~Point2d();
virtual void mumble();
virtual float z();
// ...
protected:
float _x, _y;
};
class Point3d : public virtual Point2d
public:
Point3d( float = 0.0, float = 0.0, float = 0.0 );
~Point3d();
float z();
protected:
float _z;
};
和多重继承类似,如果继承体系下,派生类中基类的结构位置发生了变化,在使用指针时,编译器可能对指针需要做相应的调整,以访问到正确的内容。事实上,虚继承下派生类中基类的位置确实方生了变化,可以对比单一继承来看,因此调整指针是在所难免。cfront的设计者之一也是本文作者对编译器支持虚继承的各种自认为诡异的方案也是感慨不已,他的建议是不要在virtual base class中声明nonstatic data members。
参考资料:
深度探索C++对象模型