在 C++ 中,由于存在指针与引用这类概念,便会产生这种类的实例之间复制时底层原理不同的问题。本文将通过编写一个 string 类来讲讲,到底什么是深复制、浅复制。
1. 引入
尝试阅读下面一段代码:
#include <iostream>
#include <cstring>
using namespace std;
class String { //模仿C++的string类
char * str;
public:
String(const char * s); //构造函数1:通过字符串常量来初始化
String(); //构造函数2:默认构造函数
~String(); //析构函数
void OutPut();
};
String::String(const char * s) {
int len = strlen(s); //检查字符串的长度
str = new char[len + 1]; //给字符串分配存储空间,并用指针指向
strcpy(str, s); //把字符串常量复制到存储空间中
}
String::String() {
int len = 4; //默认字符串长度为4
str = new char[4]; //给字符串分配存储空间,并用指针指向
strcpy(str, ""); //把字符串常量复制到存储空间中,默认为空
}
String::~String() {
delete [] str; //删除曾给字符串分配的空间
}
void String::OutPut() {
cout << str << endl; //输出字符串
}
int main() {
String name("Huizhou University");
name.OutPut();
return 0;
}
运行结果:
Huizhou University
--------------------------------
Process exited after 0.6706 seconds with return value 0
这段代码演示了一个类似C++的string类的功能,当然,这只是一个简化版。接下来,我们通过这个程序来讲解今天的主题——深复制与浅复制。
2. 发现问题
相信,学习C语言已久的你受够了必须要使用库函数 cstring 的 strcpy() 来复制字符数组的事实。前文的程序既然作为一个类似string的类,我们要求他能做许多事情。就如便捷地将一个字符串赋值给另一个字符串。
就前文这个程序而言,能否实现这点呢?
我们来大胆地尝试一下,在 return 0; 之前添加了两行代码:
#include <iostream>
#include <cstring>
using namespace std;
class String { //模仿C++的string类
char * str;
public:
String(const char * s); //构造函数1:通过字符串常量来初始化
String(); //构造函数2:默认构造函数
~String(); //析构函数
void OutPut();
};
String::String(const char * s) {
int len = strlen(s); //检查字符串的长度
str = new char[len + 1]; //给字符串分配存储空间,并用指针指向
strcpy(str, s); //把字符串常量复制到存储空间中
}
String::String() {
int len = 4; //默认字符串长度为4
str = new char[4]; //给字符串分配存储空间,并用指针指向
strcpy(str, ""); //把字符串常量复制到存储空间中,默认为空
}
String::~String() {
delete [] str; //删除曾给字符串分配的空间
}
void String::OutPut() {
cout << str << endl; //输出字符串
}
int main() {
String name("Huizhou University");
name.OutPut();
//新添加的代码
String name2(name);
name2.OutPut();
return 0;
}
若你执行编译,你可能会惊讶地发现它竟然能通过编译,但是问题却出现在了运行结束时:
运行结果:
Huizhou University
Huizhou University
--------------------------------
Process exited after 3.015 seconds with return value 3221226356
它的确能够输出两行相同的字符串,但返回值告诉我们,程序并未正常地结束(因为其返回值并不是 0)。
为什么呢?
3. 分析问题
在解释为什么之前,我们先要了解一个概念:复制构造函数。
既然名中有“构造函数”四字,说明它的确是一种构造函数,只不过它有点特殊而已。它的原型通常是:
ClassName(const ClassName & AnotherClass)
它与普通的构造函数一样,在每一个类中,也有默认的复制构造函数(只要你还未显式地定义它)。
默认复制构造函数的默认实现是这样的:
ClassName(const ClassName & AnotherClass) {
member1 = AnotherClass.member1;
member2 = AnotherClass.member2;
member3 = AnotherClass.member3;
//...
}
没错,它会默认地把 AnotherClass 中的所有成员变量 赋值给 ClassName 的所有成员变量。
回到前文的程序中,我们也用到了系统自动给我们实现的默认复制构造函数。它应是这样的:
String::String (const String & s) {
str = s.str;
}
没错,你只是把一个 str 指针(所指向的地址)赋值过去了(这也是本文的主角之一:浅复制)。name2 中的 str 与 name 中的 str 指针都是指向同一个动态存储空间。
通过前文知道,str 是一个指向通过运算符 new 创建的动态空间的指针。在程序结束前,类会调用析构函数。而类析构函数中有运用运算符 delete 归还动态空间的行为。
而在函数 main 中,我们声明了两个类:name, name2,在这两个类调用析构函数时,它尝试把同一个空间归还了两次!这样,程序在第二次归还空间时,发现找不到这个空间。因为前一个空间已经被归还,从而导致运行时错误。
这就是问题的根源所在。
4. 解决问题
我们如何避免这个错误呢?
很简单,只要在我们复制的时候,另外地创建一个新的空间来存储(这也是本文的另一个主角:深复制),那么当析构函数被调用时,程序便能分别将两个空间归还。如:
String(const String & s); //函数原型
String::String(const String & s) { //函数实现
int len = strlen(s.str); //检查字符串的长度
str = new char[len + 1]; //给字符串分配存储空间,并用指针指向
strcpy(str, s.str); //把字符串常量复制到存储空间中
}
完整程序便是:
#include <iostream>
#include <cstring>
using namespace std;
class String { //模仿C++的string类
char * str;
public:
String(const char * s); //构造函数1:通过字符串常量来初始化
String(); //构造函数2:默认构造函数
~String(); //析构函数
void OutPut();
String(const String & s);
};
String::String(const char * s) {
int len = strlen(s); //检查字符串的长度
str = new char[len + 1]; //给字符串分配存储空间,并用指针指向
strcpy(str, s); //把字符串常量复制到存储空间中
}
String::String() {
int len = 4; //默认字符串长度为4
str = new char[4]; //给字符串分配存储空间,并用指针指向
strcpy(str, ""); //把字符串常量复制到存储空间中,默认为空
}
String::~String() {
delete [] str; //删除曾给字符串分配的空间
}
void String::OutPut() {
cout << str << endl; //输出字符串
}
String::String(const String & s) { //函数实现
int len = strlen(s.str); //检查字符串的长度
str = new char[len + 1]; //给字符串分配存储空间,并用指针指向
strcpy(str, s.str); //把字符串常量复制到存储空间中
}
int main() {
String name("Huizhou University");
name.OutPut();
String name2(name);
name2.OutPut();
return 0;
}
运行结果:
Huizhou University
Huizhou University
--------------------------------
Process exited after 1.17 seconds with return value 0
至此,程序的确正常运行了。
5. 总结
本文通过编写一个 string 类来讲解了深复制与浅复制。
- 浅复制:对类进行逐个成员的复制。若成员是一个值,则复制值。若成员是一个指针,则复制指针。
- 深复制:对类进行逐个成员的复制。若成员是一个值,则复制值。若成员是一个指针,则另外对该指针所指向的内容分配空间,并复制相应值。
了解复制构造函数。
注意,当我们的类成员变量包含指针类型时,应利用运算符 new 及 delete 人工编写构造函数、析构函数以及复制构造函数(其实还应编写重载赋值运算符,但其不在本文讨论范围)以避免发生内存错误。
本文作者:以成
本文链接:
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 。转载请注明本文作者与链接!