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

3.3 Data Member的存取
1.   不管什么情况,每一个static data member只有一个实体,放在程序的data segment之中,每次程序取用static member,不管是通过operator::还是member selection operator,都会被内部转化为对该唯一extern实体的直接参考操作。每一个static member的存取以及与class的关联不会导致任何执行时间或空间上的额外负担。如果有两个classes,每一个都声明了一个static member freeList,那么当它们都放在程序的data segment时,就会导致名称冲突,编译器的解决方法是使用name-mangling,暗中对每一个static data member编码,以获得一个独一无二的程序识别代码。

2.   有多少个编译器,就有多少种name-mangling做法,任何name-mangling做法都有两个要点:

ü          一种算法,推导出独一无二的名称;

ü          如果编译系统或者环境工具必须和使用者交谈,那些独一无二的名称可被轻易推导回原先的名称。

3.   取一个static data member的地址,会得到一个指向其数据类型的常量指针,而不是指向其class member的指针。

4.   nonstatic data members直接放在每一个class object之中,除非经过显示的explicit或隐含的implicit class object,没有办法直接存取它们。只要程序员在一个member function中直接处理一个nonstatic data member,所谓implicit class object就会发生,其实质是编译器会为这个member function增添一个const this指针,而在函数体内通过这个this指针来存取nontatic data member。

5.   欲对一个nonstatic data member进行存取操作,编译器需要把class object的起始地址加上data member的编译量offset,如地址&someObject.someMember等于&someobject + (&theClass::someMember – 1);指向data member的指针,其offset值总是会被加上1,这样可以使编译系统区分出一个指向class第一个data member的指针和一个没有指向任何data member的指针。

6.   每一个nonstatic data member的偏移量在编译时期即可获知,甚至如果member属于一个单一或多重继承体系中base class subobject也是一样,因此其存取效率和一个C struct member或一个nonderived class的member的存取效率是一样的。但是在虚拟继承的情况下就另当别论了:如果该nonstatic data member是一个virtual base class的member,并且通过指针来存取的话,在编译时期就不会得知这个member真正的offset位置,所以这个存取操作必须延迟至执行期,经由一个额外的间接导引才能够解决。

2002-7-7

3.4 “继承”与Data Member
1.   在C++继承模型中,一个derived class object所表现出来的东西,是其自己的members加上其base classes members的总和。C++并未规定derived class members和base classes members的排列次序。不过,在大部分编译器上,除virtual base class外,base class members总是先出现。

2.   一般而言,具体继承concrete inheritance并不会增加空间或存取时间上的额外负担。

3.   把两个原本独立不相干的classes凑成一对type/subtype,并带有继承关系容易犯两个错误。一是可能会重复设计一些相同操作的函数,一般而言,选择某些函数做成inline函数,是设计class的一个重要课题;二是把一个class分解为多层,有可能会为了表现class体系之抽象化,因为编译器的边界调整而膨胀所需空间。其根本原因是C++保证出现在derived class中的base class subobject有其完整原样性。

4.   C++最初问世时,许多编译器把vptr放在class object的尾端,这样可以保留base class C struct的对象布局。此后,某些编译器开始把vptr放在class object的开始处,这样会给多重继承下通过指向class members之指针调用virtual function带来一些帮助,否则,在执行期不仅必须备妥从class object起点处开始量起的offset,而且必须备妥class vptr之间的offset。

5.   单一继承提供了一种自然多态的形态,是关于class体系中base type和derived type之间的转换。一般来说,base class和derived class objects都是从相同的地址开始。但若将vptr放在class object的起始处,如果base class没有virtual function而derived class有,那么单一继承的自然多态就会打破。此时,把一个derived object转换为其base类型就需要编译器的介入,用以调整地址。而在既是多重继承又是虚拟继承的情况下,编译器的介入则更有必要。

6.   多重继承的复杂度在于derived class和其上一个base class乃至上上一个base class之间的非自然关系,其主要问题发生在derived class objects和其第二或后继的base class objects之间的转换。对一个多重派生对象,将其地址指定给最左端base class的指针,情况将和单一继承相同,而第二个或后继的base class的地址指定操作则需要修改地址,加上或减去(若是downcast)介于中间的base class subobjects的大小。C++并未要求多重继承时derived class object中各个base class subjectes的排列次序,目前各个编译器都是根据声明次序来排列它们。

7.   class内如果内含一个或多个virtual bass class subobjects,将被分割为两部分:一个不变局部和一个共享局部。不变局部总是拥有固定的offset,其数据用以指定共享局部的位置,可以直接存取;而共享局部表现的就是virtual base class subobject,其位置会因为每次的派生操作而变化,只可间接存取。各家编译器实现技术之间的差异就在于间接存取的方法不同。

