《深度探索C++对象模型》第四章 Function语意学收货

目录

1.编译器对类普通成员函数的处理

2.编译器对静态成员函数的处理

3.编译器对虚函数的处理

4.虚函数指针和虚函数表

5.虚函数继承中的几种情况区分

情况一:

情况二:

情况三:

情况四:

6.多重继承下的虚函数表布局

???但是其中对于打星号的调整this指针的项不是很理解。 

7.inline关键词理解


1.编译器对类普通成员函数的处理

我们知道,其实类是编译器弄的一个小把戏,在底层上,CPU或者存储上并没有类这一概念,所以编译器其实是将类成员函数转为普通的函数存储在代码段。

假设有这样一个类,我们看一下其中的类成员函数经过编译以后的变化

class Point3d
{
public:
    float magnitue() const;
    Point3d normalize();
    void setxyz(float _x, float _y, float _z);
private:
    float x,y,z;
};

float Point3d::magnitue() const
{
    return sqrt(x*x+y*y+z*z)
}

Point3d Point3d::normalize()
{
    float mag=magnitude();
    Point3d normal;
    normal.x=x/mag;
    normal.y=y/mag;
    normal.z=z/mag;
    return normal;
}

void setxyz(float _x, float _y, float _z)
{
    Point3d::x=_x;
    Point3d::y=_y;
    Point3d::z=_z;
}

 

这总共分为三步:

①改写成员函数原型,在入口参数中增加一个额外的参数,这个额外参数就是this指针(就像python中的self一样)

float Point3d::magnitue() const;
编译后:
float magnitue(const Point3d *const this);


Point3d Point3d::normalize();
编译后:
Point3d normalize(Point3d *const this);



void Point3d::setxyz(float _x, float _y, float _z);
编译后:
void setxyz(Point3d *const this, float _x, float _y, float _z);

可以看到,this指针参数有两种形式,

如果是常数成员函数,如magnitue,那么this指针就如第一种形式

否则,就是第二种形式,是一个常量指针,也就是指针指向的地址不能改变,但是地址中的值可以改变

②对每一个nonstatic data member的存取操作改为由this指针来存取。

以magnitue为例,就会改写为

float magnitue(const Point3d *const this)
{
    return sqrt(this->x*this->x+
                this->y*this->y+
                this->z*this->z);
}

③将函数名进行mangling处理

也就是将类成员函数变为普通函数,其实做到第二步基本就做到这一点了,但是函数名可能会同名,所以需要改写函数名,保证函数名独一无二。

这一步不同的编译器方法不一样,就不细究了。还是以magnitue举个例子

float magnitue_7Point3dFv(const Point3d *const this)
{
    return sqrt(this->x*this->x+
                this->y*this->y+
                this->z*this->z);
}

此时假设有两个Point3d的对象,如下

Point3d obj;
Point3d *ptr=&obj;

obj.magnitude();
编译后:
magnitue_7Point3dFv(&obj);

ptr->magnitude();
编译后magnitue_7Point3dFv(ptr);

2.编译器对静态成员函数的处理

由于静态成员函数没有this指针,所以就省略了普通成员函数的前两项,只有最后一项,mangling改写函数名了。

假设Point3d类中加入了一些东西

class Point3d
{
public:
    static int count();
    ...
private:
    static int c;
    ...
};

int Point3d::c=0;

经过编译器编译后,count函数可能变为

int count_5Point3dSFv();

这里也可以解释为什么static函数没有this指针,因为static中只能使用局部变量和静态变量,不需要知道对象的指针,所以入口参数不需要加入对象指针this。

3.编译器对虚函数的处理

假设现在Point3d类变成了这样

class Point3d
{
public:
    virtual float magnitue() const;
    virtual Point3d normalize();
    virtual void setxyz(float _x, float _y, float _z);
private:
    float x,y,z;
};

个人理解:除了和普通成员函数一样要执行三步以后,但会加一步,就是会把该函数地址加入到虚函数表中。当调用该函数时,就不会使用函数名代表函数地址,而是使用虚函数表中的地址值。

所以当对象调用时,编译器会处理成这样,以magnitude为例:

Point3d *ptr=new Point3d;
ptr->magnitue();
编译后:
(* ptr->vptr[1])(ptr);

注意:显示的调用会压制虚拟机制,可以提升效率 ,如Point3d::magnitue()就是显示调用,一般用在类内函数。

4.虚函数指针和虚函数表

以一个例子来看:

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;
};

该类对象的内存布局如下: 

虚函数表的第一项,是RTTI机制,用来存储类的相关信息。后面的顺序是按照声明顺序来排列的。 

注意:其中mult是纯虚函数,所以虚函数表在该位置放置一个pure_virtual_called函数,如果这个函数被执行,通常是结束这个程序,不过也要看编译器。

