类的函数成员(三):拷贝构造函数

一.什么是拷贝构造函数?

1.1 概念

        同一个类的对象在内存中有完全相同的结构,如果作为一个整体进行复制或称拷贝是完全可行的。这个拷贝过程只需要拷贝数据成员,而函数成员是共用的(只有一份拷贝)。
        在建立对象时可用同一类的另一个对象来初始化该对象,这时所用的构造函数称为拷贝构造函数( Copy Constructor)。

        拷贝构造函数的参数必须采用引用类型,但并不限制为const,一般普遍的会加上const限制。如果以类对象作为参数传递到拷贝构造函数,会引起无穷递归。

1.2 代码示例

        代码示例如下:

#include <iostream>

using namespace std;

class CStudent
{
public:
	CStudent(int age = 0,int score = 0);
	~CStudent();
	
	//拷贝构造函数 
	CStudent(const CStudent &stu);
	
private:
	int age;
	int score;
};

CStudent::CStudent(int age,int score)
{
	cout<<"Constructor!"<<endl;
	this->age = age;
	this->score = score;
}

CStudent::~CStudent()
{
	cout<<"Desconstructor!"<<endl;
}


CStudent::CStudent(const CStudent &stu)
{
	cout<<"Copy constuctor!"<<endl;
	this->age = stu.age;
	this->score = stu.score;
}

二.如何实现?

2.1 缺省拷贝构造函数

2.1.1 概念

        如果类中没有给出定义,系统会自动提供缺省拷贝构造函数。

        缺省的拷贝构造函数会按成员语义,依次拷贝每个类成员,亦称为缺省的按成员初始化。

        按成员作拷贝是通过依次拷贝每个数据成员实现的,而不是对整个类对象按位拷贝。

2.1.2 代码示例

        示例代码如下:

#include <iostream>

using namespace std;

class CStudent
{
public:
	CStudent(int age = 0,int score = 0);
	~CStudent();
	
	void print_info(void);
	
private:
	int age;
	int score;
};

CStudent::CStudent(int age,int score)
{
	cout<<"Constructor! "<<this<<endl;
	this->age = age;
	this->score = score;
}

CStudent::~CStudent()
{
	cout<<"Desconstructor! "<<this<<endl;
}


void CStudent::print_info(void)
{
	cout<<"age("<<this<<"): "<<age<<endl;
	cout<<"score("<<this<<"): "<<score<<endl;	
}

int main(int argc, char** argv)
{
	CStudent stu1(8,90);
	
	CStudent stu2(stu1);
	stu2.print_info();
	
	return 0;
}

        运行结果如下图所示。       

         由上图可知:

(1)只调用了一次普通构造函数,用来构造对象stu1。表明,在构造stu2时调用了一个缺省的构造函数,这个函数就是拷贝构造函数。

(2)对象stu2的所有数据成员被初始化为stu1对应数据成员的值。

(3)最后,调用了两次析构函数,用于析构stu1和stu2。

2.2 自定义拷贝构造函数

2.2.1 概念

        通常按成员语义支持已经足够。但在某些情况下,它对类与对象的安全性和处理的正确性还不够,这时就要求类的设计者提供特殊的拷贝构造函数定义。

2.2.2 代码示例

        示例代码如下:

#include <iostream>

using namespace std;

class CStudent
{
public:
	CStudent(int age = 0,int score = 0);
	~CStudent();
	
	//拷贝构造函数 
	CStudent(const CStudent &stu);

	void print_info(void);
private:
	int age;
	int score;
};

CStudent::CStudent(int age,int score)
{
	cout<<"Constructor!"<<endl;
	this->age = age;
	this->score = score;
}

CStudent::~CStudent()
{
	cout<<"Desconstructor!"<<endl;
}


CStudent::CStudent(const CStudent &stu)
{
	cout<<"Copy constuctor!"<<endl;
	this->age = stu.age;
	this->score = stu.score;
}

void CStudent::print_info(void)
{
	cout<<"age: "<<age<<endl;
	cout<<"score: "<<score<<endl;	
}

int main(int argc, char** argv)
{
	CStudent stu1(8,90);
	
	CStudent stu2(stu1);
	stu2.print_info();
	return 0;
}

        运行结果如下图所示。

        由上图可知:

