深度探索C++对象模型 学习笔记 第一章 关于对象

宏定义:

#define Point3d_print(pd) \
    printf("(%g, %g, %g)", pd->x, pd->y, pd->z);    // %g表示自动选择合适的表示法输出

在C++中,可以通过抽象数据类型ADT(一个class中的成员函数)实现(纯C的struct中不能有函数,但可以通过struct中的函数指针实现),也可以通过一个继承体系将操作传给基类函数等方法实现。还能使用模板参数化来实现类,如三维点类可传入坐标类型实例化模板,还可传入维数实例化模板。

使用C++的class封装后,布局成本没有增加,三维点坐标还是像C的struct一样内含在一个class object中,而成员函数虽然声明在class中,但不出现在对象中,每一个非inline的成员函数只会诞生一个实体,而inline函数会在使用处展开。C++在布局及存取时间上的额外负担主要由virtual引起,包括动态绑定、虚继承、派生类向基类转换。

如下类:

class Point {
public:
    Point(float xVal);
    virtual ~Point();

    float x() const;
    static int PointCount();
private:
    virtual ostream &print(ostream &os) const;
    float _x;
    static int _point_count;
};

简单对象模型:
在这里插入图片描述
这个模型很简单,它可能是为了降低C++编译器设计复杂度开发出来的,赔上的是空间和执行期效率。这个简单模型中,对象是一系列的slots,每个slot指向一个成员函数或数据成员,它们按声明次序排列。

这个简单模型中,成员并不在该对象中,其中只有指向成员的指针,可以避免因成员类型不同,而需要不同存储空间导致的问题。对象中成员由slot的索引来寻址,本例中_x索引为6,一个类对象的大小即指针大小乘成员数。该此模型没有被用于实际产品。

表格驱动对象模型:
在这里插入图片描述
这种对象模型是把成员函数和数据成员分别放在两个表格中,而类对象本身含指向这两个表格的指针。成员函数表中有很多slots,存放指向函数的指针,而数据成员表中直接含实际数据。它也没有用于实际的C++编译器上。

C++对象模型:
在这里插入图片描述
此模型中,非static数据成员被放在类对象之内,static数据成员存放在类对象之外,static和非static成员函数都被放在类对象之外,以下两个步骤用以支持虚函数功能:
1.每个类产生一个虚表,其中存放指向虚函数的指针。
2.在类对象中有指向虚表的指针,通常这个指针被称为vptr,它的设定和重置都由每一个class的构造函数、析构函数和拷贝赋值运算符自动完成。每个类关联的type_info对象用以支持RTTI(运行时类型识别),也经由虚表指出,通常存在虚表的第一个slot处。

一个派生类如何表示基类实体,在简单对象模型中,每一个基类可被派生类对象中的一个slot指出,该slot内含基类子对象的地址,但由于间接性会导致空间和存取时间上的额外负担,优点是类对象的大小不会因基类的改变受到影响。

另一种派生类表示基类的方式是基类表,类似于虚函数表:
在这里插入图片描述
上述方式的间接性由继承的深度而增加,继承深度越深,间接性开销越大,如iostream需要两次间接存取才能取到ios的成员。如果在派生类对象中存放指针指向每一个基类的成员,可获得一个永远不变的存取时间,但在派生类中需要额外空间放置这些额外指针。

C++最初的继承模型不用任何间接性,基类的数据成员直接放在派生类对象中,这样存取基类成员最有效率,但当基类有任何改变时,所有用到此基类和其派生类对象者必须重新编译。

虚基类的原始模型是在类对象中为每一个关联的虚基类加上一个指向它的指针。其他的模型不是导入一个虚基类表就是扩充已经存在的虚表用来维护每一个虚基类的位置。

如下函数:

X foobar() {
    X xx;
    X *px = new X;
    
    // foo()是一个虚函数
    xx.foo();
    px->foo();

    delete px;
    return xx;
}

此函数可能在内部被转化为:

// 虚拟C++码
void foobar(X &_result) {
    _result.X::X();    // 构造_result

    // 扩展X *px = new X;
    px = _new(sizeof(X));
    if (px != 0) {
        px->X::X();
    }

    // 扩展xx.foo(),但不使用虚函数机制
    foo(&_result);

    // 使用虚函数机制扩展px->foo()
    (*px->vtbl[2])(px);

    // 扩展delete px;
    if (px != 0) {
        (*px->vtbl[1])(px);    // 调用虚析构函数
        _delete(px);
    }

    // 直接返回,不需要摧毁局部对象xx
    return;
}

对于以上代码中的vtbl,下图是该对象的布局:
在这里插入图片描述
由于C++尽量与C相容,C++变得比不这么做复杂得多,例如,如果不用支持C中8种类型的整数,重载函数会变得简单得多。类似地,如果C++抛弃C的声明语法,就不用向前看也能确定以下是定义还是声明:

// 向前看到1024才知道是函数调用而非声明
int (*pf)(1024);

// 以下定义即使向前看也分不出是声明还是调用
int (*pq)();    // 强制规定当分不清是函数调用还是声明时当作声明

类似地,C++可只使用class表示类,而不用兼容C中的struct。

C中可以把单一元素的数组放在一个struct的尾端,于是每个该struct的对象可以拥有可变大小的数组:

struct mumble {
    char pc[1];
};

struct memble *pmumbl = (struct mumble *)malloc(sizeof(struct mumble) + strlen(string) + 1);    // string是一个C风格字符串

strcpy(&memble.pc, string);

而如果改用class来声明,而该class是:
1.指定多个访问区段(一个public、protected或private),内含数据。
2.从另一class派生而来。
3.定义一个或多个虚函数。
那么可能不成功,此时最好不这么使用。

