架构之路_深度探索C++对象模型总结

2023-11-06

本文主要参照《深度探索C++对象模型》一书。

在这里插入图片描述

一、关于对象

C语言中,数据和处理数据的操作(函数)是分开声明的,不支持数据函数之间的关联性,称之为程序性的(procedural)。

1.1 对象类型

C++中可以通过独立抽象数据类型实现。比如:

class Point3d 
{ 
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;} 
 
private: 
    float _x; 
    float _y; 
    float _z; 
}; 
 
// 类外部inline函数 
inline ostream& 
operator << (opstream &os, const Point3d &pt ) 
{ 
    os << "(" << pt.x() << ", " << pt.y() << "," << pt.z() << ")" ; 
}; 

或以一个双层的class体系

class Point { 
public: 
    Point (float x = 0.0) : _x( x ){} 
    float x() { return _x; } 
    void x ( float xval ) {_x = xval;) 
protected:  
    float _x; 
} 
 
class Point2d : public Point { 
public: 
    Point2d( float x = 0.0, float y = 0.0 ) : Point( x ), _y( y ){} 
    float y() { return _y } 
    void y( float yval ) { _y = yval; } 
protected: 
    float _y; 
}; 

显然在C++中实现3D坐标点,比在C中复杂,尤其是在使用template的情况下。

加上了封装之后,布局成本增加了多少?答案是没有增加成本,像C struct的情况一样, member function虽然含在class的声明之内,却不出现在object之中。每一个non-inline member function只会产生一个函数实体(实体不属于对象,不增加对象空间), inline function则在每个模块身上产生一个函数实体(可看作表达式,直接嵌入到调用函数中,降低执行时间)。

C++在布局以及存取时间上主要的额外负担由vitual引起,包括

  • Virtual function : 用于支持一个有效率的“执行其绑定”
  • Virtual function : 用以实现多重继承base class只有单一的实体存在于后代中。

没有理由说C++比C庞大而迟缓。

C++中,有两种class data member: static 和 nonstatic ,三种 class member functions: static, nonstatic, virtual。

以下代码为例:

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; 
} 
 

C++对象模型中, Non-static data members配置在class object之内, static和 non-static function member则放在class object之外。

  • 每个class产生一堆指向virtual function的指针,放在表格之中。该表格称为virtual table
    (vtbl),注意只指向虚函数。
  • 每个class object被添加指针,指向virtual
    table,该指针称为vptr。vptr的设置,重置都由class的constructor, destructor, copy
    assignment运算符自动完成。每个class关联的type_info object(用以支持RTTI)也经由virtual
    table放在第一个slot处。

在这里插入图片描述

C++中,多态只存在于public class体系中,即对象指针px可能指向自我类型的一个object,或指向public派生而来的一个类型。Nonpublic的派生行为并没有被语言明白地的支持,往往需要通过转型操作来管理。 多态的实现方式如下:

  1. 经过一组隐含的转化操作,例如把一个derived class指针转化为一个指向其public base type的指针: shape
    *ps = new circle;
  2. 经由virtual function机制: ps -> rotate();
  3. 经由dynamic_case转型 if (circle *pc = dynamic_cast< circle *> (ps) )

void rotate( X datum, const X *pointer, const X &reference ) 
{ 
    // 注意: (*pointer),reference在运行期之前无法确定调用rotate()的对象类型,可以是X类型,也可以是派生类型 
    // 本例中调Z.rotate(),显然Z应是X的派生类型。  
    (*pointer).rotate(); 
    reference.rotate(); 
    // datum.rotate(); 必然是X类型的.rotate() 
    datum.rotate(); 
} 
 
main() { 
    Z z; 
    rotate( z, &z, z); 
    return 0; 
} 

1.2 指针类型

指针的类型,表征为指针的涵盖地址范围。例如一个指向地址1000的整数指针,32机器上int占4个字节。所以指针涵盖地址空间1000~1003。而void* 指针只能含有一个地址没有指定地址空间,因此不能对void*解指针操作object。

转型cast其实是一种编译器指令,大部分情况下不改变指针真正地址,只影响“指向内存大小和内容”的解释方式。对object定义指针必须要知道对象大小,作为指针的地址范围。这种情况往往会带了麻烦,例如编译依赖。

加上多态之后 :

class ZooAnimal { 
public: 
    ZooAnimal(); 
    virtual ~ZooAnimal(); 
    virtual void rotate(); 
protected: 
    int loc; 
    string name; 
} 
 
class Bear : public ZooAnimal { 
public: 
    Bear(); 
    ~Bear(); 
    void rotate(); 
    virtual void dance(); 
protected: 
    enum Dances {...} 
    Dances dances_known; 
    int cell_block; 
}; 
 
ZooAnimal za("Zoey"); 
ZooAnimal *pza = &za; 
 
Bear b("Yogi") 
Bear *pb = &b; 
Bear *rb = *pb; 
 

ZooAnimal的object布局和pointer布局,注意到string是一个对象,成员包括String::len和char* String::str。

在这里插入图片描述

Bear的object布局和pointer布局
在这里插入图片描述

其中前三项继承至ZooAnimal。而Bear指针和ZooAnimal指针都指向相同地址,即Bear object的第一个字节。差别是pb涵盖地址范围包含整个bear object, pz只包含Bear object中的ZooAnimal部分。

此外,pz不能处理Bear独有members,唯一例外是通过virtual机制。

pz->cell_block;  //不合法,pz范围到不了Bear独有成员 
((Bear* )pz) -> cellblock;  // 经过一个downcast,改变指针范围,操作没问题 
 
if ((Bear* )pb2 = dynamic_cast< Bear* >(pz)) 
    pb2->cell_block; // 这个比较好,但作为run-time operation成本较高 
 
pb -> cell_block; //这个ok 

注意到pointer或reference之所以支持多态,因为他们只是改变指向内存大小和解释方式。使用virtual的多态也称为OO(Object oriented)操作,而不是用virtual称为OB(Object based)。OO有弹性,但OB有效率,后者所有函数引发操作均在编译时期解析完成,且不需要承担支持virtual机制需要的额外负荷。

二、构造函数语意学

2.1 Default constructor

编译器根据需要合成constructor,被合成的constructor只执行编译器所需的行动。这说明以下两点均错误(1)任何class如果没有定义default constructor,就会被合成出一个来。(错误!!!按需合成)(2) 编译器合成出来的dafault constructor会明确设定class内每一个data member的默认值 (错误,一般不会赋初值)

什么时候需要合成default constructor,有四种情况:

  1. 成员有类对象(Member class object),且该类对象有default construct。

例:

class Foo { public: Foo(), Foo( int ) ... }; 
class Bar { public: Foo foo; char *str; }; 
 
void foo_bar() 
{ 
    Bar bar; 
    if (str) { } ... 
} 

执行Bar bar需要初始化Foo foo,但不会初始化Bar :: str。因为为了保证运行,编译器需要初始化Bar::foo,但初始化Bar::str与否不影响运行,是程序员的责任。

为了保证程序正常运行,需要手工对str进行初始化。

  1. 当一个没有constructor的class派生一个有default constructor的base class,该derived class的default constructor会被合成,且调用base class 的default constructor

  2. 当class声明或继承一个virtual function。会合成default constructor,且下面两个扩张操作会在编译期间发生

  • 一个virtual function table (vtbl)会被编译器产生,内含class的virtual function地址
  • 每一个class object中,一个额外的pointer member (vptr)被编译器合成,内含class vtbl的地址。
  1. 具有virtual base class的类。需要建立default constructor实现一些功能,例如执行期存取操作。
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}; 
 
// 无法编译期确定pa->X::i的位置,因为pa类型可以是A,也可以是C 
void foo (const A* pa) { pa -> i = 1024; } 

2.2 Copy Constructor

三种情况下,一个object的内容作为另一个class object的初值,导致拷贝构造函数的调用。

// 第一种情况,明确赋值 
class X {}; 
X x; 
X xx = x;  // 明确以object内容作为另一个object的初值 
 
// 第二种情况,当object作为实参交给形参时 
extern void foo (X x); 
X xx; 
foo(xx); 
 
// 第三种情况 return 一个class object 
foo_bar() 
{ 
    X xx; 
    return xx; 
} 

并非所有对object赋值的情况都会导致拷贝构造,对于简单的情况会使用Bitwise copy Semantic(位逐次拷贝),挨个变量赋值,并不生成default copy constructor。

与default constructor类似,有四种情况下需要合成default copy constructor:

  • class内部有一个member object,且后者class声明有copy constructor。需生成default copy
  • constructor实现member object赋值逻辑
  • class继承一个base class, 且后者class声明有copy constructor。因为derived class包含
    base class的成员。
  • class声明virtual function,因为需用copy constructor 对vtbl赋值
  • class派生一个继承链,其中有virtual base class.

2.3 示例

X x0; 
void foo_bar() { 
    X x1(x0); 
    X x2 = x0; 
    X x3 = X(x0); 
} 
 
