news 2026/8/20 0:01:43

C++ 类编译器自动生成的默认函数 | 拷贝构造函数 vs 拷贝赋值运算符(赋值构造)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++ 类编译器自动生成的默认函数 | 拷贝构造函数 vs 拷贝赋值运算符(赋值构造)

C++ 类编译器自动生成的默认函数

当你写一个类,不手动写这些成员函数时,C++ 编译器会在满足条件下自动生成 6 个特殊成员函数:

  1. 默认构造函数
  2. 拷贝构造函数
  3. 拷贝赋值运算符operator=
  4. 移动构造函数(C++11 新增)
  5. 移动赋值运算符(C++11 新增)
  6. 析构函数

注意:编译器生成不代表一定生成;如果你自己手写了其中一个,会抑制部分自动生成。

1. 默认构造函数类名()

触发生成条件

没有写任何构造函数,编译器自动生成一个无参默认构造

cpp

class Test{ public: //编译器自动生成:Test(){} }; Test t; //可以调用,自动生成的构造
  • 函数体是空:不会初始化成员变量!
    • 内置类型 (int*,double 等):堆 / 栈上是随机垃圾值
    • 类类型成员:会调用它自己的默认构造

❗抑制生成(编译器不再自动生成默认构造)

只要你手写了任意一个构造函数(有参、拷贝构造),编译器就不会生成默认构造。

cpp

class Test{ public: Test(int a){} }; Test t; //编译报错!没有无参构造,编译器不会自动补

2. 析构函数~类名()

触发生成条件

没有手写析构函数,编译器自动生成默认析构。

cpp

class Test{ //编译器生成 ~Test(){} };
  • 函数体空;会自动调用成员、父类的析构。
  • 默认析构不是 virtual!只有基类手写virtual ~A()才是虚析构。

重点:做多态继承,基类要手动写 virtual 析构;编译器生成的析构没有 virtual。

抑制生成

手写析构函数,编译器不再生成默认析构。

3. 拷贝构造函数Test(const Test& other)

格式:拷贝构造参数必须是本类的引用,一般 const 引用

触发生成条件

没有手写拷贝构造函数,编译器自动生成浅拷贝拷贝构造浅拷贝:逐成员拷贝(值拷贝)

  • int、指针:直接复制值;指针只复制地址,不复制指向的堆内存!
  • 类成员:调用该成员自己的拷贝构造。

示例:

cpp

class B{ public: int* p; //编译器自动生成 B(const B& other): p(other.p) {} }; B b1; b1.p = new int[100]; B b2 = b1; //调用自动生成拷贝构造,b2.p 和 b1.p指向同一块堆内存! //析构的时候,两个对象delete[]p,**重复释放,程序崩溃**

这就是经典浅拷贝 bug;有堆资源,需要自己手写深拷贝。

抑制生成

手写拷贝构造,编译器不再自动生成。

4. 拷贝赋值运算符Test& operator=(const Test& other)

触发生成条件

没有手写operator=,编译器自动生成,同样是浅拷贝逐成员赋值

cpp

B b1,b2; b1.p = new int[10]; b2 = b1; //调用编译器生成赋值,b2.p = b1.p,指针共享堆内存

返回值是Test&,支持链式赋值a=b=c

抑制生成

手写拷贝赋值,编译器不生成。

拷贝构造:创建新对象;拷贝赋值:对象已经存在,把别的对象赋值给已经存在的对象。

5. 移动构造函数Test(Test&& other)C++11

触发生成条件

没有手写:拷贝构造、拷贝赋值、移动构造、移动赋值、析构,编译器才自动生成移动构造。

