C++独孤九剑第五式——人生几何(对象复制控制)

对酒当歌,人生几何? 譬如朝露,去日苦多。

人的一生可能惊涛骇浪,更可能波澜不惊,这次我们就来探讨一下“对象”(当然各位同学自己的对象不在本次讨论范围之内O(∩_∩)O,课后自己讨论吧)一生的“起起落落”,即对象的复制控制。

复制控制包括三个部分:复制构造函数的调用、赋值操作符的调用、析构函数的调用。下面就这三个操作来逐一进行介绍,大家共同学习(*^-^*)

一、复制构造函数

复制构造函数:首先它也是构造函数,所以函数名与类名相同,没有返回值;其次,它只有一个形参,且该形参是对本类类型对象的引用(常用const修饰)。

为什么复制构造函数的参数需要用引用呢?我们先看复制构造函数的调用场景:

1.  用一个同类对象显示或隐式初始化另一个对象时。

2.  复制一个对象,把它作为实参传递给函数时。

3.  从函数返回一个对象时。

4.  初始化顺序容器中的元素时。

5.  根据元素初始化式列表初始化数组元素时。

假设复制构造函数的参数是该类类型的对象,而不是引用。那么当我们在给一个函数传递实参的时候(其它情况也是一样的道理)会隐式调用复制构造函数,而复制构造函数本身又需要该类对象的实参,又会去调用该类的复制构造函数!从而将形成函数调用的“死循环”。

初始化和赋值有时候对于新手来说可能不是一件容易区分的事情,我就稍作说明吧。初始化就是类对象刚刚形成的同时对其进行值的设置,而赋值则是对象形成后再对其进行值的设置(在这里,我们把同一个语句中的操作当成是同时进行的操作,虽然从微观的角度来看并非如此)。

假设有如下的代码调用:

{

A  a1 = a2;//a2也是类A的对象,对象a1在形成的同时进行值设置,所以调用复制构造函数,因为这是初始化操作。

}

{

A  a1;

a1 =a2;//此时调用赋值操作符函数,因为此时不再是初始化了。

}

和默认构造函数一样,C++ Standard上说,如果类中没有声明拷贝构造函数,就会有一个隐式地声明或者定义出现。但是拷贝构造函数又会被分为trivial和nontrivial,实际上只有nontrivial的时候编译器才会合成相应的函数(很多时候都是这样的,习惯就好了)。而且,C++ Standard要求编译器尽量延迟nontrivalmembers的实际合成操作,直到真正遇到其使用场合为止。

那么什么时候编译器会真正合成复制构造函数呢?在以下四种情况出现的时候:

1.当类中含有一个成员对象,而该对象的类中有一个复制构造函数的时候(无论是显式声明或者隐式合成)。此时,需要在合成出来的复制构造函数中调用该成员对象的复制构造函数。

2.类中的基类有复制构造函数时(显式或隐式)。此时,需要在合成出来的复制构造函数中调用该基类的复制构造函数。

3.类中存在虚函数时(继承的或自身声明的)。此时,需要在合成出来的复制构造函数中设置vptr(虚函数表指针)的指向。

4.类的继承链中存在虚基类的时候(无论是直接的还是间接的)。此时,需要在合成出来的复制构造函数中维护虚基类部分的位置信息。

上面的情况与默认构造函数的情况可以类比一下,要证明也可以模仿默认构造函数调用时的证明方式,我就偷偷懒了(。?_?。)

需要注意的是,默认情况下的复制操作都是浅复制(有指针时,只是复制了指针的值,而并没有复制指针指向的对象),要实现深复制(既复制指针的值,同时也复制指针指向的对象)时,还是需要我们亲自来操作的。

二、赋值操作符

要让赋值操作符(=)有效地执行我们指定的操作,有时需要显示重载等号操作符。当然,当合成的赋值操作符可以达到要求时,就没有必要再手动定义了,因为这可能还会使执行效率降低。那么什么时候需要显示重载该操作符呢?逐个成员赋值不能满足我们需求的时候,典型的情况就是类中有指针成员的时候。