//实际上调用 
X x1; 
x1.X::X( x0 ); //表现为调用copy constructor X::X( const X& xx ); 
// x2,x3亦然 

一般的,在实参到形参,返回值时都会产生临时对象,用copy constructor赋值给临时对象。但有时编译器优化会跳过临时对象这步。

// 优化前 
vector _temp0; 
add (_temp0,a,b); 
vector c(_temp0); 
 
// 优化后 
vector c; 
add (c,a,b); 
 

成员初始化序列

// 如下这种风格最有效率 
Word::Word() 
: _cnt(0), _name( 0 ) 
{} 
// 不要这样写,显然会执行,创建临时对象,拷贝构造,析构临时对象 
Word() { 
    _name = 0; 
    _cnt = 0; 
} 
// 上面代码等价于 
_name .String::String(); 
String temp = String( 0 ); // 构建临时对象 
_name.String::operator=( temp ); //赋值 
temp.String::~String(); //摧毁临时对象 

注意:list的次序按照class member声明次序决定,与initialization list的排列顺序无关。

三、Data语意学

3.1 Data Member

Data members的布局

class Point3d { 
public: 
    // ... 
private: 
    float x; 
    static List<Point3d*> *freeList; 
    float y; 
    static const int chunksize = 250; 
    float z; 
};   
 
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"; 
} 
// class_type会被绑定为Point3d, data_type1 data_type2会被绑定为 float 
access_order( &Point3d::z, &Point3d::y); 

以上,Nonstatic data members在class object中的排列顺序和其被声明的顺序一样,即x,y,z。static data members则存放在程序的data segment中,和class objects无关。

编译器还可能会合成内部使用的data members,例如vptr。

3.2 Static data members

Static member只有一个实体,并不在class object之中,存取之不需要通过class object。

如果两个class,每个都声明了相同名称static member。当它们都放在data segment中会导致名称冲突。编译器的解决办法是对每一个static data member进行编码。

Nonstatic data member直接存放在每一个class object中,一般的,存取nonstatic data member时。编译器需要把class object的起始地址加上data member的偏移量。

3.3 继承与data member

一般而言,具体继承(concrete inheritance)不会增加空间和时间的额外负担,虚拟继承(virtual inheritance)则会。但设计继承时,应尽量避免重复设计相同操作的函数,并且选取合适的函数作为inline函数。

当涉及到多态时,如下所示

class Point2d { 
public : 
    Point2d( float x = 0.0, float y = 0.0 ) : _x(x), _y(y) { }; 
    // 加上z的保留空间 
    float x() { return _x;} 
    float y() { return _y;} 
    void x( float newX ) { _x = newX } 
    void y( float newY ) { _y = newY } 
 
    virtual float z() { return 0.0; } 
    virtual void z ( float ) { } 
 
    virtual void operator+=( const Point2d& rhs ) { 
        _x += rhs.x(); 
        _y += rhs.y(); 
    } 
} 
 
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 }; 
    float z( float newZ ) {_z = newZ; } 
    void operator+=( const Point3d& rhs ) { 
        Point2d::operator += ( rhs ); 
        _z += rhs.z(); 
    } 
protected: 
    float _z; 
} 
 
void foo( Point2d& p1, Point2d& p2) 
{ 
    p1 += p2;  // 显然p1,p2可以为2d坐标点,也可以是3d坐标点 
} 

使用virtual带来的额外负担

  1. 导入一个和Point2d有关的virtual table,用来存放所声明的virtual
    function的地址。还要加上几个slot支持runtime type identification

  2. 每个class object导入一个vptr提供执行期的链接

  3. 加强constructor,能够为vptr赋初值

  4. 加强destructor,能够对vptr,vtbl进行析构。

vptr一般放在class object的起头处或者结尾处。

3.4 虚拟继承

从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,可能存在一个基类的多份拷贝,二义性。

虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)。

vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员。派生类不必存储基类的拷贝,转而以地址代替。

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; 
}; 

在这里插入图片描述

注意到 virtual继承声明在Pointed与Vertex, Point3d之间,Vertex3d与Vertex和Point3d之间不用声明。后者可以通过正常继承得到virtual继承表和指针。
在这里插入图片描述

对比虚函数的实现原理:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)。

虚基类依旧存在继承类中,只占用存储空间;虚函数不占用存储空间,虚指针占。

虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。

四、Function语意学

C++支持三种类型的member functions: static, non-static, virtual

4.1 Nonstatic Member Functions

C++的设计准则之一就是: nonstatic member function至少和一般的nonmember function有相同的效率。实际上member function会被编译器转化为nonmember形式处理。

// 以下来两个函数应该有同等效率
float magnitude3d( const Point3d *_this) { ... }
float Point3d::magnitude3d() const { ... }

// 以下是member function被内化为nonmember形式的转化步骤
// 1. 安插一个额外的参数到member fucntion中,使class object可以调用该函数。
Point3d
Point3d::magnitude( Point3d *const this )
// 若member function为const则变成如下,这也说明const修饰后函数不能改变member data
// Point3d *const this 指针指向地址不能变,但指向的对象Point3d可以变。
// const Point3d *const this 指针指向的地址,指向的对象都不能变。
Point3d
Point3d::magnitude( const Point3d *const this)

//2. 对每一个nonstatic data member的存取改为由this指针存取
{
    return sqrt (
        this->_x * this_x);
}

// 3. 将member function重新写成外部函数,函数名称进行mangling处理。
extern magnitude_7Point3dFv (
    register Pointe3d *const this);
// 调用时由 obj.magnitude(); 变为 magnitude_7Point3dFv( &obj );
// ptr->magnitude(); 变为 magnitude_7Point3dFv( ptr );

C++为了支持重载,会对function进行mangling,以提供独一无二的名称。

对nonstatic member function的const修饰实际上是对转化后nonmember function 的obj实参的修饰。

4.2 Static Member Functions

如果Point3::normalize()是一个static member function,以下两个调用操作

obj.normalize(); 
ptr->normalize(); 
// 转换为一般的nonmember函数调用 
normalize_7Point3dSFv(); 

Static member functions的主要特性就算它没有this指针,以下的次要特性统统源自其主要特性

  1. 不能直接存取class 中的nonstatic members
  2. 不能声明为const, volatile或virtual ,因为以上声明本质是对this指针进行修饰
  3. 不需要经由class object调用

其地址类型并不是“指向class member function的指针",而是一个non member 函数指针;

&Point3d::object_count(); 
// 会得到一个数值,类型为 unsigned int (*) (); 
// 而不是 unsigned int ( Point3d::*) (); 

Static member function由于缺乏this指针,因此差不多等同于nonmember function,成为一个callback函数。

4.3 Virtual Member Functions

如果normaliza()是一个virtual member function,那么调用 ptr->normalize(); 会转化为 (* ptr -> vptr[ 1 ]) ( ptr ); 第二个ptr表示this指针。

如果magnitude()也是一个virtual function,则

register float mag = magnitude();  // 转化如下,this指针作为形参 
register float mag = ( *this->vptr[ 2 ] )(this); 

执行期多态,以ptr->z()为例,调用操作需要ptr在执行期的某些相关信息。一种直截了当的办法时把需要的信息加到指针ptr身上,但显然这种方法成本太高。另一种方法是把执行期加到对象本身,也就算通过虚函数和表的方法。

在C++中,多态(polymorphism)表示,以一个public base class的指针或reference,寻址出一个derived class object。没有使用virtual function称消极多态,反之成为积极多态。

一个class只会有一个virtual table,每一个table内含其对应的class object中所有active virtual functions函数实体的地址。这些active virtual function包括

  1. Class 所定义的函数实体,它会改写可能存在的base class virtual function函数实体。
  2. 继承自base class的函数实体,即derived class没有改写这些function
  3. 一个pure_virtual_called()函数实体,可以包括pure virtual
    function或者异常处理,一旦执行pure_virtual_called(),会停止并退出。

例:

在这里插入图片描述

class Point { 
public: 
    virtual ~Point(); 
    virtual Point& mult( float ) = 0; 
 
    float x() const { return _x; } 
    virtual float y() const { return 0; } 
    virtual float z() const { return 0; } 
protected: 
    Print( float x = 0.0 ); 
    float _x; 
}; 

其中 virtual destructor赋值slot1, mult()由于是纯虚函数,被pure_virtual_called()代替放在slot2。x()不是virtual function,不会放在slot中:


class Point2d : public Point { 
public: 
    Point2d( float x = 0.0, float y = 0.0 ) 
        : Point( x ), _y(y) { } 
    ~Point2d(); 
 
    //改写base class virtual functions 
    Point2d& mult( float ); 
    float y() const { return _y; } 
protected: 
    float _y; 
} 

在base class Point基础上,Point2d修改了~Point2d(), mult(), y()并拷贝到derive class的virtual table对应的位置上。


class Point3d : public Point2d { 
public: 
    Point3d( float x = 0.0, float y = 0.0, float z = 0.0 ) 
        : Point2d( x, y ), _z(z) { } 
    ~Point3d(); 
 
