c++: 移动构造/赋值 和 拷贝构造/赋值

51 篇文章 2 订阅

Copy Constructor vs. Assignment

https://blog.csdn.net/u013904227/article/details/107441848

[阅读原文]

这篇开始总结巩固下 C++ 的基础知识,一方面是用于做备忘,另一方面也是加深印象、发散思考。该系列也是从自己掌握不牢固的知识点开始写起,并不会尽全尽善,顶多就是记录一些零零碎碎的知识点而已,但是对于这些零碎的知识点会尽量做到这个点是全面完善的,话不多说,开搞。

拷贝构造

所谓拷贝构造就是使用一个已经构造好的类作为参数来构造另一个类,一般形式如下所示:

class A;
class B;

A a = new A;
B b = a; // 拷贝构造函数
B c(a);  // 拷贝构造函数


拷贝构造的对象就是同样数据类型的两个 class 实例,其中一个处于已初始化构造状态(a),另一个处于未构造状态(b,c),默认的拷贝构造函数会把已初始化的(a)实例成员全部拷贝一份给到未初始化的类实例(b,c),内部的值全部都是简单赋值 (possible shallow copy of pointers => if no copy method specified/overloaded, the compiler will simply issue instructions to copy bits-content),后面可以看到这个缺点是什么。关于拷贝构造我建了一个文件用于测试,测试文件可以在「阅读原文」找到,下面列出其中一部分进行试验结论描述。

这里先给出一个类定义,这个算是比较不常变的一段代码了,后面会改变一些其它的变量来进行测试:

// 类定义
class Pipeline {
public:
    Pipeline(std::string pipelineName);

    ~Pipeline();

    inline std::string* GetAndPrintPipelineName()
    {
        printf("This pipeline[%p] name is:%s/%p\n", &m_pipelineName,
            m_pipelineName.c_str(), m_pipelineName.c_str());

        return &m_pipelineName;
    }

    int AddOneNode(NodeInstanceMap nodeIns);

    int m_nodeCount;
    std::list<NodeInstanceMap> m_nodeList;
protected:
    // This will delete the default copy constructor, better use it.
    //Pipeline (const Pipeline& pipeline) = delete;
    //Pipeline& operator=(const Pipeline& pipeline) = delete;

private:
    // Private copy constructor, if use copy constructor,
    // compiler will drop a error.
    //Pipeline(const Pipeline& pipeline);
    std::string m_pipelineName;
    int* m_pPipelineNodesRef;
};

可以看到,上面的类实现里面并没有实现任何自定义的拷贝构造函数,所以系统会帮我们实现一个默认的拷贝构造函数,这里我们就看下默认的拷贝构造函数是怎么工作的:

Pipeline::Pipeline(std::string pipelineName)
{
    m_pipelineName = pipelineName;
    m_nodeCount    = 0;

    std::cout << "pipeline name is:" << m_pipelineName << std::endl;
    printf("Input string[%p], name is:%s/%p.\n",
        &pipelineName, pipelineName.c_str(), m_pipelineName.c_str());
}

int Pipeline::AddOneNode(NodeInstanceMap nodeIns)
{
    m_nodeList.push_back(nodeIns);
    m_nodeCount++;

    std::cout<<"Add one node, type:" << nodeIns.nodeType <<", id:" << nodeIns.nodeId << ", nodeCount:" << m_nodeCount << std::endl;
}

int main(void)
{
    Pipeline* pipelineCamera = new Pipeline("pipelineCamera"); // 1

    Pipeline pipelineSensor("pipelineSensor");                 // 2

    NodeInstanceMap nodeIns = {0, PipelineNodeType::NodeCameraCapture};
    pipelineSensor.AddOneNode(nodeIns);                        // 3

    Pipeline pipelineSensorCopy = pipelineSensor;              // 

    pipelineSensorCopy.GetAndPrintPipelineName();              // 4
    pipelineSensor.AddOneNode(nodeIns);                        // 5

    delete pipelineCamera;

    return 0;
}

