一、类的六个默认成员函数
默认成员函数是指用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。
对于空类,并不是什么都没有,编译器会自动默认生成以下六个默认成员函数
二、构造函数
2.1 构造函数概念
构造函数是特殊的成员函数,其中函数名与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次
构造函数目的:默认构造函数是为了解决创建对象,忘记对其对象进行初始化操作,同时解决麻烦地调用Init
函数。
造函数虽然名称叫构造,但是目的不是开辟空间创建对象,而是对象初始化
构造函数特性:
- 函数名与类名相同
- 无返回值
- 对象实例化时,编译器自动调用对应的构造函数
- 构造函数支持函数重载
2.2 构造函数分类
无参构造函数与全缺省构造函数、忘记显示写构造函数,编译器默认生成构造函数都称为默认构造函数,在使用过程中默认构造函数只能调用其中一种,这里推荐调用全缺省构造函数
class Date { public: //1.无参构造函数 Date() { _year = 2024; _day = 6; } //2.带参构造函数 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;//调用无参构造函数 Date d2(2024, 3, 6);//调用有参构造函数 d1.Print(); d2.Print(); Date d3();//未调用原型函数(是否有意用变量定义?) return 0; }
关于Date d3(void)
报错,由于编译器很难区分对象实例化是调用无参构造函数还是函数声明。为了避免混洗这两种情况,要求对象实例化调用无参构造函数,不允许添加括号
对于无参构造与有参构造,无参构造需要函数内部设置好的数值,而有参构造采用外部实参数值。对于这里两种情况可以考虑合并为全缺省的构造函数。虽然编译器支持全缺省构造函数与无参构造函数同时出现,语法上允许这种行为,但是调用构成中会存在歧义,编译器无法区分(有多种初始化方式,在条件允许实现一个全缺省最好用,比较灵活控制参数)
2.3 构造函数对于内置/自定义类型处理方式
C/C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型(int/char/double )
,自定义类型就是自己通过关键字定义的类型(struct /class/union)
对于内置与自定义类型处理:
- 对内置类型不做处理
- 对自定义类型的成员,会去调用他们的默认构造(无参构造函数、全缺省构造函数、我们没有写编译器默认生成的构成函数)
2.4 编译器默认生成构造函数意义及相关问题
提出疑问:从编译结果来看,无论是显示构造函数或编译器默认生成构造函数,对于内置类型初始化处理为随机值。虽然完成每个对象初始化,但是这些初始化的数值对于我们来说并没有多大意义,是否可以认为编译器默认生成构造函数没有意义呢?同时是否可以认为既然默认生成构造函数,我们什么事情都不用做了呢?
给出回答:我们从对于内置与自定义类型处理上来看,编译器虽然对于内置类型初始化数值为随机值,但是确保了内置类型完成了初始化操作,避免了缺乏构造函数而导致的编译错误。同时我们需要知道无论是内置类型或者是自定义类型,数据都是需要我们自己处理,只不过是间接和直接而已(套娃:所谓的自定义类型不过是包含内置类型,其中可能还有自定义类型,但是自定义类型最后一定是内置类型,是内置类型都需要人去设置处理)
对于编译器默认生成构造函数还有很有价值的,比如在MyQueue
里面定义 stack s1
和stack s2
,这里会调用默认构造,完成对象s1
、s2
的初始化(虽然内部还是需要手动设置,但是调用MyQueue
就会很爽)
2.5 不对内置类型处理
不对内置类型做处理是语言设计过程中遗留下来问题,在C++11中对于内置类型是否处理有了争执,当然内置类型不处理也可能有它的原因,对此C++11还是保持对内置类型不处理的态度,但是打了补丁,即是:内置类型成员变量在类中声明事可以给缺省值
三、析构函数
3.1 析构函数概念
析构函数与构造函数功能相反,该函数任务并不是完成对象本身销毁(局部对象的销毁时由编译器完成),而是对象在销毁时自动调用析构函数,完成对象中资源的清理工作
这里资源一般指动态开辟的资源,如果没有析构函数进行处理,而是单纯地开辟和销毁对象。没有考虑对象内部申请的动态空间,导致内存泄漏(对象是存储在栈帧上,是由系统进行处理的,也称为自动变量)
从图中也可以观察到动态开辟的资源没有释放掉
析构函数特性:
- 析构函数名为同类名前加上字符~
- 无参数无返回值类型,导致析构函数不支持重载函数
- 一个类只能有一个析构函数。若未显式定义,系统会在自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统自动调用析构函数
3.2 验证是否会自动调用析构函数
析构函数对于内置与自定义类型的处理方式(调用析构函数中this指针存储对象的地址)
对于内置与自定义类型处理:
- 内置类型不处理
- 自定义类型成员,调用对应的析构函数
3.3 析构函数处理顺序
关于析构函数顺序涉及到函数栈帧,不知道你们是否注意到上面打印顺序跟栈特性是相关的。那么可以得出两点【先定义、先构造】【后定义、先析构】
class Date { public: Date(int year=1) { _year = year; } ~Date() { cout << "~Date()->" <<_year<< endl; } private: int _year; int _month; int _day; }; Date d5(5);//全局对象 static Date d6(6);//全局对象 void func() { Date d3(3);//局部变量 static Date d4(4);//局部的静态 } int main() { Date d1(1);//局部变量 Date d2(2);//局部变量 func(); return 0; }
从中得到结论:
1.局部对象(后定义先析构)
2.局部的静态
3.全局对象(后定义先析构)
析构函数清理细节:
class Time { public: ~Time() { cout << "~Time()" << endl; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year = 1970; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d; return 0; }
提问:默认析构函数对内置类型不处理,我就想让析构函数对内置类型进行处理,怎么办?
- 对于这个问题,我们可以采用显式析构函数,里面的逻辑是自己设计的,可以要求对内置类型进行操作,但是这样子没有价值。
- 内置类型不需要进行资源清除,同时将内置类型全部设置为0,同样没有完成清除的任务,对此在程序结束后,系统会自动回收内置类型的空间,不需要我们多此一举
3.4 调用类中类的析构函数细节
d
对象的销毁时,要将其内部包含的Time类的_t对象销毁,但是这里不是直接调用Time类的析构函数。因为实际要释放的是Date类对象,对此调用Date类对象对应的析构函数(编译器默认生成的析构函数),目的是在其内部调用Time。(没有直接调用Time类析构函数,通过Date类中析构函数间接调用)
小结:
- 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收
- 创建哪个类的对象,则调用该类的析构函数,销毁那个类的对象,则调用该类的析构函数
- 关于析构函数是否显示写,主要是看是否存在资源申请,并不是每个类都需要析构。
- 析构函数可以显示调用,但是可能会用引发不安全行为,需要小心调用
四、拷贝构造函数
4.1 拷贝构造函数概念
拷贝构造函数指只存在单个形参,该形参是本类类型对象的引用(一般常用const
修饰),用已存在的类类型对象创建新对象(该过程编译器自动调用)
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // Date(const Date& d) // 正确写法 Date(const Date d) // 错误写法:编译报错,会引发无穷递归 { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(d1); return 0; }
拷贝构造函数特性:
- 拷贝构造函数本身属于构造函数一种重载,同类型对象进行初始化
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用(编译器可能会强制检查)
4.2 关于对拷贝构造疑问
1.拷贝构造函数为什么只有一个参数?
拷贝构造函数需要拷贝对象参数即可,由于存在this指针,将调用对象地址传进来(编译器会自动处理)
2.为什么传值会引发无穷递归调用呢?是否可以提前写个返回条件进行拦截呢?可以使用指针类型进行接收吗?
通过函数栈帧中学习,传值过程需要开辟空间去拷贝实参数据,这里就需要调用拷贝函数。导致了传值需要调用拷贝构造,调用拷贝构造需要传值
的套娃当中。
对于返回条件拦截,实际上这里压根没有进去函数体,返回条件都用不上。指针是可以,但是指针不适合这里。使用引用给实参取别名,指向对象共占用一块内存空间,就不需要拷贝数据去调用拷贝函数,减少拷贝次数
3.使用const
修饰引用
- 使用
const
修饰的引用意味着我们不会修改传入的对象。保证被拷贝对象不会被修改,可以及时地报错检查是否位置放反。 - 如果拷贝构造传的是
const
修饰的变量,并且拷贝构造函数 参数部分没使用const
修饰,就会造成权限放大
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(二)https://developer.aliyun.com/article/1617296