    //改写base class virtual functions 
    Point3d& mult( float ); 
    float z() const { return _z; } 
protected: 
    float _z; 
} 

在继承下,如果有ptr->z()。我们不知道ptr所指对象的真正类型,但是:

  1. 可以经由ptr获取virtual table
  2. 每个z()函数都会放在slot4

以上信息,编译器可以将该调用转化为( *ptr-> vptr[4] )(ptr)。至于ptr具体类型执行期判断即可,但无论如何( *ptr-> vptr[4] )(ptr)总会得到想要的ptr->z()。

4.4 Inline 函数

inline函数执行成本比一般函数调用及返回机制带来的负荷低。处理一个inline函数有两个阶段

  1. 分析函数定义,决定函数本质特征。若函数被判断不能成为inline,会被转换成一个static函数。
  2. inline函数扩展操作,会带来参数求值以及临时对象的管理。

inline在执行时,每个形式参数会被实际参数取代,例:

inline int 
min( int i , int j ) 
{ 
    return i < j? i : j; 
} 
 
//调用操作 
inline int 
bar() 
{ 
    int minval; 
    int val1 = 1024; 
    int val2 = 2048; 
    minval = min (val1, val2); //直接代换 minval = val1 < val2 ? val1: val2; 
    minval = min (1024,2048); // 代换之后得到minval = 1024; 
    minval = min( foo(), bar()+1);   
    // 需要导入临时对象,minval = ( t1=foo()),(t2=bar()+1), t1 < t2? t1:t2 
} 

注意到inline函数中如果存在局部变量,可能导致大量临时对象的产生。编写Inline函数时应该尽量写表达式形式,尽量少的使用内部对象,局部变量。

五、构造、析构、拷贝语意学

考虑下面的abstract base class声明

class Abstract_base { 
public: 
    virtual ~Abstract_base() = 0;  // pure virtual function 
    virtual void interface() const = 0; 
    virtual const char* mumble() const { return _mumble; } 
protected: 
    char* _mumble; 
}; 

以上代码问题如下

  1. 尽管Abstract_base为抽象的base class,有pure virtual
    function因而不可能拥有实体。但仍然需要一个明确的构造函数对_mumble进行初始化。
  2. 将析构函数设置为纯虚数,但Abstract_base仍然需要定义它。因为derived class
    destructor会被编译器扩展,以静态调用形式调用base
    destructor。若Abstract_base声明为纯虚数且未定义,则会导致析构链接失败
// 需要对virtual ~Abstract_base进行定义实现 
Abstract_base::~Abstract_base() { }  

一般的,不要把virtual destructor声明为pure virtual

  1. 将virtual interface声明为const,而derived
    instance很可能修改其中一个member,尽量不要对virtual function声明为const
  2. mumble不应该声明为virtual,函数一般不会被derived class重写,没必要声明为virtual。

修改后代码如下

class Abstract_base { 
public: 
    virtual ~Abstract_base(); 
    virtual void interface() = 0; 
    const char* mumble() const { return _mumble; } // 注意函数用const修饰,返回类型为const char*,返回值不能被修改(只能修饰指针) 
    // const char* 只能被const char* p接收 
protected: 
    Abstract_base( char* pc = 0 };    // 带参数的constructor 
    char *_mumble; 
}; 
 
// 体会下const return 
const int* integer_return (const int& a){  // 必须传引用,不能传值 
    return &a; 
} 
int b = 3; 
const int* a = integer_return(b); 
cout<< *a <<endl; 

5.1 无继承情况下的对象构造

考虑程序片段

Point global;                   //L1
 
Point foobar(); 
{ 
    Point local;                //L5
    Point *heap = new Point();  //L6
    *heap = local; 
 
    delete heap; 
    return local;               //L10
} 

以上L1,L5,L6表现出三种不同的对象产生方式,global内存配置,local内存配置,heap内存配置。一个object的生命,是object一个执行期属性。local object从L5开始,L10结束。global object声明和整个程序生命相同,heap object生命从new运算符配置出来开始,到delete运算符摧毁为止。

Point的声明 
typedef struct 
{ 
    float x,y,z; 
}Point; 

C++将以上C风格程序称之为Plain Of Data形式。在编译时,编译器会分析这个声明,贴上Plain Of Data标签。

当执行Point global时,程序一如在C表现一样。一般的trivial default constructor, trivial destructor, trivial copy constructor, trivial copy assignment operator等未被定义或调用(不是所有情况都会生成以上trivial member,前几篇文章有说)

C和C++一个差异是,C++所有全局对象都被当作初始化的数据对待。

L5中Point object local,同样没有生成default constructor,而是当作C中的struct操作,即Plain Of Data。但需要进行赋初值。注意如果写了constructor,自然通过C++构造函数来进行构造。

L6转化如下:

Point *heap = new Point; 
// 转化为 
Point *heap = __new( sizeof(Point)); 

但仍然没有default施行于new运算符返回的Point object身上。同样对于赋值,由于object是一个Plain Of Data,赋值操作只是像C那样纯粹位搬移操作。

总之,在无构造函数,无继承,无虚函数,C++类和C的struct操作无太大区别。不论private、public存取层,或是member function的声明,都不会占用额外的对象空间。

对class所有成员设定常量初值,给予一个explicit initialization list会比较高效,甚至在local scope ,如下:

void mumble() 
{ 
    Point local1 = {1.0, 1.0, 1.0}; 
    Point local2; 
    // 下列相当于inline expansion, local1比local2快 
    local2._x = 1.0; 
    local2._y = 1.0; 
    local2._z = 1.0; 
} 
 

Explicit initialization list的缺点

  1. 只能指定常量,因为编译期可以求值
  2. 初始化失败可能性较高

编译器内部有识别inline expansion并进行explicit initialization list优化的功能。

Virtual function的引入

以下列程序为例

class Point { 
public: 
    Point( float x = 0.0, float y = 0.0 ) 
        : _x( x ), _y( y ) {  } 
    // no destructor, copy constructor, copy operator defined 
    virtual float z(); 
protected: 
    float _x, _y; 
} 
 

Virtual function的引入促使每一个object拥有一个virtual table pointer。除了每一个class object多负担一个vptr之外,也引发class的膨胀。

  1. 定义的constructor被扩展,以便将vptr初始化
  2. 合成一个copy constructor和一个copy assignment operator,而且其操作不再是trivial(
    implicit destructor仍然是trivial)。一般的,将rhs的连续位拷贝给this对象,之后return
    this。这种default copy constructor可能存在问题。
// 内部膨胀 
Point* Point::Point( Point* this, float x, float y ): _x(x), _y(y) 
{ 
    //  设置object的virtual table pointer( vptr ) 
    this->__vptr_Point = __vtbl_Point; 
    //  扩展member initialization list 
    this->_x = x; 
    this->_y = y; 
 
    return this; 
} 

引入virtual function时将合成default constructor,这也导致当对象赋值,return object时候会出现copy constructor的过程,而不再是像C语言那样位搬移操作。

5.2 继承情况下的对象构造

当我们定义一个object, T object。

如果T有一个constructor(不论是user 提供还是编译器合成),constructor会被调用。而且编译器会扩充每一个constructor,扩充程度视class T的继承体系而定。一般而言,编译器所做的扩充大约如下

  1. 记录member initialization list中的data members初始化操作,并以member的声明顺序为初始化顺序
  2. 若有一个member没有出现在member initialization list之中,但它有一个default constructor,则该default constructor必须被调用
  3. 在1,2之前,若class object有virtual table pointer,它必须设置初值,指向合适的virtual table
  4. 在1,2,3之前,上一层的base class constructor必须被调用(先构建base class)。若class列在member initialization list,直接调用之。将生成默认initialization
    list调用之。对虚拟继承,class中的每一个virtual base class subobject的偏移量必须在执行期内存取。
class Derive : public Base{ 
public: 
    Derive(){ 
        Base(2); 
        cout << "construct derive class" <<endl; 
    } 
} 
// 以上将调用Base()而不是Base(2)进行构造,因为默认生成Derive():Base(){},写在constructor内部没有构造作用 
class Derive : public Base{ 
public: 
    Derive():Base(2){ 
        this->a = 3; 
    } 
} 
// 尽管调用Base(2)使Base->a = 2,但输出derive->a结果为3 

class Point { 
public: 
    Point( float x = 0.0, float y = 0.0 ); 
    Point( const Point& );    // copy constructor 
    Point& operator= (const Point& );    // copy assignment operator 
 
    virtual ~Point();    // virtual destructor 
    virtual float z() { return 0.0; } 
protected: 
    float _x, _y; 
} 
 
// Line class  
class Line { 
    Point _begin, _end; 
public: 
    Line( float=0.0, float=0.0, float=0.0, float=0.0 ); 
    Line ( const Point&, const Point& ); 
    draw(); 
} 
 
每一个explicit constructor都会被扩充,以调用其两个member Point object的constructor 
// 定义Line constructor  
Line::Line( const Point& begin, const Point& end) : _end(end), _begin(begin){} 
 
