一、String类的定义及其操作
1.出现的原因
C语言中,字符串通常都是以'\0'结尾得一些字符的集合,为了操作简单,方便,C标准库提供了一系列的库函数,但是这样使得字符串与处理这些字符串的函数是分开的,不符合OOP思想,而且底层还需要自己去维护管理,稍不注意还会出现越界访问。因此,在C++中对这个进行了优化。
2.特性
C中 char* 是一个指针,而C++中string是一个类,string对char*进行封装 ,string封装了许多实用的方法,不用考虑内存释放与越界问题,string管理char* 所分配的内存,每一次都是string复制,取值都由string类来维护,不用担心复制越界与取值越界。为了在程序中使用string类型,必须包含头文件 <string>。string类是一个模板类,位于名字空间std中,通常为方便使用还需要增加: using namespace std;
string类的意义有两个,第一个是为了处理char类型的数组,并封装了标准C中的一些字符串处理的函数。而当string类进入了C++标准后,它的第二个意义就是一个容器。
3.总结
- string是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;
- 不能操作多字节或者变长字符的序列。
- 在使用string类时,必须包含头文件以及using namespace std;
4.String类简单实现代码
class String; //String类的声明
///
class Iterator //迭代器类
{
public:
Iterator(String& str, int idx) :pstring(&str), index(idx){} //传入参数String类字符串和下标位置
bool operator!=(const Iterator& rhs)
{
return index != rhs.index;
}
char& operator*();
const Iterator operator++(int)
{
const Iterator tmp(*this);
index++;
return tmp;
}
private:
String* pstring;
int index;
};
char& Iterator::operator*() //解引用
{
return (*pstring)[index];
}
int main()
{
String str("hello world!");
String::iterator it = str.begin();
while (it != str.end())
{
std::cout << *it;
it++;
}
std::cout << std::endl;
return 0;
}
///
class String ///String类
{
public:
typedef Iterator iterator;
String()
{
pstr = new char[1] ( );
}
String(char* str)
{
pstr = new char[strlen(str) + 1] ( );
strcpy(pstr, str);
}
String& operator=(const String& rhs)
{
if (this != &rhs)
{
delete[] pstr;
pstr = new char[strlen(rhs.pstr) + 1];
strcpy(pstr, rhs.pstr);
}
return *this;
}
String(const String& rhs)
{
pstr = new char[strlen(rhs.pstr) + 1]();
strcpy(pstr, rhs.pstr);
}
~String()
{
delete[] pstr;
pstr = NULL;
}
iterator begin()
{
return iterator(*this, 0);
}
iterator end()
{
return iterator(*this, strlen(pstr));
}
const String operator+(const char* prhs)
{
int len = strlen(pstr) + strlen(prhs) + 1;
char* ptmp = new char[len]();
strcat(ptmp, pstr);
strcat(ptmp, prhs);
String tmp(ptmp);
delete[] ptmp;
return tmp;
}
bool operator<(const String& rhs)
{
return strcmp(pstr, rhs.pstr) < 0;
}
bool operator!=(const String& rhs)
{
return strcmp(pstr, rhs.pstr) != 0;
}
char& operator[](int index)
{
return pstr[index];
}
private:
char* pstr;
friend const String operator+(const char*, const String&);
friend std::ostream& operator<<(std::ostream&, const String&);
};
const String operator+(const char* plhs, const String& rhs)
{
int len = strlen(plhs) + strlen(rhs.pstr) + 1;
char* ptmp = new char[len]();
strcat(ptmp, plhs);
strcat(ptmp, rhs.pstr);
String tmp(ptmp);
delete[] ptmp;
return tmp;
}
std::ostream& operator<<(std::ostream& out, const String& rhs)
{
out << rhs.pstr;
return out;
}
std::istream& operator>>(std::istream& in, String& rhs)
{
char ptmp[1000] = {0};
in >> ptmp;
rhs = String(ptmp);
return in;
}
int main()
{
String str1;
String str2("hello");
str1 = str2 + " world";
str1 = "hi " + str2;
if (str1 < str2)
{
std::cout << str1 << std::endl;
}
if (str1 != str2)
{
std::cout << str1[0] << std::endl;
}
std::cin >> str1;
std::cout << str1 << std::endl;
return 0;
}
二、浅拷贝
1.定义
所谓浅拷贝,是指原对象与拷贝对象公用一份实体,仅仅是对象名字不同而已(类似引用,即对原对象起别名),指向同一块内存地址,其中任何一个对象改变都会导致其他的对象也跟着它变。当一个对象将这块内存释放掉之后,另一些对象不知道该块空间是否已经归还给系统
,以为还有效,所以在对这段内存进行操作的时候,发生了访问违规。
2.针对进行代码分析
class String
{
public:
String(const char *pStr = " ")//构造函数
{
if(pStr != NULL)//字符串不为空
{
_pStr = new char[strlen(pStr) + 1];
strcpy(_pStr,pStr);
}
else//字符串为空
{
_pStr = new char[1];
_pStr = '\0';
}
}
String(const String& s)//拷贝构造函数
{
_pStr = s._pStr;
}
String& operator=(const String& s) //重载赋值运算符=号
{
if(this != &s) //判断是否自己给自己赋值
{
_pStr = s._pStr;
}
return *this;
}
private:
char *_pStr;
};
void FunTest()
{
String s1("Hello world");
String s2(s1);
String s3 = s2;
String s4 = s3;
}
int main()
{
FunTest();
system("pause");
return 0;
}
3. 具体原因
4.浅拷贝问题总结
①浅拷贝只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构的时候,会造成同一份资源析构多次,即delete同一块内存多次,造成程序崩溃。
②浅拷贝使得S1、S2和S3指向同一块内存,任何一方的变动都会影响到另一方。
③在释放内存的时候,会造成原有的内存没有被释放也不走默认构造函数,走的是默认的拷贝构造函数,造成内存泄露。
三、String类加入引用计数的浅拷贝的分析与实现
1.引用计数原理
当类里面有指针对象时,进行简单赋值的浅拷贝,两个对象指向同一块内存,存在崩溃的问题!为了解决这个问题,我们可以采用引用计数。在引用计数中,每一个对象负责维护对象所有引用的计数值。当一个新的引用指向对象时,引用计数器就递增,当去掉一个引用时,引用计数就递减。当引用计数到零时,该对象就将释放占有的资源。
从上我们可以看出浅拷贝存在一定的问题,那么怎样对它进行改进防止一个对象被多次释放呢,你可能会这样想。
2.浅拷贝第一次尝试(引用计数(_count作为普通成员变量)error)
代码:
class String //浅拷贝(引用计数(_count作为普通成员变量)error)
{
public:
String(const char* pStr = ""):_pStr(new char[strlen(pStr)+1]),_count(0) //构造函数,_count初值赋值为0
{
if(0 == *pStr)
{
*_pStr = '\0';
}
else
{
strcpy(_pStr,pStr);
}
_count++; //每创建一个对象计数器加1
}
String(String& s)//拷贝构造
:_count(s._count)//将已存在的对象s的计数器赋给当前对象
{
_pStr = s._pStr;
s._count++;//将原对象的计数器加1
_count = s._count;//将原对象的计数器加1后赋值给当前对象
}
~String()//析构函数
{
if(NULL == _pStr)
{
return;
}
else
{
if(--_count == 0)//如果计数器为0,说明无对象指向该空间,可以释放
{
delete []_pStr;
_pStr = NULL;
}
}
}
String& operator=(String& s)//赋值运算符重载
{
if(_pStr != s._pStr)
{
_pStr = s._pStr;
s._count++;//将原对象的计数器加1
_count = s._count;//将已存在的对象s的计数器赋给当前对象
}
return *this;
}
private:
int _count;//给一个计数器控制析构函数
char* _pStr;
};
void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
String s4;//s4对象已经存在了
s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
Funtest();
system("pause");
return 0;
}
4个对象创建后:
调用4次析构函数之后:
??????? 我们知道,这四个对象本来指向同一块空间,计数器本来都应为4,可是现在的结果却是计数器只能控制与它相邻对象的计数器,对象创建完成后,计数器并不统一。
其次,调用4次析构函数之后,本来应该四个对象同时被释放,可是结果却是没有一个对象的计数器为0,也就是这块空间并没有被释放,内存又泄露了呗。为了保持计数器的统一,我们决定把计数器设置为类的静态成员函数,
2.浅拷贝第二次尝试(引用计数(_count作为静态成员变量)error)
代码:
//浅拷贝(引用计数(_count作为静态成员变量))
class String
{
public:
String(const char* pStr = "")//构造函数
:_pStr(new char[strlen(pStr)+1])
{
if(0 == *pStr)
{
*_pStr = '\0';
}
else
{
strcpy(_pStr,pStr);
}
_count++;
}
String(const String& s)//拷贝构造
{
_pStr = (char*)s._pStr;
s._count = _count;
_count++;
}
~String()//析构函数
{
if(NULL == _pStr)
{
return;
}
else
{
if(--_count == 0)
{
delete []_pStr;
_pStr = NULL;
}
}
}
String& operator=(String& s)//赋值运算符重载
{
if(_pStr != s._pStr)
{
_pStr = s._pStr;
s._count = _count;
_count++;
}
return *this;
}
private:
static int _count;
char* _pStr;
};
int String::_count = 0;
void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
String s4;//s4对象已经存在了
s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
Funtest();
system("pause");
return 0;
}
结果:
分析:
1.我们一共只创建了4个对象,可是计数器却为5,那是因为静态成员变量为所有对象共享,任何对象都可以对它进行修改,每创建一个对象我们都对计数器加1,却忽略了创建的新对象是否与已存在的对象占同一块空间
2.调用4次析构函数后,计数器值为1,导致空间又没有被释放。很奇怪的一个现象是,在调用4次构造函数之后,计数器count居然等于1,也就是说调用4次析构函数并没有释放掉我们的空间。抛开失败的阴霾不说,原来我们创建的静态成员变量为所有对象所共有,任何对象都可以修改它,当我们每次创建对象计数器+1的时候,却忽视了创建的新对象是否与己存在的对象占用同一块空间,导致我们定义4个对象却要析构5次的尴尬。
3.浅拷贝第二次尝试(引用计数(_count作为
指针)error)
1. 为什么要引入指针?
静态成员变量的引入最终带来的是析构调用次数的增加,那么指针的引入无疑是为了克服这一难关。但是指针的引入却增加了空间的开销,大部分程序员都知道引入指针也就相当于让程序存在了安全隐患,各种资源的释放以及释放之后防止野指针的问题同样重要。
2.代码:
//浅拷贝(引用计数(指针实现计数))
class String
{
public:
String(const char* pStr = "")//构造函数
:count(new int(0))
,_pStr(new char[strlen(pStr)+1])
{
if(NULL == pStr)
{
*_pStr = '\0';
}
else
{
strcpy(_pStr,pStr);
}
*count = 1;
}
String(const String& s)//拷贝构造
:count(s.count)
{
_pStr = (char*)s._pStr;
count = s.count;
(*count)++;
}
~String()//析构函数
{
if(NULL == _pStr)
{
return;
}
else
{
if(--(*count) == 0)
{
delete[]count;//勿忘了释放计数器指针
delete[]_pStr;
_pStr = NULL;
count = NULL;
}
}
}
String& operator=(String& s)//赋值运算符重载
{
if(_pStr != s._pStr)
{
_pStr = s._pStr;
count = s.count;
(*count)++;
}
return *this;
}
private:
int* count;
char* _pStr;
};
void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
String s4;//s4对象已经存在了
s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
Funtest();
system("pause");
return 0;
}
3.结果:
给s4赋值后,_cout被置为4,调用4次析构函数之后,空间被准确释放。但是指针计数有一个缺陷缺陷:每个对象都得为它多创建一个指针,浪费空间。还有释放麻烦,很有可能我们只记得释放_pStr,却忘了释放计数器指针造成内存泄漏,得不偿失。
接下来,对浅拷贝再次进行优化。
四、写时拷贝(写时拷贝完美诠释String类的浅拷贝)
1.浅拷贝的艰辛历程
从简单的赋值浅拷贝(内存被多次析构释放)->加入引用计数的浅拷贝实现(计数器难以统一)->利用引用计数及静态成员变量浅拷贝实现(忽略了静态成员变量为所有成员所共享)->引用计数利用指针的浅拷贝实现(功能实现了但开辟了存放指针的空间,且不安全)->写时拷贝。一代一代的过渡终于可以完美的实现Strng类的浅拷贝了。
2.定义
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。使用了引用计数,在开辟空间时会多开辟4个字节用来保存引用计数的值。当第一个对象构造时,String的构造函数会根据传入的参数从堆上分配内存,当有其他对象需要这块内存时,计数器++,当有对象进行析构释放时,引用计数--,直到最会一个对象被析构,此时计数器为0。只有这样程序才会真正的释放掉这块从堆上分配的内存。
参考:Linux下写时拷贝
3.代码:
//写时拷贝(仿照new[]实现)
class String
{
public:
String(const char* pStr = "")//构造函数
:_pStr(new char[strlen(pStr) + 4 + 1])//每次多创建4个空间来存放当前地址有几个对象
{
if(NULL == pStr)
{
(*(int*)_pStr) = 1;//将前4个字节用来计数
_pStr += 4;//指针向后偏移4个字节
*_pStr = '\0';
}
else
{
(*(int*)_pStr) = 1;//将前4个字节用来计数
_pStr += 4;//指针向后偏移4个字节
strcpy(_pStr,pStr);//将pStr的内容拷贝到当前对象的_pStr中
}
}
String(const String& s)//拷贝构造
:_pStr(s._pStr)
{
++(*(int*)(_pStr-4));//向前偏移4个字节将计数加1
}
~String()//析构函数
{
if(NULL == _pStr)
{
return;
}
else
{
if(--(*(int*)(_pStr - 4)) == 0)//向前偏移4个字节判断计数是否为0,是0则释放
{
delete (_pStr-4);
_pStr = NULL;
}
}
}
String& operator=(const String& s)//赋值运算符重载
{
if(_pStr != s._pStr)
{
if(--(*(int*)(_pStr - 4)) == 0)//释放旧空间
{
delete (_pStr-4);
_pStr = NULL;
}
_pStr = s._pStr;//指向新空间
++(*(int*)(_pStr - 4));//计数加1
}
return *this;
}
char& operator[](size_t index)//下标访问操作符重载
{
assert(index>=0 && index<strlen(_pStr));
if(*((int*)(_pStr-4)) > 1)//说明有多个对象指向同一块空间
{
char* temp = new char[strlen(_pStr) + 4 + 1];//新开辟一块空间
temp += 4;//先将开辟的空间向后偏移4个字节
strcpy(temp,_pStr);//将_pStr的内容拷贝到temp中
--(*(int*)(_pStr-4));//将原来空间的计数器减1
_pStr = temp;//将当前对象指向临时空间
*((int*)(_pStr-4)) = 1;//将新空间的计数器变为1
}
return _pStr[index];
}
private:
char* _pStr;
};
void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
s3[2] = 'g';
String s4;//s4对象已经存在了
s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
Funtest();
system("pause");
return 0;
}
五、深拷贝
所谓深拷贝,就是为新对象开辟一块新的空间,并将原对象的内容拷贝给新开的空间,释放时就不会牵扯到多次析构的问题。
/*1
String& operator=(const String &s)//赋值运算符重载
{
if(_pStr != s._pStr)
{
String temp(s._pStr);//如果不给出临时变量,交换后s的值将为NULL
std::swap(_pStr,temp._pStr);
}
return *this;
}
*/
/* 2
String& operator=(const String& s)
{
if (this != &s)
{
String temp(s);
std::swap(_pStr, temp._pStr);
}
return *this;
}*/
/* 3
String& operator=(String temp)
{
std::swap(_pStr, temp._pStr);
return *this;
}*/
https://blog.csdn.net/snow_5288/article/details/52901528