C++11中的移动语义

C++11中的移动语义

1.移动构造函数

移动语义就是使用移动构造函数来构造对象

我们知道在类中如果存在指针数据成员,那么我们就一定要写拷贝构造函数,进行深拷贝

如下所示,就是拷贝构造函数的用法:

#include<iostream>
using namespace std;
class A
{
    public:
        int * ptr;
        A():ptr(new int(0))
        {
            cout<<"constructor\n";
        };
        A(const A & h):ptr(new int(*h.ptr))//拷贝构造函数
        {
            cout<<"copy constructor\n";
            //deep copy
        };
        ~A()
        {
            cout<<"destructor\n";
            delete ptr;
        };
};
A getA()
{
    return A();
}
int main()
{
    A a=getA();
}
//g++ .\test.cpp -std=c++11 -fno-elide-constructors
constructor
copy constructor
destructor
copy constructor
destructor
destructor

可以知道上面代码中,实际上产生了3个对象,在getA()函数中,使用默认构造函数产生一个对象,然后将其作为返回值时,又会通过拷贝构造函数产生一个对象,然后在main()函数中,又会通过拷贝构造函数构造出对象a,所以总共有3个对象产生,我们这里的拷贝构造函数是进行的深拷贝,所以就会开辟3块内存.

在C++11中,我们可以使用移动构造函数,对上述代码进行优化

#include<iostream>
using namespace std;
class A
{
    public:
        int * ptr;
        A():ptr(new int(0))
        {
            cout<<"constructor\n";
        };
        A(const A & h):ptr(new int(*h.ptr))//拷贝构造函数
        {
            cout<<"copy constructor\n";
            //deep copy
        };
        A(A && h):ptr(h.ptr)//移动构造函数
        {
            h.ptr=nullptr;
            cout<<"move constructor\n";
        }
        ~A()
        {
            cout<<"destructor\n";
            delete ptr;
        };
};
A getA()
{
    return A();
}
int main()
{
    A a=getA();
}
//g++ .\test.cpp -std=c++11 -fno-elide-constructors
constructor
move constructor
destructor
move constructor
destructor
destructor

移动构造函数,它是进行的浅拷贝,由于被移动的值会立即进行析构,所以我们不关心它,只需进行浅拷贝,将其开辟的内存空间转让给别人。上述代码中,也会构造出3个对象,但是它们只开辟一块内存空间,这就是移动构造函数的优势。

总之,我们发现移动构造函数和拷贝构造函数的区别,其实就是深拷贝和浅拷贝的区别,移动构造函数的开销更小,当然我们关心的是,移动构造函数何时会被触发?在上面代码中就是一个例子,将getA()中的局部匿名对象移动给返回值,然后将返回值移动给 main()中的a

这里我们给出结论:移动构造函数只有在使用右值来构造对象时才会调用

那么什么是右值?

getA()中的A()就是右值,getA()的返回值也是右值,所以用它们构造对象时,会调用移动构造函数

2.右值引用

在C++11中,我们将值划分为:左值、右值(分为将亡值和纯右值)

左值:可以取地址,有名字的值
右值:不能取地址,没有名字的值
纯右值:运算表达式,如1+2,或者和对象无关的字面值,如true,或者非引用的函数返回值,或者lambda表达式
将亡值:仅和右值引用相关的值,它包括:右值引用的函数返回值T&&,或者std::move的返回值,或者被转换为T&&类型的函数返回值

注意:不管是纯右值还是将亡值,它们的存活时间都很短。不要被将亡值的名称所迷惑了,其实所有右值的都会即将消亡。
实际上,对于纯右值和将亡值的定义很难给出,而且我们也不需要区分它们两,但是,我们至少可以确定一个值是左值还是右值。

C++98中所提及的引用,在C++11中我们称之为左值引用,即这个引用只能绑定左值,在C++11中我们提供了一种新的能够绑定右值的引用,即右值引用
我们知道左值引用实际是一个变量的别名,右值引用它实际是一个匿名变量的别名

