• C++拷贝控制


    C++中类的基本操作主要包括:

    • 拷贝构造函数
    • 拷贝赋值运算符
    • 移动构造函数
    • 移动赋值运算符
    • 析构函数

    1. 拷贝构造函数

    1.1 定义

    若我们没有定义,则编译器会为我们默认创建一个拷贝构造函数,实现的是浅拷贝。
    浅拷贝默认会这样来实现:

    1. 对类类型的成员,会适用其拷贝构造函数来拷贝;
    2. 内置类型的成员则直接拷贝;
    3. 数组,取决于数组元素的类型,采用方式1或者方式2来进行拷贝。
    class Point
    {
    public:
        // 默认构造函数
        Point() :m_x(0), m_y(0), m_z(0)
        {
            std::cout << "Point默认构造" << std::endl;
        }
        // 拷贝构造函数
        Point(const Point& other) :m_x(other.m_x), m_y(other.m_y), m_z(other.m_z)
        {
            std::cout << "Point拷贝构造" << std::endl;
        }
    private:
        unsigned int m_x;
        unsigned int m_y;
        unsigned int m_z;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    1.2 调用时机

    拷贝构造函数被调用可能发生在以下几种情况:

    1. 用=来定义变量时;
    Point p1;//调用默认构造函数
    Point p2 = p1;//调用拷贝构造函数
    
    • 1
    • 2
    1. 将一个对象作为实参传递给一个非引用类型的形参
    void Test(Point point)
    {
    //即使什么都不做,也会调用一次Point的拷贝构造函数
    }
    
    • 1
    • 2
    • 3
    • 4
    1. 从一个返回类型为非引用类型的函数返回一个对象
    Point Test()
    {
        Point point;
        return point;//调用拷贝构造函数
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
    std::vector<Point> points{ Point(1, 2, 3) };//有参构造和拷贝构造
    
    • 1

    1.3 什么时候需要定义自己的拷贝构造函数

    如果一个类中的成员变量没有指针,那么默认的拷贝构造函数就可以了(即使是浅拷贝)。
    我们来看下面的例子:

    class Line
    {
    public:
        // 有参构造函数
        Line(const Point& startPoint, const Point& endPoint)
            :m_startPoint(new Point(startPoint)), m_endPoint(new Point(endPoint))
        {
            std::cout << "Line有参构造" << std::endl;
        }
        ~Line()
        {
            delete m_startPoint;
            delete m_endPoint;
        }
    private:
        Point* m_startPoint;
        Point* m_endPoint;
    };
    
    int main()
    {
        Point p0;
        Point p1;
        Line line1(p0, p1);
        Line line2 = line1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    程序结束的时候,line1line2都会自动被销毁,这个时候就会抛出异常,因为两者的成员变量m_startPoint其实指向了同一块内存,所以在被delete第二次时候当然抛出异常,m_endPoint也是如此。这个时候我们就需要深拷贝,也就需要实现自己的拷贝构造函数了:

    class Line
    {
    public:
        // 有参构造函数
        Line(const Point& startPoint, const Point& endPoint)
            :m_startPoint(new Point(startPoint)), m_endPoint(new Point(endPoint))
        {
            std::cout << "Line有参构造" << std::endl;
        }
        // 拷贝构造函数
        Line(const Line& other)
            :m_startPoint(new Point(*other.m_startPoint)),
            m_endPoint(new Point(*other.m_endPoint))
        {
            std::cout << "Line拷贝构造" << std::endl;
        }
        ~Line()
        {
            delete m_startPoint;
            delete m_endPoint;
        }
    private:
        Point* m_startPoint;
        Point* m_endPoint;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    其实所做的无非就是对于指针类型的成员变量,在拷贝的时候,需要重新申请一块内存,而不是像浅拷贝那样多个对象最后指向了同一块内存。

    2. 拷贝赋值运算符

    2.1 定义

    与拷贝构造函数一样,如果类未定义自己的拷贝构造赋值运算符,编译器会默认创建一个。
    小区分:

    Point p1;
    Point p2 = p1;
    Point p3;
    p3 = p2;
    
    • 1
    • 2
    • 3
    • 4

    第二行并没有调用拷贝赋值运算符,而是一个拷贝初始化,所以这一行调用的是拷贝构造函数;
    p3已经在第三行完成了初始化(调用了默认构造函数),在第四行调用了拷贝赋值运算符。

    class Point
    {
    public:
        // 默认构造函数
        Point() :m_x(0), m_y(0), m_z(0)
        {
            std::cout << "Point默认构造" << std::endl;
        }
        // 拷贝构造函数
        Point(const Point& other) :m_x(other.m_x), m_y(other.m_y), m_z(other.m_z)
        {
            std::cout << "Point拷贝构造" << std::endl;
        }
    	// 拷贝赋值运算符重载
        Point& operator=(const Point& other)
        {
            std::cout << "Point拷贝赋值运算符" << std::endl;
            m_x = other.m_x;
            m_y = other.m_y;
            m_z = other.m_z;
            return *this;
        }
    private:
        unsigned int m_x;
        unsigned int m_y;
        unsigned int m_z;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    注意:赋值运算符重载中,最后返回的是一个此对象的引用return * this;

    2.2 何时需要定义自己的拷贝赋值运算符

    赋值运算符通常组合了析构函数和构造函数的操作。类似析构函数,赋值操作会销毁左侧运算对象的资源;类似拷贝构造函数,赋值操作会从右侧运算对象拷贝数据。
    同样的,如果类的成员变量中没有指针,默认的拷贝赋值运算符即可满足要求。
    还是上面的例子重写一遍:

    class Line
    {
    public:
        // 有参构造函数
        Line(const Point& startPoint, const Point& endPoint)
            :m_startPoint(new Point(startPoint)), m_endPoint(new Point(endPoint))
        {
            std::cout << "Line有参构造" << std::endl;
        }
        // 拷贝构造函数
        Line(const Line& other)
            :m_startPoint(new Point(*other.m_startPoint)),
            m_endPoint(new Point(*other.m_endPoint))
        {
            std::cout << "Line拷贝构造" << std::endl;
        }
        ~Line()
        {
            delete m_startPoint;
            delete m_endPoint;
        }
    private:
        Point* m_startPoint;
        Point* m_endPoint;
    };
    
    int main()
    {
        Point p0;
        Point p1;
        Line line1(p0, p1);
        Line line2(p0, p1);
        line2 = line1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    最后一行调用默认的拷贝赋值运算符(浅拷贝),程序结束析构line1line2的时候,导致m_startPointm_endPoint都被delete两次,然后抛异常。

    class Line
    {
    public:
        // 有参构造函数
        Line(const Point& startPoint, const Point& endPoint)
            :m_startPoint(new Point(startPoint)), m_endPoint(new Point(endPoint))
        {
            std::cout << "Line有参构造" << std::endl;
        }
        // 拷贝构造函数
        Line(const Line& other)
            :m_startPoint(new Point(*other.m_startPoint)),
            m_endPoint(new Point(*other.m_endPoint))
        {
            std::cout << "Line拷贝构造" << std::endl;
        }
        // 拷贝赋值运算符重载
        Line& operator=(const Line& other)
        {
            Point* newStart = new Point(*other.m_startPoint);
            Point* newEnd = new Point(*other.m_endPoint);
    
            delete m_startPoint;//删除旧内存(不可以先delete再new,防止other和*this是同一个对象)
            delete m_endPoint;
    
            m_startPoint = newStart;//从右侧运算符拷贝数据到本对象
            m_endPoint = newEnd;
    
            return *this;//返回本对象
        }
        ~Line()
        {
            delete m_startPoint;
            delete m_endPoint;
        }
    private:
        Point* m_startPoint;
        Point* m_endPoint;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    可以看出,拷贝赋值运算符重载考虑的其实比拷贝构造函数更多。

    大多数赋值运算符组合了析构函数和拷贝构造函数的工作。

    3. 析构函数

    3.1 定义

    析构函数的作用与构造函数刚好相反,构造函数初始化对象的非static数据成员;析构函数释放对象使用的资源,并销毁对象的非static数据成员。

    class Point
    {
    public:
        ~Point();//析构函数
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    成员销毁时发生什么完全依赖于成员的类型:

    • 销毁类类型的成员需要执行成员自己的析构函数
    • 内置类型没有析构函数,无需执行操作

    3.2 调用时机

    当一个对象被销毁,就会自动调用析构函数:

    1. 变量在离开其作用域时被销毁;
    {
        Point p1;
    }
    
    • 1
    • 2
    • 3
    1. 当一个对象被销毁时,其成员被销毁;
    2. 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁;
    Point p1;
    Point p2;
    {
        std::vector<Point> points;
        points.reserve(2);
        points.push_back(p1);//拷贝构造
        points.push_back(p2);//拷贝构造
    }
    //离开大括号作用域后,points对象被销毁,points容器内的p1'和p2'(不是p1和p2)也被销毁
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁;
    Point* p1 = new Point();
    delete p1;
    
    • 1
    • 2
    1. 对于临时对象,当创建它的完整表达式结束时被销毁;
    std::vector<Point> points{ Point(1, 2, 3) };
    
    • 1

    这里的Point(1, 2, 3)就是一个临时变量,被塞入points之后,创建它的表达式就结束了,该临时变量会被销毁。

    4. 移动构造函数

    4.1 定义

    移动针对的都是右值,拷贝的是左值。

    class Point
    {
    public:
     	...
        // 移动构造函数
        Point(Point&& other) :m_x(other.m_x), m_y(other.m_y), m_z(other.m_z)
        {
            std::cout << "Point移动构造" << std::endl;
        }
        // 移动赋值运算符
        Point& operator=(Point&& other)
        {
            std::cout << "Point移动赋值运算符" << std::endl;
            m_x = other.m_x;
            m_y = other.m_y;
            m_z = other.m_z;
            return *this;
        }
        ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    4.2 合成的移动操作

    只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
    所有内置类型的成员,都可以移动。

    struct X
    {
        int i;
        std::string s;
    };
    
    struct hasX
    {
        X mem;
    };
    
    X x1;
    X x2 = std::move(x1);//会使用默认的移动构造函数来初始化x2
    hasX h1;
    hasX h2 = std::move(h2);//会使用默认的移动构造函数来初始化h2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    class Foo
    {
    public:
        Foo() = default;
        Foo(const Foo&);
    };
    
    Foo x;
    Foo y(x);//调用拷贝构造函数,x是一个左值
    Foo z(std::move(x));//调用拷贝构造函数,因为定义了拷贝构造,所有不会有默认的移动构造。
    //如果定义了移动构造,那么这里就会调用移动构造
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    4.3 右值引用和成员函数

    如果一个成员函数同时提供拷贝和移动版本,它也能从中受益。
    和拷贝/移动构造的参数一样,会有2个版本:

    • const Foo&(拷贝)
    • Foo&&(移动)
      例如,定义了push_back的标准库容器提供2个版本:一个版本有一个右值引用参数,而另一个版本有一个const左值引用:
    void push_back(const T&);// 拷贝
    void push_back(T&&);// 移动
    
    • 1
    • 2

    4.4 调用时机

    若定义了移动构造,则当参数为右值引用时(比如std::move,就是为了得到一个右值引用)

  • 相关阅读:
    1.14.C++项目:仿muduo库实现并发服务器之Util模块的设计
    雅思口语同替高分表达
    WP-AutoPostPro 汉化版: WordPress自动采集发布插件 WordPress文章采集
    ArcGIS应用(二十六)按照属性分割矢量图层要素为新的图层
    sql server 查询所有表所有字段中包含某个字符
    基于PYQT5的GUI开发系列教程【二】框架安装和基础环境配置
    多人聊天室 (epoll - Linux网络编程)
    Zynq UltraScale+ XCZU4EV 纯VHDL解码 IMX214 MIPI 视频,2路视频拼接输出,提供vivado工程源码和技术支持
    ClickHouse(24)ClickHouse集成mongodb表引擎详细解析
    Redis基础(一)
  • 原文地址:https://blog.csdn.net/niaxiapia/article/details/127657753