我们说,当类中没有定义自己的赋值操作符,则编译器会合成一个。这是理论上的(很多事情都是理论上的,大家都懂的),实际上编译器不一定会合成相应的操作符函数。

那么什么时候编译器会合成呢?主要是以下四种情况(编译器永远只在自己需要的时候才合成相应的函数):

1.类中有一个成员对象,而该对象的类中有一个赋值操作符函数。这时,我们在为该成员对象赋值时需要调用该操作符。

2.类的基类中有赋值操作符。这时,我们在为基类对象(作为派生类对象的一部分)赋值时会调用该操作符。

3.类中声明了虚函数。此时不能直接复制右端vptr(虚函数指针)的值,因为它可能是一个派生类对象。

4.该类继承了虚基类。此时对象虚基类部分的位置可能会发生改变(如果从派生类对象赋值的话)。

可以简单地证明一下:

当只有如下简单的类定义时

class A

{

int a;

};

进行如下操作:

{

A a1;

A a2;

a2 = a1;

}

其反汇编代码如下图:

由此可见,其中并没有调用赋值操作符函数。

如果为上面的类添加一个虚函数(其它情况就不一一证明了),则同样的代码调用,其反汇编情况如下:

有上可见,其中调用了合成的默认构造函数和赋值操作符。

上面的四种情况可以对比复制构造函数合成的情况,其实赋值操作符的工作和复制构造函数也差不多,只是调用时机有所区别。实际上,我们应该将这两个操作看作一个单元。如果需要其中一个,我们几乎也肯定需要另一个。

赋值操作符必须定义为成员函数而不能是友元函数。这是语法规则上的限制,当然即使没有这个限制也不应该定义为为友元,毕竟调用的时候可能会让人不适应。例如:operator=(a1,a2)相对应a1 = a2这种方式,是不是显得有点别扭。

自己定义时有一种情况需要特别注意:自身赋值,尤其是有资源需要释放的时候。

正确的定义方式如下:

Class&  Class::operator=(const Class &rhs)

{

if(this == &rhs)

return *this;

//释放对象持有的资源

//分配指定的新资源

……

return *this;

}

如果没有判断自身赋值的情况直接释放原有资源肯定是要出事的,我想这就不用多说了。

三、析构函数

析构函数作为构造函数的补充,在构造函数中打开和分配所需的资源,在析构函数中关闭和释放已有的资源。

析构函数没有返回值,没有形参,就像是在默认构造函数的前面加了一个‘~’符号。如下:

class A{

……

~A(){cout<<”在析构函数中”<<endl;}//简单的析构函数

};

当对象被撤销时会自动调用析构函数,如果是动态分配的对象,只有在指向该对象的指针被删除时才撤销。注意:当对象的引用或指针(不论是栈指针还是接收new操作首地址的指针)超出作用域时,不会运行析构函数。只有删除指向动态分配对象的指针或实际对象(而不是对象的引用或指针)超出作用域时,才会运行析构函数。

示例代码如下:

1.栈指针的情况:

{

A  a;

{

A*pa = &a;

}//此时pa超出作用域,但并不运行A的析构函数

}//此时实例对象a超出作用域,将运行相应的析构函数

2.接收new操作符首地址的情况:

{

A*pa = new A();

//delete pa;在此处显示调用delete语句可以析构pa指向的对象

}//此时pa指针超出作用域,但并不调用析构函数;而且错过释放内存的机会,将造成内存泄漏。

3.引用的情况:

{

Aa;

{

A&pa = a;

}//此时a的引用pa超出作用域,但不会调用析构函数

}//此时对象a自身超出作用域,将会调用析构函数

当撤销一个容器(不论是标准库容器还是内置数组)时,也会运行容器中的类类型元素的析构函数。

示例代码如下:

