C++ 对象移动(右值引用(&&)、移动构造函数、移动赋值运算符、引用限定函数)

原文:对象移动(右值引用(&&)、移动构造函数、移动赋值运算符、引用限定函数)

一、对象移动概述

  • C++11标准引入了“对象移动”的概念
  • 对象移动的特性是:可以移动而非拷贝对象
  • 在C++旧标准中,没有直接的方法移动对象。因此会有很多不必要的资源拷贝
  • 标准库容器、string、share_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝

对象移动的特点

  • 在很多情况下会发生对象拷贝的现象,对象拷贝之后就被销毁了,在这种情况下,对象移动而非对象拷贝会大幅度提升性能
  • 使用移动而非拷贝的另一个原因是:类似于IO类或unique_ptr这样的类,这些类都不能被共享资源(如指针或IO缓冲)。因此,这些类型的对象不能拷贝但可以移动

二、右值引用(&&)

  • 为了支持移动操作,C++11标准引入了新的引用类型——右值引用
  • 所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用
  • 右值有一个很重要的性质:只能绑定到一个将要销毁的对象

右值引用的使用方法

  • 左值引用:
  1. 不能将其绑定到要求“转换的表达式、字面值常量、返回右值的表达式
  2. 返回左值的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式。我们可以将一个左值引用绑定到这类表达式的结果上
  • 右值引用:
  1. 则与左值引用相反,我们可以将一个右值引用到上面所述的表达式上,但是不能将一个右值引用直接绑定到一个左值上
  2. 返回非引用类型的函数,连同算术、关系、位以及后置递增运算符,都生成右值。我们可以将一个const的左值引用或一个右值引用绑定到这类表达式上
int i = 42;
int &r = i;            //正确,r引用i
int &&rr = i;          //错误,不能将一个右值引用到左值上
int &r2 = i * 42;      //错误,i*42是一个右值
const int &r3 = i * 42;//正确,我们可以将一个const的引用绑定到一个右值上
int &&rr2 = i * 42;    //正确,将rr2绑定到乘法结果上(右值)
int ret(int i) {
    return i * 2;
}
int& ret2(int& i) {
    return i;
}
 
int &r = ret(1);     //错误
int &&rr = ret(1);   //正确
 
int &r2 = ret2(1);   //正确
int &&rr2 = ret2(1); //错误

左值持久、右值短暂

  • 左值一般是绑定到对象身上,因此左值是持久的
  • 而右值要么绑定在字面值常量、要么绑定到表达式求值过程中创建的临时对象身上,因此:
    • 右值引用所引用的对象将要被销毁
    • 该对象没有其他用户
  • 这两个特性意味着,使用右值引用的代码可以自由地接管所引用的对象的资源

变量是左值

  • 变量可以看做只有一个运算对象而没有运算符的表达式。因此不能将一个右值引用绑定到一个右值引用类型的变量上
int &&rr1 = 42;  //正确,42是字面值
int &&rr2 = rr1; //错误,表达式rr1是左值

标准库move()函数

  • 虽然不能将一个右值引用绑定到一个左值上,但是我们可以显式地将一个左值转换成对应的右值引用类型
  • move函数就是实现上面的功能,move函数用来获得绑定到左值上的右值引用
  • 此函数定义在头文件<utility>中
int &&rr1 = 42;             //正确,42是字面值
int &&rr2 = std::move(rr1); //正确了

 

三、移动构造函数和移动赋值运算符

  • 与string一样,我们自己的类也支持移动和拷贝。为了支持移动,我们需要自己定义移动构造函数与移动赋值运算符
  • 下面是一个类的定义,用来作为下面讲解的基础
lass StrVec
{
public:
    StrVec() 
        :elements(nullptr), first_free(nullptr), cap(nullptr) {}
    ~StrVec() 
    {
        if (elements) { //如果数组不为空
            //释放内存
        }
    }
private:
    std::string *elements;  //指向数组首元素的指针
    std::string *first_free;//指向数组第一个空闲元素的指针
    std::string *cap;       //指向数组尾后位置的指针
};

移动构造函数

