前言:
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
一、构造函数
1、概念
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名叫做构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
2、特征
- 函数名与类名相同。
- 无返回值
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
- 如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成。
下面代码构造函数与缺省参数结合,非常实用!
#include<iostream>
using namespace std;
class date
{
private:
int _year;
int _month;
int _day;
public:
//函数名与类名相同。无返回值
date(int year = 2023, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << '-' << _month << '-' << _day;
}
};
int main()
{
date a;
date _a(1, 2, 3);
a.Print();
return 0;
}
但此时如果把自己定义的构造函数删除,默认生成构造函数,但是不会自己初始化。
3、默认生成的构造函数,到底有什么用?
处理自定义(回去调用这个成员的默认构造函数),但对于内置类型不确定(看编译器),建议不处理。
比如上面是自定义类型,就会自己调用Stack的默认构造函数
默认构造函数的概念:
- 我们不写显示构造函数,编译器默认生成的构造函数,叫默认构造
- 无参构造函数也可以叫默认构造
- 全缺省也可以叫默认构造
小总结:
内置类型成员不做处理,自定义类型会去调用它的默认构造。
所以对于内置类型,还是需要程序员自己去创建构造函数,而对于自定义类型,会自动调用这个成员的默认构造函数,其实还是自己创建的构造函数
- 内置类型:int/double/……注意指针都是内置类型
- 自定义类型:class/struct
其实上面的构造函数并不好,对于自定义类型和构造类型区别对待,在C++11中,会支持对内置类型的初始化,在private声明的时候进行初始化。支持声明时给缺省值
总结:
- 一般情况下,我们都要自己写构造函数
- 内置类型都不处理
- 成员都是自定义类型,或者声明时给了缺省值,可以考虑让编译器自己生成构造函数
- 构造函数的参数声明给,定义不给(当声明和定义分离时)
可以不传参数就调用构造,都可以叫默认构造,这三个函数不能同时存在,只能存在一个
再谈构造函数(初始化列表)
先记一下格式,冒号开始,逗号分割。
在创建对象时,编译器通过调用构造函数,给对象中的各个成员变量一个合适的初始值。
虽然上述构造函数调用后,随想中已经有一个初始值,但是不能将其称为对对象中成员变量的初始化。
构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
为什么引用和const变量不能在函数体内初始化?
我们首先要明确上面的代码是声明,没有开空间
这里类的实例化才是定义,就比如上面是房子的图纸,而下面就是对象每一栋房子,所以开了空间,这里是对象的整体定义!
那问题来了,每个成员变量在哪里定义呢?
答案:每个成员在初始化列表定义!
而引用和const类型的变量必须在定义的时候进行初始化,所以祖师爷特地使用了初始化列表来表示成员变量在初始化列表进行定义,这样就解决了上面两种特殊变量必须在定义的时候进行初始化。
注意初始化列表可以和函数体内初始化混合使用,如果有成员变量没有在初始化列表显示定义,他们也会定义。如果是内置类型会去默认给的随机值;如果是自定义类型,会去调用他的默认构造函数。
初始化列表解决的问题:
- 必须在定义的地方显示初始化:引用、const、没有默认构造自定义成员(引用和const都是只能定义一次,必须在定义的时候初始化)
- 有些自定义成员想要显示初始化,或者该自定义成员没有默认构造,都可以使用初始化列表进行初始化
尽量使用初始化列表初始化。
构造函数能不能只要初始化列表,不要函数体初始化?
答案是不能,因为有些初始化或者检查的工作,初始化列表也不能全部搞定。所以需要混合使用。
一道具有迷惑性的题目:
最终输出的结果是1 和 随机值。
为什么不是两个1呢?因为初始化列表定义的顺序跟成员变量声明的顺序一致。在private中_a2是先声明,所以在初始化列表中先给_a2进行定义,因此就是随机值!!!
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的顺序无关~
二、析构函数:
1、概念
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
2、特性
- 析构函数名是在类名前加上字符~
- 无参数无返回值类型
- 一个类只能有一个析构函数。若未显示定义,系统会自动生成默认的析构函数。注意析构函数不能重载
- 对象生命周期结束时,C++编译系统自动调用析构函数。
3、功能
对象销毁不需要析构函数,对象的销毁靠系统,更本质一点是函数栈帧的销毁,而析构函数的作用是处理动态开辟的空间,比如栈开辟的动态的空间。
我们如果不写析构函数,那系统自动默认生成的析构函数,不会把开辟的指针处理
默认生成析构函数,行为跟构造类似,内置类型成员不做处理,自定义类型成员会去调用他的析构
三、拷贝构造函数
概念:
我们在创建对象时,创建一个与已经存在对象一模一样的新对象。
那我们为什么要创建一个与已经存在的对象一模一样的新对象呢?
问题:
举个例子
下面的程序会报错。
我们已经在上一章学习过了析构函数,析构函数的作用是处理动态开辟的空间,比如栈开辟的动态的空间。下图是栈的析构函数。
我们来分析一下,因为上面的函数是传值传参,而形参是实参的一份临时拷贝,所以本来st1中含有的_a空间,而st中也复制拷贝了一份,st中同样的_a也指向了相同动态开辟数组a的空间,而析构函数会自动清理动态开辟的空间,所以在fun2函数调用后会将动态开辟的a的空间释放,将其变成空指针,而在主函数调用后,也会调用析构函数,所以就会造成空间的二次释放!
所以值拷贝/浅拷贝对栈这些类是有风险的,那我们如何解决这一问题呢?
规定,自定义类型对象拷贝的时候,调用一个函数,这个函数就叫做拷贝构造。
什么时候需要调用拷贝构造函数呢?
场景一:初始化构造的时候
场景二:参数传参的时候
拷贝构造函数:本质上是构造函数的重载,只有单个形参,该形参是对本类对象的引用(一般常用const修饰,防止代码写反,将原本的值变成随即值)
为什么一定是引用?
C++规定,自定义类型对象传参拷贝,必须调用拷贝构造,此时先传参,但是传参又会调用新的拷贝构造,会引发无穷递归调用!
调用拷贝构造->先传参->传值传参->形成新的拷贝构造->……引发无穷递归调那如何解决呢?
我们可以传引用去解决!
下面是日期的拷贝构造函数,是浅拷贝。
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
对于日期这样的类,编译器自动生成的默认拷贝构造函数(浅拷贝/值拷贝)就可以解决问题。
但是我们祖师爷创建拷贝构造函数的目的就是针对栈、队列等自定义类型中需要我们自己创建的空间被析构两次的问题。
调用顺序就是如果传值传参,会调用拷贝构造,然后再调用func函数
解决方法:
下面是栈的拷贝构造函数(深拷贝)
深拷贝:本质拷贝指向的资源,让我跟你有一样的资源,一样的值!
所以会创建一个相同资源的空间,分别析构,就不会造成统一空间被析构两次的问题了。
总结:
- 内置类型成员完成值拷贝(Data)
- 自定义类型成员调用这个成员的拷贝构造(MyQueue)
- Stack需要自己写拷贝构造,完成深拷贝
- 顺序表、链表、二叉树等自己创建空间的类,都需要深拷贝
成员函数定义的原则
- 能定义成const的成员函数都应该定义成const,这样const对象(权限平移)和非const对象(权限缩小)都可以调用
- 要修改成员变量的成员函数,不能定义const