《深度探索C++对象模型》读书笔记第一章:关于对象

《深度探索C++对象模型》读书笔记第一章:关于对象

前言

c语言中,“数据”和“处理数据的操作(函数)” 是分开声明的,将这种程序方法称为程序性(procedural),比如声明一个struct Point3d:

struct Point3d {
    float x;
    float y;
    folat z;
};

而操作该数据数据的函数例如打印函数,只能另外定义成:

void Point3d_print(const Point3d *pd) {
    print("(%f, %f, %f)", pd->x, pd->y, pd->z);
}

或者定义成一个宏:

#define Point3d_print(pd) print("(%f, %f, %f)", pd->x, pd->y, pd->z);

而在C+中,可以采用独立的“抽象数据类型”(ADT,abstract data type)来实现:

class Point3d {
private:
    float _x;
    float _y;
    float _z;
public:
    Point3d(float x = 0.0, float y = 0.0, float z = 0.0) :
        _x(x), _y(y), _z(z) { }
    float x() { return _x; }
    float y() { return _y; }
    float z() { return _z; }
};
inline osteream&
operator<< (ostream& os, const Point3d &pt) {
    os << "(" << pt.x() << ", " << pt.y() << ", " << pt.z() << ")";
}

或者一个三层的class层次结构来完成,例如
类继承
此外利用模板template,可以将坐标类型和坐标个数都参数化:

template <typename type, int dim>
class Point {
private:
    type _coords[dim];
public:
    Piont();
    Point(type corrds[dim]) {
        for(int i = 0; i < dim; ++i) {
            _corrds[i] = corrds[i];
        }
    }
    type& operator[](int index) {
        return const_cast<type&>
            (static_cast<const type&>(*this)[index]);
    }
    type& operator[](int index) const {
        assert(index >= 0 && index < dim);
        return _corrds[index];
    }
};
inline template<typename type, int dim>
ostream&
operator<<(ostream& os, const Point<type, dim> &pt) {
    os << "(";
    for(int i = 0; i < dim - 1; ++i) {
        os << pt[i] << ",";
    }
    os << pt[dim - 1] << " )";
}

从软件工程的角度来看,C++中的一个ADT(尤其是使用template)比C中的程序性使用全局数据要更好,威力更大。
封装之后的成本:主要是由virtual引起的,包括:

  1. virtual function,用于支持一个有效率的执行期绑定(running binding)。
  2. virtual base class,用于实现多继承体系中的base class,单一共享实例。

一.C++对象模型(The C++ Object Model)

在C++中有两种成员数据:static data members(静态) 以及non-static data members(普通非静态)
三种成员函数:static member function(静态成员函数), non-static member function(一般非静态成员函数)以及virtual member function(虚函数)
例如下面这个类:

class Point {
public:
    Point(float xval);
    virtual ~Point();  //virtual member function(虚析构函数)
    float x() const;  //non-static member function
    static int PointCount();  //static member function
private:
    float _x;    //static  data member
    static int _point_count;  //non-static  data member
}

(1). 简单对象模型
一个object都是一系列的slot, 每一个slot指向一个member,按照声明顺序,如下:
简单对象模型
slot的索引为3时,指向的是函数float Point::x();很容易知道一个class object的大小为指针大小乘上class中声明的member的个数
(2). 表格驱动的对象模型
这种模型抽取members 的信息,放在一个data member table 和一个 member function table中,而class object中则含有指向这两个table的指针,例如:
表格驱动
(3). C++ 对象模型
C++对象模型将静态数据成员静态成员函数一般非静态成员函数均存放在个别的class object之外(单独存取,和对象无关),而非静态数据成员则被放在每一个class object内,虚函数则以下面两个步骤支持:
1. 每一个class产生一堆指向虚函数的指针,并且按照顺序置于虚函数表(virtual table,vbtl)
2. 每一个class object安插一个指针(vptr)指向第一步的virtual table。vptr的设定以及重置都由每一个class的construtor, destrutor和copy assignment自动完成。(由于构造函数来设定vptr,故构造函数无法称成为虚函数)。每一个class 所关联的type_info object(用于支持RTTI)也通常放在虚函数表的第一个slot
例如:
C++对象模型
(4).加上继承后的对象模型简介
在虚继承,virtual base class不管在继承链上被派生多少次,派生类中永远只会存在一个virtual base class的一个实例(有且仅有一个subobject),例如在以下iostream继承体系中,派生类iostream只有virtual ios base 的一个实例。
虚继承
那么一个派生类是如何模塑其基类的实例呢?
base table模型:每一个class object内含一个bptr(pointer to base ctable),例如:
bptr
iostream类中的pbtr指向它的两个基类的table,此外,这种模型下存取的时间和继承的深度是密切相关的。例如:iostream object 要存取到ios subobject必然要经过两次。更加详细的讨论可以见第三章第四节。
(5). 对象模型是如何影响程序?
举个例子:class X定义了copy constructor, virtual destructor和一个virtual function foo()。

