C++学习之第十三天-移动语义与完成COW String类

1.如何区分左值、右值、左值引用、右值引用、const左值引用。

String(const char *);
String(const String &);
~String();
问题:临时对象的构造和析构带来了不必要的资源拷贝
解决:右值机制-可以在语法层面识别出临时对象,在使用临时对象构造新对象时(即拷贝构造),将临时对象的所有资源都转移到新的对象中,能消除这种不必要的拷贝。

左值和右值:
1.能对表达式进行取地址的为左值,否则为右值;左值和右值都针对表达式而言的。
2.左值是指表达式结束后,依然存在的持久对象。
3.右值是指表达式结束时就不再存在的临时对象。

左值引用

根据修饰符的不同,可以把左值引用分为非常量左值和常量左值
1.非常量左值引用只能绑定到非常量左值,eg:
    int ia=10; int &a = ia;
    //int &ib = 10;//这个是错误的,不允许绑定到常量右值
2.常量左值引用-可以绑定所有类型的值:左值、右值。
    int ib = 30; 
    const int &b = ib; //绑定到非常量左值
    const int &ri = 20; //绑定常量右值

右值引用,int &&ref

1.右值只能绑定到右值:int &&ref=0;
2.右值不允许绑定到左值:int x; int &&ref=x;//会语法报错
3.常量右值引用没有现实意义(毕竟右值引用的初衷在于移动语义,而移动就意味着『修改』)。

移动语义

1.编译器只对右值引用才能调用转移构造函数和转移赋值函数,而所有命名对象都只能是左值引用,如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用,怎么做呢?

标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。

2.对于右值引用而言,它本身ref是可以是左值、也可以是右值。int &&ref

二、编程题

1、移动语义实战-完成自定义String类,写时复制COW string类的实现,要求能够区分下标运算符的读写操作。

复习COW原理:

1.引出

       对于字符串的深拷贝,无论什么情况,都是采用拷贝字符串内容的方式解决,这也是我们之前已经实现过的方式。这种实现方式,在需要对字符串进行频繁复制而又并不改变字符串内容时,效率比较低下。

2.原理和实现方式。

1.string发生拷贝构造或者赋值的时候,不会复制字符串的内容,而是增加一个引用计数,字符串指针进行浅拷贝,当需要对字符串内容进行修改或者写操作时,才进行深拷贝。

​ 2.实现。

        1.给管理字符串指针多申请4个字节的空间大小,前4个字节用来存储引用计数。

        2.字符串发生拷贝构造操作的时候和赋值操作的时候,只进行浅拷贝,引用计数+1

        3.发生取下标操作的时候,返回一个代理类对象来管理和重载=和<<运算符,因而能够区分读写操作。设计模式之代理模式的运用。eg: str[0]是一个类的对象

                1.=, 代理对象发生赋值操作时,先判断引用计数

                       -大于1,说明数据被共享:开辟临时空间,引用计数减1后再指向新空间,初始化新空间的引用计数,再进行真正进行下标修改的位置

                       -不大于1的话,直接进行修改即可

                2.<<, 可以在代理类用友元方式进行操作;也可以在类中用operator来进行自定义类型转换。---了解operator自定义类型的用法

​         4.析构函数:在引用计数为0的情况才释放。

mystring.h

#include <iostream>
using namespace std;
#include <string.h>
class CharProxy;
class String
{
    friend ostream& operator<<(ostream &os,const String &rhs);
    //设计模式之代理模式
    class CharProxy
    {
    public:
        CharProxy(String &self,size_t idx)
            :_self(self)
             ,_idx(idx)
        {
        }
        char &operator=(const char &ch);

        operator char()//自定义CharProxy类型向char类型进行转换    
        {   //str[i]是一个CharProxy对象,被强制转成了char类型
            //str[i]就变成了一个字符
            return _self._pstr[_idx];//目标类型是返回一个char类型
        }


