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

https://www.cnblogs.com/wickedpriest/p/6527181.html

https://www.cnblogs.com/wickedpriest/p/6580134.html

前言

今年中下旬就要找工作了,我计划从现在就开始准备一些面试中会问到的基础知识,包括C++、操作系统、计算机网络、算法和数据结构等。C++就先从这本《深度探索C++对象模型》开始。不同于《Effective C++》,这本书主要着眼于C++实现的底层机制,因此我在写这个系列时默认读者已经熟悉C++的基本语法(包括类、继承、多态、泛型等等),将更多地介绍C++具体是如何实现这些语法的。这次我就先写第一、二章,之后每读两章都会更新该系列。如果你有什么问题,欢迎在博客的评论版块和我探讨,或者联系我的邮箱:[email protected],很乐意与你互相切磋、提高。

第一章:关于对象

 在传统的C程序中,采用的是过程式的思维:“数据”和“处理数据的操作(函数)”是分开来声明的,它们二者之间并没有关联性。但到了C++里,倾向于采用独立的“抽象数据类型”(ADT)来实现数据的封装。从代码结构上来看,C++似乎比C要消耗更高的时间和空间成本,但事实未必如此。相比于C,C++在布局和时间上主要的额外负担是由virtual引起的,这分为两部分:

1.virtual function机制 用以支持一个有效率的“执行期绑定”。

2.virtual base class 用以实现“多次出现在继承体系中的base class,有一个单一而被共享的实例”。

接下来,书里介绍了三种的C++对象模式:简单对象模型、表格驱动对象模型、C++对象模型,其中第三种最为编译器所常用,因此在这里主要介绍第三种。

C++对象模型中,非nonstatic data members被配置于每一个class object之内,static data members、function members则被存放在class object之外。编译器为每一个class产生一个表格,表格里是一堆指向virtual functions的指针,该表格我们称之为virtual table(vtbl);每一个class object被安插一个指针,指向相关的virtual table,这个指针被称为vptr。vptr的设定和重置由constructor、destructor以及copy assignment运算符自动完成。每一个class所关联的type_info object也存放在virtual table之中。

该模型的优点是,它的空间和时间的效率较高;缺点在于,如果应用程序本身的代码未改变,而所用到的class object的nonstatic data members有所修改,那么应用程序的代码同样需要重新编译(因为内存分布发生了改变)。

在接下来的篇幅里,书中探讨了struct和class的区别,对于这个问题可以参考这篇博文:http://blog.csdn.net/omegayy/article/details/7470316。其实struct和class从语法本质上差别并不大,无非是二者的默认继承和默认成员访问级别不同。但从一般来说,我们习惯用struct来代表一些简单数据的集合,用class来代表更为复杂的封装、继承的数据。

扫描二维码关注公众号,回复: 2953593 查看本文章

与C不同,在C++中,对象的内存分布未必和类中成员的声明顺序一致,比如C++的编译器可能会将protected data members放在private data members前面存储,与声明顺序无关;同理,base classes和derived classes的data members的布局也未有谁先谁后的强制规定。但类中处于同一个access section中的数据,其内存分布顺序是按照声明顺序排列的,只是不同access section之间未必按声明顺序排列。 

 之后书中花了一些篇幅介绍多态的机制,我觉得熟悉C++的人应当对此并不难理解,在这里不再赘述。

第二章:构造函数语义学

本章主要介绍了C++中构造函数的生成机制。

首先是default constructor,C++的编译器真的会为每一个没有声明构造函数的类生成default constructor吗?其实并不是的。书中指出,只有四种情况,会造成“编译器必须为未声明constructor的classes合成一个default constructor”:

1.member constructor有defualt constructor;

2.base class具有default constructor;

3.该类具有virtual funciton;

4.该类具有一个virtual base class。

在合成的defualt constructor中,只有base class subobjcets和member class objects会被初始化,而所有其他的nonstatic data member(int、int*、char等类型)都不会被初始化。

之后是copy constructor。与defualt constructor一样,编译器并不是为所有无用户定义copy constructor的类都创建copy constructor,如上的四种情况下,编译器才会为class合成copy constructor;否则编译器会执行bitwise copy。