#include<iostream>
using namespace std;
class A
{
    public:
        int * ptr;
        A():ptr(new int(0))
        {
            cout<<"constructor\n";
        };
        A(const A & h):ptr(new int(*h.ptr))//拷贝构造函数
        {
            cout<<"copy constructor\n";
            //deep copy
        };
        A(A && h):ptr(h.ptr)//移动构造函数
        {
            h.ptr=nullptr;
            cout<<"move constructor\n";
        }
        ~A()
        {
            cout<<"destructor\n";
            delete ptr;
        };
};
A getA()
{
    return A();
}
int main()
{
    A&& a=getA();//右值引用
}
constructor
move constructor
destructor
destructor

在上述代码中getA()的返回值是一个右值,它是一个临时值,如果我们写成A a=getA();,那么这个临时值给a进行移动构造后就会立即被析构,而如果我们使用A&& a=getA();,那就意味著我们给这个临时值进行续命,a就是这个临时值的别名,所以上述代码就会少一个对象的构造。

总之,右值引用就是一种绑定右值的引用,实际上在C++98中,我们所知的const T &,这样的引用,也可以绑定右值,他也叫做万能引用,当他绑定右值的时候它的作用和右值引用是一样的,只不过这里的const是底层的,所以我们不能用其修改右值,所以右值引用绑定右值时,可以修改该右值,而当万能引用绑定右值时,我们不可以修改该右值

T& a;//左值引用,只能绑定非常量左值
T&& a;//右值引用,只能绑定非常量右值
const T& a;//万能引用,它可以绑定一切值,但是它不能修改该值
const T&& a;//和万能引用功能一样(一般不使用)

我们仔细来思索一下右值引用的用处,从本质上讲,它是给右值进行续命,而从实践上讲,它就是用来移动语义的,但是移动语义的时候,我们希望修改原来的右值(看上面代码中的移动构造函数,它实际上修改了右值),所以我们说const T&&这种是无用的,

我们在学习了C++11中的移动语义和右值引用知识后,我们要深知一个编程规矩:
只要类中有指针数据成员,就一定要重写拷贝构造函数和移动构造函数

3.右值引用本身是左值

我们知道在C++98中,左值引用本身是个左值,同样的,在C++11中,我们规定右值引用本身是左值

int num=1;
int &a =num;
int &b=a;
int &&c =2;
int && d=c;//报错

上面代码中只有最后一行会报错,因为虽然c是右值2的引用,但是c本身是一个左值,也可以这样理解,右值引用采用借尸还魂的方式,赐予右值一个名字,从而给右值续命

void foo(int &&);
int main()
{
    int &&a=1;
    foo(a);//报错
}

同样的,上面代码也会报错,因为非常量右值引用只能绑定非常量右值,而a是一个左值

当右值引用作为函数返回值时,它却是右值

int && foo()
{
    return 1;
}
int main()
{
    int && b=foo();
    
}

上面代码是正确的

这实际就是将亡值的定义::右值引用的函数返回值T&&,或者std::move的返回值,或者被转换为T&&类型的函数返回值

4.std::move()将左值强制转换为将亡值

实际上,std::move()等价于static_cast<T&&>()

看一下下面这段代码

#include<iostream>
using namespace std;
class A
{
    public:
        int * ptr;
        A():ptr(new int(0))
        {
            cout<<"constructor\n";
        };
        A(const A & h):ptr(new int(*h.ptr))//拷贝构造函数
        {
            cout<<"copy constructor\n";
            //deep copy
        };
        A(A && h):ptr(h.ptr)//移动构造函数
        {
            h.ptr=nullptr;
            cout<<"move constructor\n";
        }
        ~A()
        {
            cout<<"destructor\n";
            delete ptr;
        };
};
A&& getA()
{
    return std::move(A());
}
int main()
{
    A&& a=getA();
}
constructor
destructor