// 运行结果
pipeline name is:pipelineCamera // 1
Input string[0x7fffe90bc840], name is:pipelineCamera/0x7fffe06d1ea0. // 1
pipeline name is:pipelineSensor // 2
Input string[0x7fffe90bc840], name is:pipelineSensor/0x7fffe90bc820. // 2
Add one node, type:2, id:0, nodeCount:1 // 3
This pipeline[0x7fffe90bc860] name is:pipelineSensor/0x7fffe90bc870  // 4
Add one node, type:2, id:0, nodeCount:2 // 5

从上面的输出结果,我们可以知道,对于 pipelineSensorCopy 这个实例来讲,其初始化的时候并没有调用构造函数,而是默认拷贝构造函数起了作用,并且可以看到,里面的额成员全部都被 pipelineSensor 的内容覆盖了,尤其是 nodeCount 这个值比较明显,但是这里并没有出现问题,因为里面成员所有的都非指针类型,并且 C++ 也支持了 list,string 的直接赋值拷贝,可以看到里面字符串的地址都是不一样的,所以不会出现内存问题。

下面就来加入另外一个成员的初始化:m_pPipelineNodesRef.事情开始起了变化,改变构造与析构函数如下述所示:

Pipeline::Pipeline(std::string pipelineName)
{
    m_pipelineName      = pipelineName;
    m_nodeCount         = 0;
    m_pPipelineNodesRef = NULL;

    if (NULL == m_pPipelineNodesRef)
    {
        m_pPipelineNodesRef = new int[10];
        printf("Pipeline nodes ref[%p].\n", m_pPipelineNodesRef);
    }

    std::cout << "pipeline name is:" << m_pipelineName << std::endl;
    printf("Input string[%p], name is:%s/%p.\n",
        &pipelineName, pipelineName.c_str(), m_pipelineName.c_str());
}

Pipeline::~Pipeline()
{
    if (NULL != m_pPipelineNodesRef)
    {
        printf("Delete pipeline nodes ref[%p].\n", m_pPipelineNodesRef);
        delete m_pPipelineNodesRef;
    }
}

// 运行结果
Pipeline nodes ref[0x7fffe1a49ec0].
pipeline name is:pipelineCamera
Input string[0x7fffe9c42b80], name is:pipelineCamera/0x7fffe1a49ea0.
Pipeline nodes ref[0x7fffe1a4af00].
pipeline name is:pipelineSensor
Input string[0x7fffe9c42b80], name is:pipelineSensor/0x7fffe9c42b60.
Add one node, type:2, id:0, nodeCount:1
This pipeline[0x7fffe9c42ba0] name is:pipelineSensor/0x7fffe9c42bb0
Add one node, type:2, id:0, nodeCount:2
Delete pipeline nodes ref[0x7fffe1a49ec0].
Delete pipeline nodes ref[0x7fffe1a4af00]. // 这个地址被 delete 了两次
Delete pipeline nodes ref[0x7fffe1a4af00].

可以看到,如果是指针的话,这个指针会被直接赋值过去,这种是比较危险的事情,因为如果其中一个被析构了,另外一个再去访问那块内存就会造成内存访问错误,我这里 delete 了两次都没有出现问题,可能是和编译器还有系统运行策略有关系,总之如果是指针类型的话是会直接赋值指针地址值过来的,而不是另外一个自动分配好的地址,这种情况下默认拷贝构造函数显然就是很不适用的。

在最新的 C++ 标准里面已经加入了移除默认拷贝构造函数的接口,形式如:Pipeline (const Pipeline& pipeline) = delete;,当我们在类里面加入这句话的时候会出现以下的编译错误,这样就可以在编译时间禁止进行默认拷贝构造

/Constructor.cpp: In function ‘int main()’:
/Constructor.cpp:47:35: error: ‘NewbieC::Pipeline::Pipeline(const NewbieC::Pipeline&)’ is protected within this context
     Pipeline pipelineSensorCopy = pipelineSensor;
                                   ^~~~~~~~~~~~~~
In file included from /Constructor.cpp:1:0:
/Constructor.h:46:5: note: declared protected here
     Pipeline (const Pipeline& pipeline) = delete;
     ^~~~~~~~
