0. 引入6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,
编译器会自动生成以下 6 个默认成员函数。
C++类中有6个默认函数,分别是:
构造函数、 析构函数、 拷贝构造函数、 赋值运算符重载、 取地址及 const取地址运算符重载。
这六个函数是很特殊的函数,如果我们不自己实现,编译器就会自己实现。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
对于默认成员函数,如果我们不主动实现,编译器会自己生成一份。
比如我们在上一篇里举过的一个 Stack 的例子,如果需要初始化和清理,"构造函数" 和 "析构函数" 就可以帮助我们完成。构造函数就类似于 Init,而析构函数就类似于 Destroy。
1. 构造函数(默认成员函数)
对于以下 Date 类
#include <iostream> using namespace std; class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { printf("%d-%d-%d\n", _year, _month, _day); } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Init(2023, 4, 23); d1.Print(); Date d2; d2.Init(2022, 5, 2); d2.Print(); return 0; }
对于Date 类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,而且写其它类的时候可能会忘记初始化,会出现程序崩溃的情况, 那能否在对象创建时,就将信息设置进去呢?
1.1 构造函数的概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,
以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数的意义:能够保证对象被初始化。
构造函数是特殊的成员函数,主要任务是初始化对象,而不是开空间。
(虽然构造函数的名字叫构造)
1.2 构造函数的特性和用法
构造函数是特殊的成员函数,主要特征如下:
① 构造函数的函数名和类名是相同的
② 构造函数无返回值(也不用写void)
③ 构造函数可以重载
④ 会在对象实例化时自动调用对象定义出来。
构造函数的用法:
#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() { printf("%d-%d-%d\n", _year, _month, _day); } private: int _year; int _month; int _day; }; int main() { Date d1; // 对象实例化,此时触发构造,调用无参构造函数 d1.Print(); Date d2(2023, 5, 2); // 对象实例化,此时触发构造,调用带参构造函数 // 这里如果调用带参构造函数,我们需要传递三个参数(这里我们没设缺省) 。 //如果想传几个就传几个可以自己设置重载 d2.Print(); // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象 Date d3(); // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?) //构造函数是特殊的,不是常规的成员函数,不能直接调d1.Data(); return 0; }
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,
一旦用户显式定义编译器将不再生成。
对于上面的d1中,如果只有带参构造函数就会报错,对于d2,如果只有无参构造函数就会报错,所以把自己写的构造函数都删除之后d1可以运行,d2会报错。
1.3 默认构造函数
class Date { public: //无参构造函数 是 默认构造函数 Date() { _year = 1; _month = 1; _day = 1; } //全缺省构造函数 也是 默认构造函数 (一般写全缺省,不写上面那个) Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
无参构造函数、全缺省构造函数都被称为默认构造函数。并且默认构造函数只能有一个。
注意事项:
① 无参构造函数、全缺省构造函数、我们没写编译器默认生成的无参构造函数,这三个都可以认为是默认构造函数。
② 语法上无参和全缺省可以同时存在,但如果同时存在会引发二义性:无参的构造函数和全缺省的构造函数都成为默认构造函数,并且默认构造参数只能有一个,语法上他们两个可以同时存在,但是如果有对象定义去调用就会报错。
关于编译器生成的默认成员函数,很多人会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d 对象调用了编译器生成的默认构造函数,但是d 对象 _year/_month/_day ,依旧是随机值。也就说在这里编译器生成的 默认构造函数并没有什么用?? 解答:
C++把类型分成 内置类型(基本类型)和自定义类型 。
内置类型就是语言提供的数据类型,如:int/char/指针等等,
自定义类型就是我们使用class/struct等自己定义的类型,
C++ 规定:我们不写编译器默认生成构造函数,对于内置类型的成员变量,不做初始化处理。
但是对于自定义类型的成员变量会去调用它的默认构造函数(不用参数就可以调的)初始化。
如果没有默认构造函数(不用参数就可以调用的构造函数)就会报错。
#include <iostream> using namespace std; class Time { public: Time() { cout << "Time()" << endl; _hour = 0; _minute = 0; _second = 0; } private: int _hour; int _minute; int _second; }; class Date { public: void Print() { printf("%d %d %d\n", _year, _month, _day); } private: int _year;// 基本类型(内置类型) int _month; int _day; Time _t;// 自定义类型 }; int main() { Date d; d.Print(); return 0; }
很多人吐槽不写构造函数编译器会默认生成的这个特性设计得不好,因为没有对内置类型和自定义类型统一处理,不处理内置类型成员变量,只处理自定义类型成员变量。
但是覆水难收,所以C++11 中针对内置类型成员不初始化的缺陷,又打了补丁:内置类型成员变量在类中声明时可以给默认值:
#include <iostream> using namespace std; class Time { public: Time() { cout << "Time()" << endl; _hour = 0; _minute = 0; _second = 0; } private: int _hour; int _minute; int _second; }; class Date { public: void Print() { printf("%d %d %d\n", _year, _month, _day); } private: int _year = 1;// 基本类型(内置类型) int _month = 1; int _day = 1; //注意这里不是初始化,是给默认构造函数缺省值 Time _t;// 自定义类型 }; int main() { Date d; d.Print(); return 0; }
需要注意的是,上面代码中如果自定义类型Time没有写构造函数,编译器也什么都不会处理。
总结:
构造函数分为三类:
①无参构造函数、
②全缺省构造函数、
③我们没写编译器默认生成的构造函数,
这三类都可以认为是默认构造函数。并且默认构造函数只能有一个。 一般的类都不会让编译器默认生成构造函数,一般显示地写一个全缺省,非常好用, 特殊情况才会默认生成。
2. 析构函数(默认成员函数)
2.1 析构函数概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会 自动调用 析构函数,完成对象中资源的清理工作。
以前我们写数据结构的时候经常忘记调用 destroy 函数,但是现在我们有析构函数了。
2.2 析构函数特性
构造函数是特殊的成员函数,主要特征如下:
① 析构函数名是在类名前面加上字符
② 析构函数既没有参数也没有返回值(因为没有参数,所以也不会构成重载问题)
③ 一个类的析构函数有且仅有一个(如果不写系统会默认生成一个析构函数)
④ 析构函数在对象生命周期结束后,会自动调用。(和析构函数是对应的构造函数是在对象实例化时自动调用)
#include <iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void Print() { printf("%d-%d-%d\n", _year, _month, _day); } ~Date() { cout << "~Date()" << endl;// 日期类没有资源需要清理,所以只打印下知道调用了 } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(2023, 5, 2); return 0; }
d1 和 d2 都会调用析构函数:
拿 Stack 来举个例子,体会下构造函数和析构函数的用处,我们知道,栈是需要 destroy 清理开辟的内存空间的。
#include<iostream> #include<stdlib.h> using namespace std; typedef int StackDataType; class Stack { public: Stack(int capacity = 4) // 这里只需要一个capacity就够了,默认给4(利用缺省参数) { _array = (StackDataType*)malloc(sizeof(StackDataType) * capacity); if (_array == NULL) { cout << "Malloc Failed!" << endl; exit(-1); } _top = 0; _capacity = capacity; } ~Stack() // 这里就用的上析构函数了,我们需要清理开辟的内存空间(防止内存泄漏) { free(_array); _array = nullptr;//下面这两行可以不写,这个野指针已经没人能访问到了 _top = _capacity = 0;//但写了也是个好习惯 } private: int* _array; size_t _top; size_t _capacity; }; int main(void) { Stack s1; Stack s2(20); //初始capacity给20 return 0; }
代码解读:我们在设置栈的构造函数时,定义容量 capacity 时利用缺省参数默认给个4的容量,这样用的时候默认就是4,如果不想要4可以自己传。如此一来,就可以保证了栈被定义出来就一定被初始化,用完后会自动销毁。以后就不会有忘记调用 destroy 而导致内存泄露的惨案了,这里的析构函数就可以充当销毁的作用。
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
有没有想过,这里是先析构 s1 还是先析构 s2?既然都这样问了,应该是先析构 s2 了 ,没错没错,栈帧和栈里面的对象都符合栈的性质,析构的顺序在局部的栈中是相反的,栈帧销毁清理资源时 s2 先清理,然后再清理 s1 。(可以在析构函数打印参数看看)(贴两个图)
这张图3也是全局的:
如果我们不自己写析构函数,让编译器自动生成,那么这个默认析构函数和默认构造函数类似: ① 对于 "内置类型" 的成员变量:不作处理,② 对于 "自定义类型" 的成员变量:会调用它对应的析构函数。
可能有人要说帮我都销毁掉不就好了?举个最简单的例子,迭代器,析构的时候是不释放的,因为不需要析构函数来管,所以默认不对内置类型处理是正常的,这么一来默认生成的析构函数不就没有用了吗?
有用,他对内置类型的成员类型不作处理,会在一些情况下非常的有用。
从C语言到C++⑤(第二章_类和对象_中篇)(6个默认成员函数+运算符重载+const成员)(中):https://developer.aliyun.com/article/1513647?spm=a2c6h.13148508.setting.14.5e0d4f0eApSShM