格式如下:

  • 参数为“&&”类型,因为是移动操作
  • 参数不必设置为const,因为需要改变
  • 在构造函数后添加“noexcept”关键字,确保移动构造函数不会抛出异常

针对上面的StrVec类,其移动构造函数的定义如下:

  • noexcept确保移动构造函数不会抛出异常
  • 在参数初始化列表中将参数s的资源移动给自己(先执行)
  • 然后在函数体内释放参数s的资源,这样之后就达到了资源移动的目的(后执行)
StrVec(StrVec &&s) noexcept
    :elements(s.elements),first_free(s.first_free),cap(s.cap)
{
    s.elements = s.first_free = s.cap = nullptr;
}

几点需要注意:

  • 移动构造函数不分配任何内存,只是简单的资源移动而已
  • 参数s在资源移动之后,其对象还是存在的。当s被销毁时,其会执行析构函数,从上面StrVec的析构函数可以看出我们将elements设置为nullptr之后,析构函数就不会释放资源了(因为资源是被移动了,不应该被释放)

移动赋值运算符

格式如下:

  • 参数为“&&”类型,因为是移动操作
  • 参数不必设置为const,因为需要改变
  • 在函数后添加“noexcept”关键字,确保移动赋值运算符函数不会抛出异常
  • 与拷贝赋值运算符一样,函数返回自身引用
  • 在函数执行前,应该检测自我赋值的情况

针对上面的StrVec类,其移动赋值运算符函数的定义如下:

  • noexcept确保函数不会抛出异常
  • 函数执行之前先判断一下是否为自我赋值
  • 先释放自身资源,再拷贝参数rhs的资源,最后再将rhs置为空