  • 参数是右值引用 &&
  • 作用:把临时对象的资源偷过来,不用拷贝,性能高。
  • 编译器自动生成版本:逐成员移动

⚠只要你手写了析构 / 拷贝构造,编译器就不会自动生成移动构造!

6. 移动赋值运算符Test& operator=(Test&& other)C++11

触发生成条件

同上,五个函数都没有手动写,编译器才生成移动赋值;执行逐成员移动。

移动语义例子

cpp

Test func(){ Test t; return t; //返回临时对象,会调用移动构造 } Test obj = func();

一张表:什么时候编译器自动生成(重点记忆)

表格

自动生成函数什么时候编译器生成什么操作会抑制它不再生成
默认构造Test()没有手写任何构造函数手写任意构造(有参、拷贝构造)
析构~Test()没有手写析构函数手写析构函数
拷贝构造Test(const Test&)没有手写拷贝构造手写拷贝构造
拷贝赋值operator=(const Test&)没有手写拷贝赋值手写拷贝赋值
移动构造Test(Test&&)C++11拷贝、赋值、析构全都没写手写拷贝构造 / 拷贝赋值 / 移动构造 / 移动赋值 / 析构
移动赋值operator=(Test&&)C++11拷贝、赋值、析构全都没写手写拷贝构造 / 拷贝赋值 / 移动构造 / 移动赋值 / 析构

古老规则:如果你手写了析构,编译器依然生成拷贝构造、拷贝赋值;但是不会生成移动版本

四 / 五法则(工程必背)

  1. 三法则(C++98):如果你手动写了其中一个:析构 / 拷贝构造 / 拷贝赋值,那么另外两个也应该手写。

场景:类内部持有堆内存指针,手写析构做 delete;那拷贝构造、拷贝赋值必须手写深拷贝,否则浅拷贝造成双重释放崩溃。

  1. 五法则(C++11):写了析构 / 拷贝构造 / 拷贝赋值;建议同时手写移动构造、移动赋值。

也可以用=default显式让编译器生成,=delete禁止生成。

=default 和 =delete 用法

cpp

class Test { public: Test() = default; //强制编译器生成默认构造 Test(const Test&) = default; //强制生成拷贝构造 Test& operator=(const Test&) = delete; //删除拷贝赋值,禁止赋值 ~Test() = default; };

举例子,结合你刚刚代码中的 B 类

cpp

class B : public A { int *p; //堆指针 public: B(){ p=new int[1024]; } //手写构造,编译器不会生成默认构造 ~B(){ delete[] p; } //手写析构 //编译器自动生成拷贝构造 B(const B& b):p(b.p){} 浅拷贝! //编译器自动生成拷贝赋值 operator=(const B&b){ p=b.p; }浅拷贝 //⚠因为手写析构,编译器**不会自动生成移动构造、移动赋值** };

B 这里编译器生成的拷贝构造是浅拷贝,如果执行B b2 = b1,两个对象 p 指向同一块堆内存,析构的时候两次 delete [] p,直接崩溃。这种类就必须自己手写深拷贝的拷贝构造和拷贝赋值,遵守三法则。

手写深拷贝示例

cpp

B(const B& other) { p = new int[1024]; //分配自己独立堆内存 memcpy(p,other.p,1024*sizeof(int)); cout<<"B深拷贝构造"<<endl; } B& operator=(const B& other) { if(this == &other) return *this; //防止自赋值 delete[] p; //释放旧资源 p = new int[1024]; memcpy(p,other.p,1024*sizeof(int)); return *this; }

补充误区

  1. 编译器自动生成析构不是 virtual!只有基类手动写virtual ~A(){}才是虚析构。
  2. 自动拷贝构造是浅拷贝!指针成员只会复制地址,不会复制堆内存。
  3. 只要手写析构,移动构造、移动赋值就不会自动生成,C++11 规则。
  4. 默认构造不会初始化内置类型成员!栈对象 int / 指针成员是垃圾值。

如果你需要,我可以写一份完整可运行 demo,演示浅拷贝崩溃、深拷贝、=default、=delete 全部效果。

拷贝构造函数 vs 拷贝赋值运算符(赋值构造)

拷贝构造:创建新对象; 拷贝赋值:对象已经存在,给已有的对象赋值

1. 函数写法

① 拷贝构造函数

cpp

// 拷贝构造:参数必须是同类的引用,const 是推荐写法 class Test { public: int* p; //拷贝构造函数 Test(const Test& other) { cout << "拷贝构造函数" << endl; //浅拷贝版本 //p = other.p; //深拷贝版本(持有堆内存必须深拷贝) p = new int[100]; memcpy(p, other.p, 100*sizeof(int)); } };
  • 函数没有返回值,名字和类名完全一样。
  • 参数必须是本类引用,不能传值Test(Test other),会无限递归。

② 拷贝赋值运算符 operator=

cpp

class Test { public: int* p; Test& operator=(const Test& other) { cout << "拷贝赋值运算符" << endl; //1. 处理自赋值:自己赋值给自己,直接返回 if(this == &other) { return *this; } //2. 释放自己旧的堆资源 delete[] p; //3. 分配新内存,拷贝数据(深拷贝) p = new int[100]; memcpy(p, other.p, 100*sizeof(int)); //4. 返回*this,支持链式赋值 a=b=c; return *this; } };
  • 返回值必须是Test&引用,支持链式赋值。
  • 参数同样用const Test& other

2. 触发调用场景(核心区别)

cpp

Test t1; t1.p = new int[100]; //========= 调用【拷贝构造】:正在创建新对象 ========= Test t2 = t1; //定义新对象,用别的对象初始化 Test t3(t1); //直接调用拷贝构造 Test t4{t1}; Test func(Test tmp){ return tmp; } Test t5 = func(t1); //函数返回、传值参数,也会触发拷贝构造 //========= 调用【拷贝赋值operator=】:对象已经存在!========= Test t6; t6.p = new int[100]; t6 = t1; // t6已经存在,把t1赋值给t6 → 拷贝赋值!

表格

项目拷贝构造函数拷贝赋值运算符operator=
本质创建一个全新对象对象已经存在,覆盖原有对象内容
函数签名Test(const Test& other)无返回值Test& operator=(const Test& other)返回引用*this
调用时机定义新对象时初始化:Test t2 = t1;对象已经实例化完成,执行t6 = t1;赋值
堆内存处理不需要释放旧内存(新对象还没有分配资源)必须先释放自己旧的堆内存,防止内存泄漏
自赋值判断不需要,新对象不存在自己的资源必须判断this == &other,否则 delete 自己内存直接崩溃

重点坑:

  • 拷贝构造:新对象,还没有旧堆内存,不要写 delete
  • 拷贝赋值:对象已经存在,里面有旧堆内存,一定要先 delete [] 释放旧资源,否则内存泄漏;还要判断自赋值。

3. 浅拷贝版本(编译器自动生成的版本)

编译器自动生成的版本,两者都是简单逐成员复制(浅拷贝)

cpp

//编译器生成拷贝构造 Test(const Test& other): p(other.p) { } //编译器生成拷贝赋值 Test& operator=(const Test& other) { p = other.p; return *this; }

只复制指针的值,两个对象p指向同一块堆内存,析构的时候两次 delete [],程序崩溃。只要类里面有指针指向堆内存,必须手动写深拷贝版本!

4. 完整可运行示例

cpp

#include <iostream> #include <cstring> using namespace std; class Test { public: int* p; Test() { p = new int[100]; cout << "默认构造" << endl; } //拷贝构造函数(深拷贝) Test(const Test& other) { cout << "====拷贝构造执行====" << endl; p = new int[100]; memcpy(p, other.p, sizeof(int)*100); } //拷贝赋值运算符(深拷贝) Test& operator=(const Test& other) { cout << "====拷贝赋值执行====" << endl; //自赋值检测 if(this == &other) return *this; //释放自身旧资源 delete[] p; //分配新内存复制 p = new int[100]; memcpy(p, other.p, sizeof(int)*100); return *this; } ~Test() { delete[] p; cout << "析构" << endl; } }; int main() { Test t1; Test t2 = t1; //拷贝构造,新建t2 Test t3; t3 = t1; //拷贝赋值,t3已经存在 return 0; }

输出:

plaintext

默认构造 ====拷贝构造执行==== 默认构造 ====拷贝赋值执行==== 析构 析构 析构

5. 常见混淆

cpp

Test t2(t1); //拷贝构造 Test t2 = t1; //拷贝构造,不是赋值!这里是初始化,t2刚创建 Test t2; t2 = t1; //拷贝赋值!t2早已创建完毕

6. 三法则回顾

如果手写了析构(里面做 delete 堆内存),必须同时手写拷贝构造、拷贝赋值运算符,否则编译器生成浅拷贝会双重释放崩溃。

补充:禁止拷贝

如果你不想对象被拷贝 / 赋值,可以用=delete

cpp

Test(const Test&) = delete; Test& operator=(const Test&) = delete;

如果你想,我可以再对比移动构造和移动赋值的写法与区别。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/19 23:52:31

基于改进CASCADE_RCNN的噬菌体检测算法研究

概述 本项目旨在开发一种基于改进CASCADE_RCNN的噬菌体检测算法&#xff0c;专门用于实验室噬菌体激活状态的识别。作为目标检测任务&#xff0c;该算法采用CASCADE_RCNN\cascade-rcnn_r50-caffe_fpn_1x_coco作为后端模型&#xff0c;针对二分类问题&#xff08;activated和no…

作者头像 李华
网站建设 2026/8/19 23:51:28

太阳能树莓派供电系统:从硬件选型到软件优化的完整实践指南

1. 项目概述&#xff1a;为什么选择太阳能驱动树莓派&#xff1f; 如果你和我一样&#xff0c;喜欢把树莓派这种微型电脑部署在户外、阳台或者一些没有稳定市电的地方&#xff0c;比如远程气象站、花园监控摄像头或者野外数据采集点&#xff0c;那么供电问题绝对是个绕不开的坎…

作者头像 李华
网站建设 2026/8/19 23:50:58

DARRMS算法:资源受限多智能体系统的动态注意力半径优化

1. 项目概述&#xff1a;当多智能体系统遇上资源瓶颈在机器人集群、无人机编队、物联网节点协同这些领域&#xff0c;多智能体系统&#xff08;MAS&#xff09;正变得越来越普遍。我们理想中的场景是&#xff0c;每个智能体都像电影里的超级英雄&#xff0c;拥有全局视野和无限…

作者头像 李华
网站建设 2026/8/19 23:49:42

NL2SQL智能体系统:模式感知与多智能体协同实现自然语言数据查询

1. 从“听懂话”到“会查数”&#xff1a;NL2SQL的进化与Agentic System的破局 如果你做过数据分析&#xff0c;或者和数据库打过交道&#xff0c;大概率经历过这种场景&#xff1a;业务同事跑过来&#xff0c;指着屏幕上的报表说&#xff1a;“我想看看上个月华东地区销售额超…

作者头像 李华
网站建设 2026/8/19 23:45:08

AC/DC通用软启动器设计:从PCB布局到控制算法的硬件工程实践

1. 项目概述&#xff1a;什么是软启动器&#xff1f; 如果你拆开过家里的空调、大功率风扇或者工业上的电机控制柜&#xff0c;可能会注意到一个现象&#xff1a;这些设备在通电的瞬间&#xff0c;并不会“嗡”的一声直接全速运转&#xff0c;而是会有一个缓慢加速的过程。这个…

作者头像 李华