无论是C++自定义的类还是STL内部的容器类,会显式的定义类的对象在拷贝、赋值和销毁时执行的操作,一个类通过五个成员函数来控制这些操作:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。其中,拷贝构造函数和移动构造函数定义了当用相同类型的一个对象初始化另一个对象时的操作,拷贝赋值运算符和移动赋值运算符定义类用一个对象给另一个相同类型的对象赋值时的操作,析构函数定义了类的对象被销毁时的操作。

上述五个成员函数如果没有自定义、那么,编译器将会在必要的时候自动定义这些操作,但是编译器定义的版本可能并不是实际想要的

本文先介绍拷贝构造函数,拷贝赋值运算符和析构函数后面再说

一、拷贝构造函数

1、定义

在之前一系列的STL的相关的文章中,已经多多少少接触到了拷贝构造函数。拷贝构造函数也是构造函数,只不过拷贝构造函数的参数是所在类的类型的引用,且其他参数都有默认值

示例

class test
{
public:test(const test &t) {cout<<__func__<<endl;}~test() {cout<<__func__<<endl;}
};

其中test(const test &t)就是拷贝构造函数,也可以把const去掉,但是const一般都会加上,好处见博客https://blog.csdn.net/Master_Cui/article/details/106389112

注意:因为拷贝构造函数也是构造函数,只不过参数类型比较特别,如果只定义了拷贝构造函数,那么编译器就不会提供默认构造函数了

示例

int main(int argc, char const *argv[])
{test t;return 0;
}

当创建一个test的对象时,编译器提示找不到默认构造函数,所以,定义拷贝构造函数时,一定要定义默认构造函数

2.拷贝构造函数的调用时机

时机1:在拷贝初始化一个非引用参数时,会调用拷贝构造函数

时机2:当一个函数的形参是非引用类型时,初始化形参的时候会调用拷贝构造函数(包括使用容器的insert或者push_back或者push操作)

时机3:当一个函数用返回值初始化一个非引用类型的对象时,会调用拷贝构造函数

时机4:直接创建对象时,会调用拷贝构造函数(这点和直接初始化相同,类的对象直接初始化实际上是匹配对应的构造函数,可是此时能匹配的只有拷贝构造函数,所以,依然会调用拷贝构造函数)

示例

class test
{
public:test() {cout<<__func__<<endl;}test(const test &t) {cout<<"test(const test &t)"<<endl;}~test() {cout<<__func__<<endl;}void func1(test t) {cout<<__func__<<endl;}test func2() {cout<<__func__<<endl;test t; return t;}
};int main(int argc, char const *argv[])
{test t, m;cout<<"---------"<<endl;test t2=t;cout<<"---------"<<endl;m.func1(t);cout<<"---------"<<endl;test t3=m.func2();cout<<"---------"<<endl;test t4(m);return 0;
}

上述代码开始创建两个test对象,两次构造函数,之后test t2=t;,执行一次拷贝构造,接着m.func1(t);,初始化形参时,会调用拷贝构造函数,当函数结束时,形参的作用域结束,销毁形参,调用析构函数;然后调用test t3=m.func2();,在func2中,创建一个对象,调用构造函数,接着函数返回时,创建一个临时test对象并用func2的返回值初始化,调用了一次拷贝构造函数;func2作用域结束,局部对象t被销毁,调用析构函数,用生成的临时变量初始化另一个对象test t3,又调用一次拷贝构造函数,临时变量销毁,又调用了一次析构函数;紧接着执行test t4(m);,调用对应的构造函数,发现只有拷贝构造函数的参数符合要求,所以还得再调用一次拷贝构造函数,最后main函数执行结束,将五个test对象释放,按照逆序t4,t3,t2,m,t销毁对象,调用了五次析构函数

和博客https://blog.csdn.net/Master_Cui/article/details/109289758中描述的情况相同

3.拷贝构造函数的参数必须是类的类型的引用的原因

通过拷贝构造函数的调用时机我们可以知道,拷贝构造函数的参数必须是引用,因为如果不是引用,那么在拷贝初始化一个非引用参数时,会调用类似于这样的拷贝构造函数test(const test t);但是将右值传入拷贝构造函数后,满足了调用时机2,又得调用拷贝构造函数test(const test t),然后又满足了调用时机2,又得调用拷贝构造函数test(const test t),这样就会不停的重复调用拷贝构造函数,陷入无线递归中,使程序不能正常运行,所以,拷贝构造函数的参数必须是类的类型的引用。

4.合成的拷贝构造函数

如果一个类中没有定义拷贝构造函数,那么编译器会自动生成。与默认构造函数不同的是:即使定义了其他构造函数,如果没有定义拷贝构造函数,编译器依然会自动生成。合成的拷贝构造函数一般会将参数的非static成员逐个拷贝到创建的对象中。

示例

