🧸🧸🧸各位大佬大家好,我是猪皮兄弟🧸🧸🧸
在string类的模拟实现当中,我觉得最后的\0是很重要的,不管是push_back后的调整,还是开辟空间考虑+1,这些都很容易忘记\0
一、string拷贝构造模拟
1.无参的默认构造模拟
//无参
class string
{
public:
string()
:_str(new char[1])//主要是为了与delete[] 匹配
,_size(0)
,_capacity(0)
{
_str[0]=0;//'\0'
}
private:
char*_str;
size_t _size;
size_t _capacity;
};
2.缺省的默认构造模拟
注意:"“空串是默认有\0的,所以缺省值给”"也行
class string
{
public:
string(const char*str = "")//空串里面默认有\0
:_str(new char[strlen(str)+1])//主要是为了与delete[] 匹配
,_size(strlen(str))
,_capacity(strlen(str))
{
strcpy(_str,str);
}
private:
char*_str;
size_t _size;
size_t _capacity;
};
优化写法:
因为初始化列表初始化的顺序是按照成员变量的声明顺序来的,所以这里不建议用初始化列表去初始化
class string
{
public:
string(const char*str = "")//空串里面默认有\0
{
_size= strlen(str);
_capacity = _size;
_str = new char[_size+1];//+1存放\0
strcpy(_str,str);
}
private:
char*_str;
size_t _size;
size_t _capacity;
};
二、string拷贝构造模拟
1.默认生成的拷贝构造 深浅拷贝 问题
当我们不去显示写拷贝构造函数,编译器会自己生成,对于内置类型,完成值拷贝(浅拷贝),对于自定义类型,去调用该自定义类型的拷贝构造函数,那么,对于指针这种内置类型,它拷贝的是地址,所以拷贝出来的string指向的是同一块空间
问题:
1.两个字符串可以互相修改,因为是同一个地址
2.析构的时候会降同一个地址析构两次 ,直接崩溃(野指针问题)
3.中途 改变了指向的话还会出现内存泄漏的问题
2.拷贝构造模拟(普通写法)
class string
{
public:
string(const string&s)//构造函数是char*,拷贝构造是用的同类型 string
:_str(new char[s._capacity+1])
,_size(s._size)
,_capacity(s._capacity)
{
strcpy(_str,s._str);
}
private:
char *_str;
size_t _size;
size_t _capacity;
};
3.拷贝构造模拟(现代写法,复用构造函数)
现代写法的优势在于:对于以后更加复杂的类,复用是一种很好的办法
class string
{
public:
//库里提供的swap交换代价极高,会先深拷贝出来一个字符进行交换
//所以对深拷贝的类,绝对不能不用库里的swap
void swap(string&s)
{
::swap(_str,s._str);
::swap(_size,s._size);
::swap(_capacity,s._capacity);
}
string(const string&s)//构造函数是char*,拷贝构造是用的同类型 string
:_str(nullptr)
//这里是对_str指针进行一个初始化,我们都知道,随即指针是不能被使用的,也不能被释放,直接交换的话会崩溃。因为空间可能不属于我们
,_size(0)
,_capacity(0)
{
string tmp = s._str;
swap(tmp);//交换this和tmp的数据
}
private:
char *_str;
size_t _size;
size_t _capacity;
};
浅拷贝也就是逐字节进行值拷贝,遇到指针的时候拷贝过来的就是起始的地址,所以会出现深浅拷贝的问题,那么对于深拷贝,深拷贝也就是自己开辟一块空间
三、赋值运算符重载模拟
默认得赋值运算符重载也非常粗暴,也是浅拷贝
1.operator=(普通写法)
string &operator=(const string &s)
{
if(&s!=this)
{
char*tmp = new char[s._capacity+1];
strcpy(tmp,s._str);
delete[] _str;
_str =tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
1.如果不delete原来的而直接进行拷贝
a.原来的空间不够,扩容或者重新开辟
b.原来的空间非常大,现在需要的非常小
那不如直接释放了开空间
/
2.如果说需要new出来的空间很大,虽然说new是抛异常,会跳到catch去处理异常,但是把原对象破坏掉了,所以,需要需要先用一个临时的来试试水。
2.operator=(现代写法)
string& operator=(const string&s)
{
if(this!=&s)
{
string tmp(s);
/*
利用拷贝构造,拷贝构造是构造函数的一种函数重载
构造函数传递的是char*的字符串
拷贝构造传递的是string同类型的字符串
*/
swap(tmp);
}
return *this;
}
对于深拷贝的类不要去用全局的swap,首先,代价极大,而且在其过程中又会存在赋值运算符,所以会出现一直循环调用,最后栈溢出
string& operator=(string s)
{//更简洁
swap(s);//上面写的swap
rerturn *s;
}
四、其他部分的模拟实现
1.reserve模拟
void reserve(size_t n)
{
if(n>_capacity)
{
char*tmp = new char[n+1];
//注意n+1,给\0留位置
strcpy(tmp,_str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
2.push_back模拟
void push_back(char ch)
{
if(_size==_capacity)
{
//扩容
reserve(_capacity==0?4:_capacity*2);
}
_str[_size]=ch;
++_size;
_str[_size]=0;//\0
}
3.append模拟实现
void append(const char* str)
{
int len = strlen(str);
if(_size+len<_capacity)
{
reserve(_size+len+1);
//reserve里面会 修改capacity
}
strcpy(_str+_size,str);
_size+=len;
_str[_size++]=0;
}
4.strcat相比之下的缺点
strcat追加的缺点
1.strcat是从头开始找的,所以效率会低很多
2.strcat是找到\0就开始追加,然而string是以size为准的,中间可能也会有\0,所以可能错误追加
5.operator+=模拟
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(char* str)
{
append(str);
return *this;
}
6.insert模拟
string&insert(size_t pos,char ch)
{
assert(pos<=_size);//因为size_t,不用>=0
if(_size==_capacity)
{
reserve(_capacity ==0?4:2*_capacity);
}
int end = _size+1;//将\0也移走
while(end>pos)
{
_str[end] = _str[end-1];
--end;
}
_str[pos] = ch;
_size++;
return *this;
}
string& insert(size_t pos,const char*str)
{
assert(pos<=_size);
int len = strlen(str);
if(_size+len > _capacity)
{
reserve(_size+len);
}
int end = _size+len;
while(end>pos+len)
{
_str[end]=_str[end-len];
--end;
}
strncpy(_str+pos,str,len);
_size +=len;
return *this;
}
push_back()可以复用insert
7.operator<<与operator>>
因为需要保证<<两边的操作数的顺序,所以需要在类外去写流插入和流提取的重载,类里写的话左边都是this
//在string类中友元这两个函数
ostream& operator<<(ostream&out,const string &str)
{
//因为string中可能有\0,所以不能直接用cout,
//cout碰到\0就停止了,所以
for(int i=0;i<str.size();i++)
{
out<<str[i];
}
return out;
}
istream& operator>>(istream&in,string&str)
{
str.clear();
char ch;
ch=in.get();//不会忽略换行和空格
const size_t N=32;
char buff[N];
size_t i=0;
//优化频繁扩容
while(ch!=' '&&ch!='\n')
{
buff[i++]=ch;
if(i==N-1)
{
buff[i]='\0';
s+=buff;
i=0;
}
ch=in.get();
}
buff[i]=0;
s+=buff;
return in;
}
1.关于流插入平凡扩容的问题,用一个buff数组来做缓冲,这是一个临时的,出了作用域就销毁了,设计的思路类似与缓冲区
2.其次,在流提取之前还要先clear一下内容
3.C++允许const的数去做一个定长数组
在vs下对于string也有一个优化,是加了一个buff[16]的缓冲,作为成员变量,所以在sizeof(string)的时候,发现标准库里的string大小并不是12而是28
8.resize模拟
void resize(size_t n,char ch=0)
{
if(n>_size)
{
reserve(n);
for(size_t i=_size;i<n;i++)
{
_str[i]=ch;
}
_str[n]='\0';
_size=n;
}
else
{
_str[n]='\0'
_size = n;
}
}
9.string类型中的排序
这里需要利用到仿函数,后面再说,先简单带过一下
五、优化 Copy-On-Write
有人觉得深拷贝代价太大,就提出了 引用计数+写时拷贝 的方法
1.增加一个引用计数,多少个引用指向浅拷贝的哪个空间,就计数多少,计数器减到1的时候才去析构,不然不析构
2.写时拷贝,去写的时候才去深拷贝(延迟拷贝动作)
六、总结
上面对部分string类中的东西进行了模拟实现,希望能帮助大家,也希望能和大家一起学到东西,下一篇更新vector相关内容,谢谢大家支持!!