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; };
下面我们来看看数据成员的访问。
在这里我先抛出一个问题,如下:
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有无重大差异?
然而,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是否有种赤裸裸的感觉呢?