(1)构造stu2对象时,调用了一次自定义的拷贝构造函数。

(2)关注一下自定义构造函数代码,发现在函数域内可通过引用对象访问私有数据成员age和score。

        从逻辑上讲,每个对象有自己的成员函数,访问同类其他对象的私有数据成员应通过该对象的公有函数,不能直接访问。但在物理上只有一个成员函数拷贝,所以直接访问是合理的。

        即,C++有个原则:类的成员函数可以访问私有数据成员。

CStudent::CStudent(const CStudent &stu)
{
	cout<<"Copy constuctor!"<<endl;
	this->age = stu.age;
	this->score = stu.score;
}

三.何时调用?

3.1 用对象初始化对象

        以下两种形式都是用已存在的对象初始化对象:

CStudent stu1(8,90);

CStudent stu2(stu1);
或者
CStudent stu2 = stu1;

        以上两种形式是等价的,只是写法上不同。

3.2 给函数传递类的对象参数

       当函数的形参是类的对象时, 一旦调用函数,要在内存新建立一个局部对象,并把实参拷贝到新的对象中。

        代码示例(部分)如下:

void func(CStudent stu)
{
	cout<<"func"<<endl;	
}

int main(int argc, char** argv)
{
	CStudent stu1(8,90);
	
	func(stu1);
	
	return 0;
}

        运行结果如下图所示。

        由上图可知。调用func函数时,会调用拷贝构造函数构造一个临时对象传给func。

3.3 函数返回类的对象(部分编译器)

        很多资料提到:如果函数的返回值是类的对象,那么函数执行完成后,返回调用者时会调用拷贝构造函数。其实这不严谨。

        有些编译器在函数返回类的对象时,不会调用拷贝构造函数。下面单独一节详细分析。

四.函数返回类的对象但不调用拷贝构造函数

        本次实验使用64位TDM-GCC 4.9.2编译器。

4.1 示例代码        

#include <iostream>

using namespace std;

class CStudent
{
public:
	CStudent(int age = 0,int score = 0);
	~CStudent();
	
	//拷贝构造函数 
	CStudent(const CStudent &stu);

	void print_info(void);
private:
	int age;
	int score;
};

CStudent::CStudent(int age,int score)
{
	cout<<"Constructor!"<<endl;
	this->age = age;
	this->score = score;
}

CStudent::~CStudent()
{
	cout<<"Desconstructor!"<<endl;
}


CStudent::CStudent(const CStudent &stu)
{
	cout<<"Copy constuctor!"<<endl;
	this->age = stu.age;
	this->score = stu.score;
}

void CStudent::print_info(void)
{
	cout<<"age: "<<age<<endl;
	cout<<"score: "<<score<<endl;	
}

CStudent func(void)
{
	CStudent tmp(11,88);
	
	return tmp;	
}

int main(int argc, char** argv)
{
	CStudent stu1(8,90);
	CStudent stu2;
	
	stu2 = func();
	stu2.print_info();
	
	return 0;
}

4.2 运行结果

        如下图所示。

        由下图可知:

(1)func函数的返回值是类的对象,但并没有调用拷贝构造函数。

(2)从stu2打印的信息来看,func函数中创建的tmp对象,的确“赋值”给了stu2。这怎么理解?下面看看汇编代码。

4.3 汇编代码

        汇编代码中r8d是指r8寄存器的低32位。

4.3.1 func函数汇编代码    

        完整的汇编代码如下:

push   %rbp
mov    %rsp,%rbp
sub    $0x20,%rsp
mov    %rcx,0x10(%rbp) //rcx存储了对象tmp的地址
mov    $0x58,%r8d   //r8d的低32位初始化为88
mov    $0xb,%edx    //edx初始化为11
mov    0x10(%rbp),%rcx //即是tmp对象地址
callq  0x401530 <CStudent::CStudent(int, int)>
nop
mov    0x10(%rbp),%rax
add    $0x20,%rsp
pop    %rbp
retq   

        如上图中的注释,func函数里的对象tmp的地址是由调用者main函数传入的,即tmp对象是在main函数的堆栈里存储,而不是在func函数的堆栈里。

