C++ 拷贝构造与拷贝赋值

[阅读原文]

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

拷贝构造

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

class A;
class B;

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

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

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

// 类定义
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 一份过去。

示例如下:

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 年啊,一个普通人几乎不可能留下任何能够证明自己存在过的东西了,而这个代码我想是一个比骨灰更加有意义一点的东西,希望这个计划能够如实执行下去吧,就算没有人看,就算它可能被遗忘了,但是至少它依然存在。


©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页