一开始,C++只支持非静态成员函数,后来加入了虚拟成员函数,最后才加入了静态成员函数。通过研究Point类,深入成员函数语意学学习。
class Point
{
public:
Point(float p): _x(p){ _y += 1; }
virtual ~Point() {};
Point func();
Point func() const;
virtual Point vfunc();
static Point sfunc();
float x() const { return _x; }
virtual float y() const { return 0; }
virtual float z() const { return 0; }
virtual Point& mult(float) = 0;
private:
float _x;
static float _y;
};
Point Point::func()
{
return Point(2*_x);
}
Point Point::func() const
{
return Point(2*_x);
}
Point Point::vfunc()
{
return Point(2*_x);
}
Point Point::sfunc()
{
return Point(2*_y)
}
非静态成员函数的设计准则为:“非静态成员函数至少和一般的非成员函数有相同的效率”。
编译器通过将成员函数实体转换成为对等的非成员函数实体。编译器是如何做到这一点?以Point类 func成员函数
为例。
1、改写函数签名(原型),加入额外参数 this指针
// non-const non-static member function
Point Point::func(){};
Point Point::func(Point* const this);// convertion by compiler
//const non-static member function
Point Point::func()const{};
Point Point::func(const Point* const this); // convertion by compiler
2、改写成员函数的存取操作
Point Point::func(Point* const this)
{
return Point(2*this->_x);
}
3、将成员函数改写成外部函数
extern func__5PointFv(register Point* const this); // name mangling
4、优化后伪代码
void func__5PointFv(register Point* const this, Point &__result)
{
__result.Point::Point(this->_x + 1);
return;
}
5、调用操作
Point p1, p2;
p2 = p1.func(); // equal to func__5PointFv(&p, p2)
func__5PointFv(&p, p2);
虚拟成员函数引入了虚指针vptr和虚函数表,也引入了多态。
1、通过类指针对虚函数的调用,会被转换成通过虚指针vptr调用虚函数表中对应的虚函数。Point类 vfunc虚函数
的调用如下:
Point p1;
Point* p2 = &p1;
p2->vfunc(); // equal to (*p2->vptr[1])(p2)
(*p2->vptr[1])(p2);
2、通过类对象对虚函数的调用,总是被编译器向对待一般非静态成员函数一样对待,如下:
Point p;
p.vfunc(); // equal to vfunc__5PointFv(&p)
vfunc__5PointFv(&p);
C++类通过虚指针vptr和虚函数表机制实现多态。假设Point2d派生自Point,Point3d派生自Point2d,下图是带有虚函数表的单一继承情况下,类Point、Point2d、Point3d的内存布局:
现在已经知道加入虚函数后,类对象的内存布局了,那么如何实现多态呢?
Point2d obj;
Point *ptr = &obj;
ptr->y();
在编译器,我们并不知道ptr所指对象的真正类型,但是根据ptr一定能够取到该对象的虚函数表,并且y()函数的地址都被存放在虚函数表的slot3中。编译器就可以做以下处理:
(*ptr->vptr[3])(ptr);
到执行期时,确定了ptr所指向的对象,也就能确定slot3所指的到底是哪一个y()函数实体。
相对于单一继承下的虚函数机制,多重继承下其复杂度围绕在第二个及后继的基类身上,以及必须在执行期调整this指针这一点。Point类已经无法满足要求了,看下面的继承体系:
class Base1{
public:
Base1();
virtual ~Base1();
virtual void speakClearyly();
virtual void Base1* clone() const;
protected:
float data_Base1;
}
class Base2{
public:
Base2();
virtual ~Base2();
virtual void mumble();
virtual void Base2* clone() const;
protected:
float data_Base2;
}
class Derived : pulic Base1, public Base2{
public:
Derived();
virtual ~Derived();
virtual void Derived* clone() const;
protected:
float data_Derived;
}
在多重继承下,一个派生类内含n-1个额外的虚函数表(n为上一层基类的数目)。对于类Derived,会有两个虚函数表被编译出来:
所以Derived类在多重继承情况下的内存布局如下:
1、第二个或后继的基类指针的地址偏移
用第二个或后继的基类指针指向派生类对象,编译器会在编译期进行调整:
Base2 *pbase2 = new Derived;
//调整后代码
Derived *temp = new Derived;
Base2 *pbase2 = temp ? temp + sizeof(Base1) : 0;
用第二个或后继的基类指针 析构
派生类对象,指针会被调整以求指向Derived对象的起始处。这个偏移操作需要在执行期通过trunk技术(用适当的offset来调整this指针,同时跳到虚函数中去)来完成。
delete pbase2;
2、通过一个指向派生类的指针,调用第二个或后继的基类中继承而来的虚函数,该指针必须要被调整到指向第二个基类子对象处。
Derived *pder = new Derived;
//pder必须被调整sizeof(Base1)个bytes
pder->numble();
3、允许一个虚函数的返回值类型有所变化,可以是基类类型,也可以是派生类类型。
当我们通过第二个或后继的基类的指针来调用这样的虚函数时,必须要将该指针调整到派生类对象的起始地址。
Base2 *pb1 = new Derived;
// 调用Derived* Derived::clone();
// 返回值必须被调整,以指向Base2 subobject
Base2 *pb2 = pb1->clone();
还没研究。
静态成员函数的主要特性是 没有this指针 。以下的次要特性统统根源于其主要特性:
1、对静态成员函数的调用也会经过编译器的处理,Point类 sfunc静态函数
的调用如下:
Point p1;
p1.sfunc(); // equal to sfunc__5PointSFv()
sfunc__5PointSFv();
2、对静态成员函数的取地址操作,得到的是一个非成员函数指针,而非成员函数指针。
&Point::sfunc(); // Point (*) ();
&Point::func() // Point (Point::*) ();
取成员函数地址得到的结果是其在内存中真正的地址,然而这个值并不完整,它需要被绑定到某个类对象上,才能够通过它调用该函数。一个指向Point类的成员函数指针声明如下:
double (Point::* pmf)();
double (Point::*coord)() = &Point::x;
// call
(obj.*coord)();
(ptr->*coord)();
多态机制在虚函数指针情况下仍然适用。
float (Point::*pmf)() = &Point::z;
Point *ptr = new Point3d;
ptr->z(); // Point3d::z()
(ptr->*pmf)(); // Point3d::z()
对一个虚函数取址,所能获得的只是一个索引值,该索引值对应其在虚函数表中的索引值。通过pmf来调用z(),会被内部转换为一个编译时期的式子。
&Point::~Point; // output: 1
&Point::z(); // output: 2
(*ptr->vptr[(int)pmf])(ptr);
多态机制在多重继承下的虚函数指针情况下仍然适用。这种情况比较负责,暂不讨论。