8.   一般而言,virtual base class最有效的一种运用方式是:一个没有任何data member的抽象class。

2002-7-14

3.5 对象成员的效率
如果没有把优化开关打开,就很难猜测一个程序的效率表现,因为程序代码潜在性的受到某些与编译器有关的东西的影响。程序员如果关心效率,应该实际测试,不要光凭推论或常识判断或假设。优化操作并不一定总是能够有效运行。

2002-7-15

3.6 指向Data Members的指针
指向data members的指针可用来详细调查class members的底层布局,可用来决定vptr是放在class的起始处还是尾端,还可用来决定class中access sections的次序。

取一个nonstatic data member的地址,将会得到它在class的offset;而取一个static data member的地址或者取一个绑定于真正class object身上的data member的地址,将会得到该member在内存中的真正地址。这也正是someType someClass::*和someTye *潜在的区别。

2002-7-16

Function语意学 The Semantics of Function
C++支持三种类型的member functions:static、nonstatic和virtual,每一种类型的调用方式都不同。

4.1 Members的各种调用方式
1.   C++的设计准则之一便是nonstatic member function至少必须和一般的nonmember function有着相同的效率。编译器内部会将member函数实体转换为对等的nonmember函数实体,其步骤为:

ü          改写函数原型signature以安插一个额外的参数this到member function中,使得class object可以调用该函数。其中,this是const指针,若该函数为const,则反映在this上面的结果是this指向的data也为const;

ü          将每一个对nonstatic data member的存取操作改为经由this指针来存取;

ü          将member function重新写成一个外部函数,对函数名称进行mangling处理;

此后,每一个函数调用操作也都必须转换,用以提供相应的实参。

2.   关于虚拟函数的内部转换步骤:若normalize是一个virtual member function,ptr->normalize();会被内部转化为(*ptr->vptr[t])(ptr); 事实上,vptr名称也会被mangled,因为可能存在有多个vptrs;t是vitrual table slot的索引值,关联到normalize函数;第二个ptr表示this指针。

3.   使用class scope operator明确调用一个vitual function,或经由一个class object调用一个vitual function其决议方式会和nontatic member function一样!故virtual function的一个inline函数实体可被扩展开来,因而提供极大的效率利益。

4.   static member function的主要特征是没有this指针,这导致它不能直接存取其class中的nonstatic members,不能被声明为const、volatile或virtual,也不需要经由class object才能调用。static member function会被提出于class声明之外,并给予一个经过mangled的适当名称。如果取一个static member function的地址,得到的将是其在内存中的地址,其地址类型并不是一个指向class member function的指针,而是一个nonmember函数指针。static member function的一个意想不到的好处是可以成为一个callback函数,也可以成功地应用在thread函数身上。

2002-07-17

4.2 Virtual Member Functions虚拟成员函数
1.   C++中,多态polymorphism表示以一个public base class指针或reference寻址出一个derived class object。识别一个class是否支持多态,唯一适当的方法试看它是否有任何virtual function。只要class拥有一个virtual function,它就需要一份额外的执行期型别判断信息。

2.   一个class只会有一个virtual table,其中内含对应class object中所有的active virtual functions的函数实体的地址。这些active virtual functions包括:

ü          一个class定义的函数实体。它会改写overriding一个可能存在的base class virtual function。

ü          继承自base class的函数实体。此时该class不改写base class virtual function。

ü          一个pure_virtual_called()函数实体,它既可以扮演pure virtual function的空间保卫者,也可以当作执行期异常处理函数。如果该函数被调用,通常的操作是结束程序。

3.   每一个virtual function都被指派一个固定不变的索引值,该值在整个继承体系中保持与特定virtual function的关联。这样就可以在编译时期设定virtual function的调用。

2002-7-20

4.   多重继承下,一个上层basse classes数目为n的derived class,它将内含n-1个额外的virtual tables。其主要实体与最左端的base class共享,其中包含所有virtual functios的地址;n-1个次要实体与其它base classes有关,其中只包含出现在对应base class中virtual functions的地址。

5.   在多重继承中支持virtual function,其复杂度围绕在第二个及后继base class上,以及执行期this指针调整上。第二(或后继)base class会影响对virtual function支持的3种情况:

ü          通过指向第二个base class的指针,调用derived class virtual function;

ü          通过指向derived class的指针,调用第二个base class中一个继承而来的virtual function;

