Chapter 2 构造函数语义学习小结
1、 C++编译器何时会为C++中的类生成缺省的构造函数(Default constructor)?
如果程序员没有定义构造函数,编译器会在下面四种情况为类生成缺省的构造函数:
? 类中聚合的元素有构造函数(可以是程序员自定义的或者编译器生成的);
? 类的基类有构造函数(可以是程序员自定义的或者编译器生成的);
? 类中有虚函数;
? 类虚继承其他类。
对于不符合以上四种情况,C++编译器并不会生成一个构造函数,那类实例化的对象怎么初始化?实际上编译器都是直接分配内存而已,并没有初始化对象。
2、 C++编译器何时会为C++中的类生成缺省的拷贝构造函数(Copy constructor)?
拷贝构造函数的合成也使用于上述四种情况。对于符合上述四种情况拷贝构造函数执行按成员拷贝(memberwise copy),对于不符合上述四种情况执行按位拷贝(bitwise copy)。【这是个人区分memberwise和bitwise,而书中的意思好像是都是memberwise,在memberwise中分为是否进行bitwise】。那些bitwise拷贝带来的后果是什么?对于一般的类型int、double等都不会有问题,但是对于指针类型就会发生两个对象指向同一个指针变量【也就是所谓的浅拷贝】。从语义角度来看,当含有指针数据成员必须提供构造函数。
3、 什么时间需要程序员提供一个拷贝构造函数?
对于含有指针数据成员必须提供构造函数,那么对于其他情况是否都不需要?从语义上讲,是这样的。但是对于编译器来讲涉及到编译优化中的返回值优化【Named return Value】,这中优化是指至少可以减少一次返回值构造,但NRV的前提是程序员已经定义好拷贝构造函数,这需要根据对效率的需求来决定。
4、 书中的一个示例,为什么在类的构造函数中使用高效的memcpy()或者memset()库函数对对象进行拷贝有时候会发生错误【P73】。
原因是当类中含有vptr或者vbtl时,如果直接使用库函数,会修改这些指针。
5、 对于NRV的讨论,到底是否必须存在显示的Copy ctor才能实行NRV?
问:在67页,最下面两行:这个程式的第一个版本不能实施NRV最佳化,因为test class缺少一个copy constructor。但是在66页「在编译器层面做最佳化」那一段中所列的码显示,当编译器把xx以__result取代,变成__result.X::X();即default constructor被唤起。唤起default constructor是可以理解的,可是编译器转换后的码并没有使用到copy constructor呀,为什么67页最后两行却说缺少一个copy constructor,就不能实施这个最佳化了呢?
观点1:如同63页与64页「返回值的初始化」这一段,编译器可能将63页下面的X bar()函数定义转换成64页的虚拟码,其中有一行__result.X::X(xx); 这会使用到copy constructor。 转换成64页的码后,65页与66页分述了两种后续可能出现的最佳化动作,其中一种即是66页的编译器层面做最佳化。如此,虽然66页最佳化后的码看起来并不使用到copy constructor,但是这些码是根据像64页那种样子的码最佳化而来的,而若没有copy constructor,根本无法转换成64页那种虚拟码,因为其中有一个呼叫copy constructor的动作。所以,虽然66页经过编译器最佳化的结果省去了__result.X::X(xx);这个copy constructor的呼唤动作(因为根本没有xx了),但若没有明白提供一个copy constructor,却无法让编译器进行这样的最佳化。【第5章,205页最下面一段话:一般而言如果你的设计之中,有许多函式都需要以传值(by value),传回一个local class object....那么提供一个copy constructor,就比较合理--甚至即使default memberwise语意已经足够。它的出现会触发NRV最佳化。然而,就像我在前一个例子中所展现的那样,NRV最佳化后将不再需要唤起copy constructor,因为运算结果已经被直接计算于「将被传回的object」体内了。」】
观点2:Lippman在p67最后一行所言『这个程式的第一个版本不能实施NRV最佳化,因为test class缺少一个copy constructor』,此语错误。如果程式没有explicit copy constructor,编译器会自动为我们做出来(如为trivial,则直接bitwise copy;如为nontrivial,则由编译器为我们合成出一个copy constructor)。因此,有没有explicit copy constructor并不影响 NRV 最佳化的实施。NRV 最佳化主要是由编译器option来决定要不要实施。做了一些实验,判断VC和GCC都没有做到NRV最佳化,而其不做的理由不是因为技术上的困难,是为了避免造成「user defined copy constructor之副作用失效」-所谓副作用是指,例如「在user defined copy constructor中做一个cout出」之类这种「与memberwise copy 无关」的动作。
Chapter 3 成员数据语义学习小结
1、 C++编译器对类增加那些数据?
? 类没有定义任何数据成员【编译器会为类增加1byte,原因是为了区分类实例化的多个对象都有不同的地址】;
? 含有虚函数【编译器会为类添加一个vptr指针,指向虚函数表,virtual table】;
? 虚继承其他类【编译器会为类添加一个vbtl指针,指向虚基类】。
2、 C++编译器如何处理类的非静态数据成员?
如果我们直接访问类的非静态数据成员的地址,发现它仅仅是数据成员在类中的偏移量,如果要访问某一对象数据成员,那么该数据的地址是对象的地址加上数据成员在类中的偏移量。在P98中的表示是:&original + (&Point3d::_y -1),就是访问的是Point3d实例化的对象original中的数据成员_y,为什么会有一个减一的动作?这个的目的是为了区分指向第一个member的指针和一个指向数据成员,但没有指向任何member【如Point3d::*pM = NULL和Point3d中的第一个数据成员的偏移量不能都是0,目前在Dev-Cpp和VC6.0中都不是这样处理的,他们都是对一个数据成员的空指针减去一,变成0xFFFFFFFF,而编译器对数据成员的偏移量都不变化,直接按照他们在类中的声明顺序】。
3、 C++编译器如何处理多继承时的vptr?
如果一个类含有虚函数或者单继承有虚函数,编译器还是比较容易处理的,只要设置一个vptr就可以访问到对应的函数。但是当类多继承其他类(且都有虚函数),那么编译器必须处理指向派生类对象的指针能够指向多个基类。派生类和第一个基类共享vptr,对于第二个或者以后的基类,这个指向派生对象的指针必须加上一个偏移量来指向对应的类的vptr数据。【第二类的偏移量为sizeof(Base_Class1),后面的依次类推】。
Chapter 4 成员函数语义学习小结
在C++编译器中有一个技术为了支持多态、命名空间等,叫做Name-mangling,就是把一个名字转化为一个编译器可以唯一识别的名字。
1、 C++编译器把成员函数编译成什么样子?
C++编译器把成员函数分为两类,静态和非静态成员函数。假定CExampleClass中有3个函数原型如下:
? int NormalFun(parameter…);
? int NormalFunConst(parameter…) const;
? static int SNormalFun(parameter…);
C++编译器编译后会把这个函数都编译成全局函数,如下:
? int CExampleClass13NormalFun1p(CExampleClass * const this, parameter…);
? int CExampleClass13NormalFunConst1p(const CExampleClass * const this, parameter…);
? int CExampleClass13 SNormalFun1p(parameter…);
2、 C++编译器如何调用成员函数?
C++编译器在调用方面也可以总结为三类调用方式,静态函数(不能为const,virtual等修饰),非静态非虚成员函数,单继承的虚函数,多继承的虚函数。
? 静态成员:直接调用,如CExampleClass::SNormalFun(parameter…)或者根据对象可以调用,编译器把这种调用直接转化为上面的CExampleClass13 SNormalFun1p(…)形式;
? 非静态非虚成员函数,必须通过对象调用,如Obj.NormalFun(…),编译器把这种形式转化为CExampleClass13NormalFun1p(&Obj, parameter…)的形式;
? 对于虚函数的调用,是通过vptr进行的,如ptr->vFun(pararmeter…),编译器将转为为:(*ptr->vptr[index])(ptr,parameter…)形式;
? 对于多继承下虚函数的调用,必须调整后面基类的偏移量。主要有两种方式:第一种形式为(*ptr->vptr[index].addr)(ptr+ptr->vptr[index].offset,parameter…)【这个offset在编译器中生成的是一个负数】,这个设计的目的增加一个结构,保证派生类同时override多个基类的虚函数都能指向同一个函数;第二种形式是使用thunk技术,vptr中对应的index存放的是简单虚函数的地址或者是指向一个相关的thunk(用于调整this指针);
? C++编译器对函数指针的翻译:对于多继承下一个函数指针翻译,(pClass.*pfm)()被转化为pfm.index < 0 ? (*pfm.fadd)(&pClass + pmf.offset) : (*pClass.vptr[pfm.index].faddr) (&pClass + pClass.vptr[pfm.index].offset)【index小于0表示该函数不是虚函数】。
3、 C++编译器把vptr放在类的什么位置?
C++标准认为可以放在任何位置,可以在类的头部,目前VC和DEV-Cpp都是如此,为什么?如果把vptr放在尾部,其实是可以直接兼容C语言中的struct结构,但是C++是为了节省空间,便于操纵vptr没有这样做。如何节约空间,当有继承的时候,放在头部时,派生类是可以共享基类的vptr。
4、 C++编译器中的那些操作会增加代码?
? 宏展开:宏是一定会被展开的,这一定会增加代码量;
? Inline函数:如果编译器决定把该函数Inline,那么也会增加代码,其中涉及到增加参数、局部变量和代码,特别是对于在一个表达式中又多次inline函数调用,则inline函数的局部变量会被扩展多次,然后合成一个scope;
? Deconstructor函数:因为C++保证资源获得即初始化(RAII),所以如果在源代码中又多个出口,编译器都会在出口点增加析构变量的操作;
? 异常,C++为了异常的try,catch处理,必须增加代码;
? Template:当模板函数或者模板类被使用的时候,C++编译器会保证实例化模板类或者模板函数。
Chapter 5 构造、析构和拷贝语义学习小结
1、 C++编译器是怎么样实现虚继承的构造函数?
假定Derived继承于Base1和Base2,Base1和Base2继承于Base,那么Derived的构造函数和析构函数编译器是如何生成?
Derived7DerivedV(Derived * const this, bool _most_derived)
{
if( _most_derived != false ) this->Base::Base();
this->Base1::Base1(false);
this->Base2::Base2(false);
this->vptr = _vbtl_derived; //设置vptr
this->vptr_Base = _vbtl_Base_derived; //设置虚基类指针
//user code
return this;
}
Derived7DectorV(Derived * const this, bool _most_derived)
{
this->vptr = _vbtl_derived;
//user code
this->Base1::Base1(false);
this->Base2::Base2(false);
if( _most_derived != false ) this->Base::Base();
}
注:在VC中就是这么实现的,在DEV_Cpp中的实现与此不同,它是产生两个版本的constructor,一个是设定vptr,并调用虚基类,另外一个是不调用虚基类也不设置vptr。
注:【P234中的译者加了译注,实际上是不对的,因为译者没有考虑多继承的情况。译者的理解对于是单继承的情况是正确的,对于多继承必须考虑到先设置vptr,因为可能是一个Base2的指针指向Derived对象的,如果不先设置则不能正确的调用Derived的析构函数】。
注:【后来在网上看到一篇文章《<深度探索C++对象模型>>(简体版)中的蛇足》,作者viper,http://blog.csdn.net/Viper/。作者文中描述的第二点,不过我是认为侯先生加的译注是错误的,而不是太理论化。侯先生加的译注第三点的意思应该是指正确的设置基类的vptr,其实在调用基类的析构函数中都会设置的。在http://dev.csdn.net/article/10/10874.shtm有关于这篇文章的讨论,很有意思的。】
2、 C++编译器把成员编译后结果是什么样子?
成员主要指的是静态数据、静态成员函数、非静态数据、非静态成员函数和全局数据和heap数据。
Class
Data
Static Data
Static Fun
Function
Virtual Fun
Global Data
Static Data
Fun(this,…)
Static Fun(…)
Vptr
Vbcb
Data
编译后的结果,不考虑Name-mangling
全局可见
.Data
全局
可见
类对象可见,局部和heap对象数据
3、 C++如何处理全局对象、静态对象?
对于全局变量,针对特定平台的C++编译器的一种处理方法,增加两个Sections,分别为.init和.fini,处理全局对象的构造和析构。【全局对象要求在main函数之前就存在】。.init section主要完成的是调用对象的构造函数,为了保证一个文件中的所有的全局对象都能够初始化,一般会为每个文件生成一个_sti(),该函数负责初始化该文件中所有的全局对象。全局对象的析构是在main函数结束之前完成【.fini中析构】
【全局对象要求在main函数之前完成初始化,如果在执行全局对象的构造函数时,发生异常,那么C++编译器将直接调用terminate()函数,main函数将不会执行。VC6.0和DEV-Cpp都是如此】
静态对象如果是全局的,那么初始化的过程和上面的过程是一致的。如果是静态局部对象,他的初始化是在该函数第一次执行的时候才完成初始化。C++编译器怎么知道该函数是第一次执行?C++编译器设置一个全局的指针,如果没有初始化该指针为NULL,如果初始化则该指针为静态对象的地址,当完成初始化的时候改变指针的状态就能区分。
全局静态对象的析构和全局对象的析构一样。局部静态对象的析构也需要根据指针是NULL还是对象的地址来判断是否析构。【对于局部静态对象的处理VC6.0和DEV-Cpp都是通过一个byte存放标志位来完成】。
Chapter6 执行期语义学习小结
1、 New[]的学习和讨论。
C++编译器如何完成New[]?New operator实际上完成两步操作,第一:根据对象类型分配内存【如调用free来完成】,调用构造函数初始化对象【对于New[]构造函数限定为default ctor或者带有构造函数的参数都有缺省值】。对于New来说可以一次完成,但是对于New[]来说必须借助一个新的函数来完成【原因很简单:可能存在异常,那么必须析构已经完成构造的对象,异常可能发生在任何时候】。一般会把New[]修改为什么样子然后调用?一般封装为:vec_new (pVoid ptrArray, int elemCount, int objSize, pVoid ctor, pVoid dtor)【当ptrArray不为0,表示placement operator new语义】。New[]存放的数组的长度一般在真正存储对象地址的前4个Byte中【VC和GCC都是如此】。
讨论:在网上《Const的思考一文》中的一个例子:
class A
{
public:
A(int i=0):test[2]({1,2}) {} //你认为行吗?
private:
const int test[2];
};
vc6下编译通不过,为什么呢?
观点1:编译器堆初始化列表的操作是在构造函数之内,显式调用可用代码之前,初始化的次序依据数据声明的次序。初始化时机应该没有什么问题,那么就只有是编译器对数组做了什么手脚!其实做什么手脚,我也不知道,我只好对他进行猜测:编译器搜索到test发现是一个非静态的数组,于是,为他分配内存空间,这里需要注意了,它应该是一下分配完,并非先分配test[0],然后利用初始化列表初始化,再分配test[1],这就导致数组的初始化实际上是赋值!然而,常量不允许赋值,所以无法通过。
观点2:认为上一个观点错误【我第一次看到也是上面的解释,汗先】,C++标准有一个规定,不允许无序对象在类内部初始化,数组显然是一个无序的,所以这样的初始化是错误的!对于他,只能在类的外部进行初始化,如果想让它通过,只需要声明为静态的,然后初始化。
2、 临时对象的讨论。
C++中有很一部分工作是为程序员的代码添加一些临时对象完成语义/语法上的要求。例如:使用转换函数,不同对象之间的赋值(包括一些返回值和目标对象不一致),还有一些是程序员没有明确的指定运算的结果等等。
但是C++编译器在一些情况下生成临时对象会带来一些问题【效率降低或者语义的复杂】。例如:在一个复合语句中有临时对象,C++编译器一定会生成一些判断代码,为什么?要判断何时处理对象析构;临时对象的析构一般来说是语句结束以后,有两种例外,在生成临时对象用来初始化另外的对象,那么必须等待初始化结束后临时对象才能析构,还有一些情况是C++生成的临时对象的作用域等同于目标对象的作用域啦,C++编译器生成的临时对象初始化reference对象,析构必须是在临时对象作用域和reference作用域取小者才能析构。
Chapter7 站在对象的顶端学习小结
1、 异常中try、catch是如何实现?
对于异常的处理,构造program counter-range。对于try block来说,把一个函数的try block的起始位置和结束位置保存在上述表格中,当发生异常时,当前的program counter(也就是程序执行的位置)和program counter-range进行比较,以判断出是否在try block中,如果是,就要找到对应的catch,否则当前的函数会从程序的栈(ESP,EBP等寄存器信息)中弹出,并从新设置program counter为调用的地址,然后继续上述的判断过程。
对于抛出的异常对象,编译器产生一个类型描述符,对异常的类型产生编码。编译器还必须为catch子句产生类型描述符,执行期的异常处理模块则会比较抛出的对象的类型描述符和catch子句的类型描述符,找到合适的catch或者最后到terminate()处理。
2、 为什么向下转换(downcast)中对于Pointer和reference的处理不一致?
对于downcast来说(dyanmaic_cast<type>(Object)),对于这两种的处理分别是:
? 对于Pointer来说,当转换成功时,成功的返回派生类对象的指针,当发生转化错误时候,返回0(也就是NULL);
? 对于reference来说,当转换成功时,成功的返回派生类对象,当发生转化错误时候,抛出一个bad_cast exception(为什么不是0?很简单,对于reference,0会被转换成临时对象,然后reference到这个临时对象)。
|
相关推荐
1.5 小结 1.6 问题 第2章 类 2.1 构造函数 2.2 赋值 2.3 公用数据 2.4 隐式类型转换 2.5 操作符重载:成员或非成员? 2.6 重载、缺省值以及省略符 2.7 Const 2.8 返回值为引用 2.9 静态对象的构造 2.10 小结 2.11 ...
Lippman丰富的实践经验和C++标准委员会原负责人Josée Lajoie对C++标准深入理解的完美结合,已经帮助全球无数程序员学会了C++。本版对前一版进行了彻底的修订,内容经过了重新组织,更加入了C++ 先驱Barbara E. Moo...
如 果你会一点C、C++语言,你就可以学习游戏编程了,开发真正的游戏!如果你学过一点C++更好,没学过也没关系。本课程教你从零基础开始开发7个完整的 游戏:Brainiac、Light Cycles、Henway、Battle Office、Meteor ...
16.1 COM对象模型 16.2 创建一个ATL多边形工程 16.2.1 优化模块代码 16.2.2 测试控件 16.3 调试ATLCOM控件 16.4 小结 第17章 STL和 MFC编程 17.l 产生一个STL和MFC应用程序 17.1.l 复数 17.1.2 模板...
/ 305 11.3.3 数组边界检查消除 / 307 11.3.4 方法内联 / 307 11.3.5 逃逸分析 / 309 11.4 Java与C/C++的编译器对比 / 311 11.5 本章小结 / 313 第五部分 高效并发 第12章 Java内存模型与线程 / 316 12.1 ...
5.5 深入理解光照计算模型 5.6 小结 第6章 纹理映射基础 6.1 基本概念 6.2 使用纹理 6.3 纹理过滤方式 6.3.1 最近点采样 6.3.2 线性纹理过滤 6.3.3 各项异性纹理过滤 6.3.4 多级渐进纹理过滤 6.3.5 纹理过滤方式...
9.2 对象的模型技术 .103 9.3 面向对象的分析 .105 9.4 面向对象的设计 .107 9.5 小 结 .110 第十章 类 .112 10.1 类 的 声 明 .112 10.2 类 的 成 员 .113 10.3 构造函数和析构函数 .119 10.4 小 ...
其对象模型是能定义对应真实事物的数据结构,使得程序的任务和任务如何实现 二者之间的转换变得基本上透明。 优点三,由于有了 JVM,一个 Java 应用程序与操作系统或硬件完全隔绝,因此计算机 病毒或其他作祟的代码...
C#静态成员和方法的学习小结 C#中结构与类的区别 C#中 const 和 readonly 的区别 利用自定义属性,定义枚举值的详细文本 Web标准和ASP.NET - 第一部分 XHTML介绍 在ASP.NET页面中推荐使用覆写(Override)而不是事件...
6.2.5 深入理解DBMS_XPLAN的细节 156 6.2.6 使用计划信息来解决问题 161 6.3 小结 169 第7章 高级分组 170 7.1 基本的GROUP BY用法 171 7.2 HAVING子句 174 7.3 GROUP BY的“新”功能 175 7.4 GROUP BY的CUBE...