4.3.2 构造函数汇编代码

           CStudent::CStudent(int, int)函数的完整汇编代码如下:

push   %rbp
mov    %rsp,%rbp
sub    $0x20,%rsp
mov    %rcx,0x10(%rbp)//rcx存储了对象tmp的地址
mov    %edx,0x18(%rbp) //初始化tmp.score的值为11
mov    %r8d,0x20(%rbp) //初始化tmp.age的值为88
lea    0x86ab6(%rip),%rdx        # 0x488000
mov    0x8b17f(%rip),%rcx        # 0x48c6d0 <.refptr._ZSt4cout>
callq  0x46ee10 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
mov    0x8b183(%rip),%rdx        # 0x48c6e0 <.refptr._ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_>
mov    %rax,%rcx
callq  0x44d500 <_ZNSolsEPFRSoS_E>
mov    0x10(%rbp),%rax
mov    0x18(%rbp),%edx
mov    %edx,(%rax)
mov    0x10(%rbp),%rax
mov    0x20(%rbp),%edx
mov    %edx,0x4(%rax)
add    $0x20,%rsp
pop    %rbp
retq   
retq  

        注意第4~5行代码的注释。构造函数里,初始化了tmp对象的数据成员。

 4.3.3 main函数汇编代码

        main函数的完整汇编代码如下:

push   %rbp
push   %rbx
sub    $0x58,%rsp
lea    0x80(%rsp),%rbp
mov    %ecx,-0x10(%rbp)
mov    %rdx,-0x8(%rbp)
callq  0x40e950 <__main>
lea    -0x50(%rbp),%rax //堆栈偏移0x50的空间,分配给对象stu1.这里rax存储了stu1的地址
mov    $0x5a,%r8d    	//r8的低32位初始化为90
mov    $0x8,%edx     	//edx寄存器初始化为8
mov    %rax,%rcx     	//传递stu1的地址给构造函数
callq  0x401530 <CStudent::CStudent(int, int)>
lea    -0x60(%rbp),%rax //堆栈偏移0x60的空间,分配给对象stu2.这里rax存储了stu2的地址
mov    $0x0,%r8d
mov    $0x0,%edx
mov    %rax,%rcx   		//传递stu2的地址给构造函数
callq  0x401530 <CStudent::CStudent(int, int)>
lea    -0x40(%rbp),%rax	//堆栈偏移0x40的空间,分配给了一个临时对象,暂时命名为m_tmp.这里rax存储了m_tmp的地址
mov    %rax,%rcx		//传递m_tmp的地址给func函数
callq  0x401685 <func()> //func函数里的tmp对象直接使用了main函数创建的m_tmp
mov    -0x40(%rbp),%rax  
mov    %rax,-0x60(%rbp)  //将m_tmp赋值给stu2
lea    -0x40(%rbp),%rax
mov    %rax,%rcx
callq  0x40157e <CStudent::~CStudent()> //析构m_tmp
lea    -0x60(%rbp),%rax
mov    %rax,%rcx
callq  0x401606 <CStudent::print_info()>
mov    $0x0,%ebx
lea    -0x60(%rbp),%rax
mov    %rax,%rcx
callq  0x40157e <CStudent::~CStudent()>
lea    -0x50(%rbp),%rax
mov    %rax,%rcx
callq  0x40157e <CStudent::~CStudent()>
mov    %ebx,%eax
jmp    0x401770 <main(int, char**)+192>
mov    %rax,%rbx
lea    -0x60(%rbp),%rax
mov    %rax,%rcx
callq  0x40157e <CStudent::~CStudent()>
jmp    0x401759 <main(int, char**)+169>
mov    %rax,%rbx
lea    -0x50(%rbp),%rax
mov    %rax,%rcx
callq  0x40157e <CStudent::~CStudent()>
mov    %rbx,%rax
mov    %rax,%rcx
callq  0x40f670 <_Unwind_Resume>
add    $0x58,%rsp
pop    %rbx
pop    %rbp
retq   

        如代码中的注释:

(1)main函数在调用func函数前,创建了一个临时对象,这里给它命名为m_tmp。

