前言
- 一、String类初识
- 二、String类的简单赋值浅拷贝的内存分析
- 三、String类加入引用计数的浅拷贝的分析与实现
- 四、String类加入引用计数及静态成员变量的分析与实现
- 五、String类加入引用计数及指针的分析与实现
- 六、写时拷贝完美诠释String类的浅拷贝
- 1. 浅拷贝的艰辛历程
- 2. 什么是写时拷贝
- [Linux下写时拷贝](https://mp.weixin.qq.com/s/hTD9HEIbSx69wJhA_dv9Qg)
- [写时拷贝详解](https://coolshell.cn/articles/12199.html)
- [写时拷贝缺陷详解](https://coolshell.cn/articles/1443.html)
- 七、String类的普通版本与简洁版本的深拷贝
一、String类初识
1.String类出现的原因
C语言中,
字符串通常都是以'\0'结尾得一些字符的集合
,为了操作简单,方便,C标准库提供了一系列的库函数,但是这样使得字符串与处理这些字符串的函数是分开的,不符合OOP思想
,而且底层还需要自己去维护管理
,稍不注意还会出现越界访问。
2.string类的特性
char* 是一个指针,string是一个类
,string封装了char* ,管理这个字符串是一个char* 型的容器,string封装了许多实用的方法
,不用考虑内存释放与越界问题
,string管理char* 所分配的内存,每一次都是string复制,取值都由string类来维护,不用担心复制越界与取值越界
//string to char*
string str = "nihao";
char strr = str.c_str();
//char* to string
char* str = "nihao c++";
string sstr(str);;
string类的意义有两个,第一个是为了处理char类型的数组,并封装了标准C中的一些字符串处理的函数
。而当string类进入了C++标准后,它的第二个意义就是一个容器
总结:
- string是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:
basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;
- 不能操作
多字节或者变长字符
的序列。
在使用string类时,必须包含头文件以及using namespace std;
二、String类的简单赋值浅拷贝的内存分析
1. 什么是浅拷贝?
浅拷贝
,也称位拷贝,编译器只是直接将指针的值拷贝过来
,结果多个对象共用了同一块内存空间
,当一个对象将这块内存释放掉之后,另一些对象不知道该块空间是否已经归还给系统
,以为还有效,所以在对这段内存进行操作的时候,发生了访问违规。
源代码及注释
#include <iostream>
#include <Windows.h>
using namespace std;
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()//析构函数
{
if(_pStr == NULL)
return;
else
{
delete[] _pStr;
_pStr = NULL;
}
}
//重载赋值运算符=号
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;
}
开辟空间、释放资源分析
①首先根据创建的对象进行开辟空间。
②在对对象依次进行释放资源时,根据先入后出的顺序依次释放,但是,结果是造成了同时释放,因为共用同一块内存,造成对象成员全部释放。
③当S3调用析构函数释放之后,共用的空间已经释放,当程序再次调用析构函数释放S2时,程序也必将奔溃,因为不能对同一块空间释放多次。
⑤给出地址空间图,方便大家理解
2. 浅拷贝问题总结
①浅拷贝只是拷贝了指针
,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构
的时候,会造成同一份资源析构多次
,即delete同一块内存多次,造成程序崩溃。
②浅拷贝使得S1、S2和S3指向同一块内存,任何一方的变动都会影响到另一方。
③在释放内存
的时候,会造成原有的内存没有被释放
也不走默认构造函数,走的是默认的拷贝构造函数,造成内存泄露。
三、String类加入引用计数的浅拷贝的分析与实现
1. 引用计数原理
当类里面有指针对象
时,进行简单赋值的浅拷贝
,两个对象指向同一块内存
,存在崩溃
的问题!为了解决这个问题,我们可以采用引用计数
。在引用计数中,每一个对象负责维护对象所有引用的计数值
。当一个新的引用指向对象时,引用计数器就递增
,当去掉一个引用时,引用计数就递减
。当引用计数到零时,该对象就将释放占有的资源。
源代码及注释
#include <iostream>
#include <Windows.h>
using namespace std;
#pragma warning (disable:4996)
class String
{
public:
String(const char* pStr = "")//构造函数
:_pStr(new char[strlen(pStr) + 1])
,_count(0)//引用计数
{
if(NULL == *pStr)
*_pStr = '\0';
else
strcpy(_pStr,pStr);
_count++;//每次创建对象时+1
}
String(String& s)//拷贝构造函数
:_count(s._count)//将已存在的对象计数赋值给当前对象
{
_pStr = s._pStr;
_count = s._count + 1;//将原对象的计数器+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;
_count = s._count + 1;//将已存在的对象+1赋值给当前对象
}
return *this;
}
private:
char *_pStr;
int _count;//计数器
};
void FunTest()
{
String s1("Hello world");
String s2(s1);
String s3 = s2;
}
int main()
{
FunTest();
system("pause");
return 0;
}
开辟空间、释放资源分析
①首先根据创建的对象开辟空间
②调用3次析构函数后,其对应的计数器count令人发指
很遗憾,虽然引用计数的引入
防止了一块内存空间被多次析构释放的可能,但随之而来的计数器count并没有达到计数的统一,因此这样的程序依旧存在严重的Bug。
四、String类加入引用计数及静态成员变量的分析与实现
1. 为什么要加入静态成员变量
如何你阅读了博文的前半部分,就会对引用计数非常谨慎
,引入计数虽然防止了内存被析构释放多次的可能
,但是随之带来的却是计数器count的不统一
,导致内存无法被释放
。为了解决这个问题,我加入了静态成员变量来保证计数器的统一,来方便后面内存空间的释放。
源代码及注释
#include <iostream>
#include <Windows.h>
using namespace std;
#pragma warning (disable:4996)
class String
{
public:
String(const char* pStr = "")//构造函数
:_pStr(new char[strlen(pStr) + 1])
{
if(NULL == *pStr)
*_pStr = '\0';
else
strcpy(_pStr,pStr);
_count++;//每次创建对象时+1
}
String(String& s)//拷贝构造函数
{
_pStr = (char*)s._pStr;
s._count = _count;
_count++;
}
~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 = _count;
_count++;
}
return *this;
}
private:
char *_pStr;
static int _count;//计数器
};
int String::_count = 0;
void FunTest()
{
String s1("Hello world");
String s2(s1);
String s3 = s2;
}
int main()
{
FunTest();
system("pause");
return 0;
}
开辟空间、释放资源的分析
①根据创建的对象开辟内存空间
②调用3次析构函数的计数器分析
虽然静态成员变量的引入确实让计数器高度统一
,不过创建3个成员对象却要析构释放4次空间,无疑又是一个严重的Bug
,看来实现一个程序并没有那么容易,又得钻研其他的方法了。
五、String类加入引用计数及指针的分析与实现
1. 为什么要引入指针?
静态成员变量的引入
最终带来的是析构调用次数的增加
,那么指针的引入无疑是为了克服这一难关。但是指针的引入却增加了空间的开销
,大部分程序员都知道引入指针也就相当于让程序存在了安全隐患
,各种资源的释放以及释放之后
防止野指针的问题同样重要。
源代码及注释
#include <iostream>
#include <Windows.h>
using namespace std;
#pragma warning (disable:4996)
class String
{
public:
String(const char* pStr = "")//构造函数
:_pStr(new char[strlen(pStr) + 1])
,_count(new int(0))
{
if(NULL == *pStr)
*_pStr = '\0';
else
strcpy(_pStr,pStr);
*_count = 1;
}
String(String& s)//拷贝构造函数
:_count(s._count)
{
_pStr = (char*)s._pStr;
_count = s._count;
(*_count)++;
}
~String()//析构函数
{
if(NULL == _pStr)
return;
else
{
if(--(*_count) == 0)//计数器为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:
char *_pStr;
int* _count;//计数器
};
void FunTest()
{
String s1("Hello world");
String s2(s1);
String s3 = s2;
}
int main()
{
FunTest();
system("pause");
return 0;
}
开辟空间、释放资源的分析
①根据创建的3个对象进行开辟空间,查看count指针中保存的数值
②调用3次析构函数,计数器指针count中保存的数值归0
经过多次修改调试,String类的浅拷贝Bug也相继修复完毕
,但是,值得注意的是,count指针的引入带来了空间复杂度的增大
,而且如果忘记释放count指针,以及count指针为指向Null成为野指针都会是程序的一大败笔
。因此,指针的引入确实让程序更好的运行
,最优的算法依旧需要去揣摩去发现。
六、写时拷贝完美诠释String类的浅拷贝
1. 浅拷贝的艰辛历程
从简单的赋值浅拷贝
(内存被多次析构释放)->加入引用计数的浅拷贝实现
(计数器难以统一)->利用引用计数及静态成员变量浅拷贝实现
(忽略了静态成员变量为所有成员所共享)->引用计数利用指针的浅拷贝实现
(功能实现了但开辟了存放指针的空间,且不安全)->写时拷贝
。一代一代的过渡终于可以完美的实现Strng类的浅拷贝了
。
2. 什么是写时拷贝
Linux下写时拷贝
写时拷贝详解
写时拷贝缺陷详解
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
写时拷贝(Copy On Write) 使用了引用计数
,在开辟空间时会多开辟4个字节用来保存引用计数的值
。当第一个对象构造时,String的构造函数会根据传入的参数从堆上分配内存
,当有其他对象需要这块内存时,计数器++
,当有对象进行析构释放时,引用计数--,直到最会一个对象被析构,此时计数器为0
。只有这样程序才会真正的释放掉这块从堆上分配的内存。
源代码及注释
#include <iostream>
#include <Windows.h>
#pragma warning (disable:4996)
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);//拷贝字符串
}
}
String(const String& s)//拷贝构造函数
:_pStr(s._pStr)
{
++(*(int*)(_pStr - 4));//向前偏移4个字节将计数+1
}
~String()//析构函数
{
if(NULL == _pStr)
{
return;
}
else
{
if(--(*(int*)(_pStr - 4)) == 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 t)
{
if(t >= 0 && t < strlen(_pStr))//下标非法判断
{
if((*(int*)(_pStr - 4)) > 1)//多个对象指向同一块空间
{
char *pTemp = new char[strlen(_pStr) + 4 + 1];//开辟临时空间
pTemp += 4;//向后偏移4个字节
strcpy(pTemp,_pStr);//拷贝字符串
--(*(int*)(_pStr - 4));//计数-1
_pStr = pTemp;//将当前的对象指向临时空间
*((int*)(_pStr - 4)) = 1;//将新空间的计数置为1
}
return _pStr[t];
}
}
private:
char *_pStr;
};
void FunTest()
{
String s1("Hello world");
String s2(s1);
String s3 = s2;
s3[3] = 'm';
}
int main()
{
FunTest();
system("pause");
return 0;
}
地址空间图
开辟空间、释放资源的分析
①根据创建的对象开辟空间
②执行s3[3] = ‘m’,改变s3对象中的值
③调用析构函数释放s3
④调用析构函数释放s2
⑤调用析构函数释放s1
大家从监视窗口可以清晰发现,当依次调用析构函数进行资源释放的时候
,创建的对象遵循先入后出的原则
,依次释放,没有发生内存空间的非法访问或内存泄漏问题,因此就目前来说,写时拷贝算是浅拷贝中最合适的方法。
七、String类的普通版本与简洁版本的深拷贝
1. 什么是深拷贝?
深拷贝不同于浅拷贝,它在拷贝的时候会为新对象开辟一块新的内存空间,然后将原对象的内容拷贝到新开辟的空间
,这样在资源释放的时候就不会牵扯到多次析构的问题。比如构造了S1与S2两个对象,在构造S2时拷贝一块跟S1指向数据库一样大的数据块,并将值拷贝下来,这样S1与S2指向各自的数据块,析构时也自然释放自己的数据块。
源代码及注释(简洁版)
#include <iostream>
#include <Windows.h>
using namespace std;
#pragma warning (disable:4996)
class String
{
public:
//构造函数
String(const char* pStr = "")
:_pStr(new char[strlen(pStr) + 1])
{
if(*_pStr == NULL)
*_pStr = '\0';
else
strcpy(_pStr,pStr);
}
//拷贝构造函数
String(String& s)
:_pStr(NULL)//初始化_pStr
//防止交换后pTemp指向随机空间
{
String pTemp(s._pStr);//给出临时空间,交换后s不为NULL
std::swap(_pStr,pTemp._pStr);
}
//析构函数
~String()
{
if(NULL == _pStr)
{
return;
}
delete[] _pStr;
_pStr = NULL;
}
// 赋值运算符=号的重载
String& operator=(const String& s)
{
if(_pStr != s._pStr)
{
String pTemp(s._pStr);//给出临时空间,交换后s不为NULL
std::swap(_pStr,pTemp._pStr);
}
return *this;
}
private:
char *_pStr;
};
void FunTest()
{
String s1("Hello world");
String s2(s1);
String s3 = s2;
}
int main()
{
FunTest();
system("pause");
return 0;
}
深拷贝的内存分析
①调用构造函数创建S1、S2、S3后,发现3个对象的地址完全不同,证明处于不同的内存空间中。
②调用三次析构函数,发现在释放的过程中遵循先入后出的原则,没有发生任何的内存泄漏或者程序奔溃问题。
2. 普通版本的深拷贝
对于拷贝构造函数:开辟新空间
,在进行内容的拷贝使其各有各自的空间以至于在析构的时候不至于程序运行崩溃
。比如创建了S1、S2两个对象,构造S2对象时拷贝一块跟S1指向数据块一样大的数据块,并将值拷贝下来,这样S1、S2指向各自的数据块,析构时释放各自的数据块,因此就不会出现浅拷贝空间被多次析构的错误。但是美中不如的是,虽然深拷贝看起来较为容易的解决了字符串的拷贝,但是新开辟的空间无疑成为了资源的浪费。
//String.h
#include <iostream>
#include <Windows.h>
#include <string.h>
#pragma warning (disable:4996)
using namespace std;
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 = new char[strlen(s._pStr) + 1];
strcpy(_pStr,s._pStr);
}
~String()
{
if(_pStr)
{
delete[] _pStr;
_pStr = NULL;
}
}
size_t Size()const;//字符串的大小
size_t Lengh()const;//字符串的长度
char& operator[](const size_t index);//下标界定符
const char& operator[](size_t index)const;
bool operator>(const String& s);
bool operator<(const String& s);
bool operator==(const String& s);
bool operator!=(const String& s);
void Copy(const String& s);
String operator+(const String& s);
bool strstr(const String& s);
String& operator=(const String& s);
String& operator+=(const String& s);
friend ostream& operator<<(ostream& _cout,const String& s);
private:
char *_pStr;
};
//main.cpp
#include "String.h"
void FunTest()
{
String s1("Hello world");
String s2(s1);
String s3 = s1 + s2;
cout<<"s1 = "<<s1<<endl;
cout<<"s2 = "<<s2<<endl;
cout<<"s3 = "<<s3<<endl;
}
int main()
{
FunTest();
system("pause");
return 0;
}
//main.cpp
#include "String.h"
size_t String::Size()const
{
size_t count = 0;
char *p = _pStr;
while(*p != '\0')
{
++count;
p++;
}
return count;
}
size_t String::Lengh()const
{
size_t count;
char *p = _pStr;
while(*p != '\0')
{
++count;
p++;
}
return count;
}
char& String::operator[](size_t t)
{
if(t >= 0 && t <= strlen(_pStr))
return _pStr[t];
}
const char& String::operator[](size_t t)const
{
return _pStr[t];
}
bool String::operator>(const String& s)
{
char *pTemp1 = _pStr;
char *pTemp2 = s._pStr;
while(*pTemp1 == *pTemp2)
{
pTemp1++;
pTemp2++;
}
if(*pTemp1 > *pTemp2)
return true;
else
return false;
}
bool String::operator<(const String& s)
{
char *pTemp1 = _pStr;
char *pTemp2 = s._pStr;
while (*pTemp1 == *pTemp2)
{
pTemp1++;
pTemp2++;
if(*pTemp1 < *pTemp2)
return true;
else
return false;
}
return false;
}
bool String::operator==(const String& s)
{
int ret = strcmp(_pStr,s._pStr);
if(0 == ret)
return true;
return false;
}
bool String::operator!=(const String& s)
{
return !(*this == s);
}
bool String::strstr(const String& s)
{
char* pTemp1 = _pStr;
char* pTemp2 = s._pStr;
char pTemp = NULL;
while(*pTemp1 != '\0' && *pTemp2 != '\0')
{
while(*pTemp1 != *pTemp2)
{
pTemp1++;
pTemp2++;
}
pTemp1++;
}
if(*pTemp2 == '\0')
return true;
else
return false;
}
String& String::operator=(const String& s)
{
if(this != &s)
{
delete[] _pStr;
_pStr = new char[strlen(s._pStr) + 1];
strcpy(_pStr,s._pStr);
}
return *this;
}
ostream& operator<<(ostream& _cout,const String& s)
{
_cout<<s._pStr;
return _cout;
}
String String::operator+(const String& s)
{
String s1;
if (!s._pStr)
s1 = *this;
else if (!_pStr)
s1 = s;
else {
s1._pStr = new char[strlen(_pStr) + strlen(s._pStr) + 1];
strcpy(s1._pStr, _pStr);
strcat(s1._pStr, s._pStr);
}
return s1;
}
String& String::operator+=(const String& s)
{
char *Temp1 = (*this)._pStr;
char *Temp2 = s._pStr;
int len1 = strlen(Temp1);
int len2 = strlen(Temp2);
char *Buff = NULL;
char *end = NULL;
Buff = new char[len1 + len2 + 1];
strcpy(Buff, Temp1);
end = Buff + len1;
strcpy(end, Temp2);
delete[]_pStr;
_pStr = Buff;
return (*this);
}
void String::Copy(const String& s)
{
int idx = 0;
char *pTemp1 = new char[strlen(s._pStr) + 1];
char *pTemp2 = s._pStr;
for (idx = 0; pTemp2[idx] != '\0'; idx++)
{
pTemp1[idx] = pTemp2[idx];
}
pTemp1[idx] = '\0';
delete[]_pStr;
_pStr = pTemp1;
}