C++对象模型那点事儿(成员篇)

1 前言

上篇提到了类的数据成员有两种:static和nonstatic。类中的函数成员有三种:static,nonstatic和virtual。不知道大家有没有想过类到底是怎封装数据的?为什么只能通过对象或者成员函数来访问?static数据既然不单独属于某个对象,外界可否访问?类的函数成员不存在于单个对象中,为何外界又不能访问这些函数成员?这些都是怎么做到的?

让我们带着这些问题开始这一章的阅读。

2 数据成员

我们先来看一个例子:

class Point3d{
public:
	//......

	float x;
	static list<Point3d*> *freelist;
	float y;
	static const int chunksize = 250;
	float z;
};

任何静态数据成员都不会被放进对象的布局之中,而被放在程序的data segment中,与个别的对象无关。对于同一access section,nonstatic数据成员在对象之中的排列顺序和其被声明的顺序是一样的。C++standard 要求“较晚出现的members在对象中具有较高的地址“,也就是说由于会进行内存对齐优化,members在对象中不一定为连续排列。

下面我们来看看数据成员的访问。

在这里我先抛出一个问题,如下:

Point3d origin,*pt;
origin.x = 0;
pt->x = 0;

通过origin存取x与通过pt存取x有什么差异?

2.1 static 数据成员

static数据成员被编译器提出于class之外,被视为一个在class声明周期范围之内可见的全局变量。每一个static Data member 的存取,以及与class的关联,并不会招致任何时间上或空间上的开销。每次程序访问static members时,编译器内部会发生如下转化:

//我们知道chunksize 为Point3d中的一个静态数据成员
origin.x == 250;//访问chunksize 并判断
pt->x == 250; // 访问chunksize 并判断

显然外界不可访问chunksize。我们来猜测下原因。

不知道大家是否知道name-mangling手法(编译器会对static data member重新命名)。我们来看看下面的代码:

class A{
	static int x;
};
class B{
	static int x;
};

A和B中的x都放在data segment中,为何两个变量没有冲突?

答案似乎很明显了,编译器将A中的x与B中的x进行了重命名。这个新名字独一无二,且与各自的作用域类名有关。而这个重命名算法就是name-mangling。

所以外界想访问data segment中的chunksize,根本访问不到,人家已经隐姓埋名了。而新的名称只有作用域类知道。

有木有豁然开朗的感觉?

我们来接着上文的转化。

origin.chunksize == 250 ;
//===>>被编译器转化为 Point3d::chunksize == 250;
pt->chunksize == 250;
//===>>被编译器转化为 Point3d::chunksize == 250

此时,分别通过origin和pt存取chunksize是没有差异的。

若chunksize是继承自基类而来,或者继承自虚基类,情况又会发生什么变化呢?

答案是static member成员还是只有一个实例,其存取路径仍然是那么直接。

2.2 nonstatic 数据成员

nonstatic data member直接存放在每一个class对象之中,除非经由显式的或者隐式的类型class object调用,否则没有办法直接存取它们。

还是上面的例子:

class Point3d{
public:
	//......
	float x;
	float y;
	float z;
};
Point3d origin;
//那么地址&origin.x等于多少?
cout<<"&origin: "<<&origin<<endl;
cout<<"&Point3d::x: "<<&Point3d::x<<endl;
cout<<"&origin.x: "<<&origin.x<<endl;

程序运行结果:

运行结果是不是已经很清楚 了?访问对象中的数据成员即是在对象起始地址的基础上增加一个偏移量:

&origin+(&Point3d::x-1)

而这个偏移量在编译时期即可获知。

关于类中的成员函数对于数据成员的访问如下:

//我们假设Point3d中有一个成员函数如下
Point3d Point3d::translate(const Point3d &pt){
	x += pt.x;
	y += pt.y;
	z += pt.z;
}

类中的函数成员看似直接访问的数据成员,事实真是如此吗?非也,我们看编译器对这个成员函数干了些什么事。

上述函数经过转化:

Point3d Point3d::translate(Point3d *const this,const Point3d &pt){
	this->x += pt.x;
	this->y += pt.y;
	this->z += pt.z;
}

是的,编译器在每个成员函数的参数列表中加入了一个this指针,以此来激活重载,稍后详解。

所以类中的nonstatic data member必须通过对象来调用。

那么我们再回到上面一个问题。