getA()中的A()是右值,为什么还要用std::move将其转换为右值?因为A()是一个纯右值,右值引用当然可以绑定纯右值,但是A()是一个局部对象,在函数中返回引用时,我们禁止返回局部对象的引用,但是当我们使用std::move后,A()就会转换为将亡值,这样子就可以将其作为引用返回。这是一种返回局部对象引用的特殊方法。

注意,这是一个涉及原则的问题,匿名对象是纯右值

class  A
{};
int main()
{
    A& a=A();//报错,左值引用无法绑定纯右值
}

但是,std::move()有一个bug,即被转化为右值的左值,不会被立即析构。

#include<iostream>
using namespace std;
class A
{
    public:
        int* ptr;
        A():ptr(new int(999)){}
        ~A(){delete ptr;}
        A(const A& h):ptr(new int(*h.ptr)){}
        A(A&& h):ptr(h.ptr)
        {
            h.ptr=nullptr;
        }
};
int main()
{
    A a;
    A b(std::move(a));
    cout<<*a.ptr<<endl;//报错
}

上述代码就会报错,因为a被转化为右值引用后,b会调用移动构造函数来构造它自己,而在移动构造函数中,它将a.ptr置空

#include<utility>
class A
{
    public:
        int *ptr;
        A():ptr(new int(0)){}
        ~A(){delete ptr;}
        A(const A& h):ptr(new int(*h.ptr)){}
        A(A&& h):ptr(h.ptr){h.ptr=nullptr;}
};
class B
{
    public:
        int *ptr;
        A elem;
        B():ptr(new int(0)){}
        ~B(){delete ptr;}
        B(const B&h):ptr(new int(*h.ptr)),elem(h.elem){}
        B(B&& h):ptr(h.ptr),elem(std::move(h.elem)){h.ptr=nullptr;}
};

注意看,B(const B&h):ptr(new int(*h.ptr)),elem(h.elem){}中对elem的初始化使用的是A的拷贝构造函数,
B(B&& h):ptr(h.ptr),elem(std::move(h.elem)){h.ptr=nullptr;}中对elem的初始化使用的是是A的移动构造函数.
注意一点,即使这里我们忘记写std::move()也并无大碍,它会自行调用拷贝构造函数,当然这也会导致一些开销,所以在做类开发的时候,在写类的移动构造函数的时候,总是要记得将类成员move成右值。

5.拷贝语义和移动语义

如果一个类支持拷贝构造函数和拷贝赋值函数,那么我们就称该类具有拷贝语义;同样的如果一个类支持移动构造函数和移动赋值函数,那么我们就称该类具有移动语义。
当然有些类是同时支持移动语义和拷贝语义的。
在C++98中的类基本都是只具有拷贝语义的,而在C++11中的基本所有类都支持移动语义,特别的,有些类只支持移动语义,而不支持拷贝语义,这种类,我们称之为资源型类,即资源只能被移动而不能被拷贝,例如智能指针类unique_ptr,文件流ifstream等都是资源型类,在C++11中,我们可以通过一些工具来判断一个类是否支持移动语义。

我们看一下下面的代码

template <class T>
void swap(T& a, T& b)
{
    T tmp(move(a));
    a=move(b);
    b=move(tmp);
}

上述代码中,如果T支持移动语义,那么它就会调用移动构造函数和移动赋值函数,而如果T只支持拷贝语义,那么它也可以调用拷贝构造函数和拷贝赋值函数

我们关于移动语义的另一个话题是:异常。因为如果移动语义没有完成,却抛出异常,那么可能会导致产生悬挂指针。所以在C++11中我们同样有std::move_if_noexcept()函数来检测,移动构造函数是否用noexcept修饰。
再讨论一个关于编译器优化的问题,如今c++编译器已经非常优化了,RVO机制,即所谓返回值优化机制,他能帮你完成类似移动语义的智能优化,但是要记住,编译器优化不是完全奏效的,最好还是自己提高代码效率。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值