C++知识点37——拷贝构造函数

2023-10-27

无论是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》

 

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++知识点37——拷贝构造函数 的相关文章

  • 如何获取正在访问 ASP.NET 应用程序的当前用户?

    为了获取系统中当前登录的用户 我使用以下代码 string opl System Security Principal WindowsIdentity GetCurrent Name ToString 我正在开发一个 ASP NET 应用程
  • C 编程 - 文件 - fwrite

    我有一个关于编程和文件的问题 while current NULL if current gt Id Doctor 0 current current gt next id doc current gt Id Doctor if curre
  • 我如何才能等待多个事情

    我正在使用 C 11 和 stl 线程编写一个线程安全队列 WaitAndPop 方法当前如下所示 我希望能够将一些内容传递给 WaitAndPop 来指示调用线程是否已被要求停止 如果 WaitAndPop 等待并返回队列的元素 则应返回
  • 通过 CMIS (dotCMIS) 连接到 SP2010:异常未经授权

    我正在使用 dotCMIS 并且想要简单连接到我的 SP2010 服务器 我尝试用 C 来做到这一点 如下所示http chemistry apache org dotnet getting started with dotcmis htm
  • “构建”构建我的项目,“构建解决方案”则不构建

    我刚刚开始使用VS2010 我有一个较大的解决方案 已从 VS2008 成功迁移 我已将一个名为 Test 的控制台应用程序项目添加到解决方案中 选择构建 gt 构建解决方案不编译新项目 选择构建 gt 构建测试确实构建了项目 在失败的情况
  • 在哪里可以找到列出 SSE 内在函数操作的官方参考资料?

    是否有官方参考列出了 GCC 的 SSE 内部函数的操作 即 头文件中的函数 除了 Intel 的 vol 2 PDF 手册外 还有一个在线内在指南 https www intel com content www us en docs in
  • 如何使用 ICU 解析汉字数字字符?

    我正在编写一个使用 ICU 来解析由汉字数字字符组成的 Unicode 字符串的函数 并希望返回该字符串的整数值 五 gt 5 三十一 gt 31 五千九百七十二 gt 5972 我将区域设置设置为 Locale getJapan 并使用
  • OleDbDataAdapter 未填充所有行

    嘿 我正在使用 DataAdapter 读取 Excel 文件并用该数据填充数据表 这是我的查询和连接字符串 private string Query SELECT FROM Sheet1 private string ConnectStr
  • 堆栈溢出:堆栈空间中重复的临时分配?

    struct MemBlock char mem 1024 MemBlock operator const MemBlock b const return MemBlock global void foo int step 0 if ste
  • 使用 WebClient 时出现 System.Net.WebException:无法创建 SSL/TLS 安全通道

    当我执行以下代码时 System Net ServicePointManager ServerCertificateValidationCallback sender certificate chain errors gt return t
  • 重载<<的返回值

    include
  • 如何在整个 ASP .NET MVC 应用程序中需要授权

    我创建的应用程序中 除了启用登录的操作之外的每个操作都应该超出未登录用户的限制 我应该添加 Authorize 每个班级标题前的注释 像这儿 namespace WebApplication2 Controllers Authorize p
  • 如何在 C 中调用采用匿名结构的函数?

    如何在 C 中调用采用匿名结构的函数 比如这个函数 void func struct int x p printf i n p x 当提供原型的函数声明在范围内时 调用该函数的参数必须具有与原型中声明的类型兼容的类型 其中 兼容 具有标准定
  • 这些作业之间是否存在顺序点?

    以下代码中的两个赋值之间是否存在序列点 f f x 1 1 x 2 不 没有 在这种情况下 标准确实是含糊不清的 如果你想确认这一点 gcc 有这个非常酷的选项 Wsequence point在这种情况下 它会警告您该操作可能未定义
  • 通过指向其基址的指针删除 POD 对象是否安全?

    事实上 我正在考虑那些微不足道的可破坏物体 而不仅仅是POD http en wikipedia org wiki Plain old data structure 我不确定 POD 是否可以有基类 当我读到这个解释时is triviall
  • cmake 将标头包含到每个源文件中

    其实我有一个简单的问题 但找不到答案 也许你可以给我指一个副本 所以 问题是 是否可以告诉 cmake 指示编译器在每个源文件的开头自动包含一些头文件 这样就不需要放置 include foo h 了 谢谢 CMake 没有针对此特定用例的
  • 如何在Xamarin中删除ViewTreeObserver?

    假设我需要获取并设置视图的高度 在 Android 中 众所周知 只有在绘制视图之后才能获取视图高度 如果您使用 Java 有很多答案 最著名的方法之一如下 取自这个答案 https stackoverflow com a 24035591
  • 将控制台重定向到 .NET 程序中的字符串

    如何重定向写入控制台的任何内容以写入字符串 对于您自己的流程 Console SetOut http msdn microsoft com en us library system console setout aspx并将其重定向到构建在
  • 基于 OpenCV 边缘的物体检测 C++

    我有一个应用程序 我必须检测场景中某些项目的存在 这些项目可以旋转并稍微缩放 更大或更小 我尝试过使用关键点检测器 但它们不够快且不够准确 因此 我决定首先使用 Canny 或更快的边缘检测算法 检测模板和搜索区域中的边缘 然后匹配边缘以查
  • 如何在文本框中插入图像

    有没有办法在文本框中插入图像 我正在开发一个聊天应用程序 我想用图标图像更改值 等 但我找不到如何在文本框中插入图像 Thanks 如果您使用 RichTextBox 进行聊天 请查看Paste http msdn microsoft co