ü          允许virtual function函数的返回值类型有所变化,可能是base type,也可能是publicly derived type。

6.   关于执行期this指针调整比较有效率的解决方法是thunk。所谓thunk是一小端assembly码,用来以适当的offset值来调整this指针并跳到相应的virtual function。thunk技术允许virtual table slot继续内含一个简单的指针,此时多重继承将不需要任何空间上的额外负担!slots中的地址可以直接指向virtual function,也可以指向一个相关的thunk。

4.3 函数的效能
nonmember、static member和nonstatic member function在内部都会转化为完全相同的形式,三者效率相同。

2002-08-08

4.4 指向Member Function的指针
对一个nonstatic member function取址,得到的是该函数在内存中的地址;而面对一个virtual function,得到的将是一个索引值。这个值是不完整的,必须被绑定于一个class object上,才能够通过它调用函数。指向member function的指针的声明语法,以及指向member selection运算符的指针,其作用是作为this指针的空间保留者。因此,static member function的类型是函数指针,而不是指向member function的指针。

使用一个member function指针,如果并不用于virtual function、多重继承、virtual base class等情况的话,其成本并不比使用一个nonmember function指针要高。

4.5 Inline Functions
关键词inline只是一项请求。如果在某个层次上,函数的执行成本比一般的函数调用及返回机制所带来的负荷低,那么该请求被接受,编译器就用一个表达式合理地将函数扩展开来。真正的inline函数扩展操作是在函数调用的那一点上。在inline扩展期间,每一个形式参数会被对应的实际参数所取代,inline函数中的每一个局部变量都必须被放在函数调用的一个封闭区段中,并拥有一个独一无二的名称。这会带来参数的求值操作以及临时性对象的管理。

2002-08-11

构造、解构、拷贝语意学  Semantics of Construction, Destruction, and Copy
1.   一般而言,class的data member应该被初始化,而且只在constructor中或其它member functions中初始化,其它任何操作都将破坏其封装性质,使其维护和修改更加困难。

2.   可以定义并调用invoke一个pure virtual function,但它只能被静态调用,不能经由虚拟机制调用。每一个derived class destructor会被编译器加以扩展,静态调用每一个virtual base class以及上一层base class的destructor。因此,不管base class的virtual destructor是否声明为pure,它必须被定义。

5.1 无继承情况下的对象构造
C++ Standard要求编译器尽量延迟nontrivial members的实际合成操作,直到真正遇到其使用场所为止。

5.2 继承体系下的对象构造
一般而言,继承体系下编译器对constructor所作的扩充操作以及次序大约如下:

ü          所有virtual base class constructors必须从左到右、从深到浅被调用:如果class被列于member initialization list中,那么任何明确指定的参数都必须传递过去,否则如果class有一个default constructor,也应该调用它;class中的每一个virtual base class subobject的偏移量offset必须在执行期可被存取;如果class object是最底层most-derived的class,其constructors可能被调用,某些用以支持这个行为的机制必须被方进来。

ü          以base class的声明次序调用上一层base class constructors:如果base class被列于member initialization list中,那么任何明确指定的参数都必须传递过去,否则若它有default constructor或default memberwise copy constructor,那么就调用它;如果base class是多重继承下的第二或后继的base class,那么this指针必须有所调整。

ü          如果class object有virtual table pointer(s),它(们)必须被设定初值,指向适当的virtual table(s)。

ü          如果有一个member没有出现在member initialization list中,但它有default constructor,调用之。

ü          将member initialization list中的data members的初始化操作以members的声明次序放进constructor的函数本身。

2002-8-18

5.3对象复制语意学 Object Copy Semantics
1.   只有在默认行为所导致的语意不安全或者不正确以致发生别名化aliasing或者内存泄漏memory leak时,才需要设计一个copy assignment operator。否则,程序反倒会执行得较慢。

2.   如果仅仅是为了把NRV优化开关打开而提供一个copy constructor,那么就没有必要一定要提供一个copy assignment operator。

3.   copy assignment operator有一个非正交情况,那就是它缺乏一个平行于member initialization list的member assignment list。调用base class的copy assignment operator示例:

Point::operator = (p3d); 或 (*(Point*)this) = p3d; 或 (Point &)(*this) = p3d;

4.   事实上,copy assignment operator在虚拟继承情况下行为不佳,需要小心设计和说明。许多编译器甚至并不尝试取得正确的语意,它们在每一个中间的copy assignment operator中调用每一个base class instance,于是造成virtual base copy assignment operator的多个实体被调用。建议尽可能不要允许一个virtual base class的拷贝操作,并不要在任何virtual base class中声明data member。