当有一个class Point2D继承自Point:

class Point2d : public Point {
public:
    Point2d(float x = 0.0, float y = 0.0) : Point(x), _y(y) {}
    ~Point2d();
    Point2d& mult(float);
    float y() const { return _y; }
protected:
    float _y;
};

Point2D对象的内存布局如下:

父类虚函数表与子类虚函数表中函数有如下三种关系

①如果父类存在这个虚函数,并且子类没有改写的,就将父类虚函数表中的函数地址原封不动(甚至连在表中的索引位置都不能变)复制到子类当中去。

②如果父类存在这个虚函数,但是子类中有改写,就将子类中虚函数的地址添加到虚函数表中,但在表中的索引一定是和该函数在父类中的索引是一致。

③如果父类不存在这个虚函数,那么子类中就会在虚函数表末尾增加一个新的索引。

所以总结一下就是会把父类的虚函数表复制过来,其中子类如果有改写的虚函数,就替换掉,如果子类还有新的虚函数,就加在表后面。

注意:这其中有个点很重要,就是平常都说虚函数是在执行期动态调用的函数,如何理解?

我们以之前讲过的一个例子来解释

我们在第三点中解释了虚函数如果被编译以后,是这样的。

Point3d *ptr=new Point3d;
ptr->magnitue();
编译后:
(* ptr->vptr[1])(ptr);

这里假设Point3d继承了Point2d,并且两者都有virtual magnitue()函数 

其中索引1是在编译结束就确定了,似乎在编译结束以后就确定的在执行期应该执行的函数了。但是其实不是,无论是Point3d还是Point2d类中,vptr[1]指向的都是magnitue函数,所以知道执行的时候,根据ptr是属于哪个类的,才能确定究竟使用的是哪个类的虚函数。这也是为什么子类虚函数表中索引位置要和父类一样的原因。

5.虚函数继承中的几种情况区分

情况一:

class father
{
public:
	void show(){ cout << "father\n";}
};

class child : public father
{
public:
	void show(){cout << "child\n";}
};

int main()
{
	child t1;
	father *ptr = &t1;
	ptr->show();
	t1.show();
	system("pause");
}

结果:

正确解释:这是C++中的隐藏,而隐藏的定义是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。可以参考博客:https://www.cnblogs.com/zhangjxblog/p/8723291.html

其中隐藏有一个重要特性就是父类指针指向子类时,父类执行的函数是被隐藏的父类函数,我理解如下:

个人理解:子类父类有同样的普通成员函数,这两个函数是重载的关系,入口参数表明上似乎是一样的,因为都没有,其实是不一样的,因为还有一个隐含的入口参数this指针,而重载在编译过程中的处理就是在mangling时取名不一样。

所以这两个show在编译结束以后,应该是这样的:

ptr->show();
转换成普通函数就是:show(ptr),对应的函数原型是show(father *const)


t1.show();
转换成普通函数就是:show(&t1),对应的函数原型是show(child *const)

情况二:

class father
{
public:
	virtual void show(){ cout << "father\n";}
};

class child : public father
{
public:
	void show(){cout << "child\n";}
};

//main函数上同

结果: 

理解:这是虚函数重写,子类的show函数会放到虚函数表中去。

情况三:

class father
{
public:
	void show(){ cout << "father\n";}
};

class child : public father
{
public:
	virtual void show(){cout << "child\n";}
};
//main函数上同

结果:

理解:这也属于隐藏,所以和情况一是一样的分析。

情况四:

class father
{
public:
	virtual void show(){ cout << "father\n";}
};

class child : public father
{
public:
	virtual void show(){cout << "child\n";}
};
//main函数上同

结果:

理解:这就是最普通的虚函数调用。

总结:可以看到其实最后结果分为两类,一种是情况一和情况三,一种是情况二和情况四。其中情况二和情况三我理解不是很确定。所以最好多使用情况一和情况三。

6.多重继承下的虚函数表布局

以下面这个例子来阐释:

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();
    float z();
protected:
    float _z;
}

???但是其中对于打星号的调整this指针的项不是很理解。 

7.inline关键词理解

①如果在类内定义的函数,就是默认内联的。比如如下所示的set和get函数都是默认的内联函数

class test
{
public:
    int get(){return a;}
    int set(int b){a=b;}
private:
    int a;
}

②inline关键词并不一定就会把函数在程序中展开,需要编译器去判断。

③如果真的被展开了,也并不是将函数代码直接复制到函数调用的程序中,而是需要改动的,主要改动就是有关形式参数和局部变量。

④形式参数不是简单的变成实际参数,而是会需要根据实际情况改变的,具体可以看书的185页

⑤局部变量也不能直接复制,而是要进行mangling操作,防止和程序中的变量重名。

⑥所以千万不要所有函数都内联展开,会出现大量扩展码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值