0%

  • 一个空的class如:
    1
    2
    3
    4
    5
    // sizeof X == 1
    class X { }; // sizeof X == 1
    class Y : public virtual X {}; // sizeof X == 8
    class Z : public virtual X {}; // sizeof X == 8
    class A : public Y, public Z {};// sizeof X == 12
    1
    2
    3
    4
    5
    graph BT
    Y --- X
    Z --- X
    A --- Y
    A --- Z
    事实上并不是空的,它有一个隐藏的1byte大小,那是编译器安插进去的一个char。有机器上Y、Z得出大小是8。这个值的大小和机器有关,也和编译器有关:
    1. 语言本身所造成的额外负担(overhead)。当语言支持virtual base classes时,会导致额外负担。在derived中,反映在某种形式的指针身上,它或者指向virtual base class subobject,或指向一个相关表格;表格中是virtual base class subobject地址。
    2. 编译器对于特殊情况所提供的优化处理
    3. Allgnment的限制。大部分机器结构体大小会收到alignment的限制(内存对齐),使它们能够更有效率在内存中被存取。alignment就是将数值调整到某数的整数倍
  • Empty virtual base class已经成为C++OO设计的一个特有术语了。他提供一个virtual interface,没有定义任何数据。
  • 对于class A竟然大小为12这个结果。记住,一个virual base class sbobject只会在derived class中存在一份实例,不管它在继承体系中出现多少次。它的大小由以下决定:
    1. 被大家共享的唯一一个Class X,大小为1byte
    2. Base class Y,减去因virtual base class X二配置的大小,结果是4bytes
    3. class A自己的大小:0byte
    4. class A的alignment数量
  • C++对象模型尽量以空间优化和存取速度优化的考虑来表现nonstatic data members,并且保持和C语言struct数据配置的兼容性。至于static data members,则被放置在程序的一个global data segment中,不会影响个别的class object大小。在程序之中,不管class被产生出多少个objects(经由直接产生或间接产生),static data members永远只存在一份实例。但是一个template class的static data members的行为稍有不同。
  • 每一个class object必须有足够的大小容纳所有的nonstatic data members,因为它可能比你想象的还大,原因是:
    • 编译器自动加上额外的data members,用以支持某些特性(virtual)。
    • 因为alignment的需要

3.1 Data Member的绑定(The Binding of a Data Member)

  • 早期C++有两种防御性程序设计风格:
    1. 把所有data members放在class声明处,以确保正确的绑定:
      1
      2
      3
      4
      5
      6
      7
      class Point3d
      {
      float x, y, z;
      public:
      float X() const { return x; }
      // ...etc
      };
    2. 把所有的inline functions,不管大小都放在class声明之外:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      class Point3d
      {
      public:
      Point3d();
      float X() const;
      void X(float) const;
      // ...etc
      };

      inline float Point3d::X() const
      {
      return x;
      }
      但这种设计风格在C++2.0之后就不存在了。这个古老的语言规则称为“member rwriting rule”。大概意思是“一个inline函数实体,在整个class声明未被完全看见之前,不会被评估求值(evaluated)的”。也就是说:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      extern int x;
      class Point3d
      {
      public:
      // 对于函数本体的分析延迟,直到class声明右大括号出现才开始
      float X() cons { return x; }
      private:
      float x;
      };
      // 分析在这里进行
      因此一个inline member function躯体之内的data member绑定操作,会在整个class声明完成之后才发生。但这对member function的argument list并不是真的。argument list中的名称会在第一次遇到的时候被适当resolved完成。因此extern和nested type names之间的非直觉绑定操作还是会发生。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      typedef int length;
      class Point3d
      {
      public:
      // length is resolved for global
      // _val is resolved for Point3d::_val
      void mumble(length val) { _val = val; }
      length mumble() { return _val; }
      private:
      // length必须在class对他第一个参考操作前被看见
      // 这样的声明使之前操作不合法
      typedef float length;
      length _val;
      // ...
      };
      上述的语言状况,仍然需要某种防御性程序风格:请总是把“nested type 声明”放在class的起始处。这样做才能保证非直觉绑定的正确性。

3.2 Data Member的布局(Data Member Layout)

  • 已知一组data members:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Point3d {
    public:
    // ...
    private:
    float x;
    staic List<Point3d*> *freeList;
    float y;
    static const int chunkSize 250;
    float z;
    };
    Nonsatic data members在class object中的排列顺序和其被声明的顺序一样任何中介的static data members都不会放进对象布局之中。上述例子中,每个Point3d对象是由三个float组成的。static data members存放在程序的data segment中,和个别class objects无关。
    C++ Standard要求,在同一个access section(private、public、protected等区段)中,members的排列只需要符合“较晚出现的members在class object中有较高的地址”这一条件就可以了。下面这个template funciton,接受两个data members,然后判断谁先出现在class object中。如果两个members都是不同的access sections中的第一个被声明者,函数就会判断哪个section先出现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    template<class class_type, class data_type1, class data_type2>
    char* access_order(data_type1 class_type::*mem1, data_type2 class_type::*mem2)
    {
    assert (mem1 != mem2);
    return mem1 < mem2 ? "Member 1 occurs first" : "member 2 occurs first";
    }

    // call
    access_order(&Point3d::z, &Point3d::y);

3.3 Data Member的存取

  • 已知这段代码:
    1
    2
    3
    4
    Point3d origin, *pt = &origin;
    // 存取data members, like this:
    origin.x = 0.0;
    pt->x = 0.0;
    通过origin存取和通过pt存取有什么重大差异吗?
Static Data Members
  • Static data members,按字面意义,被编译器提出class之外,并被视为global变量。每个member的存取许可,以及class的关联,都不会招致任何空间上或执行时间上的额外负担。每个static data member只有一个实例。
  • 但如果static data members是一个从复杂继承关系中继承而来的,它仍然只有唯一一个实例,其存取路径仍然那么直接。如果static data member经由函数调用,或其他某些语法存取呢?例如:
    1
    2
    3
    4
    5
    foobar().chunkSize = 250;

    // 可能的转化
    (void) foobar();
    Point3d.chunkSize = 250;
    若取一个static data member的地址,会得到一个指向其数据类型的指针,而不是指向其class member的指针,因为static member并不内含在一个class object之中:
    1
    2
    3
    &Point3d::chunkSize;
    // 会得到如下类型的内存地址
    const int*
  • 如果有两个classes,每个都声明了一个static member freeList,那么当它们放在程序的data segment时,会导致名称冲突。编译器的解决方式是暗中对每个static data member编码name-mangling),以获得一个独一无二的程序识别代码。任何name-mangling做法都有两个重点:
    1. 一个算法,推导出独一无二的名称
    2. 万一编译系统(或环境工具)必须和使用者交谈,那些独一无二的名称可以轻易被推导回到原来的名称
Nonstatic Data Members
  • Nonstatic data members直接放在每个class object中。除非经由显式explcit)或隐式的(implicit),否则没有办法直接存取它们。只要在member funcion中直接处理一个nonstatic data member,implicit class object就会发生。例如:
    1
    2
    3
    4
    5
    Point3d Point3d::translate(const Point3d &pt) {
    x += pt.x;
    y += pt.y;
    z += pt.z;
    }
    表面上x、y、z直接存取,事实上是经由implicit class object(由this指针表述)完成的。这个函数的参数是:
    1
    2
    3
    4
    5
    6
    // member function的内部转化
    Point3d Point3d::translate(Point3d *const this, const Point3d &pt) {
    this->x += pt.x;
    this->y += pt.y;
    this->z += pt.z;
    }
  • 欲对一个nonstatic data member进行存取操作,编译器需要把class object的起始地址加上data member的偏移位置(offset)。例如:
    1
    2
    3
    origin._y = 0.0;
    // 相当于
    &origin + (&Point3d::_y - 1);
    每一个nonstatic data member的偏移位置(offset)在编译时期即可获知,甚至如果派生自单一或多重继承串链也是一样。
  • 再来看看虚拟继承。虚拟继承将为经由base class subobject存取class members导入一层间接性。
    1
    2
    origin.x = 0.0;
    pt->x = 0.0;
    从origin存取和从pt存取有什么重大差异?答案是当Pointe3d是一个derived class,而其继承结构中有一个virtual base class,并且被存取member是一个从该virtual base class继承而来的member就会有重大差异。这时候,我们无法在编译时期直到member真正的offset位置。这个存取操作必须延迟到执行期。如果使用origin就不会有这个问题。

3.4 “继承”于Data Member

  • 如果为2D和3D坐标点提供两个抽象数据类型:
    1
    2
    3
    graph BT
    Point2d
    Point3d
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // supporting abstract data types
    class Point2d {
    public:
    // constructor(s)
    // operations
    // access functions
    private:
    float x, y;
    };

    class Point3d {
    public:
    // constructor(s)
    // operations
    // access functions
    private:
    float x, y, z;
    };
    这和提供两层或三层继承结构,每一层(代表一个维度)是一个class,派生自较低维层次有什么不同?
