C++移动构造函数以及move语句简单介绍

C++移动构造函数以及move语句简单介绍

首先看一个小例子:

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

int main()
{
    string st = "I love xing";
    vector<string> vc ;
    vc.push_back(move(st));
    cout<<vc[0]<<endl;
    if(!st.empty())
        cout<<st<<endl;

    return 0;
}

结果为:

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

int main()
{
    string st = "I love xing";
    vector<string> vc ;
    vc.push_back(st);
    cout<<vc[0]<<endl;
    if(!st.empty())
        cout<<st<<endl;

    return 0;
}

结果为:

这两个小程序唯一的不同是调用vc.push_back()将字符串插入到容器中去时,第一段代码使用了move语句,而第二段代码没有使用move语句。输出的结果差异也很明显,第一段代码中,原来的字符串st已经为空,而第二段代码中,原来的字符串st的内容没有变化。

好,记住这两端代码的输出结果之间的差异。下面我们简单介绍一下移动构造函数。

在介绍移动构造函数之前,我们先要回顾一下拷贝构造函数。

我们都知道,C++在三种情况下会调用拷贝构造函数(可能有纰漏),第一种情况是函数形实结合时,第二种情况是函数返回时,函数栈区的对象会复制一份到函数的返回去,第三种情况是用一个对象初始化另一个对象时也会调用拷贝构造函数。

除了这三种情况下会调用拷贝构造函数,另外如果将一个对象赋值给另一个对象,这个时候回调用重载的赋值运算符函数。

无论是拷贝构造函数,还是重载的赋值运算符函数,我记得当时在上C++课的时候,老师再三强调,一定要注意指针的浅层复制问题。

这里在简单回忆一下拷贝构造函数中的浅层复制问题

首先看一个浅层复制的代码

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

class Str{
    public:
    char *value;
    Str(char s[])
    {
        cout<<"调用构造函数..."<<endl;
        int len = strlen(s);
        value = new char[len + 1];
        memset(value,0,len + 1);
        strcpy(value,s);
    }
    Str(Str &v)
    {
        cout<<"调用拷贝构造函数..."<<endl;
        this->value = v.value;
    }
    ~Str()
    {
        cout<<"调用析构函数..."<<endl;
        if(value != NULL)
            delete[] value;
    }
};

int main()
{

    char s[] = "I love BIT";
    Str *a = new Str(s);
    Str *b = new Str(*a);
    delete a;
    cout<<"b对象中的字符串为:"<<b->value<<endl;
    delete b;
    return 0;
}

输出结果为:

首先结果并不符合预期,我们希望b对象中的字符串也是I love BIT但是输出为空,这是因为b->value和a->value指向了同一片内存区域,当delete a的时候,该内存区域已经被收回,所以再用b->value访问那块内存实际上是不合适的,而且,虽然我运行时程序没有崩溃,但是程序存在崩溃的风险呀,因为当delete b的时候,那块内存区域又被释放了一次,两次释放同一块内存,相当危险呀。

我们用valgrind检查一下,发现,相当多的内存错误呀!

其中就有一个Invalid free 也就是删除b的时候调用析构函数,对已经释放掉对空间又释放了一次。

那么深层复制应该怎样写呢?

代码如下:

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

class Str{
    public:
    char *value;
    Str(char s[])
    {
        cout<<"调用构造函数..."<<endl;
        int len = strlen(s);
        value = new char[len + 1];
        memset(value,0,len + 1);
        strcpy(value,s);
    }
    Str(Str &v)
    {
        cout<<"调用拷贝构造函数..."<<endl;
        int len = strlen(v.value);
        value = new char[len + 1];
        memset(value,0,len + 1);
        strcpy(value,v.value);
    }
    ~Str()
    {
        cout<<"调用析构函数..."<<endl;
        if(value != NULL)
        {
            delete[] value;
            value = NULL;
        }
    }
};

int main()
{

    char s[] = "I love BIT";
    Str *a = new Str(s);
    Str *b = new Str(*a);
    delete a;
    cout<<"b对象中的字符串为:"<<b->value<<endl;
    delete b;
    return 0;
}

结果为:

这次达到了我们预想的效果,而且,用valgrind检测一下,发现,没有内存错误!

所以,写拷贝构造函数的时候,切记要注意指针的浅层复制问题呀!

好的,回顾了一下拷贝构造函数,下面回到移动构造函数上来。

有时候我们会遇到这样一种情况,我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷。

下面这个图,很好地说明了拷贝构造函数和移动构造函数的区别。

看明白了吗?

通俗一点的解释就是,拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。

但是上面提到,指针的浅层复制是非常危险的呀。没错,确实很危险,而且通过上面的例子,我们也可以看出,浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间(同时也是b->value指向的空间)

所以我们可以把上面的拷贝构造函数的代码修改一下:

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

class Str{
    public:
    char *value;
    Str(char s[])
    {
        cout<<"调用构造函数..."<<endl;
        int len = strlen(s);
        value = new char[len + 1];
        memset(value,0,len + 1);
        strcpy(value,s);
    }
    Str(Str &v)
    {
        cout<<"调用拷贝构造函数..."<<endl;
        this->value = v.value;
        v.value = NULL;
    }
    ~Str()
    {
        cout<<"调用析构函数..."<<endl;
        if(value != NULL)
            delete[] value;
    }
};

int main()
{

    char s[] = "I love BIT";
    Str *a = new Str(s);
    Str *b = new Str(*a);
    delete a;
    cout<<"b对象中的字符串为:"<<b->value<<endl;
    delete b;
    return 0;
}

结果为:

修改后的拷贝构造函数,采用了浅层复制,但是结果仍能够达到我们想要的效果,关键在于在拷贝构造函数中,最后我们将v.value置为了NULL,这样在析构a的时候,就不会回收a->value指向的内存空间。

这样用a初始化b的过程中,实际上我们就减少了开辟内存,构造成本就降低了。

但要注意,我们这样使用有一个前提是:用a初始化b后,a我们就不需要了,最好是初始化完成后就将a析构。如果说,我们用a初始化了b后,仍要对a进行操作,用这种浅层复制的方法就不合适了。

所以C++引入了移动构造函数,专门处理这种,用a初始化b后,就将a析构的情况。

*************************************************************

**移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。(关于右值引用大家可以看我之前的文章,或者查找其他资料)。这意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。

移动构造函数应用最多的地方就是STL中

给出一个代码,大家自行验证使用move和不适用move的区别吧

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>
using namespace std;

class Str{
    public:
        char *str;
        Str(char value[])
        {
            cout<<"普通构造函数..."<<endl;
            str = NULL;
            int len = strlen(value);
            str = (char *)malloc(len + 1);
            memset(str,0,len + 1);
            strcpy(str,value);
        }
        Str(const Str &s)
        {
            cout<<"拷贝构造函数..."<<endl;
            str = NULL;
            int len = strlen(s.str);
            str = (char *)malloc(len + 1);
            memset(str,0,len + 1);
            strcpy(str,s.str);
        }
        Str(Str &&s)
        {
            cout<<"移动构造函数..."<<endl;
            str = NULL;
            str = s.str;
            s.str = NULL;
        }
        ~Str()
        {
            cout<<"析构函数"<<endl;
            if(str != NULL)
            {
                free(str);
                str = NULL;
            }
        }
};
int main()
{
    char value[] = "I love zx";
    Str s(value);
    vector<Str> vs;
    //vs.push_back(move(s));
    vs.push_back(s);
    cout<<vs[0].str<<endl;
    if(s.str != NULL)
        cout<<s.str<<endl;
    return 0;
} 

如果你觉得对你有用,请咱一个吧~~~

时间: 2024-09-28 03:06:34

C++移动构造函数以及move语句简单介绍的相关文章

javascript的return语句简单介绍

