ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

【C++】-2-类和对象(中-1):类的默认成员函数,构造函数,析构函数,拷贝构造函数

【C++】-2-类和对象(中-1):类的默认成员函数,构造函数,析构函数,拷贝构造函数 个人主页深邃-❄️专栏传送门《C语言》《数据结构与算法》《Web安全》《C》《ACM算法篇》Gitee仓库《C语言》《数据结构与算法》《C》目录类和对象(中-1)1. 类的默认成员函数2. 构造函数3. 析构函数4. 拷贝构造函数拷贝构造的特点拷贝构造底层函数理解类和对象(中-1)1. 类的默认成员函数默认成员函数就是用户没有显式实现编译器会自动生成的成员函数称为默认成员函数。一个类我们不写的情况下编译器会默认生成以下6个默认成员函数需要注意的是这6个中最重要的是前4个最后两个取地址重载不重要我们稍微了解一下即可。其次就是C11以后还会增加两个默认成员函数移动构造和移动赋值这个我们后面再讲解。默认成员函数很重要也比较复杂我们要从两个方面去学习第一我们不写时编译器默认生成的函数行为是什么是否满足我们的需求。第二编译器默认生成的函数不满足我们的需求我们需要自己实现那么如何自己实现2. 构造函数构造函数是特殊的成员函数需要注意的是构造函数虽然名称叫构造但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时空间就开好了)而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能构造函数自动调用的特点就完美的替代的了Init。创建对象时初始化构造函数的特点函数名与类名相同。无返回值。(返回值啥都不需要给也不需要写void不要纠结C规定如此)对象实例化时系统会自动调用对应的构造函数。构造函数可以重载。如果类中没有显式定义构造函数则C编译器会自动生成一个无参的默认构造函数一旦用户显式定义编译器将不再生成。无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数都叫做默认构造函数。但是这三个函数有且只有一个存在不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造实际上无参构造函数、全缺省构造函数也是默认构造总结一下就是不传实参就可以调用的构造就叫默认构造。我们不写编译器默认生成的构造对内置类型成员变量的初始化没有要求也就是说是是否初始化是不确定的看编译器。对于自定义类型成员变量要求调用这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数那么就会报错大白话嵌套类被嵌套的类调用自己的默认构造我们要初始化这个成员变量需要用初始化列表才能解决初始化列表我们下个章节再细细讲解。说明C把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型如int/char/double/指针等自定义类型就是我们使用class/struct等关键字自己定义的类。#includeiostreamusingnamespacestd;classDate{public:// 1.无参构造函数Date(){_year1;_month1;_day1;}// 2.带参构造函数Date(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}// 3.全缺省构造函数/*Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; }*/voidPrint(){cout_year/_month/_dayendl;private:int_year;int_month;int_day;};intmain(){// 如果留下三个构造中的第二个带参构造第一个和第三个注释掉// 编译报错error C2512: “Date”: 没有合适的默认构造函数可用Date d1;// 调用默认构造函数Dated2(2025,1,1);// 调用带参的构造函数// 注意如果通过无参构造函数创建对象时对象后面不用跟括号否则编译器无法// 区分这里是函数声明还是实例化对象// warning C4930: “Date d3(void)”: 未调用原型函数是否是有意用变量定义的Dated3();d1.Print();d2.Print();return0;}#includeiostreamusingnamespacestd;typedefintSTDataType;classStack{public:Stack(intn4){_a(STDataType*)malloc(sizeof(STDataType)*n);if(nullptr_a){perror(malloc申请空间失败);return;}_capacityn;_top0;}// ...private:STDataType*_a;size_t _capacity;size_t _top;};// 两个Stack实现队列classMyQueue{public://编译器默认生成MyQueue的构造函数调用了Stack的构造完成了两个成员的初始化private:Stack pushst;Stack popst;};intmain(){MyQueue mq;return0;}3. 析构函数析构函数与构造函数功能相反析构函数不是完成对对象本身的销毁比如局部对象是存在栈帧的函数结束栈帧销毁他就释放了不需要我们管C规定对象在销毁时会自动调用析构函数完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能而像Date没有Destroy其实就是没有资源需要释放所以严格说Date是不需要析构函数的。析构函数在对象销毁时自动调用内置类型成员本身内存随对象自动回收但内置指针指向的堆内存不会自动释放自定义类如果持有自己申请的外部资源堆内存、文件等就需要手写析构释放资源如果没有独占资源编译器默认生成的析构就够用不用自己写。析构函数的特点析构函数名是在类名前加上字符 ~。无参数无返回值。(这里跟构造类似也不需要加void)一个类只能有一个析构函数。若未显式定义系统会自动生成默认的析构函数。对象生命周期结束时系统会自动调用析构函数。跟构造函数类似我们不写编译器自动生成的析构函数对内置类型成员不做处理自定类型成员会调用他的析构函数。还需要注意的是我们显示写析构函数对于自定义类型成员也会调用他的析构也就是说自定义类型成员无论什么情况都会自动调用析构函数。如果类中没有申请资源时析构函数可以不写直接使用编译器生成的默认析构函数如Date如果默认生成的析构就可以用也就不需要显示写析构如MyQueue但是有资源申请时一定要自己写析构否则会造成资源泄漏如Stack。一个局部域的多个对象C规定后定义的先析构从下往上析构。栈内存本身就是先进后出 (LIFO)这个规则是配合栈的存储模型保证对象销毁顺序安全#includeiostreamusingnamespacestd;typedefintSTDataType;classStack{public:Stack(intn4){_a(STDataType*)malloc(sizeof(STDataType)*n);if(nullptr_a){perror(malloc申请空间失败);return;}_capacityn;_top0;}~Stack(){cout~Stack()endl;free(_a);_anullptr;_top_capacity0;}private:STATType*_a;size_t _capacity;size_t _top;};// 两个Stack实现队列classMyQueue{public:// 编译器默认生成MyQueue的析构函数调用了Stack的析构释放的Stack内部的资源// 显示写析构也会自动调用Stack的析构/*~MyQueue() {}*/private:Stack pushst;Stack popst;};intmain(){Stack st;MyQueue mq;return0;}对比一下用C和C实现的Stack解决之前括号匹配问题isValid我们发现有了构造函数和析构函数确实方便了很多不会再忘记调用Init和Destory函数了也方便了不少。#includeiostreamusingnamespacestd;// 用最新加了构造和析构的C版本Stack实现boolisValid(constchar*s){Stack st;while(*s){if(*s[||*s(||*s{){st.Push(*s);}else{// 右括号比左括号多数量匹配问题if(st.Empty()){returnfalse;}// 栈里面取左括号chartopst.Top();st.Pop();// 顺序不匹配if((*s]top![)||(*s}top!{)||(*s)top!()){returnfalse;}}s;}// 栈为空返回真说明数量都匹配左括号多右括号少匹配问题returnst.Empty();}// 用之前C版本Stack实现boolisValid(constchar*s){ST st;STInit(st);while(*s){// 左括号入栈if(*s(||*s[||*s{){STPush(st,*s);}else// 右括号取栈顶左括号尝试匹配{if(STEmpty(st)){STDestroy(st);returnfalse;}chartopSTTop(st);STPop(st);// 不匹配if((top(*s!))||(top{*s!})||(top[*s!])){STDestroy(st);returnfalse;}}s;}// 栈不为空说明左括号比右括号多数量不匹配boolretSTEmpty(st);STDestroy(st);returnret;}intmain(){coutisValid([()][])endl;coutisValid([(])[])endl;return0;}4. 拷贝构造函数如果一个构造函数的第一个参数是自身类类型的引用且任何额外的参数都有默认值则此构造函数也叫做拷贝构造函数也就是说拷贝构造是一个特殊的构造函数。拷贝构造的特点拷贝构造函数是构造函数的一个重载。拷贝构造函数的第一个参数必须是类类型对象的引用使用传值方式编译器直接报错因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数但是第一个参数必须是类类型对象的引用后面的参数必须有缺省值。C规定自定义类型对象进行拷贝行为必须调用拷贝构造所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。若未显式定义拷贝构造编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝)对自定义类型成员变量会调用他的拷贝构造。内置类型指针仅仅拷贝 p 中存储的地址数值不会去 new 一份新 A不会调用 A 的拷贝构造。两个对象的p指向同一块堆内存典型浅拷贝。像Date这样的类成员变量全是内置类型且没有指向什么资源编译器自动生成的拷贝构造就可以完成需要的拷贝所以不需要我们显示实现拷贝构造。像Stack这样的类虽然也都是内置类型但是_a指向了资源编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员编译器自动生成的拷贝构造会调用Stack的拷贝构造也不需要我们显示实现MyQueue的拷贝构造。这里还有一个小技巧如果一个类显示实现了析构并释放资源那么他就需要显示写拷贝构造否则就不需要。传值返回会产生一个临时对象调用拷贝构造传值引用返回返回的是返回对象的别名(引用)没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象函数结束就销毁了那么使用引用返回是有问题的这时的引用相当于一个野引用类似一个野指针一样。传引用返回可以减少拷贝但是一定要确保返回对象在当前函数结束后还在才能用引用返回。拷贝构造函数理解#includeiostreamusingnamespacestd;classDate{public:Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}// 编译报错error C2652“Date”非法的复制构造函数第一个参数不应是“Date”// Date(Date d)Date(constDated){_yeard._year;_monthd._month;_dayd._day;}//还是构造函数只不过原理上和拷贝构造相似Date(Date*d){_yeard-_year;_monthd-_month;_dayd-_day;}voidPrint(){cout_year-_month-_dayendl;}private:int_year;int_month;int_day;};voidFunc1(Date d){coutdendl;d.Print();}// Date Func2()DateFunc2(){Datetmp(2024,7,5);tmp.Print();returntmp;}intmain(){Dated1(2024,7,5);// C规定自定义类型对象进行拷贝行为必须调用拷贝构造所以这里传值传参要调用拷贝构造// 所以这里的d1传值传参给d要调用拷贝构造完成拷贝传引用传参可以较少这里的拷贝Func1(d1);coutd1endl;// 这里可以完成拷贝但是不是拷贝构造只是一个普通的构造Dated2(d1);d1.Print();d2.Print();//这样写才是拷贝构造通过同类型的对象初始化构造而不是指针Dated3(d1);d2.Print();// 也可以这样写这里也是拷贝构造Date d4d1;d2.Print();// Func2返回了一个局部对象tmp的引用作为返回值// Func2函数结束tmp对象就销毁了相当于了一个野引用Date retFunc2();ret.Print();return0;}深拷贝与浅拷贝/值拷贝#includeiostreamusingnamespacestd;typedefintSTDataType;classStack{public:Stack(intn4){_a(STDataType*)malloc(sizeof(STDataType)*n);if(nullptr_a){perror(malloc申请空间失败);return;}_capacityn;_top0;}//带上const 1.防止修改 2.防止权限问题Stack(constStackst){// 需要对_a指向资源创建同样大的资源再拷贝值//深拷贝_a(STDataType*)malloc(sizeof(STDataType)*st._capacity);if(nullptr_a){perror(malloc申请空间失败!!!);return;}memcpy(_a,st._a,sizeof(STDataType)*st._top);_topst._top;_capacityst._capacity;}voidPush(STDataType x){if(_top_capacity){intnewcapacity_capacity*2;STDataType*tmp(STDataType*)realloc(_a,newcapacity*sizeof(STDataType));if(tmpNULL){perror(realloc fail);return;}_atmp;_capacitynewcapacity;}_a[_top]x;}~Stack(){cout~Stack()endl;free(_a);_anullptr;_top_capacity0;}private:STDataType*_a;size_t _capacity;size_t _top;};// 两个Stack实现队列classMyQueue{public:private:Stack pushst;Stack popst;};intmain(){Stack st1;st1.Push(1);st1.Push(2);// Stack不显示实现拷贝构造用自动生成的拷贝构造完成浅拷贝// 会导致st1和st2里面的_a指针指向同一块资源析构时会析构两次程序崩溃Stack st2st1;MyQueue mq1;// MyQueue自动生成的拷贝构造会自动调用Stack拷贝构造完成pushst/popst的拷贝只要Stack拷贝构造自己实现了深拷贝他就没问题MyQueue mq2mq1;return0;}拷贝构造底层函数理解我们的 C 真实代码classA{public:intx;// 默认构造A(){x10;}//拷贝构造A(constAother){this-xother.x;}//普通成员函数voidshow(){coutthis-xendl;}};voidfun(A p){}intmain(){A a;a.show();fun(a);return0;}编译器完全展开后的伪 C 代码全部显性没有任何隐藏 this//------------------------------//1. 默认构造函数 A()//编译器自动增加第一个参数 A* this//------------------------------voidA_constructor(A*this){this-x10;}//------------------------------//2.拷贝构造 A(const A other)//this待构建新对象的地址//other引用底层等价指针 const A* const other//------------------------------voidA_copy_constructor(A*this,constA*constother){// this → 新对象内存地址// other → 源对象地址this-xother-x;}//------------------------------//3.成员函数 void show()//编译器增加隐式参数 A* this//------------------------------voidA_show(A*this){coutthis-xendl;}//普通全局函数没有thisvoidfun(A p){}intmain(){//① 分配a的栈内存原始垃圾内存未初始化A a;//调用默认构造**自动把 a 传给形参this**A_constructor(/*this*/a);//a.show();成员函数调用展开//把对象a的地址传给show的this参数A_show(/*this*/a);// fun(a); 传值调用完整展开 //第一步栈上为fun的形参p分配A大小内存原始内存A p;//第二步调用拷贝构造// this pp的地址交给拷贝构造的this新对象// other a源对象a地址传入A_copy_constructor(/*this*/p,/*other*/a);//p构造完毕进入funfun(p);return0;}在拷贝构造函数内部this p // this指向fun的形参p新对象-新拷贝的对象other a // other指向main里面的a源对象-被拷贝的对象this-x other-x;//等价p.x a.x对应的关系
返回列表