文章目录

  • 什么是拷贝构造函数
  • 为什么拷贝构造要传引用
  • 默认拷贝构造函数
  • 浅拷贝和深拷贝
  • 编译器可能的优化
    • 自定义类型做函数返回值时传值返回
    • 自定义类型做函数参数时传值调用
    • 总结一下

什么是拷贝构造函数

如果已经存在一个对象,我想对这个对象再复制一份,该怎么做呢?

有两种方法,拷贝构造和赋值运算符重载,但显然赋值运算符重载不是这里的重点,这里要讲的是前者。

拷贝构造函数是类的六大特殊成员函数之一,它是构造函数的一个重载形式,且参数只有一个,必须使用引用传参

而且由于拷贝并不需要改变参数,所以参数部分还要用 “const” 来修饰

下面是用例示范:

class Date {public:Date(int year = 2022, int month = 8, int day = 20):_year(year), _month(month), _day(day){cout << "Date(int year = 2022, int month = 8, int day = 20)" << endl;}Date(const Date& d):_year(d._year), _month(d._month), _day(d._day){cout << "Date(const Date& d)" << endl;}private:int _year;int _month;int _day;
};int main() {Date d1;Date d2(d1);Date d3 = d1;return 0;
}

运行结果如下:

拷贝构造是已经存在的对象拷贝给即将要创建的对象,所以 d3 虽然像是去调用赋值运算符重载函数,实际上还是拷贝构造。


为什么拷贝构造要传引用

一开始说拷贝构造必须传引用,那传值为什么不行呢?

首先传值调用的话,我们要明确一点,就是形参是实参的临时拷贝

以下面的代码为例:

void f(Date d)
{}int main() {Date d;f(d);return 0;
}

现在已经明确了,传参时会调用一次拷贝构造函数。

那么如果拷贝构造的参数部分也是传值调用呢?

每次调用拷贝构造函数传参时都要进行临时拷贝,

临时拷贝又要调用拷贝构造函数,

如此往复,就引发了无穷递归。

下面的图就能很形象地解释这个问题:


默认拷贝构造函数

如果我忘了写拷贝构造函数,但后边又调用了,会发生什么呢?