(2)m_tmp对象的地址传递给func函数,func函数里的tmp对象直接使用了m_tmp的地址。因此,可以认为,tmp就是m_tmp的别名。

(3)func函数返回后,将m_tmp对象的数据赋值给stu2对象。

(4)最后,析构m_tmp。

        所以,从始至终,没有调用过拷贝构造函数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/508078.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

实践笔记-harbor-02-仓库镜像上传与拉取

harbor仓库镜像上传与拉取 1.上传镜像修改 daemon.json 配置文件上传镜像至harbor 2.拉取镜像登录账号&#xff08;跟上传镜像那里一样操作登录步骤就可以了&#xff09;拉取镜像 环境&#xff1a;centos7 1.上传镜像 修改 daemon.json 配置文件 # 编辑daemon.json文件&#…

C语言学习 六、指针

6.1指针的定义 6.1.1指针的本质&#xff08;间接访问原理&#xff09; 内存区域中每字节对应一个编号&#xff0c;这个编号就是“地址”。如果在程序中定义了一个变量&#xff0c;那么在对程序进行编译时&#xff0c;系统会给这个变量分配内存单元。按变量存取变量值的方式称…

【御控物联】JavaScript JSON结构转换(8):数组To数组——多层属性重组

文章目录 一、JSON结构转换是什么&#xff1f;二、案例之《JSON数组 To JSON数组》三、代码实现四、在线转换工具五、技术资料 一、JSON结构转换是什么&#xff1f; JSON结构转换指的是将一个JSON对象或JSON数组按照一定规则进行重组、筛选、映射或转换&#xff0c;生成新的JS…

【项目技术介绍篇】若依项目代码文件结构介绍

作者介绍&#xff1a;本人笔名姑苏老陈&#xff0c;从事JAVA开发工作十多年了&#xff0c;带过大学刚毕业的实习生&#xff0c;也带过技术团队。最近有个朋友的表弟&#xff0c;马上要大学毕业了&#xff0c;想从事JAVA开发工作&#xff0c;但不知道从何处入手。于是&#xff0…

node.js的模块化 与 CommonJS规范

一、node.js的模块化 (1)什么是模块化&#xff1f; 将一个复杂的程序文件依据一定的规则拆分成为多个文件的过程就是模块化 在node.js中&#xff0c;模块化是指把一个大文件拆分成独立并且相互依赖的多个小模块&#xff0c;将每个js文件被认为单独的一个模块&#xff1b;模块…

Python爬虫:爬虫常用伪装手段

目录 前言 一、设置User-Agent 二、设置Referer 三、使用代理IP 四、限制请求频率 总结 前言 随着互联网的快速发展&#xff0c;爬虫技术在网络数据采集方面发挥着重要的作用。然而&#xff0c;由于爬虫的使用可能会对被爬取的网站造成一定的压力&#xff0c;因此&#…

PS从入门到精通视频各类教程整理全集,包含素材、作业等(5)复发

PS从入门到精通视频各类教程整理全集&#xff0c;包含素材、作业等 最新PS以及插件合集&#xff0c;可在我以往文章中找到 由于阿里云盘有分享次受限制和文件大小限制&#xff0c;今天先分享到这里&#xff0c;后续持续更新 初寒调色案例及练习图 等文件 https://www.alipan…

Redis改造原始代码

基础篇Redis 5.2.2.改造原始代码 代码说明: 1.在我们完成了使用工厂设计模式来完成代码的编写之后&#xff0c;我们在获得连接时&#xff0c;就可以通过工厂来获得。 &#xff0c;而不用直接去new对象&#xff0c;降低耦合&#xff0c;并且使用的还是连接池对象。 2.当我们…

书生·浦语大模型-第二节课笔记/作业

笔记 实验一 cli-demo import torch from transformers import AutoTokenizer, AutoModelForCausalLMmodel_name_or_path "../models"tokenizer AutoTokenizer.from_pretrained(model_name_or_path, trust_remote_codeTrue, device_mapcuda:0) model AutoModelF…

给虚拟机配置静态IP并使用FileZIlla在虚拟机和Windows之间传输文件(ssh和ftp两种方法)