Point3d *pt3d;
<pre name="code" class="cpp">pt3d<span style="font-family: Arial, Helvetica, sans-serif;">->x = 0;</span>

//效率如何呢?


答曰,其执行效率在x为struct member,class member,单一继承,多继承的情况下完全相同。但如果x是一个virtual base class member,存取速度稍慢一些。

老问题:

Point3d origin,*pt;
origin.x = 0;
pt->x = 0;
通过origin存取x与通过pt存取x有无重大差异?

答案是当Point3d继承自一个virtual base class,而x又是这个virtual base class的一个member时会有差异。这个时候我们不确定pt指向的class类型(即它到底指向的是派生类还是基类对象?)也就不知道编译时候这个member真正的偏移值。 所以这个存取必须延迟至执行期,经由额外间接引导才能访问。

然而,origin不会有这些问题。因为其class类型是确定的,无疑为Point3d,而virtual base class中的member的偏移值在编译的时候已经固定。所以origin.x可以毫无压力的做到。

好了,关于data member就言尽于此吧。如果大家还想知道更深层次的内容,可以查阅相关资料。

下面我们来看看成员函数的问题。

3 成员函数

上文不是说明member functions 有三种:nonstatic,static和virtual,我们就按这个顺序一一讨论吧。

3.1 nonstatic 成员函数

C++的设计准则之一就是成员函数必须与普通非成员函数有相同的执行效率,同时外界又不能访问类中的nonstatic member functions 那么它是怎么做到的呢?

道理很简单,编译器暗地里已经向member函数实例转换为对等的nonmember 函数实例。

举个例子:

//假设Point3d中有如下一个成员函数
Point3d Point3d::magnitude(){
	//具体实现不是我们所关心的
}
//被编译器转化为(此处先不涉及name-mangling)===>>
Point3d Point3d::magnitude(Point3d *const this){
	//具体实现不是我们所关心的
}
//如果member function为const,则被转化为(此处先不涉及name-mangling)==>>
Point3d Point3d::magnitude(const Point3d *const this){
	//具体实现不是我们所关心的
}

是的,你没有看错,编译器会在member function的参数列表中加入一个指向该对象本身的this指针。至于在参数列表的头部还是尾部加入则可不比深究。所以,外界无法访问到member functions,因为参数列表不匹配。

然后再有mangling生成一个新的函数名,成为一个独一无二的外部函数。所以即使参数列表匹配也无法进行访问,因为函数名字也改变了。

老问题:

Point3d obj,*pt;
pt = &obj;
obj.magnitude();
pt->magnitude();

大家觉得上述两种函数的调用有无重大差异?

下面,我们来看看经过编译器的mangling算法转化后的样子。

obj.magnitude();
//==>>
magnitude_7Point3dFv(&obj);

pt->magnitude();
//==>>
magnitude_7Point3dFv(pt);

显然,几乎没有什么区别。

大家现在是不是对nonstatic member function有一定的了解了呢?那么,我们接着看static member functions吧。

3.2 静态成员函数

static member functions与nonstatic member functions的重大差异在于static member functions没有this指针。那么,必然导致以下结果:

1 它不能直接存取class中的nonstatic data members;

2 其不能被声明为const,volatile或virtual。

3 其不需要经由对象来调用,虽然我们一般都是用对象在调用之。

一个static member function 几乎就是经过mangling的nonstatic member function。

我们来看看mangling对static member function的转化:

//假设count()为Point3d中的一个static member function
unsigned int Point3d::count(){
	//.....
}
//===>>
unsigned int count_5Point3dSFV(){
}

函数名中的大写字母S就代表着static。

我们还有一个证据,看下面的例子:

&point3d::count()

大家猜猜得到的值得类型是什么样子的?unsigned int (Point3d::*)()还是unsigned int (*)() ?

答案显然是后者,static member function俨然已是半个nonmember function了。

那么我们再来看看

obj.count();
pt->count();

两者有无重大差异?

显然没有了this指针以后,count()会被转化为一般的nonmember 函数:

count_7Point3dSFV();

两者的调用几乎一样。

3.3 虚成员函数

我们大家都知道的是对象中会有一个虚表指针,对应的虚表中有各个虚函数的slot。

这个地方水有点深,我不想讨论那么深,原因有二:

1 自己没把握把这个地方说透。

2 并不是所有人都对那么深的东西感兴趣。

