类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
class null //null是类名 { };
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员
函数。
构造函数
概念
我们可以先通过一个对象的初始化函数引入。
#include <iostream> using namespace std; class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-"; cout << _month << "-"; cout << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date a; a.Init(2023, 10, 20); a.Print(); return 0; }
每一次我们创建出一个对象都要手动为其初始化,如果忘了的话,轻点是随机值,重点程序就崩了,所以我们的构造函数就解决了这个问题。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证
每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
看代码。
#include <iostream> using namespace std; class Date { public: //构造函数支持重载 Date() { _year = 1; _month = 1; _day = 1; } Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-"; cout << _month << "-"; cout << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date a; a.Print(); return 0; }
我们可以看到我们a对象里的年月日确实有值了,被我们所写的的默认构造函数所调用,并全部赋值为1.
接下来我们试试有参数的构造函数。
int main() { Date a(2023,10,21); a.Print(); return 0; }
我们发现编译器调用的是我们有参数的构造函数。
特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任
务并不是开空间创建对象,而是初始化对象。
其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
在上述代码中,也体现出了构造函数的这四个特性。
但是有几点我们要注意
1:没有参数的构造函数,系统默认的构造函数,全缺省的构造函数,都叫做默认构造函数,而这三个构造函数不可同时写出,任意二者不可同时存在,我们看代码解释。
class Date { public: Date() { _year = 1; _month = 1; _day = 1; } Date(int year = 10, int month = 10, int day = 10) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date a; return 0; }
因为编译器无法区分到要调用哪个构造函数,所以就报错了。
2:当我们定义了构造函数后,编译器不会再生成默认构造函数,但这样也会出现一个小问题需要我们去掌控。
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date a; return 0; }
因为我们自定义了一个构造函数,所以系统不会再生成默认构造函数,而我们实例化的对象要调用无参的构造函数,但是我们又没有无参的构造函数,所以编译器就只能报错,我们后面会讲到有解决方法。
3.如果说我们没有写构造函数,内置类型的成员变量使用系统的默认构造函数,不会做处理,自定义类型的成员变量会去调用他自己的默认构造函数。
解释:内置类型就是系统自带的,比如int,double之类的,像类和结构体等就是自定义类型。
class Stack { public: Stack(int capacity = 4) { _capacity = capacity; int top = 0; int* a = (int*)malloc(sizeof(int) * capacity); } ~Stack() { free(_a); _top = 0; _capacity = 0; } private : int* _a; int _top; int _capacity; }; class Queue { Stack _a; Stack _b; int size; }; int main() { Stack a; Queue b; return 0; }
细心的朋友们可能会发现size被初始化为0了,你不是说内置类型的变量不初始化吗?是的,但是不同的编译器处理结果不同,我这个是Visual Studio 2022 ,在2013下是不会给初始化的,我们想要我们写出的代码具有跨平台性,就不要寄希望于编译器会给优化,所以我们就当做他不会给优化处理,写出优质代码。
4:C++11新特性,允许声明成员变量时给默认值。
class Date { public: void Print() { cout << _year << _month << _day; } private: int _year = 1; int _month = 1; int _day = 1; }; int main() { Date a; a.Print(); return 0; }
析构函数
概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
像我们上面有代码~Stack就是析构函数。
特性
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
我们来测试一下。
class Stack { public: Stack(int capacity = 4) { _capacity = capacity; int top = 0; int* a = (int*)malloc(sizeof(int) * capacity); } ~Stack() { free(_a); _top = 0; _capacity = 0; cout << "haha" << endl; } private : int* _a; int _top; int _capacity; }; class Queue { Stack _a; Stack _b; int size; }; int main() { Queue b; return 0; }
结果显而易见,我们的确是调用了析构函数。
拷贝构造函数
概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存
在的类类型对象创建新对象时由编译器自动调用。
特性
我们先来写一段代码证明拷贝构造函数的必要性
首先是日期类的测试
class Date { public: Date(int year = 2023, int month = 10, int day = 22) { _year = year; _month = month; _day = day; cout << "Date的构造" << endl; } private: int _year; int _month; int _day; }; void func1(Date a) { cout << "func1(Date a)" << endl; } int main() { Date d1(2023, 10, 22); func1(d1); return 0; }
注意日期类函数调用的析构函数是系统默认的,而且也没什么需要释放的。
所以在我们将d1这个对象传给func1这个函数的时候时值传递,当fun1结束时,a这个对象要销毁,会去调用析构函数。
接下来我们来看栈这个类
class Stack { public: Stack(int capacity) { _capacity = capacity; int top = 0; _a = (int*)malloc(sizeof(int) * _capacity); if (_a == nullptr) { perror("malloc"); } cout << "Stack的构造" << endl; } ~Stack() { free(_a); _a = nullptr; _top = 0; _capacity = 0; cout << "Stack的析构" << endl; } private: int* _a; int _top; int _capacity; }; void func2(Stack st) { //... } int main() { Stack st1(4); func2(st1); return 0; }
这个栈类再这么调用就会出问题,当st销毁时,去调用我们所写的析构函数,在func2函数结束时会释放一次_a,当主函数中的对象st1销毁时,会对已经释放的那块空间再释放一次,这样程序就崩溃了,因为此时的_a就是野指针了,别忘了我们是传值,空间释放后_a就成了野指针,释放野指针指向的空间是不合法的,所以就崩了。
那也许有人会说,我传引用不就好了吗,但是假设我们有一个需求,不改变对象本身,就是要拷贝一份去实现,那么拷贝构造就凸显出作用来了。(如果还有人说栈这个类我用系统默认的析构函数不好吗?那您可真是昏了头了)
lesson-2C++类与对象(中)(二)+https://developer.aliyun.com/article/1393892