文章目录
- 前言
- 一、关于对象
- 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); // 调用拷贝构造,初始化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”的拷贝构造
- 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) == 1
sizeof (Y) == 9
sizeof (Z) == 9
sizeof (A) == 17
// 考虑内存对齐
sizeof (X) == 1
sizeof (Y) == 16
sizeof (Z) == 16
sizeof (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() 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
成员函数调用机制
非静态成员函数的转化步骤:
- 改写函数签名,添加额外参数到形参中,该额外参数就是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 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;时编译器所作的扩充操作大约如下:
- (如果有的话)为虚表指针设定初值
- (如果有的话)先初始化基类部分
- 如果基类被列于成员初始化列表中,那么任何明确指定的参数都应该传递过去
- 如果基类没有被列于成员初始化列表中,而它有默认构造器,就调它
- 如果基类时多重继承下的第二或后继的基类,那么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++对象模型的某些实现方式依然沿用至今。这本书依然不过时。