前言
楼主现在大三,鉴于年后想找一个实习,故寒假在家复习一些学过的知识点,前几天看到一篇关于string类的文章,细读之后,发现我还是有许多遗漏的地方,经过这几天的学习,想通过这篇文章,将有关string类的东西都记录一下。
重点介绍:
- 浅拷贝
- 深拷贝
- 写时拷贝
- 写时拷贝隐含的问题
下面的文章将会以上面的四点进行展开,至于代码,我会在linux和vs下都跑一下,因为我发现编译器不同。有些问题是不易被发现的。
浅拷贝问题
代码贴一下,然后再解释
/*
* string类的浅拷贝
*/
class String
{
public:
String()
:str(new char[])
{
*str = '\0';
}
String(const char* str)
:str(new char[strlen(str) + ])
{
strcpy(str, str);
}
String(const String& str)
:str(str.str)
{}
String& operator=(const String& str)
{
if (this != &str)
{
str = str.str;
}
return *this;
}
~String()
{
if (str)
{
printf("~String()\n");
delete[] str;
str = nullptr;
}
}
private:
char* str;
};
缺点:多个对象指向同一个地址,当析构时,就会出现对同一块内存多次释放的问题。
vs2013:
调用两次析构函数,然后程序崩溃了。
linux下:
调用两次析构函数,程序运行结束,并没有崩溃。
看源代码可知,浅拷贝存在这种多个对象指向同一块地址的问题,会引发多次析构的问题,然后vs和gcc两个不同的编译器对于这一现象的默认行为是不同的。
询问了一下老师,说对于这种行为,是标准手册中未定义的,则具体的行为由编译器决定。
深拷贝
对于深拷贝,我实现的函数比较多,因为主要目的还是为了复习知识点,一般再面试中,如果面试官让你实现string类也是实现简易的深拷贝,至于为什么不写高大山的COW(Copy On Write)即写时拷贝,下文会提到。
// Sting类的深拷贝实现
class String
{
public:
String()
:_str(new char[])
{
*_str = '\0';
}
String(const char* str)
:_str(new char[strlen(str) + ])
{
strcpy(_str, str);
}
String(const String& rhs)
:_str(new char[rhs.Size() + ])
{
strcpy(_str, rhs.C_Str());
}
~String()
{
delete[] _str;
}
// 最传统的写法
String& operator=(const String& rhs)
{
if (this != &rhs)
{
char* tmp = new char[rhs.Size() + ];
delete[] _str;
strcpy(tmp, rhs.C_Str());
_str = tmp;
}
return *this;
}
//现代写法
/*String& operator=(const String& rhs)
{
String tmp(rhs);
Swap(tmp);
return *this;
}*/
//最简洁的写法
/*String& operator=(String rhs)
{
Swap(rhs);
return *this;
}*/
// C++ 11中的实现
/*String(String&& rhs)
: _str(rhs._str)
{
rhs._str = nullptr;
}
String& operator=(String&& rhs)
{
Swap(rhs);
return *this;
}*/
//辅助函数的实现
size_t Size()const
{
return strlen(_str);
}
const char* C_Str()const
{
return _str;
}
void Swap(String& rhs)
{
std::swap(_str, rhs._str);
}
char& operator[](size_t index)
{
return _str[index];
}
//重载了一些运算符
friend std::ostream& operator<<(std::ostream& out, const String& rhs);
friend std::istream& operator>>(std::istream& in, const String& rhs);
friend String operator+(const String& str1, const String& str2);
friend String operator+=(String& str1, const String& str2);
friend bool operator ==(const String& str1, const String& str2);
friend bool operator !=(const String& str1, const String& str2);
private:
char* _str;
};
std::ostream& operator<<(std::ostream& out, const String& rhs)
{
out << rhs._str;
return out;
}
std::istream& operator>>(std::istream& in, const String& rhs)
{
in >> rhs._str;
return in;
}
String operator+(const String& str1, const String& str2)
{
String tmp;
delete[] tmp._str;
size_t new_length = str1.Size() + str2.Size();
tmp._str = new char[new_length + ];
strcpy(tmp._str, str1._str);
strcat(tmp._str, str2._str);
return tmp;
}
String operator+=(String& str1, const String& str2)
{
str1 = str1 + str2;
return str1;
}
bool operator==(const String& str1, const String& str2)
{
return strcmp(str1._str, str2._str);
}
bool operator!=(const String& str1, const String& str2)
{
return !(strcmp(str1._str, str2._str));
}
注:深拷贝解决了浅拷贝多个对象指向一块内存的问题,但是同时也引入了自己的问题。
即只要对象间进行拷贝或者赋值,都是重新开辟内存。
但有的字符串拷贝或赋值之后,并没有发生变化,故此引入了写时拷贝的概念。
对于上面的深拷贝,主要掌握拷贝构造和赋值运算符的几种实现,都了解一下思想。
写时拷贝(Copy On Write)
写时拷贝,即再对象的实现中加入一个引用计数,只要对象不进行写入操作,即[],就只将引用计数进行+1操作,只有当引用计数为1时,才真正释放对象。
对于写时拷贝的实现,由两种实现思路。
由于先在vs上跑,刚开始多开辟四个字节保存count,没什么毛病,拿到linux下跑,直接coredump了,所以一定要注意这个细节。
1.0引用计数分开
先来看第一种实现思路:
//1.0写实拷贝
class String
{
public:
String()
:_str(new char[])
, _count(new int())
{
*_str = '\0';
}
String(const char* str)
:_str(new char[strlen(str) + ])
, _count(new int())
{
strcpy(_str, str);
}
~String()
{
if (--(*_count) == )
{
delete[] _str;
delete _count;
_str = nullptr;
_count = nullptr;
}
}
String(const String& rhs)
:_str(rhs._str)
, _count(rhs._count)
{
(*_count)++;
}
String& operator=(const String& rhs)
{
if (this != &rhs)
{
if (--(*_count) == )
{
delete[] _str;
delete _count;
}
_str = rhs._str;
_count = rhs._count;
(*_count)++;
}
return *this;
}
//只有当真正修改时,才重新开辟内存
char& operator[](size_t index)
{
if (*_count > )
{
char* tmp = new char[strlen(_str) + ];
strcpy(tmp, _str);
_str = tmp;
(*_count)--;
_count = new int();
}
return *(_str + index);
}
const char* C_Str()
{
return _str;
}
friend std::ostream& operator<<(std::ostream& out,String& rhs);
private:
char* _str;
int* _count;//思考为什么不用int _count或者static ——count
};
std::ostream& operator<<(ostream& out,String& rhs)
{
out<<rhs._str;
return out;
}
为了测试写时拷贝,所以我重载了标准输出和C_Str()函数,方便一会打印。
测试思路:
先只进行拷贝,然后打印地址。(按照写时拷贝的思想,没进行修改操作,不会重新开辟空间)
然后修改其中一个对象,再次打印地址。(这时应该重新开辟了空间)。
代码如下:
void TestCOW()
{
String s1("hello");
String s2;
s2 = s1;
printf("进行写入前:\n");
cout<<"s1 :"<<s1;
printf(" 地址:%p\n",s1.C_Str());
cout<<"s2 :"<<s2;
printf(" 地址:%p\n",s2.C_Str());
s2[] = 'H';
printf("进行写入后:\n");
cout<<"s1 :"<<s1;
printf(" 地址:%p\n",s1.C_Str());
cout<<"s2 :"<<s2;
printf(" 地址:%p\n",s2.C_Str());
}
linux下跑:
vs2013下跑:
结果都符合预期。
2.0引用计数和字符串一起开辟
class String
{
public:
String()
:str(new char[ + ])
{
str = str + ;
*str = '\0';
int count = Count(str);
count = ;
}
String(char* str)
:str(new char[strlen(str) + + ])
{
str = str + ;
strcpy(str, str);
}
String(const String& rhs)
:str(rhs.str)
{
++Count(str);
}
String& operator=(const String& rhs)
{
if (--Count(str) == )
{
delete[](str + );
str = nullptr;
}
str = rhs.str;
++Count(str);
return *this;
}
~String()
{
if (--(Count(str)) == )
{
delete[] str;
str = nullptr;
}
}
char& operator[](size_t index)
{
if (Count(str) > )
{
char* tmp = new char[strlen(str) + + ];
}
}
int& Count(char* str)
{
return *(int*)(str - );
}
char* str;
};
注
由于Copt-On-Write本身存在的问题,C++11开始,已经摒弃了这一技术。
有兴趣去可以写个小例子测一下。
关于具体的开发中写时拷贝的缺陷,下面给出几篇比较好的博文,可以看一下。
写时拷贝的缺陷和问题:http://blog.jobbole.com/100844/
string类写时拷贝:https://coolshell.cn/articles/12199.html
面试中string类的书写及写时拷贝缺点:https://coolshell.cn/articles/10478.html