《深度探索c++对象模型》第一章:C++对象

本文探讨了C++对象模型,包括对象的存储结构、虚拟继承的影响,以及对象的多态性和指针的使用。强调了虚函数在实现多态中的关键作用,以及指针类型在解析内存内容时的重要性。
摘要由CSDN通过智能技术生成

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;

  内存布局可能为:
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值