深度探索C++对象模型

member function 虽然含在class声明之内,却不出现在object之中。

每一个non-inline member function只会诞生一个函数实体。

每一个“拥有零个或一个定义的inline function则会在其每一个使用者(模块)身上产生一个函数实体。

C++在布局以及存取时间上主要的额外负担是由virtual引起,包括:

virtual function 机制 用以支持一个有效率的”执行器绑定“(runtime binding).

virtual base class 用以实现”多次出现在继承体系中的base class, 有一个单一而被共享的实体“。

Stroustrup当初设计,当今仍占优势的C++对象模型中,nonstatic data member被配置于每一个class object之内,static data member则被存放在所有的class object 之外,static 和nonstatic function members也被放在所有的class object之外。

virtual functions 以两个步骤支持之:

1.每一个class产生出一堆指向virtual functions 的指针,放在表格之中。这个表格被称为virtual table(vbtl)。

2.每一个class object 被添加了一个指针,指向相关的virtual table ,通常这个指针被称为vptr。vptr的设定和重置都由每一个class的constructor、destructor和copy assignment运算符自动完成。每一个class所关联的type info object(用以支持runtime type identification, RTTI)也经由virtual table 被指出来,通常是放在表格的第一个slot处。这个模型的主要优点是它的空间和存取时间效率;主要缺点是如果应用程序本身代码未曾改变,但所用的class object的nonstatic data members有所修改(可能是增加、移除或更改),那么那些应用程序代码同样得重新编译。

继承关系也可以指定为虚拟(virtual ,也就是共享的意思)

class istream: virtual public ios { ... };

class ostream : virtual public ios { ... };

在虚拟继承的情况下,base class 不管在继承链中被派生多少次,永远只会存在一个实体(称为subobject)。

在简单对象模型中,一个derived class通过如下的方法模塑其base class 的实现:每一个base class可以被derived class object 内的一个slot 指出,该slot 内含base class subobject 的地址。这个体制的主要缺点是,因为间接性而导致空间和存取时间上的额外负担,优点则是class object 的大小不会因其base classes 的改变而受到影响。


// 不合法,以下两个声明造成矛盾的存储空间

static int foo;

...

extern int foo;


例如把单一元素的数组放在一个struct 的尾端,于是每个struct objects可以拥有可变大小的数组:

struct mumble {

/* stuff */

char pc[1];

};

//从档案或标准输入装置中取得一个字符串,然后为struct 本身和该字符串配置足够的内存

struct mumble *pmumbl = (struct mumble*)malloc(sizeof(struct mumble) + strlen(string) +1);

strcpy(&memble.pc, string);

如果我们改用class来声明,而该class是:

指定多个access sections, 内含数据;

从另一个class 派生而来;

定义有一个或多个virtual functions

那么或许可以顺利转化,但也许不行!

C++中凡处于同一个access section的数据,必定保证以其声明次序出现在内存布局当中。然而被放置在多个access sections中的各笔数据,排列次序就不一定了。

测试一下复制构造函数是引用还是常量引用?如果同时定义这两个,会调用哪一个?

然而被放置在多个access sections中的各笔数据,排列次序就不一定了。下面的声明中,前述的C伎俩或许可以有效运行,或许不能,需视protected data members被放在private 打他members的前面或后面而定(放在前面才可以);

class stumble {

public:

//operations

protected:

//protected stuff

private:

/** private stuff */

char pc[1];

};

base class 和derived class的data members的布局也没有谁先谁后的强制规定,因而也就不保证前述的C伎俩一定有效。

如果一个程序员迫切需要一个相当复杂的C++ class 的某部分数据,使他拥有C声明的那种样子,那么那一部分最好抽取出来称为一个独立的struct 声明。将C与C++组合在一起的做法就是,从C struct 中派生C++部分:

struct c_point { ... };

class point: public c_point { ... };

于是C和C++两种用法都可获得支持:

extern void draw_line(Point, Point);

extern "C" void draw_rect(c_point, c_point);

draw_line(Point(0,0), Point(100,100));

draw_rect(Point(0, 0), Point(100, 100));