class Date {public:Date(int year = 2022, int month = 8, int day = 20) :_year(year), _month(month), _day(day){cout << "Date(int year = 2022, int month = 8, int day = 20)" << endl;}void Print() {cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main() {Date d1;Date d2(d1);d2.Print();return 0;
}

运行结果如下:

d2 也成功完成拷贝构造了。

拷贝构造函数毕竟是六个特殊的成员函数之一,所以我们不写编译器也是会自动生成的,编译器自动生成的这个就是所谓的默认拷贝构造函数

同样地,和编译器默认生成的构造函数一样,默认拷贝构造函数对内置类型会完成拷贝,对自定义类型会去调用它的拷贝构造函数:

class A {public:A() :_a(0){cout << "A()" << endl;}A(const A& aa):_a(aa._a){cout << "A(const A& aa)" << endl;}private:int _a;
};class B {public:B():_b(0){cout << "B()" << endl;}private:int _b;A aa;
};int main() {B b1;B b2(b1);return 0;
}

运行结果如下:

前面调用的编译器自动生成的拷贝构造函数,也完成了我们想要的效果。

既然编译器自动生成的默认拷贝构造函数也能完成拷贝构造,那我们是不是可以不写了呢?

看下面一段代码:

class Stack {public:Stack(int k = 4) :_arr(new int[k]),_size(0),_capacity(k){cout << "Stack(int k = 4)" << endl;}~Stack() {cout << "~Stack()" << endl;delete[] _arr;_arr = nullptr;_size = _capacity = 0;}private:int* _arr;int _size;int _capacity;
};int main() {Stack st1;Stack st2(st1);return 0;
}

这是粗略地写了一个 Stack 类,~Stack() 是析构函数,出对象的作用域会自动调用,完成对象内容的清理,这里是释放掉 _arr 指向的空间并置空。

那么程序走起来:

程序运行过程中挂掉了。

调试看一下监视窗口:

st1st2_arr 竟然都指向一块空间。

这里就可以看出系统默认生成的拷贝构造函数完成的是浅拷贝,或者说是值拷贝

因为 arr 只是一个指针变量,它的值是指向的空间的首地址,所以 st2.arr 是直接得到了 st1.arr 的值,所以他俩指向一块空间,实际上就是他俩之间进行了值拷贝。

很显然,这种场景下编译器默认生成的拷贝构造函数已然满足不了我们的需求,这时就要我们自己写一个拷贝构造,自己完成深拷贝。

那什么是深拷贝呢?


浅拷贝和深拷贝

前面我们已经知道,编译器默认生成的拷贝构造函数只能进行简单的浅拷贝

所谓浅拷贝,其实就是按内存存储按字节序完成拷贝

形象一点就是,假设一个四个字节的变量存放的内容是 0x11223344,,那么浅拷贝就会把它存放的内容依次复制过去,拷贝后的结果也是 0x11223344。如果这块空间存放的是地址,那么拷贝到的也是同样的地址,这就会导致有两个指针指向同一块地址,这并不符合我们想要的拷贝效果。

我们想要的拷贝效果是拷贝构造的对象指向一块与源对象不同的空间,但空间大小和存放的内容都相同

这其实就是所谓的深拷贝

知道了想要的功能,下面就去实现它。

还是以我们自己写的 Stack 类为主:

class Stack {public:Stack(int k = 4) :_arr(new int[k]),_size(0),_capacity(k){}Stack(const Stack& st):_arr(new int[st.capacity()]),_capacity(st.capacity()),_size(st.size()){memcpy(_arr, st._arr, st.size() * sizeof(int));}~Stack() {delete[] _arr;_arr = nullptr;_size = _capacity = 0;}int size() const {return this->_size;}int capacity() const {return this->_capacity;}private:int* _arr;int _size;int _capacity;
};int main() {Stack st1;Stack st2(st1);return 0;
}

运行一下:

可见 st1._arrst2.arr 确确实实指向了两块空间,这里并没有存放数据,所以没看出来数据的拷贝,不过无伤大雅~

总结一下,上面我们简单认识了一下浅拷贝和深拷贝。编译器自动生成的默认拷贝构造函数只能帮我们完成浅拷贝,所以当没有深拷贝需求时是可以不用我们自己写拷贝构造函数的。但当成员变量有指针且指向一块空间,需要拷贝构造的对象需要指向另外一块空间时,就需要我们自己写拷贝构造完成深拷贝了。


编译器可能的优化

我们下面要讨论的是自定义类型做函数返回值时传值返回自定义类型做函数参数传值调用的场景。

自定义类型做函数返回值时传值返回

首先明确一点,传值返回返回的并不是函数栈帧里的对象,而是临时拷贝出了一个对象,而且这个临时拷贝的对象是常量性质的,这个在后面会验证。

可以看下面一段代码:

class A {public:A(int a = 0) {cout << "A()" << endl;}A(const A& aa) {cout << "A(A& aa)" << endl;}private:int _a;
};A f1() {static A aa;return aa;
}int main() {f1();return 0;
}

注意这里调用 f1 函数并没有用一个对象接收它的返回值,

运行结果如下:

函数体内调用了构造函数,返回时调用了一次拷贝构造函数,即使 aa 的生命周期是全局的。

这是第一点需要明确的。

其次还要明确一点,拷贝构造函数是一个已经存在的变量拷贝给一个即将创建的变量,比如下面一段代码:

A a1;
A a2 = a1;

这里调用的是拷贝构造函数而不是赋值运算符重载函数:

这是第二点需要明确的。

明确了以上两点后在第一段代码的基础上改变一下 main 函数:

int main() {A a1 = f1();return 0;
}

先分析一波:

这里 f1 返回的时候会临时拷贝一份对象返回,即使要返回的对象是 static 性质的。这是第一次调用拷贝构造函数。而这里又用返回值拷贝构造了一个新对象 a1,这是第二次拷贝构造。

那么看运行结果:

运行结果表示这里只调用了一次拷贝构造函数,与我们所分析的相悖。

而这里也就是我所想说的编译器对于同一个表达式中连续拷贝构造的优化,把两个拷贝构造优化成了一个。

但这个优化并不是所有编译器都支持的,一般新版的编译器(比如VS2019…)会做这样的优化,因为这种优化并不是C++标准所规定的,所以做不做就取决于开发者了。

上面我们还提到了函数返回时临时拷贝出来的对象是常量性质的,这一点用下面的代码去验证:

class A {public:A(A& aa) {cout << "A(A& aa)" << endl;}private:int _a;
};A f1() {static A aa;return aa;
}int main() {A a1 = f1();return 0;
}

注意,这里只写了一个拷贝构造函数,它的参数类型不是 const ,这时程序是跑不动的:

原因就在于拷贝出来的临时对象是常量性质的,拷贝构造 a1 时需要把这个常量性质的临时对象作为参数传给拷贝构造函数,实参是 const 类型,虚参是普通变量,传一下参操作权限就放大了,显然是错误的,所以这里会报错,也印证了临时对象的常量性质。

但这同时也说明了一个问题,虽然编译器把两次连续拷贝优化成了一次,但我们写代码分析时还是要按着两次拷贝的逻辑来,不然上面这段代码也不会出错了。

这里多提一嘴,对于函数返回时临时拷贝的那个常量性质的对象或变量是存在哪呢?

这里直接给结论,如果拷贝对象比较小,只有 4/8byte 时,可能依靠寄存器临时存放;如果比较大时,可能就会在上一个函数栈帧中预留出一块空间留着拷贝,比如上面可能就会在 main 函数中预留出一块存放临时拷贝对象的空间。


自定义类型做函数参数时传值调用

代码中类的定义部分就不写出来了,跟上面都是统一的。

首先看下面一段代码:

void f2(A aa)
{}int main() {A a1;f2(a1);return 0;
}

首先分析一下,先是调用构造函数创建了一个对象 a1 ,然后函数传参时由于是传值调用,所以传过去的是实参的一份拷贝,所以这里会临时拷贝出来一个对象,所以还会调用一次拷贝构造函数。

程序运行结果也可以验证这一点:

先引入一种新的对象,因为后面需要用。

当我们创建一个对象变量时一般是这样 A aa;

但还有一种方式就是 A()

我们称之为匿名对象它的生命周期只在这一行,可以看下面的代码验证一下:

有了匿名对象的概念我们接着讨论。

上面函数传参传过去一个已经存在的对象时需要对实参进行临时拷贝,而如果我这样做呢:

void f2(A aa)
{}int main() {f2(A());return 0;
}

按照正常的逻辑,应该会调用一次构造函数创建匿名对象,然后再调用拷贝构造函数对实参进行临时拷贝。

那么看运行结果:

这里编译器只调用了一次构造函数,并没有进行拷贝,这也是编译器进行优化的一点。


总结一下

在同一个表达式中,如果产生了临时对象,再用临时对象去拷贝构造一个对象,那么编译器可能会优化,两个对象合二为一,直接构造出一个对象。

-优化一定发生在一个表达式中的连续步骤,连续步骤可以是连续拷贝构造,也可以是一次构造+一次拷贝构造。而且优化掉的都是临时对象,或者是匿名对象。

注意,上面说的是可能优化,只有部分比较新的编译器支持这种操作。

当然,无论再怎么优化,传值调用或传值返回总是避免不了临时拷贝,所以当能传引用的时候还是要传引用,尽量避免传值。

【C++】类和对象——拷贝构造函数相关推荐

