0


【C++】string类的模拟实现.深浅拷贝与引用计数,写时拷贝的优化

🧸🧸🧸各位大佬大家好,我是猪皮兄弟🧸🧸🧸
在这里插入图片描述

文章目录

在string类的模拟实现当中,我觉得最后的\0是很重要的,不管是push_back后的调整,还是开辟空间考虑+1,这些都很容易忘记\0

一、string拷贝构造模拟

1.无参的默认构造模拟

//无参classstring{public:string():_str(newchar[1])//主要是为了与delete[] 匹配,_size(0),_capacity(0){
        _str[0]=0;//'\0'}private:char*_str;
    size_t _size;
    size_t _capacity;};

2.缺省的默认构造模拟

注意:"“空串是默认有\0的,所以缺省值给”"也行

classstring{public:string(constchar*str ="")//空串里面默认有\0:_str(newchar[strlen(str)+1])//主要是为了与delete[] 匹配,_size(strlen(str)),_capacity(strlen(str)){strcpy(_str,str);}private:char*_str;
    size_t _size;
    size_t _capacity;};

优化写法:
因为初始化列表初始化的顺序是按照成员变量的声明顺序来的,所以这里不建议用初始化列表去初始化

classstring{public:string(constchar*str ="")//空串里面默认有\0{
        _size=strlen(str);
        _capacity = _size;
        _str =newchar[_size+1];//+1存放\0strcpy(_str,str);}private:char*_str;
    size_t _size;
    size_t _capacity;};

二、string拷贝构造模拟

1.默认生成的拷贝构造 深浅拷贝 问题

当我们不去显示写拷贝构造函数,编译器会自己生成,对于内置类型,完成值拷贝(浅拷贝),对于自定义类型,去调用该自定义类型的拷贝构造函数,那么,对于指针这种内置类型,它拷贝的是地址,所以拷贝出来的string指向的是同一块空间
问题:
1.两个字符串可以互相修改,因为是同一个地址
2.析构的时候会降同一个地址析构两次 ,直接崩溃(野指针问题)
3.中途 改变了指向的话还会出现内存泄漏的问题

2.拷贝构造模拟(普通写法)

classstring{public:string(const string&s)//构造函数是char*,拷贝构造是用的同类型 string:_str(newchar[s._capacity+1]),_size(s._size),_capacity(s._capacity){strcpy(_str,s._str);}private:char*_str;
    size_t _size;
    size_t _capacity;};

3.拷贝构造模拟(现代写法,复用构造函数)

现代写法的优势在于:对于以后更加复杂的类,复用是一种很好的办法

classstring{public://库里提供的swap交换代价极高,会先深拷贝出来一个字符进行交换//所以对深拷贝的类,绝对不能不用库里的swapvoidswap(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 =newchar[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模拟

voidreserve(size_t n){if(n>_capacity){char*tmp =newchar[n+1];//注意n+1,给\0留位置strcpy(tmp,_str);delete[] _str;
        _str = tmp;
        _capacity = n;}}

2.push_back模拟

voidpush_back(char ch){if(_size==_capacity){//扩容reserve(_capacity==0?4:_capacity*2);}
    _str[_size]=ch;++_size;
    _str[_size]=0;//\0}

3.append模拟实现

voidappend(constchar* 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,不用>=0if(_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,constchar*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模拟

voidresize(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相关内容,谢谢大家支持!!
在这里插入图片描述

标签: c++

本文转载自: https://blog.csdn.net/zhu_pi_xx/article/details/126699084
版权归原作者 猪皮兄弟 所有, 如有侵权,请联系我们删除。

“【C++】string类的模拟实现.深浅拷贝与引用计数,写时拷贝的优化”的评论:

还没有评论