只要继承不要多态(Inheritance without Polymorphism)
  • 我们可能希望不论是2D或3D坐标点,能共享同一个实例,但又能继续使用于类型性质相关的实例。以上的设计策略。带来的影响则是可以共享数据本身以及数据处理方法,并将它局部化。一般而言,具体继承concrete inheritance,相对于虚拟继承virtual inheritance)并不会增加空间或存取时间上的额外负担。
    1
    2
    graph BT
    Point3d --- Point2d
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class Point2d {
    public:
    Point2d(float x = 0.0, float y = 0.0) : _x(x), _y(y) {};
    float x() { return _x; }
    float y() { return _y; }
    void x(float newX) { _x = newX; }
    void y(float newY) { _y = newY; }
    void operator+=(const Piont2d &rhs) {
    _x += rhs.x();
    _y += rhs.y();
    }
    // ... more members
    protected:
    float _x, _y;
    };

    // inheritance from concrete class
    class Point3d : public Point2d {
    public:
    Point3d(float x = 0.0, float y = 0.0, float z = 0.0) : Point2d(x, y), _z(z) {};
    float z() { return _z; }
    void z(float newZ) { _z = newZ; }
    void oeprator+=(const Point3d &rhs) {
    Point2d::operator+=(rhs);
    _z += rhs.z();
    }
    // ... more members
    protected:
    float _z;
    };
    这样的设计的好处是可以把管理x和y坐标的程序代码局部化。也表现处两个类之间的紧密关系。但把原本两个独立不相干的classes凑成一堆”type/subtype“,并带有继承关系,会有什么易犯的错误呢?
    • 经验不足的人可能会重复设计一些相同操作的函数。以例子中的constructor和operator+=为例,它们并没有被做成inline函数。
    • 第二是,把class分解成两层或更多层,可能会为了”表现class继承体系的抽象化“而膨胀所需的空间。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      class Concrete {
      public:
      // ...
      private:
      int val;
      char c1;
      char c2;
      char c3;
      };
      在32位机器中,每个Concrete class object大小是8bytes,细分如下:
    1. val占用4bytes;
    2. c1、c2和c3各占用1bytes;
    3. alignment(调整到word边界)需要1bytes。
    现在把Concrete分裂为三层结构:
    1
    2
    3
    graph BT
    Concrete2 --- Concrete1
    Concrete3 --- Concrete2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Concrete1 {
    public:
    // ...
    private:
    int val;
    char bit1;
    };

    class Concrete2 : public Concrete1 {
    public:
    // ...
    private:
    int val;
    char bit2;
    };

    class Concrete3 : public Concrete2 {
    public:
    // ...
    private:
    int val;
    char bit3;
    };
    现在Concrete3 object的大小是16bytes,比原先的设计多了100%。(p106)
    1
    2
    3
    4
    5
    6
    7
    Concrete2 *pc2;
    Concrete1 *pc1_1, *pc1_2;
    // 如果C++把derived class members和Concrete1 subobject捆绑在一起,去除填补空间
    pc1_1 = pc2; // pc1_1指向Concrete2对象
    // derived class subobject被覆盖掉
    // 于是bit2 member现在有了一个并非预期的数值
    *pc1_2 = *pc1_1;
加上多态(Adding Polymorphism)
  • 如果在继承关系中提供要给virtual function接口:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class Point2d {
    public:
    Point2d(float x = 0.0, float y = 0.0) : _x(x), _y(y) {};
    // 之前的存取操作
    // 加上z的保留空间(目前什么也不做)
    virtual float z() { return 0.0; }
    virtual void z(float) {}
    // 谁当以下运算符为virtual
    virtual void operator+=(const Point2d &rhs) {
    _x += rhs.x();
    _y += rhs.y();
    }
    // ... more members
    protected:
    float _x, _y;
    };

    class Point3d : public Point2d {
    pblic:
    Point3d(float x = 0.0, float y = 0l0, float z = 0.0) : Ponit2d(x, y), _z(z) {};
    float z() { return _z; }
    void z(float newZ) { _z = newZ; }
    vodi operator+=(const Piont2d &rhs) {
    Point2d::oeprator+=(rhs);
    _z += rhs.z();
    }
    // ... more members
    protected:
    float _z;
    };
    这个设计有个好处是可以把operator+=运用在一个Pointe3d对象和一个Point2对象身上:
    1
    2
    3
    Point2d p2d(2.1, 2.2);
    Point3d p3d(3.1,, 3.2, 3.3);
    p3d += p2d;
    虽然class的声明语法没变,但事情不一样了:z() member function和operator+=()运算符都成了虚函数;每个Point3d class object内含一个额外的vptr member和一个Piont3d virtual table;此外每个virtual member function的调用也复杂了。
  • C++编辑器领域主要讨论的问题是把vptr放置在class object哪里最好?cfont编译器中,被放在class object的尾端:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct no_virts {
    int d1, d2;
    };

    class has_virts : public no_virts {
    public:
    virtual void foo();
    // ...
    private:
    int d3;
    };

    no_virts *p = new has_virts;
    struct no_virts nv; class has_virts :
    public no_virts hv;
    int d1 int d1
    int d2 int d2
      int d3
      __vptr__has_virts
    把vptr放在object尾端,可以保留base class C struct的对象布局,因而允许C程序代码也能使用,这做法出现在C++问世时。
    到了C++2.0,开始支持虚继承以及抽象基类,某些编译器开始把vptr放到class object前端:
    struct no_virts nv; class has_virts :
    public no_virts hv;
    :-: :-:
    int d1 __vptr__has_virts
    int d2 int d1
      int d2
      int d3
    vptr放在class object前端,对于多继承下通过指向class members的指针调用virtual function会有一些帮助。否则,不仅从class object起始点开始量起的offset必须在执行器备妥,class vptr之间的offset也必须备妥
    以下是Point2d和Point3d加上了virtual function之后的继承布局(单一继承):
    class Point2d p2d; class Point3d :
    public Point2d pt3d;
    :-: :-:
    float _x float _x
    float _y float _y
    __vptr__Point2d __vptr__Point2d
      float _z
多重继承(Multiple Inheritance)
  • 单一继承提供了一种自然多态natural polymorphism)形式,是关于base type和derived type之间的转换。base 它们的object都是从相同的地址开始。差异只在derived object比较大,用以容纳它自己的nonstatic data members。一个derived class指定给base class的指针或reference。这个操作并不需要编译器调停或修改地址。可以很自然地发生,而且提供了最佳执行效率。
  • 多重继承不像单一继承,也不容易模塑处模型。它的复杂度在于derived class和其base class之间的非自然关系:
    1
    2
    3
    4
    graph BT
    Point3d --- Point2d
    Vertex3d --- Point3d
    Vertex3d --- Vertex
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class Point2d {
    public:
    // ...有virtual接口,会有vptr
    protected:
    float _x, _y;
    };

    class Point3d : public Point2d {
    public:
    // ...
    protected:
    float _z;
    };

    class Vertex {
    public:
    // ... vptr
    protected:
    Vertex *next
    };

    class Vertex3d : public Point3d, public Vertex {
    public:
    // ...
    protected:
    float mumble;
    };
    多重继承的问题主要发生在derived class objects和后继base class objects之间的转换,不论是:
    1
    2
    3
    4
    extern void mumble(const Vertex&);
    Vertex3d v;
    ...
    mumble(v); // 不自然
    或是经由所支持的virtual function机制做转换。多继承对象符出的成本在于地址的指定操作而已:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Vertex3d v3d;
    Vertex *pv;
    Point2d *p2d;
    Point3d *p3d;

    // 指定操作
    pv = &v3d;
    // 需要内部转化
    pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d));
    // 如果v3d为指针类型,即pv3d,则:
    pv = pv3d ? (Vertex*)((char*)pv3d) + sizeof(Point3d) : 0; // 防止pv3d为0
    以下为多重继承Multiple Inheritance):
    class Point2d pt2d;
    float _x
    float _y
    __vptr__Point2d
    class Point3d :
    public Point2d pt3d;
    :-:
    float _x
    float _y
    __vptr__Point2d
    float _z
    Vertex v;
    :-:
    Vertex *next
    __vptr__Vertex
    class Vertex3d :
    public Point3d,
    public Vertex
    {}v3d;
    :-:
    float _x
    float _y
    __vptr__Point2d
    float _z
    Vertex *next
    __vptr__Vertex
    float mumble
虚拟继承(Virtual Inheritance)
  • 多重继承的一个语意上的副作用是,必须支持某种形式上的”shared subobject继承“。如早期的iostream library:
    1
    2
    3
    4
    5
    // pre-standard iostream implementation
    class ios { ... };
    class istream : public ios { ... };
    class ostream : public ios { ... };
    class iostream : public istream, public ostream { ... };
    下图可表现iostream的继承体系图。第一个为多重继承,第二个为虚拟多重继承:
    1
    2
    3
    4
    5
    graph BT
    istream --- ios1[ios]
    ostream --- ios2[ios]
    iostream --- istream
    iostream --- ostream
    1
    2
    3
    4
    5
    graph BT
    istream --- ios
    ostream --- ios
    iostream --- istream
    iostream --- ostream
    在iostream对象布局中,只需要一份就好:
    1
    2
    3
    4
    class ios { ... };
    class istream : public virtual ios { ... };
    class ostream : public virtual ios { ... };
    class iostream : public istream, public ostream { ... };
    上述iostream的实现挑战在于:**一个有效的方法,将istream和ostream各自维护ios subobjet,折叠成由iostream维护的单一ios subobject,并且保存base class和derived class的指针(reference)之间的多态指定操作polymorphism assignments)**。
  • 一般的实现方法是。Class如果内含一个或多个virtual base class subobjects,像istream那样,将被分割两部分:一个不变区域一个共享区域。不变区域中的数据,总是有固定的offset,所以这里可以直接存取。而共享区域,就是virtual base class subobject。它们只能被间接存取。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class Point2d {
    public:
    ...
    protected:
    float _x, _y;
    };

    class Vertex : public virtual Point2d {
    public:
    ...
    protected:
    Vertex *next;
    };

    class Point3d : public virtual Point2d {
    public:
    ...
    protected:
    float _z;
    };

    class Vertex3d : public Vertex, public Point3d {
    public :
    ...
    protected:
    float mumble;
    };
    1
    2
    3
    4
    5
    6
    graph BT
    Vertex -- _x,_y--- Point2d
    Point3d -- _z --- Point2d
    Vertex3d -- next --- Vertex
    Vertex3d -- _z --- Point3d
    none[ ] -- mumble --- Vertex3d
    这中间存在一个问题:如何能够存取class共享部分呢?cfont编译器是在derived class object中插指针,每个指向virtual base class。需要完成操作都是通过指针间接完成的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void Point3d::operator+=(const Point3d &rhs)
    {
    _X += rhs._x;
    _y += rhs._y;
    _z += rhs._z;
    };

    // 转化为虚构代码vbc为virtual base class
    __vbcPoint2d->_x += rhs.__vbcPoint2d->_x;
    __vbcPoint2d->_y += rhs.__vbcPoint2d->_y;
    _z += rhs._z;
    这样实现模型有两个主要缺点:
    1. 每个对象针对其每个virtual base class背负一个额外的指针。而我们希望class object负担是稳定的。
    2. 虚继承链加长会导致存取层次增加。我们呢希望有着固定的存取时间。
  • 对于第二个问题。MetaWare和其他编译器到今天还是用cfont原始模型:它们经由拷贝操作取得所有nested virtual base class指针,放到derived class object中,虽然有空间代价。
  • 至于第一个问题,有两个解决方法。Microsoft编译器引入virtual base class table。每个class object如果有一个以上virual base classes,编译器就安插指针,指向virtual base class table,真正的vptr则被放在该表格中。第二个解决方法,是在virtual function table中放置virtual base class的offset(而非地址)。
    class Vertex3d :
    public Vertex,
    public Point3d
    {…} v3d;
    Vertex *next
    Point2d *pPoint2d
    __vptr__Vertex
    float _z
    Point2d *pPoint2d
    __vptr__Point3d
    float mumble
    float _x
    float _y
    __vptr__Point2d
    该方法把virtual base class offsetvirtual function entires混杂在一起。Sum编译器中,virtual function table由正值或负值来索引。正值则索引到virtual functions;负值则索引到virtual base class offsets。
    1
    2
    3
    4
    5
    (this + __vptr__Point3d[-1])->_x +=
    (&rhs + rhs.__vptr__Point3d[-1])->_x;
    (this + __vptr__Point3d[-1])->_y +=
    (&rhs + rhs.__vptr__Point3d[-1])->_y;
    _ += rhs._z;
    因此Derived class实例和base class实例之间的转换操作为:
    1
    2
    3
    Point2d *p2d = pv3d;
    // translation
    Point2d *pt2 = pv3d ? pv3d + pv3d->__vptr__Point3d[-1] : 0;

