c++笔记-内存管理对象移动

  1. 新标准最主要的特性是可以移动而非拷贝对象的能力。避免了由于移动对象较大而拷贝代价过大的缺点。标准库容器、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。
  2. 右值引用。为了支持移动操作而引入的新的引用类型。通过&&来获得右值引用,而不是(&)取址符。一般而言,一个左值表达式是一个对象身份,而一个右值表达式是对象的值。
类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。
对于常规的引用,我们在定义时不能将其绑定到要求转换的表达式、字面常量或是返回最右值的表达式
但是,右值引用却相反,可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上
int i = 33;     //我们定义了一个int类型值
int &r = i;     //正确:r引用i
int &&rr = i;   //错误:不能将右值引用绑定到一个左值上
int &r2 = i * 2;    //错误:i * 2是一个右值
const int &r3 = i * 3;  //正确:我们可以用一个const的引用绑定到一个右值上
int &&rr2 = i * 3;  //正确:可以将rr2绑定到乘法结果上

####返回左值引用的函数,连同赋值,下标,解引用和前置递增/递减运算符,都是返回左值表达式的例子。我们可以将常规的引用绑定到其上
####返回非引用类型的函数,连同算术,关系,位运算和后置递增/递减运算符,都是生成右值。我们可以将一个const的左值(常规)引用或者一个右值引用绑定到这类表达式上
  1. 左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,因此
所引用的对象将要被销毁
该对象没有其他用户

也就意味着:使用右值引用的代码可以自由接管所引用的对象的资源。
#Note#
    右值引用指向将要被销毁的对象。因此,我们可以从绑定到右值引用的对象“窃取”状态
  1. 变量可以看作只有一个运算对象而没有运算符的表达式。类似其他任何表达式,变量表达式也有属性,它是左值。因此,不能将一个右值引用绑定到另一个右值引用上来。
int &&rr1 = 32; //正确:字面常量是右值
int &&rr2 = rr1;    //错误:表达式rr1是左值

但是我们可以通过调用move标准库函数来获得绑定到左值上的右值引用。
int &&rr3 = std::move(rr1); //正确
#注意#
    调用move就意味着承诺:除了对rr1赋值或销毁外,我们将不再使用它。
    即,我们调用move后,我们不能对移后源对象的值做任何假设
    我们可以销毁一个移后源对象,也可以赋予新值,但是不能使用一个移后源对象的值。
  1. 类似string类,如果我们自己的类也支持移动和拷贝,那么也能从中受益
StrVec::StrVec(StrVec &&s) noexcept : element(s.element), first_free(s.first_free), cap(s.cap)
{
    //令s进入空指针状态,对其运行析构函数是安全的
    s.element = s.first_free = s.cap = nullptr;
}
关键字noexcept在新标准中引入的。
noexcept是我们承诺一个函数不抛出异常的一种方法,出现在阐述列表和初始化列表直接
class StrVec
{
public:
    StrVec(StrVec&&) noexcept;
};
StrVec::StrVec(StrVec &&s) noexcept : /* 成员初始化器 */
{ /* 函数体 */ }
#Note#
    不抛出异常的移动构造函数和移动赋值运算符必须标记位noexcept
    
StrVec &StrVec::operator=(StrVec &&rhs) noexcept 
{
    //直接检测自赋值
    if(this != &rhs)    //这里检测this的地址和rhs的地址是否一样,即两者是否指向相同的对象
    {
        free(); //释放原有元素
        element = rhs.element;
        first_free = rhs.first_free;
        cap = rhs.cap;
        //然后将rhs置为可析构状态
        rhs.element = rhs.first_free = rhs.cap = nullptr;
    }
    return *this;
}
  1. 只有当一个类没有定义任何自己的拷贝控制成员,且他的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
假定Y是一个类,它定义了自己拷贝构造函数,但是未定义自己的移动构造函数
struct hasY
{
    hasY() = default;
    hasY(hasY&&) = default;
    Y mem;  //hasY将有一个删除的移动构造函数
}   //即编译器可以拷贝类型为Y的对象,但不能移动他们。

//如果我们显示地要求一个移动构造函数,编译器无法为其生成
hasY hy, hy2 = std::move(hy);   //错误:移动构造函数是删除的

如果一个编译器既有移动构造函数,也有拷贝构造函数,编译器将使用普通的函数匹配规则来确定使用哪个构造函数。
例如:在StrVec类中,拷贝构造函数接收一个const StrVec的引用,可以适用于任何可以转换为StrVec的类型
      移动构造函数接收一个StrVec&&参数,只能用于实参是右值的情况
StrVec v1, v2;
v1 = v2;    //v2是左值:使用拷贝赋值
StrVec getVec(istream &);   //getVec返回一个右值
v2 = getVec(cin);   //getVec(cin)是一个右值:使用移动赋值

