第一章 关于对象(Object Lessons)

第一章 关于对象(Object Lessons)

C++类加上封装后的成本:这里直接给出答案,C++的封装并没有增加许多成本,事实上C++的布局以及存取时间主要的额外负担是由virtual引起的。包括:

  1. virtual function机制,用以支持一个有效率的“执行期绑定”。
  2. virtual base class 用以实现“多次出现在继承体系中的base class,有一个单一而被共享的实例”。

一般而言,没有理由说C++程序比C兄弟庞大。

上面的结论会在之后的解释慢慢清晰明了。

1.1 C++对象模式(The C++ Object Model)

假设我们在C++中声明了如下一个类,其包括静态和非静态的数据成员及成员函数,以及virtual函数

class Point{
public:
    Point(float xval);
    virtual ~Point();
    
    float x() const;
    static in PointCount();
    
protected:
    virtual ostream& print(ostream& os)const;
    
    float _x;
    static in _point_count;
};

我们来看看Point在机器中怎么被表现:

1.1.1 简单对象模型(A simple Object Model)

在这里插入图片描述

该简单模型很简单,一个object是一系列的slots,每一个slots指向一个members,每一个类成员都有自己的slot.在简单模型中members本身并不在object内,只有指向members的指针在object内,这么做避免了member是不同类型而需要不同大小空间的问题。同时简单模型的对象大小是容易计算的:指针大小乘以members个数。

要说明的是该模型没有用于实际产品,但是关于slot个数的概念倒是被引用到C++的“指向成员的指针”的概念中。

1.1.2 表格驱动模型(A Table-driven Object Model)

在这里插入图片描述

为了对所有classes的所有objects都有统一表达方式,该对象模型是把所有与members相关的信息抽取出来放在一个data members table和一个members function table中,class object本身则内含指向这两个表的指针。

要说明的是该模型也没有用于实际产品,但是member function table这个观念成为支持virtual funtionc的一个有效方案。

1.1.3 C++对象模型(The C++ Object Model)

在这里插入图片描述

该模型由上面两个模型演化而来,在该模型中:Nostatic data members被置于每一个class objects中,static data members则被放在class object之外,static 和 Nostatic funtion members也被放在class object之外。virtual funtion则由以下两个步骤支持:

  1. 每一个class产生一堆指向virtual functions的指针,放在表格之中,该表被称为virtual table(vtbl)
  2. 每一个class object被安插一个指针,指向相关的virtual table。这个指针被称为vptr。每一个class所关联的type_info object(用以支持 runtime type identification,RTTI)也经由virtual table被指出来,通常放在第一个slot.

1.1.4 加上继承(Adding Inheritance)

C++同时支持单重继承和多重继承,甚至也可以指定为虚拟继承(保证base class在继承链中永远只有一个实例),例如下面的iostream就只有一个virtual ios base class的实例。

在这里插入图片描述

我们来看看对象模型是如何支持这个继承关系的:

  1. 简单对象模型中:每个base class可以被每个drived class object内的一个slot指出。由于间接性指引,优点是class object的大小不会因为其base classes的改变而受影响。

  2. 类似virtual table的思想:每个base class table被产生出来时,表格中每一个slot内含一个相关地base class地址,每个class object内含一个bptr,它会被初始化指向其base class table。这使所有class object对于继承都有统一的表达方式(初始都只有一个指针),另外base calss table的改变不受class object的影响。

    在这里插入图片描述

对于上述两种思想都会因为间接性的加上带来一些效率问题,但是想一想不用间接指引会发生什么?对于任何一个base class 的修改都会直接影响其他相关class导致代码需要重新编译。当然C++具体实现模型比这复杂地多,这里只是给一个直观感受。

1.1.5 对象模型如何影响程序(How the Object Model Effects Programs)

我们通过下面的示例性程序来看对象模型影响:

X foobar(){
    X xx;
    X *px= new X;
    
    //foo是 一个virtual function
    xx.foo();
    px->foo();
    
    delete px;
    return xx;
}

这个函数可能在内部被转化为:(注意这里只是伪代码,看不懂没关系的,了解大概就行)

