标准库类型string表示可变长的字符序列,使用string类型必须首先包含它的头文件。
作为标准库的一部分,string定义在命名空间std中。
#include<string>//注意这里没有.h
using namespace std;
下面让我们一起来模拟string类中的几个比较重要的成员函数,一步一步的剖析它们的实现机制:
1.构造函数:
示例①
class String
{
public :
String(char *str = "")//construction function
:_str(str)
{}
~String()
{
if (NULL != _str)
{
delete[] _str;
}
}
private :
char *_str;
};
void Test2()
{
String s1("hello");
String s2(new char[3]);
}
从监视窗口可以看出,s1貌似构建成功了,但是在Test2()快要结束时,程序崩溃了,我们知道,析构函数是在对象销毁前执行的,
所以程序崩溃的原因就是执行析构函数的时候,析构函数中释放对象中指针_str指向的空间,
但是S1中指针_str指向的是一个常量字符串—->存放在代码区(c语言)或者说常量区(操作系统),而delete[]释放的空间必须要是动态分配的,至此我们就可知程序崩溃的原因。
经过上例的讨论,我将构造函数做了些许的修改:
示例②:
class String
{
public :
String(char *str = "")//构造函数
{
if (NULL == str)
{
_str = new char[1];//为了和delete[]配合使用
*_str = '\0';
}
else
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
}
~String()
{
if (NULL != _str)
{
delete[] _str;
}
}
private :
char *_str;
};
构造函数的参数列表带有默认值也是为了和类库中的一致,当以string s1;形式构造对象时,希望构造出的字符串为空串(含有’\0’)。
2.拷贝构造函数:
①浅拷贝:
String::String(const String& s)//浅拷贝
:_str(s._str)
{}
测试:
void Test2()
{
String s1("hello");
String s2(s1);
}
上面的程序会崩溃,下面我们来分析原因:
可以看出这里的s2采用了浅拷贝,而析构的时候,先析构的是s2,析构s2时会释放掉“hello”这块空间,接下来再释放s1时,程序就会崩溃,因为这块空间被释放了两次。
浅拷贝:又称为位拷贝,编译器只是将指针的内容拷贝过来,导致多个对象共用一块内存空间,当其中任意对象将这块空间释放之后,另外一些对象并不知道这块空间已经还给了操作系统,以为还有效,所以再对这块空间进行操作时,造成了违规访问。
为了解决这个问题,我们引入了深拷贝(给要拷贝构造的对象重新分配空间):
代码如下:
String::String(const String& s)//深拷贝
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
测试:
void Test2()
{
String s1("hello");
String s2(s1);
}
由监视窗口可知,拷贝的对象s2中_str的值(字符串的地址)和s1对象中的_str的值不同,说明是重新开辟了空间(即就是深拷贝)。
3.赋值运算符重载:
//方法①
String& String::operator=(const String& s)
{
if (this != &s)
{
delete[] _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
return *this;//为了支持链式访问
}
//方法②(优)
String& String::operator=(const String& s)
{
if (this != &s)
{
char* tmp = new char[strlen(s._str) + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
}
return *this;//为了支持链式访问
}
测试:
一般情况下,上面的两种写法都可以,但是相对而言,第二种更优一点。
对于第一种,先释放了旧空间,但是如果下面用new开辟新空间时有可能失败——>抛异常,而这时你是将s2赋值给s3,不仅没有赋值成功(空间开辟失败),而且也破坏了原有的s3对象。
对于第二种,先开辟新空间,将新空间的地址赋给一个临时变量,就算这时空间开辟失败,也不会影响原本s3对象。
综上:第二种方法更优一点。
最后的返回值是为了支持链式访问。
例如:s3 = s2 = s1;
上面所写的拷贝构造函数和赋值运算符重载函数属于传统写法,下面我们一起来看看它们的现代写法:
拷贝构造的现代写法:
赋值运算符重载函数的两种现代写法:
在面试时,一般写出上面四个string类的成员函数即可,除非面试官特别要求。
从上面的深拷贝我们可以看出,相比浅拷贝,深拷贝的效率明显较低,因为每拷贝一个对象就需要开辟空间和释放空间,再有就是赋值运算符重载也是一样的需要重新开辟空间并释放空间。
假如有这样的一种情况,拷贝和赋值得到的对象只用于”读”,而不用于”写”,那么是不是就不需要重新开辟空间了呢?
下面让我们一起来了解一下引用计数的浅拷贝:
我们需要使用一个变量可以标记同一块空间同时有几个对象在使用—–>为了析构(当标记的这个变量为1时,说明只有一个对象在使用这块空间,那么在使用完成后就可以去释放空间,否则让这个计数的变量减1)。
引用计数的浅拷贝:
首先,我们需要讨论下这个所谓的计数变量:
①使用数据成员 int _count;
在构造函数中使得_count = 1;
在拷贝构造函数中_count(++s._count).
但是析构的时候无法改变每个对象中的_count的值。
String.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class String
{
public:
String(const char* str = "");
String(String& s);
String& operator=(const String& s);
~String();
private:
int& GetRef(char *str);
void release();
char *_str;
int _count;
};
String.cpp
String::String(const char* str )
{
if (NULL == str)
{
_str = new char[1];
*_str = '\0';
}
else
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
_count = 1;
}
String::String(String& s)
:_str(s._str)
, _count(++(s._count))
{
strcpy(_str, s._str);
}
String::~String()
{
if (--_count == 0 && NULL != _str)//_count是每个对象的,减去一个共享一块空间的其中一个对象的成员,并不会改变其他对象的成员变量_count的值
{
cout << this << endl;
delete[] _str;
}
}
所以,使用数据成员变量—–>pass
②使用静态数据成员:static int _count;
构造函数内:_count =1;
拷贝函数内部:_count++;
当出现情况:String s1;
String s2(s1);
String s3(s2);
这时的引用计数_count = 3;
但是当重新构造一个对象时:
String s4;——–>使得该类所有的引用计数_count = 1;
所以:静态数据成员也不可以。
下面详细讲解:
String.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class String
{
public:
String(const char* str = "");
String(String& s);
String& operator=(const String& s);
~String();
private:
int& GetRef(char *str);
void release();
char *_str;
static int _count;
};
String.cpp
String::String(const char* str )
{
if (NULL == str)
{
_str = new char[1];
*_str = '\0';
}
else
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
_count = 1;
}
String::String(String& s)
:_str(s._str)
{
strcpy(_str, s._str);
_count++;
}
String::~String()
{
if (--_count == 0 && NULL != _str)
{
cout << this << endl;
delete[] _str;
}
}
int String:: _count = 0;
void Test1()
{
String s1("hello");
String s2(s1);
String s3(s2);
}
从上图可以看出静态成员变量是这个类所有的对象所共享的,所以当只是用一个对象去拷贝另一个对象时,这样不会出错,而且貌似也达到我们的要求了,可是当重新构造一个对象时,就会发现所有对象中的_count都修改为了1。
所以,使用静态成员(static int count)————>pass
③使用指针:
使得拷贝的对象共用指向一个引用计数。
赋值运算符重载函数中的两种情况:
第一种情况:
//String.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class String
{
public:
String(const char* str = "");
String(String& s);
String& operator=(const String& s);
~String();
private:
int& GetRef(char *str);
void release();
char *_str;
int *_RefCount;
};
//String.cpp
#include"String-Ref.h"
String::String(const char* str )
{
if (NULL == str)
{
_str = new char[1];
*_str = '\0';
}
else
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
_RefCount = new int(1);
}
String::String(String& s)
:_str(s._str)
,_RefCount(s._RefCount)
{
strcpy(_str, s._str);
(*_RefCount)++;
}
String::~String()
{
if (--(*_RefCount) == 0 && NULL != _str)
{
cout << this << endl;
delete[] _str;
_str = NULL;
delete _RefCount;
_RefCount = NULL;
}
}
String& String::operator=(String& s)
{
if (this != &s)
{
if (--*_RefCount == 0)
{
delete[] _str;
_str = NULL;
delete _RefCount;
_RefCount = NULL;
}
_str = s._str;
_RefCount = s._RefCount;
(*s._RefCount)++;
}
return *this;
}
void Test1()
{
String s1("hello");
String s2(s1);
String s3(s2);
String s4("world");
s4 = s2;
}
由监视窗口的数据可得,使用指针可以完成引用计数的浅拷贝,但是因为每构造一个对象,都需要开辟两块空间,这样容易造成内存碎片。
由new[]可以联想到类似模型—->只开辟一块空间(多开四个字节),把引用计数放在字符串首地址的前四个字节上。
这样不但解决了内存碎片问题,而且也可以程序的运行效率。
引用计数的浅拷贝的最优解决方案:
(这里我就不一一分析了,思路和上面使用指针是一样的)
//String.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class String
{
public :
String(const char* str = "");
String(const String& s);
String& operator=(const String& s);
~String();
private :
int& GetRef(char *str);
void release();
char *_str;
};
//String.cpp
#include"string_simi.h"
String::String(const char* str)//构造函数
{
if (NULL == str)
{
_str = new char[5];
_str += 4;
*_str = '\0';
}
else
{
_str = new char[strlen(str) + 5];
_str += 4;
strcpy(_str, str);
}
GetRef(_str) = 1;
}
String::String(const String& s)//拷贝构造函数
:_str(s._str)
{
GetRef(_str)++;
}
//s1 = s2;
String& String::operator=(const String& s)//赋值运算符重载
{
if (_str != s._str)//判断自赋值
{
//1.当s1的引用计数为1时
//①释放空间
//②改变s1指针的指向
//③s2的引用计数加1
release();
//当s1的引用计数大于1时
//①s1的引用计数减1
//同上②③
_str = s._str;
GetRef(s._str)++;
}
return *this;
}
void String::release()
{
if (--GetRef(_str) == 0)
{
delete[](_str - 4);
}
}
int& String::GetRef(char *str)//获取引用计数
{
return *(int*)(str - 4);
}
String::~String()//析构函数
{
if (--GetRef(_str) == 0)
{
delete[] (_str - 4);
}
}
void TestString1()
{
String s1("hello");
String s2(s1);
String s3("world");
s1 = s3;
}
int main()
{
TestString1();
return 0;
引用计数的浅拷贝同样存在缺陷—>当几个共用同一块空间的对象中的任一对象修改字符串中的值,则会导致所有共用这块空间的对象中的内容被破坏掉。
由此—->引入了写时拷贝(Copy On Write)
顾名思义,写时拷贝—>写的时候进行拷贝(深拷贝)
//String.h
#pragma once
#include<iostream>
using namespace std;
#include<assert.h>
class String
{
public:
String(const char* str = "")//构造函数
{
if (NULL == str)
{
char* pTemp = new char[1 + 4];
_pStr = pTemp + 4;
_GetRefer() = 1;
*_pStr = '\0';
}
else
{
char* pTemp = new char[strlen(str) + 1 + 4];
_pStr = pTemp + 4;
strcpy(_pStr, str);
_GetRefer() = 1;
}
}
String(const String& s)
:_pStr(s._pStr)
{
_GetRefer()++;
}
~String()
{
Release();
}
String& operator=(const String& s);
char& operator[](size_t index);
const char& operator[](size_t index)const;
int& String::_GetRefer();
void Release();
friend ostream& operator<<(ostream& _cout, const String& s);
private:
char* _pStr;
};
//String.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"String3.h"
int& String::_GetRefer()//获取引用计数
{
return *((int*)_pStr - 1);
}
void String::Release()//释放空间
{
if (--_GetRefer() == 0)
{
_pStr -= 4;
delete[] _pStr;
_pStr = NULL;
}
}
String& String::operator=(const String& s)
{
if (_pStr != s._pStr)
{
if (--_GetRefer() == 0)//需要考虑两种情况
{
Release();
_pStr = s._pStr;
_GetRefer()++;
}
else
{
_pStr = s._pStr;
_GetRefer()++;
}
}
return *this;
}
char& String::operator[](size_t index)
{
if (_GetRefer() > 1)
{
_GetRefer()--;
char* pTemp = new char[strlen(_pStr) + 1 + 4];
pTemp += 4;
strcpy(pTemp, _pStr);
_pStr = pTemp;
_GetRefer() = 1;//新开的空间
}
return _pStr[index];
}
const char& String::operator[](size_t index)const
{
return _pStr[index];
}
void Test1()
{
const String s1("hello");
String s2(s1);
//String s3;
//s3 = s2;
cout << s1[4] << endl;
}
int main()
{
Test1();
return 0;
}
[]重载运算符需要成对重载:
char& String::operator[](size_t index)
{
if (_GetRefer() > 1)
{
char* pTemp = new char[strlen(_pStr) + 1 + 4];
pTemp += 4;
strcpy(pTemp, _pStr);
_GetRefer()--;
_pStr = pTemp;
_GetRefer() = 1;//新开的空间
}
return _pStr[index];
}
const char& String::operator[](size_t index)const
{
return _pStr[index];
}