资讯动态

拷贝构造的深度解析

发布时间:2026/10/2 16:51:47 来源:尧图企业网站定制
目录摘要一概念二书写格式三触发拷贝构造的三种情况四注意事项1二次析构2C的不足3静态数组自带深拷贝五思考六总结摘要本文讲解拷贝构造从概念到书写格式明白为什么参数一定是引用再到拷贝构造的触发场景以及拷贝构造怎么弥补了C的不足然后到使用拷贝构造的注意事项最后总结拷贝构造的特点和细节一概念拷贝构造是构造函数的重载换句话说拷贝构造也是一种构造函数不过拷贝构造的参数是一个本类的对象引用仅此而已构造函数对象的内存空间已经分配好了构造函数负责初始化这个对象拷贝构造用一个已存在的对象来复制构造出另一个新对象所以你可以理解为构造函数利用直接的参数如year, month, day来初始化已经分配好空间的对象拷贝构造利用一个已存在的对象的成员变量如other.year_, other.month_, other.day_来初始化已经分配好空间的对象注拷贝构造又叫复制构造两者完全等价某些报错中会遇到复制构造这个名词二书写格式拷贝构造函数的第⼀个参数必须是类类型对象的引用如果使用传值方式则编译器直接报错因为语法逻辑上会引发无穷递归调用所以日期类的拷贝构造正确写法// 拷贝构造利用已存在对象 other 的成员变量来初始化新对象 Date(Date other) : year_(other.year_), month_(other.month_), day_(other.day_) {} //main中 // 拷贝构造用 d2 的成员变量初始化 d1 Date d2(d1); // 调用拷贝构造// 拷贝构造利用已存在对象 other 的成员变量来初始化新对象 // 初始化在函数体内完成 Date(Date other) { year_ other.year_; month_ other.month_; day_ other.day_; } //main中 // 拷贝构造用 d2 的成员变量初始化 d1 Date d2(d1); // 调用拷贝构造解释这两种都是可以的因为拷贝构造是构造函数的重载所以拷贝构造也可以用初始化列表去初始化d2这个对象的三触发拷贝构造的三种情况❓️重点在于理解为什么拷贝构造的参数类型一定要是引用要理解为什么参数是引用类型则需要从触发拷贝构造的情况去反推C规定自定义类型对象进行拷贝行为必须调用拷贝构造下面3种情况会触发拷贝构造①使用已存在对象创建新对象如Date d2(d1(已存在))②函数参数类型为类这种类型的对象③函数返回值类型为类这种类型的对象我们用②来举例验证拷贝构造的参数类型一定要是引用现在我们去掉拷贝构造的引用❌️错误的拷贝构造写法如下// 拷贝构造利用已存在对象 other 的成员变量来初始化新对象 // 初始化在函数体内完成 Date(Date other) { year_ other.year_; month_ other.month_; day_ other.day_; } //main中 // 拷贝构造用 d2 的成员变量初始化 d1 Date d2(d1); // 调用拷贝构造解释用②来举例此时我们 Date d2(d1);会调用拷贝构造然后发现拷贝构造函数的参数是对象值传递所以满足②又会去调用拷贝构造然后发现又满足②又去调用拷贝构造........所以无穷递归直接报错下面用两种图来理解无穷递归直接报错的过程图1图2总结其实本质就是触发拷贝构造去调用拷贝构造时发现拷贝构造函数本身的参数也触发了拷贝构造就导致了无限递归所以任何触发拷贝构造的情况当其来到调用拷贝构造这一步时我们就应该避免调用拷贝构造的时候又触发了②这种情况所以这就是为什么拷贝构造的参数必须是引用此外需要注意的是当我们执行Date d2(d1);时拷贝构造函数内部的other是d1的引用而类中的this指针就是指向d2的所以 year_ other.year_;其实就等同于 *this.year_ other.year_正确赋值顺序如下// 拷贝构造利用已存在对象 other 的成员变量来初始化新对象 // 初始化在函数体内完成 Date(Date other) { year_ other.year_; month_ other.month_; day_ other.day_; } //main中 // 拷贝构造用 d2 的成员变量初始化 d1 Date d2(d1); // 调用拷贝构造所以当对other是d1引用*this是d2这两组对应关系理解不深刻的时候往往就会出现错误的拷贝构造的书写情况如下// 拷贝构造利用已存在对象 other 的成员变量来初始化新对象 // 初始化在函数体内完成 Date(Date other) { other.year_ year_; other.month_ month_; other.day_ day_; } //main中 // 拷贝构造用 d2 的成员变量初始化 d1 Date d2(d1); // 调用拷贝构造解释这必然是错的d1已经存在我们要用d1的成员变量给d2赋值写反之后反而是让d1的成员变量被改变了且d2的成员变量如果是int可能是随机值如果是指针可能是空指针还会造成空指针的解引用错误所以为了安全起见我们的拷贝构造参数的引用常常和const是绑定在一起的加上一个const就可以避免other引用的变量被修改也从根本上避免了写反的情况正确且安全的拷贝构造写法如下// 拷贝构造利用已存在对象 other 的成员变量来初始化新对象 // 初始化在函数体内完成 Date(const Date other) { year_ other.year_; month_ other.month_; day_ other.day_; } //main中 // 拷贝构造用 d2 的成员变量初始化 d1 Date d2(d1); // 调用拷贝构造此时即使你写反了也会被报错所提醒不会进一步加深错误四注意事项1二次析构既然拷贝构造是构造函数的重载那拷贝构造当然也是默认成员函数的一种所以我们不写拷贝构造编译器也会自己默认生成拷贝构造那编译器生成的拷贝构造会做什么呢自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝)对自定义类型成员变量会调用他的拷贝构造。这意味着如果你的成员变量都是int char这种内置类型则你压根不需要写拷贝构造因为浅拷贝就已经能够完成任务而如果你的成员变量有指针指针指向一份资源的话那么你就必须要手动写拷贝构造了因为如果你不写那么默认生产的拷贝构造是浅拷贝则通过拷贝构造生成的新的对象会和老对象的成员变量指针是一致的二者指向同一份资源二者指向同一份资源会导致什么呢当两个对象都结束生命周期的时候就会调用析构函数则同一份空间会被free两次必然报错所以对于栈啊队列啊这种类的成员变量指针指向一份资源必须手动拷贝构造其实换个角度但凡需要手动写析构函数的那么99%都是有资源的所以也必须手动写构造函数和拷贝构造2C的不足❓️那C没有拷贝构造岂不是一个坑是的拷贝构造的意义正是为了解决C语言中结构体传参的天然缺陷比如栈类C中有了正确的拷贝构造之后我们可以随意采用栈的值传递因为触发拷贝构造之后形参为引用接受直接完成二者赋值整个过程没有任何额外的空间开销而C语言的栈作为值传递传参则必定出错因为C语言就是采用的浅拷贝所以当形参和实参会生成两份栈两份栈中的指针变量指向同一份资源当各自调用StackDestroy(s);则会出现对同一份资源二次free的问题必然报错所以在C引用中我们往往采取的措施是址传递我们传递的是栈的地址过去这样的确是一种解决措施但是和C的拷贝构造相比就显得繁琐了前者需要传递s参数为指针类型指针也要开辟空间后者只需传递s参数为引用类型引用不开辟空间对比分析对比维度C语言址传递C拷贝构造传参方式必须取地址s直接传递s函数签名void func(Stack* ps)void func(Stack s)内部使用必须用-访问成员直接用.访问成员代码简洁性繁琐到处是指针简洁像操作普通变量内存安全需手动管理自动管理可读性指针满天飞清晰直观拷贝构造的最大意义让复杂的数据结构也能像int、double一样简单、安全地传递和使用这正是C对C的最重要改进之一3静态数组自带深拷贝❓️现在我们知道了malloc出的数组算资源需要手动编写拷贝构造那成员变量有没有可能就是int arr[10]这种成员变量数组呢这样即使采取编译器默认生产的拷贝构造也会逐字节拷贝出一个新的数组(元素相同)对吗是的对于int arr[10]这种成员变量拷贝构造后各自拥有一个独立的数组所以不出错数组根本没有指针的值可以复制只有数据本身所以天然就是深拷贝。但是这种写法的数组缺点多对比固定数组 vs 动态数组特性固定数组int arr_[10]动态数组int* data_大小编译期固定运行期动态空间位置栈上堆上内存效率可能浪费或不足按需分配精确拷贝开销每次拷贝整个数组只拷贝指针浅拷贝或选择性深拷贝扩展性无法扩展可以 realloc/resize性能栈访问更快堆访问稍慢内存限制受栈大小限制8MB受堆大小限制更大拷贝构造天然深拷贝需要手动实现深拷贝五思考❓️拷贝构造要求参数必须为引用那当错误写法中不使用引用时这个函数不是应该不叫作拷贝构造了吗应该是构造函数那为什么调用拷贝构造的时候还会调用该函数从而导致无线递归不应该无法识别出这是一个拷贝构造函数吗①错编译器可以直接识别出你这个函数是拷贝构造而且是写错的拷贝构造让我们运行没有引用的拷贝构造时报错已经说明了编译器看出来了这是一个错误的拷贝构造参数不应是类类型缺乏引用②并且编译器的报错直接阻止了无限递归的发生而不是让代码编译通过后发生无限递归。避免了更大的错误和损失❓️那怎么样让一个参数为类对象值传递的函数被识别成构造函数呢所以像下面这两种刻意写错的拷贝构造都会被编译器识别出来并报错// ❌ 非法拷贝构造 会被识别出错误第一个参数是 Date且只有一个参数 Date(Date other) : year_(other.year_), month_(1), day_(1) {} // ❌ 非法拷贝构造 会被识别出错误第一个参数是 Date即使有 const Date(const Date other) : year_(other.year_), month_(1), day_(1) {}而这两种写法才会被识别为是构造函数的重载class Date { public: // ✅ 普通构造函数第一个参数是 Date但有第二个参数 Date(Date other, int x) : year_(other.year_), month_(x), day_(1) {} // ✅ 普通构造函数第一个参数是 Date但有多余参数且有缺省值 Date(Date other, int x 0) : year_(other.year_), month_(x), day_(1) {} };解释但此时我们也演示不出无限递归因为编译器找不到拷贝构造函数会直接默认生产拷贝构造进行浅拷贝而对于日期类浅拷贝就够用了对于栈类浅拷贝会造成二次析构但无论是哪种都不会无限递归六总结①.拷贝构造函数是构造函数的一个重载。②.拷贝构造函数的第一个参数必须是类类型对象的引用如果使用传值方式编译器直接报错因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数但是第一个参数必须是类类型对象的引用后面的参数必须有缺省值。③.C规定自定义类型对象进行拷贝行为必须调用拷贝构造所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。④.若未显式定义拷贝构造编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝)对自定义类型成员变量会调用他的拷贝构造。⑤.像Date这样的类成员变量全是内置类型且没有指向什么资源编译器自动生成的拷贝构造就可以完成需要的拷贝所以不需要我们显示实现拷贝构造。像Stack这样的类虽然也都是内置类型但是_a指向了资源编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员编译器自动生成的拷贝构造会调用Stack的拷贝构造也不需要我们显式实现MyQueue的拷贝构造。这里还有一个小技巧如果一个类显式实现了析构并释放资源那么他就需要显式写拷贝构造否则就不需要。⑥.传值返回会产生一个临时对象调用拷贝构造传值引用返回返回的是返回对象的别名(引用)没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象函数结束就销毁了那么使用引用返回是有问题的这时的引用相当于一个野引用类似一个野指针一样。传引用返回可以减少拷贝但是一定要确保返回对象在当前函数结束后还在才能用引用返回。对第⑥点的解释在我们构造函数博客中我们已经讲过函数采用值返回是会生成临时变量的把返回变量的值赋给临时变量真正的函数返回值是一个具有常性的临时变量所以如果函数返回值是一个类类型同理会生成临时变量所以就会有把类类型的值赋值给临时变量这一个拷贝的动作而上文说过C规定自定义类型对象进行拷贝行为必须调用拷贝构造所以函数返回值为类类型就会触发拷贝构造最后介绍调用拷贝构造的另一种写法Stack st1; // 默认构造 Stack st2(st1); // ① 括号法直接调用拷贝构造 Stack st3 st1; // ② 等号法也是拷贝构造不是赋值解释使用等号也可以触发拷贝构造但是这和后面的赋值符重载容易混淆只需记住st1存在st2不存在就是触发拷贝构造st1和st3两者都存在就是触发赋值运算符重载代码性质说明Stack st3 st1;✅ 拷贝构造st3首次定义用st1初始化st3 st1;❌ 赋值操作st3已经存在这是赋值运算符 [ 作者 ] shylyly [ 首次发布 ] 2026.7.16❌ [ 最新修改 ] 2026.10.1 [ 声明 ] 由于笔者水平有限文中难免有疏漏或不妥之处还望读者不吝赐教

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