//可能的内部转换结果
//虚拟的C++代码
void foobar(X &_result){
    //构造_result
    //_result取代local xx...
    _result.X::X();
    
    //扩展X *px = new X;
    px= _new(sizeof(X));
    if(px!=0)
        px->X::X();
    
    //扩展xx.foo()但不使用virtual机制
    //以_result取代xx
    foo(&_result);
    
    //使用virtual机制拓展px->foo()
    (*px->vtbl[2])(px)
    
    //扩展delete px;
    if(px!=0){
        (*px->vtbl[1])(px);//destructor
        _delete(px);
    }
    
    //无须使用named return statment
    //无须摧毁local object xx
    return; 
}

我们用图片来稍微解释以下,有个大概直观感受

在这里插入图片描述

由于X有两个virtual functions,一个是destructor,一个是foo.所以X object的布局如上。上述代码中的px->vtbl[0]指向X的type_info object,

px->vtbl[1]指向X::~X(), px->vtbl[2]指向X::foo()。

1.2 关键词所带来的差异(A Keyword Distinction)

如果C++不是为了兼容C兄弟,C++要远比现在简单得多。

C++中绝不要以struct取代class,原因学完本书会了解。struct在C++中的作用大多是为了向旧C代码传递一个组合数据集。

1.3 对象的差异(An Object Distinction)

范式:一种环境设计和方法论的模型或范例;系统和软件以此模型来开发和运行。

C++直接支持3种程序设计范式:

  1. 程序模型(procedural model),就像C一样,C++当然很好的支持该范式,字符串的处理就是一个例子:

    //procedural model式伪代码
    char boy[] = "Danny";
    char *p_son;
    ...
    p_son = new char[strlen(boy)+1];
    strcpy(p_son,boy);
    ...
    if(!strcmp(p_son,boy))
        take_to_disneyland(boy);
    
  2. 抽象数据类型模型(Abstract data type model,ADT),抽象和一组表达式是一起提供的,那时其运算定义可能仍不清晰,string Class就是一个例子:

    //ADT式伪代码
    String girl="Anna";
    String daughter;
    ...
    //String::operator=();
    daughter=girl;
    ...
    //String::operator==();
    if(girl==daughter)
        take_to_disnneyland(girl);
    
  3. 面向对象模型(object-oriented model)在此模型中有一些彼此相关的类型,通过一个抽象的base class被封装起来,面向对象大家都知道,例子不给了。

纯粹的以某种范式写程序,有助于整体行为的良好稳固。但是混用不同范式可能会发生一些不同的错误。我们用下面示例代码来引入:

//完成多态时用到的两种范式

//ADT范式
Library_b thing1;
//Book:public Library_b
Book book;
thing1=book;//thing1不是一个Book,thing1被切割了,其保有Linrary_b
thing1.check_in();//调用的是Library_b的check_in()

//OO范式
Library_b &thing2=book;
thing2.check_in();//调用的是Book的check_in()

在ADT范式中,程序员处理的是一个固定而单一类型的实例,它在编译期就完全定义好了。

在OO范式中,程序处理的是一个未知实例,其类型受限于继承体系,原则上每个object的类型在某一个特定执行点前都是无法解析的,C++中通过pointer和reference操作来完成,再看一个例子印证上面的话:

//描述objects:不确定类型
Library_b *px=ret_some_book();
Library_b &rx=*px;

//描述已知事物:不可能有令人惊讶的结果
Library_b dx=*px;

//解释:我们没有办法说出px或rx具体是那种类型,只能说它要么是Library_b要么是其子类;
//不过我们可以确定dx一定是Library_b类型

当然C++中并不是所有pointer或reference都带来多态结果,例如:

//没有多态,因为操作对象不是class object
int* pi;

//没有多态
void* pi;

//有多态,操作对象是class object
X *px;

C++有以下方法支持多态:

  1. 经过一系列隐式转化,例如derived class的指针指向base class

    shape *ps=new circle();
    
  2. 经由virtual function机制:

    ps->rotate();
    
  3. 经过动态转换(cast)和typeid运算符:

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

本小节最后我们来看一个多态的例子:

class Z:public X;
void rotate(X d,const X *p,const &r){
    //执行期前无法知道到底调用哪一个rotate()实例
    *p.rorate();
    r.rorate();
    
    //总是调用X::rotate()
    d.rotate();
}
main(){
    Z z;
    rotate(z,&z,z);//那个参数调用哪一个rotate()实例应该能清楚了吧?
    return 0;
}

最后补充一个常识:指针大小是固定的,无论它指向什么对象。

1.3.1 指针的类型(The Type of a Pointer)