接下来书中介绍了NRV优化以及member initialization list。

对于NRV(Named Return Value)优化,我们可以看一个例子:

假设有一个foo函数:

1

2

3

4

5

6

7

8

X foo()

{

    X xx;

    if(...)

        return xx;

    else

        return xx;

}

经过编译器的优化后,代码改成如下样子:

1

2

3

4

5

6

7

8

9

10

11

12

void  foo(X &result)

{

    result.X::X();

    if(...)

    {

        return;

    }

    else

    {

        return;

    }

}

这样就省去了临时对象的默认构造函数、拷贝构造函数、析构函数的成本,从而有助于减少运行时间。另外,有一点要注意的是,NRV优化,有可能带来程序员并不想要的结果,比如当你的类依赖于构造函数或者拷贝构造函数的调用次数时。
 

关于member initialization list,我们首先需要知道需要它的时机:

1.当初始化一个reference member时;

2.当初始化一个const member时;

3.当调用一个base class的constructor,而它拥有一组参数时;

4.当调用一个member class的constructor,而它拥有一组参数时。

接下来需要注意的一点就是:list中的初始化顺序是由class中member的声明顺序决定的,而不是由initialization list中的排列顺序决定的!这是一个非常容易出错的地方。

第三章:Data语意学

这一章主要讲了类中的数据在内存中是如何分配的,包括(多重)继承和多态。

让我们首先从一段代码开始:

1

2

3

4

5

6

7

8

class X{};

class Y :virtual public X{};

class Z :virtual public X{};

class A :public Y, public Z{};

std::cout << sizeof(X) << std::endl;

std::cout << sizeof(Y) << std::endl;

std::cout << sizeof(Z) << std::endl;

std::cout << sizeof(A) << std::endl;

    在VS2013上输出的结果为1,4,4,8。为什么会这样呢?这就涉及到编译器针对C++语法而采取的对象模型。X虽然是一个空类,但为了使它的对象具有地址,编译器实际上是会为它的对象分配1个字节大小的空间,因此它的大小为1。Y和Z是对X的虚拟继承,编译器会为它们各分配一个指针,指向X的对象,因此它们的大小为4.A同理,它是Y和Z的多重继承,因此编译器会为它分配两个指针,所以它的大小为8.内存分布如图所示:

   

   书中还提到了一种模型,其输出的结果为1,8,8,12。这种模型的思路是给每一个空类分配1 byte的空间,再考虑到4 byte的alignment,因此可以推导出书中的结果,在这里不再赘述。

   关于类中Member的布局,C++ Standard是这样规定的:static data members不会被放到对象的布局之中;在同一个access section(public、private、protected等区段),member的排列是按照声明顺序分布的,但不一定是连续排列(因为存在alignment);同时,对于不同的access section,它们的data members自由排列,不必在乎声明顺序(也就是说access section之内是按照声明顺序排列,而access section之间自由排列)

   之后,书中讨论了单一继承、多态、多重继承、虚拟继承下类中内存分布的表现,这也是本章的重中之重。

   这是一个很简单的程序:

1

2

Point3d origin;

origin.x=0.0;

 执行这段程序所需要的时间和空间代价,随x的性质而不同。让我们分情况讨论:

1.x是static data members

   类的每个static成员都只有一个实例,存放在程序的data segment之中,和对象无关;因此对于这种情况,对x的存取并不会招致任何空间和时间上的额外负担。

2.x是 nonstatic data members

   访问类的nonstatic data members时,实际上编译器做了如下工作:

1

2

3

4

//源代码

origin.x=0.0;

//编译后的代码

*(&origin+(&Point3d::x))=0.0;

  也就是说,为了得到x的地址,编译器需要将origin的起始地址加上x的地址偏移量。(实际上,起码在VS里用&Point3d::x来表示偏移量是有问题的,但思想可以先这样理解)

 3.x是基类的变量,没有多态与多重继承

    在有继承的情况下,可能会导致空间上的浪费。我们来看这样一个例子:

    

    这个类中存有一个int和三个char,如果我们把这些变量都放到一个类中声明,那么算上alignment,它的对象大小为8字节。

    假设我们要继承:

    

    那么Concrete3的对象的大小将达到16字节,比原先的设计多了100%!

    这是因为alignment导致的,因为C++的对象模型中,在一个继承而来的类的内存分布里,各个基类需要分别遵循alignment,从而导致了空间的浪费。具体地对象布局可见下图:

    

