深度探索C++对象模型

news2024/11/24 11:28:24

文章目录

  • 前言
  • 一、关于对象
    • C++对象模型
  • 二、构造函数
      • 实例分析
    • 拷贝构造函数
    • 程序转化语意学(Program Transformation Semantics)
    • 成员初始化列表
  • 三、数据语义学(The Semantics of Data)
    • 数据存取
    • 多种继承情况讨论
      • 仅单一继承
      • 加上虚函数
      • 多重继承
      • 虚拟继承
    • Pointer to Data Members
  • 四、The Semantics of Function
    • 成员函数调用机制
    • 虚函数
    • 效率
    • 指向成员函数的指针(Pointer-to-Member Functions)
  • 深度探索指向类的数据成员的指针
  • 五、构造、析构...语义学
    • 对象构造
    • 继承体系下的对象构造
    • 对象复制语义学
    • 析构语义学
  • 六、执行期语义学 (Runtime Semantics)
    • new 和 delete 运算符
  • 七、On the Cusp of the Object Model
  • 总结


前言

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++对象模型的某些实现方式依然沿用至今。这本书依然不过时。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2229779.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TLV320AIC3104IRHBR 数据手册 一款低功耗立体声音频编解码器 立体声耳机放大器芯片麦克风

TLV320AIC3104 是一款低功耗立体声音频编解码器&#xff0c;具有立体声耳机放大器以及在单端或全差分配置下可编程的多个输入和输出。该器件包括基于寄存器的全面电源控制&#xff0c;可实现立体声 48kHz DAC 回放&#xff0c;在 3.3V 模拟电源电压下的功耗低至 14mW&#xff0…

【Rust中的序列化:Serde(一)】

Rust中的序列化&#xff1a;Serde Serde是什么&#xff1f;什么是序列化序列化&#xff1f;Serde运行机制Serde Data ModelVistor ApiSerializer ApiDeserializer Api 具体示例流程分析具体步骤&#xff1a;那么依次这个结论是如何得出的呢?什么是de? 总结 Serde是什么&#…

普通的Java程序员,需要深究源码吗?

作为Java开发者&#xff0c;面试肯定被问过多线程。对于它&#xff0c;大多数好兄弟面试前都是看看八股文背背面试题以为就OK了&#xff1b;殊不知现在的面试官都是针对一个点往深了问&#xff0c;你要是不懂其中原理&#xff0c;面试就挂了。可能你知道什么是进程什么是线程&a…

【vue项目中添加告警音频提示音】

一、前提&#xff1a; 由于浏览器限制不能自动触发音频文件播放&#xff0c;所以实现此类功能时&#xff0c;需要添加触发事件&#xff0c;举例如下&#xff1a; 1、页面添加打开告警声音开关按钮 2、首次进入页面时添加交互弹窗提示&#xff1a;是否允许播放音频 以上两种方…

2024 windos运行程序的时候弹窗:找不到ddl文件【已经解决,只要三步】修复ddl文件

一、错误复现 就是这个错误&#xff0c;网上一顿乱找&#xff0c;也解决不来&#xff0c;不是花钱就是付费就是充会员&#xff01;&#xff01; 二、ddl官网地址下载新的ddl文件&#xff08;自己缺哪个&#xff0c;搜索哪个下载&#xff09; 然灵机一动&#xff0c;ddl肯定有官…

Java并发常见面试题总结(上)

线程 ⭐️什么是线程和进程? 何为进程? 进程是程序的一次执行过程&#xff0c;是系统运行程序的基本单位&#xff0c;因此进程是动态的。系统运行一个程序即是一个进程从创建&#xff0c;运行到消亡的过程 在 Java 中&#xff0c;当我们启动 main 函数时其实就是启动了一…

分类算法——逻辑回归 详解

逻辑回归&#xff08;Logistic Regression&#xff09;是一种广泛使用的分类算法&#xff0c;特别适用于二分类问题。尽管名字中有“回归”二字&#xff0c;逻辑回归实际上是一种分类方法。下面将从底层原理、数学模型、优化方法以及源代码层面详细解析逻辑回归。 1. 基本原理 …

AutoGLM:智谱AI的创新,让手机成为你的生活全能助手

目录 引言一、AutoGLM&#xff1a;开启AI的Phone Use时代二、技术核心&#xff1a;AI从“语言理解”到“执行操作”三、实际应用案例&#xff1a;AutoGLM的智能力量1. 智能生活管理&#x1f34e;2. 社交网络的智能互动&#x1f351;3. 办公自动化&#x1f352;4. 电子商务的购物…

利用ChatGPT完成2024年MathorCup大数据挑战赛-赛道A初赛:台风预测与分析