接上文,既然指针都一样大,我们怎么区分对待不同类型的指针呢?

C++中编译器会根据指针类型解释出某个特定的地址(包括其大小和内容)。所以例如:

  1. 一个指向地址1000的整数指针,在32位机器上将涵盖地址1000~1003;

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

    class ZooAnimal{
    public:
        ZooAnimal();
        virtual ~ZooAnimal();
        //...
        virtual void rotate();
    protected:
        int loc;
        String name;
    };
    
    ZooAnimal za("Zoey");//假设存在地址1000处
    ZooAnimal *pza = &za;
    

    上述两条语句指针布局:

在这里插入图片描述

那么,一个void *指针涵盖的空间大小是多少呢?诚然,我们并不知道!这就是为什么一个void * 指针只能够持有一个地址而不能通过它操作指向的对象的原因。

所以C++的转换(cast)其实是一种编译器指令,它通常不改变一个指针涵盖的真正地址,而是去影响对指针“所指内存大小和内容”的解释方式。

1.3.2 加上多态之后(Adding Polymorphism)

现在我们看看加上多态后的指针布局

class Bear:ZooAnimal{
public:
    Bear();
    ~Bear();
    //...
    void rotatea();
    virtual void dance();
    //...
protected:
    enum Dances{...};
    
    Dances dances_known;
    int cell_block;
};

Bear b("Yogi");//假设存在地址1000处
Bear *pb = &b;
Bear &rb = *pb;

上述代码指针的内存布局可能会是这样的:

在这里插入图片描述

知道了上述的内存布局,那么我们来看看下面的两个指针有什么区别:

Bear b;
ZooAnimal *pz = &b;
Bear *pd = &b;

他们都指向Bear object(b)的第一个byte.差别是,pb涵盖的地址是包含整个Bear object,而pz包含的地址只包括Bear objct中ZooAnimal subobject。

所以说除了ZooAnimal中出现的members,你不能用pz来直接处理Bear中的任何members.唯一的例外是通过virtual机制:

//不合法:cell_block不是ZooAnimal的一个member
//虽然我们知道pz目前指向一个Bear object
pz->cell_block;

//ok:经过显示的downcast操作就没问题!
(static_cast<Bear*>(pz))->cell_block;

//这样更好,但这是一个runtime operation(注:操作成本较高)
if(Bear* pb2 = dynamic_cast<Bear*>(pz))
    pb2->cell_block;
//ok:因为cell_block是Bear的一个member
pb->cell_block;

//当我们写:
pz->rotate();
//编译器又会怎么做呢?

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

  1. 固定可用的接口。也就是说pz只能调用ZooAnimal的public接口。
  2. 该接口的access level。(例如rotate()是ZooAnimal的public member)。

在每一个执行点,pz所指向的object类型可以决定rotate()所调用的实例。要说明的是类型信息并不是由pz维护,而是由维护在link中(后续章节会解释link)

现在我们再来看下面的情况:

Bear b;
ZooAnimal za = b;//引起切割
//调用ZooAnimal::rotate();
za.rotate();

对此我们提出两个问题并一一解答:

  1. 为什么调用的是ZooAnimal::rotate()? 答:za是一个ZooAnimal对象而不是(也绝不可能是)一个Bear对象。
  2. 为什么assignment操作时(za=b),za的vptr不指向bear的virtual table? 答:编译器必须确保如果某个object含有一个或一个以上的vptrs,那些vpters的内容不会被base class object初始化或改变。

这里引入一个观念:OO程序设计并不支持对object的直接处理。来看下面的代码:

class Panda:public Bear{...};

ZooAnimal za;
ZooAnimal *pza;

Bear b;
Panda *pp = new Panda;

pza = &b;

代码的内存布局可能如下图:

在这里插入图片描述

这里将za或b或pp所含的内容(也是地址)指定给pza显然是没有问题的。pointr或reference之所以支持多态是因为他们并不引发内存中任何“与类型有关的内存委托操作。”然而如果任何人企图改变object za 的大小就会违反资源分配的要求。

所以当一个base class object被初始化为一个derived class object,derived class object就会被切割以塞入更小的base class object,多态不再呈现。

总而言之多态是一个强大的机制,C++通过pointer和reference来支持多态!

第一章到这里就结束了,可能会云里雾里,但是坚持就会有 收获哦。。下一章继续努力,yeah!!!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值