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来支持多态.