C++中深拷贝与浅拷贝问题
C++中深拷贝与浅拷贝问题
在c++中,深拷贝与浅拷贝一直是一个难点(java中也一样,不过不常见),特别对于初学者来说,总是搞不懂其含义,搞不懂也就算了,有时候还会无意中使用了浅拷贝导致出错,对于这类错误,如果不理解深拷贝与浅拷贝的含义是无法检测出来的,对于我们程序员来说,检测不出bug在哪的确是件蛋疼的事,今天我就与大家一起探讨有关深拷贝与浅拷贝的一些问题,帮助大家理解。
废话不多说,先来一段代码(注意一些老的编译器可能不支持名字空间的头文件用.h结尾)
//MyString类的头文件
#ifndef MYSTRING_H_
#define MYSTRING_H_
#include<iostream>
class MyString{
private:
char *str; //字符指针
int length;//字符长度
static int num;//创建对象的数量
public:
MyString();
MyString(const char *s);
~MyString();
friend std::ostream & operator<<(std::ostream &os,const MyString & ms);//友元函数重载<<运算符
};
#endif
#include<cstring>
#include"MyString.h"
using namespace std;
int MyString::num=0;//初始化静态变量(别问我为什么这样初始化,记住就好了)
//构造函数
MyString::MyString(){
length=4;
str = new char[4];
strcpy(str,"C++");
num++;
cout << num << ": " << str << "对象被创建\n" ;
}
MyString::MyString(const char *s){
length=strlen(s);
str = new char[length+1];
strcpy(str,s);
num++;
cout << num << ": " << str << "对象被创建\n" ;
}
//析构函数
MyString::~MyString(){
cout << "\"" << str << "\" 对象被释放";
num--;
cout << " 剩余 " << num << " 个对象\n";
delete []str;//释放heap内存
}
//重载<<运算符
std::ostream & operator<<(std::ostream &os,const MyString & ms){
os << ms.str;
return os;
}
#include<iostream>
#include"MyString.h"
using namespace std;
//注意一个是传值,一个传引用
void fun1(MyString &ms);
void fun2(MyString ms);
int main()
{
MyString ms1;
MyString ms2("Java");
MyString ms3("Python");
cout << "ms1 " << ms1 << endl;
cout << "ms2 " << ms2 << endl;
cout << "ms3 " << ms3 << endl;
fun1(ms1);
cout << "ms1 " << ms1 << endl;
fun2(ms2);
cout << "ms2 " << ms2 << endl;
MyString ms4 = ms3;
cout << "ms4 " << ms4 << endl;
return 0;
}
void fun1(MyString &ms){
cout << "fun1被调用 : " << ms << endl;
}
void fun2(MyString ms){
cout << "fun2被调用 : " << ms << endl;
}
这是我的运行结果,很明显出错了。
可是问题出在哪呢,从代码上看没问题,就连一编译连个警告也不给我,这个问题的确很多蛋疼。
首先我们来看我们自己写的类的实现文件,在构造函数中我们使用了 new 关键字,这是必须的,因为我们不不知道传进来的字符串有多大,所以必须动态分配内存。然后,我们也知道c++必须手动释放内存(就这一点还是java方便)所以在析构函数中必须使用delete关键字来释放,这点我也不多说了,学过c++都知道的。
下面我们一步一步的分析。
直到fun1()函数被调用之前,一切都正常运行
当调用fun2()的时候,第一个问题来了,fun2()函数打印正常,但是紧接着对象被释放,这点我们也好理解,因为fun2()函数传递的是值而不是引用,传值就相当与传递ms2的一个副本所以fun2(ms2)就相当于fun2(MyString(ms2))重新创建了一个对象。在这里我们补充一点,在c++中,按值传递对象或者赋值给另一个对象,编译器会自动调用一个叫做复制构造函数:MyString(MyString &);所以调用fun2()的时候,编译器拷贝了一个ms2副本给fun2的局部变量。当然,打印什么的都很正常。但是当到达函数结束时,析构函数会自己调用,同时释放str的内存。
也许你们会说,这也没错,的确很正常,正常到出了错我们都还不知道,就在这里涉及到了深拷贝与浅拷贝额问题,在这里的传值用的是浅拷贝,那什么是深拷贝,什么又是浅拷贝呢?
如上图所示:所谓浅拷贝,就是原封不动的全部都拷贝过去。当然静态变量不会拷贝。
既然是原封不动的拷贝过去,那么我们知道str里面存的是地址,所以fun2里面的ms的str拷过去的也是地址
看到这里我想大家都明白了吧,在fun2中已经释放过对象,也就是说,str所指向的“Java”这个字符串内存已经被释放,那么当ms2企图再想访问的时候,打印出来的自然是乱码了。
接着,我们把ms3的值赋给ms4,这相当于ms4=MyString(ms3);我前面讲过,就不重复了。
我们来看最后一段,首先ms4的对象被释放。由于局部变量储存在stack里,所以遵循后进先出的模式(LIFO)那么当ms4被释放以后,接下来释放的是ms3,然后大家看到ms3中的str已经被释放,再释放一次的话,自然程序就崩溃了。
细心的同学也许发现了一点,就是当前最后一条是运行到释放ms3就被终止,那么如果在释放ms2与ms1呢,那么剩余的不就是负值了吗。的确,那么这又是什么原因呢?其实我前面也说过,当复制或赋值对象的时候会调用默认的复制构造函数,既然是默认的,那么肯定不会帮我们把num++吧,编译器不会那么智能吧(要是真有那么智能我们就不用编代码了,直接让电脑自己编不就得了)。所以我们复制与赋值各一个,分别是在调用fun2()函数与ms4=ms3的时候。所以析构函数里num多减了两次,结果自然是-2了。
讲了那么多,还没讲什么是深拷贝,通俗易懂,所谓深拷贝就是比浅拷贝更深的拷贝呗(好像是废话。。)
也就是说,深拷贝不只是原封不动的拷贝,他会把str的内容拷贝了,而不是地址,所谓拷贝内容,就是重新开辟内存,然后复制内容。
接下来修改一下代码
//MyString类的头文件
#ifndef MYSTRING_H_
#define MYSTRING_H_
#include<iostream>
class MyString{
private:
char *str; //字符指针
int length;//字符长度
static int num;//创建对象的数量
public:
MyString();
MyString(const char *s);
MyString(const MyString &ms);
~MyString();
friend std::ostream & operator<<(std::ostream &os,const MyString & ms);//友元函数重载<<运算符
};
#endif
#include<cstring>
#include"MyString.h"
using namespace std;
int MyString::num=0;//初始化静态变量(别问我为什么这样初始化,记住就好了)
//构造函数
MyString::MyString(){
length=4;
str = new char[4];
strcpy(str,"C++");
num++;
cout << num << ": " << str << "对象被创建\n" ;
}
MyString::MyString(const char *s){
length=strlen(s);
str = new char[length+1];
strcpy(str,s);
num++;
cout << num << ": " << str << "对象被创建\n" ;
}
MyString::MyString(const MyString &ms){
num++;
length = ms.length;
str = new char[length+1];
strcpy(str,ms.str);
}
//析构函数
MyString::~MyString(){
cout << "\"" << str << "\" 对象被释放";
num--;
cout << " 剩余 " << num << " 个对象\n";
delete []str;//释放heap内存
}
//重载<<运算符
std::ostream & operator<<(std::ostream &os,const MyString & ms){
os << ms.str;
return os;
}
#include<iostream>
#include"MyString.h"
using namespace std;
//注意一个是传值,一个传引用
void fun1(MyString &ms);
void fun2(MyString ms);
int main()
{
{
MyString ms1;
MyString ms2("Java");
MyString ms3("Python");
cout << "ms1 " << ms1 << endl;
cout << "ms2 " << ms2 << endl;
cout << "ms3 " << ms3 << endl;
fun1(ms1);
cout << "ms1 " << ms1 << endl;
fun2(ms2);
cout << "ms2 " << ms2 << endl;
MyString ms4=ms3;
cout << "ms4 " << ms4 << endl;
}
return 0;
}
void fun1(MyString &ms){
cout << "fun1被调用 : " << ms << endl;
}
void fun2(MyString ms){
cout << "fun2被调用 : " << ms << endl;
}
只要我们自己提供复制构造函数,并且进行深拷贝就不会出错啦。