X foobar() {
    X xx;
    X *px = new X;
    xx.foo();
    px->foo();
    delete px;
    return xx;
}

内部的转化可能是:

void foobar(X& _result) {   //NRV优化
    //构造_result来代替返回的local xx
    _result.X::X(); 

    //扩展X *px = new X;
    X *px = new(sizeof(X));
    if(px != 0) px->X::X();
    foo(&_result);

    //扩展px->foo();
    (*px->vtbl[2])(px);

    //delete px;
    if(px != 0) {
            (*px->vtbl[1])(px);//destructor
        _delete(px);
    }
    return;
}

类X的对象模型如下:
类X的对象模型
函数转化的过程可以见第四章,new和delete的转化可以见第六章.

二.关键字多带来的差异

关键字struct和class:在C语言中,struct代表的是一个数据集合体,没有private data,member function;而在C++中,struct和class均是代表类,唯一的差别在于类的默认访问权限和默认继承类型是不同的:

扫描二维码关注公众号,回复: 1106465 查看本文章
关键字 默认访问权限 默认继承类型
struct public public
class private private

另外还有一点:template并不兼容C,因此template<> struct是错误的。

三.对象的差异

C++支持三种程序设计范式(programming paradigms):程序模型(procedural model),抽象数据类型模型(abstract data type model, ADT)以及面向对象模型(object-orient model)。
只有通过pointer或者reference的间接处理,才能支持多态(用基类对象的指针或者引用处理派生类接口),否则很可能产生切割(将派生类对象直接赋给基类对象)
关于多态的动态类型和动态绑定参见c++中使用空指针调用成员函数的理解
举个例子:class Z public继承自 class X,X中有个虚函数rotate(),考虑下面的代码:

void rotate(X datum, const X *pointer, const X& reference) {
    //下面这个操作总是调用X::rotate();
    datum.rotate();
    //下面这两个操作必须在运行期才能知道调用的是那个rotate()实例
    (*pointer).rotate();
    reference.rotate();
}
main() {
    Z z;
    rotate(z, &z, z);
    return 0;
}

main()函数中,rotate(z, &z, z);中的第一个参数z不经过virtual机制,因此总是调用X::rotate(),而另外两个参数由于一个是指针,一个是引用,因此都调用的是Z::rotate();
class object需要的内存大小:
一般情况下等于其non-static data member的总和大小加上为了支持virtual而内存产生的额外负担以及由于alignment的padding(内存对齐)
指针的类型:
指向不同的类型的指针之间的差异在于:其所寻地址出来的object的类型不同,也就是说,指针类型会教导编译器如何解释某个特定的地址中的内存内容与大小;另外转换cast其实是一种编译器指令,只影响地址中的内存内容与大小的解释方式,并不会改变指针所指向的地址。
例如,下面class ZooAnimal声明:

class ZooAnimal {
public:
    ZooAnimal();
    virtual ~ZooAnimal();
    virtual rotate();
protected:
    int loc;
    string name; 
};
ZooAnimal za("Zoey");
ZooAnimal *pza = &za;

内存布局如下:
内存布局
int在32位机器上一般是4bits,内存中的地址涵盖1000~1003,string通常是8bits(包括4bits的字符指针以及4bits的字符长度的整数),地址涵盖1004~1011,最后是4bits的vptr,地址涵盖1012~1015.
加上多态之后:
定义一个class Bear继承自class ZooAnimal:

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;
ZooAnimal *pz = &b;

b,pb,rb的内存需求是怎样的呢?
b是一个Bear Object,所需要的内存位24bytes,
包括ZooAnimal的16bytes以及Bear本身的8bytes,而指针pb以及引用rb只需要4bytes(在32位机器上)。具体见下图:
内存2
假设Bear object b放在地址1000处,那么Bear指针pb 和ZooAnima指针pz有什么区别呢?它们都是指向Bear Object的首地址(第一个byte即1000),差别在于pb所涵盖地址包括整个Bear Object即1000~1023,而pz所涵盖地址仅仅包括ZooAnimal Subobject即1000~1015.

派生类和基类的类型转换

1.针对类对象,用基类对象为派生类对象赋值或者初始化,或者用类型转换符将基类对象转化为派生类对象,都是非法的;用派生类对象为基类对象赋值或者初始化,或者用类型转换符将派生类对象转化为基类对象,是可以的,但是部发生切割(sliced).
2.针对指针或者引用,将基类对象绑定到派生,类对象的指针或者引用上,这是非法的;而将派生类对象绑定到基类的指针或者引用上,则是合法的,并且这是多态的基础条件。
3.总结:不能讲基类转化为派生类

主要参考自侯捷老师的《深度探索C++对象模型》第一章 关于对象。

猜你喜欢

转载自blog.csdn.net/qq_25467397/article/details/80390917