// 被编译器扩充为 
Line* Line::Line(Line *this, const Point &begin, const Point& end) 
{ 
    this->_begin.Point::Point( begin ); 
    this->_end.Point::Point( end ); 
    return this; 
} 

当写下Line a, implicit Line destructor会被合成出来(若Line派生Point,则合成virtual destructor,但本例Line只是内带Point objects而非继承,所以只需合成nontrivial destructor),会调用其member object的destructor。

inline void Line::~Line( Line *this ) 
{ 
    this->_end.Point::~Point(); 
    this->_begin.Point::~Point(); 
} 

写下 Line b = a, implicit Line copy constructor会被合成出来,成为一个inline public member

写下a = b, implicit copy assignment operator会被合成出来,成为一个inline public member。

注意在写copy operator时

// 添加如下条件句筛选 
if (this == & rhs) 
    return this; 
// 否则可能会做多余拷贝工作,如下 
Line p1 = &a; 
Line p2 = &b; 
*p1 = *p2; 
 
//而且需要注意释放资源筛选 
String& String::operator=( const String &rhs ) {     
    if (this == & rhs) 
        return this; 
    delete [] str;   //前面如果不加条件筛选,有可能this == rhs,直接把两个都delete了。 
    str = new char[ strlen( rhs.str) + 1]; 
} 

5.3 虚拟继承

考虑虚拟继承

class Point3d : public virtual Point { 
public: 
    Point3d( float x = 0.0, float y = 0.0, float z = 0.0 ) 
        : Point( x, y ), _z( z ) { } 
    Point3d( const Point3d& rhs ) 
        : Point( rhs ), _z( rhs._z ) { } 
    ~Point3d(); 
    Point3d& operator= (const Point3d& ); 
 
    virtual float z() { return _z } 
 
 protected: 
     float _z; 
} 

虚拟继承不同在于,虚基类不会被所有子类初始化,而只会被最终的派生类初始化。
在这里插入图片描述

以上,Vertex3d有责任将Point初始化。而Point3d和Vertex同为Vertex3d的subobjects,它们都不能对Point constructor进行调用。

// Vertex3d的constructor扩充内容 
Vertex3d* 
Vertex3d::Vertex3d( Vertex3d *this, bool __most_derived, float x, float y, float z ) 
{ 
    //  根据提条件决定是否调用Base class Point 
    if (__most_derived != false ) 
        this -> Point::Point(x,y); 
    // 调用上一层的base class 
    // 设置false表示Point3d Vertex均不能调用Point constructor,只有Vertex3d可以。 
    // 如果不是虚拟继承,则相反。Vertex3d不能调用Point, Pointe3d Vertex可以 
    this -> Point3d:Point3d( false, x, y, z); 
    this -> Vertex( false, x, y); 
 
    // 设定vptrs; 
    // 安插user code 
    return this; 
} 

注意到当定义 Point3d origin;时,Point3d会先调用Point。而定义Vertex3d cv;时,Point3d只是cv的subobject,Point3d不会调用Point constructor。

因此有的编译器将constructor分成两种情况,一种接收完整object(如Point3d origin),一种则接收subobject(如Vertex3d cv)。完整object版无条件调用virtual base constructor,设定vptrs;subobject版则不调用virtual base constructors,也可能不调用vptrs。

Constructor的调用顺序是:由根源到末端,由内而外。

// 定义一个Pertex object时,constructor的调用顺序是 
Point (x, y); 
Point3d(x, y, z,); 
Vertex(x, y, z); 
Vertex3d(x, y, z); 
PVertex(x, y, z); 

当base class constructor执行时,derived实体还没有构造出来。Point3d constructor执行之后,只有Point3d subobject构造完毕。

这种机制要求,每一个调用操作以静态方式,原因很简单,derived object还没有构造,谈不上动态绑定,只能用base object的member。

constructor的执行流程如下

  1. 在derived class constructor中,所有virtual base class 及上一层base class的constructor会被调用
  2. 上述操作完成后,对象的vptr被初始化,指向相关的virtual tables
  3. 如果有 member initialization
    list的话,将在constructor体内扩展。这必须在vptr被设定之后,因为有可能会调用virtual function。
  4. 最后执行程序员编写的代码

例 定义的PVertex constructor

PVertex:PVertex( float x, float y, float z ) 
    : _next(0), Vertex3d(x, y, z), Point(x, y) 
{ 
    if ( spyOn ) 
        cout << "Within Pertex::Pertex() " << "size: " << size() <<endl; 
} 
 
//  被扩展为 
Pertex* 
Pertex( PVertex* this, bool __most__derived, float x, float y, float z) 
{ 
    // 条件地调用virtual base constructor 
    if ( __most__derived != false ) 
        this -> Point::Point(x, y); 
    // 无条件的调用上一层base 
    this -> Vertex3d::Vertex3d(x, y, z); 
 
    // 将相关vptr初始化 
    this->__vptr_Pertex = __vtbl_PVertex; 
    this -> __vptr_Point_PVertex = __vtbl_Point_PVervex; 
 
    //  程序员写的码 
    if ( spyOn ) 
        cout << "Within Pertex::Pertex() " << "size: " << (*this->__vptr__PVertex[3].faddr)(this) <<endl; 
    return this; 
} 

Destruction步骤与Constructor相反

  1. destructor的函数本身先执行
  2. 如果class有member class objects, 后者拥有destructors,那么以声明相反顺序调用
  3. 若object内含vptr,则重新设定,指向适当的base class的virtual table
  4. 若nonvirtual base class拥有destructor,按声明相反顺序调用
  5. 若virtual base class 拥有destructor,按声明相反顺序调用。
举例  
#include<iostream> 
using namespace std; 
 
class D{ 
public: 
    D(){cout<<"D Constructor\n";} 
    ~D(){cout<<"D Destructor\n";} 
};  
class C : public virtual D{ 
    public: 
        C(){cout<<"C Constructor\n";} 
        ~C(){cout<<"C Destructor\n";} 
}; 
class B : public virtual D{ 
    public: 
        B(){cout<<"B Constructor\n";} 
        ~B(){cout<<"B Destructor\n";} 
 
}; 
class A:public C,public B 
{ 
    public: 
        A(){cout<<"A Constructor\n";} 
        ~A(){cout<<"A Destructor\n";} 
 
}; 
int main(){ 
    A a; 
    return 0; 
} 
// 结果; 
// Constrctor执行与Destructor完全相反 
D Constructor 
C Constructor 
B Constructor 
A Constructor 
A Destructor 
B Destructor 
C Destructor 
D Destructor 
 

当设计一个class,并以一个class object指定给另一个class object时,有三种选择

  1. 什么都不做,默认复制行为
  2. 提供一个explicit copy assignment operator
  3. 明确拒绝一个class object指定给另一个class object,只要将copy assignment operator声明为private,编译器即不能调用。

一般的,由bitwise copy完成,期间没有copy assignment operator被调用。以下情况不会出现bitwise copy,而会产生copy assignment operator

  1. class内含一个object,该object的class有一个copy assignment operator
  2. Class base class有copy assignment operator
  3. 当class声明了virtual fucntion,需要使用copy assignment operator处理vptr
  4. 当class继承自一个virtual base class,同样有虚类表。

六、执行期语义学

考虑以下代码

if ( yy == xx.getValue() ) ... 
 
//其中 xx和yy定义为 
X xx; 
Y yy; 
 
// class Y定义为 
class Y { 
public: 
    Y(); 
    ~Y(); 
    bool operator== ( const Y&) const; 
}; 
// class X定义为 
class X { 
public: 
    X(); 
    ~X(); 
    operator Y() const;    // conversion运算符 
    X getValue(); 
}; 
 
// 第一次转换 
if (yy.operator== (xx.getValue()) ) 
// 第二次转换 
if (yy.operator== (xx.getValue.operator Y() )) 

实际上对if ( yy == xx.getValue() )的转换:

// 产生一个临时的class object,放置getValue()的返回值 
X temp1 = xx.getValue(); 
 
// 产生一个临时的class Y object,放置operator Y()的返回值 
Y temp2 = temp1.operator Y(); 
 
// 产生临时的int object,放置equality 运算符的返回值 
int temp3 = yy.operator==(temp2); 
 
// 剩余代码 
if (temp3) ... 
temp2.Y::~Y();    // 等价于temp2.~Y(); 
temp1.X::~X(); 

6.1 全局对象

对以下程序片段

Matrix identity; 
int main() 
{ 
   // identity在此处必须初始化 
   Matrix m1 = identity; 
   ... 
   return 0; 
}  
 

C++保证,一定会在main()函数第一次用到identity之前,把identity构造出来,而在main()函数结束之前把identity摧毁掉。像identity这样的global object如果有constructor和destructor的话,称之为需要静态的初始化操作和内存释放操作。C++一般会对global object进行集中初始化。