class test
{
public:test() {cout<<__func__<<endl;}test(const test &t) {cout<<"test(const test &t)"<<endl;}~test() {cout<<__func__<<endl;}void func1(test t) {cout<<__func__<<endl;}test func2() {cout<<__func__<<endl;test t; return t;}
};class test1
{
public:test1():a(0),na{1,2}{cout<<__func__<<endl;}~test1(){cout<<__func__<<endl;}int a;int na[5];test nt[3];test t;};int main(int argc, char const *argv[])
{test1 t1;test1 t2=t1;cout<<t2.a<<endl;for (auto c:t2.na) {cout<<c<<endl;}return 0;
}

通过代码可知:虽然test1对象中没有拷贝构造函数,但是编译器会自动生成并执行拷贝动作。在创建test1时,会先创建test以及test数组,所以先打印出四次test的log,之后创建一个test1对象t2,并进行拷贝初始化,将t1中的int以及int、test数组以及test对象拷贝到t2中,所以又会调用4次test的拷贝构造函数,接着打印出t2中的int数据和数组,最后主函数退出,逆序销毁对象

虽然不能用一个数组初始化另一个数组,但是拷贝构造函数会将数组中的元素逐个拷贝到创建对象中的数组中,如果数组元素是自定义的类类型,那么还会调用拷贝构造函数来

5.拷贝构造函数与explicit

如果一个类的普通构造函数是explicit的,那么在进行拷贝构造函数的时候,不能出现隐式转换

示例

class test3
{
public:test3() {cout<<__func__<<endl;}explicit test3(int a) {cout<<"test3(int a)"<<endl;}~test3() {cout<<__func__<<endl;}
};class test4
{
public:test4() {cout<<__func__<<endl;}test4(int a) {cout<<"test4(int a)"<<endl;}~test4() {cout<<__func__<<endl;}
};int main(int argc, char const *argv[])
{//test3 t3=10;test4 t4=10;return 0;
}

因为test4的构造函数不是explicit的,所以,在用10对一个test4对象进行初始化时,会先调用对应的构造函数将10隐式转换为一个test4对象,然后在进行拷贝初始化,主函数退出后,释放t4和那个隐式转换的test4对象。

但是因为test3的拷贝构造函数是explicit的,所以不允许隐式转换,所以第19行代码需要注释掉

6.拷贝构造函数与指针

如果一个类中的成员没有指针,那么用编译器自动生成的拷贝构造函数一般没啥问题,但是,如果类的成员中含有指针,那么使用编译器合成的拷贝构造函数就有可能出问题

示例

class hasptr
{
public:hasptr(const string &s);~hasptr();string *ps;int i;
};hasptr::hasptr(const string &s):ps(new string(s)),i(0)
{cout<<__func__<<endl;
}hasptr::~hasptr()
{if (ps!=nullptr) {cout<<__func__<<endl;delete ps;ps=nullptr;}
}int main(int argc, char const *argv[])
{hasptr hp("1234");hasptr hp1=hp;cout<<*hp.ps<<endl;return 0;
}

上述代码之所以出现段错误是因为当创建hp1时,使用的是hp进行拷贝初始化,此时hp的ps成员和hp2的ps成员都指向了同一个string对象,当任意一个对象的ps成员的被delete后,其他对象的ps成员就会指向一块无效的内存,而此时对ps进行解引用或者再次delete,就会解引用一个已经被delete的指针或者对一个指针二次delete(危!!!),所以出现段错误

解决办法有两个:

1、使用智能指针shared_ptr替代普通指针

通过shared_ptr的引用计数功能防止string对象被销毁

修该后的代码

class hasptr
{
public:hasptr(const string &s);~hasptr();shared_ptr<string> sps;int i;
};hasptr::hasptr(const string &s):sps(make_shared<string>(s)),i(0)
{cout<<__func__<<endl;
}hasptr::~hasptr()
{cout<<__func__<<endl;
}int main(int argc, char const *argv[])
{hasptr hp("1234");hasptr hp1=hp;hp.sps.reset();cout<<*hp1.sps<<endl;return 0;
}

当用hp初始化hp1时,二者内部的sps对象也被拷贝初始化,此时sps的引用计数为2,当调用reset时,引用计数为1,string对象没有被销毁。所以依然可以正常的访问string

关于智能指针见博客https://blog.csdn.net/Master_Cui/article/details/109147470 https://blog.csdn.net/Master_Cui/article/details/109264151 https://blog.csdn.net/Master_Cui/article/details/109289758

2、自己实现hasptr的拷贝构造函数

class hasptr
{
public:hasptr(const string &s);hasptr(const hasptr &t);~hasptr();string *ps;int i;
};hasptr::hasptr(const string &s):ps(new string(s)),i(0)
{cout<<__func__<<endl;
}hasptr::hasptr(const hasptr &t)
{cout<<"hasptr(const hasptr &t)"<<endl;this->ps=new string(*t.ps);this->i=t.i;
}hasptr::~hasptr()
{cout<<__func__<<endl;if (ps) {delete ps;ps=nullptr;}
}int main(int argc, char const *argv[])
{hasptr hp("1234");hasptr hp1=hp;cout<<*hp.ps<<endl;return 0;
}

在重新实现拷贝构造函数时,ps重新指向的了一个新的string对象,只不过这个string对象的值是ps指向的值,所以,hp和hp1的ps成员分别指向两个不同的对象,而不再指向同一个对象,所以delete其中一个对另一个没有影响