当一个类只定义拷贝构造函数,没有定义移动构造函数时,由于可以将一个右值引用转换为类的常量引用
class Foo
{
public:
    Foo() = default;
    Foo(const Foo&);    //拷贝构造函数
    //其他成员定义,但未定义移动构造函数
}
Foo x;
Foo y(x);//使用拷贝构造函数
Foo z(std::move(x));    //使用拷贝构造函数,因为移动构造函数未定义
因为调用了move(x),返回一个绑定到x的Foo&&。由于可以将一个Foo&&转化为const Foo&,
因此可以执行拷贝而不报错
  1. 建议:应该将五个拷贝控制成员看作一个整体,一般来说,如果一个类中定义了任何一个拷贝操作,它就应该定义所有五个操作。有些类必须要定义拷贝构造函数、拷贝赋值运算符和析构函数。这些类通常拥有一个资源,而拷贝资源又通常会产生不必要的开支,因此定义移动构造函数和移动赋值运算符可以避免此问题。
对于Message类和Folder类,通过定义移动操作,Message可以使用string和set的移动操作来避免contents和folders拷贝时的额外开销
//从本Message移动Folder指针
void Message::move_Folders(Message *m)
{
    folders = std::move(m->folders);    //使用set的移动赋值运算符
    for(auto f : folders)
    {
        f->remMsg(m);
        f->addMsg(this);
    }
    m->folders.clear(); //确保销毁m是无害的
    //在执行move后,m.folders是空的,但不知道包含什么内容。因此确保为空
}
//Message的移动构造函数调用move来移动contents,并默认初始化自己的folders成员
Message::Message(Message&& m) : contents(std::move(m.contents))
{
    move_Folders(&m);   //移动folders并更新Folder指针
}
//移动赋值运算符直接检查自赋值情况
Message& Message::operator=(Message &&rhs)
{
    if(&rhs != this)
    {
        remove_from_Folders();  //将原本的Message从所有Folders中移除
        contents = std::move(rhs.contents); //移动赋值运算符,从rhs将contests移动到this对象。
        move_Folder(&rhs);  //重置Folders指向本Message
    }
    return *this;
}
  1. 建议:由于一个移后源对象具有不确定状态,对其调用std::move时危险的。当我们调用move时,必须确认移后源对象没有其他用户。
  2. 除了构造函数和赋值运算符之外,如果一个成员函数同时提供拷贝和移动的版本,它也能从中受益。
区分移动和拷贝的重载函数通常是,一个版本接收一个const T&,另一个版本接收T&&
例如:
class StrVec
{
public:
    void push_back(const std::string&); //拷贝元素
    void push_back(std::string&&);  //移动元素
    //其他成员定义
};
void StrVec::push_back(const string& s)
{
    chk_n_alloc();  //确保又空间容纳新的元素
    //在first_free指向的元素中构造一个s的副本
    alloc.construct(first_free++, s);
}
void StrVec::push_back(string&& s)
{
    chk_n_alloc();  //确保空间够用
    alloc.construct(first_free++, std::move(s));
}
StrVec vec; //空vec
string s = "some string";   //定义了一个string,s是一个左值
vec.push_back(s);   //调用的const string&版本
vec.push_back("other string");  //“other string”为字符串常量,因此调用的string&&版本
  1. 引用限定符的使用
我们通常在一个对象上调用成员函数,而不管该对象是一个左值还是右值
string s1 = "vaule 1", s2 = "value 2";
auto n = (s1 + s2).find('v');
s1 + s2返回的是一个右值,我们直接调用了find成员,有时候右值的使用方式可能令人惊讶
可以 s1 + s2 = "wow!" 编译器不会报错并能正确赋值给一个右值

通过在参数列表后放置一个引用限定符
class Foo
{
public:
    Foo &operator=(const Foo&) &;   //表示左值限定,即只能向可修改的左值赋值
    Foo someMem() const &;  //当引用限定符和const同时使用,const一定在前。
    Foo sorted() &&;    //表示右值限定,即只能向可改变的右值赋值
    Foo sorted() const &;   //可用于任何类型的Foo限定
private:
    vector<int> data;
}
Foo &Foo::operator=(const Foo &rhs) &
{
    //执行rhs赋予本对象所需的工作
    return *this;
}
Foo Foo::sorted() &&
{
    sort(data.begin(), sort.end());
    return *this;
}
Foo Foo::sorted() const &
{
    Foo ret(*this); //拷贝一个副本
    sort(ret.data.begin(), ret.data.end()); //排序副本
    return ret; //返回副本
}

Foo &retFoo();  //返回一个引用,是左值
Foo retVal();   //返回一个值,是右值
Foo i, j;   //i和j是左值
retFoo() = i;   //正确:retFoo()返回一个左值
retVal() = j;   //错误:reVal()返回一个右值
i = retVal();   //正确,我们可以将一个右值赋值操作给一个左值

retVal().sorted();  //reVal()是一个右值,调用Foo::sorted() &&
retFoo().sorted();  //retFoo()是一个左值,调用Foo::sorted() const &
  1. 如果我们定义两个或两个以上具有相同名字,相同参数列表的成员函数,如果有其中一个具有引用限定符,其他所有同名同参的成员都要带上限定符
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值