ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

《C++深度解构03》类和对象(中)——从零拆解六大默认成员函数底层逻辑

《C++深度解构03》类和对象(中)——从零拆解六大默认成员函数底层逻辑 目录一.什么是默认成员函数1.1六大默认成员函数1.2为什么需要默认成员函数二.1构造函数2.1概念与定义2.2默认构造函数的局限性2.3内置与自定义类型默认行为二.2初始化列表与构造函数三.析构函数3.1析构函数概念及定义3.2内置类型与动态资源的析构四.拷贝构造函数4.1拷贝构造函数的定义4.2深拷贝与浅拷贝五.赋值运算符重载5.1运算符重载operator5.2赋值运算符重载六.取地址运算符重载6.1const修饰对象6.2取地址重载相关专栏与源码导航配套源码 本专栏所有配套代码已同步至 GitHub点击直达系统专栏 《从零开始学习数据结构》专栏直达⚡ 《C 深度解构》专栏直达一.什么是默认成员函数概念定义在我们没有显示实现编译器默认生成的成员函数1.1六大默认成员函数构造函数析构函数拷贝构造函数赋值重载函数取地址重载函数1.2为什么需要默认成员函数在C语言中功能的实现必定离不开初始化 \ 资源销毁但是在操作中很容易忘记操作的实施但是在C中将对象的“初始化”和 “资源释放”与生命周期强行绑定创造对象时自动初始化构造离开作用域自动销毁析构。内置类型的赋值非常明显int a b, 对于类类型而言也需要支持对象之间的拷贝与赋值功能所以需要我们自定义一个函数来保证对象复制时的完整与安全。二.1构造函数函数特性函数名与类名一致无返回值对象实例化时编译器自动调用、支持重载功能定位构造函数功能类似于语言的 Init2.1概念与定义无参构造函数全缺省构造函数未显式定义自动生成的默认构造函数核心避坑 \colorbox{#E53935}{\color{#FFFFFF}\text{ 核心避坑 }}核心避坑​默认构造函数不仅仅只包含未显示定义时编译器默认生成的构造函数上面提到的三种统称为默认构造函数。歧义无参构造函数和全缺省构造函数不能同时存在因为都未传参编译器无法正确调用 \colorbox{#FFEAEA}{\color{#C22828}\text{歧义无参构造函数和全缺省构造函数不能同时存在因为都未传参编译器无法正确调用}}歧义无参构造函数和全缺省构造函数不能同时存在因为都未传参编译器无法正确调用​错误示例对比Data(intyear2022,intmonth10,intday9);// 全缺省Data();// 无参Data d1;// ❌ 语法允许两者同时存在但实例化时不传参会引发调用歧义编译报错2.2默认构造函数的局限性常见情形大多数情况下如果不显式定义构造函数编译器自动生成的构造函数无法满足我们资源初始化的要求特殊情形经典案例两个栈实现队列 \colorbox{#FFEAEA}{\color{#C22828}\text{经典案例两个栈实现队列}}经典案例两个栈实现队列​在模拟队列Queue类中含有两个成员变量Stack对象这里的Stack为自定义类型。实例化Queue时因为成员变量为自定义类型所以编译器生成的默认构造函数会自动调用Stack内部的默认构造函数来进行初始化所以这种情况我们无需再给出构造函数2.3内置与自定义类型默认行为内置类型int, double, 指针等如果未显示实现构造函数那么编译器会不做处理变量呈现的值为随机值并非编译器主动赋予而是直接忽略了该内存块的初始化内存并非为空所以读取到了之前的垃圾值自定义类型若自定义类型成员含有对象那么不显示实现构造函数会调用对象内部默认构造函数。如果给了含参构造函数但是没有传入参数或未指定缺省值那么编译器会因为找不到构造函数而报错 \colorbox{#FFEAEA}{\color{#C22828}\text{ 如果给了含参构造函数但是没有传入参数或未指定缺省值那么编译器会因为找不到构造函数而报错}}如果给了含参构造函数但是没有传入参数或未指定缺省值那么编译器会因为找不到构造函数而报错​//未显式实现构造函数classHy{public:voidprint(){cout_aendl;}private:int_a;};intmain(){Hy a1;a1.print();//打印结果未垃圾值return0;}二.2初始化列表与构造函数初始化列表 \colorbox{#FFEAEA}{\color{#C22828}\text{初始化列表}}初始化列表​在类的定义中所有成员变量仅仅是声明而真正为所有成员变量分配内存空间是在初始化列表中构造函数是为被定义了的成员变量进行初始化即所有成员变量都会进入初始化列表中被定义。public:Data(intyear,intmonth,intday):_year(year)//⚠️⚠️⚠️此处被正式定义,_month(month),_day(day),_a((int*)malloc(12){}private:int_year;//⚠️⚠️⚠️这里仅仅是声明还未被定义int_month;int_day;int*_a;初始化列表中初始化值可以是表达式也可以是单一变量初始化列表和构造函数的区别 \colorbox{#FFEAEA}{\color{#C22828}\text{初始化列表和构造函数的区别}}初始化列表和构造函数的区别​都具有初始化的功能因为成员变量都要进入初始化列表被定义所以本质上构造函数花括号内的初始化是二次赋值覆盖初始化列表中同一成员变量只能出现一次变量不可重定义构造函数中初始化可以多次出现覆盖赋值核心避坑: \colorbox{#E53935}{\color{#FFFFFF}\text{ 核心避坑: }}核心避坑:​普通成员变量的初始化两者都可以做到无差别但是三种特例无法只能使用初始化列表来完成初始化。const修饰被const修饰的成员变量具有常属性只有在定义时能被初始化所以必须使用初始化列表引用变量引用变量必须在定义时就与被引用变量绑定必须初始化后续不可以改变绑定关系自定义类型无默认构造函数成员变量中如果存在自定义类型且给只显式定义了带参构造函数编译器不在生成无默认构造函数此时需要初始化列表来给自定义类型成员传入构造函数形参classTime{public:Time(inttime){_timetime;}private:int_time;};//利用初始化列表为自定义类型成员传入参数Data(intyear,intmonth,intday):_year(year),_month(month),_day(day),_t(1)//✅{}⚠️ 注意:能使用初始化列表尽量都使用构造函数会存在两次操作定义 二次赋值初始化列表只有一次定义在实际使用中初始化列表中与构造函数相辅相成并不存在对立关系初始化列表只能完成简单赋值绑定但是复杂对象的初始化还需要判断及校验C11改变 \colorbox{#FFEAEA}{\color{#C22828}\text{C11改变}}C11改变​C98中规定声明的成员变量不可以给值但是C11中可以提供缺省值不在初始化列表中初始化的成员使用缺省值初始化。总结 \colorbox{#FFEAEA}{\color{#C22828}\text{总结}}总结​1.所有成员变量都会进入初始化列表2.在初始化列表中初始化的成员直接使用传入形参3.不在初始化列表中初始化的成员a.在声明位置有缺省值使用缺省值b.没有缺省值x.内置类型成员编译器不做处理随机值y.自定义类型成员自动调用其默认构造函数无默认构造函数则报错三.析构函数功能定位析构函数类似于C语言中的Destroy函数特性没有返回值函数名也为类名对象生命周期结束时自动调用不支持重载3.1析构函数概念及定义//析构函数classStack{public://构造函数Stack(intn4){_a(int*)malloc(sizeof(int)*n);_top0;_capacityn;}//析构函数~Stack(){free(_a);//✅将申请的空间资源进行释放_anullptr;}private:int*_a;int_top;int_capacity;};在对象生命周期结束前return 前编译器自动调用析构函数完成已申请的资源释放但并非销毁对象本身析构函数有且仅有一个所以不能重载 \colorbox{#FFEAEA}{\color{#C22828}\text{析构函数有且仅有一个所以不能重载}}析构函数有且仅有一个所以不能重载​3.2内置类型与动态资源的析构无资源申请时析构函数仅对有资源申请的类进行操作对于单纯的内置类型不做处理函数调用结束自动释放有资源申请时通过malloc或new动态申请内存的前提下如果不显式实现析构函数编译器虽然会自动生成析构函数但是无法对mlloc或new申请的堆区空间进行释放会造成内存泄漏。注意一个类实例化出多个对象时析构满足栈的性质先进后出所以先构造的后析构、后构造的先析构 \colorbox{#FFEAEA}{\color{#C22828}\text{注意一个类实例化出多个对象时析构满足栈的性质先进后出所以先构造的后析构、后构造的先析构}}注意一个类实例化出多个对象时析构满足栈的性质先进后出所以先构造的后析构、后构造的先析构​四.拷贝构造函数函数特性无返回值函数名与类名一致参数必须是传引用传值功能定位完成自定类型的拷贝4.1拷贝构造函数的定义定义用已经实例化出来的对象去初始化另一个正在实例化的对象拷贝构造函数仅针对于类类型成员所以第一个参数为类类型并且如果有其他额外参数都需要有默认值即缺省值classData{public://构造函数Data(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}//拷贝构造函数Data(constDatad)//思考为什么需要引用传参{_yeard._year;_monthd._month;_dayd._day;}private:int_year;int_month;int_day;};⚠️ 注意拷贝构造函数的形参必须是传引用传参而不能是传值传参否则会造成死循环 核心避坑: \colorbox{#E53935}{\color{#FFFFFF}\text{ 核心避坑: }}核心避坑:​如果形参为传值传参则外部对象需先拷贝给形参d此时d相当于实例化出来的另一个对象触发拷贝构造函数再次进入形参d的拷贝又一次调用拷贝构造函数无休止进行。传引用传参不会造成拷贝所以可以直接进入拷贝构造函数内进行拷贝传引用传参本质是为了减少拷贝次数但是如果作为返回值返回一定要确定变量是否还存在若出作用域被销毁那么就相当于野引用传出值不可预测4.2深拷贝与浅拷贝编译器默认生成的浅拷贝值拷贝有相应指针指向的空间也是直接拷贝会造成拷贝出的对象与被拷贝对象指向同一块内存深拷贝则是在面对存在有指针指向资源的情况时为拷贝出的函数重新创建一个指向空间而非与被拷贝空间指向同一块地址。经典案例栈 \colorbox{#FFEAEA}{\color{#C22828}\text{经典案例栈}}经典案例栈​//拷贝构造函数Stack(constStacks){_a(int*)malloc(sizeof(int)*s._capacity);//浅拷贝只能值拷贝无法完成对象的指向关系_tops._top;_capacitys._capacity;//只需拷贝有效数据个数拷贝整个capacity会有空间还未利用memcpy(_a,s._a,sizeof(int)*s._top);}未显式实现拷贝构造函数 \colorbox{#FFEAEA}{\color{#C22828}\text{未显式实现拷贝构造函数}}未显式实现拷贝构造函数​intmain(){Stack s1;Stack s2s1;//或者Stack s2 (s1);couts1._aendl;couts2._aendl;return0;}从图中可以看出如果不显式实现拷贝构造函数那么s1中的_a和s2中的_a所指向的是同一块空间地址相同。为什么会产生崩溃显现呢核心避坑: \colorbox{#E53935}{\color{#FFFFFF}\text{ 核心避坑: }}核心避坑:​未显式定义拷贝构造函数类内成员变量指向同一块空间在程序结束后自动调用析构函数会对同一块空间进行两次资源释放Double free五.赋值运算符重载5.1运算符重载operator函数特性有返回值函数名为operator加运算符如void operator( ) 不能凭空创造运算符等.* :: sizeof ?: .五大运算符不可以重载运算符操作对象最少有一个类类型内置类型无法进行运算符重载按照正常逻辑执行重载 或 --运算符时为了区分前置或后置定义重载函数时给后置参数多一个int变量(值无所谓只是为了区分)如operator()前置、operator(int)后置重载流运算符 或时为了符合习惯通常定义在类外否则写法为对象cout(不符合我们平时的习惯)核心避坑 \colorbox{#E53935}{\color{#FFFFFF}\text{ 核心避坑 }}核心避坑​如果运算符重载的操作对象为两个并且重载定义在类内则只需要传入第二个参数第一个为隐式的this若定义在类外则需要传入两个参数两个操作对象的运算符参数的传递是左操作对象传第一个参数右操作对象传第二个参数样例示范 \colorbox{#FFEAEA}{\color{#C22828}\text{样例示范}}样例示范​通过重载运算符来判断两个对象的日期是否相同当然也可以实现大小比较日期相减减等操作流输出、流提取运算符重载 \colorbox{#FFEAEA}{\color{#C22828}\text{流输出、流提取运算符重载}}流输出、流提取运算符重载​在类内定义时第一个参数隐式传递this指针所以只需要传入ostream类型参数vs默认做操作对象传第一个参数第二个操作对象传入第二个参数voidoperator(/*第一参数默认传入this指针*/ostreamout){out_yearendl;}Datad1(2026,8,25);d1cout;//✅为满足对象传第一this参数右对象传第二参数在类外定义类外可以自定义传入参数顺序则可以符合使用习惯将cout放在操作符前voidoperator(constDatad,ostreamout){outd._yearendl;}Datad1(2026,8,25);coutd1;5.2赋值运算符重载上面讲的运算符重载是我们对于一个操作符进行的自定义行为而对于赋值运算符的重载是真正的默认成员函数功能 \colorbox{#FFEAEA}{\color{#C22828}\text{功能}}功能​用一个已经实例化成功的对象去赋值另一个已经实例化的对象操作对象为两个已经实例化的对象。核心避坑 ⚠️ \colorbox{#E53935}{\color{#FFFFFF}\text{ 核心避坑 ⚠️}}核心避坑⚠️​拷贝构造函数是用已经实例化的对象去初始化正在实例化的对象操作对象是一个实例化完成的对象一个正在实例化的对象样例栈 \colorbox{#FFEAEA}{\color{#C22828}\text{样例栈}}样例栈​voidoperator(constStackst){_a(int*)malloc(sizeof(int)*st._capacity);_topst._top;_capacityst._capacity;memcpy(_a,st._a,sizeof(int)*st._top);}如果不显示实现赋值运算符重载那么有资源申请的请款下同一块内存将会被两个对象同时指向。赋值运算符重载还支持连续赋值ru(st3 st2 st1),表达式st2 st1 赋值完成后左值st2作为返回值返回所以在赋值运算符重载中可以将this指针解引用返回小技巧 \colorbox{#FFEAEA}{\color{#C22828}\text{小技巧}}小技巧​有析构函数时大概率会使用拷贝构造函数和赋值重载六.取地址运算符重载6.1const修饰对象classData{public:Data(intyear):_year(year){}voidpinrt()const//⚠️⚠️⚠️const放在函数尾代表传入参数为const类型{cout_yearendl;}private:int_year;};//实例化一个const对象constd1(2026);d1.print();默认隐式传入的this指针为Data* this,而对象被const修饰后相对应的指针类型为const Data* 这里存在权限放大问题。解决方法: \colorbox{#FFEAEA}{\color{#C22828}\text{解决方法:}}解决方法:​C规定不可以显式提供this指针所以无法在括号内完成const修饰所以将const放在函数参数列表尾6.2取地址重载提醒 \colorbox{#E53935}{\color{#FFFFFF}\text{ 提醒}}提醒​编译器默认生成的取地址重载会直接返回对象的实际内存地址即this指针99%情况下无需显式实现。特例 \colorbox{#FFEAEA}{\color{#C22828}\text{特例}}特例​想让其返回nullptr或者假地址时才需要显式实现Data*operator(){returnnullptr;}“保持热爱奔赴山海保持专注雕琢底层。”Crafted with passion byHhy当前更新时间{{ 2026 / 8 / 25 }} | 此博文系博主学习复盘欢迎指正本文专栏由 Hhy 维护。如果这篇文章曾带给你启发请考虑关注我的 CSDN 博客或 GitHub 仓库以获取最新技术分享。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表