利用ChatGPT完成2024年MathorCup大数据挑战赛-赛道A初赛&#xff1a;台风预测与分析 引言 在2024年MathorCup大数据挑战赛中&#xff0c;赛道A聚焦于气象数据分析&#xff0c;特别是台风的生成、路径预测、和降水风速特性等内容。本次比赛的任务主要是建立一个分类评价模型&…

Latex中Reference的卷号加粗的问题

找到模板中的.bst文件&#xff0c;查找volume&#xff0c;修改如下 添加bold&#xff0c;卷号会加粗&#xff0c;去掉则正常

国产光耦合器在现代应用中的作用和进步

国产光耦合器已成为各行各业必不可少的元件&#xff0c;有助于确保信号完整性、保护控制系统并提供强大的电气隔离。随着技术的进步&#xff0c;国内制造商提高了光耦合器的质量和可靠性&#xff0c;使其适用于一系列关键应用。本文探讨了国产光耦合器的优势、其应用及其对关键…

《数值分析》实验报告-线性方程组求解

文章目录 1. 实验目标2. 实验内容2.1 设计界面2.2 实现解法2.2.1 高斯消元法2.2.2 克劳斯消元法2.2.3 列主元素法 2.3 结果展示 3. 实现过程3.1 选择并设计算法3.1.1 高斯消元法3.1.2 克劳斯消元法3.1.3 列主元素法 3.2 设计 Tkinter 界面3.3 编写代码实现3.4 结果显示 4. 输入…

SpringBoot接入星火认知大模型

文章目录 准备工作整体思路接入大模型服务端和大模型连接客户端和服务端的连接测试 准备工作 到讯飞星火大模型上根据官方的提示申请tokens 申请成功后可以获得对应的secret&#xff0c;key还有之前创建的应用的appId&#xff0c;这些就是我们要用到的信息 搭建项目 整体思…

使用OpenAI控制大模型的输出(免费)——response_format

免费第三方api-key(硅基流动)使用OpenAI格式&#xff0c;还能控制大模型的输出格式&#xff0c;不能说真香&#xff0c;只能说 真香Plus&#xff01; API-Key领取方法看这篇教程 【1024送福利】硅基流动送2000万token啦&#xff01;撒花✿✿ 附使用教程 支持十几个免费的大模…

Databend 产品月报(2024年10月)

很高兴为您带来 Databend 2024 年 10 月的最新更新、新功能和改进&#xff01;我们希望这些增强功能对您有所帮助&#xff0c;并期待您的反馈。 Databend Cloud&#xff1a;多集群的计算集群 多集群的计算集群会根据工作负载需求自动调整计算资源&#xff0c;添加或移除集群。…

多线程编程与并发控制缓存策略负载均衡数据库优化

本人详解 作者:王文峰,参加过 CSDN 2020年度博客之星,《Java王大师王天师》 公众号:JAVA开发王大师,专注于天道酬勤的 Java 开发问题中国国学、传统文化和代码爱好者的程序人生,期待你的关注和支持!本人外号:神秘小峯 山峯 转载说明:务必注明来源(注明:作者:王文峰…

硅谷甄选(8)spu

Spu模块 SPU(Standard Product Unit)&#xff1a;标准化产品单元。是商品信息聚合的最小单位&#xff0c;是一组可复用、易检索的标准化信息的集合&#xff0c;该集合描述了一个产品的特性。通俗点讲&#xff0c;属性值、特性相同的商品就可以称为一个SPU。 7.1 Spu模块的静态…

【Three.js】SpriteMaterial 加载图片泛白,和原图片不一致

解决方法 如上图所示&#xff0c;整体泛白了&#xff0c;解决方法如下&#xff0c;添加 material.map.colorSpace srgb const imgTexture new THREE.TextureLoader().load(imgSrc)const material new THREE.SpriteMaterial({ map: imgTexture, transparent: true, opacity:…

【高阶数据结构】红黑树的插入

&#x1f921;博客主页&#xff1a;醉竺 &#x1f970;本文专栏&#xff1a;《高阶数据结构》 &#x1f63b;欢迎关注&#xff1a;感谢大家的点赞评论关注&#xff0c;祝您学有所成&#xff01; ✨✨&#x1f49c;&#x1f49b;想要学习更多《高阶数据结构》点击专栏链接查看&a…

CCNA对学历有要求吗?看看你是否有资格报考

思科认证网络助理工程师CCNA作为网络工程领域的权威认证之一&#xff0c;备受年轻人的青睐。然而&#xff0c;对于部分文化水平较低的年轻人来说&#xff0c;他们可能会有一个疑问&#xff1a;CCNA认证对学历有要求吗? 一、CCNA对学历有要求吗? 没有! 针对这一问题&#…