javascript的return语句简单介绍:return语句在js中非常的重要,不仅仅具有返回函数值的功能,还具有一些特殊的用法,有个清晰的把握是非常有必要的.下面就结合实例简单介绍一下return语句的作用.一.用来返回控制和函数结果:通常情况,return语句对于一个函数是很有必要的,因为往往需要函数在一系列的代码执行后会得到一个期望的返回值,而此值就是通过return语句返回,并且将控制权返回给主调函数.语法格式: return 表达式 代码实例如下: function add(){

javascript标签语句简单介绍

javascript标签语句简单介绍:由于对于标签语句的应用并不多,所以可能很多朋友都不是太了解,下面就对它做一下简单介绍,希望能够给需要的朋友带来一定的帮助.标签其实是一个标示符,关于表示符这里就不多介绍了,具体可以参阅javascript标示符的概念是什么一章节, 标签可以与变量重名,它是一个独立的语法元素,它的作用是标识标签化语句(labeled statement).一.标签声明:标签可以声明在任何一个语句前面,或者语句块前,以使得语句或语句块被“标签化(labeled)”,简单的说就是

用if条件语句来实现浏览器兼容简单介绍

用if条件语句来实现浏览器兼容简单介绍:提到if语句一般都是编程语言使用,其实css中也有,不过只有IE浏览器支持,由于低版本IE浏览器兼容性很差,所以就可以使用仅有IE支持的if语句来实现兼容,下面是一段简单的实例.代码如下: <!DOCTYPE html> <html> <head> <meta charset=" utf-8"> <meta name="author" content="http:

简单介绍如何使用PowerMock和Mockito来mock 1. 构造函数 2. 静态函数 3. 枚举实现的单例 4. 选择参数值做为函数的返回值(转)

本文将简单介绍如何使用PowerMock和Mockito来mock1. 构造函数2. 静态函数3. 枚举实现的单例4. 选择参数值做为函数的返回值5. 在调用mock出来的方法中,改变方法参数的值 一点简要说明:Mockito其实已经可以满足大部分的需求,但是它的实现机制是使用cglib来动态创建接口的类的实例.但是这种实现方式不能用于构造函数和静态函数,因为那需要使用类的字节码(比如使用javassist). 所以我们才需要结合使用PowerMock. 1. mock构造函数, 如果有代码没有

webstorm简单介绍,webstrom基本使用

WebStorm混搭svn WebStorm混搭nodeJS     以less和uglify-js为例     如何用npm在root中下载模块  --------------------------------------------- webstorm简单介绍 官网地址:http://www.jetbrains.com/webstorm/features/index.html 参考地址:http://www.html5jscss.com/ide-webstrom-27.html 欲先善其事

iOS-iOS开发简单介绍

概览 终于到了真正接触IOS应用程序的时刻了,之前我们花了很多时间去讨论C语言.ObjC等知识,对于很多朋友而言开发IOS第一天就想直接看到成果,看到可以运行的IOS程序.但是这里我想强调一下,前面的知识是你日后开发IOS的基础,没有那些知识你开发IOS会很痛苦,现在很多开发人员做开发都是一知半解,程序质量确实令人担忧,所以还是希望大家能够熟练掌握前面的内容,开发过程中多思考,彻底理解程序运行的原理.机制.好了言归正传,不管怎么样正式进入IOS开发还是令人兴奋的,今天的内容虽然说是开发预览,其实

Rebound动画框架简单介绍

Rebound动画框架简单介绍 Android菜鸟一枚,有不对的地方希望大家指出,谢谢. 最近在接手了一个老项目,发现里面动画框架用的是facebook中的Rebound框架,由于以前没听说过,放假时闲得蛋痛,看看了源码,就顺手写这一篇吧. 写了一个小Demo,具体效果如下: 代码很简单,这是xml布局: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools=&

python的列表,元组和字典简单介绍

引 入 java                                   python 存取多个值:数组或list集合 ------------------------> 列表,元组 key-value格式:    Map        ------------------------>    字典 自己学习发现,java跟python这两门面向对象语言在数据类型的定义上,很多思想都是互通的,这里不说java,简单介绍一下python的列表,元组和字典. 一.列表 List: 最通

plsql的环境与介绍:环境的搭建和plsql的简单介绍

PLSQL编程 1.环境的搭建 (1)创建一个存储表空间 SQL> conn /as sysdbaConnected. SQL> create tablespace plsql datafile '/u01/oracle/oradata/ORCL/plsql01.dbf' size 1G; Tablespace created. (2)创建PLSQL用户SQL> create user plsql identified by plsql default tablespace plsql;