深度探索C++对象模型
文章目录
- 前言
- 一、关于对象
- 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++对象模型
- 语言中直接支持面向对象程序设计的部分
- 对于各种支持的底层实现机制
说白了是介绍编译器是如何支持、实现C++的面向对象机制的。如,继承、虚函数、指向class members的指针等等,编译器是如何实现的。
本书C++语法基于C++95
一、关于对象
首先你得有个对象
数据结构是数据存储方式、组织结构等。
算法是指如何读取这些安一定规则组织的数据。
C++相比C在内存布局及存取时间上的额外开销是由virtual引起的,包括:
- 虚函数机制
- 虚基类
C++对象模型
- 对象之中只存放指向members的指针
- 表格驱动对象模型,对象中只放指向表格的指针
在GCC的实现中:
- 每一个class产生一堆指向虚函数的指针,这些指针放在表格中,此表称为虚函数表vtbl
- 每一个含有虚函数的类对象被添加了一个指针,该指针指向虚表。该指针被称为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: 语意
-
如果程序员没有为类定义构造函数,则编译器会自动生成默认构造函数
如果类成员都是基本类型,则自动生成的默认构造函数什么也不做,即为随机值
如果类成员有自己的构造函数,则默认构造函数会自动调用它 -
如果程序员为类定义了构造函数,子编译器不会生成默认构造函数
-
如果程序员为类定义了构造函数,但初始化不完全
如果未初始化的成员有默认构造函数,则编译器会在程序员定义的构造函数基础上扩充其内容,将初始化成员的默认构造函数调用之 -
在有继承的情况下,与之类似,有构造函数就调用构造函数,没有就添加自动生成的默认构造函数;如果程序员定义的构造函数没有初始化全成员,则编译器视情况扩充构造函数
带有一个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); // 调用拷贝构造,初始化xX 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”的拷贝构造
- class的数据成员有一个拷贝构造函数
- class继承自一个基类,而该基类有一个拷贝构造函数
- 当class有虚函数时
- 当class派生自一个继承串链,其中有一个或多个虚继承
程序转化语意学(Program Transformation Semantics)
void foo_bar()
{X x1(x0);X x2 = x0;X x3 = X(x0);
}
必要的程序转化有两个阶段:
- 重写每一个定义,其中的初始化操作会被剥除。(这里的所谓“定义”是指上述的x1,x2,x3三行;在严谨的C++用词中,“定义”是指“占用内存”的行为)
- 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) == 1sizeof (Y) == 9sizeof (Z) == 9sizeof (A) == 17// 考虑内存对齐sizeof (X) == 1sizeof (Y) == 16sizeof (Z) == 16sizeof (A) == 24
XYZ的大小和机器有关也和编译器有关:
- 语言本身所造成的额外负担(overhead) 当语言支持虚继承时,会在子类中产生额外的内容。在子类中这个内容指某种形式的指针身上,它或指向虚基类字部分,或指向一个相关表格:表格中若不是虚基类的实例部分就是其偏移量(offset)
- 编译器对于特殊情况所提供的优化处理。虚基类的1字节大小也出现在子类Y和Z身上。传统上它被放在子类的固定部分的尾端。某些编译器会对空虚基类提供特殊支持。
- 对齐限制。
空虚基类。某些编译器提供了特殊处理,在该策略下,空虚基类被视为子类对象最开头的部分,于是就省略的那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的位置就不一定了
多种继承情况讨论
讨论多种继承情况:
- 单一继承,不含虚函数
- 单一继承,含有虚函数
- 多重继承
- 虚继承
仅单一继承
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() constsizeof(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
成员函数调用机制
非静态成员函数的转化步骤:
- 改写函数签名,添加额外参数到形参中,该额外参数就是this指针:
Point3d Point3d::magnitude(Point3d *const this); Point3d Point3d::magnitude(const Point3d *const this);
- 将每一个对非静态数据成员的存取操作改为经由this指针来存取
{ return sqrt(this->_x * this_x + this->_y * this->_y + this->_z * this->_z; }
- 将成员函数重写成一个外部函数,对函数名称进行“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身上:
- virtual destructor
- 被继承下来的Base2::mumble()
- 一组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 rbpmov rbp, rspmov QWORD PTR [rbp-32], 0mov QWORD PTR [rbp-24], 0mov DWORD PTR [rbp-32], 1mov DWORD PTR [rbp-28], 2mov DWORD PTR [rbp-24], 3mov DWORD PTR [rbp-20], 4mov 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 = &aadd rax, rdx # 将rax += rdx即 rax = &a.dmov DWORD PTR [rax], 90 # a.d = 90mov eax, 0pop 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;时编译器所作的扩充操作大约如下:
- (如果有的话)为虚表指针设定初值
- (如果有的话)先初始化基类部分
- 如果基类被列于成员初始化列表中,那么任何明确指定的参数都应该传递过去
- 如果基类没有被列于成员初始化列表中,而它有默认构造器,就调它
- 如果基类时多重继承下的第二或后继的基类,那么this指针必须有所调整
- 所有虚基类的构造函数必须被调用,从左到右,从最浅到最深
- 记录在成员初始化列表中的数据成员初始化操作会被放进构造函数本身,并以成员的声明顺序为顺序
- 如果有成员没有在初始化列表中,但它有一个默认构造器,则调用该默认构造器
虚继承
vptr初始化
在基类构造函数调用之后,但是在程序员提供的代码或是成员初始化列表之前。
琐碎的细节令人头大
对象复制语义学
一个奇怪的建议:不要在任何虚基类中声明数据。
虚基类声明为接口时最好也不要有任何数据。
析构语义学
子类的析构函数执行完成后,会自动调用父类的析构函数。
- 如果对象内带有一个vptr,那么首先重设(reset)相关的虚表
- 析构函数本身现在被执行,也就是说vptr会在程序员的代码执行浅被重设(reset)
- 如果class拥有member class objects,而后者拥有析构函数,那么它们会以其声明顺序相反的顺序被调用
- 如果有任何直接的上一层非虚基类拥有析构函数,他们会以其声明顺序相反的顺序被调用。
- 如果有任何虚基类拥有析构函数,而当前的这个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 运算符
- 通过适当的new运算符函数实体,配置所需的内存。
- 调用配置对象的构造函数
- delete与之相反
new int[5] delete[]
new:
- 调用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对象模型 二、构造函数实例分析 拷贝构造函数程序转化语意学(Program Transformation Semantics)成员初始化列表 三、数据语义学(The Semantics of Data)数据存取多种继承情况讨论仅单一继承加上虚函数多重继承虚拟继承 Pointer to Data Members 四、…...

电脑怎么设置开机密码:保障个人信息安全的第一步
在数字化时代,个人信息的安全至关重要。电脑作为我们日常工作和生活中不可或缺的设备,存储了大量的私人数据和敏感信息。为了防止未经授权的访问,设置开机密码是保护个人隐私和信息安全的基本措施之一。本文将详细介绍如何在不同操作系统下为…...

MybatisPlus入门(六)MybatisPlus-null值处理
一、MybatisPlus-null值处理 1.1)问题引入: 在查询中遇到如下情况,有部分筛选条件没有值,如商品价格有最大值和最小值,商品价格部分时候没有值。 1.2)解决办法: 步骤一:新建查询实…...

