—— 默认成员函数详解(构造、析构和拷贝构造))
文章目录1. 类的默认成员函数1.1构造函数1.1.1构造函数的形式1.1.2 为什么要有构造函数1.1.3 构造函数可以重载1.1.4 构造函数功能容易混淆的地方1.1.5 默认生成的构造函数1.1.6 默认构造函数1.1.7 默认生成的构造函数的功能1.2 析构函数1.2.1 析构函数的形式1.2.2 为什么要有析构函数1.2.3 析构函数的功能1.2.4 默认生成的析构函数功能1.2.5 需要写析构函数的情况1.2.6 析构函数调用顺序1.3 拷贝构造函数1.3.1 拷贝构造函数形式1.3.2 为什么要有拷贝构造函数1.3.3拷贝构造函数的作用1.3.4 拷贝构造函数中的引用1.3.5默认生成的拷贝构造函数1.3.6 需要写拷贝构造函数的情况1.3.7 传值返回这里是think的博客希望可以一起交流知识一起think今天我们来学习C类和对象二一起来think1. 类的默认成员函数类的默认成员函数一共有六个今天我们讲3个分别是构造函数析构函数拷贝构造函数类的默认成员函数表示如果我们不写类会默认生成那么生成的是长什么样的有什么功能是否可以实现我们需要的功能如果实现不了我们自己如何自己实现1.1构造函数1.1.1构造函数的形式classDate{public:Date(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};构造函数名和类名相同。构造函数没有返回值这种没有是连void都不用写的意思。1.1.2 为什么要有构造函数我们看到C语言的Stack中有初始化函数每次用Stack结构都要调用初始化函数比较麻烦而有了构造函数在用类实例化对象时编译器会自动调用对应的构造函数。1.1.3 构造函数可以重载构造函数一般有三种最为常见的形式无参构造函数全缺省构造函数和正常形式。有参但是无缺省classDate{public://1Date(){_year1;_month1;_day1;}//2Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}//3Date(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};1和2冲突了Date d1;时会冲突因为不知道编译器不知调用哪一个但是如果没有这么写还是可以编译通过。23冲突是函数重载上面的冲突因为函数重载只看参数个数和参数类型不看有无缺省值。13可以共存而我们一般写2因为2的功能可以完美实现13函数的功能并且还有半缺省的功能可谓功能完全。1.1.4 构造函数功能容易混淆的地方从字面上看构造函数好像是开空间创建一个对象的意思实际上不是常用的局部对象在函数栈帧创建以后空间就开好了实际上构造函数的功能是初始化。1.1.5 默认生成的构造函数编译器默认生成的构造函数是在我们不写任何构造函数的时候会生成的但是一旦我们写了构造函数其就不会生成。1.1.6 默认构造函数默认构造函数和编译器默认生成的构造函数是不同的前者包含后者而前者除了后者还有无参构造函数和全缺省构造函数classDate{public://1 无参构造函数Date(){_year1;_month1;_day1;}//2 全缺省构造函数Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}//3 编译器默认生成的构造函数Date(){}private:int_year;int_month;int_day;};在VS2022中依据编译器默认生成的构造函数的功能来看Date类中的编译器默认生成的构造函数的形式如上图3中所示。形式上默认构造函数是指可以在不传递任何参数的情况下调用的构造函数。理解1作用上默认构造函数是指在没有显式指定初始化方式时编译器默认调用的构造函数。理解21.1.7 默认生成的构造函数的功能编译器默认生成的构造函数的功能对内置类型intdouble等等定义的变量的初始化是没有要求的看编译器VS2022中一般不初始化一般是随机值。对自定义类型调用它的的默认构造函数如果该类型没有默认构造函数那么会直接报错解决方法除了续写一个默认构造函数还可以用初始化列表的方式解决这里如果写初始化列表篇幅过长留到下一篇。什么情况下没有默认构造函数只有半缺省或者正常函数。此时因为有构造函数所以编译器默认生成的构造函数不会生成usingnamespacestd;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;}如果成员变量中加上int sizesize在VS2022中会被初始化为0即这里是特殊情况这里的内置类型会被初始化。1.2 析构函数1.2.1 析构函数的形式classDate{public:~Date(){}private:int_year;int_month;int_day;};析构函数名就是类名前面加上一个~。和构造函数一样无返回值连void都没有而且还没有参数1.2.2 为什么要有析构函数C语言中Stack有销毁函数为了销毁资源防止内存泄漏但是每一次都要自己调用其实是比较麻烦的也可能会遗忘但是如果有了析构函数就不会有这种情况的出现因为析构函数是会在对象生成周期结束之前自动调用。1.2.3 析构函数的功能析构函数并不是销毁对象的而是来完成对象中的资源清理工作的防止内存泄漏的。1.2.4 默认生成的析构函数功能如果不写析构函数编译器会默认生成析构函数并且一个对象只有一个析构函数默认生成的析构函数对内置类型不做处理对于自定义成员调用它的析构函数。1.2.5 需要写析构函数的情况当类没有管理资源如日期类不需要写析构函数。当类间接管理资源如两个栈实现一个队列不用写析构函数。当类直接管理资源如栈需要自己写析构函数。注意当有类间接管理资源时我们又显式写了析构函数此时会自动调用自定义成员的析构函数。即不管写不写析构函数都会自动调用自定义成员的析构函数。总结 当类直接管理资源需要自己写析构函数。1.2.6 析构函数调用顺序函数栈帧是先进后出所以一个局部域中的多个对象先定义的后析构1.3 拷贝构造函数1.3.1 拷贝构造函数形式cppclassDate{public:Date(constDated){_yeard._year;_monthd._month;_dayd._month;}private:int_year;int_month;int_day;};拷贝构造函数其实就是构造函数的重载但是它的第一个参数必须是自身类类型的引用语法上可以不加const但是实际开发中要加因为加了实参范围更广如果有其他参数这些参数必须要有缺省值。1.3.2 为什么要有拷贝构造函数问题C语言中结构体的拷贝是直接逐字节拷贝即浅拷贝不管内部是否存在资源如果有资源浅拷贝是直接对指针的值进行拷贝这样会导致重复释放同一块资源导致程序崩溃。#includestdio.hstructA{int*a;intb;};intmain(){structAa1;a1.a(int*)malloc(sizeof(int)*2);structAa2a1;free(a1.a);free(a2.a);return0;}解决方法在C中就把C语言中的只能值/浅拷贝变成了自己定这就是拷贝构造函数出现的原因。1.3.3拷贝构造函数的作用拷贝构造函数用于用一个已经存在的对象初始化另一个新对象。源对象作为参数传入目标对象调用拷贝构造函数完成初始化。在实例化对象会用到。#includeiostreamusingnamespacestd;classDate{public:Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}Date(constDated){_yeard._year;_monthd._month;_dayd._day;}private:int_year;int_month;int_day;};intmain(){Dated1(3,4,5);//1Date d2d1;//2Dated2(d1);}一般常用第一个因为方便““本来就有拷贝之意第二个符合构造函数的调用形式也算是一个倾向拷贝一个倾向构造了。在函数传值传参的时候会用到。#includeiostreamusingnamespacestd;classDate{public:Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}Date(constDated){_yeard._year;_monthd._month;_dayd._day;}private:int_year;int_month;int_day;};voidfun(Date d){}intmain(){Dated1(3,4,5);fun(d1);}我们知道函数形参是传递的时候才会实例化所以也符合一个已经存在的对象初始化另一个新对象的情况。1.3.4 拷贝构造函数中的引用为什么拷贝构造函数第一个参数一定要是自身类类型的引用不可以直接是自身类类型吗不可以因为如果传值传参会导致无穷递归。可以看到fun(d1)d1初始化d有Date dd1;编译器调用Date类的拷贝构造函数使用 d1 初始化 d我们发现要调用拷贝构造这里还是正常的而调用拷贝构造函数之前要传参传参也是Date dd1;又要调用拷贝构造。流程要调用fun函数要先传参传参涉及拷贝要调用拷贝构造函数调用前要传参传参涉及拷贝会调用拷贝构造函数所以即调用拷贝构造函数之前要先调用拷贝构造函数。无穷递归下去导致栈溢出而报错。1.3.5默认生成的拷贝构造函数编译器默认生成的拷贝构造函数是在我们不写任何构造函数的时候会生成的如果写了拷贝构造函数就不会生成。特别注意我们说过拷贝构造函数是构造函数的重载而编译器默认生成的构造函数是在我们不写任何构造函数的时候会生成的所以我们写了拷贝构造函数时编译器默认生成的构造函数不会生成功能值/浅拷贝。1.3.6 需要写拷贝构造函数的情况我们说过编译器默认生成的拷贝构造函数的功能是浅拷贝那么自然需要写拷贝构造函数的情况就是需要深拷贝的情况而直接对资源管理的类才需要深拷贝。即stack类需要深拷贝日期类不需要Myqueue也不需要。stack类有直接的资源要管理。日期类无资源要管理。Myqueue有间接资源要管理默认生成的拷贝构造会调用自定义成员的拷贝构造也不需要我们写。usingnamespacestd;#includeiostreamtypedefintSTDataType;classStack{public:Stack(intn4){_a(STDataType*)malloc(sizeof(STDataType)*n);if(nullptr_a){perror(malloc申请空间失败);return;}_capacityn;_top0;}Stack(constStacks){_a(STDataType*)malloc(sizeof(STDataType)*s._capacity);if(_anullptr){perror(malloc);exit(1);}memcpy(_a,s._a,sizeof(STDataType)*s._top);_capacitys._capacity;_tops._top;}private:STDataType*_a;size_t _capacity;size_t _top;};intmain(){Stacks1(10);Stack s2s1;return0;}写了这个拷贝构造函数就不会因为析构中释放同一块资源导致程序崩溃了。有资源的类不写拷贝构造导致的后果1.3.7 传值返回我们一般用引用返回以减少拷贝但是如果要返回的值是在局部栈帧中的出函数后就销毁了那么就不可以用引用返回尽管引用返回减少拷贝提高效率但是准确性和效率相比肯定优先保证正确性再考虑效率。usingnamespacestd;#includeiostreamtypedefintSTDataType;classStack{public:Stack(intn4){_a(STDataType*)malloc(sizeof(STDataType)*n);if(nullptr_a){perror(malloc申请空间失败);return;}_capacityn;_top0;}Stack(constStacks){_a(STDataType*)malloc(sizeof(STDataType)*s._capacity);if(_anullptr){perror(malloc);exit(1);}memcpy(_a,s._a,sizeof(STDataType)*s._top);_capacitys._capacity;_tops._top;}private:STDataType*_a;size_t _capacity;size_t _top;};Stackfun(){Stack s;returns;}intmain(){fun();return0;}在 VS2019无优化下可以理解为先构造局部对象 s然后执行return s时编译器调用Stack类的拷贝构造函数使用 s 初始化临时对象随后局部对象 s 析构最后临时对象析构。而在 VS2022 开启优化后由于进行了返回值优化省略了拷贝构造过程直接将对象构造在返回位置因此最终只看到一次构造和一次析构。