3.5 对象成员的效率(Object Member Efficiency)

  • 下面测试旨在测试聚合aggregation)、封装encapsulation)以及继承inheritance)所引发的额外负荷程序。跳过。

3.6 指向Data Members的指针(Poniter to Data Members)

  • 考虑下面Point3d声明。有一个virtual function,一个static data member,以及三个坐标:
    1
    2
    3
    4
    5
    6
    7
    8
    class Point3d {
    public:
    virtual ~Point3d();
    // ...
    protected:
    static Point3d origin;
    float x, y, z;
    };
    每个Point3d object含有三个坐标值,依序为x、y、z,以及vptr。static data member origin放在class object之外。vptr的位置根据编译器不同而不同。不是放头就是尾。&Point3d::z;取得某个坐标成员的地址代表什么呢?代表着z坐标在class object中偏移位置(offset)
  • 一台32位机器上,每一个float是4bytes,所以期望获取地址偏移位置要么是8要么是12。然而获取的总是多1,也就是9和13(我的输出并没有+1)。
  • 在多继承下,要将第二个或后继base class的指针,和一个与derived class object绑定的member结合起来,那么将会因为需要加入offset值而变得复杂。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    struct Base1 { int val1; };
    struct Base2 { int val2; };
    struct Derived : Base1, Base2 { ... };
    void func1(int Derived::*dmp, Derived *pd)
    {
    // 期望第一个参数是一个derived class的member指针,结果是base class的会怎样。
    pd->*dmp;
    }

    void func2(Derived *pd)
    {
    // bmp将成为1
    int Base2::*bmp = &Base2::val2;
    // bmp == 1但在Derived中val2 == 5
    func1(bmp, pd);
    }
    要解决这个问题,必须:
    1
    2
    // 经由编译器内部转换
    func1(bmp ? bmp + sizeof(Base1) : 0, pd);
“指向Members的指针”的效率问题
  • 下面是测试数据。了解在3D坐标点的不同class表现方式下指向members的指针所带来的影响。

  • 关键词explicit之所以被导入这个语言,就是为了给程序员一个方法,使他们能够制止“单一参数的constructor”被当作conversion运算符。

2.1 Default Constructor的构造操作

“带有Default Constructor”的Member Class Object
  • 如果class没有constructor,但有一个member object,而且这个object有default constructor,那么这个class的implicit default constructor是nontrivial的。举个例子,下面程序片段中,编译器为class Bar合成一个default constructor:
    1
    2
    3
    4
    5
    6
    7
    class Foo { public: Foo(), Foo(int) ... };
    class Bar { public: Foo foo; char *str; }
    void foo_bar()
    {
    Bar bar; // Bar::foo 必须在此处初始化。
    if (str) { } ...
    }
    被合成的Bar default constructor内含代码,能够调用class Foo的default constructor处理member object Bar::foo,因为Bar::foo初始化是编译期的责任:
    1
    2
    3
    4
    5
    // 为member foo调用class Foo的default construcotr
    inline Bar::Bar()
    {
    foo.Foo::Foo();
    }
    而且,被合成default constructor只满足编译期需求,并不是程序员需求。如果default constructor由程序员显式定义出来了,那么编译器的行动是:
  • 如果class A内含一个或一个以上的member class objects,那么class A的每一个constructor必须调用每一个member classes的default constructor。编译器会扩张已存在的constructors,使得user code被执行之前,先调用的default constructor。
“带有Default Constructor”的Base Class
  • 如果class没有constructors,却派生自有default constructor的base class,那么这个derived class的default constructor会被视为nontrivial。
  • 如果提供多个constructor,但都没有default constructor,编译器会扩张现有的每一个constructors,将用以调用必要default constructors的程序代码加进去。它不会合成一个新的default constructor。
“带有一个Virtual Function”的Class
  • 另外有两种情况,也要合成default constructor:
    1. class声明(或继承)一个virtual function
    2. class派生自一个继承串链,其中有一个或更多的virtual base classes。
      1
      2
      3
      graph BT
      Bell --- Widget
      Whistle --- Widget
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      class Widget {
      public:
      virtual void flip() = 0;
      // ...
      };
      void flip(const Widget &widget) { Widget.flip(); }
      void foo()
      {
      Bell b;
      Whistle w;
      flip(b);
      flip(w);
      }
      两个扩张在编译期间发生了:
    3. 一个virtual function table(vtbl)被产生出来,内含class的virtual functions地址。
    4. 每个class object中,额外的pointer member(vptr)会被合成出来,内含相关的class vtbl地址。
    widget.flip()虚拟调用操作会被重新改写,以使用widget之中的vtpr和vtbl:
    1
    { *widget.vptr[1])(&widget)
“带有一个Virtual Base Class”的Class
  • 看以下代码:
    1
    2
    3
    4
    5
    graph BT
    A --- X
    B --- X
    C --- A
    C --- B
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class X { public: int i; } ;
    class A : public virtual X { public: int j; };
    class B : public virtual X { public: double d; };
    class C : public A, public B { public: int k; };
    // 无法在编译时期决定(resolve)pa->X::i的位置
    void foo(const A *pa) { pa->i = 1024; }
    main()
    {
    foo(new A);
    foo(new C);
    // ...
    }
    因为pa的类型可以改变,编译器无法固定住foo()中经由pa而存取的X::i的实际偏移位置。编译器必须改变“执行存取操作”的那些代码,使X::i可以延迟至执行期才决定下来。cfont做法是在virtual base classes中安插指针完成。经由reference或pointer存取virtual base class的操作都可以通过指针完成。
    1
    void foo(const A *pa) {pa->__vbcX->i = 1024; }
    __vbcX实在class object构造期间被完成的。编译器会安插允许每个virtual base class在执行器存取操作的代码,如果base没有声明任何constructor,编译器必须为它合成一个default的。

2.2 Copy Constructor的构造操作

  • 有三种情况,会以一个obect的内容作为另一个class object的初值。
    • 对object做显式初始化操作
    • 当object作为参数交给某个函数时
    • 函数回传一个class object时
Default Memberwise Initialization
  • 如果class没有提供explicit copy constructor,其内部是以default memberwise initialization手法完成的,就是把每个内建的或派生的data member(如指针或数组)的值,从object拷贝到另一个object上,不过它并不会拷贝其中的member class object。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class String {
    public:
    // ... 没有explicit copy constructor
    private:
    char *str;
    int len;
    };

    // String object的default memberwise initialization发生在这种情况下:
    String noun("book");
    String verb = noun;

    // 完成方式好像个别设定每个members一样
    verb.str = noun.str;
    verb.len = noun.len;
    如果String object被声明为一个class的member:
    1
    2
    3
    4
    5
    6
    7
    class Word {
    public:
    // ... 没有explicit copy constructor
    private:
    int _occurs;
    String _word;
    };s
    那么Word object的default memberwise initialization会拷贝_occurs,然后再于_word身上递归实施memberwise initialization。
  • 一个class可用两种方式复制得到:
    • 一是被初始化。以copy constructor完成。
    • 二是被指定(asignment)。以copy assignment operator完成。
Bitwise Copy Semantics(位逐次拷贝)
  • 一个class没有定义explicit copy constructor,是否有编译器合成实例,取决于class是否展现bitwise copy semantics而定。有两个例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // 声明展现了bitwise copy semantics
    // 这种情况下不需要合成default copy constructor
    class Word {
    public:
    Word(const char*);
    ~Word() { delete []str; }
    // ...
    private:
    int cnt;
    char *str;
    };

    // 以下声明未展现出bitwise copy semantics
    // 这种情况下需要合成出一个copy constructor,以便调用member class String object的copy constructor
    class Word {
    public:
    Word(const String&);
    ~Word();
    // ...
    private:
    int cnt;
    String str;
    };
    // 其中String声明了explicit copy constructor
    class String {
    public:
    String(const char*);
    String(const String&);
    ~String();
    // ...
    };s
不要Bitwise Copy Semantics!
  • 有4中情况下,class不展现出“bitwise copy semantics”
    • 当class内有个member object,而该object声明有copy constructor时
    • 当class继承自一个base class,而该class存在copy constructor时,不论是显式声明的还是被合成的;
    • 当class声明了一个或多个virtual functional时
    • 当class派生自一个继承串联,其中有一个或多个virutal base clases时
