1.1 C++对象模型
C++对象模型
c++中,类的数据成员static和non-static两种,成员函数有static,non-static和virtual三种。
书中的例子:
class Point{
public:
Point(float xval);
virtual ~Point();
float x() const;
static int PointCount();
protected:
virtual ostream& print(ostream &os)const;
float _x;
static int _point_count;
};
假如现在通过 Point pt; 来定义一个对象,那么该对象的存储模型如图
对象pt里面存储了non-static非静态变量_x,还存储了一个指向虚函数表的指针。可以看到,所有static的成员,不管是数据成员还是成员函数都被存放在对象之外,这些静态的成员不属于某个特定的对象(如对象pt),他们属于整个类Point。
虚拟继承
class son:public father1,public father2 {...};
class:father1:virtual public grandfather {...};
class:father2:virtual public grandfather {...};
虚拟继承的情况下,不管基类grandfather被派生多少次,永远只会存在一个实体,所有的派生类共享这一个实体。例如,class son中就只有grandfather的一个实体。
对象模型如何影响程序
假设现在有一个类X,定义了一个拷贝构造函数,virtual的析构函数,和一个virtual的函数foo():
X foobar(){
X xx;
X *px=new X;
//foo是一个virtual的函数
xx.foo();
px->foo();
delete px;
return xx;
}
foobar()这个函数可能在内部被转化为:
//可能的内部转换结果,虚拟c++码
void foobar(X &_result){
//构造_result
//_result用来取代local xx ...
_result.X::X(); //调用构造函数,构造_result
//扩展X *px=new X;
px=_new(sizeof(X)); //根据类X的大小给指针px分配空间
if(px!=0)
px->X::X(); //如果内存分配成功,调用构造函数
//扩展xx.foo()但不使用virtual机制
//用_result取代xx
foo(&_result); //直接调用foo函数
//我自己的理解:这里和下面调用foo和虚析构函数都将_result或者px作为隐式参数传进去了,这样foo才直到处理的是哪个对象
//原本_result.foo()转换为foo(&_result);
//使用virtual机制扩展px->foo()
(*px->vtbl[2])(px); //通过虚函数表指针调用foo,详见下图
//扩展delete px;
if(px!=0){
(*px->vtbl[1])(px); //通过虚函数表指针调用虚析构函数,详见下图
_delete(px); //释放指针px的空间
}
//不需使用named return statement
//不需摧毁local object xx
return; //函数类型是void,而且传进来的参数是引用(&_result),在调用处_result的值已经改变,不需要返回值了
}
1.2 关键词所带来的差异
略
1.3 对象的差异
多态与指针
class ZooAnimal{
public:
ZooAnimal(){};
virtual ~ZooAnimal(){};
//...
virtual void rotate(){cout<<"ZooAnimal rotate"<<endl;};
protected:
int loc;
string name;
};
class Bear:public ZooAnimal{
public:
Bear(){};
~Bear(){};
//...
void rotate(){cout<<"Bear rotate"<<endl;};
virtual void dance(){};
//...
protected:
enum Dances {d1,d2};
Dances dances_known;
int cell_block;
};
Bear b;
ZooAnimal z1=b; //b被裁切了,不过z1仍然保有一个ZooAnimal
ZooAnimal *z2=&b;
ZooAnimal &z3=b;
z1.rotate(); //编译时已知,调用的是ZooAnimal::rotate()
z2->rotate(); //运行时才能确定z2指向什么类型的对象,调用的是Bear::rotate()
z3.rotate(); //运行时才能确定z3指向什么类型的对象,调用的是Bear::rotate()
多态的主要用途是经由一个共同的接口来影响类型的封装,这个接口通常被定义在一个抽象的基类base class里,例如上述ZooAnimal就定义了rotate这个接口,这个共享接口是以virtual function机制引发的,它可以在执行期根据object的真正类型解析出到底是哪一个函数实体被调用。
所以上述多态要满足的条件:
1、基类提供的接口为虚函数(要有virtual关键字)
2、用基类的指针或者引用指向派生类对象,然后通过指针或者引用去调用
若ZooAnimal里rotate前的virtual去掉,那么z1,z2,z3调用的都是ZooAnimal::rotate()。所以上面这两个条件缺一不可。
注意以上表达的多态是动态多态,此外还有静态多态(函数重载)。
指针:
ZooAnimal za;
ZooAnimal *pza=&za;
其中对象za和指针pza在内存布局可能为:
1、对象za存放着loc,name两个数据成员,还有一个虚函数表指针
2、传统的string是8-bytes(4-bytes的字符长度和4-bytes的字符指针)
3、对象za在内存中的地址为1000-1015(4+8+4)
4、指针pza指向对象za,所以pza存放着za的首地址1000
那么问题来了:pza里存放的知识za的首地址,编译器怎么知道到什么地址结束呢?
这是根据指针的类型确定的,指针类型是ZooAnimal*,那么编译器就会根据ZooAnimal对象的大小确定指针所覆盖的空间,并且根据对象的内容来解析内存里的数据(例如上述例子,前4个字节解析成int,8字节解析成string,最后4个字节解析成指针)。总的来说,“指针类型”会教导编译器如何解释某个特定地址中的内存内容及大小。所以一个void*类型的指针如果指向一个对象,只能存地址,而不能通过它操作所指向对象,如果要通过它操作所指向的对象,就要先进行转型。转型(cast)其实是一种编译器指令,大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指向的内存的大小及其内容”的解释方式。
现在考虑这种情况的内存布局:
Bear b;
Bear *pb=&b;
Bear &rb=*pb;
一个Bear类的对象object占用24-bytes(ZooAnimal 16-bytes加上Bear的两个数据成员 8-bytes)
再考虑这种情况:
Bear b;
ZooAnimal *pz=&b; //pz equals pointer ZooAnimal
Bear *pb=&b; //pb equals pointer Bear
注意:这里pz和pb存储的都是b的首地址,但是上面也说到“指针类型”会教导编译器如何解释某个特定地址中的内存内容及大小,编译器会按照ZooAnimal对象的大小和内容去解析pz,按照Bear对象的大小和内容去解析pb,所以pz所覆盖的地址是上图的ZooAnimal subobject部分,而pb所覆盖的地址是整个Bear object。
除了ZooAnimal中出现过的成员,你不能直接使用pz来直接处理Bear中的任何成员,唯一的例外是通过virtual机制。看下面的例子:
pz->cell_block; //不合法
((Bear*)pz)->cell_block;//合法,向下转型,告诉编译器按照Bear去解析pz所指向的内存
//合法,这样更好,但它是一个run-time operation(成本较高)
if(Bear* pb2=dynamic_cast<Bear*>(pz))
pb2->cell_block;
pb->cell_block; //合法,因为pb是一个Bear*类型的指针
再考虑下面这种情况:
Bear b;
ZooAnimal za=b; //会引起切割
za.rotate(); //调用ZooAnimal::rotate()
如果硬要将整个Bear对象指定给ZooAnimal的对象za,则会溢出它所分配到的内存,执行的结果当然就不对了。当一个基类对象被直接初始化(或被指定为)一个派生类对象时,派生类对象就会被切割(sliced),以塞入更小的基类对象内存,没有任何派生类的内容,多态不再呈现,编译器回避virtual机制。
现在再创建一个类Panda继承Bear:
class Panda:public Bear{
//...
};
ZooAnimal za;
ZooAnimal *pza;
Bear b;
Panda *pp=new Panda; //这里new Panda 和 new Panda()的写法都是可以的
pza=&b;
内存布局可能为: