【c++】类和对象(四)深入了解拷贝构造函数

Alt

🔥个人主页:Quitecoder

🔥专栏:c++笔记仓

Alt

朋友们大家好啊,本篇内容带大家深入了解拷贝构造函数

目录

  • 1.拷贝构造函数
    • 1.1传值调用的无限调用
    • 1.2浅拷贝
    • 1.3深拷贝
    • 1.4深拷贝的实现

1.拷贝构造函数

拷贝构造函数是一种特殊的构造函数,在对象需要以同一类的另一个对象为模板进行初始化时被调用。它的主要用途是初始化一个对象,使其成为另一个对象的副本

我们先引用前面所用到的日期类的例子:

class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};

简单来说,假如我现在定义了一个日期对象:

int main()
{Date d1(2005, 6, 23);return 0;
}

我需要定义一个d2,与我的d1的数据相同,如何定义呢?

方法如下:

int main()
{Date d1(2005, 6, 23);Date d2(d1);return 0;
}

这里用到了拷贝构造,那么拷贝函数是如何实现的呢?我们接下来来探讨一下

拷贝构造函数通常声明为接受一个对同一类对象的常量引用参数:

class ClassName {
public:ClassName(const ClassName& other);
};
  • 参数:const ClassName& other是对另一个同类型对象的引用,使用const确保不会无意中修改other。
  • 函数体:在函数体内部,你可以决定如何复制other对象的成员到新对象中。对于简单的情况,这可能仅仅是复制每个成员变量的值。对于涉及动态分配内存或其他资源的类,可能需要进行深拷贝

下面来探讨上述的Date类的实现

class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2005, 6, 23);Date d2(d1);return 0;
}

拷贝构造函数是构造函数的一个重载形式,拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用,这个我们后面进行讲解

Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}

这里的d2就相当于this,d1就是另一个参数
在这里插入图片描述

1.1传值调用的无限调用

我们上面提到,拷贝构造函数参数只有一个且必须是类类型对象的引用,那么如果我使用传值调用会有什么结果呢??

我们下面先来进行简单的铺垫

void fun1(Date d)
{}
void fun2(Date& rd)
{}
int main()
{Date d1(2005, 6, 23);fun1(d1);fun2(d1);return 0;
}

构造两个函数,他们的参数不同,第一个函数为传值传参,在c语言中我们知道,传值传参是一个拷贝的过程,即把d1的值拷贝给dc++规定,自定义类型的拷贝,都会调用拷贝构造

我们进行调试
在这里插入图片描述

在这里按F11,我们目的是进入fun1,函数,这里却跳入拷贝构造函数
在这里插入图片描述
再按f11,才会进入fun1函数中
在这里插入图片描述
大概过程如下
在这里插入图片描述
传值传参需要调用拷贝构造

在这里插入图片描述
在这里插入图片描述
fun2函数可以直接进入
在这里插入图片描述

在上述讲解后,我们来探讨,如果拷贝函数是传值引用,会发生什么?

调用拷贝构造,需要传参,这里传值传参,就会调用一个新的拷贝构造

在这里插入图片描述
所以,这里也是我们为什么只能用引用传参

1.2浅拷贝

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

我们现在屏蔽掉拷贝构造,看会发生什么
在这里插入图片描述

若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象(内置类型成员)按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝

那如果有自定义类型呢?
我们看下面的代码:

class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 2024;int _month = 1;int _day = 1;// 自定义类型Time _t;
};
int main()
{Date d1;Date d2(d1);return 0;
}

在这个代码示例中,我们有两个类:TimeDateDate 类中包含了一些基本类型的成员变量(_year, _month, _day)和一个自定义类型的成员变量(_t,一个 Time 类型的对象)。当创建 Date 类的对象时,不仅会初始化其基本类型的成员变量,也会调用其自定义类型成员的构造函数来初始化