  1. C++ 笔记(17)— 类和对象(构造函数、析构函数、拷贝构造函数)

    1. 构造函数 构造函数是一种特殊的函数(方法),在根据类创建对象时被调用.构造函数是一种随着对象创建而自动被调用的函数,它的主要用途是为对象作初始化. 构造函数的名称与类的名称是完全相同的,并且不会 ...

  2. C++类中的拷贝构造函数

    文章目录 前言 二.拷贝构造函数 1.拷贝构造函数定义 2.默认拷贝构造函数存在的问题 3.解决方式 三 总结 四 参考 如有错误,希望大家批评指正,日拱一卒,功不唐捐. 前言 最近项目的技术栈是C+ ...

  3. C++编程入门系列之十四(类与对象:构造函数和析构函数)

    C++编程入门系列之十四(类与对象:构造函数和析构函数) 鸡啄米上一节中给大家讲解了类的声明.成员的访问控制和对象,今天鸡啄米给大家讲C++编程入门时同样必须掌握的构造函数和析构函数.从上一讲开始已经 ...

  4. 实现数组类(C++ 拷贝构造函数、拷贝函数)要判断赋值左右对象不相等,坑惨了...

    #include <iostream> using namespace std; class ArrayIndexOutOfBoundsException{ // 异常类 public:i ...

