zl程序教程

您现在的位置是:首页 >  其它

当前栏目

何时需要自定义复制构造函数?

自定义 需要 复制 构造函数 何时
2023-09-11 14:15:57 时间

本文涉及对象的赋值和复制(也称为克隆)。必要时,先看谭浩强教材P291-295的相关内容或PPT,重温一下有关概念。

一、一般情况

先看一个例子:

//例程1

#include iostream 

using namespace std;

class Complex

public:

 Complex(){real=0;imag=0;}

 Complex(double r,double i){real=r;imag=i;}

 friend Complex operator+(const Complex c1, const Complex c2);

 friend ostream operator (ostream output,const Complex c);

private:

 double real;

 double imag;

//复数相加:(a+bi)+(c+di)=(a+c)+(b+d)i. 

Complex operator+(const Complex c1, const Complex c2)

 Complex c;

 c.real=c1.real+c2.real;

 c.imag=c1.imag+c2.imag;

 return c;

//输出的运算符重载

ostream operator (ostream output,const Complex c)

{ output "(" c.real;

 if(c.imag =0) output "+"; 

 output c.imag "i)"; 

 return output;

int main()

 Complex c1(3,4),c2(5,-10),c3;

 cout "c1=" c1 endl;

 cout "c2=" c2 endl;

 c3=c1+c2;

 cout "c1+c2=" c3 endl;;

 system("pause");

 return 0;

注意对相加运算重载函数的定义:

Complex operator+(const Complex c1, const Complex c2)

 Complex c;

 c.real=c1.real+c2.real;

 c.imag=c1.imag+c2.imag;

 return c;

}
从变量的作用域角度讲,Complex c是函数的局部变量,意味着当函数执行完后,c占用的内存空间将被释放。那么,在main()函数中调用c3=c1+c2时,c3能够得到正确的结果吗?答案是肯定的,在operate+(c1,c2)的最后,执行return c 的时候,c 被返回,通过c3=c1+c2中的赋值(=),c 的值被(复制)赋值给了c3,在完成使命之后,c 潇洒谢幕。

对象的赋值(=)运算符的重载是默认的,不需要专门定义对“=”的重载去完成自定义类中对象的赋值。但是,这里有一个前提,类中不能包括动态分配的数据,否则“可能出现严重的后果”(谭浩强教材P293页)。那这个严重的后果是什么呢?稍后讲。

与对象的赋值相类似的还有对象的复制。其实,在函数返回值为对象的时候,系统会将其中返回的对象复制出一个新的临时对象,并传递给该函数的调用处。在复制中,需要用到复制构造函数,但这个复制构造函数一般也不需要用户定义,系统可以自动完成。这个“一般”暗示着什么?返回的对象中不能包括动态分配的数据。

那我们勇敢一些,去以身试法,看看如果对象中包含了动态分配的数据后究竟会发生什么事情。领教一下后果不是目的,目的在于找到解决的办法。因为这也是实际应用中必须面临的问题。


二、以身试法——返回包含动态分配数据的临时对象

也从一个例子开始。下面的例子建立一个二维数组类Douary,完成矩阵的输入、输出和相加操作。与例程1 的区别是,数据成员中有指针,并其指针指向的空间在构造函数中动态分配,在析构函数中释放。

//例程2

#include iostream 

using namespace std;

class Douary