重新设定Virtual Table的指针
  • 只要有一个class声明了一个或多个virtual functional,就会:
    • 增加vtbl,内含virtual function的地址
    • 一个指向vtbl的vptr,插在class object内
    当vptr导入到class之中,class就不展示biwise semantics了。一个新产生的class object的vptr不能成功而正确地设好初值会导致可怕的后果,编译器需要合成出一个copy constructor以求将vptr初始化。
    1
    2
    graph TD
    ZooAnimal --- Bear
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class ZooAnimal {
    public:
    ZooAnimal();
    virtual ~ZooAnimal();
    virtual void animate();
    virtual void draw();
    // ...
    private:
    // ...
    };

    class Bear : public ZooAnimal {
    public:
    Bear();
    void animate();
    void draw();
    virtual void dance();
    // ...
    private:
    // ...
    }
    ZooAnimal class object以另一个ZooAnimal class object作为初值,或Bear class object以另一个Bear class object作为初值,都可以直接靠bitwise copy semantics完成(除了member pointer)。
    1
    2
    Bear yogi;
    Bear winnie = yogi;
    这个例子里,yogi会被default Bear constructor初始化。yogi的vptr被指向Bear的vtbl(靠安插)。
    1
    ZooAnimal franny = yogi;    // sliced
    frany的vptr不可以被指向Bear的vtbl(如果yogi的vptr被bitwise copy,会导致此结果),否则当draw()被调用而franny被传进去时,会blow up:
    1
    2
    3
    4
    5
    6
    7
    8
    void draw(const ZooAnimal &zoey) { zoey.draw(); }
    void foo()
    {
    // franny的vptr指向ZooAnimal的vtbl
    ZooAnimal franny = yogi;
    draw(yogi); // call Bear::draw()
    draw(franny); // call ZooAnimal::draw()
    }
    通过franny调用virtual function draw(),调用的时ZooAnimal而非Bear实例。事实上,yogi中的Bear部分在franny初始化时被sliced掉了,只有franny声明为reference或pointer时才会是Bear的函数实例。
处理Virtual Base Class Subobject
  • 编译器必须让derived class object中的virtual base class subobject位置在执行期准备妥当。Bitwise copy semantics可能会破坏位置的完整性,所以编译器必须必须在它自己合成出来的copy constructor中做出仲裁:
    1
    2
    3
    4
    graph BT
    Bear -- public --- ZooAnimal
    Raccoon -- public virtual --- ZooAnimal
    RedPanda -- public --- Raccoon
    1
    2
    3
    4
    5
    6
    7
    8
    class Raccoon : public virtual ZooAnimal {
    public:
    Raccoon() { /*设定private data初值*/ }
    Raccoon(int val) { /*设定private data初值*/ }
    // ...
    private:
    // ...
    };
    一个virtual base class的存在会使bitwise copy semantics无效。问题不在于一个class object以另一个同类的object作为初值之时,而是发生于一个class object以其derived classes的某个object作为初值之时。
    一个Raccoon object作为另一个Raccoon object的初值,bitwise copy绰绰有余,而如果企图以RedPanda object作为little_critter的初值,编译器必须判断当后续企图存取ZooAnimal subobject时是否能正确执行:
    1
    2
    3
    // 简单的bitwise copy还不够,必须显式将little_critter的virtual base class pointer/offset初始化
    RedPanda little_red;
    Raccoon little_critter = little_red;
    在这种情况下,为了完成正确的little_critter初值设定,编译器合成一个copy constructor,安插代码设定virtual base class pointer/offset的初值(或是简单的确定它没被抹消),对每个members执行memberwise初始化操作,一起执行其他内存相关工作。

2.3 程序转化语义学(Program Transformation Semantics)

显式的初始化操作(Explicit Initialization)
  • 已知有定义:

    1
    X x0;

    下面三个定义,每个都以x0来初始化class object:

    1
    2
    3
    4
    5
    6
    7
    void foo_bar() 
    {
    X x1(x0);
    X x2 = x0;
    X x3 = X(x0);
    // ...
    }

    以上必要的程序转化有两个阶段

    • 重写每个定义,其中初始化操作被剥除
    • class的copy constructor调用操作被安插进去

    foo_bar()可能看起来这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    C++ 伪码
    void foo_bar()
    {
    // 定义重写,初始化操作被剥除
    X x1;
    X x2;
    X x3;
    // 编译器安插copy construction的调用操作
    // 表现出X::X(const X &xx);
    x1.X::X(x0);
    x2.X::X(x0);
    x3.X::X(x0);
    // ...
    }
参数的初始化(Argument Initialization)
  • C++ Standard说,把class object当作参数传给函数,或者作为函数的返回值,相当于初始化操作:
    1
    X xx = arg;
    这里xx是形式参数(或返回值),arg是真实参数。因此,函数:
    1
    2
    3
    4
    void foo(X x0);
    X xx;
    // ...
    foo(xx);
    会要求局部实例local instance)x0以memberwise方式将xx当初值。代码转换为:
    1
    2
    3
    4
    5
    6
    // C++伪码
    // 编译器产生的临时对象
    X __temp0;
    // 调用copy constructor
    __temp0.X::X(xx);
    foo(__temp0);
返回值的初始化(Return Value Initialization)
  • 已知下面函数的定义:
    1
    2
    3
    4
    5
    6
    X bar()
    {
    X xx;
    // ...
    return xx;
    }
    bar()的返回值如何从局部对象xx中拷贝过来?在cfont中的做法是一个双阶段转化:
    • 加上额外参数,类型是class object的reference。这个参数用来放置被拷贝构建copy constructed)而来的返回值。
    • return之前安插一个copy constructor调用
    bar()转换如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void bar(X &__result)
    {
    X xx;
    // default constructor
    xx.X::X();
    // ...
    // copy constructor
    __result.X:XX(xx);
    return; // 即使不传回任何值
    }
    1
    2
    3
    4
    X xx = bar();
    // 不用default constructor,NRV
    X xx;
    bar(xx);
    1
    2
    3
    4
    bar().memfunc();
    // 执行bar()所传回的X class object的memfunc()可能转换
    X __temp0;
    (bar(__temp0), __temp0).memfunc();
    1
    2
    3
    4
    5
    6
    // 同理,声明一个函数指针
    X (*pf)();
    pf = bar;
    // 转为
    void (*pf)(X&);
    pf = bar;
在使用者层面做优化(Optimization at the User Level)
  • 以下代码:
    1
    2
    3
    4
    5
    6
    7
    X bar(const T &y, const T &z)
    {
    X xx;
    // ...以y
    来处理xx
    return xx;
    }
    可以这么写:
    1
    2
    3
    4
    X bar(const T &y, const T &z)
    {
    return X(y, z);
    }
    当bar()被转换后,效率会比较高:
    1
    2
    3
    4
    5
    6
    // C++伪码
    void bar(X &__result)
    {
    __result.X::X(y, z);
    return;
    }
在编译器层面做优化(Optimization at the Compiler Level)
  • 在一个像bar()这样的函数中,所有return指令传回相同具名数值(named value),因此编译器可能自己做优化
    1
    2
    3
    4
    5
    6
    X bar()
    {
    X xx;
    // ...
    return xx;
    }
    xx以_result取代:
    1
    2
    3
    4
    5
    6
    7
    void bar(X &__result)
    {
    // default constructor被调用
    __result.X::X();
    // ... 直接处理__result
    return;
    }
    这样的优化操作,称为Named Return ValueNRV)。虽然NRV优化提供了重要效率改善,但它还是饱受批评。
    • 优化是默默的,不透明
    • 一旦函数复杂,很难优化
    • 某些人不喜欢
    举个例子,以下三个初始化语义上相等:
    1
    2
    3
    X xx0(1024);    // xx0.X::X(1024);
    X xx1 = X(1024);
    X xx2 = (X)1024;
    xx0是被单一的constructor操作设定初值:
    1
    xx0.X::X(1024);
    而xx1或xx2却调用两个constructor,产生临时性object,并针对临时object调用classX的destructor:
    1
    2
    3
    4
    X __temp0;
    __temp0.X::X(1024);
    xx1.X::X(__temp0);
    __temp0.X::~X();
Copy Constructor:要还是不要?
  • 一个3D坐标点类:
    1
    2
    3
    4
    5
    6
    7
    class Point3d {
    public:
    Point3d(float x, float y, float z);
    // ...
    private:
    float _x, _y, _z;
    };
    class的default copy constructor被视为trivial。默认情况下,一个Point3d class object的“memberwise”初始化操作会导致“bitwise copy”。这样效率高,也安全。
    实现copy constructor的最简单方法像这样:
    1
    2
    3
    4
    5
    6
    Point3d::Point3d(const Point3d &rhs)
    {
    _x = rhs._x;
    _y = rhs._y;
    _z = rhs._z;
    };
    但使用C++library的memcpy()会更有效率:
    1
    2
    3
    4
    Point3d::Point3d(const Point3d &rhs)
    {
    memcpy(this, &rhs, sizeof(Point3d));
    };

2.4 成员们的初始化队伍(Member Initialization List)

  • 当你写下一个constructor时,就有机会设定class members的初值,不是在member initialization list,就是在constructor函数本体。
  • 下列情况下,为了让你程序能够顺利执行,必须使用member initialization list:
    1. 当初始化reference member时
    2. 当初始化const member时
    3. 当调用一个base class的constructor,且拥有一组参数时
    4. 当调用一个member class的constructor,而 它拥有一组参数时
      这四种情况都能正确编译并执行,但效率不高。例如:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      class Word {
      String _name;
      int _cnt;
      public:
      Word() {
      _name = 0;
      _cnt = 0;
      }
      };
      constructor可能的内部扩张结果:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      Word::Word( /* this pointer goes here */ )
      {
      // String default constructor
      _name.String::String();
      // tempory
      String temp = String(0);
      // memberwise copy _name
      _name.String::operator=(temp);
      // destroy tempory
      temp.String::~String();
      _cnt = 0;
      }
      一个有效率的实现方法是:
      1
      2
      3
      4
      Word::Word : _name(0)
      {
      _cnt = 0;
      }
      它会扩张:
      1
      2
      3
      4
      5
      6
      Word::Word( /* this pointer goes here */ )
      {
      // String(int) constructor
      _name.String::String(0);
      _cnt = 0;
      }

  • C语言中,数据和函数是分开来声明的,也就是说,语言本身没有支持”数据和函数“之间的关联性,这种程序方法称为程序性的procedural)。一般从C
    struct转到C++的封装类后,唯一在布局以及存取时间上的负担是virtual引起的。

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

  • C++中有两种class data members:static和nonstatic,以及三种class member functions:static、nonstatic和virtual。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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;
    };
