深度探索C++对象模型

C++对象模型详解(整理) 一、何为C++对象模型? C++对象模型可以概括为以下2部分: 1. 语言中直接支持面向对象程序设计的部分 2. 对于各种支持的底层实现机制 语言中直接支持面向对象程序设计的部分,如构造函数、析构函数、虚函数、继承(单继承、多继承、虚继承)、多态等等第一部分这里我简单过一下,重点在底层实现机制。 阅读详情
AI摘要

该博客围绕C++对象模型展开,介绍编译器对C++面向对象机制的实现,如继承、虚函数等。详细阐述构造函数、数据语义学、函数语义学等内容,包括默认构造、拷贝构造、数据存取、成员函数调用机制等,还探讨执行期语义学,指出部分实现方式沿用至今。


前言

c++对象模型

  1. 语言中直接支持面向对象程序设计的部分
  2. 对于各种支持的底层实现机制

说白了是介绍编译器是如何支持、实现C++的面向对象机制的。如,继承、虚函数、指向class members的指针等等,编译器是如何实现的。

本书C++语法基于C++95


一、关于对象

首先你得有个对象

数据结构是数据存储方式、组织结构等。
算法是指如何读取这些安一定规则组织的数据。

C++相比C在内存布局及存取时间上的额外开销是由virtual引起的,包括:

  • 虚函数机制
  • 虚基类

C++对象模型

  1. 对象之中只存放指向members的指针
    在这里插入图片描述
  2. 表格驱动对象模型,对象中只放指向表格的指针
    在这里插入图片描述

在GCC的实现中:

  1. 每一个class产生一堆指向虚函数的指针,这些指针放在表格中,此表称为虚函数表vtbl
  2. 每一个含有虚函数的类对象被添加了一个指针,该指针指向虚表。该指针被称为vptr。vptr的值由构造函数管理(实测g++11中赋值运算符不能正确赋值vptr)。每一个类所关联的type_info object(用以支持runtime type identification, RTTI)也由虚表指出(通常在第一个位置)

在这里插入图片描述

虚继承或多重继承

class ios;
class iostream: public istream, public ostream;
class istream: virtual public ios;
class ostream: virtual public ios;

一种对象模型方式:
在这里插入图片描述
这种方式的优点是基类的改变不影响子类,但是随着继承深度的增加,对基类成员的访问变慢。

C语言动态多态:

void a()
{
    cout << "aaa \n";
}
void b()
{
    cout << "bbb \n";
}
void c()
{
    cout << "ccc \n";
}

int main()
{
    void* p;
    int a;
    cin >> a;
    switch (a)
    {
    case 1: p = (void*)&a; break;
    case 2: p = (void*)&b; break;
    case 3: p = (void*)&c; break;
    default: break;
    }
    using fn = void(*)();
    ((fn)p)();
}

"指针类型"会导致编译器如何解释某个特定地址中的内存内容及其大小,所以转型(cast)其实是一种编译器指令。大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出之内存的大小和其内容”的解释方式。

二、构造函数

The Semantics of Constructors
implicit: 隐式的
explicit: 明确的
trival: 没有用的
nontrivial: 有用的
memberwise: 对每一个成员施加
bitwise: 对每一个位施加
semantics: 语意

  1. 如果程序员没有为类定义构造函数,则编译器会自动生成默认构造函数
    如果类成员都是基本类型,则自动生成的默认构造函数什么也不做,即为随机值
    如果类成员有自己的构造函数,则默认构造函数会自动调用它

  2. 如果程序员为类定义了构造函数,子编译器不会生成默认构造函数

  3. 如果程序员为类定义了构造函数,但初始化不完全
    如果未初始化的成员有默认构造函数,则编译器会在程序员定义的构造函数基础上扩充其内容,将初始化成员的默认构造函数调用之

  4. 在有继承的情况下,与之类似,有构造函数就调用构造函数,没有就添加自动生成的默认构造函数;如果程序员定义的构造函数没有初始化全成员,则编译器视情况扩充构造函数

带有一个virtual function的class
带有虚函数的类必须要有vptr,因此编译器自动合成的默认构造函数必须正确处理它。

class Widget{
public:
	virtual void flip() = 0;
};
虚函数调用操作会有类似如下转变:
Widget widget = new XXX;
widget.flip();  <=> (* widget.vptr[1])(&widget)

C语言如何实现面向对象风格编程

实例分析