随机推荐

  • Hexo 完整使用教程

    官网 官网地址 https hexo io zh cn 环境 1 node hexo 基于 node 所以首先要安装node环境 2 npm 包管理工具 环境配置请参考本站对应安装教程 快速开始 安装 hexo npm install g
  • Mysql数据库内联查询、左连接查询、右连接查询、自连接查询介绍

    目录 一 内联查询 1 inner join 只查询键值一致 交叉 的部分 2 演示 二 左连接 1 left join 以左表为标准 查询输出左表中没有的字段信息 2 演示 三 右连接 1 right join 以右表为标准 查询输出右表
  • 区块如何防篡改_一种区块链防篡改技术的优化方法与流程

    本发明涉及区块链技术领域 具体涉及一种区块链防篡改技术的优化方法 背景技术 区块链是比特币等数字虚拟货币的底层技术 通过去中心化的数据记录 由全网所有的节点共同维护数据 实现安全地存储数据 具有不可伪造性 不可篡改性 可追溯性 匿名性等特点
  • Java编程练习之:水仙花数

    文章目录 1 题目 2 思路 3 代码 4 运行结果 1 题目 打印出所有的 水仙花数 所谓 水仙花数 是指一个三位数 其各位数字立方和等于该数本身 例如 153是一个 水仙花数 因为153 1的三次方 5的三次方 3的三次方 2 思路 这
  • 线程池的简介说明

    在多线程应用程序开发中 如果我们不使用线程池 则每次创建和销毁线程将会消耗宝贵的CPU 内存资源 所以我们必须创建一个线程池 线程池的功能 线程池用于管理线程 用于减少系统资源消耗 创建一个线程池 实现思路 借助线程池类Executor 借
  • Java Map集合 体系

    1 Collection集合 1 1 常用集合的体系 mermaid svg dmg6k5CugOsij3Ax label font family trebuchet ms verdana arial font family var mer
  • openGL之API学习(九十四)几何着色器的几个参数设置含义

    设定输入几何图元的类型 比如GL TRIANGLES glProgramParameteriEXT program GL GEOMETRY INPUT TYPE EXT inputGeometryType 设定输出几何图元的类型 比如GL
  • Leetcode之KMP字符串算法

    针对题目28题 实现strStr 功能找出needle在haystack字符串的第一个位置 否则返回 1 当然有暴力法 但是时间复杂度是O mn 而KMP算法提前计算出needle字符串的重复数据加以利用 j能够有效的回退到可能的位置 时间
  • 树干树叶点云分类

    将扫描的树木点云的树干树枝和树叶分类出来 后续放出程序代码
  • Cluster & Docker

    操作系统解决主要问题之一就是如何在一台机器上调度硬件资源 比如为一个进程分配cpu gpu 存储资源等 使得进程可以高效 按照预期地完成运行 云和互联网解决的主要问题之一是如何在多台机器之间调度硬件资源or调度信息资源 这就产生了更丰富架构
  • SpringCloud最新版环境集成-2021年11月

    总述 之前搞过SpringCloud项目 但版本是netflix维护的伦敦地铁站名称的版本 现在想做一下笔记 并尝试最新版本 看有什么变动没 结果还真有一堆坑 此项目集成了eureka feign ribbon hystrix zuul五大
  • 在.Net 6项目中设置MySql数据库的连接配置文件appsettings.json及在Program.cs中读取配置文件并向DI容器注册服务。

    1 配置一 配置好ConnectionStrings 通过ConnectionString动态读取ServerVersion 推荐使用 appsettings json ConnectionStrings MoviesDBContext s
  • MFC + 自定义类的序列化与反序列化

    使类可序列化需要五个主要步骤 从 CObject 派生类 或从 CObject 派生的某个类中派生 重写 Serialize 成员函数 使用 DECLARE SERIAL 宏 在类声明中 定义不带参数的构造函数 为类在实现文件中使用 IMP
  • SQL 删除表数据行与重置自动增长字段

    delete 语句 delete from tablename 表名 where ID 列名 x 表示删除数据表中ID列的值为x的数据行 在ID列设置了自动增长主键列的情况下 这种删除方式将会保留该数据行所占用的自动增长值 此后添加数据时此
  • Linux内嵌链表(sys/queue.h)详解

    Linux 内嵌链表 sys queue h 详解 queue 简介 SLIST STAILQ LIST TAILQ CIRCLEQ 例程 queue 简介 C语言不像C 语言可以使用STL 在编程中需要用到链表时 通常需要程序员重新设计链
  • java中数据库重连

    当数据库重新启动 而导致程序无法连接 需要重启tomcat才能重连的解决办法 方法一 将连接池由DBCP改为C3P0 c3p0连接池本身具有数据库重连机制
  • 传统的目标检测算法

    1 基于滑动窗口的目标检测算法 滑动窗口 gt gt 特征提取 gt gt 分类器 图 滑动窗口目标检测流程 对输入的图像设置不同大小的滑窗 确定步长遍历整个图像 每次滑动完成后对当前选择框进行特征提取 SIFT HOG等 并使用事先训练好
  • python+opencv学习之路(一 )学习打开图片

    python opencv学习之路 一 学习打开图片 载入opencv和numpy模块 import cv2 import numpy as np 读取图片 img cv2 imread 1 jpg cv2 IMREAD COLOR 1 j
  • Android学习路线:如何成长为高级工程师

    博主参加了2014 CSDN博客之星评选 帮我投一票吧 点击给我投票 前言 之所以写这篇文章 是因为最近博客和我的开发群 215680213 中 不少小伙伴都让我讲讲android学习路线 所以我决定写一篇博客 来说明这个问题 既然有不少小
  • C++知识点37——拷贝构造函数

    无论是C 自定义的类还是STL内部的容器类 会显式的定义类的对象在拷贝 赋值和销毁时执行的操作 一个类通过五个成员函数来控制这些操作 拷贝构造函数 拷贝赋值运算符 移动构造函数 移动赋值运算符和析构函数 其中 拷贝构造函数和移动构造函数定义