C++类对象的深拷贝、浅拷贝构造函数

时间:2021-12-03 19:54:00

在学习这一章内容前我们已学习过了类的构造函数和析构函数的相关知识,对于普通类型的对象来说,他们之间的复制是非常简单的,例如:

 

int a = 10; 
int b =a;

 

自己定义的类的对象同样是对象,谁也不能阻止我们用以下的方式进行复制,例如:

 

#include <iostream>
usingnamespacestd;

classTest
{
public:
Test(inttemp)
{
p1=temp;
}
protected:
intp1;

};

voidmain()
{
Test a(99);
Test b=a;
}

 

普通对象和类对象同为对象,他们之间的特性有相似之处也有不同之处,类对象内部存在成员变量,而普通对象是没有的,当同样的复制方法发生在不同的对象上的时候,那么系统对他们进行的操作也是不相同的,就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的,在上面的代码中,我们并没有看到拷贝构造函数,同样完成了复制工作,这又是为什么呢?因为当一个类没有自定义的拷贝构造函数的时候系统会自动提供一个默认的拷贝构造函数,来完成复制工作。

下面,我们为了说明情况,就普通情况而言(以上面的代码为例),我们来自己定义一个和系统默认拷贝构造函数相同的拷贝构造函数,看看他的内部是怎么工作的!

 

代码如下:

 

#include <iostream>
usingnamespacestd;

classTest
{
public:
Test(inttemp)
{
p1=temp;
}
Test(Test &c_t)//这里就是自定义的拷贝构造函数
{
cout<<"进入copy构造函数"<p1=c_t.p1;//这句如果去掉就不能完成复制工作了,此句复制过程的核心语句
}
public:
intp1;
};

voidmain()
{
Test a(99);
Test b=a;
cout<cin.get();
}

 

上面代码中的Test(Test &c_t)就是我们自定义的拷贝构造函数,拷贝构造函数的名称必须和类名称一致,函数的形式参数是本类型的一个引用变量,且必须是引用。

当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用,如果你没有自定义拷贝构造函数的时候系统将会提供给一个默认的拷贝构造函数来完成这个过程,上面代码的复制核心语句就是通过Test(Test &c_t)拷贝构造函数内的p1=c_t.p1;语句完成的。如果取掉这句代码,那么b对象的p1属性将得到一个未知的随机值;

下面我们来讨论一下关于浅拷贝和深拷贝的问题。

就上面的代码情况而言,非常多人会问到,既然系统会自动提供一个默认的拷贝构造函数来处理复制,那么我们没有意义要去自定义拷贝构造函数呀,对,就普通情况而言这的确是没有必要的,但在某写状况下,类体内的成员是需要开辟动态开辟堆内存的,如果我们不自定义拷贝构造函数而让系统自己处理,那么就会导致堆内存的所属权产生混乱,试想一下,已开辟的一端堆地址原来是属于对象a的,由于复制过程发生,b对象取得是a已开辟的堆地址,一旦程式产生析构,释放堆的时候,计算机是不可能清晰这段地址是真正属于谁的,当连续发生两次析构的时候就出现了运行错误。

为了更周详的说明问题,请看如下的代码。

 

#include <iostream>
usingnamespacestd;

classInternet
{
public:
Internet(char*name,char*address)
{
cout<<"载入构造函数"<strcpy(Internet::name,name);
strcpy(Internet::address,address);
cname=newchar[strlen(name)+1];
if(cname!=NULL)
{
strcpy(Internet::cname,name);
}
}
Internet(Internet &temp)
{
cout<<"载入COPY构造函数"<strcpy(Internet::name,temp.name);
strcpy(Internet::address,temp.address);
cname=newchar[strlen(name)+1];//这里注意,深拷贝的体现!
if(cname!=NULL)
{
strcpy(Internet::cname,name);
}
}
~Internet()
{
cout<<"载入析构函数!";
delete[] cname;
cin.get();
}
voidshow();
protected:
charname[20];
charaddress[30];
char*cname;
};
voidInternet::show()
{
cout<}
voidtest(Internet ts)
{
cout<<"载入test函数"<}
voidmain()
{
Internet a("中国软件研发实验室","www.cndev-lab.com");
Internet b =a;
b.show();
test(b);
}

 

上面代码就演示了深拷贝的问题,对对象b的cname属性采取了新开辟内存的方式避免了内存归属不清所导致析构释放空间时候的错误,最后我必须提一下,对于上面的程式我的解释并不多,就是希望读者本身运行程式观察变化,进而深刻理解。

深拷贝和浅拷贝的定义能简单理解成:如果一个类拥有资源(堆,或是其他系统资源),当这个类的对象发生复制过程的时候,这个过程就能叫做深拷贝,反之对象存在资源但复制过程并未复制资源的情况视为浅拷贝。

浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程式运行出错,这点尤其需要注意!

 

 

 

 

C++中对象的复制就如同“克隆”,用一个已有的对象快速地复制出多个完全相同的对象。一般而言,以下三种情况都会使用到对象的复制:

(1)建立一个新对象,并用另一个同类的已有对象对新对象进行初始化,例如:

class Rect

{

private:

int width;

int height;

};

Rect rect1;