函数的调用过程

  1. Date 对象的默认构造函数调用:当 Date 类的对象被创建时,它的默认构造函数(编译器自动生成的,因为没有显式定义)会被调用。由于成员变量 _year, _month, _day 在类定义中已经被直接初始化,编译器将这些初始化纳入默认构造函数的操作中。

  2. Time 成员的构造函数调用:在 Date 的构造函数执行过程中,会自动调用 _tTime 类型的成员变量)的默认构造函数来初始化 _tTime 的默认构造函数设置 _hour, _minute, _second 为 1,并不打印任何信息。

  3. 拷贝 Date 对象:当 Date d2(d1); 执行时,d2 是通过拷贝构造函数初始化的。因为 Date 类没有显式定义拷贝构造函数,编译器会为它生成一个默认的拷贝构造函数。这个默认的拷贝构造函数会逐个拷贝 Date 类中的所有成员变量,包括基本类型和自定义类型的成员。

    • 对于基本类型成员(如 _year, _month, _day),直接进行值的复制
    • 对于自定义类型的成员(_t),会调用该成员的拷贝构造函数(Time 类中定义的 Time(const Time&))来进行拷贝。在这个过程中,Time 的拷贝构造函数会输出信息:Time::Time(const Time&)

因此,在执行 Date d2(d1); 时,调用过程如下:

  1. 首先,调用 Date 的默认拷贝构造函数(自动生成)来初始化 d2
  2. 在初始化 d2 的过程中,对于其自定义类型成员 _t,调用 Time 的拷贝构造函数来初始化,此时会输出 Time::Time(const Time&)

这就是自定义类型成员在 Date 类拷贝过程中构造函数的调用情况,其他的基本类型成员变量则是通过简单的值复制来初始化的

在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的

如果我们删掉Time的默认的拷贝构造函数呢?

class Time
{
public:Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 2024;int _month = 1;int _day = 1;// 自定义类型Time _t;
};
int main()
{Date d1;Date d2(d1);return 0;
}

拷贝构造本身就是一种构造函数,所以编译器不会生成默认构造函数

在这个代码中,由于 Time 类中没有显式定义一个无参数的默认构造函数(只定义了一个拷贝构造函数),而 Date 类的实现依赖于 Time 类的这个默认构造函数来初始化其 _t 成员,所以编译器将尝试调用 Time 类的默认构造函数时会失败,因为找不到合适的构造函数来初始化 _t

当尝试创建 Date 类的实例 d1 时,Date 类的默认构造函数(由编译器隐式生成)会被调用。默认构造函数会尝试初始化所有成员变量,对于基本类型的成员变量 _year,_month, _day,由于它们已经在类定义中直接初始化,不会有问题。但对于 _tTime 类型的成员变量),编译器需要调用 Time 类的默认构造函数来初始化它。由于 Time类中没有定义无参数的默认构造函数,编译过程中会出现错误

当尝试通过拷贝构造函数创建 d2 时(Date d2(d1);),同样会遇到问题。虽然 Date 类的拷贝构造函数(编译器自动生成的)会尝试逐个拷贝所有成员变量,对于 _t,它会尝试调用 Time类的拷贝构造函数,这部分没有问题。但在创建 d1 时已经失败,因此这一步也无法成功执行

c++也可以加入这串代码进行强制生成:

Time() = default;

1.3深拷贝

如果你没有为类显式定义拷贝构造函数,C++编译器会自动生成一个默认的拷贝构造函数。默认拷贝构造函数会逐个复制对象的所有成员(浅拷贝)。对于基本数据类型和指向动态分配内存的指针成员这意味着只复制指针值而不复制指针指向的数据

我们来看下面的代码:

typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");return;}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _array;size_t _size;size_t _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);return 0;
}

我们没有提供拷贝构造函数,编译器默认提供,我们来看运行结果:
在这里插入图片描述
程序崩溃,我们进行调试观察
在这里插入图片描述
当通过 Stack s2(s1); 这样的语句创建一个 Stack 类的对象时,如果没有显式定义拷贝构造函数,C++ 编译器会提供一个默认的拷贝构造函数,它进行浅拷贝。这意味着 _array 指针的值被复制过来,但指向的内存空间没有被复制。这会导致多个对象共享同一块内存空间,进而导致双重释放等问题

在这里插入图片描述

