一.拷贝
1. 拷⻉构造函数是构造函数的⼀个重载。
2. 拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。 拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。
3. C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。
4. 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成 员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。
我们先来写一个简单的拷贝。
这里就实现了拷贝。我们调用了拷贝构造函数实现了拷贝功能,这是普通成员变量的深拷贝,我们来讲一下为什么一定要传地址,不能传值呢?
首先编译器上如果你传的是值,此时就会报错,但是为什么不能传呢?这时我们就来讲一下。
-
如果在拷贝构造函数中按值传递参数,例如将
Data(const Data d1)
(假设没有&
),当调用这个拷贝构造函数时,为了传递参数d1
,编译器会创建一个新的Data
对象副本作为参数。这个副本的创建过程本身又会调用拷贝构造函数来进行初始化。-
这样就会导致无限递归地调用拷贝构造函数。因为每次尝试创建参数副本时都需要调用拷贝构造函数,而这个调用又会因为需要创建新的参数副本而再次调用拷贝构造函数,如此循环下去,直到栈溢出等错误发生。
-
简单来说就是在传值的时候,每次都会调用这个拷贝函数,每次都要创建一个副本,每次都调用,所以最后就会陷入循环当中。
-
-
- 传地址(引用)的优势:
-
当使用引用传递参数,如
Data(const Data& d1)
,引用只是原始对象的一个别名。它不需要创建新的对象副本,而是直接操作原始对象。这样就避免了因为创建参数副本而导致的无限递归调用拷贝构造函数的问题,能够正确地实现拷贝构造函数的功能,按照预期的方式复制对象的成员。
-
我们可以验证一下。
调用了一下。
发现我们调用了拷贝函数了。为什么会调用拷贝呢?
当你将一个对象作为参数按值传递给一个函数时(如func(d1)中的d1),通常情况下会调用拷贝构造函数。这是因为函数参数是通过值传递的,需要创建一个新的对象副本作为函数内部的参数。
返回值也是可以调用这个函数的,但是在现代 C++ 编译器中,存在一种优化机制叫做返回值优化(Return Value Optimization,RVO)和具名返回值优化(Named Return Value Optimization,NRVO)。这些优化可能会导致在某些情况下,看起来拷贝构造函数没有被调用。vs2022演示不出来,但是vs2019可以演示出来。
为什么返回值也可以调用呢?
当一个函数返回一个对象时,在没有优化的情况下,会创建一个临时对象来保存函数返回值。这个临时对象是通过调用拷贝构造函数,将函数内部局部对象的内容复制到临时对象中得到的。
当一个函数返回一个对象时(如func2()返回Data类型的对象d1),编译器可以直接在调用函数的栈帧上构造返回值对象,而不是先在函数内部构造一个局部对象(这里是d1),然后再拷贝到调用函数的栈帧上。这种优化避免了不必要的拷贝构造。
以func2()为例,正常情况下,如果没有优化,会先在func2()函数内部构造d1,然后将d1拷贝到main()函数中的一个临时对象位置(用于接收func2()的返回值),这个拷贝过程会调用拷贝构造函数。
其实你传一个地址过去也是可以的,只是把 地址传过去了,不是传值它就不会陷入循环。
这个调用函数,当我们不要改变d1当中的值,我们就可以把它写成const的形式,防止你改变了它的值造成了不必要的影响,你像这里的if语句如果你写错了,少写了一个=,此时d1.year=year,被改变了值,此时都是随机值了,下面的赋值就毫无意义了。
如果我们不写拷贝函数,系统实现的拷贝函数能实现我们的功能吗?
我们运行一下试试。
发现到达了预期结果,和初始化构造函数注意区分。
1.1 浅拷贝
这里就是数组的浅拷贝,此时我们的两个数组都指向了同一块地址,此时你改变一个就会影响到另外一个。
我们来运行一下。
为什么会存在这个错误呢?
存在对内存的错误分配或释放,比如重复释放已经释放过的内存,或者试图访问未分配的内存区域。因为我们每次调用完成一个类对象之后都会调用析构函数,此时同一块空间就被释放了两次内存,就会出现错误了。
我们改变了d2的a[1],但是d1也改变了,此时就出现了问题,我们不想这样。此时就要用到深拷贝了。
1.2 深拷贝
这就是深拷贝,通过一个malloc在堆中建立一个数组,此时就不是指向同一个地址了,互相不影响。
直到main函数结束整个生命周期结束,此时开始执行析构函数了。
一定要记住,你这里定义的是一个指针,不是数组,不能通过sizeof(a)/sizeof(a[0])来求数组的大小。
-
在你的代码中,
p
是一个int*
类型的指针。在 32 - bit 系统中,指针通常占用 4 字节,在 64 - bit 系统中,指针通常占用 8 字节。假设是 64 - bit 系统,sizeof(p)
的值为 8。-
总结一下,就是如果类中是int,double,float等基本类型的话,直接用默认拷贝函数就行(它们各自有各自的空间),如果有指针什么的,就要用深拷贝。
-
-
堆内存资源(如果对象成员中有指针指向堆内存)
- 当一个类的对象通过amlloc操作符在堆上分配内存,并且该类中有一个指针成员指向这块堆内存时,默认析构函数不会自动释放这块内存。但是如果这个指针成员的类型是带有正确析构函数的类(例如
std::string
),那么当对象销毁时,这个成员的析构函数会被调用,从而释放相关资源。
C语言结构体中也存在浅拷贝和上面的基本一样。
我们看一下这个代码,为什么我们注释掉的第二个d3,为什么他还能打印出来值呢?
这里的func2返回的是引用,也就是自己本身,他不是随着函数栈帧的销毁而销毁了吗,为什么还可以返回给d3还可以打印出来值呢?
此时虽然d1销毁了,但是这是基本类型的变量会有自己的空间,被放入一个特定空间以便完成拷贝,如果是指针的话就会出现问题了。
下面没有注释的d3就是指针了,此时我们返回了一个销毁的d1,此时就会出现随机值了,什么原因呢?
因为此时d1的对象已经被销毁了,这块malloc的空间在堆上,但是d1的a指针已经被销毁了,此时访问不到这块内存了,但是成员变量被存在一些特定的空间利于拷贝。如果返回的不是引用,而是Data是返回值,那么它会先通过拷贝构造先创建一个对象再返回,返回的是一个d1的副本,此时就不会出现随机值了,引用返回的是本身,出了函数被摧毁了,所以才出现随机值。
二. 赋值运算符重载
什么是赋值运算符的重载呢?
你像这个代码,它的赋值是合理的,但是如果是两个对象进行比较呢?
我们来举个例子。
此时它就会报错,此时我们应该怎么解决呢?
就要用到赋值运算符的重载了,形式是operator和后⾯要定义的运算符共同构成。
我们来写一个。
我们先别管报错,先解决几个问题,为什么要用引用呢?传值可以吗?
答案是传值是可以的,但是需要开辟空间,但是传引用则不需要开辟空间,节省了空间。
为什么会编译不过呢?
因为我们的成员变量都是私有的,无法在类外访问,所以就出现了编译错误,我们该怎么解决呢?可以通过Java写get方法,或者用我们c++自己的,写在类里面。
这里下面不报错了,但是上面又报错了,这是为什么呢?
因为类里面的函数都自带一个this指针参数,所以此时变成三个参数了,就不符合要求了,就会报错。我们只需要去掉一个参数即可。
此时就是谁调用的它谁就是this,传入的是x2,此时我们主函数中的d1==d2,就相当于是d1.opperator(d2),谁在前面谁就是this,此时就完成了。
注意:
当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规 定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编 译报错。
运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其 他函数⼀样,它也具有其返回类型和参数列表以及函数体。
重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元 运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。
如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算 符重载作为成员函数时,参数⽐运算对象少⼀个。
运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。