public:

 Douary(int m, int n);//构造函数:用于建立动态数组存放m行n列的二维数组(矩阵)元素,并将该数组元素初始化为0

 ~Douary(); //析构函数:用于释放动态数组所占用的存储空间

 friend istream operator (istream input, Douary //重载运算符“ ”输入二维数组,其中d为Dousry类对象;

 friend ostream operator (ostream output, Douary //重载运算符“ ”以m行n列矩阵的形式输出二维数组,其中d为Douary类对象。

 friend Douary operator+(const Douary d1,const Douary d2);//两个矩阵相加,规则:对应位置上的元素相加

private:

 int *Array; //Array 为动态数组指针。

 int row; //row 为二维数组的行数。

 int col; //col 为二维数组的列数。

Douary::Douary(int m, int n) //构造函数:用于建立动态数组存放m行n列的二维数组(矩阵)元素,并将该数组元素初始化为

 row=m;

 col=n;

 Array = new int[m*n];

 for(int i=0; i ++i)

 for(int j=0; j ++j)

 Array[i*col+j]=0;

Douary::~Douary() //析构函数:用于释放动态数组所占用的存储空间

 delete [] Array;

istream operator (istream input, Douary d)//重载运算符“ ”输入二维数组,其中d为Dousry类对象

 for(int i=0; i d.row; ++i)

 for(int j=0; j d.col; ++j)

 cin d.Array[i*d.col+j];

 return input;

ostream operator (ostream output, Douary d)//重载运算符“ ”以m行n列矩阵的形式输出二维数组,其中d为Douary类对象

 for(int i=0; i d.row; ++i)

 for(int j=0; j d.col; ++j)

 cout d.Array[i*d.col+j] "\t";

 cout endl;

 cout endl;

 return output;

Douary operator+(const Douary d1,const Douary d2)//两个矩阵相加,规则:对应位置上的元素相加

 //在此可以先判断d1和d2的行列是否相同,如果不相同可以报错退出,不做运算。本参考解答忽略了这一前提

 Douary d(d1.row,d1.col);

 for(int i=0; i d1.row; ++i)

 for(int j=0; j d1.col; ++j)

 d.Array[i*d1.col+j]=d1.Array[i*d1.col+j]+d2.Array[i*d1.col+j];

 return d;

int main()

 Douary d1(2,3),d2(2,3);

 cout "输入d1(2,3):" endl;

 cin d1;

 cout "输入d2(2,3):" endl;

 cin d2;

 cout "d1+d2=" endl;

 Douary d3=d1+d2;

 cout d3;

 system("pause");

 return 0;

在operate+函数中,与例程1的operate+ 一样,声明了一个临时的局部变量,经过一些运算后,函数返回这个局部变量。

那结果又如何呢?看来是领教“严重后果”的时候了。

程序运行的结果是这样的:

输入d1(2,3):

1 2 3

4 5 6

输入d2(2,3):

9 8 7

6 5 4

d1+d2=

-17891602 1 0

0 -33686019 -1414812757

请按任意键继续. . .
我们看到,相加结果是错误的!在某些时候,类似的程序是弹出一个窗口,报告内存溢出。

原因何在?在例程2中,第62 行return d; 后仍然也执行默认的复制构造函数将 d 对象复制给main()函数中的一个临时变量再赋值给了对象d3(第73行),复制完后,d 的空间被释放。d3的Array(指针)指向的空间,此时显然已经不能由d3继续使用,而是可以被系统分配了。d3的Array指向一个无法控制的空间,后果真的很严重。


三、解决办法

究其原因,是因为默认的构造函数过于简单,干不了复制“带有需要动态分配空间的数据成员”类的“瓷器活”。实际上,当类中无动态分配空间的数据成员时,复制工作也就是对应的成员逐一复制,而有了动态分配空间的数据成员,那是各有各的样,没法统一。于是在这个时候,需要我们做的是,自己定义复制构造函数,关键是在复制的时时候,动态分配相应的空间,将完整的对象复制下来。

例程2改进之后为:(注意新增加的复制构造函数Douary(const Douary 的声明(第8行)和定义(第28-36行)即可,其他位置同例程2完全一样)

#include iostream 

using namespace std;

class Douary

public:

 Douary(int m, int n);//构造函数:用于建立动态数组存放m行n列的二维数组(矩阵)元素,并将该数组元素初始化为0

 ~Douary(); //析构函数:用于释放动态数组所占用的存储空间

 Douary(const Douary //复制构造函数

 friend istream operator (istream input, Douary //重载运算符“ ”输入二维数组,其中d为Dousry类对象;

 friend ostream operator (ostream output, Douary //重载运算符“ ”以m行n列矩阵的形式输出二维数组,其中d为Douary类对象。

 friend Douary operator+(const Douary d1,const Douary d2);//两个矩阵相加,规则:对应位置上的元素相加

private:

 int *Array; //Array 为动态数组指针。

 int row; //row 为二维数组的行数。

 int col; //col 为二维数组的列数。

Douary::Douary(int m, int n) //构造函数:用于建立动态数组存放m行n列的二维数组(矩阵)元素,并将该数组元素初始化为

 row=m;

 col=n;

 Array = new int[m*n];

 for(int i=0; i ++i)

 for(int j=0; j ++j)

 Array[i*col+j]=0;

Douary::Douary(const Douary d)

 row=d.row;

 col=d.col;

 Array = new int[row*col];

 for(int i=0; i ++i)

 for(int j=0; j ++j)

 Array[i*col+j]=d.Array[i*col+j];

Douary::~Douary() //析构函数:用于释放动态数组所占用的存储空间

 delete [] Array;

istream operator (istream input, Douary d)//重载运算符“ ”输入二维数组,其中d为Dousry类对象

 for(int i=0; i d.row; ++i)

 for(int j=0; j d.col; ++j)

 cin d.Array[i*d.col+j];

 return input;

ostream operator (ostream output, Douary d)//重载运算符“ ”以m行n列矩阵的形式输出二维数组,其中d为Douary类对象

 for(int i=0; i d.row; ++i)

 for(int j=0; j d.col; ++j)

 cout d.Array[i*d.col+j] "\t";

 cout endl;

 cout endl;

 return output;

Douary operator+(const Douary d1,const Douary d2)//两个矩阵相加,规则:对应位置上的元素相加

 //在此可以先判断d1和d2的行列是否相同,如果不相同可以报错退出,不做运算。本参考解答忽略了这一前提

 Douary d(d1.row,d1.col);

 for(int i=0; i d1.row; ++i)

 for(int j=0; j d1.col; ++j)

 d.Array[i*d1.col+j]=d1.Array[i*d1.col+j]+d2.Array[i*d1.col+j];

 return d;

int main()

 Douary d1(2,3),d2(2,3);

 cout "输入d1(2,3):" endl;

 cin d1;

 cout "输入d2(2,3):" endl;

 cin d2;

 cout "d1+d2=" endl;

 Douary d3=d1+d2;

 cout d3;

 system("pause");

 return 0;

四、总结

当类中的数据成员需要动态分配存储空间时,不可以依赖默认的复制构造函数。在需要时(包括这种对象要赋值、这种对象作为函数参数要传递、函数返回值为这种对象等情况),要考虑到自定义复制构造函数。

另外,复制构造函数一经定义,赋值运算也按新定义的复制构造函数执行。



本文完



【C++要笑着学】类的默认成员函数详解 | 构造函数 | 析构函数 | 构造拷贝函数(二) 朋友们好啊,今天终于更新了。我是柠檬叶子C,本章将继续讲解C++中的面向对象的知识点,本篇主要讲解默认成员函数中的构造函数、析构函数和拷贝构造函数。还是和以前一样,我们将由浅入深地去讲解,以 初学者 的角度去探索式地学习。会一步步地推进讲解,而不是直接把枯燥的知识点倒出来,应该会有不错的阅读体验。如果觉得不错,可以 一键三连 支持一下博主!你们的关注就是我更新的最大动力!Thanks ♪ (・ ・)ノ
【C++要笑着学】类的默认成员函数详解 | 构造函数 | 析构函数 | 构造拷贝函数(一) 朋友们好啊,今天终于更新了。我是柠檬叶子C,本章将继续讲解C++中的面向对象的知识点,本篇主要讲解默认成员函数中的构造函数、析构函数和拷贝构造函数。还是和以前一样,我们将由浅入深地去讲解,以 初学者 的角度去探索式地学习。会一步步地推进讲解,而不是直接把枯燥的知识点倒出来,应该会有不错的阅读体验。如果觉得不错,可以 一键三连 支持一下博主!你们的关注就是我更新的最大动力!Thanks ♪ (・ ・)ノ
贺利坚 烟台大学计算机学院教师,建设系列学习资源,改革教学方法,为IT菜鸟建跑道,让大一的孩子会编程,为迷茫的大学生出主意,一起追求快乐的大学。 著书《逆袭大学:传给IT学子的正能量》,帮助处于迷茫中的大学