C++类的浅拷贝深拷贝以及写时拷贝问题
Posted 与偶然一起学编程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++类的浅拷贝深拷贝以及写时拷贝问题相关的知识,希望对你有一定的参考价值。
平时我们在编写代码时经常会使用字符串进行相关操作,在C语言中,没有提供字符串这样的类型,但是在C++中,STL为我们实现string类这样的近容器,使得我们在编写代码中非常方便。该类维护一个char指针,并封装和提供各种的字符串操作。
在刚开始学习C++时自己实现一个string类却遇到了各种各样的问题,最主要的问题是深拷贝与前拷贝的问题。
1.浅拷贝问题
代码如下:
class String
{
public:
String(const char *str = nullptr)
{
if (str == nullptr)
{
m_data = new char[1];
m_data[0] = '\0';
}
else
{
m_data = new char[strlen(str) + 1];
strcpy(m_data, str);
}
}
~String()
{
delete[] m_data;
m_data = nullptr;
}
private:
char *m_data;
}
int main()
{
String s1("abc");
String s2 = s1;
s1.show();
s2.show();
return 0;
}
当我们去运行程序时,程序会崩溃,如下图:
s1与s2的m_data被打印出来后崩溃了。为什么呢,这就是浅拷贝的原因,如下图:
当程序执行s1拷贝构造s2时,编译器会调用自带的默认拷贝构造函数,将s2的m_data指向s1的m_data;在程序结束时会调用析构函数,在析构s1对象时,将m_data所指的空间释放。当去析构s2对象时,再去释放空间时,就会导致程序崩溃。因此我们需要在拷贝构造的时候进行深拷贝,也就是说给s2的m_data也开辟一块空间。
2.深拷贝
class String
{
public:
String(const char *str = nullptr)
{
if (str == nullptr)
{
m_data = new char[1];
m_data[0] = '\0';
}
else
{
m_data = new char[strlen(str) + 1];
strcpy(m_data, str);
}
}
//深拷贝
String(const String &s)
{
m_data = new char[strlen(s.m_data)+1];
strcpy(m_data,s.m_data);
}
~String()
{
delete[] m_data;
m_data = nullptr;
}
void show()
{
cout<<m_data<<endl;
}
private:
char *m_data;
};
int main()
{
String s1("abc");
String s2 = s1;
s1.show();
s2.show();
return 0;
}
此时程序就会正常运行,当我们去调试观察s1和s2各自的m_data时,他们都有各自的空间 且都是“abc”
那么问题来了如果我们进行大量的拷贝构造对象时,并且m_data的所占空间非常大时,此时是非常消耗内存空间的,我们如何去解决这样的问题呢?
3.带引用计数的浅拷贝
我们给类增加一个静态成员变量count,用来描述有多少对象引用了当前的内存,在析构时只需要进行count--操作就可以了。
代码如下:
class String
{
public:
String(const char *str = nullptr)
{
if (str == nullptr)
{
m_data = new char[1];
m_data[0] = '\0';
}
else
{
m_data = new char[strlen(str) + 1];
strcpy(m_data, str);
}
}
// 深拷贝
// String(const String &s)
// {
// m_data = new char[strlen(s.m_data) + 1];
// strcpy(m_data, s.m_data);
// }
//代引用计数的浅拷贝
String(const String &s)
{
m_data = s.m_data;
m_count++;
}
~String()
{
if (--m_count == 0)
{
delete[] m_data;
m_data = nullptr;
}
}
void show()
{
cout << m_data << endl;
}
private:
char *m_data;
static int m_count; //描述资源被引用的次数
};
int String::m_count = 1;
int main()
{
String s1("abc");
String s2 = s1;
s1.show();
s2.show();
return 0;
}
此时程序运行成功,我们可以调试看到s1与s2的m_data指向的是同一块空间,m_count资源引用计数是2,符合的要求。
但是问题又来了如果我们改变了m_data的数据会怎么样的?我们给类增加一个toupper函数会发生什么?代码如下:
void to_upper()
{
char *p = m_data;
while (*p != '\0')
{
*p -= 32;
p++;
}
}
int main()
{
String s1("abc");
String s2 = s1;
s1.show();
s2.show();
s1.to_upper();
s1.show();
s2.show();
return 0;
}
我们发现s1中的字符变为大写,s2中的字符也变为大写了。这样不符合我们的要求,我们只想让s1的字符变为大写,而s2中的字符不变,显然,上述代码无法完成我们的要求。因此我们就需要使用到写时拷贝机制了
4.写时拷贝
字符串写时拷贝实现原理:简单说还是使用引用计数+浅拷贝的方法,但是在当对象中的数据需要改变时我们就要进行深拷贝。
代码如下:
class String;
//引用计数器类
class String_rep
{
public:
friend class String;
friend ostream &operator<<(ostream &out, String &s);
String_rep(const char *str = nullptr) : m_count(0)
{
if (str == nullptr)
{
m_data = new char[1];
m_data[0] = '\0';
}
else
{
m_data = new char[strlen(str) + 1];
strcpy(m_data, str);
}
}
~String_rep()
{
delete[] m_data;
m_count = 0;
}
void increament()
{
m_count++;
}
void decreament()
{
if (--m_count == 0)
{
delete this;
}
}
private:
char *m_data;
int m_count;
};
class String
{
public:
String(const char *str = nullptr)
{
pn = new String_rep(str);
pn->increament();
}
String(const String &s)
{
pn = s.pn;
pn->increament();
}
String &operator=(const String &s)
{
if (this != &s)
{
pn->decreament();
pn = s.pn;
pn->increament();
}
return *this;
}
~String()
{
pn->decreament();
}
void to_upper()
{
char *p = pn->m_data;
pn->decreament();
pn = new String_rep(p);
pn->increament();
p = pn->m_data;
while (*p != '\0')
{
if (*p >= 'a' && *p <= 'z')
*p -= 32;
p++;
}
}
friend ostream &operator<<(ostream &out, String &s);
private:
String_rep *pn;
};
ostream &operator<<(ostream &out, String &s)
{
out << s.pn->m_data;
return out;
}
int main()
{
String s("abc");
String s1 = s;
String s2 = s1;
s2.to_upper();
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
我们实现一个引用计数器类,用于去管理内存的申请和释放,String类只需要对引用计数进行操作,不用关心底层是如何去深拷贝还是浅拷贝的。
此时的内存模型如下图:
代码运行如下:
运行后内存模型如下图:
我们可以调试去追踪代码看一下里面究竟发生了什么
我们让代码跑起来可以看到s s1 s2的字符串是同一块内存
而引用计数刚好都是3
现在让代码继续往下走 看到s2的字符串已经发生了改变,并且引用计数也发生了变化 详细信息如下图
此时我们就通过String类了解写时拷贝思想。因此在我们实现一些函数功能的时候当它不需要改变对象内容时我们就进行浅拷贝,当它需要改变对象内容时我们就用深拷贝。
以上是关于C++类的浅拷贝深拷贝以及写时拷贝问题的主要内容,如果未能解决你的问题,请参考以下文章