【C++面向对象】类的数据成员:绑定、布局和存取

一、数据成员的绑定(The binding of a data member)

先看一段代码:

//某个头文件,从某处含入 extern float x; class Point3d { public: Point3d(float,float,float); float X() { return x; } void X(float new_x) { x = new_x; } private: float x,y,z; };

现在的问题是:在类的两个成员函数X中,被返回和设定的x是哪一个呢?是extern声明的x还是类的成员变量x呢?

在现在的编译器中,一般都指的是类中的成员变量x。但是在C++最早的编译器中则不是这样做的。因此导致了两种程序设计风格:

(1)把所有的数据成员放在类的声明的起始处。

(2)所有的内联函数都放在类声明之外。

虽然他们的必要性已经自C++2.0之后消失了,但是这种程序设计风格在今天依然存在。这个古来的语言规则被称为“member rewriting rule”,意思是“一个内联函数实体,在整个类声明未被完全看见之前,是不会被评估求值的”。C++标准中的效果是:一个内联函数在类声明之后立即被定义的话,依然对他评估求值,但是是在整个类声明完成之后才开始。


但是对于成员函数的参数列表则不是这样的。参数列表中的参数还是i会在它们第一次遇到时被决议。例如:

typedef int length; class Point3d { public: void mumble(length val) { _val = val; } length mumble() { return _val; } private: typedef float length; length _val; };两个函数umber中的length会被决议为int,但是后续出现的length声明则会使先前的操作不合法。
因此,我们最好把嵌套类型声明放在类的起始处。


二、数据成员的布局(Data member layout)

先看下面一个类的定义:

class Point3d { public: //... private: float x; static List<Point3d*> *freeList; float y; static const int chunksize = 250; float z; };数据成员的布局情况是:

(1)非静态成员在类对象中的排列顺序和声明顺序一致,任何在其中间声明的静态成员都不会被放进对象布局中。

(2)静态数据成员存放在程序的data segment中,和个别类对象无关。

注:data segement(来自维基百科):

是程序的虚地址空间的一部分,包含全局变量和静态变量,其大小由程序在运行之前程序员所放置的变量决定。

Program memory包括三部分:

(1)Data Segment(Data + BSS + Head)l

(2)Stack

(3)Code Segment


C++标准规定,在同一个访问块即private、public、protected等区段中,成员的排列只需符合较晚出现的成员在类对象中有较高的地址即可。也就是说,并不一定要连续排列

什么东西可能介于被声明的成员之间的?由于对齐可能需要填补一些字节。


编译器还会合成一些内部使用的数据成员,以支持整个对象模型。比如指向虚函数表的指针vptr。vptr的放置位置由编译器决定。传统是放在类的所声明的成员的最后。


C++标准允许编译器把多个访问块指针的数据成员自由排列,而不必在乎在类声明中的次序。

当前的众多编译器都是把一个以上的访问块连锁在一起,依照声明次序形成一个连续区块。而且访问块的多少不会带来额外负担。

例如在一个访问块中声明4个成员和在4个访问块中分别声明一个成员得到的对象大小相同。

判断一个类中两个成员谁先谁后可用如下代码实现:

template<class class_type, class data_type1, class data_type2> char* acess_order(data_type1 class_type::*mem1, data_type1 class_type::*mem2) { assert(mem1 != mem2); return mem1 < mem2 ? "mem1 first" : "mem2 first"; }

三、数据成员的存取

静态数据成员

每一个 静态数据成员只有一个实体,被各个类对象所共享。它被存放在程序的data segment中,并被视为一个全局变量,值在类声明范围之内。

注意:每一静态成员的存取许可(private、protected和public)以及与类的关联并不会导致任何空间或执行时间上的额外负担。

每次程序对静态成员的调用,都会被内部转化为该唯一的实体的直接操作。所以通过指针或对象来调用静态成员,效果完全相同。比如

Point3d origin,*pt = &origin; origin.x = 0.0; pt ->x = 0; 这也是C++中通过指针和通过对象存取数据成员结果完全相同的唯一情况。

这是因为,静态数据成员并不在类对象之中,存取静态成员不需要通过类对象。通过操作符“.”进行存取只是语法上的一种便宜行事而已。
如果静态数据成员是从一个复杂关系中继承而来的,那么程序中也依然只有一个实体,存取依然是直接的。


如果取一个静态数据成员的地址,会得到一个指向其数据类型的指针,而不是指向其类成员的指针,因为静态成员并不在类对象之中。

比如:

#include <iostream> #include <typeinfo> using namespace std; class Point3d { public: float x; static const int chunksize = 250; float z; }; int main() { cout << typeid(&Point3d::chunksize).name() << endl; cout << typeid(&Point3d::x).name() << endl; }程序输出:


如果有两个类,都声明了同一个静态成员比如sta_mem,当它们都被放在程序的data segment时,会导致名称冲突,编译器会进行解决:暗中对每一个静态数据成员编码以获得唯一的程序识别代码。

class base1 { public: base1(int x=8):a(x){};//const成员在构造函数初始化 const int a; static int b; const static int c=9;//const static成员在类内初始化 }; class base2 { public: base2(int x=8):a(x){};//const成员在构造函数初始化 const int a; static int b; const static int c=9;//const static成员在类内初始化 }; int base1::b=9;//static成员在类外初始化,可以修改 int base2::b=10;//static成员在类外初始化,可以修改 int main() { base1 obj1; base2 obj2; cout<<base1::b<<endl; cout<<base2::b<<endl; }程序会输出9和10.


非静态数据成员

存放在每一个类对象中,只有经由显式的(explicit)或者隐式的(implicit)类对象才能存取。

程序员在成员函数中处理非静态数据成员,隐式的类对象就会发生。比如:

void Point3d::translate(const Point3d &pt) { x += pt.x; y += pt.y; z += pt.z; }
事实上x、y、z的存取是经由this指针完成的。其函数参数应该是:

void Point3d::translate(Point3d * const this,const Point3d &pt) { this ->x += pt.x; this ->y += pt.y; this ->z += pt.z; } 每一个非静态数据成员的偏移量在编译时期即可获得,即便这个成员属于一个基类(派生自单一或者多重继承串链)子对象也是一样的。

因此,存取一个非静态数据成员的效率和存取一个C struct成员或者非继承类的成员是相同的。


下面看看虚拟继承。虚拟继承将为经由基类子对象存取类成员导入一层新的间接性。比如

Point3d *pt1; Point3d pt2; pt1 ->_x = 0; pt2._x = 0.1; 从类指针存取和从类对象存取有什么重大差异?

当类Point3d是一个继承类,在其继承结构中有一个虚拟基类,并且被存取的成员是从该基类继承未来的时候,就会有大的差异。

因为在这个时候我们不知道pt1到底指向那种类类型,也就不知道这个成员的真正偏移量。所以存取操作要被延迟至执行期,经由一个额外的导引,才能解决。

但是用类对象来存取则不会导致这样的问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值