4.加上多态

   在这种情况下,无论是时间还是空间上,访问类的成员都会带来一定额外的负担,主要体现在以下几个方面:

   1.virtual table,用来存放它所声明的每一个virtual functions的地址。

   2.每一个对象中会有一个vptr,提供执行期的链接。

   3.编译器会重写constructor和destructor,使其能够创建和删除vptr。

5.多重继承

   在多重继承的条件下,对于指针之间的赋值需要运行时计算。举个例子,以下的继承结构:

   

  我们声明几个对象和指针并赋值:

1

2

3

4

5

6

7

Vertex3d v3d;

Vertex *pv;

Point2d *p2d;

Point3d *p3d;

pv=&v3d;

p2d=&v3d;'

p3d-&v3d;

  对于p3d和p2d的赋值,只需要直接将v3d的地址赋过去就好。但对于pv的赋值,编译器需要计算一个Vertex在Vertex3d中的偏移量,从这个偏移量起始来得到pv的地址。因为类之间的内存分布如下所示:

   

   Vertex3d中Vertex部分的起始地址并不是Vertex3d对象的起始地址,因此对pv赋值需要一个运行时计算的开销。

6.虚拟继承

    在虚拟继承中,C++对象模型将Class分为两个区域,一个是不变区域,直接存储在对象中;一个是共享区域,存储的是virtual base class subobjects,它在内存中单独存储在某处,derived class object持有指向它的指针。在cfront编译器中,每一个derived class object中安插一些指针,每个指针指向一个virtual base class,为此需要付出相应的时间和空间成本。如下所示:

1

2

3

4

5

6

7

8

9

10

11

12

//具体的类同上一节多重继承,不同的是Vertex和Point3d虚拟继承了Point2d

void Point3d::operator+=(const Point3d&rhs)

{

    x+=rhs.x;

    y+=rhs.y;

    z+=rhs.z;

}

//编译器翻译后的版本

_vbcPoint2d->x+=rhs._vbcPoint2d->x;

_vbcPoint2d->y+=rhs._vbcPoint2d->y;

z+=rhs.z;

  这只是最基本的解决方案,书中还提出了一些编译器优化时间和空间的方法,感兴趣可以深入阅读一下。

最后,书中探讨了如何获得类中某个成员的地址偏移。我在这里就总结两种方法:

1.((int)&((structure*)0)->member)

2.先通过 int Test::* pOffset = &Test::x;获取偏移变量,再利用reinterpret_cast<int>(*(void**)(&pOffset))将其转化为整形量。

=======

http://dsqiu.iteye.com/blog/1669614

之前一直对C++内部的原理的完全空白,然后找到《Inside The C++ Object Model》这本书看了下, 感觉收获很大,因为书写得比较早,有些知识应该要更新,但是还是值得好好研读,由于该书的内容给人比较散的感觉,所以一直想找个时间整理一下,遂成此文,虽然都是抄书上的,但是却让我有了温故而知新的觉悟,附近里有三个好资料,一并共享了!2012年9月2日凌晨 4:31 谢谢 张雨生的歌声的相伴!

《深度探索C++对象模型(Inside The C++ Object Model )》学习笔记

第一章 关于对象

使用class封装之后的布局成本:

class并没有增加成本,data members直接内含在每一个class object之中,就像C struct一样。而member functions虽然被包含在class的声明之内,但是不出现在Object之中。每一个non-inline function 只会产生一个函数实体。至于inline function则会在每一个调用使用的地方产生一个函数实体(在调用点展开函数体)。

class在布局以及存取时间上主要的额外负担是由virtual 引起,包括:

virtual function 机制 用以支持一个有效率的“执行期绑定(runtime binding)”。

virtual base class 用以实现“多次出现在继承体系中的 base class ,有一个单一的被共享的实体”。