类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝

  • **浅拷贝(Shallow Copy)**只复制对象的顶层结构,如果对象中包含指针指向动态分配的内存,则副本的这些指针将指向与原始对象相同的内存地址。这意味着两个对象共享部分资源。浅拷贝通常是通过默认的拷贝构造函数和赋值操作符实现的
  • 深拷贝则复制对象所有的层级结构。对于对象内部的每一个指针指向的内存,深拷贝都会在堆上分配新的内存,然后将原始数据复制到这块新分配的内存中。这样,原始对象和副本对象将拥有完全独立的数据副本

1.4深拷贝的实现

深拷贝需要我们手动实现,对于上述的代码,我们需要手动补充,于对象内部的每个指向动态分配内存的指针,都需要:

  1. 为副本分配新的内存空间
  2. 将原始对象指针指向的数据复制到新分配的内存中
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");return;}_size = 0;_capacity = capacity;}//注意,这里s2是this,s是s1Stack(const Stack& s){DataType* tmp == (DataType*)malloc(sizeof(DataType) * s._capacity);if (tmp = nullptr){perror("malloc fail");exit(-1);}memcpy(tmp, s._array, sizeof(DataType) * s._size);_array = tmp;_size = s._size;_capacity = s._capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _array;size_t _size;size_t _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);return 0;
}
//注意,这里s2是this,s是s1Stack(const Stack& s){DataType* tmp = (DataType*)malloc(sizeof(DataType) * s._capacity);if (tmp == nullptr){perror("malloc fail");exit(-1);}memcpy(tmp, s._array, sizeof(DataType) * s._size);_array = tmp;_size = s._size;_capacity = s._capacity;}

这个拷贝构造函数的主要功能是创建一个新的 Stack 对象,该对象是对现有 Stack 对象(称为 s)的深拷贝。深拷贝意味着新对象将拥有与原对象相同的数据副本,但这些数据存储在新分配的内存中。这样,两个对象的状态互不影响,修改一个对象的内容不会影响另一个

  • 内存分配
    使用 malloc 根据原栈 (s) 的容量 (_capacity) 分配足够的内存空间来存储数据副本。这里的内存大小是 s._capacity * sizeof(DataType)

  • 数据复制
    使用 memcpy 将原栈 (s) 的数据 _array 复制到新分配的内存 tmp 中。复制的长度是 s._size * sizeof(DataType),即仅复制原栈中实际存在的元素

  • 更新成员变量
    将新栈的 _array 指针更新为指向新分配的内存 tmp
    将新栈的 _size 和 _capacity 设置为与原栈 (s) 相同的值。这样保证了新栈在逻辑上与原栈完全相同,拥有相同数量的元素和相同的容量

在这里插入图片描述
这下我们的问题也就解决了
在这里插入图片描述

class myqueue {private:Stack st1;Stack st2;
};
int main()
{myqueue q1;myqueue q2(q1);return 0;
}

有一个 Stack 类,它实现了一个简单的栈,并提供了深拷贝功能。然后,创建一个 myqueue 类,它内部使用了两个 Stack 实例。在 main 函数中,创建了一个 myqueue 对象 q1 并尝试使用 q1 来初始化另一个 myqueue 对象 q2。这里的关键点在于理解 Stack 的深拷贝实现如何影响 myqueue 对象的复制行为

  • myqueue 类及其复制行为
    myqueue 类内部包含两个 Stack 对象:st1st2。当使用一个 myqueue 对象来初始化另一个(如 myqueue q2(q1);)时,myqueue 的隐式(或默认)拷贝构造函数被调用。C++ 默认的拷贝构造函数会逐个复制类的成员,使用各成员自己的拷贝构造函数。因此,q1 中的 st1st2 会使用它们各自的深拷贝构造函数来初始化 q2 中的 st1st2

由于 Stack 类已经提供了深拷贝的实现,myqueue 类中的 st1st2 成员在 myqueue
对象被复制时也会被深拷贝。这意味着 q1q2 中的 st1st2 在内存上是独立的:q1.st1
q2.st1 指向不同的内存区域,q1.st2q2.st2 同理。因此,q1q2
在逻辑上是完全独立的队列,它们内部的栈互不影响

  • 隐式拷贝构造函数myqueue 类在这段代码中并没有显式定义自己的拷贝构造函数。它依赖于 C++ 自动生成的默认拷贝构造函数来正确地复制其成员。这在 Stack 提供深拷贝的情况下是安全的