上述两种方式推荐用第一种,有了智能指针之后,程序中最好不要出现普通指针。如果类的成员中一定要存在普通指针,那么请一定要自己实现拷贝构造函数

参考

《C++ Primer》

欢迎大家评论交流,作者水平有限,如有错误,欢迎指出

C++知识点37——拷贝构造函数相关推荐

  1. 拷贝构造函数和赋值函数的一些知识

    /*******************拷贝构造函数和赋值运算符重载有以下两个不同之处***************************/ 1.拷贝构造函数生成新的类对象,而赋值运算符不能. 2. ...

  2. 【C++ Primer 第15章】定义派生类拷贝构造函数、赋值运算符

    学习资料 • 派生类的赋值运算符/赋值构造函数也必须处理它的基类成员的赋值 • C++ 基类构造函数带参数的继承方式及派生类的初始化 定义拷贝构造函数 [注意]对派生类进行拷贝构造时,如果想让基类的成 ...

  3. C++:构造函数2——拷贝构造函数

    前言:拷贝构造函数是C++中的重点之一,在这里对其知识进行一个简单的总结. 一.什么是拷贝构造函数 在C++中,对于内置类型的变量来说,在其创建的过程中用同类型的另一个变量来初始化它是完全可以的,如: ...

  4. 理解C++中拷贝构造函数

    拷贝构造函数的功能是用一个已有的对象来初始化一个被创建的同样对象,是一种特殊的构造函数,具有一般构造函数的所有特性,当创建一个新对象的时候系统会自动调用它:其形参是本类对象的引用,它的特殊功能是将参数 ...

  5. 【转】拷贝构造函数的参数类型必须是引用

    在C++中, 构造函数,拷贝构造函数,析构函数和赋值函数(赋值运算符重载)是最基本不过的需要掌握的知识. 但是如果我问你"拷贝构造函数的参数为什么必须使用引用类型?"这个问题, 你 ...

  6. 深入C++中构造函数、拷贝构造函数、赋值操作符、析构函数的调用过程总结

    转自 http://www.jb51.net/article/37527.htm,感谢作者 #include "stdafx.h"       #include <iostr ...

  7. 备忘录_C++_拷贝构造函数

    脚踏实地,仰望星空 目录视图 摘要视图 订阅 程序员必须要学会算法吗     博客专家庄晓立:我为什么要选择Rust?     从零练就iOS高手实战班震撼来袭     新型数据库利弊谈     C+ ...

  8. 构造函数的调用场景--构造函数与拷贝构造函数、移动构造函数之辨

    在Scott Meyers的著作<Effective C++>条款5中,明确指出要"了解C++默默编写并调用哪些函数".这里通过一个简短的C++代码,阐述哪些场景调用构 ...

  9. C++ 复制构造函数或者拷贝构造函数

    复制构造函数 是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象. 复制构造函数通常用于: 通过使用另一个同类型的对象来初始化新创建的对象. 复制对象把它作为参数传 ...

最新文章

  1. Autodesk MapGuide Enterprise 2011 Update 1 for Windows发布了
  2. photoshop切片的取消操作
  3. 计算机架构及开机过程
  4. 《机器学习》 周志华学习笔记第三章 线性模型(课后习题)python 实现
  5. Association, Composition and Aggregation in UI5, CRM, S/4HANA and C4C
  6. bch怎么挖_BCH是什么?
  7. oracle导入报错39002,oracle impdp ORA-39002
  8. 7时过2小时是几时_2017最北师大版二年级下册数学第七单元《时、分、秒》过关检测卷...
  9. 线上分享|云和恩墨大讲堂201902:MySQL基础之体系结构
  10. Python继承范例
  11. UE4 碰撞射线检测
  12. 电子计算机奏出美妙的交响改为把字句,部编版四年级语文上册(课文内容填空+句子专练含答案).doc...
  13. Excel图表制作(一):商务图表之甘特图
  14. 做IT民工还是IT精英?
  15. ibm服务器硬盘raid检测,IBM 3650 服务器做的RAID5,两块硬盘亮黄灯,但是系统正常,更换...
  16. 3篇SCI二区认定A类博士!送120㎡住房+78万引进费+4500/月博士津贴!5年副教授待遇!...
  17. Unity3D播放ogv格式的视频
  18. 前端、后台、客户端以及服务器
  19. 云知声持续发挥企业优势,赋能AIoT 落地,让未来生活更进一步
  20. R安装与卸载、RStudio安装

热门文章

  1. DateTools使用「日期工具库」
  2. 高可用集群技术之heartbeat应用详解(一)
  3. Open***异地机房互连以及负载均衡高可用解决方案
  4. Redefine:Change in the Changing World
  5. 应用虚拟化技术的五大理由
  6. 关于可管理交换机VLAN的四种划分
  7. 什么是ERP (转载自百度知道)
  8. 基于SSM实现在线课程学习及作业提交系统
  9. 非平衡数据处理方式与评估
  10. 将Windows下的InfluxDB、Grafana做成Windows服务