1、类的六个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
2、构造函数
2.1 构造函数的概念
我们这里来看看日期类的初始化:
class Date { public: void Init(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; d1.Init(2022, 7, 5); d1.Print(); return 0; }
运行结果:
我们刚接触C++,一定会这样初始化。
如果我们实例化的对象太多了,忘记初始化对象了,程序运行出来的结果可能就是随机值了,也可能出问题。
这里C++祖师爷想到了,为我们设计了构造函数。
我们先来看一下忘记初始化直接打印的结果:
这里是随机值,那这是为什么呢?我们接着往下看。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1. 函数名与类名相同。
2. 无返回值(不是void,是不用写)。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
我们先写一个日期类的构造函数来看看:
class Date { public: Date()//构造函数,无参构造 { cout << "Date()" << endl; _year = 1; _month = 1; _day = 1; } void Print() { cout << _year << "/" << _month << "/" << _day; } private: int _year; int _month; int _day; };
我们测试看一下:
我们main函数里没有调用构造函数,但是这里打印了我们做的标记,这里我们实验出来了实例化对象时构造函数是自动调用的。
我们再来看将我们写的构造函数注释掉会发生什么:
我们能看到,注释掉后,仍然能打印出来,只不过是随机值。因为当我们不写,编译器会自动生成默认的构造函数,并自动调用。
C++将类型分为内置类型(基本类型):如int,char,double,int*……(自定义类型*也是);
自定义类型:如class,struct,union……。
并且这里我们能看出来,对于内置类型的成员不会处理,在C++11,支持成员变量给缺省值,算是补漏洞了。
2.2.1 构造函数的重载:
class Date { public: Date() { cout << "Date()" << endl; _year = 1; _month = 1; _day = 1; } 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; d1.Print(); Date d2(2023, 8, 1);//这里初始化必须是这样写,这是语法 d2.Print(); return 0; }
运行结果:
注意:我们在实例化对象的时候,当调用的构造函数没有参数,不能在对象后加括号,语法规定。
如果这样写,编译器分不清这到底是函数声明还是在调用。d2不会混淆是因为有传值,函数声明不会出现那样的写法。
2.2.2 全缺省的构造函数:
我们其实可以将上面的两个构造函数合并为一个
class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print(); Date d2(2023, 8, 1); d2.Print(); Date d3(2023, 9); d3.Print(); return 0; }
运行结果:
全缺省构造函数才是最适用的。无参构造与全缺省可以同时存在,但是不建议这样写,虽然不报错,但是在调用全缺省时我们不想传参,编译器不知道我们到底想调用哪个构造,会产生二义性。
我们再看用两个栈实现队列的问题:
class Stack { public: Stack(int n = 4) { if (n == 0) { _a = nullptr; _size = -1; _capacity = 0; } else { int* tmp = (int*)realloc(_a, sizeof(int) * n); if (tmp == nullptr) { perror("realloc fail"); exit(-1); } _a = tmp; _size = -1; _capacity = n; } } void Push(int n) { if (_size + 1 == _capacity) { int newcapacity = _capacity == 0 ? 4 : 2 * _capacity; int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity); if (nullptr == tmp) { perror("realloc fail:"); exit(-1); } _a = tmp; _capacity = newcapacity; } _a[_size++] = n; } int Top() { return _a[_size]; } void Pop() { assert(_size > -1); _size--; } void Destort() { free(_a); _a = nullptr; _size = _capacity = 0; } bool Empty() { return _size == -1; } private: int* _a; int _size; int _capacity; }; class MyQueue { private: Stack _pushst; Stack _popst; };
一般情况下都需要我们自己写构造函数,决定初始化方式,成员变量全是自定义类型,可以考虑不写构造函数。会调用自定义类型的默认构造函数。
总结:无参构造函数、全缺省构造函数、我们不写编译器默认生成的构造函数,都可以认为是默认构造函数,并且默认构造函数只能存在一个(多个并存会产生二义性)。
3、析构函数
3.1 析构函数的概念
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2 特性
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数(对内置类型不做处理,自定义类型会调用它自己的析构函数)。注意:析构函数不能重载。
4. 对象生命周期结束时,C++编译系统会自动调用析构函数。
我们先来看日期类的析构:
class Date { public: Date(int year = 1, int month = 1, int day = 1) { cout << "Date(int year = 1, int month = 1, int day = 1)" << endl; _year = year; _month = month; _day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } ~Date() { cout << "~Date()" << endl; _year = 0; _month = 0; _day = 0; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2; return 0; }
运行结果:
我们这里可以看出析构函数也是自动调用的。
我们不写,编译器自动生成默认的析构函数。
析构函数的调用顺序跟栈类似,后实例化的先析构。