个人网站icp,自己名字怎么设计logo,深圳建设工程协会网站,江西赣州网络公司文章目录 前言一、关于对象C对象模型 二、构造函数实例分析 拷贝构造函数程序转化语意学(Program Transformation Semantics)成员初始化列表 三、数据语义学(The Semantics of Data)数据存取多种继承情况讨论仅单一继承加上虚函数多重继承虚拟继承 Pointer to Data Members 四、… 文章目录 前言一、关于对象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语法基于C95 一、关于对象
首先你得有个对象
数据结构是数据存储方式、组织结构等。 算法是指如何读取这些安一定规则组织的数据。
C相比C在内存布局及存取时间上的额外开销是由virtual引起的包括
虚函数机制虚基类
C对象模型
对象之中只存放指向members的指针 表格驱动对象模型对象中只放指向表格的指针
在GCC的实现中:
每一个class产生一堆指向虚函数的指针这些指针放在表格中此表称为虚函数表vtbl每一个含有虚函数的类对象被添加了一个指针该指针指向虚表。该指针被称为vptr。vptr的值由构造函数管理实测g11中赋值运算符不能正确赋值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) 24XYZ的大小和机器有关也和编译器有关
语言本身所造成的额外负担(overhead) 当语言支持虚继承时会在子类中产生额外的内容。在子类中这个内容指某种形式的指针身上它或指向虚基类字部分或指向一个相关表格表格中若不是虚基类的实例部分就是其偏移量(offset)编译器对于特殊情况所提供的优化处理。虚基类的1字节大小也出现在子类Y和Z身上。传统上它被放在子类的固定部分的尾端。某些编译器会对空虚基类提供特殊支持。对齐限制。
空虚基类。某些编译器提供了特殊处理在该策略下空虚基类被视为子类对象最开头的部分于是就省略的那1字节于是sizeof Y 8GCC似乎没有采用该策略
编译器之间的潜在差异正说明了C对象模型的演化。这个模型(略不重要)为一般情况提供了解决方法。当特殊情况逐渐被挖掘出来时种种启发尝试错误法于是被引入提供优化的处理。如果成功启发法于是就提升为普遍的策略并跨越各种编译器而合并。它被视为标准虽然它并不被规范为标准久而久之也就成了语言的一部分。虚函数表就是一个好例子。
总结上面代码的表现 空类编译器会为其添加1字节当出现虚继承时子类会额外添加一个指针大小另加从基类继承的1字节A则会有188 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) 4111 1(padding) 8
sizeof(Cont3) 413(padding) 13(padding) 13(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*)v3dsizeof(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*pbase2tmp?tmpsizeof(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要被删除。
为什么freedelete[] 不用指定大小应为在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
templateexception handlingruntime type identification(RRRI) 总结
本书出版自2001年虽然书中用到的标准早已盖棺定论cfront编译器也早已过时当时来看一些无法确定的标准、难以实现的技术、功能也早已实现但是对C对象模型的某些实现方式依然沿用至今。这本书依然不过时。