C 类和对象入门二默认成员函数、构造函数和析构函数详解 星恒随风个人主页❄️ 个人专栏《指针合集》《C语言基础》《数据结构》《机器学习导论》《前端基础》《python基础》✨ 数据即知识压缩即智能目录C 类和对象入门二默认成员函数、构造函数和析构函数详解前言一、什么是默认成员函数1.1 默认成员函数的基本概念二、构造函数对象出生时自动初始化2.1 构造函数解决了什么问题2.2 构造函数的基本写法三、默认构造函数是编译器生成的吗3.1 什么叫默认构造函数3.2 默认构造函数只能有一个能被无参调用四、一个容易踩的坑4.1 这不是创建对象五、编译器默认生成的构造函数会做什么5.1 对内置类型成员5.2 对自定义类型成员六、析构函数对象死亡前自动清理资源6.1 析构函数解决了什么问题6.2 析构函数的基本写法七、析构函数不是销毁对象本身7.1 对象空间谁来释放八、有资源申请时一定要自己写析构8.1 Date 不需要Stack 需要九、多个对象的析构顺序9.1 后创建的先析构十、构造和析构带来的代码变化10.1 C 语言版本容易忘记 Init 和 Destroy10.2 C 版本自动调用构造和析构十一、本文总结前言这一章要讲的内容是默认成员函数。所谓默认成员函数就是即使我们不写编译器在某些情况下也会自动生成的成员函数。一个类中比较重要的默认成员函数主要有构造函数析构函数拷贝构造函数赋值运算符重载另外还有取地址运算符重载C11 之后还有移动构造和移动赋值。入门阶段先重点理解前四个。这一篇先讲前两个构造函数对象创建时负责初始化析构函数对象销毁时负责清理资源它们解决的问题很现实对象创建时怎么保证内部数据是合理的对象销毁时怎么保证申请过的资源被释放一、什么是默认成员函数1.1 默认成员函数的基本概念默认成员函数指的是用户没有显式实现时编译器可能会自动生成的成员函数。例如classDate{private:int_year;int_month;int_day;};这个类里我们没有写构造函数、析构函数、拷贝构造和赋值运算符重载。但这并不代表这些函数完全不存在。在需要的时候编译器会尝试自动生成它们。不过这里有一个关键点编译器自动生成不代表一定符合我们的需求。比如Date这种类里面只有三个int编译器默认生成的函数一般够用。但如果是Stack这种类内部有动态申请的空间classStack{private:int*_a;size_t _capacity;size_t _top;};那编译器默认生成的拷贝行为可能就有问题。所以学习默认成员函数要抓住两个问题第一如果我们不写编译器默认生成的行为是什么第二如果默认行为不满足需求我们应该怎么自己写二、构造函数对象出生时自动初始化2.1 构造函数解决了什么问题在学习构造函数之前我们经常会写一个Init函数。比如日期类classDate{public:voidInit(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};使用时Date d;d.Init(2024,4,14);这种写法能用但有一个问题如果用户忘记调用Init()对象内部数据就是不确定的。构造函数就是用来解决这个问题的。构造函数会在对象实例化时自动调用负责完成对象初始化。2.2 构造函数的基本写法构造函数有几个特点函数名和类名相同没有返回值连void也不写对象创建时自动调用构造函数可以重载。例如classDate{public:Date(){_year1;_month1;_day1;}Date(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}voidPrint(){cout_year/_month/_dayendl;}private:int_year;int_month;int_day;};使用时Date d1;Dated2(2024,4,14);其中Date d1;调用无参构造函数。Dated2(2024,4,14);调用带参构造函数。三、默认构造函数是编译器生成的吗3.1 什么叫默认构造函数很多初学者会误以为默认构造函数就是编译器自动生成的构造函数。这个说法不够准确。准确地说不需要传实参就能调用的构造函数都叫默认构造函数。所以以下三种都可以叫默认构造函数第一种无参构造函数。Date(){_year1;_month1;_day1;}第二种全缺省构造函数。Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}第三种用户没有写构造函数时编译器自动生成的构造函数。3.2 默认构造函数只能有一个能被无参调用下面这种写法会有问题classDate{public:Date(){_year1;_month1;_day1;}Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};因为Date d;既可以调用无参构造函数也可以调用全缺省构造函数。编译器会不知道你到底想调用哪个。所以实际写代码时无参构造和全缺省构造一般不要同时写。更推荐这种写法Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}这样既能支持Date d1;也能支持Dated2(2024,4,14);四、一个容易踩的坑4.1 这不是创建对象看下面代码Dated3();很多初学者会以为这是创建了一个对象d3并调用无参构造。但它不是。它会被编译器理解成声明了一个函数 d3这个函数无参返回值类型是 Date。所以创建无参对象时不要写括号。五、编译器默认生成的构造函数会做什么5.1 对内置类型成员如果我们什么构造函数都不写编译器会自动生成一个默认构造函数。但它对内置类型成员比如intchardouble指针通常不会主动初始化成我们想要的值。例如classDate{private:int_year;int_month;int_day;};写Date d;对象确实被创建了。但_year、_month、_day不一定是合理值。所以对于这种类如果你希望日期默认是1900-1-1就应该自己写构造函数。5.2 对自定义类型成员如果类中有自定义类型成员编译器默认生成的构造函数会尝试调用这个成员自己的默认构造函数。例如classStack{public:Stack(intn4){_a(int*)malloc(sizeof(int)*n);_capacityn;_top0;}private:int*_a;size_t _capacity;size_t _top;};classMyQueue{private:Stack pushst;Stack popst;};MyQueue没有写构造函数。但创建MyQueue对象时MyQueue mq;编译器生成的默认构造函数会自动调用pushst和popst的构造函数。这也是 C 对象组合中非常重要的一点外层对象创建时内部成员对象也会被构造。六、析构函数对象死亡前自动清理资源6.1 析构函数解决了什么问题构造函数负责对象初始化。析构函数负责对象销毁前的清理工作。比如Date类classDate{private:int_year;int_month;int_day;};它没有动态申请资源所以通常不需要自己写析构函数。但是Stack类中有动态内存classStack{private:int*_a;size_t _capacity;size_t _top;};如果构造函数里malloc了空间析构函数里就应该free掉。否则就会造成资源泄漏。6.2 析构函数的基本写法析构函数的特点函数名是在类名前面加~没有参数没有返回值连void也不写一个类只能有一个析构函数对象生命周期结束时自动调用。例如classStack{public:Stack(intn4){_a(int*)malloc(sizeof(int)*n);if(_anullptr){perror(malloc fail);return;}_capacityn;_top0;}~Stack(){free(_a);_anullptr;_capacity0;_top0;}private:int*_a;size_t _capacity;size_t _top;};这里~Stack()就是析构函数。当Stack对象生命周期结束时系统会自动调用它。七、析构函数不是销毁对象本身7.1 对象空间谁来释放需要注意析构函数不是负责销毁对象本身而是负责清理对象内部申请的资源。比如局部对象voidFunc(){Stack st;}st是局部对象它的对象空间在栈帧里。函数结束时栈帧销毁对象空间自然释放。析构函数负责的是free(_a);也就是释放对象内部动态申请的资源。所以可以这样理解对象空间由生命周期管理对象内部资源由析构函数清理。八、有资源申请时一定要自己写析构8.1 Date 不需要Stack 需要对于DateclassDate{private:int_year;int_month;int_day;};成员都是内置类型没有动态资源默认析构函数就够了。对于StackclassStack{private:int*_a;size_t _capacity;size_t _top;};_a指向动态申请的空间。如果不写析构函数这块空间不会自动释放。所以需要自己写~Stack(){free(_a);_anullptr;_capacity0;_top0;}可以记住类里如果自己申请了资源通常就要自己释放资源。九、多个对象的析构顺序9.1 后创建的先析构在一个局部作用域中如果有多个对象intmain(){Stack st1;Stack st2;Stack st3;return0;}析构顺序通常是st3 st2 st1也就是后定义的对象先析构。这个规则和栈的特点很像先进后出。十、构造和析构带来的代码变化10.1 C 语言版本容易忘记 Init 和 Destroy以前用 C 写栈时可能是ST st;STInit(st);// 使用栈STDestroy(st);如果中间某个分支提前返回就可能忘记调用STDestroy。比如括号匹配问题中一旦发现不匹配直接返回如果忘记销毁栈就可能造成资源泄漏。10.2 C 版本自动调用构造和析构C 中写成Stack st;对象创建时自动调用构造函数。函数结束或对象离开作用域时自动调用析构函数。这样就不需要我们手动写st.Init();st.Destroy();这就是构造和析构最大的价值让对象的初始化和清理自动发生减少忘记调用的风险。十一、本文总结这一篇主要讲了 C 默认成员函数中的构造函数和析构函数。构造函数对象创建时自动调用主要任务是初始化对象函数名和类名相同没有返回值可以重载不传参能调用的构造函数都叫默认构造函数。析构函数对象生命周期结束时自动调用主要任务是清理对象内部资源函数名是~类名无参数、无返回值一个类只能有一个析构函数有动态资源申请时通常必须自己写析构。构造函数负责对象出生时初始化析构函数负责对象死亡前清理资源。
C++ 类和对象入门(二):默认成员函数、构造函数和析构函数详解
C 类和对象入门二默认成员函数、构造函数和析构函数详解 星恒随风个人主页❄️ 个人专栏《指针合集》《C语言基础》《数据结构》《机器学习导论》《前端基础》《python基础》✨ 数据即知识压缩即智能目录C 类和对象入门二默认成员函数、构造函数和析构函数详解前言一、什么是默认成员函数1.1 默认成员函数的基本概念二、构造函数对象出生时自动初始化2.1 构造函数解决了什么问题2.2 构造函数的基本写法三、默认构造函数是编译器生成的吗3.1 什么叫默认构造函数3.2 默认构造函数只能有一个能被无参调用四、一个容易踩的坑4.1 这不是创建对象五、编译器默认生成的构造函数会做什么5.1 对内置类型成员5.2 对自定义类型成员六、析构函数对象死亡前自动清理资源6.1 析构函数解决了什么问题6.2 析构函数的基本写法七、析构函数不是销毁对象本身7.1 对象空间谁来释放八、有资源申请时一定要自己写析构8.1 Date 不需要Stack 需要九、多个对象的析构顺序9.1 后创建的先析构十、构造和析构带来的代码变化10.1 C 语言版本容易忘记 Init 和 Destroy10.2 C 版本自动调用构造和析构十一、本文总结前言这一章要讲的内容是默认成员函数。所谓默认成员函数就是即使我们不写编译器在某些情况下也会自动生成的成员函数。一个类中比较重要的默认成员函数主要有构造函数析构函数拷贝构造函数赋值运算符重载另外还有取地址运算符重载C11 之后还有移动构造和移动赋值。入门阶段先重点理解前四个。这一篇先讲前两个构造函数对象创建时负责初始化析构函数对象销毁时负责清理资源它们解决的问题很现实对象创建时怎么保证内部数据是合理的对象销毁时怎么保证申请过的资源被释放一、什么是默认成员函数1.1 默认成员函数的基本概念默认成员函数指的是用户没有显式实现时编译器可能会自动生成的成员函数。例如classDate{private:int_year;int_month;int_day;};这个类里我们没有写构造函数、析构函数、拷贝构造和赋值运算符重载。但这并不代表这些函数完全不存在。在需要的时候编译器会尝试自动生成它们。不过这里有一个关键点编译器自动生成不代表一定符合我们的需求。比如Date这种类里面只有三个int编译器默认生成的函数一般够用。但如果是Stack这种类内部有动态申请的空间classStack{private:int*_a;size_t _capacity;size_t _top;};那编译器默认生成的拷贝行为可能就有问题。所以学习默认成员函数要抓住两个问题第一如果我们不写编译器默认生成的行为是什么第二如果默认行为不满足需求我们应该怎么自己写二、构造函数对象出生时自动初始化2.1 构造函数解决了什么问题在学习构造函数之前我们经常会写一个Init函数。比如日期类classDate{public:voidInit(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};使用时Date d;d.Init(2024,4,14);这种写法能用但有一个问题如果用户忘记调用Init()对象内部数据就是不确定的。构造函数就是用来解决这个问题的。构造函数会在对象实例化时自动调用负责完成对象初始化。2.2 构造函数的基本写法构造函数有几个特点函数名和类名相同没有返回值连void也不写对象创建时自动调用构造函数可以重载。例如classDate{public:Date(){_year1;_month1;_day1;}Date(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}voidPrint(){cout_year/_month/_dayendl;}private:int_year;int_month;int_day;};使用时Date d1;Dated2(2024,4,14);其中Date d1;调用无参构造函数。Dated2(2024,4,14);调用带参构造函数。三、默认构造函数是编译器生成的吗3.1 什么叫默认构造函数很多初学者会误以为默认构造函数就是编译器自动生成的构造函数。这个说法不够准确。准确地说不需要传实参就能调用的构造函数都叫默认构造函数。所以以下三种都可以叫默认构造函数第一种无参构造函数。Date(){_year1;_month1;_day1;}第二种全缺省构造函数。Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}第三种用户没有写构造函数时编译器自动生成的构造函数。3.2 默认构造函数只能有一个能被无参调用下面这种写法会有问题classDate{public:Date(){_year1;_month1;_day1;}Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};因为Date d;既可以调用无参构造函数也可以调用全缺省构造函数。编译器会不知道你到底想调用哪个。所以实际写代码时无参构造和全缺省构造一般不要同时写。更推荐这种写法Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}这样既能支持Date d1;也能支持Dated2(2024,4,14);四、一个容易踩的坑4.1 这不是创建对象看下面代码Dated3();很多初学者会以为这是创建了一个对象d3并调用无参构造。但它不是。它会被编译器理解成声明了一个函数 d3这个函数无参返回值类型是 Date。所以创建无参对象时不要写括号。五、编译器默认生成的构造函数会做什么5.1 对内置类型成员如果我们什么构造函数都不写编译器会自动生成一个默认构造函数。但它对内置类型成员比如intchardouble指针通常不会主动初始化成我们想要的值。例如classDate{private:int_year;int_month;int_day;};写Date d;对象确实被创建了。但_year、_month、_day不一定是合理值。所以对于这种类如果你希望日期默认是1900-1-1就应该自己写构造函数。5.2 对自定义类型成员如果类中有自定义类型成员编译器默认生成的构造函数会尝试调用这个成员自己的默认构造函数。例如classStack{public:Stack(intn4){_a(int*)malloc(sizeof(int)*n);_capacityn;_top0;}private:int*_a;size_t _capacity;size_t _top;};classMyQueue{private:Stack pushst;Stack popst;};MyQueue没有写构造函数。但创建MyQueue对象时MyQueue mq;编译器生成的默认构造函数会自动调用pushst和popst的构造函数。这也是 C 对象组合中非常重要的一点外层对象创建时内部成员对象也会被构造。六、析构函数对象死亡前自动清理资源6.1 析构函数解决了什么问题构造函数负责对象初始化。析构函数负责对象销毁前的清理工作。比如Date类classDate{private:int_year;int_month;int_day;};它没有动态申请资源所以通常不需要自己写析构函数。但是Stack类中有动态内存classStack{private:int*_a;size_t _capacity;size_t _top;};如果构造函数里malloc了空间析构函数里就应该free掉。否则就会造成资源泄漏。6.2 析构函数的基本写法析构函数的特点函数名是在类名前面加~没有参数没有返回值连void也不写一个类只能有一个析构函数对象生命周期结束时自动调用。例如classStack{public:Stack(intn4){_a(int*)malloc(sizeof(int)*n);if(_anullptr){perror(malloc fail);return;}_capacityn;_top0;}~Stack(){free(_a);_anullptr;_capacity0;_top0;}private:int*_a;size_t _capacity;size_t _top;};这里~Stack()就是析构函数。当Stack对象生命周期结束时系统会自动调用它。七、析构函数不是销毁对象本身7.1 对象空间谁来释放需要注意析构函数不是负责销毁对象本身而是负责清理对象内部申请的资源。比如局部对象voidFunc(){Stack st;}st是局部对象它的对象空间在栈帧里。函数结束时栈帧销毁对象空间自然释放。析构函数负责的是free(_a);也就是释放对象内部动态申请的资源。所以可以这样理解对象空间由生命周期管理对象内部资源由析构函数清理。八、有资源申请时一定要自己写析构8.1 Date 不需要Stack 需要对于DateclassDate{private:int_year;int_month;int_day;};成员都是内置类型没有动态资源默认析构函数就够了。对于StackclassStack{private:int*_a;size_t _capacity;size_t _top;};_a指向动态申请的空间。如果不写析构函数这块空间不会自动释放。所以需要自己写~Stack(){free(_a);_anullptr;_capacity0;_top0;}可以记住类里如果自己申请了资源通常就要自己释放资源。九、多个对象的析构顺序9.1 后创建的先析构在一个局部作用域中如果有多个对象intmain(){Stack st1;Stack st2;Stack st3;return0;}析构顺序通常是st3 st2 st1也就是后定义的对象先析构。这个规则和栈的特点很像先进后出。十、构造和析构带来的代码变化10.1 C 语言版本容易忘记 Init 和 Destroy以前用 C 写栈时可能是ST st;STInit(st);// 使用栈STDestroy(st);如果中间某个分支提前返回就可能忘记调用STDestroy。比如括号匹配问题中一旦发现不匹配直接返回如果忘记销毁栈就可能造成资源泄漏。10.2 C 版本自动调用构造和析构C 中写成Stack st;对象创建时自动调用构造函数。函数结束或对象离开作用域时自动调用析构函数。这样就不需要我们手动写st.Init();st.Destroy();这就是构造和析构最大的价值让对象的初始化和清理自动发生减少忘记调用的风险。十一、本文总结这一篇主要讲了 C 默认成员函数中的构造函数和析构函数。构造函数对象创建时自动调用主要任务是初始化对象函数名和类名相同没有返回值可以重载不传参能调用的构造函数都叫默认构造函数。析构函数对象生命周期结束时自动调用主要任务是清理对象内部资源函数名是~类名无参数、无返回值一个类只能有一个析构函数有动态资源申请时通常必须自己写析构。构造函数负责对象出生时初始化析构函数负责对象死亡前清理资源。