简单对象模型(A Simple Object Model)
1
2
graph LR
slot[Point pt] ==> m[all members]
  • 这个模型简单,一个object是一系列的slots,members按照声明次序各被指定一个slot。一个class object的大小是”指针大小*members个数“。
    • 这个模型应用到C++的”指向成员的指针“(pointer-to-member)观念中。
表格驱动对象模型(A Table-driven Object Model)
1
2
3
4
5
graph LR
S[Point pt] == data member table ==> e[内含实际数据]
S == member function table ==> m[内含函数地址]
m ==> a[...]
m ==> b[...]
  • 这种模型把所有与members相关的信息抽出来,放在一个data member table和一个member function table之中,class object则包含指向这两个表格的指针。member function table是一系列slots,一个slot一个member function;data member table则直接含有data本身。
  • 这个模型member function table这个观念称为支持virtual functions的一个有效方案
C++对象模型(The C++ Object Model)
1
2
3
4
5
6
7
8
graph LR
e1(static data members)
e2(static function members)
e3(nonstatic function members)
Point[nonstatic data members] == Virtual table for Point ==> vptr[vtbl]
vptr == RTTI ==> ti[type_info for Point]
vptr ==> a[...]
vptr ==> b[...]
  • nonstatic data members被配置于每个class object之内,static data members则被存放在class object之外;
  • static和nonstatic function members也放在所有class object之外;
  • virtual functions以两个步骤支持它:
    • 每个class产生一堆指向virtual functions的指针,放在virtual tablevtbl)中;
    • 每个class object被添加一个指针,指向vtbl,通常这个指针被称为vptr。vptr的设定(setting)和重置(resetting)由每个class的constructor、destructor和copy assignment运算符自动完成。每个class所关联的type_info object(用以支持runtime type identificationRTTI)也经由vtbl指出,通常放在表格第一个slot处。
  • 这个模型优点在于空间和存取时间的效率;缺点则是如果所用到的class objects的nonstatic data members有所改动,那么那些应用程序得重新编译。
加上继承(Adding Inheritance)
  • C++支持单一继承多继承虚拟继承
    1
    2
    3
    4
    5
    graph BT
    ostream --- ios
    istream --- ios
    iostream --- ostream
    iostream --- istream
对象模型如何影响程序
  • 下面这个函数,其中class X定义一个coyp constructor,一个virtual destructor和一个virtual function foo:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    X foobar()
    {
    X xx;
    X *px = new X;
    // foo() is a virtual function
    xx.foo();
    px->foo();
    delete px;
    return xx;
    }
    // 这个函数可能在内部转化为:
    // px->_vtbl[0] --> type_info object
    // px->_vtbl[1] --> X::~X()
    // px->_vtbl[2] --> X::foo()
    void foobar(X &_result)
    {
    _result.X::X();
    // X *px = new X;
    px = _new(sizeof(x));
    if (px != 0)
    px->X::X();
    // xx.foo(),不适用virtual机制
    foo(&_result);
    // virtaul机制扩展px->foo()
    (*px->vtbl[2])(px)
    // delete px
    if (px != 0)
    {
    (*px->vtbl[1])(px); // destructor
    _delete(px);
    }
    // 无须使用named return
    statement
    // 无须摧毁local object xx
    return;
    }

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

关键词的困扰
  • 关键词struct的意思是一个数据集合体,没有private data,也没有data的相应操作。它和C++的使用者自定义类型user-defined type)区分开来。struct关键词的使用实现了C的数据抽象观念,而class关键词实现的是C++的ADTAbstract Data Type)观念。
策略性正确的struct(The Politically Correct Struct)
  • C的巧计有时候是C++的陷阱。例如一个在struct尾端的单一数组,也是每个struct objects有可变长数据:
    1
    2
    3
    4
    5
    6
    7
    struct mutable {
    /* stuff */
    char pc[1];
    };
    // 为struct本身和该字符串配置足够的内存
    struct mumble *pmumb1 = (struct memble*)malloc(sizeof(struct mumble) + strlen(string) + 1);
    strcpy(&mumble.pc, string);
    让C++ class的部分数据拥有C声明那模样,是将那部分抽取出来成为独立的struct声明,然后组合在一起,它能保证拥有与C兼容的空间布局:
    1
    2
    3
    4
    5
    6
    7
    struct C_Point { ... };
    class Point {
    public:
    operator C_point() { return _c_point; }
    private:
    C_point _c_point;
    };

1.3 对象的差异(An Object Distinction)

  • C++有以下方法支持多态
    • 隐式转化操作。(Shape *ps = new Circle()
    • 经由virtual function机制。(ps->rotate();
    • 经由dynamic_cast和typeid操作符。(if(Circle \*pc = dynamic\_cast<Circle\*>(ps))
  • 需要多少内存才能够表现一个class object大小
    • nonstatic data members的总和大小
    • 根据alignment需求padding的空间。(alignment是将数值调整到某数的倍数。32位机器上位4 bytes,64位机器上位8bytes)。
    • 支持virtual而内部产生的额外负担(overhead)
指针的类型(The Type of a Pointer)
  • 一个指向ZooAnimal的指针与一个指向整数的指针或template Array的指针在内存上没什么区别。真正的不同是其所寻址出来的object类型不同,因为指针类型会教导编译器如何解释某个地址中的内存内容及其大小。所以,这就是为什么void*能持有一个地址,但不同操作该地址的原因了。而转换cast)也是一种编译器指令,它不改变地址,只影响内存大小和内容的解释方式
加上多态之后(Adding Polymorphism)
  • 定义一个Bear,作为一种ZooAnimal:
    1
    2
    graph TD
    ZooAnimal --- Bear
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Bear : public ZooAnimal {
    public:
    Bear();
    ~Bear();
    // ...
    void Rotate();
    virtual void dance();
    // ...
    protected:
    enum Dances { ... };
    Dances dances_known;
    int cell_block;
    };

    Bear b;
    ZooAnimal *pz = &b;
    Bear *pb = &b;
    两个对象都同指Bear object的第一个byte。区别是pb的地址包含Bear object,而pz的地址包含Bear Object中的ZooAnimal subobject。除非ZooAnimal subobject出现的members,否则不能使用pz直接处理Bear的members,唯一例外是virtual机制:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // error. cell_block不是ZooAnimal的member
    pz->cell_block;
    // ok
    (static_cast<Bear*>(pz))->cell_block;
    // better
    if (Bear *pb2 = dynamic_cast<Bear*>(pz))
    pb2->cell_block;
    // ok
    pb->cell_block
    当写下pz->rotate();时,pz的类型在编译期决定了两点:
    • 固定的可用接口。就是说,pz只能调用ZooAnimal的public接口
    • 该接口的access level(例如ratate()时ZooAnimal的public member)。
      每一个执行点,pz指的object类型决定rotate()调用的实例。类型信息的封装维护于object vptr和vtbl之间的link之中。
  • 一个pointer或一个reference之所以支持多态,是因为它们不引发内存中任何于类型有关的内存委托操作(type-dependent commitment),会受到改变的只有指向内存的大小和内容解释方式而已。当一个base class object被直接初始化(或被指定为)一个derived class object时,derived object就会被切割sliced)以塞入较小的base type内存中,derived type将没有留下任何蛛丝马迹,因为配置所得的内存有限。
  • C++支持具体的ADT程序风格,被称为object-basedOB)。一个OB设计可能比一个对等oo设计速度更快而且空间更紧凑,因为所有函数调用操作在编译时期解析完成,对象构建起来不再设置virtual机制,而且空间紧凑,缺点是OB设计比较没有弹性。

使用Dockerfile创建镜像

  • Dockerfile分为四部分
    • 基础镜像信息
    • 维护者信息
    • 镜像操作指令
    • 容器启动时执行指令

detail

