跳转至

📔 Chapter01 关于对象 读书笔记

Chapter01 关于对象

对于C来说,数据和函数分开声明,语言本身并不支持 “数据和函数” 之间的关联性。这种程序称为过程性语言。

// 定义数据本身
typedef struct point3D
{
    float x;
    float y;
    float z;
}Point3D;

// 声明定义函数
// 函数:处理数据的操作
void Point3D_print(const Point3D *pd)
{
    printf("{%g, %g, %g}",pd->x, pd->x, pd->z);
}

在C++则是使用独立的 抽象数据结构(ADT) 来实现或者通过一个以二层或者三层的class体系来完成。不管什么方式实现,都可以被参数化。例如实现上述程序转化为C++实现。

template <class type>
class Point3D
{
public:
    point (type x = 0.0, type y = 0.0, type z = 0.0) : _x(x), _y(y), _z(z) {};
    type x() {return _x;}
    void setX(type xval) {_x = xval;}
    void setY(type yval) {_y = yval;}
    void setZ(type zval) {_z = zval;}
private:
    type _x;
    type _y;
    type _z;
};

通过坐标类型和坐标数目两者都参数化:

template<clas type, int dim>
clas Point3D
{
public:
    Point3D();
    Point3D(type coords[dim]) {
      for (int i = 0; i < dim; i++){
        _coords[i] = coords[i];
      }
    }
    // [] 运算符重载
    type& operator[](int i) {
      assert(i < dim && i >=0);
      return _coords[i];
    }

    //
    type operator[](int i) const { /*same as non-const instance */}

private:
    type _coords[dim];
};

inline
template<class type, int dim>
ostream& operator<<(ostream &os, const Point3D<type, dim> &pt){
    os << "{";
    for (int ix = 0; ix < dim - 1; ix++){
      os << pt[ix] << ", ";
    }
    os << pt[dim - 1];
    os << "}";
}

1.1 加上封装后的布局成本

对于封装后,一般不会增加任何成本。三个data members 直接内含于一个class object中,类似C中struct,而members function 虽然在class 声明中,却不出现在object中。

实际上,类的封装不会带来额外的成本。

C++在布局以及存取时间上主要的额外负担是由 virtual 引起:

  • virtual function 机制:用来支持一个有效率的 “执行期绑定(runtime binding)”。
  • virtual base class:用来实现 “多次实现在继承体系中的 base class”,有一个单一而被共享的实体。

多重继承也会造成额外的负担。

1.2 C++对象模式

  • 两种class data members:static 和 nonstatic
  • 三种class member functions:static、nonstatic 和 virtual。

1.2.1 简单对象模型

模型中,一个object是一系列的slots,每个slot指向一个members,Members 按其声明次序,各被指定一个slot,每一个data member 或者function member 都有自己的一个slot。

对于这种索引或者slot数目的概念,被应用在C++中的 “指向成员的指针”概念中。

1.2.2 表格驱动对象模型

把所有与members相关的信息抽出来,放在两个table中:

  • data member table
  • member function table

class object本身内含这两个表格的指针。data table 直接包含member,func table则包含指针。

1.2.3 C++对象模型

Nonstatic data members 存放在class object中static data members 存放到所有的class objetc之外staticnonstatic function members 则放在所有的class object之外。virtual function则使用两个步骤来进行支持:

  • 每一个class产生一堆指向virtual function 的指针,放在叫作virtual table(vtbl)

  • 每一个class object 被添加一个指针,指向相关的 virtual table。通常称指针为vptr 。vptr的设定和重置由constructor, destructor, copy assignment 运算符自动完成。每个class关联的 type_info object(用来支持RTTI),也经由vitual table被指出来,通常放在表格的第一个slot。

说明:

  • RTTI:Runtime Type Identification。

  • 优点:

    • 空间和存取时间效率提高
  • 缺点:

    • 如果程序代码本身未改变,但所用到的class object的nonstatic data members发生变化,则需要重新编译代码。

总结:

  • 一个vtbl 对应一个class,而一个vptr 对应一个class object。

1.2.4 引入继承后的成本

C++支持单一继承,也支持多重继承。而在继承关系中也可以指定为虚拟的(virtual,也有共享的意思)。