/Constructor.cpp:47:35: error: use of deleted function ‘NewbieC::Pipeline::Pipeline(const NewbieC::Pipeline&)’
     Pipeline pipelineSensorCopy = pipelineSensor;
                                   ^~~~~~~~~~~~~~
In file included from /Constructor.cpp:1:0:
/Constructor.h:46:5: note: declared here
     Pipeline (const Pipeline& pipeline) = delete;
     ^~~~~~~~
/mnt/d/workspace/code-dev/Newbie-C/build_tools//Compile_BinTarget.mk:59: recipe for target '/Constructor.cpp.o' failed
make[1]: *** [/Constructor.cpp.o] Error 1

// 下面的定义方式也会使得编译器在编译时报错,修改为 private 让外部实例无法调用即可
private:
    // Private copy constructor, if use copy constructor,
    // compiler will drop a error.
    Pipeline(const Pipeline& pipeline);

当然除此之外,我们还可以实现自己的拷贝构造函数,不过在里面需要处理好内存的问题,在工程实践上面除了少数的类可以这样搞,大部分都是不建议的,因为代码实现起来比较严格,很难保证不整出来什么内存相关的 bug 来。

拷贝赋值

拷贝赋值简单来讲就是两个步骤:

1. 初始化的时候先构造一个实例,里面内容全部都初始化完毕

2. 拷贝赋值的时候把右边的实例成员全部 copy 一份过去

// crucially we see that there are no thrid step where the temporary construct is destructed ==> memory leak; follow the nodes ref value of the temp node in the example below:
示例如下:

Pipeline pipelineSensorCopy("pipelineSensorCopy");
pipelineSensorCopy = pipelineSensor;

// 输出的时候会多下面几行
Pipeline nodes ref[0x7fffee1cffa0].
pipeline name is:pipelineSensorCopy
Input string[0x7ffff6a5e070], name is:pipelineSensorCopy/0x7fffee1cff70.

// 完整输出
Pipeline nodes ref[0x7fffee1ceec0].
pipeline name is:pipelineCamera
Input string[0x7ffff6a5e0e0], name is:pipelineCamera/0x7fffee1ceea0.
Pipeline nodes ref[0x7fffee1cff00].
pipeline name is:pipelineSensor
Input string[0x7ffff6a5e0e0], name is:pipelineSensor/0x7ffff6a5e0c0.
Add one node, type:2, id:0, nodeCount:1
Pipeline nodes ref[0x7fffee1cffa0]. // 内存泄漏
pipeline name is:pipelineSensorCopy
Input string[0x7ffff6a5e070], name is:pipelineSensorCopy/0x7fffee1cff70.
This pipeline[0x7ffff6a5e100] name is:pipelineSensor/0x7fffee1cff70
Add one node, type:2, id:0, nodeCount:2
Delete pipeline nodes ref[0x7fffee1ceec0].
Delete pipeline nodes ref[0x7fffee1cff00].
Delete pipeline nodes ref[0x7fffee1cff00].

这里可以看到这种比默认拷贝构造多了一重危险,那就是内存泄漏,默认拷贝构造函数不会再去调用构造函数,而是直接赋值,而拷贝赋值则没有省去构造的步骤赋值完毕之后丢了另外一个指针,就会造成内存泄漏问题,所以禁掉它,方式类似于禁止默认拷贝构造一样:Pipeline& operator=(const Pipeline& pipeline) = delete;,禁掉就万事大吉

一般实现模式
Pipeline (const Pipeline& pipeline) = delete;
Pipeline& operator=(const Pipeline& pipeline) = delete;

如果不确定的话那就把上面两个加进去代码里面,这样就算是自己麻烦一点,也好歹可以保证比较不容易出现内存的 bug,在一些大型工程里面一般除了上面两个操作之外,我们的构造函数基本上也是啥也不做,就是一个空的东西,然后在类内实现一个初始化的方法以供程序显式调用,这样会使得程序更加可控化

