构造函数·析构函数·拷贝构造)
在开始之前如果没有看类和对象上的推荐先去学习前置知识传送门C类和对象上从定义格式到this指针的深度解析-CSDN博客一.类的默认成员函数前言在类和对象上中我们了解到了一些对类和类外的知识从这里开始我们要正式进入类的内部。默认成员函数就是即使用户没有显示实现编译器也会自动生成的函数。一个类在我们不写的情况下编译器会默认生成六个默认成员函数需要注意的是这六个中最重要的是前四个最后两个取地址重载不重要稍微了解即可。其次就是C11中还会再增加两个默认成员函数这个我们以后再说。默认成员函数很重要也比较复杂我们要带着下面三个问题去思考和学习。• 第一我们不写时编译器默认生成的函数行为是什么• 第二什么时候能满足我们的需求什么时候不能• 第三若编译器默认生成的函数不满足我们的需求那我们自己如何实现用何种语法实现让我们带着这三个问题来进行接下来的学习。二.构造函数更好的保障对象的初始化构造函数时是特殊的成员函数需要注意的是虽然叫做“构造”但是其主要功能却不是开空间去创建对象我们常使用的局部对象是栈帧创建时空间就开好了的而是在对象实例化时去初始化对象。其完成的是我们之前写Stack类时Init函数所做的工作。构造函数的特点1. 函数名与类名相同。2. 无返回值。(真真正正的没有返回值也不是之前的给一个void这个是直接不用写返回值的类型也不必纠结这就是规定)。3. 对象实例化时系统会自动调用对应的构造函数。(这就能很好的保证对象的初始化不用我们每次手动去调用了)4.构造函数可以重载可以根据不同要求写多个构造函数。这四点都是构造函数的基本特点让我们来写一个简单的逻辑去运用它#includeiostream using namespace std; class Date { public: //体现的构造函数特点一函数名与类名相同。二无返回值void也不用写 Date() { _year 1; _month 1; _day 1; } //第二个Date函数体现了四构造函数可以重载。 Date(int year, int month, int day) { _year year; _month month; _day day; } void Print() { cout _year / _month / _day endl; } private: int _year; int _month; int _day; }; int main() { //在调用时会自动初始化体现了特点三 Date d1; Date d2(2026, 10, 3); d1.Print(); d2.Print(); //输出结果1/1/1 // 2026/10/3 return 0; }有的同学可能疑惑我在调用d2的构造函数时显示传参了带上后面一堆实参那我d1就算没有传参为什么不带上“”呢这是因为如果带上那就和函数声明长得一样了会分不清//函数声明 Date d1(); Date func(); //正常实例化调用构造函数 Date d2;如果前四点还是说的基本语法那接下来就是构造函数真正绕的地方了5. 如果类中没有显示定义构造函数则C编译器会自动生成一个无参的构造函数去作为默认构造函数一旦用户显示定义任何一个构造函数编译器将不再生成。诶那让我们看看对与Date类默认生成的构造函数的行为是什么class Date { public: void Print() { cout _year / _month / _day endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print(); //运行结果-858993460/-858993460/-858993460 return 0; }我们发现默认生成的构造函数并没有对我们的日期类完成想要的初始化内部的数据还是一堆随机值那其实在这里构造函数的作用就是去配合语法通过编译的因为语法要求它有所以他就有了但是又啥事不干单纯凑个数。6.无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数都叫做默认构造函数三个的共同特点不传参就可调用。但是这三个函数只能存在一个不能同时存在。无参的和全缺省的虽然构成函数重载但调用时会产生歧义进而报错而编译器默认生成的在显示写了构造后就不会生成了。要注意很多同学会认为默认构造函数就是编译器默认生成的那个函数但实际上无参构造函数全缺省构造函数也是默认构造函数总结一下就是不传实参就可以调用的构造就叫做默认构造。其实编译器默认生成的函数也是个无参构造函数嘛。有些同学可能像我一样好奇如果我写了构造但是又没写默认构造只写了个有参数但是又不全缺省的构造函数会发生啥呢答报错7. 当我们不写时编译器默认生成的构造①对内置类型的成员变量的初始化没有要求也就是说是否初始化是不确定的看编译器但是为了我们的学习和之后的代码可跨平台我们不能把是否初始化寄希望于编译器我们要统一当成不做处理。②对于自定义类型的成员变量编译器会去调用这个成员变量的默认构造函数去初始化。如果这个成员变量没有默认构造函数那么就会报错。那我们在想去初始化这个成员变量就要用到初始化列表了而初始化列表在类和对象下再去讲解。总结编译器会对两种类型分开处理各用各的不会影响内置类型变量编译器不做处理自定义类型变量编译器会去调用它自己的默认构造函数没有默认构造就报错。就是假设一个类既有int类型也有Stack类型那么编译器会分开处理int类型就不处理stack就去调用其自己的构造函数。说明C把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型如int/char/double/指针等自定义类型就是我们使用class/struct等关键字自己定义的类型。下面让我们验证一下第七点所说的对于自定义成员会去调用它自己的构造函数class Stack { public: Stack(int n 4) { _a (int*)malloc(n * sizeof(int)); if (nullptr _a) { perror(malloc申请空间失败); return; } _capacity n; _top 0; } private: int* _a; int _top; int _capacity; }; class MyQueue { private: Stack st1; Stack st2; }; int main() { MyQueue q1; return 0; }使用F11去进行调试可以清楚的看到q1在实例化的时候进入了Stack的构造函数。在这里我们演示了编译器默认生成的构造函数对于MyQueue类的实现那如果我们显示写一个MyQueue的构造函数会发生什么呢代码如下class Stack { public: Stack(int n 4) { _a (int*)malloc(n * sizeof(int)); _capacity n; _top 0; cout Stack(int n)被调用了 endl; } private: int* _a; int _top; int _capacity; }; class MyQueue { public: MyQueue() { _size 0; cout MyQueue()被调用了 endl; } private: Stack st1; Stack st2; int _size; }; int main() { MyQueue q1; return 0; }运行结果为是的这里有两个重要知识点①自动调用机制只要MyQueue里包含了Stack对象无论你写不写MyQueue的构造编译器一定会自动调用Stack的构造函数②先内后外永远是内部的Stack先被构造然后才是MyQueue的构造函数执行。因为必须现有内部零件才能拼出外部整体。到这里构造函数就结束了来让我们回答一下我们提出的三个问题①编译器生成的构造函数行为是对内置类型对象不处理对自定义类型对象调用其自己的构造函数②当一个类内部全是自定义类型时可以不写但是一旦有了内置类型就要手动实现大多数时候都要自己写。③要实现一个默认构造全缺省或无参的再对需求实现其他参数的构造函数。三.析构函数更好的保障对象空间的释放避免内存泄漏析构函数和构造函数的功能相反析构函数不是完成对对象本身的销毁比如局部对象是存在栈帧的函数结束时栈帧销毁不需要我们管。C规定对象在其生命周期结束时会自动调用析构函数完成对对象中的资源的清理工作。析构函数的功能就类比之前写Stack类时Destroy函数的功能。而日期类Date其实是没有资源需要去释放的所以严格来说Data是不需要析构函数的。详细说一下就是Stack中有_a对于堆区空间的申请在其生命周期结束时我们要去释放申请的空间而像_top_year这样的就不用去管马上出就跟着销毁栈帧一起没了。析构函数的特点1. 析构函数是在类名构造函数名前加上字符 ~ 。(就是C语言里的~按位取反表面析构函数与构造函数对称)2. 无参数无返回值。这里和构造类似也不需要加void。3. 一个类只能有一个析构函数原因是第二点无参数则不能构成函数重载所以就一个。若未显式定义系统会自动生成默认的析构函数。4. 对象声明周期结束时系统会自动调用析构函数。并且与栈类似栈是后进先出析构是后定义的先析构。让我们写一个小的析构函数去用一下class Stack { public: Stack(int n 4) { _a (int*)malloc(n * sizeof(int)); _capacity n; _top 0; cout Stack(int n)被调用 endl; } ~Stack() { free(_a); _a nullptr; cout this ~Stack()被调用 endl; } private: int* _a; int _top; int _capacity; }; int main() { Stack st1; Stack st2; printf(////////////////\n); printf(%p\n, st1); printf(%p\n, st2); printf(////////////////\n); return 0; }输出结果可见析构函数确实在变量生命周期结束时被自动调用了观察中间部分两个地址的先后顺序和最下面部分两个地址的先后顺序不同可知后定义的st2被先析构了。5. 跟构造函数类似我们不写编译器自动生成的析构函数对内置类型成员不做处理自定义类型成员会调用它自己的析构函数。6. 还需注意的是我们显式写了析构函数对于其中的自定义类型成员也仍然会调用它自己的析构也就是说自定义类型成员无论什么情况都会自动调用析构函数。这里又和构造类似了让我写一个逻辑观察一下。class Stack { public: ~Stack() { free(_a); _a nullptr; cout this ~Stack()被调用 endl; } private: int* _a; int _top; int _capacity; }; class MyQueue { public: ~MyQueue() { free(ptr); cout ~MyQueue()被调用 endl; } private: int* ptr; Stack st1; Stack st2; }; int main() { MyQueue q1; return 0; }输出结果7. ①如果类中没有申请资源时如Date析构函数可以不写直接使用编译器默认生成的析构函数。②如果默认生成的析构就可以用也不需要显式写析构如只有两个Stack类型成员变量的MyQueue。③但是当有资源申请时一定要自己写析构否则会造成资源泄露如Stack。8. 一个局部域的多个对象C规定后定义的先析构。这个在前文也提到了。到这里析构函数就结束了来让我们回答一下我们提出的三个问题①编译器生成的析构函数行为是对内置类型对象不处理对自定义类型对象调用其自己的析构函数②当一个类内部全是自定义类型和全部都是内置类型没有申请资源时可以不写但是一旦有了资源申请就一定要手动实现少数情况要自己写。③这个主要就是记得把申请的资源给释放了就行了。四.拷贝构造函数同类型的一个对象去初始化另一个对象定义如果一个构造函数的第一个参数是自身类类型的引用且任何额外的参数都有缺省值默认值则次构造函数也叫做拷贝构造函数也就是说拷贝构造是一个特殊的构造函数。目的完成拷贝初始化用一个同类型的对象去初始化另一个对象。下面是两种写法这是规定哦//写法一 Date d2(d1); //写法二 Date d3 d1;拷贝构造的特点1. 拷贝构造函数是构造函数的一个重载。2. 拷贝构造函数的第一个参数必须是当前类类型的引用使用传值方式编译器直接报错因为语法上会引发无穷递归调用马上就说明为什么。拷贝构造也可以多个参数但是第一个参数必须是类类型的引用后面的参数必须有缺省值。Date(Date d) { _year d._year; _month d._month; _day d._day; } //调用 Date d2 d1 //这里就相当于调用了函数 Date (Date* const this, Date d) { this-_year d._year; ~..... } this指向的就是d2而参数d进行d._year的就是d13. C规定自定义类型对象进行拷贝行为必须调用拷贝构造所以①一个对象初始化同类型的另一个对象②自定义类型传值传参③自定义类型传值返回都会去调用拷贝构造函数去完成这也是为什么上面说若使用传值的方式去实现拷贝构造会引发无穷递归。简单说一下就是当写成传值传参时你实参传给形参的这一步就要去进行拷贝构造然后就要把实参传给新调用的拷贝构造然后就再次触发传值传参~~就停不下来了无限递归。还有就是一个以自定义类型为参数的普通函数在调用时也会出现先去拷贝构造再进入函数的情况也建议写成引用传参。//会先去把实参拷贝给形参调用拷贝构造 void func(Date d1) { cout 1111 endl; } //写成引用就没事了当然加一个const更好 void func(const Date d1) { cout 1111 endl; }4. 若未显式定义拷贝构造编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员对象会完成值拷贝/浅拷贝就是一个字节一个字节的拷贝对自定义类型成员变量会调用它自己的拷贝构造。注意这种值拷贝对于正常的内置类型没有什么问题但是当涉及到资源申请时其一个字节一个字节拷贝的行为会导致在Stack d2 d1;时因为d1 的 _a是指向堆区空间的地址值拷贝会之间把这个地址给一个字节一个字节的拷贝给d2 的_a直接导致二者的_a指向同一块空间两者在使用时会互相改变其实这还好说而当d1d2出了生命周期后自动调用析构函数去释放空间因二者释放的是同一空间直接引发程序崩溃。5. ①像Date这样的类成员变量全是内置类型且没有指向资源编译器自动生成的拷贝构造就可以完成需要的拷贝所以不需要我们显示实现拷贝构造。②像Stack这样的类虽然也都是内置类型但是_a指向了资源编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。③像MyQueue这样的类型内部主要是自定义类型Stack成员编译器自动生成的拷贝构造会调用Stack的拷贝构造也不需要我们显示实现MyQueue的拷贝构造。这里还有一个小技巧如果一个类显示实现了析构并释放资源那么他就需要显示写拷贝构造否则就不需要。这里不能光看成若构造申请了空间那我就要去实现拷贝构造因为还有打开文件这一情况。6. 传值返回会产生一个临时对象进而调用拷贝构造传引用返回返回的是返回对象的别名(引用)没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象函数结束就销毁了那么使用引用返回是有问题的这时的引用相当于一个野引用类似一个野指针一样。传引用返回可以减少拷贝但是一定要确保返回对象在当前函数结束后还在才能用引用返回。在这一节的最后让我们去回答对于拷贝构造的那三个问题①自动生成的拷贝构造对内置类型成员对象会完成值拷贝/浅拷贝就是一个字节一个字节的拷贝对自定义类型成员变量会调用它自己的拷贝构造。②对于一般的类都能满足需求而如果一个类显示实现了析构并释放资源那么他就需要显示写拷贝构造。③正常的不用写特殊的比如Stack让我们写一个看看Stack(const Stack st) { _a (int*)malloc(st._capacity * sizeof(int)); if (nullptr _a) { perror(申请空间失败); return; } memcpy(_a, st._a, st._top * sizeof(int)); _capacity st._capacity; _top st._top; }完。