我们先看一下这两个代码的区别:
第一个:
Data d2 = d1;
这一行是对象的初始化。它会调用拷贝构造函数来创建d2
,使得d2
是d1
的一个副本。在拷贝构造函数中,通常会对每个成员变量进行正确的复制操作。对于普通成员变量,会直接复制其值;对于指针成员变量,会重新分配内存(例如通过malloc
),然后将d1
中指针所指向的数据复制到新分配的内存中(例如通过memcpy
),这样d2
和d1
的指针成员变量会指向不同的内存区域,但其中的数据是相同的。- 第二个:
Data d1(2023, 1, 6);
和Data d2(2024,1,1);
这两行代码分别通过构造函数创建了两个独立的Data
类对象d1
和d2
。此时d1
和d2
的成员变量(包括普通成员变量和指针成员变量)都有各自独立的值。对于d1
,其成员变量按照构造函数传入的参数(2023, 1, 6)
初始化;对于d2
,其成员变量按照(2024,1,1)
初始化。- 然后
d2 = d1;
这一行执行了赋值操作。由于d2
已经存在,这里会调用赋值运算符(如果没有自定义赋值运算符,编译器会提供默认的赋值运算符),将d1
的成员变量的值赋给d2
的相应成员变量。对于指针成员变量(假设是a
),如果是默认赋值运算符,会简单地复制指针的值,这可能导致两个指针指向同一块内存区域(如果没有正确处理深拷贝的话)。
- 在第二种情况(
Data d1(2023, 1, 6); Data d2(2024,1,1); d2 = d1;
)中,当执行d2 = d1;
这个赋值操作时,正常情况下不会调用拷贝构造函数。因为此时d2
对象已经被构造完成,这里是对已存在的d2
对象进行赋值操作,所以会调用赋值运算符(operator =
)。
所以此时我们的代码就有问题了,会将同一块空间释放两次,我们该怎么解决呢?
此时就可以了,再创建一块空间,此时就正常了,都有各自的空间。
最后简单来说就是如果一个对象未初始化用了=,此时就要用拷贝构造(此时不会调用拷贝构造),如果是两个都已经初始化的对象用=,此时就是赋值,就要调用赋值运算符了(此时不会调用拷贝构造),你像函数传值如果是简单传值,就是利用拷贝构造创建一个副本,这个副本就是形参。
三.结束语
感谢大家的查看,希望可以帮助到大家,做的不是太好还请见谅,其中有什么不懂的可以留言询问,我都会一一回答。 感谢大家的一键三连。