《深度探索C++对象模型》 读书笔记:第四章 Function 语意学

本文深入探讨C++中成员函数的多种调用方式,包括非静态成员函数、静态成员函数和虚拟成员函数。解析了虚拟函数的工作原理,包括虚函数表的使用、多重继承和虚拟继承下虚拟函数的处理,以及如何通过指针调用成员函数。同时,文章还介绍了inline函数的实现机制。

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

4.1Member的各种调用方式

Nonstatic Member Functions(非静态成员函数)

C++的设计准则之一就是:nonstatic member function 至少必须和一般的nonmember function有相同的效率

名称的特殊处理

一般而言,member的名称前面会被加上class的明后才能,形成独一无二的命名,也就是mangling手法。对于member function由于可以被重载化,所以也需要mangling手法。

如magnitude()可能被转变为extern magnitude_7Point3dFv(register Pointed3d *const this);

用对象和用指针调用

obj.magnitude();
//变为
magnitude_7Pointed3dFv(&obj);

ptr->magnitude();
//变为
magnitude_7Pointed3dFv(ptr);

virtual member Function(虚拟成员函数)

用指针调用虚函数的步骤

如:ptr->normalize();

可能会被转化为(*ptr->vptr[1])(ptr);

1.vptr表示由编译器产生的指针,指向virtual table.它被安插在每一个“声明有(或继承自)一个或多个virtual functions"的class object中,事实上其名称也会被“mangled”,因为在一个复杂的派生体系中,可能存在有多个vptrs.

2.1是virtual table slot的索引值,关联到normalize()函数

3.第二个ptr表示this指针

用类对象调用虚函数,会被编译器像对待一般的nonstatic member function一样地加以决议

 

Static Member Functions(静态成员函数)

用指针和用对象调用是一个static member function,如Point3d::normalize(),都会被转换成一样的调用方式,如

obj.normalize();
ptr->normalize();
//变为
normalize_7Point3dSFv();

 

如果取一个static member function地址,获得的将是其在内存中的位置,也就是其地址。 由于static member function没有this指针,所以其地址类型并不是一个“指向class member function”的指针,而是一个“nonmember函数指针”

如&Point3d::object_count();

会得到一个数值,类型是:unsigned int(*)();

而不是unsigned int(Point3d::*)();

4.2Virtual Member Function(虚拟成员函数)

有虚函数的类对象都会包含一个虚指针指向虚函数表

编译器:

1.为了找到表格,每一个class object被安插上一个由编译器内部产生的指针,指向该表格

2.为了找到函数地址,每一个virtual function被指派一个表格索引值

执行期:

在特定的virtual table slot中激活virtual function。一个class只会有一个virtual table,每一个table内含其对应的class object中所有的active virtual functions函数实体的地址。这些active virtual functions包括:

1.这个class所定义的函数实体。他会改写一个可能存在的base class virtual function函数实体

2.继承自base class的函数实体。这是在Derived class决定不该写virtual function时才会出现的情况

3.一个pure_virtual_called()函数实体(纯虚函数),它既可以扮演pure virtual function的空间保卫者角色,也可以当做执行期异常处理函数(有时候会用到)

class Point
{
public:
	virtual ~Point();
	virtual Point& mult(float) = 0;
	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();
	Point2d& mult(float);
	virtual float y()const
	{
		return _y;
	}
protected:
	float _y;
};

class Point3d :public virtual Point2d
{
public:
	Point3d(float x = 0.0, float y = 0.0,float z=0.0)
		:Point2d(x,y), _z(z){}
	~Point3d();
	Point3d& mult(float);
	float z()const
	{
		return _z;
	}
protected:
	float _z;
};

Point ,Point2d,Point3d在VS2013中的布局分别为

对于派生类

1.它可以继承base class所声明的virtual functions的函数实体,正确地说,是该函数实体的地址会被拷贝到Derived class的virtual table相应的slot之中

2.他可以使用自己的函数实体,这表示它自己的函数实体地址必须放在对应的slot之中

3.他可以加入一个新的virtual function。这时候virtual table的尺寸会增大一个slot,而新的函数实体地址会被放进该slot之中。

如果有ptr->z();

1.一般而言,我并不知道ptr所指对象的真正类型,然而我们知道经由ptr可以存取到该对象的virtual table;

2.虽然我们不知道哪一个z()函数实体会被调用,但是我们知道每一个z()函数地址都被放在slot 4

这些信息使得编译器可以将该调用转化为

(*ptr->vptr[4])(ptr);

唯一一个在执行期才能知道的东西是:slot 4所指的到底是哪一个z()函数实体

多重继承下的Virtual Function

在多重继承中支持virtual functions,其复杂度围绕在第二个以及后继的base class身上,以及“必须在执行期调整this指针”这一点。