感兴趣的朋友可以查阅相关资料。

虚成员函数与nonstatic 成员函数的区别在于其存在于虚表中。

我们直接看下面的例子:

//假设 Point3d 中的第一个虚函数为normalize(),那么
Point3d obj,*pt;
pt = &obj;
pt->normalize();
obj.normalize();

pt->normalize();要想知道具体函数调用normalize()是哪个,就必须得知道pt所指对象的类型。在这个过程中我们需要知道两个信息:

1 pt所指对象的类型信息。

2 virtual function的偏移量。

一般做法是将这两样信息加入虚表中,即可在编译期间获知其具体调用。然而,visual studio 2010似乎不是这样做的。其具体做法还有待考究。

上述说的是单一继承,多重继承的时候会麻烦一些。

在vs2010下面,一个derived class内含n-1个额外的virtual table ,n表示其上一层base class的个数(单一继承不会有额外的virtual table)。

我们来看一个例子:

class Base1{
public:
	Base1();
	virtual ~base1();
	virtual Base1 *clone()const;
protected:
	float data_Base1;
};
class Base2{
public:
	Bsae2();
	virtual ~Base2();
	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;
};

内存布局图如下所示:

我们来看下面一组操作:

Base2 *phase2 = new Derived;

编译器会将上述代码翻译如下:

Derived *tmp = new Derived;
Base2 *phase2 = tmp? tmp+sizeof(Base1):0;

新的Derived对象的地址必须调整以指向其Base2子对象。大家现在是否明白了基类指针释放子类对象的时候如果不将析构函数声明为虚函数就不能释放完全的原因了吧!

然而,对于sun编译器来说,上述形式并不适用,其为了调节执行期间连接器的效率,将多个virtual table连锁为一个。感兴趣的朋友自行查阅相关资料。

我们这里没有讨论虚拟继承下的virtual function。

接着上面的话题:

pt->normalize();
obj.normalize();

两者区别在哪?

首先,pt->normalize();被内部转化为:(*pt->vptr[0])(pt);这点毋庸置疑。

vptr为指向虚表的指针,0为内部偏移量,pt为zhis指针。

obj.normalize();被内部转化为:(*obj.vptr[0])(&obj);真是这样吗?显然不是。因为没必要。

上述由obj调用的函数实例,只可以是Point3d::normalize();经过一个对象调用virtual function总是被编译器视为像对待一般nonstatic member function一样。

所以obj.normalize()被内部转化为normalize_7Point3dFV(&obj);

至此,已大体说完。你现在看到class是否有种赤裸裸的感觉呢?

C++对象模型那点事儿(成员篇)

时间: 2024-12-16 18:33:38

C++对象模型那点事儿(成员篇)的相关文章

C++对象模型那点事儿(布局篇)

1 前言 在C++中类的数据成员有两种:static和nonstatic,类的函数成员由三种:static,nonstatic和virtual.上篇我们尽量说一些宏观上的东西,数据成员与函数成员在类中的布局将在微观篇中详细讨论. 每当我们声明一个类,定义一个对象,调用一个函数.....的时候,不知道你有没有一些疑惑--编译器私底下都干了些什么?普通函数,成员函数都是怎么调用的?static成员又是个什么玩意.如果你对这些东西也感兴趣,那么好,我们一起将class的底层翻个底朝天.修炼好底层的内功

【黑金原创教程】【FPGA那些事儿-驱动篇I 】连载导读

前言: 无数昼夜的来回轮替以后,这本<驱动篇I>终于编辑完毕了,笔者真的感动到连鼻涕也流下来.所谓驱动就是认识硬件,还有前期建模.虽然<驱动篇I>的硬件都是我们熟悉的老友记,例如UART,VGA等,但是<驱动篇I>贵就贵在建模技巧的升华,亦即低级建模II. 话说低级建模II,读过<建模篇>的朋友多少也会面熟几分,因为它是低级建模的进化形态.许久以前,笔者就有点燃低级建模II的念头,但是懒惰的性格让笔者别扭许久.某天,老大忽然说道:"让咱们大干一场吧

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验六:数码管模块

