博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深度探索C++对象模型 ( 第四部分 )(转)
阅读量:2513 次
发布时间:2019-05-11

本文共 5228 字,大约阅读时间需要 17 分钟。

深度探索C++对象模型(7

我们的在设计和使用类时最常用的便是非静态成员函数,使用成员函数是为了封装和隐藏我们的数据,我想这是成员函数和外部函数的最明显的区别。但是他们的效率是否有不同呢?我们不会想为了保护我们的数据而使用成员函数,最后确导致效率降低的结果。让我们看看非静态成员函数在实际的执行时被编译器搞成了什么样子。
float magnitude3d(const Point3d *_this){…}
//
这是一个外部函数,它有参数。表示它间接的取得坐标(Point3d)成员。
float Point3d::mangnitude3d() const {…}
//
这是一个成员函数,它直接取得坐标(Point3d)的成员。 表面上看,似乎成员函数的效率高很多,但实际上他们的效率真的想我们想象的那样吗?非也。实际上一个成员函数被内部转化成了外部函数。
1
一个this指针被加入到成员函数的参数中,为的是能够使类的对象调用这个函数。
2
将对所有非静态数据成员的存取操作改为由this来存取。
3
对函数的名称进行重新的处理,使它成为程序中独一无二的。 这时后,经过以上的转换,成员函数已经成为了非成员函数。
float Point3d::mangnitude3d() const {…}//
成员函数将被变成下面的样子
//
伪码
mangnitude3d__7Point3dFv(register Point3d * const this)
{
return sqrt(this->_x * this->x+
this->_y * this->y+
this->_z * this->z);
}
调用此函数的操作也被转换
obj. mangnitude3d()
被转换成:
mangnitude3d__7Point3dFv
*obj); 怎么样看出来了吧,和我们开始声明的非成员函数没有区别了。因此得出结论:两个铁球同时落地。
一般来说,一个成员的名称前面会被加上类的名称,形成唯一的命名。实际上在对成员名称做处理时,除了加上了类名,还会将参数的链表一并加上,这样才能保证结果是独一无二的。
我们在来看看静态成员函数。我们有这样的概念,成员函数的调用必须是用类的对象,象这样obj.fun();或者这样ptr->fun().但实际上,只有一个或多个静态数据成员被成员函数存取时才需要类的对象。类的对象提供一个指针this,用来将用到的非静态数据成员绑定到类对象对应的成员上。如果没有用到任何一个成员数据,就不需要用到this指针,也就没有必要通过类的对象来调用一个成员函数。而且我们还知道静态数据成员是在类之外的,可以被视做全局变量的,只不过它只在一个类的生命范围内可见。(参考前面的笔记)。而且一般来说我们会将静态的数据成员声明为一个非Public。这样我们便必须提供一个或多个成员函数用来存取这个成员。虽然我们可以不依靠类的对象存取静态数据成员,但是这个可以用来存取静态成员的函数确实必须绑定在类的对象上的。为了更加好的解决这个问题,cfront2.0引入了静态成员函数的概念。
静态成员函数是没有this指针的。因为它不需要通过类的对象来调用。而且它不能直接存取类中的非静态成员。并且不能够被声明为virtual,const,volatile.如果取得一个静态成员函数的地址,那么我们获得的是这个函数在内存中的位置。(非静态成员函数的地址我们获得的是一个指向这个类成员函数的指针,函数指针)。可以看到由于静态成员函数没有this指针,和非成员函数非常的相似。
有了前面几章的基础,好象这些描述理解起来也不很费劲,而且我们的思路可以跟着书上所说的一路倾泻下来,这便是读书的乐趣所在了,如果一本书读起来都想读第一章时那样费劲,我想我读不下去的可能性会很高。
继续我们的学习,下面书上开始将虚函数了。我们知道虚函数是C++的一个很重要的特性,面向对象的多态便是由虚函数实现的。多态的概念是一个用一个public base class的指针(或者引用),寻址出一个派生类对象。虚函数实现的模型是这样。每一个类都有一个虚函数表,它包含类中有作用的虚函数的地址,当类产生对象时会有一个指针,指向虚函数表。为了支持虚函数的机制,便有了执行期多态的形式。