{

AarrayA[3] = {A(1),A(2),A(3)};

vector<A>  vA(arrayA,arrayA + 3);

A *pa = new A[3];

delete [] pa;//调用pa中3个元素的析构函数

}//调用arrayA和vA中元素的析构函数

如果我们的类没有显示定义的析构函数,编译器会在需要的时候为我们合成一个。合成的析构函数按对象创建时的逆序撤销每个非static成员,即按成员在类中的声明次序的逆序撤销成员。对于类类型的成员,合成析构函数会调用该成员的析构函数来撤销对象。注意:合成析构函数并不删除指针成员所指向的对象。

上面说编译器将会在需要的时候合成析构函数,那么什么时候是所谓“需要的时候”?就是以下的两种情况:

1.class内的成员对象拥有析构函数(不论是合成的还是显示定义的)。

2.class的基类含有析构函数(不论是合成的还是显示定义的)。

这个时候为什么需要合成析构函数呢?因为编译器需要在合成的析构函数中调用上面对应的析构函数达到析构相应对象的目的。

例如有如下的类定义:

classVirtualBase

{

int vb;

};

classA:public virtual VirtualBase

{

public:

int a;

virtual void vfun(){}

};

当我们有如下代码时:

{

A a;

}

其反汇编的代码如下:

尽管有虚函数、虚继承这种复杂的机制,但是此时编译器依然没有为我们合成析构函数。因为在析构函数看来,虚函数和虚继承所带来的只是两个指针而已(可参考第一式),而撤销指针不需要额外的操作(调用一个析构函数此时反而是效率上的负担)。由上也可以看到编译器此时为我们合成了构造函数(可参考第四式)。

若有如下的类定义:

class B

{

string  name;

};

类似的代码:

{

B b;

}

此时反汇编情况如下:

我们可以看到后面调用了相应的析构函数。因为string类中定义了析构函数,编译器需要合成一个析构函数来调用string类成员对象的析构函数。

下面是析构函数的操作顺序:

1.  执行析构函数的函数体

2.  如果类中具有成员对象,而后者拥有析构函数,那么它们会以其声明顺序的相反顺序被调用。

3.  如果对象内含有虚函数指针,现在被重新设定,指向适当基类的虚函数表。

4.  如果有任何直接的非虚继承的基类,而后者有析构函数,它们会以其声明顺序的相反顺序被调用。

5.  如果有任何的虚基类有析构函数,而目前的这个类是最尾端的类(最后的非虚基类),那么它们的析构函数会以其原来的构造顺序相反的顺序被调用。

关于上面的顺序,只要定义一个合适的继承链,显示定义析构函数输出相应的信息就可以得出结论了。

最后还有一个实践得出的经验法则。三法则:如果类需要析构函数,则它也需要复制构造函数和赋值操作符。

时间: 2024-10-05 22:05:07

C++独孤九剑第五式——人生几何(对象复制控制)的相关文章

C++对象复制控制

C++中对象的复制通过一系列的构造函数或者赋值函数来实现的,包括copy construstor,move construstor,copy assignment operator,move assignment operator,实现过程中还可能有destructor的参与,一般情况下,这些函数编译器会为我们自动合成,但还有很多时候,编译器会将他们合成为delete,因此需要我们自己编写.也有一些时候,我们需要禁止或者强制编译器为我们合成这些函数. 首先看一下什么情况下编译器不会为我们自动合成

OpenSCAD通过循环快速复制几何对象

OpenSCAD支持变量和循环,从而可以快速复制出大量的几何对象并且按照递归的方式进行布局. 循环的变量可以是枚举.区间和矢量对象,循环体支持几何对象构建.坐标平移与旋转.交并差等操作. 循环的递归变量:     Vector(矢量): for (variable=<vector>) {     <do_something> - <variable> is assigned to each successive value in the vector }     Ran

第十九篇:复制控制( 下 ) --- 自定义析构函数