指令 说明
FROM 指定基础镜像
MAINTAINER 维护者信息
RUN 运行命令
CMD 启动容器时默认执行命令,最多只能执行一个命令
LABEL 元数据标签信息
EXPOSE 监听端口
ENV 环境变量
ADD 复制<src>到容器<dest>,如果是tar自动解压,src可以是URL
COPY 复制本地<src>到容器<dest>
ENTRYPOINT 指定镜像默认入口,最多只有一个,可以被docker run --entrypoint覆盖掉
VOLUME 创建数据卷挂载点
USER 指定运行容器时的USER或UID
WORKDIR RUNCMDENTRYPOINT指令配置工作目录
ARG 指定一些镜像内使用的参数
ONBUILD 当创建的镜像为其他镜像的基础镜像时执行的操作命令
STOPSIGNAL 指定创建镜像启动的容器接收退出的信号值
HEALTHCHECK 配置启动容器如何进行健康检查
  • 使用.dockerignore文件可以让Docker忽略匹配路径下的目录和文件
    1
    2
    3
    4
    5
    # comment
    */temp*
    */*/temp*
    tmp?
    ~*

desc

指令 格式
FROM <image>:<tag> || <image>@<digest>
MAINTAINER <name>
RUN shell:<command> || exec:["executable", "param1", "param2"]
CMD ["executable", "param1", "param2"] || command param1 param2 || [“param1”, “param2”]
LABEL <key>=<value>[<key>=<value>...]
EXPOSE <port>[<port>...]
ENV <key> <value> || <key>=<value>[<key>=<value>...]
ADD <src> <dest>
COPY <src> <dest>
ENTRYPOINT shell:command param1 param2 || exec: ["executable", "param1", "param2"]
VOLUME ["/data"]
USER daemon
WORKDIR /path/to/workdir,如果是相对路径,路径是基于前个WORKDIR

exampe

1
2
3
4
5
6
7
8
FROM debian:jessie
MAINTAINER NGINX Docker Maintainers "docker-maint@nginx.com"
ENV NGINX_VERSION 1.10.1-1-jessie
RUN apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys ....
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
$$ ln -sf /dev/stderr /var/log/nginx/error.log
EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;"]

sys

如果使用alpine镜像

安装软件包需要用apk包管理器替换apt工具

1
$ apk add --no-cache <package>

可以从alpine搜索并确定安装包名称
如果需要指定社区的安装包,则:

1
2
$ echo "http://dl-4.alpinelinux.org/alpine/edge/teseting" >> /etc/apk/repositories
$ apk --update add --no-cache <package>

数据卷

使用Docker过程中,需要对数据进行持久化,或在多个容器间共享数据,需要容器的数据管理操作

  • 数据管理主要有两种方式:
    • 数据卷(Data Volumes):容器数据直接映射到本地主机环境
    • 数据卷容器(Data Volume Containers):使用特定容器维护数据卷

数据卷

1
2
3
4
5
 # e.g.: training/webapp镜像创建web容器,并创建数据卷挂载到容器的/webapp目录
# 创建数据卷,-P是映射本地临时端口
# 挂在主机目录为数据卷: from/path:to/path
# 指定数据卷读写权限: ro/rw
$ docker run -d -P --name web -v /src/webapp:/opt/webapp:ro training/webapp python app.py

数据卷容器

创建数据卷容器dbdata,创建数据卷挂载到容器dbdata

1
$ docker run -it -v /dbdata --name dbdata ubuntu

其他容器对其进行挂载,之后三方任何一方修改各方都可见

1
2
$ docker run -it --volumes-from dbdata --name db1 ubuntu
$ docker run -it --volumes-from dbdata --name db2 ubuntu

删除最后挂载容器需要使用docker rm -v同时删除关联容器


使用数据卷容器迁移数据

备份

1
$ docker run --volumes-from dbdata -v $(pwd):/backup --name worker ubuntu tar cvf /backup/backup.tar /dbdata

恢复

首先创建容器dbdata2

1
$ docker run -v /dbupdate --name dbdata2 ubuntu ubuntu /bin/bash

新容器挂载dbdata2,然后untar解压备份文件到所挂载的容器卷中

1
$ docker run --volumes-from dbdata2 -v $(pwd):/backup busybox tar xvf /backup/backup.tar

端口映射和容器互联

端口映射

1
2
3
4
# 可以-p映射多端口-p po -p po
# 可以指定ip:po:po
# 还可以本地随机端口ip::po
# 还可以指定udp端口ip:po:po/udp

容器互联

创建新容器

1
$ docker run -d --name db training/postgres

创建新容器连接到db容器

1
2
 # --link name:alias
$ docker run -d -P --name web --link db:db traning/webapp

修改镜像源

(manjaro)只需要添加配置文件/etc/docker/daemon.json

1
2
3
{
"registry-mirrors": ["https://docker.mirrors.ustc.edu.cn"]
}

或者使用–registry-mirror选项


一些Docker基础命令

容器操作

run: 通过运行镜像输出hello

docker run CONTAINER [COMMAND] [ARGS]

1
2
# 相当于先create再start
# e.g. docker run CONTAINER echo 'hello'

启动交互式容器

1
2
3
$ docker run -it CONTAINER /bin/bash
# e.g.:指定ip:host进行后台运行
$ docker run -it -d -p 127.0.0.1:12306:12306 --name postgres ichheitimg:9.1-deb /bin/bash

带名字的运行镜像

1
$ docker run --name='name' CONTAINER echo 'hello'

守护形式运行容器

1
2
$ docker run -it CONTAINER /bin/bash
# C^P C^Q退出,可docker ps查看守护容器

可直接启动后台

配置选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 运行相关选项
# -D, --debug=false
# -e, --exec-driver="native"
# -g, --graph="/var/lib/docker"
# --icc=true
# -l, --log-level="info"
# --label=[]
# -p, --pidfile="/var/run/docker.pid"
#
$ docker run -d CONTAINER [COMMAND] [ARG...]
# 服务器相关选项
# -G, --group="docker"
# -H, --host=[]
# --tls=false
# --tlscacert="/home/sven/.docker/ca.pem"
# --tlscert="/home/sven/.docker/ca.pem"
# --tlskey="/home/sven/.docker/ca.pem"
# --tlsverify=false
# Remote API存储相关
# -s, --storage-driver=""
# --selinux-enabled=false
# --storage-opt=[]
# Registry相关
# --insecure-registry=[]
# --registry-mirror=p[]
# 网络设置相关
# -b, --bridge=""
# --bip=""
# --fixed-cidr=""
# --fixed-cidr-v6=""
# --dns=[]
# --dns-search=[]
# --ip=0
# --ip-forward=true
# --ip-masq=true
# --iptables=true
# --ipv6=false
# --mtu=0

设置容器的端口映射

1
2
3
4
5
6
7
8
9
 # -p, --publish
# containerPort
$ docker run -p 80 -it CONTAINER /bin/bash
# hostPort:containerPort
$ docker run -p 8080:80 -it CONTAINER /bin/bash
# ip::containerPort
$ docker run -p 0.0.0.0:80 -it CONTAINER /bin/bash
# ip:hostPort:containerPort
$ docker run -p 0.0.0.0:8080:80 -it CONTAINER /bin/bash

create: 创建容器

1
2
# e.g.: 新建新容器,如果没有指定镜像,会自动pull镜像
$ docker create -it ubuntu:latest

start: 启动容器

1
2
# e.g.: 启动创建好的容器
$ docker start XXXXXXX

ps: 查看容器

docker ps [OPT]

1
2
# -a 全部记录
# -l 最新记录

rm: 删除容器

docker rm '{IMAGE}'

attach: 继续运行后台守护容器

docker attach '{IMAGE}'
dokcer attach [--detach-keys[=[]]] [--no-stdin][--sig-proxy[=true]] CONTAINER

1
2
3
4
5
6
7
8
```

### <font color="red">logs</font>: 查看容器日志
> `docker logs [OPT] '{IMAGE}'`
```zsh
# -f --follows=true|false 默认false
# -t --timestamps=true|false 默认false
# --tail="all"

exec: 在已运行的容器中启动新进程

docker exec [OPT] CONTAINER [COMMAND] {ARGS}

1
2
3
# -d
# -i
# -t

stop/kill: 停止容器

docker stop [-t | --time[=10]] CONTAINER
docker kill CONTAINER


镜像操作

images/inspect: 查看镜像

docker iamges [OPT] [REPOSITORY]
docker inspect [OPT] CONTAINER|IMAGE [...]

1
2
3
4
# -a, all=false
# -f, --filter=[]
# --no-trunc=false
# -q, --quiet=false

search: 查找镜像

docker search [OPT] TERM

1
2
3
4
# --automated=false
# --no-trunc=false
# -s, --stars=0
# --filter=stars=3

pull: 拉取镜像

docker pull [OPT] NAME [:TAG]

1
# -a, --all-ags=false

构建镜像

commit: 通过容器

docker commit [OPT] CONTAINER [REPOSITORY[:TAG]]

1
2
3
4
5
 # -a,--author=""
# -m,--message=""
# -p,--pause=true, 提交时暂停容器运行
# e.g.:提交本地deb容器
$ docker commit deb ichheitimg:9.1-deb

import: 通过本地模板导入

docker import [OPT] file |URL|[REPOSITORY[:TAG]]

1
2
# e.g.:导入
$ cat ubuntu-14.04-x86_64-minimal.tar.gz | docker import - ubuntu:14.04

build: 通过Dockerfile文件

docker build

通过Dockerfile

1
2
3
4
5
6
7
8
 # --force-rm=false
# --no-cache=false
# --pull=false
# -q, --quiet=false
# --rm=true
# -t, --tag=""
$ docker build [OPT] PATH | URL | -
# e.g. 在创建的目录中编写Dockerfile文件,然后使用$ docker build -t='name' .
Dockerfile格式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# First Dockerfile
FROM [CONTAINER]
MAINTAINER [MESSAGE]
[COMMAND...]
EXPOSE 80
CMD ["executable", "param1", "param2"] (exec mode)
CMD command param1 param2 (shell mode)
CMD ["param1", "param2"] (ENTRYPOINT指令搭配下的参数)
ENTRYPOINT ["executable", "param1", "param2"] (exec mode)
ENTRYPOINT command param1 param2 (shell mode)
# CMD与ENTRYPOINT的区别是后者命令不会被构建时命令所覆盖,也可以使用--entrypoint覆盖
ADD <src> .. <dest>
COPY <src> ... <dest>
VOLUMN ["/data"]
(共享数据等作用,使用时在build构建前应使用--volumes-from选项挂在数据卷容器)
WORKDIR /path/to/workdir (指定工作目录)
ENV <key> <value>
ENV <key>=<value> ...
USER user (e.g. | USER user | USER user:group | USER user:gid |)
ONBUILD [INSTRUCTION] (添加触发器)
Dockerfile构建过程
  • 从基础镜像运行容器
  • 执行指令,对容器做修改
  • 执行类docker commit操作,提交新镜像层
  • 基于新镜像运行新容器
  • 执行Dockerfile的下一指令直到所有指令执行完毕

save: 存入镜像

docker save

1
2
# e.g.: 导出本地的ubuntu:14.04为文件ubuntu_14.014.tar
$ docker save -o ubuntu_14.04.tar ubuntu:14:04

load: 载入镜像

docker load

1
2
3
4
# e.g.: 导出tar文件到本地镜像库
$ docker load --input ubuntu_14.04.tar
# 或
$ docker load < ubuntu_14.04.tar

push: 上传镜像

docker push NAME[: TAG] |REGISTRY_HOST[: REGISTRY_PORT]/NAME[: TAG]

1
2
3
# e.g.: 用户user上传test:latest镜像到user/test:latest
$ docker tag test:latest user/test:latest
$ docker push user/test:latest

rmi: 删除镜像

docker rmi [OPT] IMAGE [...]

1
2
# -f, --force=false
# --no-prune=false

Docker的C/S模式:Remote API

连接方式

  • unix:///var/run/docker.dock
  • tcp://host:port
  • fd://socketfd

Docker的远程访问

1
2
3
4
# 默认配置-H unix:///var/run/docker.sock
# e.g.: docker -H tcp://ip:port COMMAND
# 可选择的环境变量DOCKER_HOST
# e.g.: export DOCKER_HOST="tcp://ip:port"

Docker容器互联

1
2
3
4
5
6
7
8
 # --icc=true,允许容器间互联
# 每次镜像重启都可能导致IP的变更,因此使用以下方式
# --link
$ docker run --link=[CONTAINER]:[ALIAS] [IMAGE][COMMAND]
# e.g. docker run -it --name test --link=other:alias img:test
# 只允许特定容器间连接
$ --icc=false --iptables=true
$ --link

Docker容器的数据卷

为容器添加数据卷

1
2
3
 # -v local_catalogue:docker_catalogue:[ro]
# e.g.
$ sudo dcoker run -v ~/container_data:/data -it ubuntu /bin/bash

在Dockerfile中使用COLUMN下指定数据卷容器

1
2
3
4
5
6
# e.g.:
# 编写Dockefile,指定VOLUME目录
$ docker build -t dorman/dvt .
$ docker run -it --name test dorman/dvt
# 以下--volumes-from将共享Dockfile中的目录
$ docker run -it --name test1 --volumes-from test ubuntu /bin/bash

Docker数据卷的备份和还原

1
2
$ docker run --volumes-from [container name] -v local_catalogue:docker_catalogue ubuntu
$ tar cvf /backup/backup.tar [container data volume]

Docker容器的跨主机连接

网桥实现

  • 通过在主机和虚拟机之间使用同一个网段来跨主机连接
  • 需要网络配置并且配置Docker网桥,并通过–fixed-cidr指定网络范围
  • (/etc/network/interfaces)

Open vSwitch实现

环境准备
  • 双网卡,Host-Only & NAT
  • 安装Open vSwitch和网桥管理工具bridge-utils
  • 同一网段的IP地址
操作
  • 建立ovs网桥
  • 添加gre连接
  • 配置docker容器虚拟网桥
  • 为虚拟网桥添加ovs接口
  • 添加不同DOcker容器网段路由

weave实现

PostgreSQL官网

安装依赖:

1
2
3
4
5
# centos
$ yum -y install readline-devel zlib-devel gcc gcc-c++ zlib readline
# debian/ubuntu
$ apt-get install -y gcc make
$ apt-get install -y libreadline-dev zlib1g zlib1g.dev

安装PostgreSQL:

postgresql-10.3(maybe too
old)

1
2
3
4
5
6
7
8
9
10
11
$ wget https://sandbox-experiment-resource-north-4.obs.cn-north-4.myhuaweicloud.com/postgresql-arm/postgresql-10.3.tar.gz
$ tar -zxvf postgresql-10.3.tar.gz -C /opt/
$ cd /opt/postgresql-*/
$ ./configure --build=arm-linux --host=arm-linux --prefix=/usr/local/pgsql
# canuse:./configure --prefix=/usr/local/pgsql --with-blocksize=8 \
# --enable-dtrace --enable-debug --enable-thread-safety
# --with-libxml --with-python --with-openssl
# $ make world
# $ make check-world (by normal user)
# $ make install-world
$ make && make install && make clean