Rect rect2(rect1); // 使用rect1初始化rect2,此时会进行对象的复制

(2)当函数的参数为类的对象时,这时调用此函数时使用的是值传递,也会产生对象的复制,例如:

void fun1(Rect rect)

{

...

}

int main()

{

Rect rect1;

fun1(rect1); // 此时会进行对象的复制

return 0;

}

(3)函数的返回值是类的对象时,在函数调用结束时,需要将函数中的对象复制一个临时对象并传给改函数的调用处,例如:

Rect fun2()

{

Rect rect;

return rect;

}

int main()

{

Rect rect1;

rect1=fun2();

// 在fun2返回对象时,会执行对象复制,复制出一临时对象,

// 然后将此临时对象“赋值”给rect1

return 0;

}

对象的复制都是通过一种特殊的构造函数来完成的,这种特殊的构造函数就是拷贝构造函数(copy constructor,也叫复制构造函数)。拷贝构造函数在大多数情况下都很简单,甚至在我们都不知道它存在的情况下也能很好发挥作用,但是在一些特殊情况下,特别是在对象里有动态成员的时候,就需要我们特别小心地处理拷贝构造函数了。下面我们就来看看拷贝构造函数的使用。

一、默认拷贝构造函数

很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它一般具有以下形式:

Rect::Rect(const Rect& r)

{

width = r.width;

height = r.height;

}

当然,以上代码不用我们编写,编译器会为我们自动生成。但是如果认为这样就可以解决对象的复制问题,那就错了,让我们来考虑以下一段代码:

class Rect

{

public:

Rect() // 构造函数,计数器加1

{

count++;

}

~Rect() // 析构函数,计数器减1

{

count--;

}

static int getCount() // 返回计数器的值

{

return count;

}

private:

int width;

int height;

static int count; // 一静态成员做为计数器

};

int Rect::count = 0; // 初始化计数器

int main()

{

Rect rect1;

cout<<"The count of Rect: "<

 

二、浅拷贝

所谓浅拷贝,指的是在对象复制时,只是对对象中的数据成员进行简单的赋值,上面的例子都是属于浅拷贝的情况,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了,让我们考虑如下一段代码:

class Rect

{

public:

Rect() // 构造函数,p指向堆中分配的一空间

{

p = new int(100);

}

~Rect() // 析构函数,释放动态分配的空间

{

if(p != NULL)

{

delete p;

}

}

private:

int width;

int height;

int *p; // 一指针成员

};

int main()

{

Rect rect1;

Rect rect2(rect1); // 复制对象

return 0;

}

在这段代码运行结束之前,会出现一个运行错误。原因就在于在进行对象复制时,对于动态分配的内容没有进行正确的操作。我们来分析一下:

在运行定义rect1对象后,由于在构造函数中有一个动态分配的语句,因此执行后的内存情况大致如下:

C++类对象的深拷贝、浅拷贝构造函数

在使用rect1复制rect2时,由于执行的是浅拷贝,只是将成员的值进行赋值,所以此时rect1.p和rect2.p具有相同的值,也即这两个指针指向了堆里的同一个空间,如下图所示:

C++类对象的深拷贝、浅拷贝构造函数

当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。

 

 

三、深拷贝

在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:

class Rect

{

public:

Rect() // 构造函数,p指向堆中分配的一空间

{

p = new int(100);

}

Rect(const Rect& r)

{

width = r.width;

height = r.height;

p = new int; // 为新对象重新动态分配空间

*p = *(r.p);

}

~Rect() // 析构函数,释放动态分配的空间

{

if(p != NULL)

{

delete p;

}

}

private:

int width;

int height;

int *p; // 一指针成员

};

此时,在完成对象的复制后,内存的一个大致情况如下:

C++类对象的深拷贝、浅拷贝构造函数

此时rect1的p和rect2的p各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”。

此外,在与“对象的复制”很类似的“对象的赋值”的情况下,也会出现同样的问题。在“对象的赋值”一文中再来讨论此问题。

通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。

Rect rect2(rect1); // 使用rect1复制rect2,此时应该有两个对象

cout<<"The count of Rect: "<

return 0;

}

这段代码对前面的类进行了一下小小的修改,加入了一个静态成员,目的是进行计数,统计创建的对象的个数,在每个对象创建时,通过构造函数进行递增,在销毁对象时,通过析构函数进行递减。在主函数中,首先创建对象rect1,输出此时的对象个数,然后使用rect1复制出对象rect2,再输出此时的对象个数,按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。出现这些问题最根本就在于在复制对象时,计数器没有递增,解决的办法就是重新编写拷贝构造函数,在拷贝构造函数中加入对计数器的处理,形成的拷贝构造函数如下:

class Rect

{

public:

Rect() // 构造函数,计数器加1

{

count++;

}

Rect(const Rect& r) // 拷贝构造函数

{

width = r.width;

height = r.height;

count++; // 计数器加1

}

~Rect() // 析构函数,计数器减1

{

count--;

}

static int getCount() // 返回计数器的值

{

return count;

}

private:

int width;

int height;

static int count; // 一静态成员做为计数器

};

自己编写拷贝构造函数又可以分为两种情况——浅拷贝与深拷贝。