    private:
        String &_self; //未创建完整的类,可以声明为引用类型
        size_t _idx;
    };
public:
    String();//1.无参构造函数,前面四个字节存储引用计数,初始化的时候把指针偏移4个字节
    String(const char *pstr);//2.有参构造函数
    String(const String &rhs);//3.拷贝构造函数
    String(String &&rhs);//4.移动构造函数-fno-elide-constructors
    String &operator=(const String &rhs);//5.赋值运算符函数
    String &operator=(String &&rhs);//6.移动赋值
    //7.设计模式之代理模式,类对象遇到取下标操作,返回一个代理类的对象
    CharProxy operator[](size_t idx)
    {
        return CharProxy(*this,idx);
    }
    //1.对象+下标:str1[idx]得到的结果是一个CharProxy类对象
    //2.在CharProxy类里面对=和<<进行运算符重载可以区分str1[idx]是左值还是右值
    ~String()
    {

        //cout<<"引用计数值为:"<<getRefcount()<<endl;
        cout<<"~String()"<<endl;
        release();
    }//析构函数

    //获取C语言格式字符串
    char *c_str()const
    {
        return _pstr;
    }

    int size()
    {
        return strlen(_pstr);
    }
    int getRefcount();//获取引用计数
private:
    //1.引用计数的初始化
    void initRefcount(); //初始化引用计数,为1
    void increaseRefcount();//引用计数+1
    void decreaseRefcount();//引用计数-1
    void release();//释放左操作数
    char *_pstr;
};

cow.cc

#include "mystring.h"


//1.无参构造函数,前面四个字节存储引用计数,初始化的时候把指针偏移4个字节
String::String()//+4偏移4个字节
:_pstr(new char[4+1]()+4)//无参默认分配一个字节,留4个字节存储引用计数
{
    cout<<"String()"<<endl;
    initRefcount(); //初始化引用计数,为1
}

String::String(const char *pstr)//2.有参构造函数
:_pstr(new char[strlen(pstr)+5]()+4)
{
    cout<<"String(const char *pstr)"<<endl;
    strcpy(_pstr,pstr);
    initRefcount();//初始化引用计数
}

String::String(const String &rhs)//3.拷贝构造函数
:_pstr(rhs._pstr)//进行浅拷贝,然后引用计数加1即可
{
    cout<<"String(const String &rhs)"<<endl;
    increaseRefcount();//引用计数+1
}

char &String::CharProxy::operator=(const char &ch)
{
    if(_idx <_self.size())
    {
        if(_self.getRefcount()>1)//大于1,是共享的
        {
            char *tmp = new char[_self.size()+5]()+4;
            strcpy(tmp, _self._pstr);
            _self.decreaseRefcount(); //引用计数--
            _self._pstr= tmp;//指向新堆
            _self.initRefcount();//初始化引用计数
        }

        _self._pstr[_idx]=ch; //真正进行下标修改的位置
        return _self._pstr[_idx];
    }

    else
    {
        static char charnull = '\0';
        return charnull;
    }
}


ostream& operator<<(ostream &os,const String &rhs)
{
    os<<rhs._pstr;

    return os;
}
    //初始化引用计数
void String::initRefcount()
{
    *(int *)(_pstr-4)=1;
}
void String::increaseRefcount()//引用计数+1
{
    ++*(int*)(_pstr-4);
}
int String::getRefcount()
{
    return *(int *)(_pstr-4);
}
void String::decreaseRefcount()//引用计数-1
{
    --*(int*)(_pstr-4);
}
void String::release()
{
    decreaseRefcount();//引用计数--
    if(0==getRefcount())
    {
        delete[](_pstr-4);
    }
}

main.cc

