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

目录
  • 1.移动构造函数
  • 2.右值引用
  • 3.std::move()将左值强制转换为右值引用
  • 4.拷贝语义和移动语义

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++移动语义内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++模板编程特性之移动语义

    目录 C++的值类型 右值引用与移动构造和移动赋值 C++的值类型 我们知道,每个变量都有类型,或整形或字符型等来进行了分类,不仅如此,C++表达式(带有操作数的操作符.字面量.变量名等)在类型的属性上,还有一种属性,即值类别(value category).且每个表达式只属于三种基本值尖别中的一种:左值(lvalue),右值(rvalue),将亡值(xvalue),每个值类别都与某种引用类型对应. 其中,左值和将亡值成为泛左值(generalized value,gvalue),纯右值和将亡值

  • C++ move semantic移动语义介绍

    目录 前言 移动构造 为什么我们需要move semantic 前言 在说移动语义之前 本文作者假设你已经具备了深拷贝浅拷贝左值右值等基本概念 本文不会再过多叙述 那么接下来 让我们开始吧 Tips:(警告 警告 警告 警告)在阅读本文章之前 作者首先提醒 线代编译器有RVO和NRVO等一系列优化策略 除非你明确知道你要使用std::move 不然我并不是很推荐你使用移动语义 他很有可能是无意义的 移动构造 在说移动语义之前 让我们先来说说移动构造这玩意 我们都知道 深拷贝是会把在堆区的内存一起

  • 详解C++11中的右值引用与移动语义

    C++11的一个最主要的特性就是可以移动而非拷贝对象的能力.很多情况都会发生对象的拷贝,有时对象拷贝后就立即销毁,在这些情况下,移动而非拷贝对象会大幅度提升性能. 右值与右值引用 为了支持移动操作,新标准引入了一种新的引用类型--右值引用,就是必须绑定到右值的引用.我们通过&&而不是&来获得右值引用.右值引用一个重要的特性就是只能绑定到将要销毁的对象. 左值和右值是表达式的属性,一些表达式生成或要求左值,而另一些则生成或要求右值.一般而言,一个左值表达式表示的是一个对象的身份,而右

  • C++学习之移动语义与智能指针详解

    移动语义 1.几个基本概念的理解 (1)可以取地址的是左值,不能取地址的就是右值,右值可能存在寄存器,也可能存在于栈上(短暂存在栈)上 (2)右值包括:临时对象.匿名对象.字面值常量 (3)const 左值引用可以绑定到左值与右值上面,称为万能引用.正因如此,也就无法区分传进来的参数是左值还是右值. const int &ref = a;//const左值引用可以绑定到左值 const int &ref1 = 10;//const左值引用可以绑定到右值 (4)右值引用:只能绑定到右值不能绑

  • C++11中value category(值类别)及move semantics(移动语义)的介绍

    前言 C++11之前value categories只有两类,lvalue和rvalue,在C++11之后出现了新的value categories,即prvalue, glvalue, xvalue.不理解value categories可能会让我们遇到一些坑时不知怎么去修改,所以理解value categories对于写C++的人来说是比较重要的.而理解value categories离不开一个概念--move semantics.了解C++11的人我相信都了解了std::move,右值引用

  • 深入了解c++11 移动语义与右值引用

    1.移动语义 C++11新标准中一个最主要的特性就是提供了移动而非拷贝对象的能力.如此做的好处就是,在某些情况下,对象拷贝后就立即被销毁了,此时如果移动而非拷贝对象会大幅提升性能.参考如下程序: //moveobj.cpp #include <iostream> #include <vector> using namespace std; class Obj { public: Obj(){cout <<"create obj" << e

  • C++左值与右值,右值引用,移动语义与完美转发详解

    目录 C++——左值与右值.右值引用.移动语义与完美转发 一.左值和右值的定义 二.如何判断一个表达式是左值还是右值(大多数场景) 三.C++右值引用 四.std::move()与移动语义 五. 完美转发 总结 C++——左值与右值.右值引用.移动语义与完美转发 在C++或者C语言中,一个表达式(可以是字面量.变量.对象.函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式. 一.左值和右值的定义 1.左值的英文为locator value,简写为lvalue,可意为存储在内存中.有明

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

    目录 1.移动构造函数 2.右值引用 3.std::move()将左值强制转换为右值引用 4.拷贝语义和移动语义 1.移动构造函数 移动语义就是使用移动构造函数来构造对象. 我们知道在类中如果存在指针数据成员,那么我们就一定要写拷贝构造函数,进行深拷贝 如下所示,就是拷贝构造函数的用法: #include<iostream> using namespace std; class A { public: int * ptr; A():ptr(new int(0)) { cout<<&

  • 详细介绍Python的鸭子类型

    鸭子类型基本定义 首先Python不支持多态,也不用支持多态,python是一种多态语言,崇尚鸭子类型. 以下是维基百科中对鸭子类型得论述: 在程序设计中,鸭子类型(英语:duck typing)是动态类型的一种风格.在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由当前方法和属性的集合决定.这个概念的名字来源于由James Whitcomb Riley提出的鸭子测试,"鸭子测试"可以这样表述: "当看到一只鸟走起来像鸭子.游泳起来像鸭子.叫起来也

  • Java类加载基本过程详细介绍

    Java类加载基本过程详细介绍 基本过程: 根据类的全限定名称加载定义类的二进制字节流. 将字节流代表的静态存储结构转化为方法区的运行时数据结构 内存中生成一个代表这个类的java.lang.Class对象,作为方法去这个类的各种数据访问入口 数组类本身不通过类加载器创建,由java虚拟机直接创建,数组类的元素类型由类加载器加载. 数组类的元素类型:数组去掉所有维度后的类型, 文件格式验证: 0xCAFEBABY 魔数开头: 主次版本号当前虚拟机可处理: 常量类型: 索引执行类型: utf8编码

  • Java 高并发五:JDK并发包1详细介绍

    在[高并发Java 二] 多线程基础中,我们已经初步提到了基本的线程同步操作.这次要提到的是在并发包中的同步控制工具. 1. 各种同步控制工具的使用 1.1 ReentrantLock ReentrantLock感觉上是synchronized的增强版,synchronized的特点是使用简单,一切交给JVM去处理,但是功能上是比较薄弱的.在JDK1.5之前,ReentrantLock的性能要好于synchronized,由于对JVM进行了优化,现在的JDK版本中,两者性能是不相上下的.如果是简

  • 详细介绍Java函数式接口

    目录 Java-函数式接口 1.自定义函数式接口 1.1概述 1.2格式 1.3@FunctionalInterface注解 1.4自定义函数式接口 2.函数式编程 2.1Lambda的延迟执行 2.2使用Lambda作为参数和返回值 3.常用函数式接口 3.1Supplier接口 3.2Consumer接口 3.3Predicate接口 3.4Function接口 Java-函数式接口 1.自定义函数式接口 1.1概述 函数式接口在Java中是指:**有且仅有一个抽象方法的接口.**当然接口中

  • C# 本地函数与 Lambda 表达式详细介绍

    目录 1.C# 本地函数与 Lambda 表达式 2.Lambda 表达式 3.本地函数 4.那么,局部函数的目的是什么? 1.C# 本地函数与 Lambda 表达式 C# 局部函数通常被视为 lambda 表达式的进一步增强.虽然功能是相关的,但也存在重大差异. Local Functions 是嵌套函数]功能的 C# 实现.一种语言在支持 lambdas 之后获得对嵌套函数的支持几个版本是有点不寻常的.通常情况相反. Lambda 或一般的一流函数需要实现未在堆栈上分配且生命周期与需要它们的

  • Python 编程语言详细介绍

    目录 一.语法 1.缩进 2.变量 3.表达式 4.类型 5.方法 二. 库 1.Python的应用 3.值得学习Python吗? 前言: Python是世界上最流行的解释型编程语言之一.Python 由 Guido van Rossum 设计,作为"ABC"编程语言的继承者,于 1991 年首次发布.它是一种高级通用语言,其设计理念是通过使用缩进来强调代码的可读性.Python 的语言结构旨在帮助程序员为小型和大型项目编写逻辑代码. 该语言是动态类型的,支持多种编程范式.它完全支持面

  • Redis分布式锁详细介绍

    目录 分布式锁 redis实现分布式锁的原理 死锁问题 超时问题 锁误放问题 可重入性 Redlock 分布式锁 在单进程应用中,当一段代码同一时间内只能由一个线程执行时, 多线程下可能会出错,例如两个线程同时对一个数字做累加,两个线程同时拿到了该数字,例如40,一个线程加了10,一个线程加了20,正确结果应该是70, 但由于两个线程在自己的内存中一个算出的是50,一个算出的是60,此时二者都将自己的结果往该数字原本的地方写(保存), 这时候,肯定会有一个线程的值会被覆盖,因为读取->计算->

  • C++四种case的详细介绍小结

    目录 一.static_case 1.基本数据类型转换 2.指针和void指针的转换 3.父类和子类之间的转换 二.dynamic_case 三.const_case 1.加上const 2.去掉const 四.reinterpret_case 在C++中,我们经常使用到类型的转换,像把一个int类型转换成char类型,一个int类型转换成double类型,这些转换属于隐式类型转换.而今天我们要来讲的是显式类型转换.C++提供了四种显式类型转换,分别是:static_cast.dynamic_c

  • C++四种cast使用详细介绍

    目录 一.static_cast 1.基本数据类型转换 2.指针和void指针的转换 3.父类和子类之间的转换 二.dynamic_cast 三.const_cast 1.加上const 2.去掉const 四.reinterpret_cast 在C++中,我们经常使用到类型的转换,像把一个int类型转换成char类型,一个int类型转换成double类型,这些转换属于隐式类型转换.而今天我们要来讲的是显式类型转换.C++提供了四种显式类型转换,分别是:static_cast.dynamic_c

随机推荐