今天看到了C++ Primer中的复制、赋值、析构部分了,不知道为什么,看起来太迷茫了,而且上网上看看别人怎么说的,发现大家的理解都不一样,让我更加迷茫了。我又反反复复地读了读C++ Primer相关的章节,有了一点点自己的理解。
正如C++ Primer里面所讲,在
- 根据另一个同类型的对象显式或隐式初始化一个对象。
- 复制一个对象,将它作为实参传给一个函数。
- 从函数返回时复制一个对象。
- 初始化顺序容器中的元素。
- 根据元素初始化式列表初始化数组元素。
这 些情况下将启用复制初始化过程:复制初始化首先使用指定构造函数创建一个临时对象(待复制的对象和目标对象类型相同,则没有该过程),然后用复制构造函数 将那个临时对象复制到正在创建的对象。复制初始化包含两个步骤说明了要区分“复制初始化”和“调用复制构造函数”这两个概念,复制初始化过程还可能调用直 接构造函数。以复制对象作为实参传递给一个函数为例:如果被调函数声明为:RT f(IT obj);调用语句为:f(obj_parm),而IT类可以以obj_parm为直接构造函数的参数,那么将在调用时发生如下过程:首先生成一个临时的 IT类型变量t_obj,然后调用直接构造函数初始化t_obj(隐式转换过程),然后调用复制构造函数以t_obj参数开辟形参内存空间并初始化。上面 两个过程我想也可以这么理解:第一步发生隐式类型转换生成一个临时变量,然后把该临时变量初始化给对象(哪一种理解才是正统???)。此外,C++ Primer里叙述了那么多启用复制初始化过程的情况,也说明了初始化时使用"="运算符只是复制初始化的一种,不是全部。
以 上这些是概念上的过程,我写程序在GCC4.2下验证,发现实际过程如下:使用直接构造函数直接开辟形参内存空间并初始化。这也就是说在GCC4.2实际 工作过程中,并没有调用复制构造函数。为了检验编译器到底需不需要复制构造函数,我又作了另外一个实验:把函数形参的类的复制构造函数放到private 访问标签下,结果编译出错。函数参数的调用过程还是应该在概念上理解为两个过程,并且存在一个临时对象,但实际上编译器优化了这两个过程,合而为一,只有 一次构造函数的调用,并优化掉了临时变量,只有实参类型和形参类型匹配时才会调用复制构造函数。
我还实验了书中所说的引发复制初始 化的其他过程,发现当需要类型转换时,只有顺序容器才会调用复制构造函数。那么复制构造函数的意义在什么地方?我能想到的首先一个地方就是将复制构造函数 放入private访问标签下防止对象复制;其次是控制复制过程,但我还对C++以这种方式控制初始化的合理性表示怀疑。我想完全可以用不定义参数为同类 型引用的构造函数防止复制,定义函数体为空的复制构造函数说明采用合成复制构造函数,定义不空的函数来控制复制过程;这种方式看起来也更自然一些,不知道 是C++标准制定者有什么特殊的考虑,还是我理解还不到位。
还有一个问题困惑过我一段时间:使用"="调用复制初始化函数和使用重 载赋值运算符"="有什么区别。还句话说,在形式上看,两者的参数一致,“返回值”(认为复制构造函数返回该类型的引用)也一致,作用也似乎一致,为什么 函数体往往有那么大的区别?这个问题现在的理解是:使用"="复制初始化的过程中除了调用复制构造函数、可能的直接构造函数调用之外还有一个为对象开辟内 存空间的过程,而赋值运算符被使用时,左操作数的内存空间必须存在;这个差别就意味着写赋值运算符"="函数体的时候不仅要考虑如何构造对象,可能还需要 考虑左操作数的析构问题;而调用复制构造函数的过程中不需要考虑析构。
关于如何理解复制初始化、复制构造函数、赋值运算符这些概念的这个问题还没有完,总感觉特别别扭,准备先把书看完,这个问题以后还要解决。
==========新的一天===========
今 天bbs上牛人作答了:标准规定了语义上的确是先用直接构造函数建立一个临时对象,然后调用复制构造函数把临时对象拷贝到正在建立的对象上,但标准允许实 现省略复制拷贝函数(即使复制拷贝函数有副作用)。需要注意的是,GCC编译器仍然会检查标准规定的语义,如果复制构造函数为private时,GCC就 会报错;这点和VC不同,VC不检查该语义,直接省略了复制构造函数。
但又有一个新问题,T obj2 = obj1的实现过程我仍然有疑问:是按照两步的开辟obj2的空间,然后调用复制构造函数把obj1复制到obj2上,还是说先建立一个临时对象 t_obj,然后调用复制构造函数复制obj2到t_obj上,然后更名t_obj为obj2?我觉得t_obj完全可以不要,但bbs上的牛人为什么特 别说有t_obj???
==========新的一天===========
原来我一直不明白exciplit关键字的含义,今天看了楼下哥们的文章之后有了这么的理解:当构造函数声明为exciplit时,该构造函数只能通过调用操作符的形式调用,而没有该关键字时,构造函数的调用可以通过=操作符、return语句、实参到形参等方式完成。
此问题仍未结束……
==========我是分割符===========
本想给楼下的兄弟一个回复,结果回复太长了,被百度给截断了,于是写在这里:
我的困惑主要集中在类似于函数返回对象的内存分配和初始化是否同时完成这类问题上。
例如:
class A;
A func(void) {
return A(); // line 1
}
int main(void) {
func(/*null*/); // line 2
return EXIT_SUCCESS;
}
上面line2处的函数调用表达式将会产生一个对象(暂时命名为obj2)作为表达式的值,但问题是该对象的内存分配和对象的初始化都是在什么时间完成的。我想有两种可能的过程:
- 在 主函数中执行到func()调用的时候,编译器根据func的声明分配一篇内存空间(这个就是obj2)作为将来存放调用表达式值的空间,然后开始调用函 数func()。当在func()中,执行到return时,先使用直接构造函数产生对象obj1,然后return语句调用复制构造函数把obj1复制 初始化到obj2,而obj1将被销毁。也就是说obj2内存开辟和初始化并不是相连发生的。
- 在主函数中执行到line2时, 编译器知道表达式应该有一个名叫 obj2的对象作为表达式的值,但是它并不在此时开辟内存空间,而是先调用func()。在func()中执行到line1时,直接构造函数A()的调用 将产生一个对象obj1,然后编译器再开辟一片临时内存空间t_obj,调用复制构造函数把obj1复制初始化到t_obj。最后func()调用结 束,return语句将把t_obj更名为obj2,然后obj1被销毁。
上面两种过程都有一些问题:如果是 第一种过程,假如在func()中出现了异常而导致func没有执行return语句,就会导致在主程序中obj2对象没有初始化,以后主程序中任何使用 obj2对象的地方(除了赋值)都可能导致错误;如果是第二种过程,main()函数作用域中的ojb2对象的内存分配过程是在fun()函数作用域中完 成的,也就是说非动态对象的生命周期并不与作用域相同,不知道这是不是合理的。