授权用户:

1
2
3
$ adduser postgres
$ chown -R postgres:postgres /usr/local/pgsql
$ ls -ld /usr/local/pgsql

配置环境变量:

1
2
$ su postgres
$ vim ~/.bash_profile

配置文件中加入:

1
2
3
$ export PGHOME=/usr/local/pgsql
$ export PGDATA=/usr/local/pgsql/data
$ export PATH=$PATH:$PGHOME/bin

执行修改:

1
$ source  ~/.bash_profile && psql -V

初始化数据库:

1
$ initdb -D $PGDATA -E UTF8 --locale=C -U postgres -W

启动日志服务:

1
2
$ mkdir -p /usr/local/pgsql/log
$ chmod 755 /usr/local/pgsql/log

启动数据库:

1
$ pg_ctl -D $PGDATA  -l  $PGHOME/log/pg_server.log start

检查结果:

1
2
$ ps -ef | grep "postgres"
$ netstat -nlp | grep "postgres"

修改数据库密码:

1
$ psql中`\password postgres`可修改

配置为远程访问:

将”listen_addresses=’localhost’”取消注释并修改为任意地址’*’

1
$ vim /usr/local/pgsql/data/postgresql.conf

修改访问地址控制设置:

1
$ vim /usr/local/pgsql/data/pg_hba.conf

在”IPv4 local connections”下构建规则:

1
host    all             all             0.0.0.0/0               md5

重启生效:

1
$ pg_ctl restart

在debian中

基本安装

创建文件/etc/apt/sources.list.d/pgdg.list,里面添加:

1
2
# 其中stretch-pgdg具体可以在https://www.postgresql.org/download/linux/debian/中选择具体版本得到
deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main

Import the repository signing key, and update the package lists:

1
2
3
4
5
6
7
8
9
$ apt-get install -y gnupg gnupg1 gnupg2
$ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
$ apt-get update
$ apt-get install -y postgresql-10

$ useradd -m -s /bin/bash postgres
$ mkdir /usr/local/pgsql
$ chown postgres:postgres /usr/local/pgsql
$ sudo -u postgres /usr/lib/postgressql

数据库初始化:

1
$ /usr/lib/postgresql/11/bin/initdb -D $PGDATA -E UTF8 --locale=C -U postgres -W

vim ~/.bash_profile配置:

1
2
3
4
export PGHOME=/usr/local/pgsql
export PGDATA=/usr/local/pgsql/data
export PATH=$PATH:/usr/lib/postgresql/11/bin
export LANG=en_US.utf8

数据库日志:

1
2
3
# 大可不必
$ mkdir -p /usr/local/pgsql/log
$ chmod 755 /usr/local/pgsql/log
  • can start data server
    1
    2
    3
    # 一般启动
    $ /usr/lib/postgresql/11/bin/pg_ctl -D /usr/local/pgsql/data -l logfile start
    $ pg_ctl -D $PGDATA -l $PGHOME/log/pg_server.log start

    通过配置文件

    启动,vim /etc/sysctl.conf中配置内核相关参数:

    1
    2
    3
    4
    5
    6
    7
    8
    kernel.shmmni = 4096
    kernel.sem = 50100 64128000 50100 1280
    fs.file-max = 7672460
    net.ipv4.ip_local_port_range = 9000 65000
    net.core.rmem_default = 1048576
    net.core.rmem_max = 4194304
    net.core.wmem_default = 262144
    net.core.wmen_max = 1028576

    vim /etc/sysconfig/iptables进行防火墙设置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    -A RH-Firewall-1-INPUT -i lo -j ACCEPT
    # 允许源IP
    -A RH-Firewall-1-INPUT -s 192.168.0.0/16 -j ACCEPT
    # 允许源IP访问目标端口
    -A RH-Firewall-1-INPUT -s 192.168.1.0/24 -m state --state
    NEW -m tcp -p tcp --dport [PORT] -j ACCEPT
    # 允许任意IP访问目标端口
    -A RH-Firewall-1-INPUT -p tcp -m state --state NEW -m
    tcp --dport [PORT] -j ACCEPT
    # 不是很懂
    # 使用iptables -L -v -n查看出入规则
  • 在/etc/security/limits.conf配置。。。

    vim /usr/local/pgsql/data/pg_hba.conf中的IPv4 local
    connections修改访问地址控制规则设置:

    1
    host    all             all             0.0.0.0/0               md5

    vim /usr/local/pgsql/data/postgresql.conf

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    将listen_addresses='localhost'取消注释并修改为任意地址'*0.0.0.0'
    superuser_reserved_connections超级用户最多连接改为13
    unix_socket_directories连接改为'.'根目录下
    unix_socket_permissions权限为0700
    # 心跳检测
    tcp_keepalives_idle = 60
    tcp_keepalives_interval = 10
    tcp_keepalives_count = 10
    # 延迟
    vacuum_cost_delay = 10
    bgwritter_delay = 10ms
    wal_writer_delay = 200ms
    # 归档
    hot_standby = on
    # 日志
    log_destination = "csvlog"
    logging_collector = on
    log_directory = 'pg_log'
    log_file_mode = 0600
    log_truncate_on_rotation = on等
    # 还有checkpoint
    log_checkpoints = on
    log_connection = on
    log_disconnections = on
    log_error_verbosity = verbose

    启动psql

    1
    2
    $ pg_ctl -D $PGDATA -l $PGHOME/log/pg_server.log start
    $ psql -h localhost -U postgres

1.属性定义

Qt提供了Q_PROPERTY()宏来定义属性,它是基于元对象系统来实现的。作为平台与编译器无关的库,Qt C++程序可以被任何标注你的C++编译器编译。
在QObject的子类中,用宏Q_PROPERTY()定义属性,格式是:

1
2
3
4
5
6
7
8
9
10
11
12
Q_PROPERTY(type name
(READ getFunction [WRITE setFunction] |
MEMBER memberName [(READ getFunction | WRITE setFunction)])
[RESET resetFunction]
[NOTIFY notifySignal]
[REVISION int]
[DESIGNABLE bool]
[SCRIPTABLE bool]
[STORED bool]
[USER bool]
[CONSTANT]
[FINAL])

Q_PROPERTY宏定义一个返回值类型为type,名称为name的属性,用READWRITE来作为属性的读/写操作,还有其他的一些关键字定义属性的一些操作特性。属性的类型可以是QVariant支持的任何类型,也可以用户自定义类型。
Q_PROPERTY宏定义属性的一些主要关键字的意义如下:

  • READ指定一个读取属性值的函数,没有MEMBER关键字时必须设置READ
  • WRITE指定一个设定属性值的函数,只读属性没有WRITE设置。
  • MEMBER指定一个成员变量与属性关联,成为可读可写的属性,无需再设置READWRITE
  • RESET是可选的,用于指定一个设置属性缺省值的函数。
  • NOTIFY是可选的,用于设置一个信号,当属性值变化时发射此信号。
  • DESIGNABLE表示属性是否在Qt Designer里可见,缺省为true
  • CONSTANT表示属性值是一个常数,对于一个对象实例,READ指定的函数返回值是常数,但是每个实例的返回值可以不一样。具有CONSTANT关键字的属性不能有WRITENOTIFY关键字。
  • FINAL表示所定义的属性不能被子类重载。