一、配置操作系统网络 &#x1f338;下面的步骤主要是配置虚拟机的静态IP&#xff0c;方便后续用 FikeZilla 在windows和虚拟机之间传输文件&#xff08;否则用默认的ip分配方案为 DHCP ,每一次开机时的ip都是有可能不同的,这样就会导致每次远程连接都需要查看ip地址.&#xf…

Java学习33-Java 多线程Thread 多线程安全问题

Thread的生命周期 JDK1.5之前 JDK1.5之后分为 NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED 多线程安全问题 举例&#xff0c;要求三个窗口同时卖票&#xff0c;总共有100张票&#xff0c;打印出卖票过程&#xff0c;不允许重复售卖 package Thread;public class …

HarmonyOS实战开发-目标管理、如何实现一个自定义弹窗。

介绍 本篇Codelab将介绍如何使用State、Prop、Link、Watch、Provide、Consume管理页面级变量的状态&#xff0c;实现对页面数据的增加、删除、修改。要求完成以下功能&#xff1a; 实现一个自定义弹窗&#xff0c;完成添加子目标的功能。实现一个可编辑列表&#xff0c;可点击…

商品服务 - 三级分类

1.递归查询树形结构 Overridepublic List<CategoryEntity> listWithTree() {//1.查出所有分类List<CategoryEntity> all this.list();//2.组装成父子的属性结构List<CategoryEntity> level1Menus all.stream().filter(c -> c.getParentCid().equals(0L)…

2004-2022年上市公司企业战略激进度数据(含原始数据+计算代码+计算结果)

2004-2022年上市公司企业战略激进度数据&#xff08;含原始数据计算代码计算结果&#xff09; 1、时间2004-2022年 2、来源&#xff1a;原始数据整理自csmar 3、指标&#xff1a; 证券代码、统计截止日期、员工人数、证券简称、报表类型、固定资产净额、无形资产净额、资产…

算法学习——LeetCode力扣动态规划篇6(121. 买卖股票的最佳时机、122. 买卖股票的最佳时机 II、123. 买卖股票的最佳时机 III)

算法学习——LeetCode力扣动态规划篇6 121. 买卖股票的最佳时机 121. 买卖股票的最佳时机 - 力扣&#xff08;LeetCode&#xff09; 描述 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&…

Transformers -- 深入研究 - part 3

公众号:Halo咯咯,欢迎关注~ 前文回顾: Transformers -- 以通俗易懂的方式解释 - Part 1Transformers -- 未知英雄 - Part 2世界正在为人工智能和生成式人工智能而疯狂,特别是 2023 年的 ChatGPT 和大型语言模型。在我们讨论本系列后续部分的技术细节之前,让我们先从它的想…

用Servlet实现一个简单的表白墙

1. 准备工作 创建项目,引入依赖...... 将静态页面放到项目中(放在webapp目录下): 当前,这个表白墙页面,已经可以输入内容,点击提交之后也能显示内容,后续后端要做的工作即: ①存档 用户点提交的时候,把刚才输入的内容通过网络传输给服务器,由服务器保存这个数据. ②读档 …

NAT地址转换内外网通信

实验要求&#xff1a;内网地址通过nat转换成外网地址&#xff0c;联通外网服务器&#xff0c;达到内网外网互通 拓扑结构&#xff1a; 配置完成后&#xff0c;在ar1的G1口设置抓包&#xff0c;在pc1设备上ping ar2的地址&#xff0c;通过查看抓包信息&#xff0c;可以看到访问…

49 el-input 的 模型 视图 双向同步

前言 这里来看一下 el-input 这边的 数据 和 视图的双向绑定 最开始 我以为 这部分的处理应该是 vue 这边实现的, 但是跟踪调试了一下 发现这部分的处理是业务这边 自己实现的 这部分 还是有一些 值得记录的东西, 从这里 要去理解的而是 vue 这边从宏观的框架上面来说 帮我们…

python如何画奥运五环

绘制奥运五环主要涉及到Python中的turtle绘图库运用&#xff1a; 程序源代码为&#xff1a; import turtle turtle.width(10) turtle.color(black) turtle.circle(50) turtle.penup() turtle.goto(120,0) turtle.pendown() turtle.color(red) turtle.circle(50) turtle.penup()…