文章目录1. 类的默认成员函数2. 构造函数2.1 介绍2.2 构造函数的特点和创建2.2.1 构造函数的特点2.2.2 构造函数的创建2.3构造函数的调用2.4默认构造函数2.4.1 自动生成的默认构造函数2.4.2 全缺省构造函数2.5 构造函数对不同类型的处理2.5.1 构造函数对内置类型的处理2.5.2 构造函数对自定义类型的处理2.5.3 既有自定义类型也有内置类型3. 析构函数3.1 析构函数的定义3.1.1 为什么析构函数不是完成对对象本身的销毁3.1.2 没有空间申请的类不需要析构函数3.2 析构函数的创建3.3 析构函数的特性3.4 编译器的默认行为3.5 对自定义类型的特殊规定3.6 最后总结1. 类的默认成员函数默认成员函数就是用户没有显式实现编译器会自动生成的成员函数称为默认成员函数。一个类我们不写的情况下编译器会默认生成以下6个默认成员函数需要注意的是这6个中最重要的是前4个最后两个取地址重载不重要我们稍微了解一下即可。其次就是C11以后还会增加两个默认成员函数移动构造和移动赋值这个我们后面再讲解。默认成员函数很重要也比较复杂我们要从两个方面去学习1. 我们不写时编译器默认生成的函数行为是什么是否满足我们的需求2. 编译器默认生成的函数不满足我们的需求我们需要自己实现那么如何自己实现2. 构造函数2.1 介绍构造函数的定义构造函数是类的一种特殊成员函数在对象创建时自动调用用来初始化对象。它没有返回类型名字与类名相同可以重载。构造函数的主要任务需要注意的是构造函数虽然名称叫构造但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时空间就开好了)而是对象实例化时初始化对象。构造函数的本质构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能构造函数自动调用的特点就完美的替代了Init2.2 构造函数的特点和创建2.2.1 构造函数的特点1. 函数名与类名相同。2. 无返回值。(返回值啥都不需要给也不需要写void不要纠结C规定如此)3. 对象实例化时系统会自动调用对应的构造函数。4. 构造函数可以重载(意味着我们可以写多个构造函数)。2.2.2 构造函数的创建classData{public:Data()//函数名与类名相同无返回值。{_year1900;_month1;_day1;}//构造函数可以重载Data(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};intmain(){Data d1;//对象实例化时会自动调用//无需显式调用return0;}1. 在类Date中创建了一个构造函数Date构造函数的名称和类的名称一致。2. 构造函数Date没有返回类型也没有返回值。3. 构造函数Date和Date (int year,int month,int day)构成重载。2.3构造函数的调用对象实例化的时候构造函数会自动调用对应的构造函数1. 调用无参数的构造函数直接创建Date类的时候自动调用了构造函数。2. 调用有参的构造函数后面要加括号和传递的参数值。classData{public:Data()//函数名与类名相同无返回值。{_year1900;_month1;_day1;}//构造函数可以重载Data(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};intmain(){//无参构造函数Data d1;//对象实例化时会自动调用//无需显式调用//有参构造函数Datad2(2026,9,26);return0;}Date 类型实例化一个对象d1此时自动调用第一个无参构造函数。Date类型实例化一个对象d2此时实自动传递参数(2026,9,26)调用第二个构造函数。注意不可以在无参构造函数后加括号如Data d1();,这样会造成无法和函数声明如Data func();区分产生歧义2.4默认构造函数2.4.1 自动生成的默认构造函数如果类中没有显式定义构造函数则C编译器会自动生成一个无参的默认构造函数一旦用户显式定义构造编译器将不再生成。无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数都叫做默认构造函数。但是这三个函数有且只有一个存在不能同时存在。2.4.2 全缺省构造函数Date(intyear2026,intmonth9,intday26){_yearyear;_monthmonth;_dayday;}1. 全缺省构造函数可以无实参调用所以它也是默认构造函数。无参定义对象时写Date d;不能写Date d();传参时仍要写括号。2. 全缺省构造函数不能和无参构造函数同时存在会产生调用歧义。3. 全缺省构造函数的使用一定程度上取代了上面的两个构造函数常见错误写法与报错classData{public:Data(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};intmain(){Data d1;return0;}Date d1;要求调用默认构造函数也就是可以无参调用的构造函数。但是Date(int year, int month, int day)它必须传三个参数才能调用不是默认构造函数。而且只要类里声明了任何构造函数编译器就不会再隐式生成默认构造函数了。所以会引发报错总结默认构造函数 可以不提供实参就能调用的构造函数。它可以是你写的无参构造、全缺省构造也可以是编译器在你没写任何构造函数时隐式生成的那个。编译器隐式生成的那个会对类类型成员调默认构造对内置类型成员通常不初始化如果成员没有默认构造它就被删除。2.5 构造函数对不同类型的处理C把类型分成内置类型和自定义类型。内置类型就是语言提供的原生数据类型如int/char/double/指针等自定义类型就是我们使用class/struct等关键字自己定义的类型。2.5.1 构造函数对内置类型的处理我们不写编译器默认生成的构造对内置类型成员变量的初始化没有要求也就是说是是否初始化是不确定的看编译器。2.5.2 构造函数对自定义类型的处理对于自定义类型成员变量要求调用这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数那么就会报错我们要初始化这个成员变量需要用初始化列表才能解决初始化列表我们下个章节再细细讲解。双栈实现队列classStack{public:Stack(intn4){_a(int*)malloc(sizeof(int)*n);if(nullptr_a){perror(malloc申请空间失败);return;}_capacityn;_top0;}private:int*_a;int_capacity;int_top;};//无需自己构造函数classMyQueue{public://编译器默认⽣成MyQueue//的构造函数调⽤了Stack的构造//完成了两个成员的初始化private:Stack _st1;Stack _st2;};intmain(){MyQueue q;return0;}MyQueue 没有声明任何构造函数所以编译器会隐式生成一个默认构造函数。这个默认构造函数会1. 按成员声明顺序依次初始化 _st1、_st22. 对每个 Stack 成员调用它的默认构造函数。而 Stack 有Stack(intn4)这是一个全缺省构造函数可以无参调用所以它也是默认构造函数。因此MyQueue q;// OK等价于_st1.Stack(4);_st2.Stack(4);如果stack的默认构造函数不存在则会报错现在我们没办法解决这个问题在后面的文章中再讲初始化列表。总结类似日期类和Stack这样的默认生成的构造函数存在但是它不做实际的动作不满足我们的需求所以需要我们显式写而像上面MyQueue 这样的类它生成的默认构造对自定义类型会调用它的默认构造满足我们的需求就不需要显式写了。2.5.3 既有自定义类型也有内置类型classStack{public:Stack(intn4){_a(int*)malloc(sizeof(int)*n);if(nullptr_a){perror(malloc申请空间失败);return;}_capacityn;_top0;}private:int*_a;int_capacity;int_top;};//无需自己构造函数classMyQueue{public://编译器默认⽣成MyQueue//的构造函数调⽤了Stack的构造//完成了两个成员的初始化private:Stack _st1;Stack _st2;int_size;};intmain(){MyQueue q;return0;}这里让内置类型和自定义类型混合初始化了内置类型。我们调试发现在vs编译器下将内置类型初始化成了0这是C标准和编译器的问题。所以这个size是一个大坑因为C标准没有规定有的编译器不会处理问题就大了。因此size的初始化构造函数得自己写。classMyQueue{public://编译器默认⽣成MyQueue//的构造函数调⽤了Stack的构造//完成了两个成员的初始化private:Stack _st1;Stack _st2;int_size0;//内置类型成员显式初始化};3. 析构函数3.1 析构函数的定义析构函数与构造函数功能相反析构函数不是完成对对象本身的销毁比如局部对象是存在栈帧的函数结束栈帧销毁他就释放了不需要我们管C规定对象在销毁时会自动调用析构函数完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现Destroy功能3.1.1 为什么析构函数不是完成对对象本身的销毁这个说法是为了区分两件事资源清理和内存回收。析构函数负责的是前者不是后者。对象“本身”的销毁内存回收是在析构函数执行完之后由编译器或 delete 运算符完成的。析构函数执行时对象内存仍然有效看这个例子classA{public:intx42;~A(){std::coutx\n;// 仍然可以访问成员}};如果析构函数真的“销毁了对象本身”那它里面就不该还能访问 x。但事实上析构函数体执行时this 指针有效成员变量仍然存在你可以正常读写成员调用成员函数。说明对象的内存空间还没有被回收。所以真正的销毁顺序是先调析构再释放内存因为日常说“对象销毁了”往往把整个生命周期结束笼统地叫“销毁”。严格来说对象销毁 调用析构函数 回收内存。析构函数只是其中一环。很多教材为了简化说“析构函数完成对象销毁”其实不精确。更准确的说法是析构函数完成对象中资源的清理对象本身的内存回收由运行时/编译器/delete 完成。3.1.2 没有空间申请的类不需要析构函数如果一个类不需要手动清理任何资源就不需要自己写析构函数编译器默认生成的析构函数就足够了。这里的“资源”不仅指堆内存还包括文件句柄、网络连接、锁、数据库连接等。“空间申请”是最典型的一种。如日期类classDate{public:// ………………private:int_year;int_month;int_day;};_year、_month、_day 都是内置类型 int它们不持有任何需要手动释放的资源比如 malloc / new 出来的内存、文件句柄、锁等编译器隐式生成的析构函数对 int 成员什么都不用做这本来就是正确的。所以不写析构函数完全没问题写了空的析构函数也能编译但没必要如果以后给 Date 加了需要手动释放的成员比如裸指针再考虑自己写析构函数。3.2 析构函数的创建1. 析构函数名是在类名前加上字符~2. 无参数无返回值。(这里跟构造类似也不需要加void)以栈举例classStack{public:Stack(intn4){_a(int*)malloc(sizeof(int)*n);...}~Stack(){// 必须自己写free(_a);_anullptr;}private:int*_a;int_capacity;int_top;};3.3 析构函数的特性1. 对象生命周期结束时系统会自动调用析构函数不能像普通成员函数那样通过对象主动调用。2. 如果类中没有申请资源时析构函数可以不写直接使用编译器生成的默认析构函数如Date如果默认生成的析构就可以用也就不需要显式写析构如MyQueue但是有资源申请时一定要自己写析构否则会造成资源泄漏如Stack。3. 一个局部域的多个对象C规定后定义的先析构因为栈帧后进先出。#includeiostreamusingnamespacestd;classData{public:Data(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}voidPrint(){cout_year/_month/_dayendl;}//日期类其实不需要析构这里只是为了看谁先析构谁后析构~Data(){coutthis~Data()endl;}private:int_year;int_month;int_day;};classStack{public:Stack(intn4){_a(int*)malloc(sizeof(int)*n);if(nullptr_a){perror(malloc申请空间失败);return;}_capacityn;_top0;}// 析构函数释放自己申请的堆内存~Stack(){coutthis ~Stack()endl;if(_a!nullptr){free(_a);// 释放 malloc 申请的空间_anullptr;// 置空防止悬空指针}_capacity0;_top0;}private:int*_a;size_t _capacity;size_t _top;};classMyQueue{// 不需要写构造函数也不需要写析构函数// 编译器默认生成的析构函数会自动调用 _st1 和 _st2 的析构函数private:Stack _st1;Stack _st2;};intmain(){//后定义的先析构Data d1;Datad2(2026,8,24);d1.Print();d2.Print();Stack st1;//MyQueue q;return0;}执行后发现确实是后定义的先析构。3.4 编译器的默认行为1. 一个类只能有一个析构函数。若未显式定义系统会自动生成默认的析构函数2. 跟构造函数类似我们不写编译器自动生成的析构函数对内置类型成员不做处理自定类型成员会调用他的析构函数如果没有找到会报错。3.5 对自定义类型的特殊规定需要注意的是我们显示写析构函数对于自定义类型成员也会调用他的析构也就是说自定义类型成员无论什么情况都会自动调用析构函数。#includeiostreamusingnamespacestd;classData{public:Data(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}voidPrint(){cout_year/_month/_dayendl;}//日期类其实不需要析构这里只是为了看谁先析构谁后析构~Data(){coutthis~Data()endl;}private:int_year;int_month;int_day;};classStack{public:Stack(intn4){_a(int*)malloc(sizeof(int)*n);if(nullptr_a){perror(malloc申请空间失败);return;}_capacityn;_top0;}// 析构函数释放自己申请的堆内存~Stack(){coutthis~Stack()endl;if(_a!nullptr){free(_a);// 释放 malloc 申请的空间_anullptr;// 置空防止悬空指针}_capacity0;_top0;}private:int*_a;size_t _capacity;size_t _top;};classMyQueue{public:~MyQueue(){coutthis~MyQueueendl;}// 不需要写构造函数也不需要写析构函数// 编译器默认生成的析构函数会自动调用 _st1 和 _st2 的析构函数private:Stack _st1;Stack _st2;};intmain(){//后定义的先析构Data d1;Datad2(2026,8,24);d1.Print();d2.Print();Stack st1;MyQueue q;return0;}1. 析构顺序铁律先构造的后析构后构造的先析构。成员按声明顺序构造按声明逆序析构。2. 自定义类型成员的析构是自动的你在 MyQueue 里只写了 cout this “~MyQueue” endl;完全没有写 _st1.~Stack()但是编译器依然在 ~MyQueue() 执行完毕后自动帮你调用了 _st2 和 _st1 的析构函数。这就是 C 组合Composition机制的安全保障。3. 内置类型成员的清理靠自觉Data 里的 int 成员在析构后依然保持原样没被清零也不需要清零因为马上就被栈帧回收了。而 Stack 里的 int * _a 指针如果不写 free 和 nullptr就会造成内存泄漏和悬空指针。所以对于自定义类型的成员不写也好写也好他都会去调用这个自定义成员的自己的析构函数。目的是防止内存泄漏。3.6 最后总结1. 没有申请资源如 Date类内成员全是内置类型int 等没有需要手动释放的资源。可以不写析构函数编译器生成的默认析构对内置类型什么都不做完全正确。2. 默认生成的析构够用如 MyQueue类内成员是自定义类型Stack且这些成员自己管理好了自己的资源。不需要显式写析构函数编译器生成的默认析构会自动按逆序调用成员的析构函数完美完成清理。3. 有资源申请如 Stack类内直接使用了裸指针int * _a申请了堆内存。必须自己写析构函数否则默认生成的析构对内置指针不做处理会导致 malloc 的内存永远无法释放造成内存泄漏。