QWidget 类定义属性的一些例子如下:

1
2
3
4
Q_PROPERTY(bool focus READ hasFocus)
Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled)
Q_PROPERTY(QCursor cursor READ cursor WRITE setCursor RESET unsetCursor)
Q_PROPERTY(QString windowTitle READ windowTitle WRITE setWindowTitle NOTIFY windowTitleChanged DESIGNABLE isWindow)

上面最后这个声明包含了NOTIFY windowTitleChanged,表明当windowTitle属性发生变化时,会触发windowTitleChanged信号,相应的信号声明在QWidget中。(this->setProperty("windowTitle", "test"))


关于如何使用NTOIFY和RESET——Qt爱好者


2.动态属性

其中,不管有没有用READWRITE定义接口函数,只要知道属性名称,就可以通过QObject::property()QObject::setProperty()读取和设置属性值。e.x.:

1
2
3
4
QPushButton *button = new QPushButton;
QObject *object = button;
object->setProperty("flat", true);
bool isFlat = object->property("flat");

这就是动态属性,运行时通过setProperty()定义新属性,并用property()查询,它是针对类实例定义的。
在数据表编辑界面上,为了高亮显示一些必填字段,可以在初始化界面为这些字段关联组件定义新的required属性:

1
2
3
4
5
editName->setProperty("required", "true");
comboSex->setProperty("required", "true");
checkAgree->setProperty("required", "true");
// 应用样式定义将必填字段背景颜色设置为亮绿色
*[required="true"] {background-color: lime}

3.类的附加属性

属性系统还有一个宏Q_CLASSINFO(),可以为类的元对象定义“名称——值”信息,如:

1
2
3
4
5
6
7
8
9
class QMyClass:public QObject 
{
Q_OBJECT
Q_CLASSINFO("author", "Wang")
Q_CLASSINFO ("company", "UPC")
Q_CLASSINFO("version ", "3.0.1")
public:
...
};

Q_CLASSINFO()宏定义附加类信息后,可以通过元对象的一些函数获取类的附加信息,如classlnfo(int)获取某个加信息,函数原型定义如下:

1
QMetaClassInfo QMetaObject::classInfo(int index) const

返回值是QMetaClassInfo类型,有name()value()两个函数,可获得类附加信息的名称和值。

1
2
3
4
// 设置附加值
Q_CLASSINFO("First", "Second")
// 输出键值
qDebug() << this->metaObject()->classInfo(0).value() << ": " << this->metaObject()->classInfo(0).name();

信号和槽是Qt的一个核心特点,也是区别于其他框架的重要特性。因为有了信号和槽的编程机制,Qt中处理界面各个组件的交互操作时变得更加直观和简单。信号与槽是对象间通信的机制,由Qt的元对象系统的支持才能实现。
信号(Signal)会在某种特定情况或动作下被触发。就是特殊情况下被发射的事件,而GUI程序设计的主要内容就是对界面上各组件信号的响应。
槽(Slot)则等同于接收并处理信号的函数。它跟一般的C++函数是一样的,可以直接被调用。但槽函数与普通函数不同的是,槽函数与一个信号关联,当信号被发射时,关联的槽函数会自动执行。
对于这种对象间通信,Qt隐藏了复杂的底层实现。Qt的信号与槽机制类似于“事件——响应”,但更灵活。


1.connect函数的不同参数形式

  • QObject::connect()函数多重函数形式,一种参数原型是:
    1
    2
    3
    4
    5
    6
    7

    sender是发送信号的对象名称
    signal()是信号名称,根据SIGNAL宏展开成相应字符串
    receiver是接受信号的对象名称
    slot()是槽函数名称,根据SLOT宏展开成字符串
    ×/
    QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
    一般语法如下:
    1
    2
    3
    4
    5
    // connect()是QObject类中的一个静态函数,QObject又是所有Qt类的基类,实际调用可以忽略前面的限定符
    // 宏SIGNAL()指信号,SLOT()指槽函数
    QObject::connection(sender, SIGNAL(signal()), receiver, SLOT(slot()));
    // 如果带有参数,标明参数类型
    QObject::connection(spinNum, SIGNAL(valueChanged(int)), this, SLOT(updateStatus(int));

  • 另一种参数形式的connect()函数原型是:
    1
    QMetaObject::Connection QObject::connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod, &method, Qt::ConnectionType type = Qt::Auto Connection)
    这种具有默认参数,信号名称是唯一的,可以使用这种函数指针形式进行关联,语法如下:
    1
    2
    // QLineEdit只有一个textChanged(QString)信号,通过在窗体类widget里定义一个槽函数可以将它们关联,语法如下:
    QObject::connection(lineEdit, &QLineEdit::textChanged, this, &widget::on_textChanged);
    但如果说类中定义有不同参数的同名信号就不能用函数指针的方式来关联信号和槽了:
    1
    2
    3
    // QSpinBox有两个valueChanged()信号
    void QSpinBox::valueChanged(int i);
    void QSpinBox::valueChanged(const QString &text);
    1
    2
    // 在窗体中定义一个槽函数
    void onValueChanged(int i);
    Ctrl+B编译时将会出错:
    1
    QObject::connect(spinNum, &QSpinBox::valueChanged, this, &widget::onValueChanged); // error

connect()函数最后都有一个Qt::ConnectionType type,缺省参数是Qt::AutoConnectionQt::ConnectionType是枚举类型,表示信号与槽的关联方式:

1
2
3
4
5
6
7
8
9
10
11
12
// 缺省值,在信号触发时自动确定关联方式。如果发射者和接受者在同一线程,使用Qt::DirectConnection;否则使用Qt::QueuedConnection
Qt::AutoConnection type;

// 信号被发射时槽函数立刻执行
Qt::DirectConnection type;

// 事件循环回到接受者线程后执行槽函数
Qt::QueuedConnection type;

// 与Qt::QueuedConnection相似,但信号线程会阻塞到槽函数执行完毕。
// 注意在同一线程时不用这种方式,会死锁
Qt::BlockingQueuedConnection type;

2.使用sender获得信号发送者

在槽函数里,使用QObject::sender()可以获取信号发送者的指针,知道发射者的类型就可以将它类型转换成确定的类型。

1
2
// QSpinBox的valueChanged(int)信号里可以通过sender()和qobject_cast去的发射者指针然后对发射者进行操作:
QSpinBox *spinBox = qobject_cast<QSpinBox *>(sender());
1
2
3
4
5
// 通过sender返回并打印Button的text值
QObject::connect(button1, &QPushButton::clicked, [this]{
QPushButton *button = qobject_cast<QPushButton *>(sender());
qDebug() << button->text();
});

3.自定义信号及其使用

通过在自己设计的类中自定义信号,这个信号就是声明的函数,但这个函数不用实现,只需要发射(emit):

1
2
3
4
5
6
7
8
9
10
11
12
// 在类中定义一个 ageChanged(int)
class QPerson : public QObject
{
Q_OBJECT
private:
int m_age = 10;
public:
void incAge();
signals:
// void返回类型,可以多参数,但函数无需实现
void ageChanged(int value);
}
1
2
3
4
5
6
// 在incAge()函数中发射信号,当m_age改变时,就emit信号。至于有无信号关联的槽函数,发射者不管
void QPerson::incAge()
{
m_age++;
emit ageChanged(m_age); // emit signal
}

4.关于信号和槽的使用上的规则

  1. 一个信号可以连接多个槽
  2. 多个信号可以连接同一个槽
  3. 一个信号可以连接另一个信号
  4. 信号与槽的个数和类型需要一致,至少信号的参数不能少于槽的参数
  5. 使用信号和槽的类中,需要在类定义中加入宏Q_OBJECT
  6. 当信号被发射时,关联的槽函数会被立即执行,直到执行完毕才会执行余下的程序流程。

元对象系统

Qt的元对象编译器(Meta-Object Compiler, MOC)是一个预处理器,它用于将Qt特性的程序转换为标准C++兼容的形式,再由标准C++编译器进行编译。例如,在使用信号与槽机制的类里,通过添加一个Q_OBJECT宏,MOC才对类中的信号与槽的代码进行预处理。
Qt的元对象系统(Meta-Object System)提供了对象之间通信的信号与槽机制运行时类型信息动态属性系统
除了信号与槽机制,元对象还提供如下一些功能:

  • QObject::metaObject():返回类关联的元对象,元对象类QMetaObject包含了访问元对象的接口函数
    1
    2
    3
    // e.g. 运行时返回类的名称字符串
    QObject *obj = new QPushButton;
    obj->metaObject()->classNmae; // 返回"QPushButton"
  • QMetaObject::newInstance():创建类的一个新的实例
  • QObject::inherits(const char *className):判断对象是否是名为className的类,或者QObject的子类的实例
    1
    2
    3
    4
    5
    // e.g.
    QTimer *timer = new QTimer;
    timer->inherits("Qtimer"); // return true
    timer->inherits("QObject"); // return true
    timer->inherits("QABstraactButton");// return false
  • qobject_cast():dynamic cast功能
    1
    2
    3
    4
    5
    // e.g. 假设QMyWidget是QWidget的子类并且类中声明了Q_OBJECT宏
    QObject *obj = new QMyWidget;
    QWidget *widget = qobject_cast<QWidget *>(obj); // 向上转型
    QMyWidget *myWidget = qobject_cast<QMyWidget *>(obj); // 向下转型
    QLabel *label = qobject_cast<QLabel *>(obj); // error
  • QObject::tr()QObject::trUtf8()用于翻译字符串,用于多语言界面设计
  • QObject::setProperty()QObject::property()函数用于通过属性名称动态设置和获取属性值