5.5解构语意学 Semantics of Destruction
如果class没有定义destructor,那么只有在其内带的member object或base class拥有destructor时,编译器才会自动合成出一个destructor。一个由程序员定义的destructor被扩展的方式类似constructors被扩展的方式,只是顺序相反:

ü          destructor的函数本体首先被执行;

ü          如果class拥有member class objects,而后者拥有destructors,那么它们将以声明的相反顺序而调用;

ü          如果object内带一个vptr,则现在被重新设定以指向适当base class之virtual table;

ü          如果有任何直接的nonvirtual base classes拥有destructor,它们将以声明的相反顺序而调用;

ü          如果有任何virtual base classes拥有destructor,而前面讨论的这个class是most-derived class,那么它们会以原先构造顺序的相反顺序被调用。

2002-8-19

执行期语意学 Runtime Semantics
6.1对象的构造和解构
1.   一般而言,constructor和destructor的安插都如你所预期。但如果一个区段或函数中有一个以上的离开点,情况就会复杂一些,destructor会放在每一个离开点之前。通常,我们要求将object尽可能放在使用它的那个程序区附近,这样做可以节省不必要的对象产生和销毁操作。

2.   C++程序中所有的global objects都被放置在程序的data segment中,如果不明确指定初值,object所配置的内存内容将为0(C并不自动设定初值)。如果global object有constructor和destructor的话,我们说它需要静态的初始化和内存释放操作。

2002-8-20

3.   virtual base class的subobject在每个derived class中的位置可能会变动,不能在编译时期确定。以一个derived class的pointer或reference来存取virtual base class subobject,是一种nonconstant expression,必须在执行期方可评估求值。

4.   使用静态初始化的object有一些缺点。其一,无法放入try区段,任何throw操作必将触发exception handling library的默认函数terminate();其二,程序员必须为控制“需要跨越模块做静态初始化”objects的依赖顺序而产生的复杂度付出代价。建议根本就不要使用那些需要静态初始化的global objects。

5.   新的C++标准要求编译单位中的static local class objects必须在相应函数第一次被调用时才被构造,而且必须以相反的次序销毁。由于这些objects是在需要时才被构造,因此编译时期无法预期其集合和顺序。为支持新标准,可能要对被产生出来的static local class objects保持一个执行期链表。

2003-8-1

6.   对于对象数组定义,晚近的编译器一般会提供两个函数,分别用于处理没有virtual base class的class,以及内带virtual base class的class ,它们通常被称为vec_new、vec_vnew。前者类型通常为:

void* vec_new(                                                   // 初始化程序员未提供初值的连续元素

void *array,                                               // 数组起始地址若为0,则动态分配

size_t elem_size,                                       // 每一个class object的大小

int elem_count,                                         // 数组中的元素数目

void (*constructor) (void *),                    // class的default constructor指针

void (*destructor) (void *, char)               // class的destructor指针,以0填入

); 如果程序员提供带有默认参数值的default constructor,编译器要做特殊处理,以传入默认参数值!

对应销毁数组的两个函数分别为vec_delete、vec_vdelete。前者类型通常为:

void* vec_delete(

void *array,                                               // 数组起始地址

size_t elem_size,                                       // 每一个class object的大小

int elem_count,                                         // 数组中的元素数目

void (*destructor) (void *, char)               // class的destructor指针

);

6.2 new和delete运算符
         注意区分operator new和new operator!前者负责分配内存;后者先调用前者分配内存,然后调用constructor以实施初始化。

时间: 2024-10-27 02:37:48

[转]《深度探索C++对象模型》读书笔记[二]的相关文章

【C++】深度探索C++对象模型读书笔记--关于对象(Object Lessons)

前言中的内容: 1.什么是C++对象模型? 1.语言中直接支持面向对象程序设计的部分 2. 对于各种支持的底层实现机制 2. C++ class的完整virtual functions在编译时期就固定下来了,程序员没有办法再执行器动态增加或取代其中一个.这使得虚拟调用操作得以快速地派送结果,付出的成本则是执行期的弹性. 3. 全局对象在main()函数之前便完成初始化. 第一章 关于对象 1. 在C++中,有两种class data members:static 和 nonstatic,以及三种

《Android深度探索卷一》读书笔记二

开发,测试和调用Linux驱动,HAL程序库需要的工具: JDK6或以上版本: Eclipse3.4或以上版本: ADT(用于开发Android应用程序): CDT(用于开发AndroidNDK程序): Android SDK: Android NDK: 交叉编译环境: Linux内核源代码: Android源代码: 用于调试开发板的串口工具:minicom; 一.安装JDK 下载JDK压缩包并解压到任意目录,在Linux终端输入“getdit /etc/profile”打开profile文件,