下面这样。 我们可以定义一个基类的指针。

Point *ptr; 然后在执行期使他寻址出我们需要的对象。可以是
ptr =new Point2d;
还可以是
ptr=new Pont3d;
ptr
这个指针负责使程序在任何地方都可以采用一组由基类派生的类型。这种多态形式是消极的,因为它必须在编译时期完成。与之对应的是一种多态的积极形式,即在执行期完成用指针或引用查找我们的一个派生类的对象。 象下面这样:
ptr->z();
要想达到我们目的,这个函数z()应该是虚函数,并且还应该知道ptr所指的对象的真实类型,以便我们选择z()的实体。以及z()实体的位置,以便我们能够调用它。这些工作编译器都会为我们做好,编译器是如何做的呢? 我们已知每一个类会有一个虚函数表,这个表中含有对应类的对象的所有虚函数实体的地址,并且可能会改写一个基类的虚函数实体。如果没有改写基类存在的虚函数实体,则会继承基类的函数实体,这还没完,还会有一个pure_virtual_called()的函数实体。每一个虚函数不论是继承的还是改写的,都会被指派一个固定的索引值,这个索引在整个继承体系中保持与特定的虚函数关联。 说明:当没有改写基类的虚函数时,该函数的实体地址是被拷贝到派生类的虚函数表中的。
这样我们便实现了执行期的积极多态。这种形式的特点是,我们从头到尾都不知道ptr指针指向了那一个对象类型,基类?派生类1?派生类2?我们不知道,也不需要知道。我们只需要知道ptr指向的虚函数表。而且我们也不知道z()函数的实体会被调用,我们只知道z()函数的函数地址被放在虚函数表中的位置。
总结:在单一继承的体系中,虚函数机制是一种很有效率的机制。我们判断一个类是否支持多态,只需要看它有没有虚函数便可以了。
深度探索C++对象模型(8
内联函数和其他的函数相比是一种效率很高的函数,未优化的情况下效率可以提高25%,优化以后简直是数量级的变化,书上的给出的数据是0.084.43。简直没法比了。内联函数对于封装提供了一种必要的支持,可以有效的存去类中的非共有数据成员,同时可以替代#define(前置处理宏)。但是它也有缺点,程序会随着调用内联函数次数的增多,而产生大量的扩展码。 在内联函数的扩展时每一个形式参数被对应的实参取代,因此会有副作用。通常需要引入临时对象解决多次对实

际参数求值的操作产生的副作用。
第五章的开始给出了一个不恰当的抽象类的声明:
class Abstract_base
{
public:
virtual ~Abstract_base()=0;//
纯虚析构函数
virtual void interface() const=0; //
纯虚函数
virtual const char* mumble() const{return _mumble;}
protected:
char *_mumble;
};
这是一个不能产生实体的抽象类,因为它有纯虚函数。为什么说它存在不合适的地方呢?以下逐一进行说明。
1
它没有一个明确的构造函数,因为没有构造函数来初始化数据成员则它的派生类无法决定数据成员的初值。类的成员数据应该在构造函数或成员函数中被指定初值,否则将破坏封装性质。
2
每一个派生类的析构函数会被编译器进行扩展以静态调用方式调用其上层基类的析构,哪怕是纯虚函数。但是编译器并不能在链接时找到纯虚的析构函数,然后合成一个必要的函数实体,因此最好不要把虚的析构函数声明成纯虚的。
3
除非必要,不要把所有的成员函数都声明为虚函数。这不是一个好设计观念。
4
除非必要,不要使用
const
声明函数,因为很多派生的实体需要修改数据成员。
有了以上的观点上面的抽象类应该改为下面这种样子:
class Abstract_base
{
public:
virtual ~Absteact_base(); //
不在是纯虚
virtual void interface()=0; //
不在是
const
const char * mumble() const{return _mumble;} //
不在是虚函数
protected:
Abstract_base(char *pc=0); //
增加了唯一参数的构造
Char *_mumble;
};
下一个问题,对象的构造。构造一个对象出来很简单,这是我们在编程时经常要做的事情。我理解书上的意思是为我们分析了各种不同的类,例如一个没有
Copy constructor,Copy operator
的类,或者有私有变量但是没有定义虚函数的类等等,当他们构造对象时也有多种情况,
global,local,
还有在
new
时,编译器都做了什么,内存的分配情况如何。搞清楚它们也很有意思。另外这好象是前面几章学到的东西的一个进一步的研究。我们找出最复杂的虚拟继承来进行一下研究。当一个类对象被构造时,实际上这个类的构造函数被调用,不论是我们自己写的,还是由编译器为我们合成的。并且编译器会背着我们做很多的扩充工作,将记录在成员初始化列表中的数据成员的初始化工作放进构造函数,如果一个数据成员没有在成员初始化列表中出现,则会调用默认的构造函数,这个类的所有基类的构造都会被调用,以基类的声明顺序。所有的虚拟基类的构造也会被调用。还要为
virtual table pointers
设定初始值,指向适当的
virtual tables
。好家伙,编译器还真累。好象说的不是很清楚,抄一段书上的代码。
已知一个类的层次结构和派生关系如下图:
见书上
P211
这是程序员给出的
PVertex
的构造函数
:
PVertex::PVertex(float x,float y,float z):_next(0),Vertex3d(x,y,z),Point(x,y)
{
if(spyOn)
cerr<
它可能被扩展成为:
//C++
伪码
// PVertex
构造函数的扩展结果
PVertex *
PVertex::PVertex(PVertex * this,bool most_derived,float x,float y,float z)
{
//
条件式的调用虚基类的构造函数
if(_most_derived!=false)
this->Point::Point(x,y);
//
无条件的调用上层基类的构造函数
this->Vertex3d::Vertex3d(x,y,z);
//
将相关的
vptr
初始化
this->_vptr_PVertex=_vtbl_PVertex;
this->_vptr_Point_PVertex=_vtbl_Point_PVertex;
//
原来构造函数中的代码
if(spyOn)
cerr<//
经虚拟机制调用
<_vptr_PVertex[3].faddr )(this)/
返回被构造的对象
return this;
}
通过上面的代码我们可以比较清晰的了解在有多重继承
+
虚拟继承的时候构造一个对象时,编译会将构造函数扩充成一个什么样子。以及扩充的顺序。知道了这个相对于无继承,或者不是虚拟继承时对象的构造应该也可以理解了。与构造对象相对应的是析构。但是构造函数和析构函数和
new
delete
不同,他们并非必须成对的出现。决定是否为一个类写构造函数或者析构函数,是取决于这个类对象的生命在哪里结束(或开始)。需要什么操作才能保证对象的完整。象构造函数一样析构函数的最佳实现策略是维护两份
destructor
实体。一个
complete object
实体,总是设定好
vptrs
,并调用虚拟基类的析构函数。一个
base class subobject
实体。除非在析构函数中调用一个虚函数,否则绝不会调用虚拟基类的析构函数,并设定
vptrs
一个对象生命结束于析构函数开始执行的时候。它的扩展形式和构造函数的扩展顺序相反。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/10294527/viewspace-126265/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/10294527/viewspace-126265/

你可能感兴趣的文章
python ssh
查看>>
工作笔记一——杂项
查看>>
查看apk包名和Activity的方法
查看>>
MPU6050开发 -- 卡尔曼滤波(转)
查看>>
Redis主从实战
查看>>
plsql if
查看>>
LuoGu P2002 消息扩散
查看>>
linux 下安装JDK
查看>>
简单的ASP.NET无刷新分页
查看>>
宏定义学习
查看>>
omitting directory `folder/'
查看>>
JavaScript面试题
查看>>
TCollector
查看>>
我的博客网站开发6——博文关键字搜索
查看>>
vim7.1在windows下的编码设置[转]
查看>>
同步器之Exchanger
查看>>
IO流
查看>>
专家观点:即使在云中 硬件同样至关重要
查看>>
loadrunner11录制不成功解决方法(收集)
查看>>
jQuery 基础
查看>>