在这里插入图片描述
本篇内容到此结束,感谢大家观看!!!!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java版企业电子招标采购系统源码——鸿鹄电子招投标系统的技术特点

在数字化时代&#xff0c;采购管理也正经历着前所未有的变革。全过程数字化采购管理成为了企业追求高效、透明和规范的关键。该系统通过Spring Cloud、Spring Boot2、Mybatis等先进技术&#xff0c;打造了从供应商管理到采购招投标、采购合同、采购执行的全过程数字化管理。通过…

【Java面试题】计算机网络

文章目录 1.计算机网络基础1.1网络分层模型/OSI七层模型是什么&#xff1f;1.2TCP/IP四层模型是什么&#xff1f;每一层的作用&#xff1f;1.2.1TCP四层模型&#xff1f;1.2.2为什么网络要分层&#xff1f; 1.2常见网络协议1.2.1应用层常见的协议1.2.2网络层常见的协议 2.HTTP2…

解决华为云服务器宝塔面板无法访问显示“此站点的连接不安全”问题

已经配置好安全组以及初始化宝塔面板&#xff0c;还是无法访问镜像管理页面&#xff0c;提示此站点的连接不安全。 解决方案 将地址https改为http即可进入。 成功登录后&#xff0c;开启面板SSL即可。

js实现拖放效果

dataTransfer对象 说明&#xff1a;dataTransfer对象用于从被拖动元素向放置目标传递字符串数据。因为这个对象是 event 的属性&#xff0c;所以在拖放事件的事件处理程序外部无法访问 dataTransfer。在事件处理程序内部&#xff0c;可以使用这个对象的属性和方法实现拖放功能…

科学认识并正确运用人工智能技术赋能国际传播

以下文章来源&#xff1a;学习时报 加强国际传播能力建设&#xff0c;全面提升国际传播效能&#xff0c;形成同我国综合国力和国际地位相匹配的话语权&#xff0c;已成为实现中国式现代化需要解决好的一个重大问题。文生视频模型Sora&#xff0c;是继ChatGPT之后又一推动传播智…

鉴源论坛丨形式化工程方法之需求建模(下)

作者 | 杨坤 上海控安可信软件创新研究院系统建模组 版块 | 鉴源论坛 观模 引言&#xff1a;需求建模是一种从源头确保软件质量的重要手段。需求建模可分为需求规约和需求确认两个部分&#xff0c;前者通过严格设计的形式化语言精确地将需求文档转换为了形式化规约&#xff0…

手撕LRU 最近最少使用缓存淘汰策略 + LinkedHashMap

LRU 最近最少使用缓存淘汰策略 1 LRU 算法就是一种缓存淘汰策略2 手撕LRU3 LinkedHashMap 常见面试题 1 LRU 算法就是一种缓存淘汰策略 计算机的缓存容量有限&#xff0c;如果缓存满了就要删除一些内容&#xff0c;给新内容腾位置。但问题是&#xff0c;删除哪些内容呢&#x…

“Kimi概念”降温,长文本“担不起”大模型的下一步

Kimi火了…… 这是这波AI浪潮中&#xff0c;国内创业公司第一次真正“破圈”。最明显的标志是&#xff0c;在二级市场中&#xff0c;Kimi已被市场作为一个概念板块来对待&#xff0c;它们被称之为“Kimi概念股”。在之前爆炒的板块中&#xff0c;可能有华为产业链、苹果产业链&…

SV-7045VP sip网络草坪音箱 室外网络广播POE供电石头音箱

SV-7045VP sip网络草坪音箱 室外网络广播POE供电石头音箱 18123651365微信 SV-7045VP SIP网络草坪音箱 sip POE石头音箱 描述 SV-7041VP是深圳锐科达电子有限公司的一款防水网络草坪音箱&#xff0c;具有10/100M以太网接口&#xff0c;可将网络音源通过自带的功放和喇叭输出…

ESD保护二极管ESD9B3.3ST5G 以更小的空间实现强大的保护 车规级TVS二极管更给力

