C++对象拷贝

news2025/8/1 18:10:22

前言:本教程使用到的工具是vs2010;能用VC++6就用VC++6,因为vs2010生成的汇编代码可能有点乱;此外,文章中涉及的汇编,我都会予以解释,所以放心观看。

目录

一、什么是对象拷贝?

二、C++对象拷贝

        栈区拷贝

        堆区拷贝

        父类的值是否会被拷贝

        默认拷贝构造---浅拷贝存在的问题

        深拷贝

赋值运算符实现深拷贝

总结


一、什么是对象拷贝?

        首先看下面这张图:

        假设我们现在有一个对象数组,里边有三个对象,O1、O2和O3;但是随着业务逻辑的提升,这三个对象已经无法满足我们的使用了;那么我们肯定要再添加几个对象进去,但是我们又不想重新创建O1、O2、O3;那么怎么办呢?

        首先在C语言中,我们肯定是用一些拷贝函数之类的,通过赋值拷贝将一个对象的内容拷贝给另一个对象进行使用;这个过程就叫对象拷贝

        当然C++中也有类似拷贝对象内容的方式,并且C++的编译器支持我们用更简单的方式进行内存复制,接下来就让我们看看C++中如何实现对象拷贝;

二、C++对象拷贝

        栈区拷贝

        首先测试代码如下:

#include <stdio.h>
#include <Windows.h>

class CObject
{
private:
	int x;
	int y;
public:
	CObject() {}
	CObject(int x,int y)
	{
		this->x = x;
		this->y = y;
	}
};

int main()
{

	return 0;
}

        我们定义了一个类,这个类有两个成员x和y,然后有两个构造函数;

        那么C++中拷贝构造函数是什么样的呢?如下:

        我们先验证一下,是否真正地实现了拷贝:

        没问题;但是问题来了,我们没有写任何拷贝构造函数,那么上面的拷贝又是如何实现的呢?

        这就是C++比较人性化的一个特点,其实我们写好了任意一个类(类型)的时候,它默认都已经生成了一个拷贝构造函数,帮我们实现了最简单的内存复制; 

        那么这个拷贝构造函数是如何实现的呢?

        断点打在拷贝构造的地方,编译、调试、ALT+8转到反汇编:

        其实C++默认提供的拷贝构造函数是很简单的,就是把第一个对象的值从内存地址取出来放到第二个对象里; 

        我们上面的拷贝方式是在栈区进行对象拷贝,下面我们看如何在对象拷贝构造对象;

        堆区拷贝

        我们知道,我们平常在堆区创建一个对象,基本new一个无参构造函数,或者有参构造函数:

        那么如果我们new一个拷贝构造函数,不就是在堆区进行拷贝构造了嘛,如下:

        没有问题,那么我们看看堆区的拷贝构造函数如何实现的,一样转到反汇编:

        我们拿下来一步一步分析:

010113EA  push        8                                          -- 给new函数传入8,意为申请8字节内存
010113EC  call        operator new (1011181h)      -- 调用new,之前的文章讲过new底层
010113F1  add         esp,4                                    -- 外平栈
010113F4  mov         dword ptr [ebp-0E4h],eax    -- 将eax(new返回的地址)放到ebp-E4这个地址里
010113FA  cmp         dword ptr [ebp-0E4h],0        -- 将0与new的返回地址进行比较
01011401  je          main+62h (1011422h)             -- 如果返回地址等于0(申请堆区内存为空)就跳转到1011422h
01011403  mov         eax,dword ptr [ebp-0E4h]     -- 如果不为空,将返回地址(堆区地址)放到eax里
01011409  mov         ecx,dword ptr [c1]                 -- 将c1的首地址(第一个值)取出,放到ecx里
0101140C  mov         dword ptr [eax],ecx              -- 将ecx里的值放到eax这个地址里,也就是将c1的第一个成员放到堆区申请的地址中
0101140E  mov         edx,dword ptr [ebp-8]           -- 将ebp-8(c1的第二个成员地址)里的值放到edx里,ebp-8指的就是c1的第二个成员,这里vs2010编译器生成的汇编有点乱
01011411  mov         dword ptr [eax+4],edx           -- 将c1的第二个成员的值,放到申请到的堆区首地址+4的位置上
01011414  mov         eax,dword ptr [ebp-0E4h]     -- 将ebp-0E4中的堆区地址再次赋值给eax,反正我是没有明白这一步的意义是什么,如果是怕别人修改了eax的值,导致后面返回给c2指针的是一个别人想要的地址的话,可以解释的通;当然大家如果有不同的看法,或者我讲错了,请评论告诉我,这里我迷瞪了老半天了
0101141A  mov         dword ptr [ebp-0ECh],eax    --  将eax中存放的堆区地址存放到ebp-EC
01011420  jmp         main+6Ch (101142Ch)          --  如果堆区地址不为空,跳过下一步
01011422  mov         dword ptr [ebp-0ECh],0        --  申请的堆区地址为空的话,将ebp-EC地址中的值置空,因为下面要将EC中的值赋值给对象指针c2,如果申请地址为空,那就赋值空
0101142C  mov         ecx,dword ptr [ebp-0ECh]    -- 无论此时EC地址中的值是一个已经拷贝了c1值的堆区地址,还是一个空地址,都要赋值给ecx
01011432  mov         dword ptr [c2],ecx                 -- 将ecx赋值给对象指针c2

         我们可以看到如果是在堆区拷贝的话,是直接把c1的值放到堆区申请的地址里,然后再将这个已经赋好值的堆区地址赋值给指针c2;

        父类的值是否会被拷贝

        我们现在知道,C++提供的默认拷贝构造,可以将一个类的对象的值全部拷贝给另一个对象,那么问题来了,如果第一个对象有父类,构造的时候构造了一个父类,那么第二个对象能否继承第一个对象父类的值呢?

        代码如下:

#include <stdio.h>
#include <Windows.h>

class CBase
{
private:
	int x;
	int y;
public:
	CBase(){}
	CBase(int x,int y)
	{
		this->x = x;
		this->y = y;
	}
};

class CTeach:public CBase
{
private:
	int z;
public:
	CTeach(){}
	CTeach(int x,int y,int z):CBase(x,y)
	{
		this->z = z;
	}
};

int main()
{
	CTeach ct(10,20,30);

	CTeach ct1(ct);

	return 0;
}

        断点打到return 0;

        调试:

        可以看到父类的也被拷贝过来了;

        如果是堆区拷贝呢?如下:

        一样,也被拷贝了过来;

        我们现在知道了,通过拷贝构造也是可以将源对象父类的值拷贝过来的; 

        上面这么一说,听起来默认拷贝构造很完美啊,但是真的是这样吗?如果是这样,那本篇文章也该结束了;

        下面说说默认拷贝构造的不足之处;

        默认拷贝构造---浅拷贝存在的问题

        我们先来看如下代码:

#include <stdio.h>
#include <Windows.h>
#include <stdlib.h>

class CString
{
private:
	int   m_length;
	char* m_str;
public:
	CString() {}
	CString(const char* str)
	{
		m_length = strlen(str)+1;    // m_length大小为传入的str长度+1 
		m_str = new char[m_length];  // 申请m_length长度的堆区空间
		memset(m_str,0,m_length);    // 初始化申请到的堆区空间
		strcpy(m_str,str);           // 将传入的str拷贝到该堆区空间中
	}
    ~CString()
	{
		delete[] m_str;
	}
};