但这种习惯用法现在已不被推荐,因为某些编译器(例如Microsoft C++)在支持virtual function的机制中对于class的继承布局做了一些改变。组合,而非继承,才是把C和C++组合在一起的唯一可行方法(conversion 运算符提供了一个十分便利的萃取方法):

struct c_point {  ... };

class Point {

public:

operator c_point() { return _c_point; }

// ...

private:

c_point  _c_point;

// ...

C struct 在C++中的一个合理用途,是当你要传递“一个复杂的class object的全部或部分”到某个C函数中去时,struct 声明可以将数据封装起来,并保证拥有与C兼容的空间布局。然而这项保证只在组合的情况下才存在。如果是继承,而不是组合,编译器会决定是否应该有额外的data members被安插到base struct subobject之中。

虽然你可以直接或间接处理继承体系中的一个base class object 。但只有通过pointer或reference的间接处理,才支持OO程序设计所需的多态性质。

// class Book: public Library_materials { ... };

//现在thing2 参考到book

Library_materials& things = book;

// OK: 现在引发的是Book::check_in()

thing2.check_in();


在C++中,多态只存在于一个个的public class体系中,nonpublic的派生行为以及类型为void* 的指针可以说是多态,但他们并没有被语言明白地支持,也就是说它们必须由程序员通过明白的转型操作来管理(你或许可以说它们并不是多态对象的一线选手)。

C++通过以下方法支持多态:

1.经由一组隐含的转化操作。例如把一个derived class指针转化为一个指向其public base type 的指针:

Shape *ps = new Circle();

2.virtual function机制:

ps->rotate();

3.经由dynamic_cast 和typeid运算符:

if( circle* pc = dynamic_cast<circle*>(ps))...

多态的主要用途是经由一个共同的接口来影响类型的封装,这个接口通常被定义在一个抽象的base object中。这个共享接口以virtual function 机制引发的,它可以在执行期根据object的真正类型解析出到底是哪一个函数实体被调用。

//如果rotate()是虚函数,下面这两个调用调用的是?

(*pointer).rotate(); 

reference.rotate();

考虑这样的代码:

void rotate(X datum, const X* pointer, const X& reference) {

//在执行期之前,无法决定到底调用哪一个rotate()实体

(*pointer).rotate();

reference.rotate();


//下面这个操作总是调用X::ratate()

datum.ratate();

}

main() {

Z z;

rotate(z, &z, z);  // 经由pointer和reference完成的两个“函数调用操作”会被动态完成!在此例中它们都调用Z::rotate().经由datum完成的“函数调用操作”可能(或可能不)经由virtual机制。不过,反正它总是调用X::rotate()就是了。不管经由datum所调用的virtual function 采不采用virtual机制,从语义来说,结果都是相同的。


return 0;

}

一个指向子类的基类引用的sizeof是多大?

需要多少内存才能够表现一个class object?一般而言要有:

其nonstatic data members的总和的大小;

加上任何由于alignment的需求而填补(padding)上去的空间(可能存在于members之间,也可能存在于集合体边界).(alignment 就是将数值调整到某数的倍数,在32位计算机上,通常alignment为4 bytes, 以使bus的运输量达到最高效率)

加上为了支持virtual 而由内部产生的任何额外负担


一个指针(或是一个reference,本质上,一个reference通常是以一个指针来实现,而object语法如果转为间接手法,就需要一个指针.)不管它指向哪一种数据类型,指针本身所需的内存大小是固定的。

一个指向类的指针如何与一个指向int类型的指针或一个指向template Array的指针有所不同呢?以内存需求的观点来说,没什么不同。它们三个都需要有足够的内存来放置一个机器地址,指向不同类型的各指针之间的差异,既不在其指针表示法不同,也不在其内容(代表一个地址)不同,而是在其所寻址出来的object类型不同。也就是说,“指针类型”会教导编译器如何解释某个特定地址中的内存内容及其大小:

1. 一个指向地址1000的整数指针,在32位地址机器上,将涵盖地址空间1000~1003(因为32位机器上是4-bytes)

-------------------------------------------(下面这段参见书)

2.如果string是8-bytes(包括一个4-bytes的字符指针和一个用来表示字符串长度的整数),那么一个ZooAnimal指针将横跨地址空间1000~1015(4+8+4)




一个指向地址1000而类型为void*的指针,将涵盖怎样的地址空间呢?我们不知道!这就是为什么一个类型为void*的指针只能够含有一个地址,而不能通过它来操作所指之object的缘故。所以转型(cast)其实是一种编译器指令,大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出之内存的大小和其内容”的解释方式。

class Bear: public ZooAnimal {

public:

Bear();

~Bear();

// ...

void rotate();

virtual void dance();

// ...

protected:

enum Dances { ... };

Dances dances_known;

int cell_block;

};


Bear b("Yogi");

Bear *pb = &b;

Bear &rb = *pb;

不管是pointer或reference都需要一个4-bytes(32位机器上)的空间。

好,假设我们的Bear object 放在地址1000处,一个Bear指针和一个ZooAnimal指针有何不同?

Bear b;

ZooAnimal *pz = &b;

Bear *pb = &b;

它们每个都指向Bear object的第一个byte。其间的差别是,pb所涵盖的地址包含整个Bear object,而pz所涵盖的地址只包含Bear object中的ZooAnimal subobject.

除了ZooAnimal subobject中出现的members,你不能使用pz来直接处理Bear的任何members。唯一的例外是通过virtual机制:(也就是说通过基类指针来调用子类的非virtual函数是错误的!)

// 不合法:cell_block不是ZooAnimal的一个member,虽然我们知道pz当前指向一个Bear object。

pz->cell_block;


// ok:经过一个明白的downcast操作就没有问题!

((Bear*)pz)->cell_block;

//下面这样更好,但它是一个run-time operation(成本较高)

if(Bear* pb2 = dynamic_cast<Bear*>(pz))

pb2->cell_block;

// ok: 因为cell_block是Bear的一个member.

pb->cell_block;

当我们写:

pz->rotate();

时,pz的类型将在编译时期决定以下两点:

固定的可用接口。也就是说,pz只能够调用ZooAnimal的public接口。

该接口的access level(例如rotate() 是ZooAnimal的一个public member)。

在每一个执行点,pz所指的object类型可以决定rotate()所调用的实体.类型信息的封装并不是维护于pz之中,而是维护于link之中,此link存在于“object的vptr”和“vptr所指之virtual table”之间.(4.2节对于virtual functions有完整的讨论)

现在,请看这种情况:

Bear b;

ZooAnimal za = b;  // 译注: 这会引起切割

// 调用ZooAnimal::rotate()

za.rotate();

为什么rotate()所调用的是ZooAnimal实体而不是Bear实体?此外,如果初始化函数(译者注:应用于上述assignment操作发生时)将一个object内容完整拷贝到另一个object中去,为什么za的vptr不指向Bear的virtual table?

第二个问题的答案是编译器在(1)初始化及(2)指定(assignment)操作(将一个class object指定给另一个class object)之间做出了仲裁,编译器必须确保如果某个object含有一个或一个以上的vptrs,那些vptrs的内容不会被base class object初始化或改变。

第一个问题的答案是,za并不是一个Bear, 它是一个ZooAnimal.多态所造成的“一个以上类型”的潜在力量,并不能实际发挥在“直接存取objects”这件事情上。

例如:

{

ZooAnimal za;

ZooAnimal *pza;


Bear b;

Panda *pp = new Panda;

pza = &b;

}

将za或b的地址,或pp所含的内容(也是个地址)指定给pza,显然不是问题,一个pointer或一个reference之所以支持多态,是因为它们并不引发内存中任何“与类型有关的内存委托操作(type-dependent commitment)”;会受到改变只是它们指向的内存的“大小和内容解释方式”而已。

当把一个derived class赋值给一个base class时,derived object就会被切割,以塞入较小的base type内存中,derived type将没有留下任何蛛丝马迹。多态于是不再呈现,而一个严格的编译器可以在编译时期解析一个“通过该object而触发的virtual function调用操作”,因而回避virtual 机制。如果virtual function被定义为inline,则更有效率上的大收获。

多态是一种威力强大的设计机制,允许你继承一个抽象的public接口之后,封装相关的类型。需要付出的代价就是额外的间接性---不论是在“内存的获得”或是在“类型的决断”上,C++通过class的pointers和reference来支持多态.












评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值