String的拷贝是面试中的经常会被问到的问题,所以,学懂String类是非常重要的。
下面我们先来看一段代码:
class String
{
public:
String(const char* pStr = "")//构造函数
{
if (pStr == NULL)
{
_pStr = new char[1];
*_pStr = '\0';
}
else
{
_pStr =new char[strlen(pStr) + 1];
strcpy(_pStr, pStr);
}
}
String(const String &s)//拷贝构造函数,相当于系统默认合成
:_pStr(s._pStr){}
String& operator = (const String& s)//赋值运算符的重载
{
if (this != &s)
{
_pStr = s._pStr;
}
return *this;
}
~String()//析构函数
{
if (_pStr)
{
delete[] _pStr;
_pStr = NULL;
}
}
private:
char* _pStr;
};
void Funtest()
{
String s1;
String s2("1234");
String s3(s2);//程序崩溃
/*String s4;
s4 = s2;*/
}
int main()
{
Funtest();
system("pause\n");
return 0;
}
一运行发现程序崩溃了,那么问题到底出在哪里呢?对代码进行调试后我们可以发现如下现象:
可以看到,s2和s3的地址是指向同一块空间的,那在调用析构函数时岂不是对一段空间析构了两次吗?这里不止是程序崩溃,还出现了内存泄漏。这就是String的经典反例:浅拷贝。面试的时候可千万别写出这样的代码哦。
那么解决这种问题的方案是什么呢?下面就来介绍:深拷贝
(1)普通版本
class String
{
public:
String(const char* pStr = "")
{
if (pStr == NULL)
{
_pStr = new char[1];
*_pStr = '\0';
}
else
{
_pStr = new char[strlen(pStr) + 1];
strcpy(_pStr, pStr);
}
}
String & operator = (const String &s)
{
if (this != &s)
{
char *pTmp = new char[strlen(s._pStr) + 1];//新开辟一块空间
strcpy(pTmp, s._pStr);
delete[] _pStr;
_pStr = pTmp;
}
return *this;
}
~String()
{
if (_pStr)
{
delete[] _pStr;
_pStr = NULL;
}
}
String(const String &s)
:_pStr(new char[strlen(s._pStr) + 1])
{
strcpy(_pStr, s._pStr);
}
private:
char *_pStr;
};
void Funtest()
{
String s1;
String s2("1234");
String s3(s2);
String s4;
s4 = s3;
}
int main()
{
Funtest();
return 0;
}
调试结果如下:
此时,s2,s3,s4不再使用同一块空间,这便解决了浅拷贝中内存泄漏以及程序崩溃的问题,深拷贝还有一个简洁版,如下:
(2)简洁版
class String
{
public:
String(const char* pStr = "")
{
if (pStr == NULL)
{
_pStr = new char[1];
*_pStr = '\0';
}
else
{
_pStr = new char[strlen(pStr) + 1];
strcpy(_pStr, pStr);
}
}
String(const String &s)
:_pStr(NULL){}
{
_pStr = new char[1];
String strTmp(s._pStr);
std::swap(_pStr, strTmp._pStr);
}
String &operator = (const String &s)
{
if (this != &s)
{
String strTmp(s);
std::swap(_pStr, strTmp._pStr);
}
return *this;
}
~String()
{
if (_pStr)
{
delete[] _pStr;
_pStr = NULL;
}
}
private:
char *_pStr;
};
当我们需要写的时候才去新开辟内存空间。这种方法就是写时拷贝。这也是一种解决由于浅拷贝使多个对象共用一块内存地址,调用析构函数时导致一块内存被多次释放,导致程序奔溃的问题。这种方法需要用到引用计数:使用int *保存引用计数;采用所申请的4个字节空间。
class String
{
public:
String(const char *pStr = "")
{
if (pStr == NULL)
{
_pStr = new char[1 + 4];
*((int *)pStr) = 1;
_pStr = (char*)(((int *)_pStr) + 1);
*_pStr = '\0';
}
else
{
_pStr = new char[strlen(pStr) + 1 + 4];
strcpy(_pStr, pStr);
*((int *)_pStr - 1) = 1;
}
}
String(const String &s)
:_pStr(s._pStr)
{
++Getcount();
}
String& operator = (const String &s)
{
if (this != &s)
{
Release;
_pStr = s._pStr;
--Getcount();
}
return *this;
}
char& operator[](size_t index)
{
if (Getcount() > 1)
{
char *pTmp = new char[strlen(_pStr) + 1 + 4];
strcpy(pTmp + 4, _pStr);
--Getcount();
_pStr = pTmp + 4;
Getcount() = 1;
}
return _pStr[index];
}
const char & operator[](size_t index)const
{
return _pStr[index];
}
friend ostream operator<<(ostream& output, const String& s)
{
output << s._pStr;
return output;
}
private:
char *_pStr;
int& Getcount()
{
return *((int*)_pStr - 1);
}
void Release()
{
if (_pStr && (0 == --Getcount()))
{
_pStr = (char *)((int *)_pStr - 1);
delete _pStr;
}
}
};
修改 String 数据时,先判断计数器是否为 0( 0 代表没有其他对象共享内存空间),为 0 则可以直接使用内存空间(如上例中的 s2 ),否则触发写时拷贝,计数 -1 ,拷贝一份数据出来修改,并且新的内存计数器置 0 ; string 对象析构时,如果计数器为 0 则释放内存空间,否则计数也要 -1 。
写时拷贝存在的线程安全问题
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。