int main()
{
	CString cs01("C语言");
	CString cs02(cs01);
	return 0;                         // 断点打在这里
}

        我们调试一下:

        查看监视窗口:

        我们可以看到,使用默认拷贝构造的时候,cs02将cs01的简简单单的拷贝了过来;

        为什么这样说呢?

        首先,cs01的m_str成员申请到了一个堆区的地址,所以此时m_str中存放的是一个地址,只是这个地址中的值是"C语言";

        当cs02使用默认拷贝构造的时候,将cs01的m_str这个指针的值直接拷贝了过来,我们知道cs01的m_str的值就是cs01申请到的堆区地址,如果cs02的m_str和cs01的m_str指针的值一样的话,这就意味着cs02和cs01两个对象的指针成员指向了同一块地址;

        这会造成什么样的后果呢?

        我们想一下,如果对象cs01的生命周期结束了,是不是会调用析构函数?并且我们析构函数里边写的是释放m_str申请的内存;

        如果cs01对象释放了m_str的内存,那么指向相同地址cs02::m_str的地址也会被释放,但是cs02生命周期并没有结束啊,如果cs02再次操作m_str这个指针成员,会访问到一个不属于自己的地址,导致野指针问题的出现;

        上面这种将值简简单单的拷贝过来的拷贝构造,被称为浅拷贝(值拷贝);

        那么如果遇到这种情况我们应该如何解决呢?

        解决方法就是自己写一个深拷贝构造函数

        深拷贝

        直接看深拷贝实现的代码,然后一步一步分析,如下:

        这就是重写的深拷贝构造函数,实现深拷贝的原理主要是重新申请一份堆区地址,将原来对象堆区地址中的值拷贝到新的堆区地址中 ;

        下面让我们测试一下效果如何:

        没有问题,两个对象指向了不同的地址,不用担心一个对象的指针被释放了会影响到另一个对象了;

        当然深拷贝还有一种写法就是传入指针,如下:

        测试一下效果:

        没有问题; 

        另外提一下,如果你自己添加了拷贝构造函数,那么编译器不会再为你提供默认的拷贝构造函数,所有的事情你都必须在你自己写的拷贝函数里做好;

        这句话怎么理解呢?

        因为有时候可能有些人会想,既然我添加拷贝构造函数只是为了弥补浅拷贝的不足,只是为了实现深拷贝,那么我就给指针重新申请一个地址不就行了吗?对于不是指针的成员(例如我们上面写的m_length)我就不管了,让编译器自己提供的默认拷贝构造函数去复制,我只关心指针成员不就行了;

        这种想法不可取,因为我们上面说了,如果你自己写了拷贝构造,无论你怎么写,编译器都不会再提供拷贝构造了,所以你不能只在乎指针成员,所有成员你都要自己手动复制,因为编译器不会再帮你了;

        这也就有了一句话:如果不需要深拷贝,不要自己添加拷贝构造函数!

赋值运算符实现深拷贝

        我们上面学了默认的拷贝构造是浅拷贝,但是我们自己可以实现深拷贝;

        其实在C++中,也可以直接使用赋值运算符进行拷贝构造,和编译器提供的默认构造方式一样都是浅拷贝,如下:

​​​​​​​

 

        那么我们怎样实现深拷贝呢?和上面的差不多,只不过一个是重载构造函数,一个是重载算术运算符:

        测试一下,如下:

 

        查看一下值:

 

        实现了深拷贝;

        至于为什么构造时候的 = 和我们重载的 = 不一样,请看大佬的这篇文章《C++深拷贝赋值运算符》 ;

        需要注意的是:

        1、如果重载了赋值运算符,那么我们要和自己写深拷贝构造函数一样,必须对所有的属性都要处理;

        2、如果不需要用到深拷贝,没必要重载 = ;

 

总结

        1、当我们构造对象的时候传入的参数是一个对象的话,那么编译器会为我们提供一个默认的拷贝构造函数,但是这个默认的拷贝构造是浅拷贝,只能帮我们把一个对象的值复制给另一个对象;

        2、我们可以自己实现深拷贝构造函数,解决浅拷贝复制指针的问题;

        3、如果不需要深拷贝,不要自己添加拷贝构造函数!

        4、如果你自己添加了拷贝构造函数,那么编译器不会再为你提供默认的拷贝构造函数,所有的事情你都必须在你自己写的拷贝函数里做好;

        5、如果重载了赋值运算符,那么我们要和自己写深拷贝构造函数一样,必须对所有的属性都要处理;

        6、如果不需要用到深拷贝,没必要重载赋值运算符 ;

结语:如果有讲的不好的地方或者听不懂的地方,都欢迎在评论区留言或者私信,感谢大家的观看!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【微服务】SpringCloud微服务剔除下线源码解析