class Base1
{
public:
    Base1();
    virtual ~Base1();
    virtual void speakClearly();
    virtual Base1 *clone() const;
protected:
    float data_Base1;
};
class Base2
{
public:
    Base2();
    virtual ~Base2();
    virtual void mumble();
    virtual Base2 *clone() const;
protected:
    float data_Base2;
};
class Derived :public Base1, public Base2
{
public:
    Derived();
    virtual ~Derived();
    virtual Derived *clone() const;
protected:
    float data_Derived;
};

对于

Base *pbase1 = new Derived;
Base2 *pbase2 = new Derived;
delete pbase1;
delete pbase2;

     虽然两个delete操作导致相同的derived destructor,但它们需要两个不同的virtual table slots;

1.pbase1不需要调整this指针(因为Base1是最短base class之故,它已经指向Derived对象的起始处)。其virtual table slot需要放置真正的destructor地址

2.pbase2需要调整this指针,其virtual table slot需要相关的thunk地址

在多重继承之下,一个Derived class内含n-1个额外的virtual tables,n表示其上一层base classes的数目。对于本例而言,derived class有2个virtual tables

1.一个主要实体,与Base1共享

2.一个次要实体,与Base2有关

VS2013中的布局与书上有些许不同

 

用一个指向Derived对象的Base1指针或Derived指针时,被处理的是主要实体,用一个指向Derived对象的Base2指针时,被处理的是次要实体

虚拟继承下的virtual functions

class Point2d
{
public:
	Point2d(float x = 0.0, float y = 0.0);
	virtual ~Point2d();
	virtual void mumble();
	virtual float z();
protected:
	float _x,_y;
};

class Point3d :public virtual Point2d
{
public:
	Point3d(float x = 0.0, float y = 0.0, float z = 0.0);
	~Point3d();
	Point3d& mult(float);
	float z();
protected:
	float _z;
};

VS2013中的布局

 

4.4指向Member Function的指针

一个指向member function的指针,其声明语法如下

double(Point::* pmf)();

定义并初始化

double(Point::*coord)()=&Point::x;

调用

(orginal.*coord)();
(ptr->*coord)();

支持“指向Virtual Member Functions”之指针

对于

float (Point::*pmf)()=&Point::z;
Point *ptr = new  Point3d

对一个nonstatic member function取其地址,将获得该函数在内存中的地址,而对一个virtual function,其地址在编译期是未知的,仅知道其在virtual table 中的索引值,所以对一个virtual function取地址得到的是一个索引值。如果上述z()是虚函数,则

ptr->*pmf会被转换为 

(*ptr->vptr[(int)pmf])(ptr);

 

多重继承之下,指向Member Functions的指针

为了让member functions的指针也能够支持多重继承和虚拟继承,设计了如下结构体

struct _mptr{
    int delta;//表示this指针的offset值
    int index;
    union{
        ptrtofunc faddr;
        int v_offset;//存放一个virtual(或多重继承的第二个或后继的)base class的vptr位置
        };
};

index和faddr分别带有virtual table

(ptr->*pmf)();
//变为
(pmf.index<0)?(*pmf.faddr)(ptr):(*ptr->vptr[pmf.index](ptr));

索引和nonvirtual member function地址,当index=-1时,index不指向virtual table

 4.5Inline Functions

一般而言,处理一个inline函数,有两个阶段

1.分析函数定义,以决定函数的“instrinsic inline ability”(本质的inline能力)

2.真正的inline函数扩展操作是在调用的那一点上,这会带来参数的求值操作以及临时对象的管理

形式参数

在inline函数扩展期间,每一个像是参数都会被对应的实际参数取代。

1.常量表达式,在替换之前完成求值操作,后继的inline替换,就可以把常量直接“绑”上去

2.对于会被多次求值的实际参数,会引入临时对象

3.既不是常量表达式,也不是带有副作用的表达式,直接替换

inline int min(int i,int j)
{
    return i<j?i:j;
}
inline int bar()
{
    int minval;
    int val1=1024;
    int val2=2048;
/*(1)*/minval=min(val1,val2);
/*(2)*/minval=min(1024,2048);
/*(3)*/minval=min(foo(),bar()+1);
    return minval;
}

(1)参数直接代换

minval=val1<val2?val1:val2;

(2)直接拥抱常量

minval=1024;

(3)引发副作用,导入临时对象

int t1;
int t2;
minval=(t1=foo()),(t2=bar()+1),t1<t2?t1:t2;

 

局部变量

如果inline函数中有局部变量

inline int min(int i,int j)
{
    int minval=i<j?i:j;
    return minval;
}

 

int local_var;
int minval;
//....
minval = min(val1,val2);

//变为
int loca_var;
int minval;
//将inline函数的局部变量处以“mangling”操作
int _min_lv_minval;
minval=(_min_lv_minval=val1<val2?val1:val2),_min_lv_minval;

 inline函数中的局部变量再加上有副作用的参数,可能会导致大量的临时性对象产生。

 

inline函数如果被调用太多次的话,会产生大量的扩展码,是程序的大小暴涨。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值