#include "mystring.h"
#if 0
void test01()
{
    //1.无参构造函数、移动构造函数、赋值运算符重载函数测试。
    String str1; //1.调用无参构造函数
    
    //2.移动构造函数
    String s1 = "hello world";
    //1.先执行String(const char *)把hello world 变成String("hello world"),临时变量是一个右值 
    //2.String(String &&rhs)检测到是个右值,调用移动构造函数,转移临时对象资源
    cout<<s1<<endl;//hello world
    cout<<"s1的引用计数:"<<s1.getRefcount()<<endl;//1,获取引用计数
    cout<<endl<<endl;
    //3.赋值运算符重载
    str1 = s1;
    cout<<str1<<endl;//hello world
    cout<<"str1的引用计数:"<<str1.getRefcount()<<endl;//2
    cout<<"s1的引用计数:"<<s1.getRefcount()<<endl;//2
    
    cout<<str1.c_str()<<endl;//hello world

    cout<<endl<<endl;

    //拷贝构造只进行浅拷贝
    String s2  = s1 ;
    cout<<"s1="<<s1<<endl;
    cout<<"s2="<<s2<<endl;
    cout<<"s1.getRefCount()="<<s1.getRefcount()<<endl;//3
    cout<<"s2.getRefCount()="<<s2.getRefcount()<<endl;//3
    printf("s1's address:%p\n",s1.c_str());//地址一样
    printf("s2's address:%p\n",s2.c_str());

}
//拷贝构造和移动构造函数的使用
void test02()
{
    //1.拷贝构造函数
    String str1("hello");
    String str2(str1);
    cout<<str2<<endl;
    cout<<"str2的引用计数:"<<str2.getRefcount()<<endl;//2
    cout<<"str1的引用计数:"<<str1.getRefcount()<<endl;//2,只是进行了浅拷贝

    
    cout<<endl<<endl;
    str2 = String("world");//移动赋值运算符重载测试
    //1.先释放左操作数,str1的引用计数-1, 
    //2.转移临时对象资源,临时对象资源的引用计数由1变成2
    //3.此句话执行结束,临时对象资源要自动释放,引用计数由2变成1,临时资源被托管成功
    cout<<str2<<endl;//world
    cout<<str2.getRefcount()<<endl;//1
    cout<<str1.getRefcount()<<endl;//1

}
#endif
//COW代理模式实现,对str[0]进行读写操作
void test03()
{
    String s3("hello");
    String s2(s3);
    printf("s3's addres's:%p\n",s3.c_str());
    printf("s2's addres's:%p\n",s2.c_str());
    cout<<s3[3]<<endl;//代理对象被转为char类型
    char h = s3[3];
    cout<<h<<endl;//只进行读操作:s3和s2的地址还是一样的
    printf("s3's addres's:%p\n",s3.c_str());
    printf("s2's addres's:%p\n",s2.c_str());
    //引用计数都为2
    cout<<"s3.getRefcount()="<<s3.getRefcount()<<endl
        <<"s2.getRefcount()="<<s2.getRefcount()<<endl;
    cout<<endl<<endl;
    //对s3进行修改操作时,给s3另开辟空间
    s3[3] = 'p';
    printf("s3's addres's:%p\n",s3.c_str());
    printf("s2's addres's:%p\n",s2.c_str());
     cout<<"s3.getRefcount()="<<s3.getRefcount()<<endl
        <<"s2.getRefcount()="<<s2.getRefcount()<<endl;
    cout<<"s2="<<s2<<endl;
    cout<<"s3="<<s3<<endl;

    cout<<endl<<endl;


}
int main()
{
    //test01();
    //test02();
    test03();
    return 0;
}

运行结果:

String(const char *pstr)  //有参构造
String(const String &rhs)  //拷贝构造
s3's addres's:0x55ca9e7ade74//地址一样
s2's addres's:0x55ca9e7ade74
l
l
s3's addres's:0x55ca9e7ade74 //进行读取操作后,地址还一样
s2's addres's:0x55ca9e7ade74
s3.getRefcount()=2
s2.getRefcount()=2                //引用计数2


s3's addres's:0x55ca9e7ae2a4        //写操作完后地址变了
s2's addres's:0x55ca9e7ade74
s3.getRefcount()=1
s2.getRefcount()=1         //引用计数1
s2=hello
s3=helpo


~String()
~String()
 

猜你喜欢

转载自blog.csdn.net/weixin_49278191/article/details/121292285