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

2014-11-24 12:59:00 · 作者: · 浏览: 7

1 前言


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

2 数据成员


我们先来看一个例子:
class Point3d{
public:
	//......

	float x;
	static list
  
    *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<
  
   
程序运行结果:
\ 运行结果是不是已经很清楚 了?访问对象中的数据成员即是在对象起始地址的基础上增加一个偏移量: &Z http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcmlnaW4mIzQzOygmYW1wO1BvaW50M2Q6OngtMSkKtvjV4rj2xqvSxsG/1Nqx4NLryrHG2ry0v8m78daqoaMKudjT2sDg1tC1xLPJ1LG6r8r9ttTT2sr9vt2zydSxtcS3w87KyOfPwqO6CjxwcmUgY2xhc3M9"brush:java;">//我们假设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;
pt3d->x = 0;
//效率如何呢?

答曰,其执行效率在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 function