【C++】深度探索C++对象模型读书笔记--Data语意学(The Semantics of data)

1. 一个空类的大小是1 byte.这是为了让这一类的两个对象得以在内存中配置独一无二的地址. 2. Nonstatic data member 放置的是“个别的class object”感兴趣的数据,static data members则放置的是“整个class”感兴趣的数据. 3. C++对象模型把nonstatic data members直接放在每一个classs object之中.对于继承而来的nonstatic data members(不管是virtual 还是nonvirtua

【C++】深度探索C++对象模型读书笔记--构造函数语义学(The Semantics of constructors)(四)

成员们的初始化队伍(member Initia 有四种情况必须使用member initialization list: 1. 当初始化一个reference member时: 2. 当初始化一个const member时: 3. 当调用一个base class的constructor,而它拥有一组参数时: 4.当调用一个member class的constructor,而它拥有一组参数时: 在这四种情况下,程序可以被正确编译运行,但是效率不高.例如: 1 class Word { 2 Stri

深度探索c++对象模型读书笔记:Data语意学-继承与Data member中内存对齐问题

书中在继承之后内存对齐问题上说道下面代码: 1 #include <bits/stdc++.h> 2 using namespace std; 3 class A 4 { 5 private: 6 int val; 7 char bit1; 8 }; 9 class B : public A 10 { 11 private: 12 char bit2; 13 }; 14 class C : public B 15 { 16 private: 17 char bit3; 18 }; 19 int

【C++】深度探索C++对象模型读书笔记--执行期语意学(Runtime Semantics)

对象的构造和析构: 全局对象 C++程序中所有的global objects都被放置在程序的data segment中.如果显式指定给它一个值,此object将以此值为初值.否则object所配置到的内容为0. 如果全局对象如果有构造函数或析构函数的话,我们说它需要静态的初始化操作和内存释放操作.编译器的执行步骤如下: 1.为每一个需要静态初始化的文件产生一个_sti()函数,内含必要的构造函数调用操作. 2. 在每一个需要静态的内存释放操作的文件中,产生一个_std()函数,内含必要的析构操作

【C++】深度探索C++对象模型读书笔记--Function(The Semantics of Function)

1. Nonstatic member function(非静态成员函数)的调用方式 编译器会将”member 函数实例“转换为对等的”nonmember函数实例“. 对于非静态成员函数 float Point3d::magnitude3d() const{...} 转换步骤如下: 1. 改写函数的signature(意指:函数原型)以安插一个额外的参数到member function中,用以提供一个存取管道,使class object得以将此函数调用.该额外参数被称为this指针: //non

Android深度探索——第九章读书笔记及心得

HAL模块 ——第9章读书笔记及心得 通过本章学习学会了Android中特有的与linux驱动交互的方法,即通过HAL模块.HAL模块本质上就是通过linux共享库(.so)与linux交互驱动,然后利用应用程序再访问linux共享库.每一个HAL共享库指定一个ID,在利用这个ID配合一定的规则找到linux共享库.知道了HAL对于Android的意义.知道了Android HAL的架构. HAL是建立在linux驱动之上的一套不属于linux内核的程序库.它属于linux内核层之上的应用层.H

Android深度探索——第八章读书笔记及心得

蜂鸣器驱动 ——第8章读书笔记及心得 通过本章的学习,学会了pwm的驱动实验,知道了蜂鸣器实现的原理.蜂鸣器是开发板上的一个硬件设备,可以通过向寄存器写入特定的值来控制蜂鸣器发出尖叫声.本实验将linux驱动区分成了多个实现文件.学会了编写Makefile文件.知道了linux驱动模块的依赖.通过实验学会了linux驱动常用的代码重用方式及强行卸载linux驱动的方法. 在C语言中编译多个源代码文件时,如果a.c使用了b.c文件中的函数,需要在a.c文件中使用extern预先定义b.c中的函数,

Android深度探索——第十章读书笔记及心得

嵌入式linux的调用技术 ——第10章读书笔记及心得 通过本章的学习了解了printk函数.该函数与printf函数类似,用于打印内核调试信息.只是前者运行在内核空间,后者运行在用户空间.即linux驱动这样的linux内核程序只能使用printk函数输出调试信息.Printk函数的原型:asmLinkKage int printk(const char *fmt,…).Printk文件是一个简单的有4个数字组成的文本文件,该文件的默认值问6 4 1 7.6代表将消息输出到控制台的级别,只有高