IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++拷贝控制1 -> 正文阅读

[C++知识库]C++拷贝控制1

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 调用时机

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

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

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;
}

程序结束的时候,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;
};

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

2. 拷贝赋值运算符

2.1 定义

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

Point p1;
Point p2 = p1;
Point p3;
p3 = p2;

第二行并没有调用拷贝赋值运算符,而是一个拷贝初始化,所以这一行调用的是拷贝构造函数;
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;
};

注意:赋值运算符重载中,最后返回的是一个此对象的引用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;
}

最后一行调用默认的拷贝赋值运算符(浅拷贝),程序结束析构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;
};

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

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

3. 析构函数

3.1 定义

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

class Point
{
public:
    ~Point();//析构函数
}

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

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

3.2 调用时机

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

  1. 变量在离开其作用域时被销毁;
{
    Point p1;
}
  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. 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁;
Point* p1 = new Point();
delete p1;
  1. 对于临时对象,当创建它的完整表达式结束时被销毁;
std::vector<Point> points{ Point(1, 2, 3) };

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

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-11-05 00:06:35  更:2022-11-05 00:11:25 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 4:01:58-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码