C++程序中所有的global objects都被放置在程序的data segment中,如果明确指定一个值,object以该值为初值,否则,配置object内存内容为0。(C++对global设初值,但C语言不设置)

const Matrix&  
identity() 
{ 
    static Matrix mat_identity; 
    // ... 
    return mat_identity; 
} 
 

Local static class object中,保证

  1. mat_indentity的constructor必须只能执行一次,虽然上述函数可能被调用多次
  2. mat_indentity的destructor只能施行一次

6.2 对象数组

假设

  • Point knots [ 10 ];

如果Point没有定义constructor也没有定义destructor,那么不会比内建(bulid-in)类型组成的数组更复杂。

然而定义了default destructor, 所以这个destructor轮流施行于每个元素之上,使用一个vec_new()函数

void* 
vec_new ( 
    void *array,    // 数组起始地址 
    size_t elem_size,    // 每一个class object的大小 
    int elem_count,    // 数组的元素数目 
    void (* constructor)( void* ), 
    void (* destructor)( void*, char ) 
} 
 
// 调用操作 
vec_new( &knots, sizeof( Point ), 10, &Point::Point, 0); 
 
// 同样对于destructor 
void* 
vec_delete( 
    void *array, 
    size_t, elem_size, 
    int elem_count, 
    void (*destructor)( void*, char) 
} 

其中,constructor和destructor参数是这个class的default constructor和default destructor的函数指针。

在vec_delete中,destructor被施行于elem_count个元素身上。

如果程序员提供一个或者多个明显初值给object数组

Point knots[ 10 ] = { 
    Point(), 
    Point( 1.0, 1.0, 0.5), 
    -1.0 
}; 
 
// 很可能转化为 
Point knots[10]; 
 
// 明确初始化前三个元素 
Point::Point( &knots[0] ); 
Point::Point( &knots[1], 1.0, 1.0, 0.5 ); 
Point::Point( &knots[2], -1.0, 0.0, 0.0 ); 
 
// 以vec_new 初始化后七个元素 
vec_new( &knot+3, sizeof( Point ), 7, &Point::Point, 0); 

6.3 new 和delete运算符

运算符new的使用,看起来似乎是单一运算,事实上由两个步骤完成

int *pi = new int( 5 ); 
 
// 由以下两个步骤完成 
// 1. 通过适当的new运算符函数实体,配置所需的内存 
int *pi = __new( sizeof(int) ); 
// 2. 给配置的对象设置初值 
*pi = 5; 

注意,初始化应该确定内存配置成功才进行。

// 用constructor来配置一个class object 
Point3d *origin = new Point3d; 
// 被转化为 
Point3d *origin; 
// C++伪代码 
if ( origin == __new( sizeof( Point3d ) ) ) 
{ 
    try { 
        origin = Point3d::Point3d( origin ); 
    } 
    catch (...) { 
        __delete( origin );  // 失败,释放new配置的内存 
        throw;  // throw exception 
    } 
} 
 
// Destruction操作 
delete origin; 
// 转化为,未考虑exception 
if (origin != 0)    // 如果origin==0,不进行操作 
{ 
    Point3d::~Point3d( origin ); 
    __delete( origin );    // 释放内存 
} 

Operator new 实际上以标准C malloc()完成,delete运算符也以标准C free()完成。

可见new T[0]是合法的,返回一个指向1-byte区块的指针

extern void* 
operator new (size_t, size) 
{ 
    if (size == 0) 
        size = 1;    // 如果size=0,将返回一个指针,指向一个1-byte的区块 
    void *last_alloc; 
    while (!(last_alloc == malloc( size ))) 
    { 
        if (_new_hander )    // 允许使用者提供一个new_hander函数处理异常 
            ( *_new_hander )(); 
        else 
            return 0; 
    } 
    return last_alloc; 
} 
 
extern void* 
operator delete ( void *ptr) 
{ 
    if (ptr) 
        free ( (char*) ptr); 
} 

针对数组的new, delete

// 当这么写, vec_new不会调用。 
int *p_array = new int[ 5 ]; 
// new运算符会被调用 
int *p_array = (int*) __new( 5 * sizeof( int )); 
 
// 而如果class定义由default constructor, vec_new就会被调用,配置并构造class objects组成的数组 
Point3d *p_array = new Point3d[ 10 ]; 
// 通常被编译为 
Point3d *p_array; 
p_array = vec_new( 0, sizeof(Point3d), 10, &Point3d::Point3d, &Point3d::~Point3d );

vec_new 有责任在exception发生时把内存释放调。

T *p_array = new T[m]; 
delete[] p_array; 
 
// 完全不是好主意 Point3d 和Point的constructor各调用10次 
Point *ptr = new Point3d[10]; 
// 只有10次Point被释放,Point3d没有,造成内存泄漏 
delete[] ptr; 

注意:避免构造用base class指针指向derived class object所组成的数组,把析构函数写成virtual可以解决以上问题。

原因在于传入destructor大小为Point大小而不是Point3d大小。使用虚函数,Point3d大小会覆盖Point,因此解决问题。