在虚拟继承中,base class 不管在继承关串链中被派生了多少次,永远都只会存在一个实体(称为subobject)。

1.3 关键字带来的差异

在C所支持的struct 和 C++所支持的class 之间,如果C++使用者,使用自定义类型的格式如下,则可以称为class,但也可以说是struct:

1
2
3
4
5
6
7
8
9
// struct 名称(或者class名称)暂时省略
{
public:
    operator int();
    virtual void func();
    // ...
private:
    static int object_count;
};
  • C程序员的巧计有时却是C++程序员的陷阱。

    1
    2
    3
    4
    5
    6
    7
    struct mumble{
        /*stuff*/
        char *pc[1];
    };
    
    struct mumble *pmumble = (struct mumble*) malloc(sizeof(struct mumble) + strlen(string) + 1);
    strcpy(&mumble.pc, string);
    

如果改用class 来声明,而该class是:

  • 指定多个access sections,内含数据。
  • 从另一个class 派生而来。
  • 定义一个或者多个virtual function。

c++ 中凡是处于同一个access section的数据,必定保证以期声明顺序出现在内存布局中。然而被放置在多个access section中的成员,排列顺序就不一定了。

  • 如果需要一个相当复杂的C++ class 的某部分数据,使其有C声明的影子,那么将那部分抽取出来作为一个独立的struct声明。将C与C++组合在一起的做法是:

    • 从C struct 中派生出C++的一部分
    struct C_point {/*...*/};
    class Point : public C_point { /*...*/};
    

    但是该方式不再推荐使用,因为某些编译器在支持virtual function 机制过程中,对class的继承布局做出了调整。

  • 组合:而非继承,才是将C与C++组合在一起的唯一可行方法

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

class Point {
public:
    operator C_point() { return _c_point; }
    // ...
private:
    C_Point _c_point;
    // ...
};
  • 在组合的情况下,C++ struct 在C++中,当要传递 class object 到某个C函数中时,struct 声明会将数据封装起来,保证拥有与C兼容的空间布局。
  • 如果是继承,那么编译器会决定是否有额外data member 安插到base struct subobject 之中。

1.4 对象的差异

C++程序设计模型支持是那种程序设计范式

  • 过程式模型(procedural model)
  • 抽象数据类型模型(Abstract data type model,ADT)
  • 面向对象模型(Objec-oriented model)

在C++中,多态只存在于一个个的 public class 体系中。

Nonpublic 的派生行为以及类型为 void* 的指针可以说是多态,但支持性不足,所以严格意义上说,不算是多态。

1.4.1 C++ 支持多态的方式

  • 经由一组隐含的转化操作
// 将派生类指针转化为一个指向public base type的指针
shape *ps = new circle();
  • 经由 virtual function 机制
ps->rotate();
  • 经由 dynamic_cast 和 typeid 运算符
1
2
3
if (circle * pc = dynamic_cast<circle *> (ps)) {
  // ...
}

1.4.2 C++ class object的内存大小

  • 其nonstatic data members 的总和大小
  • 加上任何由 alignment(译注)的需求而填补(padding)上去的空间(可能存在于members之间,也可能存在于集合体边界)
  • 加上为了支持virtual 而由内部产生的任何额外负担。

一个指针,不管它指向哪一种数据类型,指针本身所需要的内存大小是固定的。

1.4.3 指针的类型

“指针类型” 会教导编译器如何解释某个特定地址中的内存内容及其大小。

对于转型(cast),其实就是一种编译器指令,大部分情况下,它并不是改变一个指针所含的真正的地址,只影响“被指出之内存的大小和其内容”的解释方式。

1.4.4 引入多态后

编译器在初始化及指定操作(将一个class object 指定给另一个class object)之间做出仲裁,编译器必须确保某个object含有一个或者一个以上的vptr,确保vptrs的内容不会被base class object 初始化或者改变。

对于一个 point 或者一个 reference 来说,之所以支持多态,是因为并不会引发内存中任何 “与类型有关的内存委托操作”,但是会受到改变它们所指向的内存的 “大小和内容解释方式” 的影响。

无论是在 “内存的获得” 或者是 “类型的决断” 上,C++通过 classpointersreference 来支持多态,这种程序设计风格称为 “面向对象”。

C++也支持具体的ADT程序风格。