&#x1f496; Spring家族及微服务系列文章 ✨【微服务】SpringCloud微服务续约源码解析 ✨【微服务】SpringCloud微服务注册源码解析 ✨【微服务】Nacos2.x服务发现&#xff1f;RPC调用&#xff1f;重试机制&#xff1f; ✨【微服务】Nacos通知客户端服务变更以及重试机制 ✨【…

SpringBoot SpringBoot 开发实用篇 4 数据层解决方案 4.4 Redis 下载安装与基本使用

SpringBoot 【黑马程序员SpringBoot2全套视频教程&#xff0c;springboot零基础到项目实战&#xff08;spring boot2完整版&#xff09;】 SpringBoot 开发实用篇 文章目录SpringBootSpringBoot 开发实用篇4 数据层解决方案4.4 Redis 下载安装与基本使用4.4.1 问题引入4.4.2 …

Linux Mint(Ubuntu)上 安装 效率神器 utools

Linux Mint&#xff08;Ubuntu&#xff09;上 安装 效率神器 utools 我的 Windows 系统的笔记本只有 256G 固态&#xff0c;磁盘已经快用满了&#xff0c;最近想装个 Linux 玩玩&#xff0c;1 选择了 Linux Mint&#xff0c;然后就在闲置的移动硬盘上安装了 Linux Mint 21 cin…

Centos 安装Java库的多种方式

安装jdk(介绍三种方法) 查看java版本&#xff1a;java -version 方法一&#xff1a;利用yum源来安装jdk&#xff08;此方法不需要配置环境变量&#xff09; 查看yum库中的java安装包 &#xff1a;yum -y list java* 安装需要的jdk版本的所有java程序&#xff1a;yum -y instal…

纯正体验,极致商务 | 丽亭酒店聚焦未来赛道,实现共赢发展

10月28日&#xff0c;锦江酒店(中国区)“齐鲁集锦 共话未来”投资人交流会在济南盛大召开&#xff0c;面向华东地区投资人&#xff0c;行业专家、商旅客、品牌代表齐聚一堂&#xff0c;共同聚焦酒店市场投资新价值&#xff0c;商讨新时代酒店行业新机遇&#xff0c;多维探索酒店…

蓝牙数据包协议解析

1.前言 由于工作需要&#xff0c;初次接触蓝牙。从最基础的知识开始了解。 引用wiki中的介绍&#xff1a; 蓝牙&#xff08;英语&#xff1a;Bluetooth&#xff09;&#xff0c;一种无线通讯技术标准&#xff0c;用来让固定与移动设备&#xff0c;在短距离间交换资料&#xff…

第一章:Spring流程执行步骤

Spring执行流程图 注意观察&#xff1a;每一个执行步骤的结果都会返回到DispatcherServlet &#xff0c;然后再出发调用&#xff0c; 所以是请求接口的入口也是出口。 简单了解几个大类的走的流程和具体功能 DispatcherServlet 类 中文调度应用程序&#xff0c;而Servlet就…

libusb系列-007-Qt下使用libusb1.0.26源码

libusb系列-007-Qt下使用libusb1.0.26源码 文章目录libusb系列-007-Qt下使用libusb1.0.26源码摘要安装编译环境确认需要的文件开始编译错误1&#xff1a;找不到文件错误2&#xff1a;expected错误3&#xff1a;SCM_CREDENTALS错误4&#xff1a;类型冲突错误5 assert断言错误错误…

低代码平台和无代码平台有什么区别

低代码&#xff08;LowCode&#xff09;/无代码(NoCode)”是技术界近几年的热门词汇之一&#xff0c;随着企业数字化发展的深入&#xff0c;越来越多的场景化需求要求企业具备更加灵活敏捷的应用开发能力&#xff0c;传统应用开发模式周期长、技术人员能力要求高&#xff0c;无…

OWASP API SECURITY TOP 10

目录 1. API 安全风险 2. 细说TOP10 1. Broken Object Level Authorization 2. Broken User Authentication 3 Excessive Data Exposure 4 Lack of Resources & Rate Limiting 5 Broken Function Level Authorization 6 Mass Assignment 7 security misconfigura…

redis哨兵系列1