C++中处于同一个访问区段内的数据在内存中的布局顺序必定和声明次序一样,而多个访问区段中的数据成员排列次序不一定,需要视protected数据成员是否在private数据成员前而定,如在前,才能完成以上目的:

class stumble {
public:
    // ...
protected:
    // ...
private:
    char pc[1];
}

基类和派生类数据成员的布局也没有谁先谁后的规定,也不保证以上C功能有效。虚函数的存在也可能导致以上功能失效。

如需要在C++的class中实现以上C中功能,最好将一部分抽取出来成为一个独立的struct声明,从C的struct中派生出C++的类,这是将C与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);    // C中函数

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

不推荐以上用法,因为某些编译器在支持虚函数时对于class的继承布局做了一些改变。组合而非继承,才是把C和C++结合在一起的唯一可行方法:

struct C_point { /* ... */ };    // C中struct

class Point {
public:
    operator C_point() {
        return _c_point;
    }
private:
    C_point _c_point;
};

C struct在C++中可以用于传递一个复杂的class对象的全部或部分到某个C函数中,struct声明可以将数据封装起来,并保证拥有与C兼容的空间布局,但只有在组合而非继承的情况下才保证。

C++程序设计模型支持三种程序设计典范:
1.程序模型,像C一样,C++也支持它,如处理字符串:

char boy[] = "Danny";
char *p_son;

p_son = new char[strlen(boy) + 1];
strcpy(p_son, boy);

if(!strcmp(p_son, boy)) {    // 返回值为0时表示两字符串相等
/* ... */
}

2.抽象数据模型类型(ADT):只提供接口而具体实现不明,如String类:

String girl = "Anna";
String daughter;

daughter = girl;    // String::operator=

if (girl == daughter) {    // String::operator==
/* ... */
}

3.面向对象模型,有彼此相关的类型,继承类体系。

使用一种典范写程序,否则可能出现不好的后果,如在一个面向对象模型中,使用派生类对象给基类赋值,会造成派生类部分被剪切,这种方法是抽象数据模型的方法,应使用指针或引用调用虚函数处理。即ADT处理固定而单一类型的实体,编译时已经定义好了,而OO典范需要处理运行时实体。

C++支持多态的方法:
1.派生类对象的指针或引用向基类的指针或引用的转换。
2.虚函数。
3.dynamic_cast和typeid运算符。

基类中的成员是派生类的共有部分,不需要在所有派生类中把公共部分都写一遍,还可以在改变派生类的共有内容时,只需要修改基类的内容。

需要多少内存才能表现一个类对象:
1.非static数据成员大小总和。
2.加上由于任何将数值调整到某数的倍数时而填补上去的空间(32位计算机上,通常为4bytes,即32位,以使bus运输量效率最高),填补空间可能存在于各数据成员之间,也可能存在于集合体对象边界。
3.为支持虚函数而产生的内部额外负担。

不管指针(或引用,引用通常以一个指针实现)指向哪种数据类型,指针本身所需内存大小是固定的。

所有指针无论是什么对象的指针,以内存需求的观点来说,没有什么不同,它们都需要足够的内存存放一个机器地址。它们的差异在于其所寻址出来的对象类型不同,即指针类型会告诉编译器如何解释某个特定地址中的内存内容和大小。

一个指向地址1000的int指针,在32位机器上,将涵盖地址空间1000~1003(因为32位机器上int是4字节)。

对于ZooAnimal类:

class ZooAnimal {
public:
    ZooAnimal();
    /* ... */
    virtual ~ZooAnimal();
    virtual void rotate();
protected:
    int loc;
    String name;
};

一个传统String是8比特(包括一个4字节的字符指针和一个用来表示字符串长度的整数),如指向ZooAnimal对象的指针指向1000,则ZooAnimal类对象可能的布局:
在这里插入图片描述

而一个指向地址1000的void *指针所涵盖的地址空间我们不知道,因此一个void *指针只能含有一个地址,而不能通过它操作它所指的对象。

转型(cast)并不改变一个指针所含的地址,只影响它所指内存的大小和内容的解释方式。

加上多态:

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

Bear b("Yoqi");
Bear *pb = &b;
Bear &rb = *pb;

不管是引用还是指针都只需要一个word(32位机器上,4字节)的空间,而Bear对象需要24比特,即ZooAnimal的16比特和Bear的8比特,可能的内存布局为:
在这里插入图片描述
假设Bear对象放在地址1000处:

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

上述代码中pz和pb都指向Bear对象的第一个比特,但pz涵盖的范围只有ZooAnimal子对象,而pb涵盖的地址包含整个Bear对象。因此不能用pz处理Bear的成员,除非通过虚函数:

((Bear *)pz)->cell_block;    // 对

if (Bear *pb2 = dynamic_cast<Bear *>(pz) {    // 也对,运行时识别,成本较高
    pb2->cell_block;    
}

当调用虚函数:

pz->rotate();

pz的类型在编译期决定:
1.固定的可用接口,即pz只能调用ZooAnimal的public接口。
2.该接口的访问等级,此处rotate是一个public成员。

每一个执行点,pz所指对象的类型决定rotate调用的实体,类型信息封装不是维护在pz中,而是维护在link中,这个link存在于对象的vptr和vptr所指的虚表之间。

当用一个派生类对象直接初始化或复制一个基类对象,派生类对象会被切割以塞入较小的基类对象内存中。

C++也支持具体的ADT程序风格,被称为object-based(OB),OB设计可能比一个OO设计速度更快且空间更紧凑,快是因为所有函数引发操作都在编译期间解析完成,空间紧凑是因为对象构筑起来不需要设置虚函数机制。但OB设计比较没有弹性。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值