目录
一、前言
大家好,我们上期简单讲了一下关于类的定义,以及this指针的一些相关内容,今天让我们来了解一下类与对象中的重难点------类的默认成员函数。废话不多说,让我们进入今天的知识分享吧。
二、正文
1.类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解一下即可。其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个我们后面再讲解。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:
- 第一:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
- 第二:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?
2.构造函数
构造函数是特殊的成员函数, 需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init。
构造函数的特点:
- 函数名与类名相同。
- 无返回值。(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
- 对象实例化时系统会自动调用对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数、都叫做默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结一下就是不传实参就可以调用的构造就叫默认构造。
- 我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表,我们下个章节再细细讲解。
说明:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型,如:int/char/double/指针等,自定义类型就是我们使用class/struct等关键字自己定义的类型。
2.1构造函数的使用
cpp#include<iostream> using namespace std; class Date { public: //1、无参构造函数 Date() { _year = 2024; _month = 11; _day = 4; } // 2、传参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } //3、全缺省构造函数 Date(int year=2024,int month=11,int day=4) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1;//构造函数其实就相当于起了一个初始化的作用,上面我们写了三个构造函数。 //但是值得注意的是,三个构造函数不能同时存在. //当不给d1传参的时候1和3不能存在的原因是编译器也不清楚到底是调用1还是3。 Date d2(1, 2, 3);//当我们给函数传参的时候,2和3也不能共存,也会爆出类似的错误。 return 0; }
cpp#include<iostream> using namespace std; class Date { public: //当我们不自己写构造函数的时候,编译器会自动构造函数,初始化我们的实例对象d1。 void Print() { cout << "11" << endl; } 2、传参构造函数 // Date(int year, int month, int day) // { // _year = year; // _month = month; // _day = day; // } // //3、全缺省构造函数 // Date(int year=2024,int month=11,int day=4) // { // _year = year; // _month = month; // _day = day; // } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print(); return 0; }
下面的MyQueue的构造函数自动会调用Stack的构造,完成两个成员的初始化。
两次初始化其实是pushst和popst分别调用了依次Stack的构造函数。
cpp#include<iostream> using namespace std; typedef int STDataType; class Stack { public: Stack(int n = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr == _a) { perror("malloc fail!"); return; } _capacity = n; _top = 0; } private: STDataType* _a; int _capacity; int _top; }; class MyQueue { Stack pushst; Stack Popst; }; int main() { MyQueue mq; return 0; }
值得注意的一点就是:我们不写,编译器默认生成的构造函数对内置类型成员的变量是没有要求的,这取决于编译器。
让我们看看VS2022的默认构造函数是否会初始化内置类型呢?
很明显,当我们没写构造函数时,内置类型成员给的值是一串随机数字,并没有完成初始化。
那么对于自定义类型不写构造函数会出现什么呢?
那为什么上面自定义类型成员出现随机值,我们说是没有完成初始化,这里我们说是编译器自动生成默认无参构造函数呢,因为这条定义:对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。
因此,自我感觉构造函数能自己写就自己写,不要指望编译器给我们初始化。写了不会出错,不写可能会导致一系列的问题。
3.析构函数
析构函效与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
析构函数的特点:
- 析构函数名是在类名前加上字符~。
- 无参数无返回值。(这里跟构造类似,也不需要加void)
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,系统会自动调用析构函数。
- 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。
- 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构函数可以用,也就不需要显示写析构,如MyQueue;但是有资源申请时,一定要自己写析构,否则会造成资源泄露,如Stack。
- 一个局部域的多个对象,C++规定后定义的先析构。(这里可以联想栈的存储结构,你就明白了)
3.1析构函数的使用
cpp#include<iostream> using namespace std; typedef int STDataType; class Stack { public: Stack(int n = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr == _a) { perror("malloc fail!"); return; } _capacity = n; _top = 0; } ~Stack()//对于需要有内存释放的的实例化对象 //一定要手动写析构函数释放这些资源,除了MyQueue // 下一张图我们会说明。而像Date这种不需要资源释放的类 // 其实,没大必要手写析构函数 { cout << "~Stack()" << endl; free(_a); _a = nullptr; _top = _capacity = 0; } private: STDataType* _a; int _capacity; int _top; }; class MyQueue { Stack pushst; Stack Popst; }; int main() { Stack s1; return 0; }
上面图中我们提到,需要内存释放的实例化对象s1,需要手写析构函数释放其中的资源,但是MyQueue则是个特例,他不需要再手写一个~MyQueue()的析构函数,而是直接调用它的自定义成员pushst和pppst的析构函数~Stack()就行了。
三、结语
今天的分享就到此结束了小伙伴们咱们下次再见,拜拜 ~