trVec& operator=(StrVec &&rhs)
{
    //检测自我赋值,不能写成*this != s
    if (this != &rhs) {
        if (this->elements) {
            //释放自身的资源
        }
        //开始接管参数的资源
        elements = rhs.elements;
        first_free = rhs.first_free;
        cap = rhs.cap;
        
        //将参数置为空
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
    return *this;
}

为什么需要检测自我赋值:

  • 我们知道,右值引用只能绑定到一个右值身上,不能绑定到一个对象身上,因此照理说移动赋值运算符不会运用于对象身上,所以检测自我赋值照理说可以取消。但是注意,我们上面介绍的move()函数,可以显式地将一个左值转换成对应的右值引用类型,因此参数可能是move()调用返回的结果(std::move(对象自身)),因此我们需要在函数运行前检测自我赋值

四、为什么需要noexcept关键字

  • 由于移动操作时“移动”资源,不分配任何资源,因此移动操作通常不会抛出异常。当我们编写一个不编写异常的移动操作时,应该将此事通知标准库

我们将看到:

  • 我们使用noexcept关键字通知标准库我们的移动操作函数不会抛出异常
  • 否则标准库就会认为我们的移动操作函数可能会抛出异常,并且为了处理这种可能性而做一些额外的工作

为什么设计noexcept

  • 为什么需要noexcept能够帮助我们深入理解标准库是如何与我们自定义的类型进行交互的

我们指定移动操作不会抛出异常,与两个事实有关:

  • 首先,虽然移动操作通常不抛出异常,但是抛出异常还是允许的

  • 其次,标准库容器能对异常发生时其自身的行为提供保障。例如,vector保证如果我们调用push_back发生异常,vector自身不会发生变化

我们以vector为例:

  • 对一个vector调用push_back可能要求为vector重新分配内存。当重新分配内存时,vector的元素将从旧内存移动到新内存中
  • 移动一个对象通常会改变它的值。如果重新分配过程中使用了移动构造函数,且在移动了部分而不是全部元素的时候抛出了一个异常,就会产生一个问题:旧内存中的元素已经被改变了,但新内存中未构造的元素可能尚不存在。在此种情况下,vector将不能满足自身保持不变的要求
  • 另一方面,如果vector使用了拷贝构造函数且发生了异常,他可以很容易地满足要求。在此情况下,当在新内存中构造函数时,旧元素保持不变。如果此时发生了异常,vector可以释放新分配的内存并返回。vector原有的匀速仍然存在
  • 为了避免这些潜在的问题,除非vector知道元素类型的移动构造函数不会抛出异常,否则构造在重新分配内存的过程中,它就必须使用拷贝构造函数而不是移动构造函数。如果希望在vector重新分配内存的情况下对我们自定义类型的对象进行移动而不是拷贝,就必须显式地告诉标准库我们的移动构造函数可以安全使用(通过noexcept)

五、移动后,对象仍是有效、可析构的

  • 从移动操作可以看出,一个对象(在此称为“源对象”)在被移动之后,源对象仍然保持有效,因此这个对象在操作完成之后仍然可以被销毁

六、合成的移动操作

  • 合成”意为“默认的”(编译器做的事)
  • 对于移动操作,编译器的规则如下:
  1. 如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器不会为自己合成移动构造函数和移动赋值运算符
  2. 只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为自己合成移动构造函数或移动赋值运算符(附加:编译器可以移动内置类型成员。如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员)
     
//编译器会为X和hasX合成移动操作
struct X {
    int i;        //内置类型可以移动
    std::string s;//string定义了自己的移动操作
};
 
struct hasX {
    X mem; //X有合成的移动操作
};
 
int main()
{
    X x;
    X x2 = std::move(x);      //使用合成的移动构造函数
    hasX hx;
    hasX hx2 = std::move(hx); //使用合成的移动构造函数
 
    return 0;
}

七、删除的移动操作

对于删除的移动操作有如下规则:

  • 与拷贝操作不同,移动操作永远不会隐式定义为删除的(=delete)函数。
  • 如果我们显示地要求编译器生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数

何时将合成的移动操作定义为删除的函数遵循与定义删除合成的拷贝操作类似的原则:

  • ①与拷贝构造函数不同,移动构造函数被定义为删除的函数的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数(移动赋值运算符的情况类似)
  • ②如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的
  • ③类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的
  • ④类似拷贝赋值运算符,如果有类成员是const的或是引用,则类的移动赋值运算符被定义为删除的

移动操作和合成的拷贝控制成员之间还有最后一个关系:

  • 一个类是否定义自己的移动操作对拷贝构造函数如何合成有影响
  • 如果类定义了一个移动构造函数和/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符是被定义为删除的
  • 总结:定义了一个移动构造函数或移动赋值运算符的类必须定义自己的拷贝操作。否则,这些成员默认地被定义为删除的
//假设Y是一个类,且Y定义了自己的拷贝构造函数但未定义自己的移动构造函数
struct hasY {
    hasY() = default;
    hasY(hasY &&) = default;
    Y mem;  //Y是一个类,且Y定义了自己的拷贝构造函数但未定义自己的移动构造函数
};
 
int main()
{
    hasY hy;
    hasY hy2 = std::move(hy); //错误,移动构造函数是删除的
    return 0;
}
//StrVec只定义了移动构造函数与移动赋值运算符,但是没有定义拷贝构造函数与拷贝赋值运算符
class StrVec
{
//...
public:
    StrVec(StrVec &&s)noexcept{}
    StrVec& operator=(StrVec &&rhs){}
//...
};
 
int main()
{
    StrVec v1, v2;
    v1 = v2;  //错误,SreVec的拷贝赋值运算符被定义为删除的
    return 0;
}

八、移动右值、拷贝左值

  • 如果类既有“”移动构造函数,也有“拷贝构造函数”,编译器使用普遍的函数匹配机制来缺点使用哪个构造函数
//假设SreVec的拷贝构造函数/拷贝赋值运算符,移动构造函数/移动拷贝赋值运算符都定义了
class StrVec{};
 
 
StrVec getVec(istream &)
{
    //该函数返回一个SreVec对象(右值)	
}
 
int main()
{
    StrVec v1, v2;
    v1 = v2;          //v2是个左值,此处调用拷贝赋值运算符
    v2 = getVec(cin); //getVec函数返回一个右值,此处调用移动赋值运算符
    return 0;
}

如果没有定义移动构造函数,右值也被拷贝

  • 如果一个类有一个拷贝构造函数但未定义移动构造函数,那么:
    • 因为类有了拷贝构造函数,编译器不会合成移动构造函数
    • 所以,对于右值的移动操作时调用拷贝构造函数的
  • 上面的规则也适用于拷贝赋值运算符
class Foo {
public:
    Foo() = default; 
    Foo(const Foo&); //拷贝构造函数
    //未定义移动构造函数
};
 
int main()
{
    Foo x;
    Foo y(x);           //调用拷贝构造函数
    Foo z(std::move(x));//因为Foo没有定义移动构造函数,所以此处调用的是拷贝构造函数
    return 0;
}
  • 使用拷贝构造函数代替移动构造函数几乎肯定是安全的(赋值运算符情况类似)。一般情况下,拷贝构造函数满足对应的移动构造函数的要求:它会拷贝给定对象,并将源对象置于有效状态。实际上,拷贝构造函数甚至都不会改变源对象的值

九、拷贝并交换赋值运算符和移动操作

class HasPtr {
public:
    HasPtr(const std::string &s = std::string())
        :ps(new std::string(s)), i(0) {}
    
    HasPtr(const HasPtr& p)  //拷贝构造函数
        :ps(new std::string(*(p.ps))), i(p.i) {}
 
 
    ~HasPtr() { delete ps; }
 
    friend void swap(HasPtr&, HasPtr&);
private:
    std::string *ps;
    int i;
};
 
inline void swap(HasPtr &lhs, HasPtr &rhs)
{
    using std::swap;
    swap(lhs.ps, lhs.ps);//交换指针,而不是string数据
    swap(lhs.i, lhs.i);  //交换int成员

}
  • 现在我们为HasPtr类添加了一个移动构造函数和一个赋值运算符(这个赋值运算符比较特殊)
lass HasPtr {
public:
    //其他内容同上
 
    //移动构造函数
    HasPtr(HasPtr &&p)noexcept :ps(p.ps), i(p.i) { p.ps = 0; }
 
    //这个赋值运算符即是移动赋值运算符,也是拷贝赋值运算符
    HasPtr& operator=(HasPtr rhs)
    {
        swap(*this,rhs);
        return *this;
    }
};

移动构造函数

  • 移动构造函数接管了给定实参的值、函数体内将p的指针置为0,从而确保销毁源对象是安全的
  • 此函数不会抛出异常,因此将其标记为noexcept

赋值运算符

  • 此处定义的赋值运算符的参数不是引用形式,意味着此参数要进行拷贝初始化
  • 依赖实参的类型,拷贝初始化:
    • 要么使用拷贝构造函数——左值被拷贝
    • 要么使用移动构造函数——右值被移动
  • 因此,此处定义的赋值运算符就实现了拷贝赋值运算符和移动赋值运运算符的两种功能

例如:

  • 第一个赋值中,右侧对象hp2是一个左值,因此使用拷贝构造函数来初始化
  • 第二个赋值中,我们调用std::move()将将一个右值绑定到hp2上。此种情况下,拷贝构造函数和移动构造函数都是可以的。但是由于实参是一个右值引用,移动构造函数时精确匹配的
HasPtr hp;
HasPtr hp2;
 
//hp2是一个左值。所以先调用拷贝构造函数复制一份HasPtr对象给operator=参数
//再调用operator=函数将hp2赋值给hp
hp = hp2;
 
//此处hp2显式成为一个右值。所以先调用移动构造函数构造一份HasPtr对象给operator=参数
//再调用operator=函数将hp2赋值给hp
hp = std::move(hp2);
  • 不管使用的是拷贝构造函数还是移动构造函数,赋值运算符的函数体内都swap两个对象的状态。交换HasPtr回交换两个对象的指针(及int)成员。在swap之后,rhs中的指针将指向原来左侧对象所拥有的string(及int)。当rhs离开作用域后,这个对象将会销毁

十、右值引用和成员函数

除了构造函数和赋值运算符之外,成员函数也可能提供两个版本:一个提供拷贝,另一份通过移动

  • 一份提供拷贝:参数为const&
  • 一份提供移动:参数为非const&&

使用规则:

  • 对于拷贝版本:我们可以将任何类型的对象传递给该版本
  • 对于移动版本:只能传递给其非const的右值
  • 一般来说,我们不需要为函数定义接受一个const T&&或是一个(普通的)T&参数的版本。当我们希望从实参“窃取”数据时,通常传递一个右值引用。为了达到这个目的,实参不能使const的。类似的,从一个对象进行拷贝的操作不应该改变该对象,因此,通常不需要定义一个接受(普通的)T&参数的拷贝版本

演示案例

  • 对于push_back的标准库容器提供两个版本:
    • 一个版本有一个右值引用
    • 另一个版本有一个const左值引用
  • 例如:
void push_(const X&); //拷贝版本
void push_(X&&);      //移动版本

演示案例

  • 作为更好的例子,我们将StrVec类进行修改,在其中添加了两个push_back()函数
class StrVec
{
public:
    //其他同上
    void push_back(const std::string&);//拷贝元素
    void push_back(std::string&&);     //移动元素
private:
    static std::allocator<std::string> alloc; //分配元素
    //其他同上
};
 
void StrVec::push_back(const std::string& s)
{
    chk_n_alloc(); //自定义函数,用来检测是否空间足够
 
    //在first_free指向的元素中构造s的一个副本,此处construct会调用string的构造函数来构造新元素
    alloc.construct(first_free++, s);
}
 
void StrVec::push_back(std::string&&)
{
    chk_n_alloc();
 
    //此处由于参数为std::move()类型,因此construct会调用string的移动构造函数来构造新元素
    alloc.construct(first_free++,std::move(s));
}
  • 当我们调用push_back()时,实参类型决定了新元素是拷贝还是移动到容器中:
StrVec vec;
string s = "some string or another";
 
vec.push_back(s);      //s为左值,因为调用push_back(const string&)
vec.push_back("done"); //调用push_back(string&&)

十一、右值和左值引用成员函数(引用限定函数)

  • 通常,我们在一个对象上调用成员函数,而不管该对象是一个左值还是一个右值:例如:
string s1 = "a value", s2 = "another";
 
//s1+s2是一个右值
auto n = (s1 + s2).find('a');
  • 有时候,右值的使用还可能是下面的奇怪形式 
string s1 = "a value", s2 = "another";
s1 + s2 = "wow"; //s1+s2是一个右值,我们此处对一个右值进行了赋值(无意义)
  • 在旧标准中,我们没有办法阻止这种使用方式。为了维持向后兼容性,新标准库类仍然允许向右值赋值。但是,我们可以在自己的类中阻止这种办法。在此情况下,我们希望强制左侧运算对象是一个左值

使用方法:

  • 在参数列表后放置一个引用限定符
  • 引用限定符可以是&或&&,分别该函数可以运用于一个左值对象(&)还是一个右值对象(&&)
  • 与const关键字一样,引用限定符只能作用于(非static)成员函数,且在声明和定义时都需要
  • 引用限定符可以和const一起使用,且const必须在限定符的前面。例如:
class Foo {
public:
    Foo someMem()&const;   //错误,const必须在&&前面
    Foo anotherMem()const&;//正确
};

演示案例

class Foo {
public:
    //此参数后面有一个&,因此这个函数只能被一个左值对象调用
    Foo &operator=(const Foo&)&;
};
 
Foo &Foo::operator=(const Foo& rhs)&
{
    //执行将rhs赋予本对象的操作(代码省略)
 
    return *this;
}
  • 在上面我们在operatror=的后面添加了一个&,因此operatror=只能运用于一个左值,见下面的代码
Foo& retFoo()
{
	//一个函数,返回Foo类,返回左值(引用)
}
 
Foo retVal()
{
	//一个函数,返回Foo类,返回右值
}
 
int main()
{
    Foo i, j;
    i = j;
 
    retFoo() = j; //正确,retFoo()返回一个左值
    retVal() = j; //错误,retVal()返回一个右值
 
    i = retFoo(); //正确,我们可以将一个左值作为赋值操作的右侧运算对象
    i = retVal(); //正确,我们可以将一个右值作为赋值操作的右侧运算对象
    return 0;
}

演示案例②

class Foo {
public:
    void push_back() && {} //这个函数只能被一个右值Foo对象调用
};
 
Foo& retFoo()
{
    //一个函数,返回Foo类,返回左值(引用)
}
 
Foo retVal()
{
    //一个函数,返回Foo类,返回右值
}
 
int main()
{
    Foo i;
 
    i.push_back();                  //错误,i是一个左值
    retFoo().push_back();           //错误
    retVal().push_back();           //错误(retVal应该返回右值的啊,但是编译器报错)
    std::move(retVal()).push_back();//正确
	
    return 0;
}

演示案例

class Foo {
public:
    //此函数只可以用于右值
    Foo sorted() && {
        std::sort(data.begin(), data.end());
        return *this;
    }
    //此函数可以用于左值或const类型的右值(因为其带有const,见下面的重载介绍)
    Foo sorted()const & {
        Foo ret(*this); //拷贝一个副本
        std::sort(ret.data.begin(), ret.data.end()); //排序副本
        return ret; //返回结果
    }
private:
    std::vector<int> data;
};
 
Foo& retFoo()
{
    //一个函数,返回Foo类,返回左值(引用)
}
 
Foo retVal()
{
    //一个函数,返回Foo类,返回右值
}
 
const Foo retVal2()
{
	//一个函数,返回Foo类,返回右值,且为const
}
 
int main()
{
    retFoo().sorted(); //调用sorted()const&
    retVal().sorted(); //调用sorted() &&
    retVal2().sorted();//调用sorted()const&
    return 0;
}
  • 对于sorted() &&:如果对象是一个右值,意味着没有其他用户,因此我们可以在函数内改变对象的内容
  • 对于sorted()const&:如果对一个const右值或一个左值执行sorted时,我们不能改变对象,因此就需要在里面使用拷贝的临时对象进行排序,然后将结果返回

十二、重载和引用函数

  • const成员函数重载时可以定义两个版本:一个有const、一个没有const
class Foo {
public:
    //下面两者形成重载
    Foo sorted();
    Foo sorted()const;
};
  • 引用限定函数规则不一样:重载时必须两者都加上引用限定符
class Foo {
public:
    Foo sorted()&&;
    Foo sorted()const&&; //正确,与上面形成重载
    //Foo sorted()const; 这个是错误的
};
  • 附加:如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符
  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++中的字符串赋值函数移动赋值函数移动构造函数分别是: 1. 字符串赋值函数(Assignment Operator):在C++中,可以使用赋值操作符(=)来将一个字符串赋值给另一个字符串。例如: ```cpp std::string str1 = "Hello"; std::string str2; str2 = str1; // 使用赋值操作符将str1的值赋给str2 ``` 这里,使用赋值操作符将str1的值赋给str2。 2. 移动赋值函数(Move Assignment Operator):移动赋值函数用于在性能上优化对象赋值过程,通过移动资源而不是进行深拷贝。它使用`&&`来接收右值引用参数。例如: ```cpp class MyString { public: // 移动赋值函数 MyString& operator=(MyString&& other) noexcept { if (this != &other) { // 释放当前对象的资源 delete[] m_data; // 移动other的资源到当前对象 m_data = other.m_data; m_size = other.m_size; // 清空other对象 other.m_data = nullptr; other.m_size = 0; } return *this; } private: char* m_data; size_t m_size; }; ``` 这里,移动赋值函数通过移动`other`对象的资源到当前对象,并清空`other`对象。 3. 移动构造函数(Move Constructor):移动构造函数用于在性能上优化对象的构造过程,通过移动资源而不是进行深拷贝。它使用`&&`来接收右值引用参数。例如: ```cpp class MyString { public: // 移动构造函数 MyString(MyString&& other) noexcept { m_data = other.m_data; m_size = other.m_size; other.m_data = nullptr; other.m_size = 0; } private: char* m_data; size_t m_size; }; ``` 这里,移动构造函数通过移动`other`对象的资源到当前对象,并清空`other`对象。 需要注意的是,移动赋值函数移动构造函数一般需要保证不抛出异常,因此通常会使用`noexcept`关键字声明它们。另外,移动赋值函数移动构造函数通常与移动语义相关的类一起使用,如智能指针、容器等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值