End
话说我自己的这个代码仓库被 github 自动加入了北极代码收藏计划,冷冻 1000 年,虽然估计也不会有人去看,但是感觉还是莫名有种很强的仪式感,就算没人去看,能够留下一点点跨越千年的足迹记号也是很浪漫的一件事,1000 年啊,一个普通人几乎不可能留下任何能够证明自己存在过的东西了,而这个代码我想是一个比骨灰更加有意义一点的东西,希望这个计划能够如实执行下去吧,就算没有人看,就算它可能被遗忘了,但是至少它依然存在。


————————————————
版权声明:本文为CSDN博主「YellowMax2001」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u013904227/article/details/107441848

 

Move Constructor

转自C语言中文网: C++11移动构造函数详解

C++11右值引用》一节中,给读者详细介绍了 C++ 右值引用的含义和用法,同时还提到“右值引用主要用于实现移动(move)语义和完美转发”。有关完美转发,后续章节会做详细介绍,本节主要讲解移动语义的含义以及实现它的方式。

值得一提的是,左值的英文简写为“lvalue”,右值的英文简写为“rvalue”。很多人认为它们分别是"left value"、"right value" 的缩写,其实不然。lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,而 rvalue 译为 "read value",指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。

!!!

for the detail of which expr. are lvalue and which are rvalue (prvalue or xvalue), see: https://en.cppreference.com/w/cpp/language/value_category

for a quick summary, see: Cpp / 右值、纯右值、将亡值

interestingly:

++a, --a ==> returns lvalue;

a++, a-- ==> returns prvalue;

which is actually quite intuitive.

C++11移动语义是什么

在 C++ 11 标准之前(C++ 98/03 标准中),如果想用其它对象初始化一个同类的新对象,只能借助类中的复制(拷贝)构造函数。通过《C++拷贝构造函数》一节的学习我们知道,拷贝构造函数的实现原理很简单,就是为新对象复制一份和其它对象一模一样的数据。

需要注意的是,当类中拥有指针类型的成员变量时,拷贝构造函数中需要以深拷贝(而非浅拷贝)的方式复制该指针成员。有关深拷贝和浅拷贝以及它们的区别,读者可阅读《C++深拷贝和浅拷贝》一文做详细了解。

举个例子:

#include <iostream>
using namespace std;

class demo{
public:
demo():num(new int(0)){
cout<<"construct!"<<endl;
}
//拷贝构造函数
demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}
~demo(){
cout<<"class destruct!"<<endl;
}
private:
int *num;
};

demo get_demo(){
return demo();
}

int main(){
demo a = get_demo();
return 0;
}

如上所示,我们为 demo 类自定义了一个拷贝构造函数。该函数在拷贝 d.num 指针成员时,必须采用深拷贝的方式,即拷贝该指针成员本身的同时,还要拷贝指针指向的内存资源。否则一旦多个对象中的指针成员指向同一块堆空间,这些对象析构时就会对该空间释放多次,这是不允许的。

可以看到,程序中定义了一个可返回 demo 对象的 get_demo() 函数,用于在 main() 主函数中初始化 a 对象,其整个初始化的流程包含以下几个阶段:

  1. 执行 get_demo() 函数内部的 demo() 语句,即调用 demo 类的默认构造函数生成一个匿名对象;
  2. 执行 return demo() 语句,会调用拷贝构造函数复制一份之前生成的匿名对象,并将其作为 get_demo() 函数的返回值(函数体执行完毕之前,匿名对象会被析构销毁);
  3. 执行 a = get_demo() 语句,再调用一次拷贝构造函数,将之前拷贝得到的临时对象复制给 a(此行代码执行完毕,get_demo() 函数返回的对象会被析构);
  4. 程序执行结束前,会自行调用 demo 类的析构函数销毁 a。

Recall from the article above, copy assignment should be avoided, due to the lack of the third step, which should be calling the destructor of the temporary value holder.

注意,目前多数编译器都会对程序中发生的拷贝操作进行优化,因此如果我们使用 VS 2017、codeblocks 等这些编译器运行此程序时,看到的往往是优化后的输出结果:

construct!
class destruct!

而同样的程序,如果在 Linux 上使用g++ demo.cpp -fno-elide-constructors命令运行(其中 demo.cpp 是程序文件的名称),就可以看到完整的输出结果:

construct!                <-- 执行 demo()
copy construct!       <-- 执行 return demo()
class destruct!         <-- 销毁 demo() 产生的匿名对象
copy construct!       <-- 执行 a = get_demo()
class destruct!         <-- 销毁 get_demo() 返回的临时对象
class destruct!         <-- 销毁 a

如上所示,利用拷贝构造函数实现对 a 对象的初始化,底层实际上进行了 2 次拷贝(而且是深拷贝)操作。当然,对于仅申请少量堆空间的临时对象来说,深拷贝的执行效率依旧可以接受,但如果临时对象中的指针成员申请了大量的堆空间,那么 2 次深拷贝操作势必会影响 a 对象初始化的执行效率。

事实上,此问题一直存留在以 C++ 98/03 标准编写的 C++ 程序中。由于临时变量的产生、销毁以及发生的拷贝操作本身就是很隐晦的(编译器对这些过程做了专门的优化),且并不会影响程序的正确性,因此很少进入程序员的视野。

(Move Constructor: Applied Scenario)

那么当类中包含指针类型的成员变量使用其它对象来初始化同类对象时,怎样才能避免深拷贝导致的效率问题呢?C++11 标准引入了解决方案,该标准中引入了右值引用的语法,借助它可以实现移动语义。

C++移动构造函数(移动语义的具体实现)

所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是其他对象(通常是临时对象)拥有的内存资源“移为已用”

以前面程序中的 demo 类为例,该类的成员都包含一个整形的指针成员,其默认指向的是容纳一个整形变量的堆空间。当使用 get_demo() 函数返回的临时对象初始化 a 时,我们只需要将临时对象的 num 指针直接浅拷贝给 a.num,然后修改该临时对象中 num 指针的指向(通常另其指向 NULL),这样就完成了 a.num 的初始化。

事实上,对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。

例如,下面程序对 demo 类进行了修改:

#include <iostream>
using namespace std;
class demo{
public:
demo():num(new int(0)){
cout<<"construct!"<<endl;
}

demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}
//添加移动构造函数
demo(demo &&d):num(d.num){
d.num = NULL;
cout<<"move construct!"<<endl;
}
~demo(){
cout<<"class destruct!"<<endl;
}
private:
int *num;
};
demo get_demo(){
return demo();
}
int main(){
demo a = get_demo();
return 0;
}

可以看到,在之前 demo 类的基础上,我们又手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。==> it's crucial to set the temp pointers to NULL, a "destructable" state.

在 Linux 系统中使用g++ demo.cpp -o demo.exe -std=c++0x -fno-elide-constructors命令执行此程序,输出结果为:

construct!
move construct!
class destruct!
move construct!
class destruct!
class destruct!

通过执行结果我们不难得知,当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。

我们知道,非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。

在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。


读者可能会问,如果使用左值初始化同类对象,但也想调用移动构造函数完成,有没有办法可以实现呢?

默认情况下,左值初始化同类对象只能通过拷贝构造函数完成,如果想调用移动构造函数,则必须使用右值进行初始化。C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数

有关 std::move() 函数的用法,后续章节会做详细讲解。

 

Move Assignment

https://www.cnblogs.com/Joezzz/p/9707512.html

move assignment takes over the resource of the src, and as a result there won't be any memory leak due to the assignment.

1.概念

1)移动赋值运算符是一个重载的赋值运算符,参数为自身类的右值引用,返回值自身类的左值引用,由于不抛出任何异常,用noexcept指定(如果定义在类的外面,那么定义也要用noexcept指定)

2.合成移动赋值运算符

  同移动构造函数

3.具有拷贝赋值和移动赋值两种功能的赋值运算符

  此赋值运算符有一个非引用形参,这就意味为使用它时会发生拷贝初始化,遇到左值则使用拷贝构造函数,遇到右值则使用移动构造函数。

  第一个赋值中:hp2是一个左值,所以会使用拷贝构造函数来生成临时对象rhs;

  第二个赋值中:move函数创建了一个右值,所以会使用移动构造函数来生成临时对象rhs,hp2被move调用后就不能再使用了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值