实验六:数码管模块 有关数码管的驱动,想必读者已经学烂了 ... 不过,作为学习的新仪式,再烂的东西也要温故知新,不然学习就会不健全.黑金开发板上的数码管资源,由始至终都没有改变过,笔者因此由身怀念.为了点亮多位数码管从而显示数字,一般都会采用动态扫描,然而有关动态扫描的信息请怒笔者不再重复.在此,同样也是动态扫描,但我们却用不同的思路去理解. 图6.1 6位数码管. 如图6.1所示,哪里有一排6位数码管,其中包好8位DIG信号还有6位SEL信号.DIG为digit,即俗称的数码管码,如果数码管

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验三:按键模块② — 点击与长点击

实验三:按键模块② - 点击与长点击 实验二我们学过按键功能模块的基础内容,其中我们知道按键功能模块有如下操作: l 电平变化检测: l 过滤抖动: l 产生有效按键. 实验三我们也会z执行同样的事情,不过却是产生不一样的有效按键: l 按下有效(点击): l 长按下有效(长点击). 图3.1 按下有效,时序示意图. 图3.2 长按下有效,时序示意图. 如图3.1所示,按下有效既是"点击",当按键被按下并且消抖完毕以后,isSClick信号就有被拉高一个时钟(Short Click).

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二:按键模块① - 消抖

实验二:按键模块① - 消抖 按键消抖实验可谓是经典中的经典,按键消抖实验虽曾在<建模篇>出现过,而且还惹来一堆麻烦.事实上,笔者这是在刁难各位同学,好让对方的惯性思维短路一下,但是惨遭口水攻击 ... 面对它,笔者宛如被甩的男人,对它又爱又恨.不管怎么样,如今 I'll be back,笔者再也不会重复一样的悲剧. 按键消抖说傻不傻说难不难.所谓傻,它因为原理不仅简单(就是延迟几下下而已),而且顺序语言(C语言)也有无数不尽的例子.所谓难,那是因为人们很难从单片机的思维跳出来 ... 此外,

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验四:按键模块③ — 单击与双击

实验四:按键模块③ — 单击与双击 实验三我们创建了“点击”还有“长点击”等有效按键的多功能按键模块.在此,实验四同样也是创建多功能按键模块,不过却有不同的有效按键.实验四的按键功能模块有以下两项有效按键: l 单击(按下有效): l 双击(连续按下两下有效). 图4.1 单击有效按键,时序示意图. 实验四的“单击”基本上与实验三的“点击”一模一样,既按键被按下,经过消抖以后isSClick信号被拉高一个时钟,结果如图4.1所示,过程非常单调.反之,“双击”实现起来,会比较麻烦一些,因为我们还要

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验五:按键模块④ — 点击,长点击,双击

实验五:按键模块④ — 点击,长点击,双击 实验二至实验四,我们一共完成如下有效按键: l 点击(按下有效) l 点击(释放有效) l 长击(长按下有效) l 双击(连续按下有效) 然而,不管哪个实验都是只有两项“功能”的按键模块而已,如今我们要创建三项“功能”的按键模块,亦即点击(按下有效),长击,还有双击.实验继续之前,让我们先来复习一下各种有效按键. 图5.1 点击(按下有效). 如图5.1所示,所谓点击(按下有效)就是按键按下以后,isSClick信号(Single Click) 产生一

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验九:PS/2模块③ — 键盘与多组合键

实验九:PS/2模块③ — 键盘与多组合键 笔者曾经说过,通码除了单字节以外,也有双字节通码,而且双字节通码都是 8’hE0开头,别名又是 E0按键.常见的的E0按键有,<↑>,<↓>,<←>,<→>,<HOME>,<PRTSC> 等编辑键.除此之外,一些组合键也是E0按键,例如 <RCtrl> 或者 <RAlt> .所以说,当我们设计组合键的时候,除了考虑“左边”的组合键以外,我们也要考虑“右边”的组合键.&

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验七:PS/2模块① — 键盘

实验七:PS/2模块① — 键盘 实验七依然也是熟烂的PS/2键盘.相较<建模篇>的PS/2键盘实验,实验七实除了实现基本的驱动以外,我们还要深入解PS/2时序,还有PS/2键盘的行为.不过,为了节省珍贵的页数,怒笔者不再重复有关PS/2的基础内容,那些不晓得的读者请复习<建模篇>或者自行谷歌一下. 市场上常见的键盘都是应用第二套扫描码,各种扫描码如图7.2所示.<建模篇>之际,笔者也只是擦边一下PS/2键盘,简单读取单字节通码与断码而已.所谓单字节通码,就是有效的按下