g++ (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0

拷贝构造函数

X ins1 = ins2; //调用拷贝构造
extern void foo(X x);
foo(ins1); // 调用拷贝构造,初始化x

X foo_bar()
{ X ins; return ins;} // 函数的返回值是调用拷贝构造初始化的

如何程序员没有定义拷贝构造函数,则编译器会自动生成拷贝构造函数,它会递归的调用成员的拷贝构造,如果是基本类型则直接赋值。

编译器自动生成的可分为trivial的和nontrivial的,所谓trivial起始就是没生成。

在有虚函数时:

  • 增加一个virtual function table (vtbl),内含一个有作用的virtual function的地址
  • 将一个指向virtual function table的指针(vptr),安插在每一个class object内

所以有虚函数类的编译器自动合成的默认拷贝、默认构造都是nontrivial的,它们会正确处理vptr。考虑如下代码,base类的vptr由自动合成的拷贝构造正确设置为了base的vtbl而非直接赋值

base b = derive;  // 会切割derive中不属于base的成员

处理虚继承

class Raccoon : public virtual ZooAnimal  // raccoon: 浣熊
{
  public:
    Raccoon() {}
    Raccoon(int val) {}
  //...
};
class RedPanda: public Raccoon 
{
};

Raccoon rocky;
Raccoon litte = rocky; // 拷贝构造直接调用// memcpy即可

// 简单的拷贝不可,必须将little_critter的virtual base class pointer/offset初始化
RedPanda little_red;
Raccoon little_critter = little_red;

那么下面的拷贝构造是“bitwise copy semantics”的吗
Raccoon *ptr;  ....
Raccoon little_critter = *ptr;

在这里插入图片描述

下面四种情况需要nontrivial的拷贝构造函数。
下面四种情况不展现出“bitwise copy semantics”的拷贝构造

  1. class的数据成员有一个拷贝构造函数
  2. class继承自一个基类,而该基类有一个拷贝构造函数
  3. 当class有虚函数时
  4. 当class派生自一个继承串链,其中有一个或多个虚继承

程序转化语意学(Program Transformation Semantics)

void foo_bar()
{
  X x1(x0);
  X x2 = x0;
  X x3 = X(x0);
}

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

  1. 重写每一个定义,其中的初始化操作会被剥除。(这里的所谓“定义”是指上述的x1,x2,x3三行;在严谨的C++用词中,“定义”是指“占用内存”的行为)
  2. class的拷贝构造调用操作会被安插进去
void foo_bar(){
  X x1; X x2; X x3; // 定义被重写,初始化操作被剥离
  //
  x1.X::X(x0); x2.X::X(x0); x3.X::X(x0);
}

  拷贝构造的应用,迫使编译器多多少少对程序代码做部分转化。尤其当一个函数以传值的方式传回一个对象,而该类有一个拷贝构造时,将导致程序转化。次外编译器也将拷贝构造的操作优化。

成员初始化列表

class Word {
  String _name;
  int _cnt;
public:
  Word():_name(0), _cnt(0) {}
};
=会被扩张为类似=>
Word::Word() {
  _name.String::String(0); _cnt = 0;
}
//-----------------------------------------
Word::Word() {
  _name = 0;
  _cnt = 0;
}
=会被扩张为类似=>
Word::Word() {
  _name.String::String();
  String tmp = String(0);  // 产生临时对象
  _name.String::operator=(tmp);
  tmp.String::~String();
  _cnt = 0;
}

三、数据语义学(The Semantics of Data)

#pragma pack(push, 1)
class X {};
class Y : public virtual X {};
class Z : public virtual X {};
class A : public Y, public Z {};
#pragma pack(pop)

    sizeof (X) == 1
    sizeof (Y) == 9
    sizeof (Z) == 9
    sizeof (A) == 17
   
// 考虑内存对齐
    sizeof (X) == 1
    sizeof (Y) == 16
    sizeof (Z) == 16
    sizeof (A) == 24

  XYZ的大小和机器有关也和编译器有关:

  1. 语言本身所造成的额外负担(overhead) 当语言支持虚继承时,会在子类中产生额外的内容。在子类中这个内容指某种形式的指针身上,它或指向虚基类字部分,或指向一个相关表格:表格中若不是虚基类的实例部分就是其偏移量(offset)
  2. 编译器对于特殊情况所提供的优化处理。虚基类的1字节大小也出现在子类Y和Z身上。传统上它被放在子类的固定部分的尾端。某些编译器会对空虚基类提供特殊支持。
  3. 对齐限制。

空虚基类。某些编译器提供了特殊处理,在该策略下,空虚基类被视为子类对象最开头的部分,于是就省略的那1字节,于是,sizeof Y == 8(GCC似乎没有采用该策略)

编译器之间的潜在差异正说明了C++对象模型的演化。这个模型(略,不重要)为一般情况提供了解决方法。当特殊情况逐渐被挖掘出来时,种种启发(尝试错误)法于是被引入,提供优化的处理。如果成功,启发法于是就提升为普遍的策略,并跨越各种编译器而合并。它被视为标准(虽然它并不被规范为标准),久而久之也就成了语言的一部分。虚函数表就是一个好例子。

总结上面代码的表现:
空类编译器会为其添加1字节;当出现虚继承时,子类会额外添加一个指针大小,另加从基类继承的1字节;A则会有1+8+8 = 17字节

–
编译器会在对象内部合成一些内部使用的数据成员,如虚表指针。

template <typename class_type, typename data_type1, typename data_type2>
char* access_order(data_type1 class_type::*mem1, data_type2 class_type::*mem2)
{
    assert(mem1 != mem2);
    return " ";
    // return mem1 < mem2 ? "mem1 occurs first": "mem2 occurs first";
}
int main()
{
    cout << access_order(&Point3d::x, &Point3d::y) << endl;
}

数据存取

Point3d origin, *pt = &origin;


struct base2{
    int x = 9;
};
struct derived : public base2 {
    int x = 10;
};
    base2 b;        derived d;
    base2* b2 = &d; derived* d2 = &d;
    b2->x == 9           d2->x == 10

当“Point3d是一个子类,而其继承结构中有一个虚基类,并且被存取的x是一个从该虚基类继承而来的时,会有重大差异”。

数据成员的存取是通过类对象基地址的偏移完成的,origin.x操作,x的位置在编译期就能确定,pt->x的位置就不一定了

多种继承情况讨论

讨论多种继承情况:

  1. 单一继承,不含虚函数
  2. 单一继承,含有虚函数
  3. 多重继承
  4. 虚继承

仅单一继承

struct Point1d {
    float x;
};
struct Point2d : public Point1d {
    float y;
};
struct Point3d : public Point2d {
    float z;
};

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

继承:设计的好处就是可以把管理x和y坐标的程序化代码局部化,此外该设计可以明显表现多个抽象类之间的紧密关系。

上面连个Point3d的定义在使用上和对象模型完全一样(仅是该例子)

基类对象在子类中保持原样性

struct Cont{
    int val;
    char b1;
    char b2;
    char b3;
};
struct Cont1{int val; char b1;};
struct Cont2: public Cont1 {char b2;};
struct Cont3: public Cont2 {char b3;};
// 4字节内存对齐
sizeof(Cont) == 4+1+1+1 + 1(padding) == 8
sizeof(Cont3) == 4+1+3(padding) + 1+3(padding) + 1+3(padding) == 16

在这里插入图片描述

加上虚函数

struct Point2d
{
    float x, y;
    virtual float Z() const {return 0.0;}
    virtual void Z(float) {};
    virtual void operator+=(const Point2d& rhs)
    {
        x += rhs.x;
        y += rhs.y;
    }
};
struct Point3d: public Point2d
{
    float z;
    float Z() const override {return z;}
    void Z(float new_z) override { z = new_z;};
    void operator+=(const Point2d& rhs) override  // good
    {
        Point2d::operator+=(rhs);
        z += rhs.Z();  // rhs是一个const,必须调用 Z() const
    }
};

void foo(Point2d& p1, Point2d &p2) {
  //... 
  p1 += p2; 
}

其中p1和p2可能是2d也可能是3d坐标点,这样的弹性正是面向对象程序设计的中心。支持这样的弹性,必然给Point2d类带来空间和存取时间的额外负担:

  • 导入一个和Point2d有关的虚表,用以存放它所声明的每一个虚函数的地址和一些slots(用以支持runtime type identification, RTTI)
  • 在每一个类对象中导入一个vptr,提供执行期的链接,使每一个对象都能找到对应的虚表
  • 加强所有的构造函数,使它能够为vptr设定初值
  • 加强析构函数,使它能够析构vptr。析构的顺序是与构造反向的

 
多了vptr后就出现了个问题,把它放在哪?
把它放在类对象的尾端可以保留C的对象布局。
把它放在对象前端对于“在多重继承之下,通过指向类成员的指针调用虚函数”,会带来一些帮助。

多重继承

struct Vertex {
    Vertex* next;
};
struct Vertex3d: public Point3d, public Vertex {
    float mumble;
};

+---------------------------+
| Point3d | Vertex | mumble |
+---------------------------+
Vertex3d v3d; Vertex3d *pv3d;
Vertex   *pv;
Point2d *p2d;
Point3d *p3d;
pv = &v3d;  // 需要转化 pv = (Vertex*)((char*)(&v3d)+sizeof(Point3d))
p2d = &v3d; //无需转换
p3d = &v3d;
// 需要转换
pv = pv3d; // pv = pv3d ? (Vertex*)((char*)v3d+sizeof(Point3d)) : 0;

虚拟继承

struct Point2d
{
    // float x, y;
    long long int d2;
    virtual float Z() const {return 0.0;}
    virtual void Z(float) {};
    virtual void operator+=(const Point2d& rhs)
    {
        // x += rhs.x;
        // y += rhs.y;
    }
};
struct Point3d: public virtual Point2d
{
    // float z;
    long long int z;
    float Z() const override {return 0;}
    void Z(float new_z) override {};
    void operator+=(const Point2d& rhs) override
    {
        // Point2d::operator+=(rhs);
        // z += rhs.Z();  // rhs是一个const,必须调用 Z() const
        sizeof(Point3d);
    }
};
struct Vertex: public virtual Point2d {
    Vertex* next;
    // virtual void fun() {}
};
struct Vertex3d: public Point3d, public Vertex {
    long long int mumble;
    virtual void fun() {}
};

下图仅供参考,用以说明编译器对原对象进行了增添操作,和C struct完全不兼容,memset,memcpy等操作变得不可用。
在这里插入图片描述

Pointer to Data Members

&Point3d::z 的到的是z的偏移量,它的类型为 float Point3d::* 😗

float Point3d::*p1 = 0;
float Point3d::*p2 = &Point3d::x;

float *ax = &pA.x;
*bx = *ax - *bz;
*by = *ay - *bx;
*bz = *az - *by;

float Point3d::*ax = &Point3d::x;
pB.*bx = pA.*ax - pB.*bz;
pB.*by = pA.*ay - pB.*bx;
pB.*bz = pA.*az - pB.*by;

四、The Semantics of Function

成员函数调用机制

  非静态成员函数的转化步骤:

  1. 改写函数签名,添加额外参数到形参中,该额外参数就是this指针:
    Point3d Point3d::magnitude(Point3d *const this);
    Point3d Point3d::magnitude(const Point3d *const this);
    
  2. 将每一个对非静态数据成员的存取操作改为经由this指针来存取
    { return sqrt(this->_x * this_x + this->_y * this->_y + this->_z * this->_z; }
    
  3. 将成员函数重写成一个外部函数,对函数名称进行“mangling”处理,使她在程序中独一无二extern magnitude__7Point3dFv(register Point3d *const this
    // 转换调用操作
    obj.magnitude()  => magnitude__7Point3dFv(&obj)
    ptr->magnitude() => magnitude__7Point3dFv(ptr)
    

  名称的特殊处理( Name Mangling ):
函数签名 signature = 函数名+形参列表类型。符号还会根据实际情况添加命名空间、类名等

  虚函数
虚函数是某种形式上的函数指针。

如果normalize()是一个虚函数

ptr->normalize() => (* ptr->vptr[1])(ptr)
vptr是虚表指针
1是虚表slot的索引值,关联到normalize()
第二个ptr是形参this的实参

obj.normalize() => normalize_7Point3dFv( &obj )
// ( * obj.vptr[1])(&obj) 语义正确,但是没必要,效率也更低

  静态成员函数
静态成员函数没有this指针

obj.normalize() => normalize__7Point3dFv()
ptr->normalize() => normalize__7Point3dFv()

// 怪异写法  ((Point3d*)0)->object_count();
  • 不能直接存取类中的非静态成员
  • 不能够声明为const, volatile, virtual
  • 不需要经由对象调用

虚函数

只有声明虚函数时才会有多态特性。

每一个虚函数都被指派一个固定的索引值,该索引在整个继承体系中保持与特定的虚函数的关联。

单一继承时虚函数表布局:
在这里插入图片描述
其中 pure_virtual_called() 被称为纯虚函数,相当与他只有函数声明,没有定义主要用来占slot用的

ptr->z() => (* ptr->vptr[4])(ptr)

多重继承下的虚函数

class Base1{
public:
  Base1();
  virtual ~Base1();
  virtual void speakClearly();
  virtual Base1* clone() const;
protected:
  float data_base1;
};
class Base2{
public:
  Base2();
  virtual ~Base2();
  virtual void mumble();
  virtual Base2* clone() const;
protected:
  float data_base2;
};
class Derived : public Base1, public Base2{
public:
  Derived();
  virtual ~Derived();
  virtual Derived* clone() const;  // 返回值可为Derive类(只有该种例外情况)
protected:
  float data_derived;
};

难点:统统落在Base2 subobject身上:

  1. virtual destructor
  2. 被继承下来的Base2::mumble()
  3. 一组clone()函数实体

如下操作:

Base2* pbase2 = new Derived;

新的Derived对象的地址必须调整,以指向其Base2 subobject。编译时期会产生以下代码:
// 转移以支持第二个base class Derived*tmp = new Derived; Base2*pbase2=tmp?tmp+sizeof(Base1):0
当要删除pbase2所指的对象时:
//必须首先调用正确的virtual destructor函数实体
//然后施行delete运算符
//pbase2可能需要调整,以指出完整对象的起始点
delete pbase2;

上述offset加法不能在编译期直接设定,因为pbase2所指的真正对象只有在执行期才能确定。

thunk技术可解决上述delete pbase2的问题:
pbase2_dtor_thunk:
  this -= sizeof(base1); // 这里应该是-才对
  Derived::~Derived(this);

在这里插入图片描述
为了连接时的效率,多个虚表会连锁为一个,每一个class只有一个具名的虚表。

虚继承下的虚函数
太过复杂,不予讨论

效率

普通函数 > 虚函数 > 多重继承下的虚函数 > 虚拟继承下的虚函数

指向成员函数的指针(Pointer-to-Member Functions)

非静态数据成员的地址本质是个偏移量,它必须依赖具体的对象。

double (Point::* pmf)();
double (Point::* coord)() = &Point::x;

(origin.*coord)();  (ptr->*coord)();

=>伪代码
    (coord)(&origin); (coord)(ptr);

指向虚成员函数的指针
依然表现出动态多态特性

float (Point::*pmf)() = &Point::z;
Point *ptr = new Point3d;

(ptr->*pmf)();  若为虚函数 =>  (* ptr->vptr[(int)pmf])(ptr);

若Point::z不是虚函数则得到函数地址,若Point::z是虚函数则得到它在虚表中的索引

多重继承下的,指向成员函数的指针
一种可能的实现方式是将指向成员数的指针翻译为以下结构体:

struct __mtpr{
  int delta; //this 指针的偏移
  int index; // < 0 代表指向非虚函数
  union {
    ptrtofunc faddr;  // 函数地址
    int       V_offset;  // 虚函数在虚表中的偏移量
  };
};

深度探索指向类的数据成员的指针

指向类的数据成员的指针的值为成员在类中的偏移量
指向类的 非虚函数 的指针 的值是该函数的地址
指向类的 虚函数 的指针 的值是在虚表中的偏移量

struct test
{
    int a;
    int b;
    int c;
    int d;
};

int main()
{
    test a = {};
    a.a = 1;
    a.b = 2;
    a.c = 3;
    a.d = 4;
    int test::* x = &test::d;  // x的值为d的在结构体中的偏移量
    a.*x = 90;
}

main:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-32], 0
        mov     QWORD PTR [rbp-24], 0
        mov     DWORD PTR [rbp-32], 1
        mov     DWORD PTR [rbp-28], 2
        mov     DWORD PTR [rbp-24], 3
        mov     DWORD PTR [rbp-20], 4
        mov     QWORD PTR [rbp-8], 12  ## 对应int test::* x = &test::d;
        mov     rax, QWORD PTR [rbp-8]  # 将rbp-8地址处的值给rax寄存器
        lea     rdx, [rbp-32]           # 将rbp-32地址给rdx  rdx = &a
        add     rax, rdx                # 将rax += rdx即 rax = &a.d
        mov     DWORD PTR [rax], 90     # a.d = 90
        mov     eax, 0
        pop     rbp                     # 从栈中弹出数据
        ret

五、构造、析构…语义学

(Semantics of Construction Destruction Copy)

子类的析构函数会被编译器加以扩展,以静态调用的方式调用每一个虚基类以及上一层基类的析构函数。

Derived::~Derived()
{
  ...
  Base::interface();  // 静态调用
  Base::~Base();
}

笔者建议:不要把虚析构函数声明为纯虚的;以下的基类可以作为接口存在

class Base{
public:
  virtual ~Base() = default;
  virtual void interface() = 0;
  virtual void interface1() = 0;
protected:
  // 将公共数据成员提取出来作为虚基类的成员似乎也说得通
  // 这里只有接口,没有数据
};

合适的声明

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

对象构造

typedef struct {
  float x, y, z;
} Point;
会有一个 Plain Ol' Data 卷标,语义上会有默认的构造、析构...等函数,但实际的行为与C无异,
这些函数都是trivial的

未初始化的全局对象由于历史的原因,会被放在称为BSS(Block Started by Symbol)的空间

 
下面的对象和C也是兼容的

class Point {
public:
  Point(float x = 0.0, float y = 0.0)
    : _x(x), _y(y) {}
protected:
  float _x, _y;
};

 
下面的拷贝构造函数会处理虚表指针,已经抛弃了和C的兼容

class Point {
public:
  Point(float x = 0.0, float y = 0.0)
    : _x(x), _y(y) {}
  ...
  virtual float z();
protected:
  float _x, _y;
};

继承体系下的对象构造

当定义对象T object;时编译器所作的扩充操作大约如下:

  1. (如果有的话)为虚表指针设定初值
  2. (如果有的话)先初始化基类部分
    • 如果基类被列于成员初始化列表中,那么任何明确指定的参数都应该传递过去
    • 如果基类没有被列于成员初始化列表中,而它有默认构造器,就调它
    • 如果基类时多重继承下的第二或后继的基类,那么this指针必须有所调整
  3. 所有虚基类的构造函数必须被调用,从左到右,从最浅到最深
  4. 记录在成员初始化列表中的数据成员初始化操作会被放进构造函数本身,并以成员的声明顺序为顺序
  5. 如果有成员没有在初始化列表中,但它有一个默认构造器,则调用该默认构造器

虚继承

vptr初始化
在基类构造函数调用之后,但是在程序员提供的代码或是成员初始化列表之前。
琐碎的细节令人头大

对象复制语义学

一个奇怪的建议:不要在任何虚基类中声明数据。
虚基类声明为接口时最好也不要有任何数据。

析构语义学

子类的析构函数执行完成后,会自动调用父类的析构函数。

  1. 如果对象内带有一个vptr,那么首先重设(reset)相关的虚表
  2. 析构函数本身现在被执行,也就是说vptr会在程序员的代码执行浅被重设(reset)
  3. 如果class拥有member class objects,而后者拥有析构函数,那么它们会以其声明顺序相反的顺序被调用
  4. 如果有任何直接的上一层非虚基类拥有析构函数,他们会以其声明顺序相反的顺序被调用。
  5. 如果有任何虚基类拥有析构函数,而当前的这个class是最尾端的class,那么它们会以其原来的构造顺序相反的顺序被调用

一个对象的声明周期开始于构造函数执行之后,结束于析构函数调用之前。在构造、析构函数执行期间该对象都不是完整的。

六、执行期语义学 (Runtime Semantics)

System V COFF (Common Object File Format)

Executable and Linking Format (ELF)

.init .fini两个section,分别对应于静态初始化和释放操作。
所谓section就是16位时代所说的segment,例如code segment或data segment等等。System V的COFF格式对不同目的的sections(放置不同的信息)给予不同的命名,如.text section, .idata section, .edata section, .src等等。每一个section名称都以字符“.”开头。

extern int i;
// 旧版本的c++编译器全部要求静态初始化,这些都是不合法的
int j = i;
int *pi = new int(i);
double sal = compnte_sal(get_employee(i));

局部静态对象只有在用到时才会初始化。

new 和 delete 运算符

  1. 通过适当的new运算符函数实体,配置所需的内存。
  2. 调用配置对象的构造函数
  3. delete与之相反

new int[5] delete[]

new:

  1. 调用void* operator new(std::size_t size);函数分配空间, 该函数一般用malloc实现
int* pi = new (std::nothrow) int;
int* pi2 = new(pi) int(5);
printf("pi: %p, p2: %p, %d, %d\n", pi, pi2, *pi, *pi2);

寻找数组维度给delete运算符的效率带来极大的影响,只有在括号中出现时,编译器才寻找数组的维度,否则便假设只有单独一个objects要被删除。

为什么free,delete[] 不用指定大小,应为在malloc, new[] 时同时为该指针分配了cookie以存储这些信息

考虑下面的问题:

struct Base { int j; virtual void f(); };
struct Derived : public Base { void f();};
void fooBar() {
  Base b;
  b.f();
  b.~Base();
  new (&b) Derived;
  b.f();  // 哪一个f被调用
}

动态多态只在指针或者引用时生效,对于对象的.操作不生效

测试结果发现,FORTRAN-77的代码快达两倍。他们的第一个假设是把责任归咎于临时对象。为了验证,他们以手工方式把cfront中介输出码中的所有临时对象一一消除。——如预期,效率增加了两倍,几乎和FORTRAN-77相当。

七、On the Cusp of the Object Model

  • template
  • exception handling
  • runtime type identification(RRRI)

总结

本书出版自2001年,虽然书中用到的标准早已盖棺定论,cfront编译器也早已过时,当时来看一些无法确定的标准、难以实现的技术、功能也早已实现,但是对C++对象模型的某些实现方式依然沿用至今。这本书依然不过时。

深度探索C++对象模型笔记 一、关于对象C 语言是程序性的,语言本身并没有支持数据和函数之间的关联性C++ 中可能采取抽象数据类型,或者是多层次的类结构完成C++ 的封装并没有增加多少成本,每一个成员函数虽然在cla... 阅读详情

相关推荐

《深度探索C++对象模型》中文电子版概述

在C++中,我们通过关键字class定义一个类,然后可以创建该类型的实例,也就是对象。例如:public:return 0;在这个例子中,Person类定义了一个成员函数sayHello,然后我们创建了一个Person类型的对象person,并通过它调用sayHello函数。异常安全通常分为三个等级:基本保证、强保证和不抛出保证。基本保证要求在异常发生时,对象处于有效状态但可能与操作前不同。强保证要求如果操作失败,对象应保持操作前的状态不变。

weixin_42581846的博客 925

深度探索C++ 对象模型

深度探索C++ 对象模型 第二章 对象 第二章第一节 类对象所占用的空间 成员函数不占用类对象的内存空间一个类对象至少占用1个字节的内存空间成员变量是占用对象的内存空间成员函数 每个类只诞生 一个(跟着类走),而不管你用这个类产生了多少个该类的对象; 第二章第二节 对象结构的发

weixin_42252757的博客 1173

深度探索C++对象模型 超高清pdf

深度探索C++对象模型 超高清

c++对象模型

一、什么是c++对象模型 语言中直接支持面向对象程序设计的部分。 对于各种支持的底层实现机制。 二、c++对象的布局成本 成员函数不占用成本 member functions虽然再class的声明之内,却不在object之中。每一个non-inline member function只会诞生一个函数实例。 每一个“拥有零个或一个定义”的inline function则会在其每一个使用者(模块)身上产生一个函数实例。 c++在布局以及存取时间上主要的额外负担是由virtual引起的 virtual

gb173652770的专栏 2271

SAP数据导入避坑指南:财务期初上线实操全流程(含常见错误排查)

本文详细解析了SAP财务期初数据导入的全流程,包括数据准备、主数据标准化、余额与未清项导入等关键步骤,并提供了常见错误排查和解决方案。特别强调了过渡科目和存货价值的处理技巧,帮助企业在SAP系统上线时避免数据导入陷阱,确保财务数据的准确性和系统平稳运行。

weixin_30832143的博客 396

HDLBits答案汇总

前言 该博客为本人做HDLBits习题时的心得记录总结,欢迎大家一起交流进步。 HDLBits网站链接 Verilog Language Basics Vectors Modules:Hierarchy Procedures More Verilog Features Circuits Combinational Logic Basic gates Multiplexers Arithmetic Circuits Karnaugh Map to Circuit Sequential Logic Latche

谦豫 9万+

《深度探索C++对象模型》阅读笔记(完整版)

《深度探索C++对象模型》完整阅读笔记

2401_82413779的博客 1903

从论文到生产:iSLIP优先级匹配算法在SDN交换机中的20年演进史

本文回顾了iSLIP优先级匹配算法在SDN交换机中的20年演进历程,从斯坦福大学的原始论文到现代芯片实现。通过独特的指针滑动和迭代循环机制,iSLIP算法显著提升了交换机的吞吐量和公平性,解决了传统FIFO调度的队头阻塞问题。文章详细探讨了算法在硬件实现中的工程挑战、开源实践及未来AI时代的智能调度方向。

weixin_29230649的博客 331

深度探索C++对象模型之美

从概念来上来讲,每一个没有定义构造函数的类都会由编译器来合成一个默认构造函数,以使得可以定义一个该类的对象,但是默认构造函数是否真的会被合成,将视是否有需要而定。C++ standard 将合成的默认构造函数分为 trivial(不重要) 和 notrivial(重要) 两种,前文所述的四种情况对应于notrivial默认构造函数,其它情况都属于trivial。对于一个trivial默认构造函数,编译器的态度是,既然它全无用处,干脆就不合成它。

谢谢大家的关注 3494

告别Keil4!手把手教你用Keil C51 V9.61编译51单片机代码(附最新激活方法)

本文详细介绍了Keil C51 V9.61在51单片机开发中的全栈升级指南,包括编译速度提升、调试器性能优化和现代开发支持等核心优势。通过智能安装方案、项目迁移实战和新特性深度应用,帮助开发者告别Keil4,大幅提升开发效率。附有最新激活方法和实用技巧,适用于嵌入式开发工程师。

weixin_26886855的博客 420

RAD Studio 12安装全攻略:从下载到破解InterBase的完整流程(附KeyPatch使用技巧)

本文提供RAD Studio 12的完整安装指南,包括系统要求、安装步骤及InterBase数据库的高效配置方法。详细介绍了如何使用KeyPatch工具进行许可证处理,并分享了IDE优化和开发技巧,帮助开发者快速上手这一强大的跨平台开发工具。

q6r7s8t9的博客 948

【亲测免费】 深度挖掘C++奥秘:《深度探索C++对象模型(清晰版)》—— 开启你的专家之路

深度挖掘C++奥秘:《深度探索C++对象模型(清晰版)》—— 开启你的专家之路 【下载地址】深度探索C对象模型清晰版资源下载分享 深度探索C++对象模型(清晰版)资源下载 项目地址: https://gitcode.com/Ope...

gitblog_06646的博客 752

DOA估计算法从原理到仿真——CBF、Capon、MUSIC算法

从原理出发理解并实现经典的DOA估计算法

m0_55845774的博客 1万+

【免费下载】 深度探索C++对象模型 PDF

深度探索C++对象模型 PDF 【下载地址】深度探索C对象模型PDF 深度探索C++对象模型 PDF欢迎来到《深度探索C++对象模型》PDF资源页面 项目地址: https://gitcode.com/open-source-to...

gitblog_09794的博客 784

CAN收发器选型避坑指南:TJA1051T与TJA1051T/3的硬件兼容性问题实录

本文详细解析了CAN收发器TJA1051T与TJA1051T/3的硬件兼容性问题,通过实测数据和故障案例揭示了VIO引脚的关键差异及其对系统的影响。提供从选型到验证的完整防坑方案,帮助开发者避免常见设计错误,确保CAN总线通信的稳定性。

weixin_42715608的博客 457

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

《深度探索C++对象模型》笔记目录

读书笔记,特此记录。 😇😇😇 《深度探索C++对象模型》阅读笔记 第一章 关于对象 《深度探索C++对象模型》阅读笔记 第二章 构造函数语意学 《深度探索C++对象模型》阅读笔记 第三章 Data语意学 《深度探索C++对象模型》阅读笔记 第四章 Function语意学 《深度探索C++对象模型》阅读笔记 第五章 构造、解构、拷贝语意学 《深度探索C++对象模型》阅读笔记 第六章 执行期语意学 《深度探索C++对象模型》阅读笔记 第七章 站在对象模型的尖端...

哇咔咔的博客 364

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

《深度探索C++对象模型》存疑

《深度探索C++对象模型》是Standley B.Lippman大师的大作,中文版是侯捷老师翻译的,在译作中侯老师的质量是没得说。三年前对想拜读此书,由于种种原因未能如愿。最近在胡乱看完几本英文书籍之后(英文不好,看着太累)终于决定看几本中文书籍放松一下。        比起英文版的书,看中文版的书真是惬意啊。之前有比较长时间的C++的经验,所以书前几章看着非常轻松,基本上都是已经知道的东西,不过

快乐菜鸟 1580

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

深度探索cpp对象模型的好博客

http%3A//www.roading.org/archives/

ptgood的博客 195

【免费下载】 深度探索C++对象模型 超高清PDF资源下载

深度探索C++对象模型 超高清PDF资源下载 【下载地址】深度探索C对象模型超高清PDF资源下载 - **文件名**: 深度探索C++对象模型 超高清pdf- **文件格式**: PDF- **文件大小**: [文件大小]- **分辨率**: 超高清 ...

gitblog_06684的博客 1955

《深度探索C++对象模型》读书笔记

 Inside The C++ Object ModelStanley B. Lippman 屋檐下的水滴--读书笔记系列http://blog.youkuaiyun.com/dwater 《深度探索C++对象模型》读书笔记                                                                          

DWATER 6126

深度探索C++对象模型—01关于对象

一本好书:《深度探索C++对象模型》, Stanley B. Lippman 著,侯捷 译 在C语言中,“数据”和“处理数据的操作(函数)”是分开声明的,这种程序方法称为“程序性的”。C++与C在风格上和思考上都有显著的不同,C精瘦简易,C++复杂,很难说明两者孰优孰劣。 加上封装后的布局成本 C++在布局以及存取时间上主要的额外负担是由virtual引起的。包括: virtual fun...

雨纷飞 358

深度探索C++对象模型(2012版本)

深度探索C++对象模型(2012版本) 【下载地址】深度探索C对象模型2012版本 深度探索C++对象模型(2012版本)欢迎来到《深度探索C++对象模型》(2012年版)的Git仓库页面 项目地址: https://gitcod...

gitblog_09768的博客 402

[读书笔记]《深度探索C++对象模型》

文章目录前言第一章 关于对象第二章 构造函数语意学构造函数拷贝构造函数初始化列表第三章 Data 语意学第四章 Function 语意学非静态成员函数静态成员函数虚成员函数第五章 构造、析构、拷贝语意学RTTI模板函数的实现逻辑 前言 俗话说得好,好记性不如烂笔头,本文主要是重温了一下经典书籍《深度探索C++对象模型》,然后一些内容摘录,主要记录的是我自己的精简版,建议读者有相关的知识需要还是慢慢去啃一下原书比较好。 第一章 关于对象 首先介绍了 C++ 类的情况, C++类的内存存储方式是:只包含非静态成

Chris_zhangrx的博客 1482

深度探索c++对象模型

大家好,本人准备推出深度探索c++对象模型的深入浅出的剖析!希望大家关注!

qq810277189的专栏 539
上一篇: 国密算法SM4实现
下一篇: 一种程序结构设计json,多线程,避免数据竞争
耶耶耶耶耶~
博客等级 码龄9年 943粉丝 153原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值