红帽认证有必要考吗?这四大人群推荐考取!
红帽认证(Red Hat Certification)作为全球公认的Linux技能认证,对于某些特定人群来说,考取这一认证无疑是一个明智的选择。本文将探讨红帽认证的必要性,并为四类人群提供考取红帽认证的建议。 1. IT专业人士 对于IT专业人士来说࿰…...

基于SSM+微信小程序的社团登录管理系统(社团1)
👉文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1 、功能描述 2、项目技术 3、开发环境 4、功能介绍 1、项目介绍 基于SSM微信小程序的社团登录管理系统实现了管理员及社团、用户。 1、管理员实现了首页、用户管理、社团管理、社团信息管理、社…...
html中cookie如何存储
在HTML中,可以使用JavaScript来创建、读取和删除cookie。以下是创建和读取cookie的基本示例: 创建cookie: function setCookie(name, value, daysToLive) { var cookie name "" encodeURIComponent(value); if (typeof daysToLive …...

C++基础三(构造函数,形参默认值,函数重载,单例模式,析构函数,内联函数,拷贝构造函数)
C有六个默认函数,分别是: 1、默认构造函数; 2、默认拷贝构造函数; 3、默认析构函数; 4、赋值运算符; 5、取址运算符; 6、取址运算符const; 构造函数 构造函数(初始化类成员变量): 1、属于类的成员函数之一 …...

Flutter Color 大调整,需适配迁移,颜色不再是 0-255,而是 0-1.0,支持更大色域
在之前的 3.10 里, Flutter 的 Impeller 在 iOS 上支持了 P3 广色域图像渲染,但是当时也仅仅是当具有广色域图像或渐变时,Impeller 才会在 iOS 上显示 P3 的广色域的颜色,而如果你使用的是 Color API,会发现使用的还是…...

如何使用VBA识别Excel中的“单元格中的图片”(2/2)
Excel 365升级了新功能,支持两种不同的插入图片方式: 放置在单元格中(Place in cell),新功能,此操作插入的图片下文中简称为单元格中的图片。放置在单元格上(Place over cell)&…...
2024系统架构师---下午题目常考概念
1.管道-过滤器的概念:管道-过滤器风格具备高内聚、低耦合、支持软件重用、扩展性好、支持并发等优点,但它有编写复杂、不适合处理交互应用等缺点。 2.隐式调用的概念:隐式调用基于事件触发的思想,具备支持软件重用,改…...

【Linux】从零开始认识五种IO模型 --- 理解五种IO模型,开始使用非阻塞IO
恐惧让你沦为囚犯, 希望让你重获自由。 --- 《肖申克的救赎》--- 五种IO模型与阻塞IO 1 前言2 五种IO模型3 非阻塞IO 1 前言 通过网络通信的学习,我们能够理解网络通信的本质是进程间通信,而进程间通信的本质就是IO。 IO就是input与outp…...
Spring Boot 集成阿里云直播点播
在当今数字化时代,视频直播和点播服务已经成为许多应用的核心功能。阿里云提供了强大的直播和点播服务,能够满足各种规模的应用需求。而 Spring Boot 作为一种流行的 Java 开发框架,能够快速构建高效的应用程序。本文将详细介绍如何在 Spring…...
舍伍德业务安全架构(Sherwood Applied Business Security Architecture, SABSA)
舍伍德业务安全架构(Sherwood Applied Business Security Architecture, SABSA)是一个企业级的安全架构框架,它提供了一个全面的方法来设计和实现信息安全策略。SABSA模型将业务需求与安全控制相结合,确保企业的信息安全措施能够支…...
论可以对抗ai编程的软件开发平台(直接把软件需求描述变成软件的抗ai开发平台)的设计
论可以对抗ai编程的软件开发平台(直接把软件需求描述变成软件的抗ai开发平台)的设计 大家知道,传统的数学密码,都可以被量子计算机破解,但是这些年发展出很多数学密码,量子计算机也破解不了,叫…...

饿了么数据库表设计
有商家表、商品表、商品规格表、购物车表,不难分析出表是不够全面的。 (1)首先分析需要补充的表 1.对于购物车而言肯定有对应的用户,因此要添加一个用户表。 2.商品规格是冷,热,半分糖、全糖,对于冷热和半分糖是可以分…...
Flink处理乱序的数据的最佳实践
目录 网络延迟和分布式系统 事件时间与处理时间的差异 事件时间和水位线(Watermark) 时间窗口(TimeWindow) 滚动窗口(Tumbling Window) 滑动窗口(Sliding Window) 会话窗口(Session Window) 自定义Watermark生成策略 设置允许延迟和侧输出 设置允许的最大延迟时间 使…...

Android OpenGL ES详解——模板Stencil
目录 一、概念 1、模板测试 2、模板缓冲 二、模板测试如何使用 1、开启和关闭模板测试 2、开启/禁止模板缓冲区写入 3、模板测试策略函数 4、更新模板缓冲 5、模板测试应用——物体轮廓 三、模板缓冲如何使用 1、创建模板缓冲 2、使用模板缓冲 3、模板缓冲应用——…...

vscode在cmake config中不知道怎么选一个工具包?select a kit
vscode在cmake config中不知道怎么选一个工具包,或者发现一直在用VS的工具包想换成自己的工具包。select a kit vscode在cmake config中不知道怎么选一个工具包,或者发现一直在用VS的工具包想换成自己的工具包。select a kit 1.在VSCode中 按ctrlshift…...

无人机之无线电监测设备技术篇
一、技术原理 无人机的无线电监测设备主要通过捕捉和分析无人机发出的无线电信号来实现对无人机的监测和定位。这些信号包括无人机的上行遥控信号、下行数据图传信号等。设备采用多种技术手段,如频谱分析、信号解调、定位算法等,对接收到的信号进行处理和…...
【系统架构设计师】预测试卷一:案例分析
更多内容请见: 备考系统架构设计师-专栏介绍和目录 文章目录 试题一(共25分)【问题 1】(12分)【问题 2】(13分)试题二(共 25分)【问题 1】(12分)【问题 2】(7分)【问题 3】(6分)试题三(共25分)【问题 1】(9分)【问题 2】(16分)试题四(共25分)【问题 1】…...
KubeSphere 容器平台高可用:环境搭建与可视化操作指南
Linux_k8s篇 欢迎来到Linux的世界,看笔记好好学多敲多打,每个人都是大神! 题目:KubeSphere 容器平台高可用:环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以? 在 Golang 的面试中,map 类型的使用是一个常见的考点,其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...
【位运算】消失的两个数字(hard)
消失的两个数字(hard) 题⽬描述:解法(位运算):Java 算法代码:更简便代码 题⽬链接:⾯试题 17.19. 消失的两个数字 题⽬描述: 给定⼀个数组,包含从 1 到 N 所有…...
在Ubuntu中设置开机自动运行(sudo)指令的指南
在Ubuntu系统中,有时需要在系统启动时自动执行某些命令,特别是需要 sudo权限的指令。为了实现这一功能,可以使用多种方法,包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法,并提供…...
Linux云原生安全:零信任架构与机密计算
Linux云原生安全:零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言:云原生安全的范式革命 随着云原生技术的普及,安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测,到2025年,零信任架构将成为超…...
Linux离线(zip方式)安装docker
目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1:修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本:CentOS 7 64位 内核版本:3.10.0 相关命令: uname -rcat /etc/os-rele…...
Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析
Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析 一、第一轮提问(基础概念问题) 1. 请解释Spring框架的核心容器是什么?它在Spring中起到什么作用? Spring框架的核心容器是IoC容器&#…...
C++.OpenGL (14/64)多光源(Multiple Lights)
多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...
Python Einops库:深度学习中的张量操作革命
Einops(爱因斯坦操作库)就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库,用类似自然语言的表达式替代了晦涩的API调用,彻底改变了深度学习工程…...

第一篇:Liunx环境下搭建PaddlePaddle 3.0基础环境(Liunx Centos8.5安装Python3.10+pip3.10)
第一篇:Liunx环境下搭建PaddlePaddle 3.0基础环境(Liunx Centos8.5安装Python3.10pip3.10) 一:前言二:安装编译依赖二:安装Python3.10三:安装PIP3.10四:安装Paddlepaddle基础框架4.1…...