前言 经过前两篇随笔( 上  中 )的分析我们已经解决了具有指针成员的同类对象“ 干涉 ”问题.可惜,前面给出的解决方案代码还是不完整.还有什么问题呢?观察发现,构造函数里面有new的关键字出现,也就是说开辟了新的内存空间,我们也知道new必须也只能对应一个delete,而不应该让系统自己处理( 还不熟练new和delete用法的点这里 ),但这里和new对应的delete去哪里了? 解决思路 应该何时delete?显然,应该在对象销毁的时候delete.C++中定义了这样一种函数 --- 析构

对象复制、克隆、深度clone

-------------------------------------------------------------------------------- ------------------------------------------------- 知道Java对对象和基本的数据类型的处理是不一样的.和C语言一样,当把Java的基本数据类型(如int,char,double等)作为入口参数传给函数体的时候,传入的参数在函数体内部变成了局部变量,这个局部变量是输入参数的一个拷贝,所有的

稍微深入点理解C++复制控制【转】

通过一个实例稍微深入理解C++复制控制过程,参考资料<C++ primer>,介绍点基本知识: 1.在C++中类通过特殊的成员函数:复制构造函数.赋值操作符和析构函数来控制复制.赋值和撤销该类的对象时会发生什么. 2.复制构造函数(copy constructor)是一种特殊的构造函数,具有单个形参,该形参(常用const)是对该类类型的引用: 当定义一个新对象并用一个同类型的对象对它进行初始化时,将显示使用复制构造函数: 当将该类的对象传递给函数或从函数返回该类型的对象时,将隐式使用复制构造

C++拾遗(六)——复制控制

年前忙了几天,到现在才算是有空休息下来.先祝大家新年快乐,心想事成:)我也会发笑脸o.o 这篇博文主要介绍定义一个类型的对象时的复制控制方式,这部分内容之前有一定的了解但又浅尝辄止,始终感觉没能找到要点.年前又拿起书细细品读,算是有了一点新的了解.几天前就想动笔了,一直没时间,拖到现在. 每种类型定义了创建该类型的对象时会发生什么——构造函数定义了该类类型对象的初始化.类型还能控制复制.赋值或撤销该类型的对象时会发生什么——类通过特殊的成员函数:复制构造函数.赋值操作符和析构函数来控制这些行为.

C++之重载String ----- 构造函数、复制控制、重载操作符

本博文 我们通过 重新实现String类 来说明构造函数,复制控制,重载操作符. 一.构造函数(包括析构函数): 1:默认构造函数: 2:用户自己定义的构造函数 注意:当用户自己定义时,也要明确显示默认构造函数,这是因为,当我们没有定义自己的构造函数时,编译器会为我们自动合成一个,而我们定义了构造函数时,编译器默认构造函数改为我们自己定义的.这时就有可能出现错误: 3:析构函数: 具体声明与实现,代码如下: 1 声明部分: 2 String(); 3 String(const char*s);

C++复制控制

1.复制构造函数可用于: (1)根据另一个同类型的对象显示或隐式初始化一个对象 string str1="test";   //隐式 string str2=str1; //显示 str1为先调用string的字符串形参的构造函数,创建一个临时对象,然后,使用string复制构造函数将str1初始化为那个临时对象的副本. (2)复制一个对象,将它作为实参传给一个函数 (3)从函数返回时复制一个对象 当形参为非引用类型的时候,将复制实参的值.类似地,以非引用类型作返回值时,将返回retu

C++复制控制:拷贝构造函数

一.拷贝构造函数是一种特殊构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用.与默认构造函数一样 ,拷贝构造函数可由编译器隐式调用.拷贝构造函数应用的场合为: (1)根据另一个同类型的对象显式或隐式初始化一个对象. (2)复制一个对象将它作为实参传给一个函数. (3)从函数返回时复制一个对象. (4)初始化顺序容器中的元素. (5)根据元素初始化式列表初始化数组元素. 下面分别对以上5点进行说明. 1.对象的定义式. C++支持两种初始化形式:直接初始化和复制初始化.复制初始