什么是汽车级TVS二极管&#xff1f; TVS二极管是一种用于保护电子电路的电子元件。它主要用于电路中的过电压保护&#xff0c;防止电压过高而损坏其他部件。TVS二极管通常被称为“汽车级”是因为它们能够满足汽车电子系统的特殊要求。 在汽车电子系统中&#xff0c;由于车辆启…

机器学习——神经网络简单了解

一、神经网络基本概念 神经网络可以分为生物神经网络和人工神经网络 (1)生物神经网络,指的是生物脑内的神经元、突触等构成的神经网络&#xff0c;可以使生物体产生意识&#xff0c;并协助生物体思考、行动和管理各机体活动。 (2)人工神经网络,是目前热门的深度学习的研究…

【第二部分--Python之基础】02

二、运算符与程序流程控制 1、运算符 1.1 算术运算符 算术运算符用于组织整数类型和浮点类型的数据&#xff0c;有一元运算符和二元运算符之分。 一元算术运算符有两个&#xff1a;&#xff08;正号&#xff09;和-&#xff08;负号&#xff09;&#xff0c;例如&#xff1…

【C++11】thread线程库

【C11】thread线程库 目录 【C11】thread线程库thread类的简单介绍函数指针lambda表达式常用在线程中 线程函数参数join与detach利用RAII思想来自动回收线程 原子性操作库(atomic)atomic中的load函数&#xff1a;atomic中对变量进行原子操作的一些函数 CAS(Compare-And-Swap)无…

Git学习笔记之基础

本笔记是阅读《git pro》所写&#xff0c;仅供参考。 《git pro》网址https://git-scm.com/book/en/v2 git官网 https://git-scm.com/ 一、git起步 1.1、检查配置信息 git config --list查看所有的配置以及它们所在的文件 git config --list --show-origin可能有重复的变量名…

科技云报道:从“算力核弹”到生成式AI,新纪元还有多远?

科技云报道原创。 “我们需要更大的GPU”&#xff01; 3月19日凌晨&#xff0c;一年一度的“AI风向标”重磅会议——GTC 2024如期而至。 英伟达CEO黄仁勋在大会上发布了包括新一代加速计算平台NVIDIA Blackwell、Project GR00T人形机器人基础模型、Omniverse Cloud API、NVI…

【prompt六】MaPLe: Multi-modal Prompt Learning

1.motivation 最近的CLIP适应方法学习提示作为文本输入,以微调下游任务的CLIP。使用提示来适应CLIP(语言或视觉)的单个分支中的表示是次优的,因为它不允许在下游任务上动态调整两个表示空间的灵活性。在这项工作中,我们提出了针对视觉和语言分支的多模态提示学习(MaPLe),以…

大数据开发(日志离线分析项目)

大数据开发&#xff08;日志离线分析项目&#xff09; 一、项目需求1、使用jqueryecharts的方式调用程序后台提供的rest api接口&#xff0c;获取json数据&#xff0c;然后通过jquerycss的方式进行数据展示。工作流程如下&#xff1a;2、七大角度1、用户基本信息分析模块2、浏览…

【计算机视觉】三、图像处理——实验:图像去模糊和去噪、提取边缘特征

文章目录 0. 实验环境1. 理论基础1.1 滤波器&#xff08;卷积核&#xff09;1.2 PyTorch:卷积操作 2. 图像处理2.1 图像读取2.2 查看通道2.3 图像处理 3. 图像去模糊4. 图像去噪4.1 添加随机噪点4.2 图像去噪 0. 实验环境 本实验使用了PyTorch深度学习框架&#xff0c;相关操作…

openGauss学习笔记-252 openGauss性能调优-使用Plan Hint进行调优-Scan方式的Hint

文章目录 openGauss学习笔记-252 openGauss性能调优-使用Plan Hint进行调优-Scan方式的Hint252.1 功能描述252.2 语法格式252.3 参数说明252.4 示例 openGauss学习笔记-252 openGauss性能调优-使用Plan Hint进行调优-Scan方式的Hint 252.1 功能描述 指明scan使用的方法&#…

【计算机操作系统】深入探究CPU,PCB和进程工作原理

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如…