C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++移动语义

C++移动语义详细介绍使用

作者:Shawn-Summer

首先,移动语义和完美转发这两个概念是在C++的模板编程的基础上,新增的特性,主要是配合模板来使用。本篇会从C++的值类型,到移动拷贝与移动赋值来理解移动语义与完美转发

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.std::move()将左值强制转换为右值引用

看一下下面这段代码

#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()等价于static_cast<T&&>(lvalue),即将左值转换为右值引用。

但是,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成右值引用。

4.拷贝语义和移动语义

如果一个类支持拷贝构造函数和拷贝赋值函数,那么我们就称该类具有拷贝语义;同样的如果一个类支持移动构造函数和移动赋值函数,那么我们就称该类具有移动语义。

当然有些类是同时支持移动语义和拷贝语义的。

在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机制,即所谓返回值优化机制,他能帮你完成类似移动语义的智能优化,但是要记住,编译器优化不是完全奏效的,最好还是自己提高代码效率。

到此这篇关于C++移动语义详细介绍使用的文章就介绍到这了,更多相关C++移动语义内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文