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

这篇具有很好参考价值的文章主要介绍了【c++】类和对象(四)深入了解拷贝构造函数。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

🔥个人主页:Quitecoder

🔥专栏:c++笔记仓

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

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

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就是另一个参数
【c++】类和对象(四)深入了解拷贝构造函数,c++笔记仓,c++

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++规定,自定义类型的拷贝,都会调用拷贝构造

我们进行调试
【c++】类和对象(四)深入了解拷贝构造函数,c++笔记仓,c++

在这里按F11,我们目的是进入fun1,函数,这里却跳入拷贝构造函数
【c++】类和对象(四)深入了解拷贝构造函数,c++笔记仓,c++
再按f11,才会进入fun1函数中
【c++】类和对象(四)深入了解拷贝构造函数,c++笔记仓,c++
大概过程如下
【c++】类和对象(四)深入了解拷贝构造函数,c++笔记仓,c++
传值传参需要调用拷贝构造

【c++】类和对象(四)深入了解拷贝构造函数,c++笔记仓,c++
【c++】类和对象(四)深入了解拷贝构造函数,c++笔记仓,c++
fun2函数可以直接进入
【c++】类和对象(四)深入了解拷贝构造函数,c++笔记仓,c++

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

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

【c++】类和对象(四)深入了解拷贝构造函数,c++笔记仓,c++
所以,这里也是我们为什么只能用引用传参

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;
}

我们现在屏蔽掉拷贝构造,看会发生什么
【c++】类和对象(四)深入了解拷贝构造函数,c++笔记仓,c++

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

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

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;
}

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

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

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

  • **浅拷贝(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是s1
	Stack(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是s1
	Stack(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) 相同的值。这样保证了新栈在逻辑上与原栈完全相同,拥有相同数量的元素和相同的容量

【c++】类和对象(四)深入了解拷贝构造函数,c++笔记仓,c++
这下我们的问题也就解决了
【c++】类和对象(四)深入了解拷贝构造函数,c++笔记仓,c++

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 提供深拷贝的情况下是安全的

【c++】类和对象(四)深入了解拷贝构造函数,c++笔记仓,c++
本篇内容到此结束,感谢大家观看!!!!文章来源地址https://www.toymoban.com/news/detail-853709.html

到了这里,关于【c++】类和对象(四)深入了解拷贝构造函数的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C++】:类和对象(中)之拷贝构造函数+赋值运算符重载

    在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎 那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢? 拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调

    2024年02月06日
    浏览(46)
  • C++ -3- 类和对象 (中) | 拷贝构造函数 & 赋值运算符重载

    示例: 拷贝初始化构造函数的作用是将一个已知对象的数据成员值拷贝给正在创建的另一个同类的对象 无穷递归 ? Date(Date d){……} 首先,分析 传值传参的过程 传引用传参 : 没有拷贝 的过程,直接传 传值传参: 内置类型 编译器可以直接拷贝(浅拷贝/值拷贝——一个字节

    2023年04月19日
    浏览(78)
  • 【C++学习】类和对象 | 拷贝构造 | 探索拷贝构造函数为什么需要引用传参 | 深拷贝 | 初识运算符重载

    上一篇文章我们开始学习类内的默认成员函数, 这里是传送门,有兴趣可以去看看:http://t.csdn.cn/iXdpH 这篇文章我们继续来学习类和对象的知识。 目录 写在前面: 1. 拷贝构造 2. 拷贝构造函数为什么需要引用传参? 3. 深拷贝 4. 初识运算符重载 写在最后: 我们在创建一个对

    2024年02月11日
    浏览(49)
  • 【C++】类和对象③(类的默认成员函数:拷贝构造函数 | 赋值运算符重载)

    🔥 个人主页: Forcible Bug Maker 🔥 专栏: C++ 目录 前言 拷贝构造函数 概念 拷贝构造函数的特性及用法 赋值运算符重载 运算符重载 赋值运算符重载 结语 本篇主要内容:类的6个默认成员函数中的 拷贝构造函数 和 赋值运算符重载 在上篇文章中我们讲到了类的默认成员函数的

    2024年04月17日
    浏览(43)
  • 【C++】:类和对象(下):explicit || 再谈构造函数 || static成员 || 友元 || 内部类 || 匿名对象 || 拷贝对象时的编译器优化问题 || 再次理解类和对象

    🔫类和对象(下篇) 🔫【本节目标】 🔫1. 再谈构造函数 🔫2. Static成员 🔫3. 友元 🔫4. 内部类 🔫5.匿名对象 🔫6.拷贝对象时的一些编译器优化 🔫7. 再次理解类和对象 🏄1.1 构造函数体赋值 在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值

    2024年01月21日
    浏览(41)
  • 【C++基础(六)】类和对象(中) --拷贝构造,运算符重载

    💓博主CSDN主页:杭电码农-NEO💓   ⏩专栏分类:C++初阶之路⏪   🚚代码仓库:NEO的学习日记🚚   🌹关注我🫵带你学习C++   🔝🔝 本章重点: 本篇文章将详细讲解拷贝构造函数 和运算符重载,并介绍const成员的概念 拷贝构造函数和运算符重载 是类和对象中六大默认成员函数

    2024年02月14日
    浏览(39)
  • 【C++】类和对象(中)之拷贝构造与运算符、操作符重载

    👀 樊梓慕: 个人主页  🎥 个人专栏: 《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》 🌝 每一个不曾起舞的日子,都是对生命的辜负 我们继续学习默认成员函数,本篇文章博主带来的是拷贝构造函数与运算符、操作符重载的讲解,并且还有

    2024年02月05日
    浏览(49)
  • c++类和对象(拷贝构造、运算符重载、初始化列表、静态成员、友元等)

    拷贝构造函数的特征: 1、拷贝构造函数是构造函数的一个重载形式; 2、拷贝构造函数的参数只有一个且必须是同类类型对象的引用, 使用传值方式编译器直接报错 ,因为会引发无穷递归调用。 在c++中自定义类型 传值传参 的时候要调用拷贝构造函数。 3、若未显式定义,

    2024年02月15日
    浏览(36)
  • 【C++】类与对象(构造函数、析构函数、拷贝构造函数、常引用)

       🌈个人主页: 秦jh__https://blog.csdn.net/qinjh_?spm=1010.2135.3001.5343 🔥 系列专栏:   目录 类的6个默认成员函数 构造函数 特性  析构函数 特性  析构的顺序 拷贝构造函数 特性 常引用      💬 hello! 各位铁子们大家好哇。              今日更新了类与对象的构造函数、

    2024年02月21日
    浏览(42)
  • C++奇迹之旅:深入思考拷贝构造函数

    在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。 那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢? 拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动

    2024年04月26日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包