当然还有一些剁成继承下的额外负担,发生在“一个 derived class 和其第二或后继之 base class 的转换”之间。

C++ 对象模型

在C++对象模型中,nonstatic data members 被放置在每一个class object之内,static data members 则被存放在所以class object 之外。static和nonstaitc function也被放在所有 class object 之外。virtual functions 则以两个步骤支持之:

1.每一个 class 产生出一堆指向 virtual functions 的指针,放在表格之中,这个表格被称为 virtual table(vtbl).

2.每一个 class object被添加一个指针,指向相关的virtual table 。通常这个指针被称为 vptr ,vptr的设定和重置都由每一个 class 的 constructor 、destructor 和 copy assignment 运算符自动完成。

C++ 以下列方法支持多态:

1.经由一组隐含的转化操作。例如把一个 derived class 指针转换为一个指向其 public base type的指针。

2.经由 virtual function 机制。

3.经由dynamic_cast和typied运算符。

class object 需要多少内存:

1.其 nonstatic data members的总和大小。

2.加上任何犹豫alignment的需求和padding(填补)上去的空间。

3.加上为了支持 virtual 而由内部产生的任何额外负担。

指针类型:告诉编译器如何解释某个特定地址中的内存内容及其大小(例如:一个string是传统的8 bytes(包括一个 4byte的字符指针和一个用来表示字符串长度的整数)。转型(cast)其实是一个编译指令,大部分不会改变一个指针所含有的真正地址,它只影响“被指出值内存的大小和其内容”的解释方式。

第二章 构造函数语意学

2.1 Default Constructor

当编译器需要的时候,default constructor会被合成出来,只执行编译器所需要的任务(将members适当初始化)。

带有 Default Constructor 的Member Class Object

编译器的出来是:如果一个class A 内含一个或者一个以上 member class objects ,那么class A 的每一个 constructor 必须调用每一个member classes 的default constructor 。编译器会扩张已存在的constructors,在其中安插一些代码,使得 user code在被执行之前,先调用(调用顺序一member objects在class 的声明次序一致)必要的 default constructors。

带有 Default Constructor 的 Base class

编译器会在 Member Class Object 的default constructor 被插入调用之前,调用(调用次序根据他们的声明次序)所有 base class constructor 的default constructor 。

带有一个 Virtual Function 的Class

下面两种情况同样需要合成default constructor:

1.class 声明(或继承)一个 virtual function。

2.class派生自一个继承串链,其中一个或者更多的 virtual base class。

扩展(constructor)操作会在编译期间发生:

1.一个virtual function table 会被编译器产生出来,内放class 的virtual functions 的地址。

2.在每一个 class object 中,一个额外的pointer member(vptr)会被编译器合成出来,内含相关的class vtbl的地址。

带有一个 Virtual Base Class 的class

Virtual base class的实现法在不同编译器之间有很大差异,然而,每一个实现的共同点在于必须使 virtual base class 在其每一个 derived class object中的位置,能够在执行期准备妥当。对于class所定义的每一个constructor 编译器都会安插那些“允许每一个virtual base class 的执行期存取操作”的码。

总结

以上四种情况,会导致“编译器必须为未声明constructor 的class 合成一个default constructor ”,这只是编译器(而非程序)的需要。它之所以能够完成任务,是借着“调用member object 或base class的default constructor ”或是“为每一个object初始化其 virtual function 机制或virtual base class 机制”完成。至于没有存在这四种情况而又没有生命constructor的class 实际上是不会被合成出来的。

在合成的default constructor 中,只有base class subobjects(子对象)和member class objects会被初始化。所有其他的nonstatic data member ,如整数,整数指针,整数数组等是不会被初始化的,这些初始化操作对程序是必须的,但对编译器则并非需要的。

C++新手一般有两个误解:

1.任何class 如果没有定义default constructor ,就会被合成出来一个。

2.编译器合成出来的default constructor 会明确设定 class 内每一个data member的默认值。

2.2 Copy Constructor 

有三种情况,会以一个object的内容作为另一class object的初值。

1.最明显的当然是对一个object做明确的初始化操作。

2.当object被当做参数交给某个函数

3.当函数返回一个class object。

这三种情况需要有 copy constructor。

Default Memberwise Initialization

如果class 没有提供一个 explicit copy constructor时,当class object以“相同的另一个object作为初值是,其内部是以所谓的default memberwise initialization方式完成的。也就是把每一个内建的或派生的 data member(例如一个数组或指针)的值,从某个object拷贝一份到另一个object上,但不拷贝其具体内容。例如只拷贝指针地址,不拷贝一份新的指针指向的对象,这也就是浅拷贝,不过它并不会拷贝其中member class object,而是以递归的方式实行memberwise initialization。

这种递归的memberwise initialization是如何实现的呢?

答案就是Bitwise Copy Semantics和default copy constructor。如果class展现了Bitwise Copy Semantics,则使用bitwise copy(bitwise copy semantics编译器生成的伪代码是memcpy函数),否则编译器会生成default copy constructor。

那什么情况下class不展现Bitwise Copy Semantics呢?有四种情况:

1.当class内含有一个member class object,而这个member class 内有一个默认的copy 构造函数[不论是class设计者明确声明,或者被编译器合成]

2.当class 继承自 一个base class,而base class 有copy构造函数[不论是class设计者明确声明,或者被编译器合成]

3.当一个类声明了一个或多个virtual 函数

4.当class派生自一个继承串链,其中一个或者多个virtual base class

下面我们来理解这四种情况为什么不能使用bitwise copy,以及编译器生成的copy constructor都干了些什么。

在前2种情况下,编译器必须将member或者base class的“ copy constructor的调用操作”安插到被合成的copy constructor中。

重新设定Virtual Table 的指针

第3种情况下,因为class 包含virtual function, 编译时需要做扩张操作:

1.增加virtual function table,内含有一个有作用的virtual function的地址;

2.创建一个指向virtual function table的指针,安插在class object内。

所以,编译器对于每一个新产生的class object的vptr都必须被正确地赋值,否则将跑去执行其他对象的function了,其后果是很严重的。因此,编译器导入一个vptr到class之中时,该class 就不在展现bitwise semantics,必须合成copy Constructor并将vptr适当地初始化。

处理Virtual Base Class Subobject

virtual base class的存在需要特别处理。一个class object 如果以另一个 virtual base class subobject那么也会使“bitwise copy semantics”失效。

每一个编译器对于虚拟继承的支持承诺,都是表示必须让“derived class object 中的virtual base class subobject 位置”在执行期就准备妥当,维护“位置的完整性”是编译器的责任。Bitwise copy semantics 可能会破坏这个位置,所以编译器必须自己合成出copy constructor。

这也就是说,拷贝构造函数和默认构造器一样,需要的时候会进行构建,而并非程序员不写编译器就帮着构建。

2.4 初始化列表

下面四种情况必须使用初始化列表来初始化class 的成员:

1.当初始化一个reference member时;

2.当初始化一个const member时;

3.当调用一个base class 的 constructor ,而它拥有一组参数(其实就是自定义的构造函数)时;

      4.当调用一个 member class 的 constructor,而它拥有一组参数时。

不过,初始化的顺序是class members声明次序决定的,不是由初始化列表决定的。

第三章 Data 语意学

3.2 Data Member 的布局

nonstatic data members 在class object中的排列顺序将和其声明的顺序一样的。但C++ standard 允许编译器将多个access sections之中的data members自由排列,不必在乎他们的出现在class中的声明顺序。

3.3 Data Member 的存取

每一个member 的存取许可(private public protected),以及与class的关联,并不会导致任何空间上或执行时间上的额外负担——不论是在个别的class objects 或是在static data member 本身。

static data members 被视为global变量,只有一个实体,存放在程序的data segment之中,每次取static member 就会被内部转化为对该唯一的extern 实体的直接参考操作。若取一个static data member的地址,会得到一个数据类型的指针,而不是只想起class member的指针。

nonstatic data members 欲对一个nonstatic data member 进行存取操作,编译器需要吧class object的起始地址加上data member的偏移量(在编译事情就可以获知)。

3.4 继承与Data Member

只要继承不要多态

这种情况并不会增加空间或存储时间上的额外负担。这种情况base class和derived class的objects都是从相同的地址开始,其差异只在于derived object 比较大,用以容纳自建的nonstatic data members,把一个derived class object指定给base class 的指针或引用,并不需要编译器去调停或修改地址,它很滋润的可以发生,而且提供了最佳执行效率。

加上多态

这种情况会带来空间和存取时间的额外负担:

1.导入一个和virtual table ,用来存储它所声明的每一个virtual functions的地址。

2.在每一个class object中导入一个vptr,提供执行期的链接,使每一个object能够找到相应的virtual table。

3.加强constructor,使它能够为vptr设定初始值,让它指向class 所对应的virtual table 。

4.加强destructor,使它能够消抹“指向class 相关virtual table”的vptr。

多重继承

对于一个多重派生对象,将其地址指定给“最左端(第一个)base class的指针”,情况和单一继承时相同,因为二者都指向了相同的起始地址,至于第二个或后面的base class 的地址指定操作,则需要将地址修改过:加上(或减去,如果是downcast)介于中间的base class subobject(s)的大小。

如果要存取第二个(或后面)的base class 中的一个data member ,不需要付出额外的成本,因为members的位置在编译时就固定了,因此存取member只是一个简单的offset的运算。

虚拟继承

class如果含有一个或多个virtual base class subobjects将被分割为两部分:一个不变局部和一个共享局部。不变局部中的数据,总是能有固定的offset,这部分可以被直接存取,至于共享部分,所表现的就是virtual base class subobject ,这个部分数据,其位置因为每次派生操作而有变化,所以只能间接存取。

如果没有virtual functions的情况下,它们和C struct完全一样。

第四章 Function 语意学

4.1 Member的各种调用方式

Nonstatic Member Functions

实际上编译器是将member function被内化为nonmember的形式,经过下面转化步骤:

1.给函数添加额外参数——this。

2.将对每一个nonstaitc data member的存取操作改为this指针来存取。

3.将member function 重写成一个外部函数。对函数名精选mangling 处理,使之成为独一无二的语汇。

Virtual Member Functions

ptr->f();   //f()为virtual member function

内部转化为

(*ptr->vptr[1](ptr);

其中:

vptr表示编译器产生的指针,指向virtual table。它被安插在每一个声明有(或继承自)一个或多个virtual functions 的class object 中。

1 是virtual table slot的索引值,关联到normalize()函数。

第二个ptr表示this指针。

Static Member Functions

不能被声明为 const volatile 或virtual。

一个static member function 会提出于class声明之外,并给予一个经过mangling的适当名称。如果取一个static member function 的地址,获得的是其在内存的位置也就是地址,而不是一个指向“class member function”的指针,如下:

&Point::count(); 

会得到一个数值,类型是:

unsigned int(*)();

而不是:

unsigned int(Point::*)();

4.2 Virtual Member Funcitons

C++中,多态表示以“一个public base class 的指针(或reference),寻址出一个derived class object”。

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

1.这个class 所定义的函数实体(改写(overriding)一个可能存在的base class virtual function函数实体。

2.继承自base class 的函数实体(不被derived class改写)

3.一个pure_virtual_called()。

一个类继承函数virtual table的三种可能性:

1.继承base class 所声明的virtual functions的函数实体。正确地说,是该函数实体的地址会被拷贝到derived class的virtual table相对应的slot之中。

2.使用自己的函数实体。这表示它自己的函数实体地址必须放在对应的slot之中。

3.可以加入一个新的virtual function。这时候virtual table 的尺寸会增大一个slot放进这个函数实体地址。

编译时期设定virtual function的调用:

一般而言,我并不知道ptr 所指对象的真正类型。然而可以经由ptr 可以存取到该对象的virtual table。

虽然我不知道哪个Z()函数实体被调用,但知道每一个Z()函数地址都被放置slot 4的索引。

这样我们就可以将

ptr->z();

转化为:(*ptr->vptr[4])(ptr);

唯一一个在执行期才能知道的东西是:slot4所指的到底是哪一个z()函数实体。

多重继承下的 Virtual Functions

在多重继承中支持virtual functions,其复杂度围绕在第二个及其后面的base class 上,以及“必须在执行期调整this 指针”这一点。一般规则是,经由指向“第二或后继base class 的指针”来调用derived class virtual function。调用操作连带的“必要的this指针调整”操作,必须在执行期完成。

虚拟继承下的 Virtual Functions

4.3 函数的效能

nonmemeber、static member或nonstatic member函数都被转换为完全相同形式,所以三者效率完全相同。

4.4 指向Member Function的指针

取一个nonstatic data member的地址,得到的结果是该member在class 布局中的bytes位置,所以它需要绑定于某个class object的地址上,才能够被存取。

取一个nonstatic member function的地址,如果该函数是nonvirtual,则得到的是内存的真正地址,然后这个值也是不完全的,也需要绑定于某个class object的地址上,才能够调用函数。

支持“指向Virtual Member Function”之指针

对于一个virtual function,其地址在编译时期是未知的,所能知道的仅是virtual function在其相关之virtual table的索引值,也就是说,对于一个virtual member function 取其地址,所能获得的只是一个索引值。

4.5 Inline Funcitons

形参   传入参数,直接替换 传入常量,连替换都省了,直接变成常量 传入函数运行结果,则需要导入临时变量

局部变量  局部变量会被mangling,以便inline函数被替换后名字唯一 也就是说一次性调用N次,就会出现N个临时变量……程序的体积会暴增

第五章 构造、解构、拷贝 语意学

继承体系下的对象构造

constructor的调用伴随了哪些步骤:

1.初始化列表(member initialization list)的data members初始化操作会被放进constructor的函数本身,并以membs的声明顺序为顺序。

2.如果有一个member并没有在初始化列表中,但它在一个default constructor,那么该default constructor 必须被调用(手动)。

3.在那之前,如果class object有virtual table pointer(s),它(们)必须被设定初始值,指定适当的virtual table(s)。

4.在那之前,所有上一层的base class constructors 必须被调用,以base class 的声明顺序为顺序(与初始化列表的顺序没有关联)。

如果base class 被列于初始化列表中,那么任何明确指定参数都应该传递过去。

如果base class 没有列于初始化列表,那么调用default constructor。

如果base class 是多重继承下的第二或后面的base class ,那么this指针必须有所调整。

5.在那之前,所有 virtual base class constructors 必须被调用,从左到右,从最深到最浅。

如果class 被列于初始化列表中,那么如果有任何明确指定的参数,都应该传递过去,若没有列于初始化列表中,则调用default constructor。

此外,class中的每一个virtual base class subobject的偏移量必须在执行期可存取。

如果class object 是最底层的class,某constructors可能被调用;某些用以支持这个行为的机制必须被放进来。

对象复制语意学

当设计一个class,并以一个class object 指定另一个class object时,有三种选择:

1.什么都不做,实施默认行为。

2.提供一个explicit copy assignment operator。

3.明确拒绝一个class object指定给另一个class object。

一个class对于默认的copy assignment operator,在以下情况下不会表现出 bitwise copy语意:

1.当一个class的base class 有一个copy assignment operator时,

2.当一个class 的member object,而其class 有一个copy assignment operator时,

3.当一个class 声明了任何virtual functions时,

4.当class继承一个virtual base class 时。

vptr语意学

vptr在constructor何时被初始化?在base class constructors调用操作之后,但是在程序员供应的码或是初始化列表中所列的members初始化操作之前。

解构语意学

destructor被扩展的方式:

1.destructor的函数本身首先被执行。

2.如果class拥有member class objects,而后拥有destructor,那么它们会以声明顺序的相反顺序被调用。

3.如果object内带一个vptr,则现在被重新设定,指向适当的base class virtual table。

4.如果有任何直接的(上一层)nonvirtual base classes 拥有destructor ,它们会以声明顺序相反顺序调用。

5.如果有任何virtual base classes 拥有destructor,而当前讨论的这个class 是最尾端的class,那么它们会以其原来顺序相反顺序被调用。

第六章 执行期语意学

第七章 ……

补充:类型向上转型和多态的混淆

构造这样的一个继承体系:

class Base {

public: virtual ~Base() {}

virtual void show() { cout << "Base" << endl; }

};

class Derived : public Base {

public: void show() { cout << "Derived" << endl; }

};

子类Derived类重写了基类Base中的show方法。 编写下面的测试代码:

Base b; Derived d;

b.show(); d.show();

结果是:

Base

Derived

Base的对象调用了Base的方法,而Derived的对象调用了Derived的方法。因为直接用对象来调用成员函数时不会开启多态机制,故编译器直接根据b和d各自的类型就可以确定调用哪个show函数了,也就是在这两句调用中,编译器为它们每一个都确定了一个唯一的入口地址。这实际上类似于一个重载多态,虽然这两个show函数拥有不同的作用域。 那这样呢: Base b; Derived d; b.show(); b = d; b.show(); 现在,一个Base的对象被赋值为子类Derived的对象。

那这样呢:

Base b; Derived d;

b.show(); b = d; b.show();

现在,一个Base的对象被赋值为子类Derived的对象。

结果是:

Base

Base

对于熟悉Java的人而言,这不可理解。但实际上,C++不是Java,它更像C。“b = d”的意思,并不是Java中的“让一个指向Base类的引用指向它的子类对象”,而是“把Base类的子类对象中的Base子对象分割出来,赋值给b”。所以,只要b的类型始终是Base,那么b.show()调用的永远都是Base类中的show函数;换句话说,编译器总是把Base中的那个show函数的入口地址作为b.show()的入口地址。这根本就没用上多态。

单继承下的重写多态

那我们再这样:

Base b; Derived d;

Base *p = &b;

p->show();

p = &d;

p->show();

这时,结果就对了:

Base

Derived

p是一个指向基类对象的指针,第一次它指向一个Base对象,p->show()调用了Base类的show函数;而第二次它指向了一个Derived对象,p->show()调用了Derived类的show函数。

总结:也就是说,只有是指针或者引用才是真正的多态,将子对象赋给父类对象其实类型向上转型……

个人觉得C++容易弄混淆的地方(持续更新):

1.const和指针的修饰问题

const char * a;   //一个指针a指向const char

char const *a;      //这两个是a指向的内容是常量,不能改变

char * const a;     //首先a 是指针然后还是const

const (char*) a;     //这两个是a指针本身是常量,指针本身不能改变

其实,可以看出如果const修饰的char(也就是类型本身或者是 *variable对指针的解引用)就是指针指向的内容是常量,反之就是修饰指针本身的。那我们可以总结一个识别方法就是:看const 两边(当然有的只有一边)的类型是类型(指针指向的内容)就是类型变量本身是常量(如const char * a和char const *a 的const两边是char,*a)。

当然两者都是常量就是:const char * const a;第一个const是类型常量,第二个才是指针常量。同样给出 const char &a ;const char *a;在传递参数时使用。

2.数组和指针的组合问题

char * a[M];  这是指针数组,就是每一个元素是指针的数组,每个元素都要初始化。a[M]一看就是数组,这个数组每一个元素是char *,所以可以将char *扩展为一维数组然后a[M]就是二维数组了。其实就是M个指针。

char (*a)[N];  这是一个指针,这个指针指向N个char元素,即指向数组的指针,其实就是一个指针。把(*a)看着一个变量,这个变量是指向N个元素的指针,所以只是一个一维数组。把char (*a)[N]看成是char b[N]就可以了。

同理,也可以用修饰的道理来区分,可以自行体会。具体二维数组的动态分配的更多精彩可以查阅我的另一个博客http://dsqiu.iteye.com/blog/1683142

3.C++变量的初始化

对于内置类型局部变量不进行初始化,但是分配地址,全局变量会进行默认初始化。对于类类型局部变量(没有显式初始化)会进行默认初始化(有默认构造函数,否则报错),但其内部的内置数据成员不会进行初始化(如果在默认构造函数没有进行初始化)。数组也是同样。

========

https://blog.csdn.net/charce1989/article/category/6912115 -- 专栏

猜你喜欢

转载自blog.csdn.net/libaineu2004/article/details/81866643