  5. 初入C++(二)类和对象,构造函数,析构函数

    1.c++类的声明和定义. class student { public: char *name; int age; float score; void say(); }; void student ...

  6. day16 初识面向对象编程(类与对象、构造函数与对象,类与类)

    目录 一.面向对象和面向过程的区别 二.类与对象的概念 三.构造函数和对象的关系 三.类与类的关系:组合 四.类与类的关系:依赖 一.面向对象和面向过程的区别 面向过程:强调过程步骤 面向对象:强调对 ...

  7. 类与对象(中) 构造函数和析构函数

    目录 一.类的6个默认成员函数 二.构造函数 2.1定义 2.2特性 1. 函数名和类名相同: 2. 没有返回值: 3.对象实例化的时候编译器自动调用: 4.可以函数重载. 2.3 无参的构造函数和全 ...

  8. C++ 课程作业 类与对象2——构造函数、拷贝构造函数、析构函数(油桶题)

    首先题目要求 某工厂使用一种圆柱形的铁桶来运输色拉油,但是关于该油桶的容量已经模糊,现在工人们测得了油桶直径和高(由键盘输入),请帮工人们计算一下油桶容量和制造该油桶需要的铁皮面积. 注意这个油桶是有 ...

  9. 3-6:类与对象下篇——构造函数中的初始化列表、匿名对象和explicit关键字

    文章目录 (1)真的是初始化吗 (2)初始化列表 A:概述 B:哪些成员必须在初始化列表进行初始化 (3)注意 (4)匿名对象 (5)explicit关键字 (1)真的是初始化吗 前文中,我们使用构造 ...

最新文章

  1. 怎么远程虚拟机中的mysql_如何从本地远程访问虚拟机内的Mysql服务器?
  2. {转} Eclipse 高亮显示选中的相同变量
  3. python模型训练效果没有优化_LSTM模型训练效果好,但测试结果较差,不能看出拟合过度...
  4. SM37 job状态意义
  5. pycharm 波浪线报绿 Typo: In word 'xxxx'(绿色波浪线)
  6. 三维重建:深度传感技术的中外差异
  7. [转载] python类运算符的重载
  8. Spring Cloud微服务系列-Eureka Client源码解析(二)
  9. 数据可视化的基础知识·翻译完成
  10. PixelFormat 枚举
  11. 《计算机系统:系统架构与操作系统的高度集成》——3.2 处理器实现涉及什么...
  12. ZENG msgbox仿qq提示
  13. oracle中主键自增长,Oracle中给主键设置自增长
  14. 2021教资高中信息技术笔记知识点48页pdf
  15. 10-特质-Scala
  16. 计算机考研时间科目,2018年考研初试各科目时间安排清单
  17. 自用房屋租住管理系统
  18. Github 学习 (整理自http://stormzhang.com/github/2016/06/04/learn-github-from-zero4/ 张哥学Git)
  19. 为什么GEMM是深度学习的核心
  20. 计算机丢失softwareinspect,logo1 systemInspect山泉查不到。。。和顶的又不一样[求助】...

热门文章

  1. 2017西安php行业工资,西安2020冬季平均工资出炉!基金、IT服务这些行业薪资高...
  2. jQuery元素列表遍历方法
  3. 手把手教处理串口数据
  4. 泰戈尔诗集-飞鸟集单词释义
  5. BES LINEIN 讲解
  6. Mysql下更改数据库密码
  7. Enhancer是啥
  8. 数据窗口控件的函数Describe()
  9. 大连民族学院计算机科学与工程学院,赵丕锡 - 大连民族大学 - 计算机科学与工程学院...
  10. 怎样识别图片中的文字?手机电脑都可以的