class Base{ 
public: 
    Base(){ 
        cout << "constructor Base" <<endl; 
    }; 
    ~Base(){ 
        cout << "destructor Base" <<endl; 
    }; 
}; 
class Derive : public Base{ 
public: 
    Derive(){ 
        cout << "constructor Derive" <<endl; 
    }; 
    ~Derive(){ 
        cout << "destructor Derive" <<endl; 
    }; 
}; 
/* 
Derive d; 输出 
constructor Base 
constructor Derive 
destructor Derive 
destructor Bas 
 
Derive *d = new Derive 输出没有析构部分 
constructor Base 
constructor Derive  
 
Derive *d = new Derive; 
delete d; 输出 
 
constructor Base 
constructor Derive 
destructor Derive 
destructor Bas 
 
Base *p = new Derive[2]; 
delete[] p; 输出 
constructor Base 
constructor Derive 
constructor Base 
constructor Derive 
destructor Base 
destructor Base 
 
将析构函数设置为virtual, 输出 
constructor Base 
constructor Derive 
constructor Base 
constructor Derive 
destructor Derive 
destructor Base 
destructor Derive 
destructor Base 

Operator new, operator delete允许申请/释放一块内存,而placement new允许在已经获得的一块内存上建立对象。它并不分配内存,只是返回指向已经分配好的某段内存的一个指针。因此不能delete,需要调用对象的析构函数来摧毁对象。

class Task ; 
char * buff = new [sizeof(Task)]; //分配内存 
Task *ptask = new (buf) Task; 
 
// 一旦你使用完这个对象,你必须调用它的析构函数来毁灭它。 
ptask->~Task(); //调用外在的析构函数,不能delete ptask 
// 释放缓存 
delete [] buf; 

显然placement new不支持多态,交给placement new的指针,应该适当的指向一块预先配置好的内存中。

6.4 临时对象

// 针对下面情况 
T c = a + b; 
// 如果进行下列转化,不会存在临时对象 
T operator+ ( const T&, const T&); 
T T::operator+ ( const T& ); 
 
// 下面这种不能忽略临时对象 
c = a+b; 
// 转化为 
T temp; 
temp.operator+(a, b); 
c.operator=(temp); 
temp.T:~T(); 
 
// 下面情况也存在临时对象 
String s("hello"), t( "world"), u( "!"); 
printf ("%s\n", s+t);  // 甚至可能printf打印时,临时对象已经被摧毁,导致使用释放的内存 
 
// 下面情况不会摧毁临时对象 
const char* progNameVersion = progName + progVersion; 
//转化为 
String temp; 
operator+ (temp, progName, progVersion); 
progNameVersion = temp.String::operator char*();  
temp.String::~String(); 
 
const String &space = " "; 
// 转化为 
String temp; 
temp.String::String(" "); 
const String &space = temp; 

以上两种情况临时对象不会被摧毁,否则programNameVersion指向未定义的heap内存。

  1. 凡含有表达式执行结果的临时对象,应该存留到object的初始化操作完成为止
  2. 若临时对象被引用或指针绑定,那么直到reference或pointer的生命结束,临时对象才被结束

七、站在对象模型的尖端

7.1 Template

C++程序设计的风格,在引入template之后就深深地改变了。原本template被视为对container class如List, Array的一项支持,但现在已经成为通用程序设计。它被用于属性混合(如内存配置策略),互斥机制(线程同步化控制)的参数化,甚至一种template metaprogram技术(模板元编程)。

template函数特化,template类偏特化、全特化是template的基础,相当于调整template具现化的参数

#include<iostream> 
using namespace std; 
//函数模板 
//普通函数模板fm 
template<typename T> 
T fm(T t1) 
{ 
  cout<<"普通函数模板"<<endl; 
  return t1; 
} 
//fm的指针特化模板 
template<typename T> 
T* fm(T* t1) 
{ 
  cout<<"int指针特化模板"<<endl; 
  return t1; 
} 
//fm的int特化模板 
template<> 
int fm(int t1) 
{ 
  cout<<"int特化函数模板"<<endl; 
  return t1; 
} 
template<> 
float fm(float t1) 
{ 
  cout<<"float特化模板"<<endl; 
  return t1; 
} 
//普通类模板Test 
template<typename T1,typename T2> 
class Test 
{ 
public: 
  void ply() 
  { 
    cout<<"普通类模板"<<endl; 
  } 
}; 
template<typename T1> 
class Test<T1,int> 
{ 
public: 
  void ply() 
  { 
    cout<<"int偏特化类模板"<<endl; 
 
  } 
}; 
template<> 
class Test<char,int> 
{ 
public: 
  void ply() 
  { 
    cout<<"全特化类模板"<<endl; 
  } 
}; 
//测试程序 
int main() 
{ 
  int a =23; 
  int *p =&a; 
 
  cout<<fm(a)<<endl;     
  // 输出: int特化函数模板 23 
  cout<<fm(p)<<endl; 
  // 输出: int指针特化模板 0x7fffffffda70 
  float f =23.4; 
  cout<<fm(f)<<endl; 
  // 输出: float特化模板 23.4 
  double d= 99.45; 
  cout<<fm(d)<<endl; 
  // 输出:普通函数模板 99.45 
  cout<<endl; 
  Test<char,char>TT1; 
  TT1.ply(); 
  // 输出: 普通类模板 
  Test<int,int>TT2; 
  TT2.ply(); 
  // int偏特化类模板 
  Test<char,int>TT3; 
  TT3.ply(); 
  // 全特化类模板 
  return 0; 
} 
 

有关template的三个主要讨论方向:

  1. template的声明,基本是声明template class , template class member function发生什么事情
  2. 如何具现(instantiates)出class object,inline nonmember, member template functions。这些是,每一个编译单位都会拥有一份实体。
  3. 如何具现出nonmember, static template class member,这些每一个可执行文件只需要一份实体。

7.2 Template具现行为

考虑template Point class

template <class Type> 
class Point{ 
public: 
    enum Status { unallocate, normalized }; // enum相当于 static const 
    Point (type x = 0.0, Type y = 0.0, Type z = 0.0);Point(); 
 
    void* operator new ( size_t ); 
    void operator delete( void*, size_t); 
    // ... 
private: 
    static Point< Type > *freeList; 
    static int chunkSize; 
    Type _x, _y, _z; 
}  

上述static Point并直接用,必须要进行具现化。即使enum与class type无关联。

// 可以这样写 
Point< float >::Status x; 
// 不能这样写 
Point::Status x; 
 
// 这样写会有一份static实体(即freeList)与Point class的float instantiation在程序中产生关联 
Point< float >::freeList; 
// 会出现第二个freeList实体,与Point class的double instantiation产生关联 
Point< double >::freeList; 
 
// 如果设置指针,具现化比较没那么必要 
Point< float > *ptr = 0; 
 
// 如果是reference,需要具现实体 
const Point< float > &ref = 0; 
// reference内部扩展如下 
Point< float > temporary( float(0)); 
const Point< float > &ref = temporary; 

注意,指向class object的指针,本身不是一个class object,编译器不需要知道与class相关的任何member数据或者object布局数据。因此具现化一个float实体显得没有必要。

reference必须要初始化,必须具现实体。指针则不一定

注意到class object的定义会导致template class的具现,但member function不应该被实体化(member function实际上不属于object)。只有在member function被使用的时候,C++ Standard才要求具现出来。

// 只有Point的 (1)template的float实例 
// (2) new 运算符 
// (3) default constructor 需要被具现化 
Point < float >*p = new Point< float >; 
 
Point<float> *ptr = 0;  // 指针其实未产生对象   
const Point<float> &ref = 0; // reference产生了对象 
 
// 尽量不要在template中设置参数值,在特化中设置 
// template的类型问题,一定小心,出错不易调试。简而言之,是否存在不适用计算规则的类型,如果有,把该类型特化 
Mumble(T t = 1024); // 设置参数值,可能存在类型问题 
 
Memble<int> mi; // 正确 
Memble<int* >pmi; //不正确,因为i不能对指针设置除0之外的常数。 

同时注意到,目前的编译器面对template声明,在被一组实际参数具现之前,只能施行以有限的错误检查。也就说,在run之前,编译器对template很少有错误检查(类似直接画波浪线),包括一些很明显的错误。只有传入类型具现之后,也就是点运行之后才会再终端显示错误。

可以使用g++ -g命令只进行编译操作,g++7.5很多问题优化的比较好,但g++4.8还是存在。

原因在于对template的处理是完全解析不做类型检验,只有在具现操作发生之时才会做类型检验。

7.3 Template的名称决议方式

区分以下两种意义

  • Scope of the template definition, 定义出template的程序
  • Scope of the template instantiation 具现出template的程序
// 第一种情况 scope of the template definition 
extern double foo( double ); 
 
template < class type > 
class ScopeRules{ 
public: 
    void invariant() { 
        _member = foo( _val ); 
    } 
    type type_dependent() { 
        return foo( _member ); 
    } 
private: 
    int _val; 
    type _member; 
}; 
 
// 第二种情况 scope of the template instantiation 
extern int foo( int ); 
ScopeRules< int > sr0; 
sr0.invariant();    // 决议:调用scope of the template definition的name 
sr0.type_dependent(); // 调用scope of the template instantiation的name 

注意到ScopeRules template中有两个foo()的调用操作。而且,在"scope of template definition"中,只有一个foo()函数声明位于scope之内,在"scope of template instantiation"中,两个foo()函数声明都位于scope之内。

template之中,对于一个nonmember name的决议结果是,根据是否参数是否需要具现。对invariant()调用的foo()。显然,参数_val是定义好的int类型,不需要具现。因此调用 extern double foo (double)。

反之对于 foo(_number),由于_number类型需要具现,调用scope of the template instantiation中的 extern int foo (int)。

如果在相应的scope找不到合适的function,就会报错。因此编译器必须保持两个scope contexts

  1. Scope of the template declaration,专注于一般的template class
  2. Scope of the template declaration, 专注于需要具现的实体。

编译器的决议算法必须决定哪一个才是适当的scope,然后在其中搜寻适当的name.

必须注意到对template的支持是编译器设计的一个挑战,由于编译器本身可能存在问题,自然在大型项目中总是出现莫名奇妙的问题。当使用template时只能说要小心,考虑全面,不要滥用template。

7.4 异常处理

C++的exception handling主要由三个组件构成

  1. 一个throw子句,它在程序某处发出一个exception,丢出去的exception可以是内建类型,也可以是使用者自定类型。
  2. 一个或多个catch子句,每一个catch子句都是一个exception handler
  3. 一个try区段。

当一个exception被丢出去,控制权会从函数调用中被释放出来,并寻找一个吻合的catch子句。如果没有吻合者,默认的处理例程terminate()会被调用,函数调用会被推离堆栈,称unwinding the stack。函数被推离之前,内部的local class objects的destructor会被调用。

Exception handling改变了函数在资源管理上的语意,例如

void mumble( void* arena ) 
{ 
    Point *p = new Point; 
    smLock( arena ); 
    //  如果由一个exception发生,问题就来了 
    smUnLock( arena ); 
    delete p; 
} 
 
// 以上应该改为 
void mumble( void* arena ) 
{ 
    Point *p = new Point; 
    try { 
        smLock( arena ); 
    } 
    catch( ... ) { 
        smUnLock( arena ); 
        delete p; 
        throw; 
    } 
    //  如果由一个exception发生,问题就来了 
    smUnLock( arena ); 
    delete p; 
} 

第一部分代码问题在于,当异常发生时,不会主动调用p指向对象的析构函数(因为是在堆上new的)。

改进程序表示,当发生异常,执行catch内部代码,即在函数被推出堆栈之前需要对unlock共享内存,并delete p。若不发生异常,正常执行,不会执行catch内部代码。但显然这种方法不是太优雅。

处理资源管理问题,建议将资源需求封装到一个class object内(即智能指针),并使用destructor来释放资源。

void mumble (void *arena ) 
{ 
    auto_ptr<Point> ph (new Point); 
    SMLock sm( arena ); 
    // 使用智能指针不需要明确的unlock和delete 
    // 自动调用析构函数进行unlock和delete 
    // sm.SMLock::~SMLock(); 
    // ph.auto_ptr<Point>::~auto_ptr<Point>(); 

一个函数可以想象成以下区域

  • try区段以外的区域,而且没有local object

  • try区段以外的区域,但有local object需要析构

  • try区段内部的区域

编译器需要标示出以上区域范围,并使他们对执行期exception handling起作用。即exception表格,描述与函数相关的各区域等

当excption发生时,编译系统必须完成以下事情

  • 检验发生throw操作的函数
  • 决定throw操作是否发生在try区段中
  • 若是,编译系统必须把exception type和每一个catch子句比较。如果吻合,控制流程交给catch字句
  • 如果throw不在try区段中,或没有一个catch子句吻合,那么系统必须(a) 摧毁所有local objects (b)从堆栈中将当前函数推出 © 进行程序堆栈的下一个函数。

对于每一个throw的exception,编译器必须产生一个类型描述器,即通过RTTI获取exception object的类型信息。编译器还需要为catch字句产生一个类型描述器,从而比较被throw之exception object的类型描述器和catch子句的类型描述器,直到吻合的一个。

一般的,当exception被throw,exception object通常被产生放置在exception数据堆栈中,从throw端传给catch 字句的exception object的地址,类型描述器。

7.5 执行期类型识别

class string { 
public: 
    operator char*();  // conversion转型运算符 
} 
 
class type { ... }; 
class fct : public type { ... }; 
 
typedef type* ptype; 
typedef fct* fct; 
 
simplify_conv_op( ptype pt ) 
{ 
    pfct pf = new pfct( pt );  // downcast向下转型 
}    

向下转型,把一个base class转换至继承结构的derived class中的某一个。Downcast有潜在的危险,因为derived class可能有特有的member而base class没有。反之,向上转型是安全的,只需要将derived部分特有的member去掉即可。因为derived class指针的范围比base class的大,因此不正确的downcast可能带来错误的解释(read操作),腐蚀掉程序内存(write操作)。

欲支持type_safe downcast,在object空间和执行时间都需要额外负担。(1)需要额外的空间储存类型信息,通常是一个指针指向某个类型节点;(2)需要额外的时间决定执行期的类型

C++的RTTI提供了安全的downcast,但只对多态(继承和动态绑定,即Base *p = new Derive()这种)的类型有效。一般的,将与class相关的RTTI object地址放进virtual table中(通常是第一个slot),指向slot的指针被编译器静态设定,而不是执行期由constructor设定(vptr才这样)。

simplify_conv_op( ptype pt) 
{ 
    if ( pfct of = dynamic_cast< pfct >( pt )) // 向下转型 
    // ... 
} 
 
// 取得pt的类型描述器 
((type_info*)(pt->vptr[ 0 ])) -> _type_descriptor; 
 
Base *Bptr1 = new Base(); 
Derived *Dptr1 = dynamic_cast<Derived *>(Bptr1);  // 这种向下转型不安全,转型失败会返回空指针Dptr1 = 0 
 
Base *Bptr2 = new Drived(); 
Derived *Dptr2 = dynamic_cast<Derived *>(Bptr2);  // 这种向下转型安全 
 
Derived &Dptr = dynamic_cast<Derived &>(Bptr); // 对引用,若转型失败直接报错 

注意到pfct的类型描述符会被编译器产生出来,但由pt指向class object类型描述器必须在执行期通过vptr取得,即dynamic_cast主要时间成本。

dynamic_cast相比于static_cast可以通过virtual table获取动态类型,向下转型更安全。

注意到若指针指向的对象转型失败,会返回空指针。但对于引用,不能够再给引用赋值,会抛出bad_cast exception。

7.6 typeid运算符

class type { ... }; 
class fct : public type { ... }; 
 
simplify_conv_op( const type &rt) 
{ 
    if ( typeid( rt ) == typeid( fct ) ) 
    { 
        fct &rf = static_cast<fct &>( rt ); // 在typeid( rt ) == typeid( fct )下,可以保证static_cast的安全性 
    } 
    else { ... } 
} 
 
class Base    
{   
public:   
    virtual ~Base() {}   
};   
class Derived : public Base {};   
int main()   
{   
    Derived d;   
    Base& b = d;   
    cout << typeid(b).name() << endl;  // 输出7Derived,说明b运行时类型为Derived 
    return 0; 
}  
 
 
Base *b = new Derived;   
 
cout << boolalpha << (typeid(b) == typeid(Derived)) << endl;  // 输出false 
cout << boolalpha << (typeid(*b) == typeid(Derived)) << endl;  // 解引用,输出true 
 
// type_info 定义 
class type_info { 
public: 
    virtual ~type_info(); 
    bool operator==(const type_info& ) const; 
    bool operator!=( const type_info& ) const; 
    bool before( const type_info& ) const; 
    const char* name() const; 
private: 
    type_info( const type_info &); // copy constructor 
    type_info& operator=(const type_info&); // operator assignment 

typeid运算符输入object(指针要解引用),返回一个类型为type_info的const reference。typeid运算符能够确定两个对象是否为同种类型。它与sizeof有些相像,既可以看成类型也可以看成操作符。

在typeid( rt ) == typeid( fct )下,可以保证static_cast向下转型的安全性,不会抛出异常。

使用typeid,dynamic_cast获得对象运行时类型,必须定义虚函数。

还要注意的就是typeid作用于指针时,因为这往往是错误的.使用时应该先解引用

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

架构之路_深度探索C++对象模型总结 的相关文章

  • 高质量、高并发的实时通信架构设计与探索

    中国互联网络信息中心 CNNIC 近日发布的第 47 次 中国互联网络发展状况统计报告 显示 截至 2020 年 12 月 我国网民规模达 9 89 亿 随着社会信息化水平持续提升及电子设备加速普及 手机网民规模持续增长 基本实现对全体网民
  • 【架构设计】阿里开源架构Cola4.0的项目实践:订单系统

    项目介绍 使用SpringBoot MybaitsPlus Cola 整洁面向对象分层架构 4 0重构订单功能 项目地址 Gitee https gitee com charles ruan smile cola Github https
  • MVC三层架构

    1 什么是MVC Model View Controller 模型 视图 控制器 模型就是Java对应数据库的那些字段 实体类 视图 就是JSP页面 控制器 就是Servlet负责跳转页面 Controller作用 Controller其实
  • 微服务项目之项目简介

    目录 项目模式 技术栈 项目架构图 模块 主模块 项目模式 电商模式 市面上有5种常见的电商模式 B2B B2C C2B C2C O2O 1 B2B模式 B2B Business to Business 是指 商家与商家建立的商业关系 如
  • 2022年数字化转型的三大基于云的驱动因素

    未来一年将标志着企业品牌 工作和生活创新的最大重置 文章来源 Venture Beat Google Cloud CTO Will Grannis 数字技术一直是并将持续是公司应对新冠疫情的背后推动力 从购物和供应链到儿童保育和工作 一切都
  • 一文带你从IntelliJ IDEA中一键生成Controller、Service、Dao、Model层代码,真的不看看吗?

    前言 EasyCode插件介绍与安装 简介EasyCode是基于IntelliJ IDEA开发的代码生成插件 支持自定义任意模板 Java html js xml 只要是与数据库相关的代码都可以通过自定义模板来生成 支持数据库类型与java
  • HCIE云计算之FusionCloud 6.3部署架构

    HCIE云计算之FusionCloud 6 3部署架构 一 不同的type类型场景需求 二 Region Type 1部署方案 1 Region Type 1简介 2 Region Type 1部署私有云介绍 3 Region Type 1
  • 如果老板要求你的系统接入春晚大流量活动,你会心慌慌吗?

    目录 回头看看 原始系统技术架构 基于CDN的活动静态页面缓存方案 基于Nginx Tomcat Redis的多级缓存方案 超高并发写请求RocketMQ削峰填谷方案 系统限流防雪崩体系架构方案 今天给大家分享一个话题 就是如果要是你老板突
  • RabbitMQ集群架构模式

    搭建Mirror镜像集群 4369是erlang的发现端口 5672是rabbitmq的通信端口 15672是rabbitmq的可视化控制台的端口号 25672是erlang底层发送消息和分配消息的底层端口 firewall cmd zon
  • [架构之路-256]:目标系统 - 设计方法 - 软件工程 - 软件设计 - 架构设计 - 软件系统不同层次的复用与软件系统向越来越复杂的方向聚合

    目录 前言 一 CPU寄存器级的复用 CPU寄存器 二 指令级复用 二进制指令 三 过程级复用 汇编语言 四 函数级复用 C语言 五 对象级复用 C Java Python 六 组件级复用 七 服务级复用 八 微服务级复用 前言 物质世界的
  • 深入微服务架构 | 微服务与k8s架构解读

    微服务项目架构解读 什么是微服务 微服务是指开发一个单个小型的但有业务功能的服务 每个服务都有自己的处理和轻量通讯机制 可以部署在单个或多个服务器上 微服务也指一种种松耦合的 有一定的有界上下文的面向服务架构 也就是说 如果每个服务都要同时
  • 微服务测试是什么?

    微服务测试是一种特殊的 测试类型 因为它涉及到多个独立的服务 以下是进行微服务测试的一般性步骤 1 确定系统架构 了解微服务架构对成功测试至关重要 确定每个微服务的职责 接口 依赖项和通信方式 了解这些信息可以帮助您更好地规划测试用例和测试
  • Java构件技术

    文章目录 Java构件技术 构件及其在信息系统项目中的重要性 常见的Java构件技术和工具 JavaBeans Servlets EJB Spring Framework Spring框架
  • 计算机网络中的通信子网:架构、协议与技术简介

    在计算机网络中 通信子网是负责实现主机之间以及主机与终端之间数据传输的核心部分 它由一系列硬件设备和通信协议组成 为上层应用提供可靠 高效和透明的数据传输服务 本文将详细介绍通信子网的架构 协议与技术 一 通信子网的架构 星型拓扑 星型拓扑
  • 双非本科进不了大厂?阿里技术四面+交叉面+HR面,成功拿到offer

    前言 前两天 我收到了阿里巴巴的实习offer 从学长内推开始面试到拿到最后offer经历了4面技术 一面交叉面和一面HR面 经过了漫长的等待和几次几乎折磨的面试之后 终于拿到了实习offer 自我介绍 本人来自西南某双非本科学校 该校学的
  • 阿里P8架构师带你“一窥”大型网站架构的主要技术挑战和解决方案

    写在前面 传统的企业应用系统主要面对的技术挑战是处理复杂凌乱 千变万化的所谓业务逻辑 而大型网站主要面对的技术挑战是处理超大量的用户访问和海量的数据处理 前者的挑战来自功能性需求 后者的挑战来自非功能性需求 功能性需求也许还有 人月神话 聊
  • 谁能想到Java技术能跟看小说一样学习,用故事给技术加点料

    Java是现阶段中国互联网公司中 覆盖度最广的研发语言 掌握了Java技术体系 不管在成熟的大公司 快速发展的公司 还是创业阶段的公司 都能有立足之地 大家都知道 阿里P7高级技术专家 基本上是一线技术人能达到的最高职级 也是很多程序员追求
  • 第六章--- 实现微服务:匹配系统(下)

    0 写在前面 这一章终于完了 但是收尾工作真的好难呀QAQ 可能是我初学的缘故 有些JAVA方面的特性不是很清楚 只能依葫芦画瓢地模仿着用 特别是JAVA的注解 感觉好多但又不是很懂其中的原理 只知道要在某个时候用某个注解 我真是有够菜的
  • 微服务常见的配置中心简介

    微服务架构中 常见的配置中心包括以下几种 Spring Cloud Config Spring Cloud Config是官方推荐的配置中心解决方案 它支持将配置文件存储在Git SVN等版本控制系统中 通过提供RESTful API 各个
  • 阿里技术官亲笔力作:Kafka限量笔记,一本书助你掌握Kafka的精髓

    前言 分布式 堪称程序员江湖中的一把利器 无论面试还是职场 皆是不可或缺的技能 而Kafka 这款分布式发布订阅消息队列的璀璨明珠 其魅力之强大 无与伦比 对于Kafka的奥秘 我们仍需继续探索 要论对Kafka的熟悉程度 恐怕阿里的大佬们

随机推荐

  • 【雕爷学编程】Arduino动手做(202)---热释电效应、热释电元件与HC-SR505运动传感器模块

    37款传感器与模块的提法 在网络上广泛流传 其实Arduino能够兼容的传感器模块肯定是不止37种的 鉴于本人手头积累了一些传感器和执行器模块 依照实践出真知 一定要动手做 的理念 以学习和交流为目的 这里准备逐一动手试试多做实验 不管成功
  • vba中find方法查找1

  • QML ListView实现树形效果

    转自 http blog huati365 com 5jELjzLwnx3YGw import QtQuick 2 11 import QtQuick Controls 2 2 import QtQuick Controls Materia
  • Android抓包工具——Fiddler

    前言 在平时和其他大佬交流时 总会出现这么些话 抓个包看看就知道哪出问题了 抓流量啊 payload都在里面 这数据流怎么这么奇怪 这里出现的名词 其实都是差不多的意思啊 这都跟抓包这个词有关 说到抓包呢我们今天就先来了解一下抓包的一些基础
  • MySQL组成

    MySQL 的组成分为两部分 服务器端 服务的提供 相当于卖家 客户端 服务的使 相当于买家 消费者 服务器端只能有一个 而客户端可以有多个 安装了 MySQL 说明我们既是服务器端又是客户端 服务器端的服务体现就是 客户端是使 MySQL
  • 微软亚洲研究院实习生面试

    上周说到微软亚洲研究院 MSRA 一下就简称MSRA吧 网络面试我这边因为连不上而要重新安排面试 周五HR就给我电话约好今天中午1 00 3 00重新面试 于是提前做好各种准备 找了个拉ADSL的宿舍来上网 然后用了人家带摄像头的笔记本 还
  • B站创建视频分集播放列表

    上传视频在B站上创建视频分集列表方法 上传时创建分集列表 1 打开B站 2 登录B站后 点击投稿上传视频 3 上传视频或把视频直接拖拽到页面里 4 点击上传第一个视频后页面下会出现一个 号的按钮 点击 继续上传 上传视频就会出现两个正在上传
  • 浅谈 Node.js 热更新

    大厂技术 高级前端 Node进阶 点击上方 程序员成长指北 关注公众号 回复1 加入高级Node交流群 记得在 15 16 年那会 Node js 刚起步的时候 我在去前东家的入职面试也被问到了要如何实现 Node js 服务的热更新 其实
  • Tuple VS ValueTuple(元组类 VS 值元组)

    Tuple VS ValueTuple 元组类 VS 值元组 文章目录 Tuple VS ValueTuple 元组类 VS 值元组 Tuple 1 创建元组 2 表示一组数据 3 从方法返回多个值 4 用于单参数方法的多值传递 缺点 Va
  • 基于php的课程网站络管理系统的设计与实现

    摘 要 管理系统是根据课程网站的需求而设计和实现的 主要 用于实现课程系统办公人员对其办公系统内所有公务员进行管理 实现对员工信息的查询 录入 修改和删除 以及发布重要通知 最新信息和规章制度 通过 网上办公 无纸办公 大大提高办公效率 体
  • 电脑关机了,内存就没数据了吗?

    前言 大家好 我是周杰伦 提到网络攻击技术 你脑子里首先想到的是什么 是DDoS 是SQL注入 XSS 还是栈溢出 RCE 远程代码执行 这些最常见的网络攻击技术 基本上都是与网络 软件 代码 程序这些东西相关 这也好理解 计算机网络安全
  • Qt项目实战 杂谈一二:中文乱码事情小,处理不好头发少

    Qt开发者来说 特别是初学者 往往最头疼的是编码的问题 举个例子 1 控件上设置中文标签 发现显示出来是乱码 怎么解决 如果标签是常量字符串 含中文 怎么处理 如果标签是变量 且可能包含字符串 又咋处理 2 Qt应用与其他应用存在进程间交互
  • 浏览器无法打开网页,报错:DNS_PROBE_FINISHED_BAD_CONFIG

    症状 手机的正常上网 电脑连不上网 浏览器打不开网页 电脑 用ping命令不能发现主机 这时判断DNS解析有问题 用ping命令能发现主机 这时判断DNS解析没有问题 解决办法 方案1 ipconfig flushdns 方案2 ping
  • openwrt 软件安装依赖冲突

    今天在安装一个插件curl 安装失败了 报错内容如下 root R619AC co router tmp tmp opkg install curl Installing curl 7 68 0 1 to root Downloading
  • 使用react-markdown与markdown-navbar实现在线浏览markdown文件并自动生成侧边导航栏目录(react项目)

    使用react markdown与markdown navbar实现在线浏览markdown文件并自动生成侧边导航栏目录 react项目 在项目中需要一个需求 需要将markdown文件放在react前端项目中实现浏览器在线浏览 修改mar
  • Python爬虫框架Scrapy实例(爬取腾讯社招信息并保存为excel)

    前言 在学习python爬虫的时候 曾经爬取过腾讯社招的网站 很久很久没有写爬虫 心血来潮打算爬一个练手 想起之前爬过腾讯社招网站 打开一看网页变了 行动 重新写一遍 这个网站相对简单 做了简单测试没有设置反爬 比较适合初学者拿来练手 搜索
  • 利用webhook实现发送通知到Slack

    概要 最近办公交流应用 Slack在各团队里大行其道 非常火热 今天我们就来说说怎么用他的incoming webhook来做一些同步通知 发送通知给Slack 我们先来看看这种incoming webhook来发送通知的优势 团队成员可以
  • Vulhub Apache HTTPD 换行解析漏洞

    漏洞介绍 漏洞原理 运维人员为了解决 Apache 解析漏洞 会使用 配置 来限制匹配到的最后一个扩展名 这种方式虽然对多个扩展名的解析漏洞进行了防护 但是因为 的正则匹配规则可以将 php n 的扩展名同样可以匹配到 php 的规则 产生
  • Android使用ViewPager实现图片的轮播

    一 概述 在现在的Android项目中 首页图片轮播是随处可见的 今天我们看看如何实现 先看效果图 二 实现 先给大家看看最简单的布局文件
  • 架构之路_深度探索C++对象模型总结

    本文主要参照 深度探索C 对象模型 一书 一 关于对象 C语言中 数据和处理数据的操作 函数 是分开声明的 不支持数据函数之间的关联性 称之为程序性的 procedural 1 1 对象类型 C 中可以通过独立抽象数据类型实现 比如 cla