需要配合源码一起康~ 9.1 哨兵基本概念 官网手册yyds&#xff1a;https://redis.io/docs/manual/sentinel/ redis主从模式&#xff0c;如果主挂了&#xff0c;需要人工将从节点提升为主节点&#xff0c;通知应用修改主节点的地址。不是很友好&#xff0c;so Redis 2.8之后开…

同花顺_代码解析_技术指标_EJK

本文通过对同花顺中现成代码进行解析&#xff0c;用以了解同花顺相关策略设计的思想 目录 EMV ENV EXPMA JF_ZNZX KD KDJ KDJFS EMV 简易波动指标 1.EMV 由下往上穿越0 轴时&#xff0c;视为中期买进参考信号&#xff1b; 2.EMV 由上往下穿越0 轴时&#xff0c;视为中…

根据以下电路图,补全STM32F103RCT6的IO口初始化程序

void KEY_Init(void)//接按键的端口初始化程序 { RCC->APB2ENR|______________; //使能PORTA时钟 JTAG_Set(SWD_ENABLE); GPIOA->CRL&__________________; // PA3设置成下拉输入 GPIOA->CRL|__________________; } void LED_Init(void)//接LED的端…

【Qt】控件探幽——QLineEdit

注1&#xff1a;本系列文章使用的Qt版本为Qt 6.3.1 注2&#xff1a;本系列文章常规情况下不会直接贴出源码供复制&#xff0c;都以图片形式展示。所有代码&#xff0c;自己动手写一写&#xff0c;记忆更深刻。 本文目录QLineEdit探幽1、设置数据/获取数据2、只读&#xff08;re…

【15-项目中服务的远程调用之OpenFeign订单模块与商品模块集成使用OpenFeign的案例】

一.知识回顾 【0.三高商城系统的专题专栏都帮你整理好了&#xff0c;请点击这里&#xff01;】 【1-系统架构演进过程】 【2-微服务系统架构需求】 【3-高性能、高并发、高可用的三高商城系统项目介绍】 【4-Linux云服务器上安装Docker】 【5-Docker安装部署MySQL和Redis服务】…

【面试题】圣杯布局和双飞翼布局

圣杯布局和双飞翼布局的特点&#xff1a; 三栏布局&#xff0c;中间一栏最先加载和渲染&#xff08;内容最重要&#xff09;两侧内容固定&#xff0c;中间内容随着宽度自适应一般用于PC页面 圣杯布局和双飞翼布局的实现方式&#xff1a; 使用float布局两侧使用margin负值&am…

git 、gitLub 提交后查看显示全部删除又全部新增

问题&#xff1a; git提交后删除的行数与增加的行数一样&#xff0c;没有修改git也会去全部删除又全部新增 大概是下面你这个样子&#xff1a; 原因&#xff1a; 换行符的原因&#xff0c;win、mac、linux/Unix 换行符是不一样的&#xff0c;提交的文件换行符和git服务器的不…

【Unity300个技巧】牛顿的学问!如何优雅地使用力?

Unity 提供了一个完备且强大物理引擎。但在刚接触的时候&#xff0c;你可能会对它与现实物理规律对应有一些疑惑。 比如&#xff1a; 现实中 1N 的力在 Unity 中怎么表现&#xff1f;Unity 中物体的质量是计算的&#xff1f;Unity 中给物体添加力的方法是 rb.AddForce()&#x…

ICME 论文Latex模版要求

2023年模版获取链接&#xff1a; 链接: https://pan.baidu.com/s/1Hjk4nnybQN6LE23SGfbxKQ 提取码: 27dj --来自百度网盘超级会员v4的分享 论文写作要求&#xff1a; 所有手稿必须是英文的&#xff1b; 论文长度不应超过6页&#xff0c;包括所有文本、图形和参考文献&#…

Qt 设置CPU亲缘性,把进程和线程绑定到CPU核心上(Linux)

Qt 设置CPU亲缘性&#xff0c;把进程和线程绑定到CPU核心上(Linux) 文章目录Qt 设置CPU亲缘性&#xff0c;把进程和线程绑定到CPU核心上(Linux)摘要1 什么是CPU亲缘性2 目的3 API 介绍3.1 进程绑定到CPU3.2 线程绑定到CPU4 实际操作4.1 实操代码主进程串口线程USB线程程序运行资…