尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
【C++】 初阶篇 类与对象(中) 六大默认成员函数
前言在 C 面向对象编程中“类与对象” 是核心基石而默认成员函数则是编译器为类自动生成的 “隐藏助手”—— 即使你没显式写它们也在默默工作。本文将从实际开发角度拆解类的 6 个默认成员函数帮你搞懂 “为什么需要它们”“怎么用” 以及 “踩坑点在哪”。同时友情提示一下这一篇的深拷贝可以先不管我是学完后面的回来写博客所以随便实现了一下我在 STL 的底层实现的时候会具体细致地讲。一、什么是默认成员函数如果一个类中什么成员都没写空类编译器会自动生成 6 个默认成员函数。它们的核心作用是处理对象的 “初始化、清理、拷贝、赋值、取地址” 等基础操作。空类class Date {};看似空实则包含以下 6 个默认成员函数函数类型核心作用显式实现场景构造函数对象创建时初始化需自定义初始化逻辑如日期析构函数对象销毁时清理资源类内有动态内存如栈、链表拷贝构造函数用已有对象创建新对象Date d2(d1)涉及资源拷贝避免浅拷贝赋值运算符重载对象间赋值d2 d1涉及资源赋值避免浅拷贝普通取地址重载获取普通对象地址几乎不用默认实现足够const取地址重载获取const对象地址几乎不用默认实现足够需要注意的是这 6 个中最重要的是前 4 个。最后两个取地址重载不重要我们稍微了解一下即可。其次就是 C11 以后还会增加两个默认成员函数移动构造和移动赋值这个我们后面再讲解。默认成员函数很重要也比较复杂我们要从两个方面去学习第一我们不写时编译器默认生成的函数行为是什么是否满足我们的需求。第二编译器默认生成的函数不满足我们的需求我们需要自己实现那么如何自己实现二、构造函数2.1 构造函数的定义每次创建对象时编译器都会自动调用一次构造函数 —— 它的核心不是 “创建对象”开空间由编译器负责而是 “给对象的成员变量赋初始值”避免成员变量出现随机值。构造函数的本质是要替代我们以前写的Init函数的功能构造函数自动调用的特点就完美的替代了Init。2.2 构造函数特性与创建函数名与类名相同。无返回值。返回值啥都不需要给也不需要写void不要纠结C 规定如此构造函数可以重载无参、带参、全缺省都可。classDate{public:// 1. 无参构造编译器默认生成的就是类似无参版本Date(){_year1900;_month1;_day1;}// 2. 带参构造重载版本支持自定义初始化Date(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};// 使用示例intmain(){Date d1;// d1实例化时自动调用无参构造Dated2(2024,5,20);// 调用带参构造// Date d3(); // 错误这是声明函数不是创建对象return0;}写到这里我们已经发现构造函数的优势了但是还可以结合我们的缺省值这个知识点我们可以只写一个来替代我们刚刚写的那两个构造函数请看全缺省构造函数Date(intyear2025,intmonth2,intday20){_yearyear;_monthmonth;_dayday;}瞬间减少了我们的代码量。但是这个全缺省的构造函数和无参的构造函数不能同时出现因为这样编译器就不知道调用哪一个了。2.3 编译器默认生成的构造函数如果类中没有显式定义构造函数则 C 编译器会自动生成一个无参的默认构造函数一旦用户显式定义编译器将不再生成。我们先来看看编译器生成的classDate{public:voidprint(){cout_year-_month-_dayendl;}private:int_year;int_month;int_day;};intmain(){Date d1;// d1实例化时自动调用默认生成的无参构造d1.print();return0;}我们发现好像编译器生成的默认构造函数好像啥也没干还是随机值那它有啥用别急我们继续看classTime{public:// Time的默认构造Time(){coutTime()被调用endl;_hour0;_minute0;_second0;}private:int_hour,_minute,_second;};classDate{public:voidprint(){cout_year-_month-_dayendl;}private:int_year;int_month;int_day;Time _t;};intmain(){Date d1;// d1实例化时自动调用默认生成的无参构造d1.print();return0;}我们发现系统生成的默认构造函数会调用 “自定义类型成员”Time的默认构造函数。也就是它没给内置类型int/char等赋初始值但是会调用 “自定义类型成员” 的默认构造函数。注意再一次强调无参构造、全缺省构造、编译器默认生成的构造函数都属于 “默认构造函数”—— 它们的共同点是 “调用时不用传参”。一个类中只能有一个默认构造函数否则会冲突三、析构函数3.1 析构函数的定义通过前面构造函数的学习我们知道一个对象是怎么来的那一个对象又是怎么没的呢析构函数与构造函数功能相反析构函数不是完成对对象本身的销毁C 规定对象在销毁时生命周期结束会自动调用析构函数完成对象中资源的清理释放工作。析构函数的功能类比我们之前的Destroy功能。为什么析构函数不是完成对对象本身的销毁因为对象是存在栈帧只有函数结束栈帧销毁并释放3.2 析构函数特性与创建函数名 ~有点像取反的意思类名无参数、无返回值一个类只能有一个析构函数不能重载若未显式实现编译器会生成默认析构函数。3.3 编译器默认析构函数和默认构造类似默认析构函数会调用 “自定义类型成员” 的析构函数而内置类型成员无需清理内存由编译器回收classTime{public:~Time(){cout~Time()被调用endl;}private:int_hour,_minute,_second;};classDate{private:int_year1970;// 内置类型默认析构不处理Time _t;// 自定义类型默认析构调用~Time()};intmain(){Date d;// 程序结束时输出~Time()被调用return0;}3.4 什么时候要显式写析构函数没有空间申请的类不需要析构函数比如只有内置类型的因为它们没有在堆上开辟空间都是在栈上开空间出来它们出了函数的作用域会自动销毁把空间还给操作系统。日期类classDate{public://…………………private:int_year;int_month;int_day;};日期类都是内置类型我们就可以不用写栈类的析构函数typedefintDataType;classStack{public:// 构造函数申请动态内存Stack(size_t capacity4){_array(DataType*)malloc(sizeof(DataType)*capacity);if(_arraynullptr){perror(malloc失败);return;}_capacitycapacity;_size0;}// 析构函数释放动态内存必须显式写~Stack(){if(_array!nullptr){free(_array);// 释放内存_arraynullptr;// 避免野指针_capacity0;_size0;}}private:DataType*_array;// 动态内存指针size_t _capacity;size_t _size;};// 使用示例voidTestStack(){Stack s;// 创建对象调用构造s.Push(1);s.Push(2);}// s出作用域自动调用析构释放内存因为像栈这种它有动态开辟内存在堆上的系统生成的它不会去清理堆上的就不够用了我们要显式写析构函数。3.5 总结对象生命周期结束系统会自动调用默认的析构函数。有资源申请的类需要写析构函数。有多个对象需要析构的时候后创建的先析构。因为栈帧后进先出。四、拷贝构造函数4.1 拷贝构造函数的定义用一个已存在的对象创建新对象时如Date d2(d1)编译器会自动调用拷贝构造函数。也就是说拷贝构造是一个特殊的构造函数。它的核心是 “复制对象的成员变量”但要注意 “浅拷贝” 的坑。4.2 拷贝构造函数特性与创建是构造函数的重载形式函数名 类名参数必须是 “类类型的引用”Date d若未显式实现编译器会生成默认拷贝构造浅拷贝。引用前面最好加上const限制。保护用来拷贝的那个数据Date(constDated){_yeard._year;_monthd._month;_dayd._day;}采用const引用的原因可以保护形式参数不被改变。传值传参拷贝构造函数只是一个中转。我不希望它在函数体内对我的原来的对象造成修改。为什么参数必须是引用如果参数是传值Date(const Date d)调用拷贝构造时会先复制实参到形参 —— 而复制形参又需要调用拷贝构造简而言之传参会调用拷贝构造调用拷贝构造又要调用拷贝构造形成 “无穷递归”具体可以看下面的图classDate{public:// 正确写法参数是const引用const避免修改原对象引用避免递归Date(constDated){_yeard._year;_monthd._month;_dayd._day;}Date(intyear0,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}// 错误写法参数传值引发无穷递归// Date(const Date d) {}private:int_year,_month,_day;};intmain(){Date d1;Dated2(d1);return0;}调用拷贝构造函数的另外一种方法第二种调用方法实质上等价于第一种。但是可读性更高Date d1;Date d2d1;4.3 编译器默认生成拷贝构造函数与构造函数和析构函数不同自动生成的拷贝构造函数会管内置类型。若未显式定义拷贝构造编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝 / 浅拷贝对自定义类型成员变量会调用它的拷贝构造。我们发现自己不写也会完成因为编译器生成的会按照字节进行拷贝。情况一类成员均为内置类型且不管理资源例如Date类。编译器生成的拷贝构造函数会完成值拷贝因此不需要我们显式实现拷贝构造。情况二类成员包含自定义类型且该类型已正确实现拷贝构造函数例如Myqueue类其内部成员主要是自定义类型Stack的对象。编译器自动生成的拷贝构造函数会调用Stack的拷贝构造函数因此也不需要我们显式实现Myqueue的拷贝构造。情况三类管理动态资源例如Stack类其成员_a指向了资源。编译器自动生成的拷贝构造函数仅完成值拷贝 / 浅拷贝仅复制指针值并不符合我们的需求因此必须手动实现深拷贝即对指针所指向的资源也进行完整拷贝。4.4 浅拷贝的坑什么时候必须显式写拷贝构造像上面的情况三就要自己写了系统生成的无法完成。我们先看为什么解决办法深拷贝深拷贝的核心是 “给新对象重新申请资源”避免指针指向同一块内存classStack{public:// 深拷贝构造Stack(constStacks){// 给新对象重新申请内存_array(DataType*)malloc(sizeof(DataType)*s._capacity);if(_arraynullptr){perror(malloc失败);return;}// 复制数据不是复制指针memcpy(_array,s._array,sizeof(DataType)*s._size);_capacitys._capacity;_sizes._size;}// 构造、析构同上...};五、赋值运算符重载5.1 运算符重载比如我们如果要比较日期类对象的大小按照我们现有的知识要去写一个函数但是我们如果想直接比较呢d1d2所以有了运算符重载。在 C 中运算符重载是一项强大的特性它允许我们为自定义类型赋予内置类型般的操作体验极大提升了代码的可读性与易用性。函数名字为关键字operator后面接需要重载的运算符符号。函数原型返回值类型 operator 操作符(参数列表)Dateoperator(constDated)不能通过连接其他符号来创建新的操作符比如operator重载操作符必须有一个类类型参数用于内置类型的运算符其含义不能改变例如内置的整型 不能改变其含义作为类成员函数重载时其形参看起来比操作数数目少 1因为成员函数的第一个参数为隐藏的this.*、::、sizeof、?:、.注意以上 5 个运算符不能重载。5.2 赋值运算符重载的特性与创建5.2.1 赋值运算符特性与创建返回值类型 operator(参数列表)参数类型为const 类类型引用提高效率const避免修改原对象返回值类型为类类型支持连续赋值如d3 d2 d1必须显式检测 “自己给自己赋值”如d1 d1避免无意义操作。classDate{public:Date(intyear1900,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}// 赋值运算符重载Dateoperator(constDated){// 1. 检测自己给自己赋值if(this!d){// 2. 复制成员变量_yeard._year;_monthd._month;_dayd._day;}// 3. 返回*this支持连续赋值return*this;//返回d2}private:int_year,_month,_day;};// 使用示例intmain(){Dated1(2024,5,20);Date d2,d3;d3d2d1;// 连续赋值等价于d3.operator(d2.operator(d1))return0;}5.2.2 全局函数 vs 成员函数运算符重载可以通过全局函数或类的成员函数实现两者的核心区别在于参数列表的隐含性。若将operator实现为全局函数需要显式接收两个Date对象作为参数【全局函数实现】classDate{public:Date(intyear1900,intmonth1,intday1):_year(year),_month(month),_day(day){}// 成员变量需公开或用后面友元解决封装性问题int_year,_month,_day;};// 全局函数重载 operatorbooloperator(constDated1,constDated2){returnd1._yeard2._yeard1._monthd2._monthd1._dayd2._day;}voidTest(){Dated1(2024,10,1);Dated2(2024,10,2);cout(d1d2)endl;// 调用全局 operator}缺点若类的成员变量是私有的全局函数无法直接访问需通过 “友元” 声明打破封装或直接将重载函数写为成员函数。【成员函数实现】若将operator实现为类的成员函数编译器会自动将左操作数作为隐含的this指针因此参数列表只需显式接收右操作数classDate{public:Date(intyear1900,intmonth1,intday1):_year(year),_month(month),_day(day){}// 成员函数重载 operator左操作数是 this 指向的对象booloperator(constDated2)const{return_yeard2._year_monthd2._month_dayd2._day;}private:int_year,_month,_day;// 成员变量可私有保证封装性};voidTest(){Dated1(2024,10,1);Dated2(2024,10,1);cout(d1d2)endl;// 调用成员函数 operator}优点无需破坏封装性且逻辑更直观左操作数的属性可直接访问。5.2.3 避坑点赋值运算符只能是类的成员函数不能把赋值运算符重载成全局函数 —— 因为编译器会默认生成类内的赋值运算符重载全局版本会和它冲突5.3 编译器默认生成的赋值运算符重载和拷贝构造一样编译器默认生成的赋值运算符重载是 “浅拷贝”—— 有动态内存的类必须显式实现深拷贝否则会重复释放内存// 栈类的深拷贝赋值运算符重载classStack{public:Stackoperator(constStacks){if(this!s){// 检测自赋值// 1. 释放当前对象的旧资源free(_array);// 2. 给当前对象重新申请资源并复制数据_array(DataType*)malloc(sizeof(DataType)*s._capacity);if(_arraynullptr){perror(malloc失败);return*this;}memcpy(_array,s._array,sizeof(DataType)*s._size);_capacitys._capacity;_sizes._size;}return*this;}// 构造、析构、拷贝构造同上...};5.3.1 不需要手动实现的情况情况一类成员均为内置类型且不管理资源例如Date类。编译器生成的赋值运算符会进行值拷贝。情况二类成员包含自定义类型且该类型已正确实现赋值运算符例如Myqueue类其成员为Stack对象。编译器会递归调用Stack的赋值运算符只要Stack已正确实现深拷贝Myqueue就无需手动实现。5.3.2 必须手动实现的情况类管理动态资源如堆内存、文件句柄等例如Stack类其成员_a指向动态数组。编译器生成的赋值运算符仅进行浅拷贝复制指针值会导致内存泄漏重复释放悬空指针等一系列问题。六、其他 2 个默认成员函数取地址重载取地址重载和const取地址重载编译器默认生成的实现完全满足需求 —— 返回对象的地址this指针几乎无需自定义。七、总结函数类型显式实现场景核心坑点构造函数需自定义初始化如日期默认构造只能有一个析构函数类内有动态资源如栈忘记释放资源导致内存泄漏拷贝构造函数类内有动态资源避免浅拷贝参数不用引用引发无穷递归赋值运算符重载类内有动态资源避免浅拷贝忘记检测自赋值、返回*this取地址重载特殊需求隐藏真实地址几乎不用自定义
RELATED

相关推荐

docker部署C# .net程序

docker部署C# .net程序

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/21 20:34:36
【RHCA+】_etc_yum.conf

【RHCA+】_etc_yum.conf

“ 目录:RHCA -> 6 软件仓库 -> 33 _etc_yum.conf ”yum默认配置文件[rootwuzuniao ~]# ll /etc/yum.conf #查看配置文件权限-rw-r--r--. 1 root root 813 Apr 15 2014 /etc/yum.conf[rootwuzuniao ~]# cat /etc/yum.conf #查看配置文件默认内容[main] #主区域…

📅 2026/10/21 20:29:35
【RHCA+】createrepo(创建repo本地索引文件)

【RHCA+】createrepo(创建repo本地索引文件)

“ 目录:RHCA -> 6 软件仓库 -> 32 createrepo(创建repo本地索引文件) ”为yum源(软件仓库)里的rpm包创建索引,描述包的互相依赖,生成元数据;语法格式:createrepo …

📅 2026/10/21 20:29:35
MORE NEWS

更多资讯

📰

Docker+WordPress阿里云部署实战:从零搭建高可用个人博客

1. 为什么现在还要亲手搭一个 WordPress 博客?不是有现成的平台吗? “dockerwordpress搭建个人博客(如何在阿里云服务器上线部署个人博客)”——这个标题背后,藏着一群正在悄悄回归技术掌控权的人。不是他们不信公众号…

📰

对比预测编码CPC:自监督学习中的对比学习与InfoNCE损失解析

1. 从"预测未来"说起:对比预测编码到底在做什么第一次看到"Representation Learning with Contrastive Predictive Coding"这个标题,很多人会以为它只是又一堆堆砌术语的论文。但如果你真正动手复现过,就会发现它解决的是…

📰

PHP调用人脸融合接口:完整链路Demo与避坑指南

简介:这是一份基于腾讯AI人脸融合API的PHP示例项目,主要面向希望快速集成人脸融合能力的Web开发者,可用于制作换装、合影、趣味营销等H5活动。项目参考了此前微信中流行的换军装玩法,通过调用腾讯AI平台接口完成人脸检测与融合处理…

📰

车牌识别可信度建模:三层过滤策略实战指南

1. 项目概述:为什么车牌识别这件事,值得拆到第七篇还在深挖? HyperLPR 这个名字,对做过图像识别、智能交通或安防类项目的开发者来说,几乎等于“开箱即用的车牌识别第一选择”。它不是那种需要从OpenCV读图、YOLO训练、…

📰

基于YOLOv8的钢材表面缺陷检测实战:数据转换、训练调优与工业部署

1. 为什么选择YOLOv8做钢材瑕疵检测:从产线痛点说起 钢铁表面缺陷检测这件事,说出来你可能不太信,很多产线到今天还在靠人工肉眼盯屏幕。钢带在产线上以每秒好几米的速度跑,质检员盯着监视器里的带钢表面,连续盯几个小…

📰

发那科FANUC电路板图纸:从电源到IGBT的数控维修核心解析

简介:发那科电路板图纸全套,面向数控设备维修人员、自动化工程师及电路板研究爱好者,涵盖主控制板、驱动板与电源模块的关键线路图,可用于日常检修、故障定位、系统升级与备件对照。压缩包共5个文件,大小约1.1兆&#…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