【C++修行之道】类和对象(三)拷贝构造函数

目录

一、 概念

二、特征

 正确的拷贝构造函数写法:

拷贝函数的另一种写法 

三、若未显式定义,编译器会生成默认的拷贝构造函数。

四、编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?

深拷贝的写法:

五、拷贝构造函数典型调用场景:

六、总结:


一、 概念

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。

那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

二、特征

拷贝构造函数也是特殊的成员函数,其特征如下: 

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
  3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
  4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
  5. 拷贝构造函数典型调用场景:
  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

    Date(const Date& d) // 正确写法
    {
	    // this->_year = d._year;
	    _year = d._year;
    	_month = d._month;
	    _day = d._day;
    }

	Date(Date& d)// 错误写法: 它不能用于从常量对象或临时对象进行拷贝构造
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	Date(Date d)// 错误写法:编译报错,会引发无穷递归
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
    // 拷贝构造:用同类型的对象拷贝初始化
	Date d2(d1);
	return 0;
}

 正确的拷贝构造函数写法:

Date(const Date& d) // 正确写法
{
	// this->_year = d._year;
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

这是正确的拷贝构造函数写法。它接受一个对Date类型的常量引用作为参数,这意味着它可以用于从常量对象、非常量对象甚至是临时对象进行拷贝构造。由于它的灵活性,这是最常用的拷贝构造函数定义方式

	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
  • 不能接收常量对象:这个构造函数只接受非常量引用(Date&),这意味着你不能使用它来拷贝一个常量对象。如果试图这样做,编译器会报错,因为常量对象不能被非常量引用所绑定。
  • 不能接收临时对象:在C++中,临时对象(也称为右值)经常出现在表达式中,例如函数返回值或者类型转换的结果。由于这个拷贝构造函数不接受右值引用或常量引用,因此它不能用于拷贝这些临时对象。
  • 发生错误操作时没有报错:赋值反了

    Date(Date d)// 错误写法:编译报错,会引发无穷递归
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

这个构造函数是错误的,会引发无穷递归。原因在于,当试图用这个构造函数创建一个Date对象时,它会尝试以值传递的方式接收一个Date对象作为参数。为了构造这个参数对象d,又需要调用拷贝构造函数,这会导致无限递归调用,最终耗尽栈空间并导致程序崩溃。 

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

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

	// Date d3(d2);
	//Date(const Date& d)
	//{
	//	// this->_year = d._year;
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//}

	Date(Date* d)
	{
		_year = d->_year;
		_month = d->_month;
		_day = d->_day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	// 给缺省值
	int _year = 1;
	int _month = 1;
	int _day = 1;
};

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		cout << "Stack(size_t capacity = 3)" << endl;

		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}

	// Stack st2 = st1;
	Stack(const Stack& st)
	{
		_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		memcpy(_array, st._array, sizeof(DataType) * st._size);

		_size = st._size;
		_capacity = st._capacity;
	}

	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	bool  Empty()
	{
		return _size == 0;
	}

	DataType Top()
	{
		return _array[_size - 1];
	}
	
	void Pop()
	{
		--_size;
	}

	// 其他方法...
	~Stack()
	{
		cout << "~Stack()" << endl;

		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

拷贝函数的另一种写法 

int main()
{
	Date d2(2024, 4, 9);
	// 下面这两种写法是等价的
	Date d3(d2);
	Date d4 = d2; // 这也是拷贝构造
	d2.Print();
	d3.Print();

	//func(d2);
	/*Date d3(&d2);
	d3.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 = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;

	// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
	// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
	Date d2(d1);
	
	return 0;
}

在上述代码中,Date 类并没有显式定义拷贝构造函数当代码中尝试通过已有的 Date 对象 d1 来拷贝构造一个新的 Date 对象 d2 时,由于没有找到用户定义的拷贝构造函数,编译器会自动为 Date 类生成一个默认的拷贝构造函数

这个默认生成的拷贝构造函数会完成以下任务:

  1. 对于基本数据类型成员:直接拷贝其值。在 Date 类中,_year、_month 和 _day 这三个整型成员变量会直接被赋值,即新对象 d2 的这些成员会获得与 d1 相同的值。
  2. 对于自定义类型成员:调用该类型的拷贝构造函数。在 Date 类中,_t 是 Time 类型的成员变量。当默认拷贝构造函数被调用时,它会进一步调用 Time 类的拷贝构造函数来初始化新对象 d2 中的 _t 成员。这就是为什么在上述代码中,即使没有显式编写拷贝操作,仍然可以看到 Time 类的拷贝构造函数被调用的输出。

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

四、编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?

尽管编译器生成的默认拷贝构造函数可以完成字节序的值拷贝,但在某些情况下,仍然需要自己显式实现拷贝构造函数。这主要是因为默认拷贝构造函数执行的是浅拷贝,它会拷贝对象的所有成员变量,但如果对象中包含指针或动态分配的资源(如使用 new 或 malloc 分配的内存),浅拷贝可能会导致问题。

当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
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);
  • 这里,试图用s1来初始化s2。由于没有为Stack类提供自定义的拷贝构造函数,编译器会使用默认的拷贝构造函数。这个默认的拷贝构造函数将s1的_array指针值直接拷贝给s2的_array,这意味着s1和s2的_array成员现在指向同一块内存地址
  • 当s1和s2的生命周期结束时,它们的析构函数都会被调用,并试图释放同一块内存,这会导致未定义行为,通常是程序崩溃,因为同一块内存被释放了两次(double free)。

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

深拷贝的写法:

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

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

	// Date d3(d2);
	//Date(const Date& d)
	//{
	//	// this->_year = d._year;
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//}

	Date(Date* d)
	{
		_year = d->_year;
		_month = d->_month;
		_day = d->_day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	// 给缺省值
	int _year = 1;
	int _month = 1;
	int _day = 1;
};

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		cout << "Stack(size_t capacity = 3)" << endl;

		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0; 
	}

	// Stack st2 = st1;
	Stack(const Stack& st)
	{
		_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		memcpy(_array, st._array, sizeof(DataType) * st._size);

		_size = st._size;
		_capacity = st._capacity;
	}

	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	bool  Empty()
	{
		return _size == 0;
	}

	DataType Top()
	{
		return _array[_size - 1];
	}
	
	void Pop()
	{
		--_size;
	}

	// 其他方法...
	~Stack()
	{
		cout << "~Stack()" << endl;

		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};


void func(Date& d)
{
	d.Print();
}



class MyQueue 
{
private:
	Stack _st1;
	Stack _st2;
	int _size = 0;
};

int main()
{
	Date d2(2024, 4, 9);
	Date d4 = d2; 
	d4.Print();

	Stack st1(10);
	st1.Push(1);
	st1.Push(1);
	st1.Push(1);

	Stack st2 = st1;
	st2.Push(2);
	st2.Push(2);

	while (!st2.Empty())
	{
		cout << st2.Top() << " ";
		st2.Pop();
	}
	cout << endl;

	while (!st1.Empty())
	{
		cout << st1.Top() << " ";
		st1.Pop();
	}
	cout << endl;

	MyQueue q1;
	MyQueue q2(q1);

	return 0;
}
  • 拷贝构造函数创建的对象:为新对象分配足够的内存空间来存储栈中的元素。内存的大小是根据原始对象的容量(_capacity)来计算的。使用memcpy函数将原始对象栈中的元素复制到新分配的内存中。将新对象的_size和_capacity设置为与原始对象相同的值。 
  • MyQueue类中的成员变量:每个MyQueue对象都包含两个Stack对象,因此当q1和q2销毁时,它们的四个Stack成员变量(q1._st1, q1._st2, q2._st1, q2._st2)也会被销毁,每个Stack成员的析构函数都会被调用。这里增加了4次析构函数的调用。
  • 有一点需要注意:如果MyQueue类没有定义拷贝构造函数,并且默认使用了浅拷贝(即只拷贝成员变量的值,而不是它们所指向的内容),那么q2中的_st1和_st2实际上只是q1中对应成员的简单复制(指针或引用的复制)。在这种情况下,析构函数的调用次数可能会少于6次,因为多个对象可能共享相同的资源。但在上述代码中,我们假设MyQueue的拷贝构造函数进行了深拷贝,即创建了Stack对象的独立副本,因此每个Stack对象都有自己的生命周期,并在结束时调用析构函数。

五、拷贝构造函数典型调用场景:

  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象
class Date
{
public:
	Date(int year, int month, int day)
	{
		cout << "Date(int,int,int):" << this << endl;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d):" << this << endl;
	}
	~Date()
	{
		cout << "~Date():" << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
Date Test(Date d)
{
	Date temp(d);
	return temp;
}
int main()
{
	Date d1(2022, 1, 13);
	Test(d1);
	return 0;
}

 构造函数 Date(int year, int month, int day) 

    Date(int year, int month, int day)
	{
		cout << "Date(int,int,int):" << this << endl;
	}

拷贝构造函数 Date(const Date& d)

当创建一个已存在Date对象的副本时会调用它。同样,当这个构造函数被调用时,会打印一条消息和当前对象的地址。

Date()

	~Date()
	{
		cout << "~Date():" << this << endl;
	}

为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。

六、总结:

今天就先到这了!!!

看到这里了还不给博主扣个:
⛳️ 点赞☀️收藏 ⭐️ 关注!

你们的点赞就是博主更新最大的动力!
有问题可以评论或者私信呢秒回哦。

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

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

相关文章

项目资源管理

目录 1.概述 2.六个过程 2.1. 规划资源管理 2.2. 估算活动资源 2.3. 获取资源 2.4. 建设团队 2.5. 管理团队 2.6. 控制资源 3.应用场景 3.1.十个应用场景 3.2.软件开发项目 3.2.1. 资源规划 3.2.2. 资源分配 3.2.3. 资源获取 3.2.4. 资源优化 3.2.5. 资源监控与…

YOLOv10(4):损失(Loss)计算及其检测模型代码部分阅读

YOLOv10&#xff08;1&#xff09;&#xff1a;初探&#xff0c;训练自己的数据_yolov10 训练-CSDN博客 YOLOv10&#xff08;2&#xff09;&#xff1a;网络结构及其检测模型代码部分阅读-CSDN博客 目录 1. 写在前面 2. 双标签分配 3. 计算Loss的预备条件 &#xff08;1&a…

OpenCV-最小外接圆cv::minEnclosingCircle

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 函数原型 void minEnclosingCircle(InputArray points, Point2f& center, float& radius); 参数说明 InputArray类型的…

Codeforces Round 950 (Div. 3) 个人题解 (A~F1)

Codeforces Round 950 (Div. 3)个人题解(A~F1) 题解火车头 #define _CRT_SECURE_NO_WARNINGS 1#include <iostream> #include <vector> #include <algorithm> #include <set> #include <unordered_map> #include <cstring> #include <…

聚观早报 | 东风奕派eπ008将上市;苹果Vision Pro发布会

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 6月3日消息 东风奕派eπ008将上市 苹果Vision Pro发布会 特斯拉Model 3高性能版开售 小米14推送全新澎湃OS系统 …

ubuntu中彻底删除mysql (配置文件删除可选)

ubuntu中彻底删除mysql (配置文件删除可选) 对于此类即搜即用的分享文章&#xff0c;也不过多赘述&#xff0c;直接依次按照下面的操作执行即可&#xff1a; 一、删除 mysql 数据文件 sudo rm /var/lib/mysql/ -R二、删除 mysql 配置文件 sudo rm /etc/mysql/ -R三、查看 m…

基于 Spring Boot 博客系统开发(十三)

基于 Spring Boot 博客系统开发&#xff08;十三&#xff09; 本系统是简易的个人博客系统开发&#xff0c;为了更加熟练地掌握 SprIng Boot 框架及相关技术的使用。&#x1f33f;&#x1f33f;&#x1f33f; 基于 Spring Boot 博客系统开发&#xff08;十二&#xff09;&…

号称超级增程电动,领克07EM-P带来技术变革?

近年来&#xff0c;自主品牌在新能源汽车领域百花齐放&#xff0c;尤其是在混合动力市场上&#xff0c;比亚迪的DM-i技术引领了风潮&#xff0c;秦L的一经亮相&#xff0c;整个车圈都沸腾了&#xff0c;“超级混动”的概念深入人心。 各大自主品牌都有了自己的混动平台和技术。…

前端加载,渲染十万条数据(性能优化)

1.场景 项目中某个弹窗展示设备信息卡片,返回的设备信息很多,页面样式有很花哨,导致渲染极其缓慢 f12,查看性能,这里可以看到页面加载在哪一步分耗时最长,针对性进行优化(图为举例) 2.解决思路 采用虚拟列表的方式,滚动时,dom元素数量不变,只改变展示的数据 结构描述: 父盒…

基于飞腾 D2000 8 核+ 32G DDR+板载 6 千兆电口+ 4 千兆光口高性能网络安全主板

第一章、产品介绍 1.1 产品概述 XM-D2000GW是一款基于飞腾 D2000 8 核X100 桥片高性能网络安全主板&#xff0c;D2000 为飞腾首款支持 8 核桌面平 台处理器&#xff0c;支持双通道 DDR4-2666 内存&#xff0c;芯片内置国密 SM2/SM3/SM4/SM9 加速引擎&#xff0c;支持单精度、双…

华为ACL实验

实验拓扑&#xff1a; 实验目的&#xff1a;使用标准ACL控制某些数据流量&#xff0c;本例中控制PC1访问PC2且不让PC1访问到服务器。 实验思路&#xff1a;依据拓扑的要求&#xff0c;配置完终端设备后需要在交换机上划分两个vlan&#xff0c;分别代表两个企业的部门&#xff…

网络协议二

一、套接字Socket 基于 TCP UDP 协议的 Socket 编程&#xff0c;在讲 TCP 和 UDP 协议的时候&#xff0c;我们分客户端和服务端&#xff0c;在写程序的时候&#xff0c;我们也同样这样分。 在网络层&#xff0c;Socket 函数需要指定到底是 IPv4 还是 IPv6&#xff0c;分别对应设…

ICLR24大模型提示(1/11) | BadChain:大型语言模型的后门思维链提示

【摘要】大型语言模型 (LLM) 已证明可从思路链 (COT) 提示中获益&#xff0c;尤其是在处理需要系统推理过程的任务时。另一方面&#xff0c;COT 提示也以后门攻击的形式带来新的漏洞&#xff0c;其中模型将在推理期间在特定的后门触发条件下输出非预期的恶意内容。发起后门攻击…

k8s牛客面经篇

k8s的pod版块: k8s的网络版块: k8s的deployment版块: k8s的service版块: k8s的探针板块: k8s的控制调度板块: k8s的日志监控板块: k8s的流量转发板块: k8s的宏观版块:

Meta的开源力作:Lexical框架,富文本的未来

引言 Lexical 是一个由 Facebook&#xff08;现在称为 Meta&#xff09;开源的可扩展 JavaScript Web 文本编辑器框架。 这个框架特别强调了三个核心特性&#xff1a;可靠性、可访问性以及高性能。 旨在为开发者创造最优的开发体验。 以下是 Lexical 框架的几个关键特点和能…

广东肇庆mes系统服务商 盈致科技

广东肇庆MES系统服务商盈致科技为企业提供专业的MES系统解决方案&#xff0c;帮助企业实现生产过程的数字化管理和优化。盈致科技的服务包括但不限于以下方面&#xff1a;MES系统定制开发&#xff1a;盈致科技可以根据企业的实际需求定制开发MES系统&#xff0c;满足企业特定的…

OJ题讲解——栈与队列

目录 一.有效的括号 1.问题描述 2.问题详解 3.代码 二.用队列实现栈 1.问题描述 2.问题详解 3.代码 三.用栈实现队列 1.问题描述 2.问题详解 3.代码 四.设计循环队列 1.问题描述 2.问题详解 3.代码 一.有效的括号 1.问题描述 OJ链接&#xff1a;. - 力扣&…

归并排序(分治)

归并排序 概念介绍原理解释&#xff1a;案例步骤&#xff1a;稳定性&#xff1a;画图理解如下 代码实现 概念介绍 归并排序&#xff08;Merge Sort&#xff09;是一种经典的排序算法&#xff0c;基于分治的思想&#xff0c;它将待排序数组分成两个子数组&#xff0c;分别排序&…

基于Python+django购物商城系统设计和实现(源码+LW+部署文档+讲解等)

&#x1f497;博主介绍&#xff1a;✌全网粉丝1W,CSDN作者、博客专家、全栈领域优质创作者&#xff0c;博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f31f;文末获取源码数据库&#x1f31f; 感兴趣的可以先收藏起来&#xff0c;还…

iPhone的5G设置怎么更改吗?设置好这些能够优化电池的使用寿命

随着5G技术的普及&#xff0c;iPhone用户现在可以享受到更快的网络速度&#xff0c;但这同时也带来了一个问题&#xff1a;如何在使用5G和保持电池寿命之间找到平衡&#xff1f;苹果公司通过引入“5G Auto”设置&#xff0c;为用户提供了一个智能的解决方案&#xff0c;但用户也…