前言: 上篇C++博客我们了解了类和对象的一些基础知识,如:类的定义、访问限定符、类实例化及其大小、this指针等,相信经过上篇博客的学习之后对于类和对象有了相应的了解,这篇博客我们开始了解学习类的6个默认成员函数,这篇博客我们先了解构造函数和析构函数,希望大家有所收获!
如果一个类中什么成员都没有,简称为空类。 空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。 默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
先来简单认识下六个默认成员函数都有哪些:
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
事实上,构造函数所要解决的问题就是初始化的问题,C语言当中我们初始化需要自己定义一个函数,并且每次都要在该初始化的地方进行调用,这就很麻烦,万一有次忘了就会导致随机值的问题,那构造函数就是解决这个问题的
特征如下:
1️⃣函数名与类名相同。
2️⃣无返回值。
3️⃣对象实例化时编译器自动调用对应的构造函数。
4️⃣构造函数可以重载。
class Time
{
public:
//构造函数所要解决的问题就是初始化的问题
Time(int hour, int minute, int second)//带参构造函数
{
_hour = hour;
_minute = minute;
_second = second;
}
Time()//无参构造函数
{
_hour = 0;
_minute = 0;
_second = 1;
}
void Print()
{
cout << _hour << ":" << _minute << ":" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
Time t1(11, 5, 59);//构造函数传参的写法
t1.Print();
Time t2;//构造函数无参的写法,不能加括号!
t2.Print();
return 0;
}
5️⃣ 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class ZSTU
{
public:
ZSTU()
{
cout << "浙江理工大学" << endl;
}
};
class Time
{
public:
void Print()
{
cout << _hour << ":" << _minute << ":" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
ZSTU s1;//此处是证明点
};
int main()
{
Time t1;//此处实例化t1,编译器会自动调用一个无参的构造函数,能打印浙江理工大学就说明调用了
return 0;
}
解释:为什么说打印了浙江理工大学就是说编译器自动调用了一个无参的构造函数呢? 事实上构造函数就是对成员变量进行操作的,我们没有定义,编译器就自动生成了一个,不管它内部对成员变量是做什么的(事实上成员变量赋的是随机值),但你只要对成员变量进行干涉,就一定涉及到ZSTU s1,也即是一定涉及到s1实例化的过程,s1实例化就会调用它的构造函数继而打印出浙江理工大学。 注意:这里我们要证明的是Time里没有定义构造函数,但是现实是调用了,可不敢掉入s1调用不调用构造的问题
6️⃣不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?上面你说对那些成员变量赋成随机值,那这有什么用,不和之前一样了,那用处体现在什么地方?
答:其实不然,上面那段程序就已经说明了它的用处,虽然对于那些内置类型定义的变量,赋了一些随机值,但是对于我们自定义的那些,如s1,它会自动调用它的构造函数,这就是用处。
需要注意:C++11 中针对内置类型成员不初始化的缺陷,又做了补充,即:内置类型成员变量在 类中声明时可以给默认值。
7️⃣ 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
Time(int hour, int minute, int second)//带参构造函数
{
_hour = hour;
_minute = minute;
_second = second;
}
Time()//无参构造函数
{
_hour = 0;
_minute = 0;
_second = 1;
}
上面我们说了,只要定义了构造函数,编译器默认生成的就不会产生,那如果我们还想要调用一个无参的构造函数,不好意思,必须自己在定义一个,即如上所示,这多麻烦,有没有什么方法解决一下,这时我们前面学到的缺省参数就排上用场了,我们可以进行全缺省,如下所示:
Time(int hour = 0, int minute = 0, int second = 1)//全缺省参数
{
_hour = hour;
_minute = minute;
_second = second;
}
但是会存在一个问题,要是有了全缺省参数的构造函数,如果再写了无参的构造函数咋办?有人会不会要说,你脑子有泡啊,我都写了全缺省参数的构造函数,我还写无参的干嘛,闲的没事干啊,先别急,你可能不会写,但需要注意,如果两个同时存在了,就会产生歧义,导致报错,因为如果调用了一个无参的实例化,你调用哪个?所以对于全缺省和无参,以及没写编译器默认生成的只允许存在一个。
就是说,默认构造函数有三个:1.编译器自动生成的(前提自己没有显式定义) 2.自己显示定义的无参构造函数 3.全缺省的构造函数。三者只能存在一个
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
特征如下:
1️⃣ 析构函数名是在类名前加上字符 ~。
2️⃣ 无参数无返回值类型。
3️⃣ 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
注意:析构函数不能重载
4️⃣ 对象生命周期结束时,C++编译系统系统自动调用析构函数。
class Time
{
public:
~Time()//析构函数也是以函数名命名只不过前面加了个取反符号~,但是不具备函数重载的功效
{
cout << this << endl;//当然析构函数是完成清理功能,此处只是演示
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
Time t1;
t1.Print();
Time t2;
t2.Print();//析构函数也是一个默认成员函数没有显式定义的话是编译器会自动生成一个,并且自动调用
return 0;
}
这里其实隐藏了一个现象,就是函数入栈,出栈顺序,即这个析构函数的调用顺序体现了函数栈帧的出入顺序,看下面代码就可以体现:
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 1)//全缺省参数
{
_hour = hour;
_minute = minute;
_second = second;
cout << this << endl;//用来显示析构函数出栈调用顺序
}
void Print()
{
cout << _hour << ":" << _minute << ":" << _second << endl;
}
~Time()//析构函数也是以函数名命名只不过前面加了个取反符号~,但是不具备函数重载的功效
{
cout << this << endl;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
Time t1(11, 5, 59);
t1.Print();
Time t2;
t2.Print();//最终就会出现先入栈的函数析构函数后调用
//析构函数也是一个默认成员函数没有显式定义的话是编译器会自动生成一个,并且自动调用
//既然是不定义编译器就默认生成,就仍然会出现上述构造函数那样,如下
return 0;
}
可以体现两个事情: 1. 先入栈的函数析构函数后调用,即函数后销毁 2. 析构函数是在main函数栈帧销毁时,即t1、t2所占用空间即将释放时所调用的并不是t1实例化之后就调用
5️⃣ 关于编译器自动生成的析构函数,是否会完成一些事情呢?
这里事实上是和构造函数是一样的,不显式定义就会编译器默认生成,默认生成也并不意味着一事无成,对于自定义的变量,如下所示的s1它会调用它的析构函数,继而打印浙江理工大学。
class ZSTU
{
public:
~ZSTU()
{
cout << "浙江理工大学" << endl;
}
};
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 1)//全缺省参数
{
_hour = hour;
_minute = minute;
_second = second;
}
void Print()
{
cout << _hour << ":" << _minute << ":" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
ZSTU s1;//此处是证明点
};
int main()
{
Time t1(11, 5, 59);
t1.Print();
//能打印浙江理工大学就说明,虽然我们没有显式定义析构函数,但是编译器自动生成了并且进行了调用
return 0;
}
解释: 本质原因与上述相似,不过需要注意浙江理工大学肯定是在打印在 t1.Print()之后,这在上面我们已经证明,也就是说虽然我们没有显式定义析构函数,但是私有变量里存在类的实例化,而这个类里面存在析构函数,那么就会调用它的析构函数。
但这里我产生了一个诡异的想法,如果此时我们将ZSTU的析构函数改成构造函数会怎样?基于这样的想法我去验证了一下结果却是:
匪夷所思,这里我们显式定义了构造函数,并且构造函数内并没有涉及s1的内容,怎么仍然会调用s1的构造函数继而打印浙江理工大学,按照我们上面的解释是因为没有显式定义,编译器自动生成的默认构造函数会出现这样的情况,但是现在也出现了。经过查阅资料,解释是:
在C++中,当一个类包含另一个类的成员对象(如Time
类中的ZSTU s1
)时,该成员对象会在主类构造函数执行前自动初始化。过程如下所示:
成员对象的自动构造:
当创建Time
对象时(如Time t1
),编译器会在Time
的构造函数体执行之前,自动调用所有成员对象(此处是s1
)的默认构造函数。因此s1
的构造函数ZSTU()
被调用,输出"浙江理工大学"。
显式构造函数的作用:
Time(int hour = 0, int minute = 0, int second = 1) {
_hour = hour;
_minute = minute;
_second = second;
}
只负责初始化_hour
、_minute
、_second
,但不干预s1
的构造。成员对象s1
的构造由编译器在进入构造函数体前自动处理。
执行顺序:
Time
对象内存
s1
(调用ZSTU()
,输出"浙江理工大学")
Time
构造函数体内的赋值代码
t1.Print()
因此,即使显式构造函数未提及s1
,成员对象s1
仍会被编译器自动初始化,导致"浙江理工大学"被打印。
按照这样的解释,上面所讲述的默认构造函数的作用便与这的理论相悖了,我也很困惑,在此先将此问题留在此处,等C++学习深入以后,我们回过头来再共同解决它
一般来说,没有什么资源申请的类,析构函数可以不用写,如之前数据结构写的什么栈啊,是不是开辟内存空间了,因此呢,需要写个析构函数对其申请的资源进行释放,这样写一个析构函数,自动调用就比较好。
C++ 中,空类并非真的什么都没有,编译器会自动生成六个默认成员函数,包括构造函数和析构函数。构造函数用于初始化对象,有无参、全缺省和编译器自动生成等类型,全缺省和无参及编译器默认生成的默认构造函数三者只能存在一个。析构函数用于清理资源,一个类只能有一个析构函数,若未显式定义,系统自动生成。编译器自动生成的构造函数和析构函数虽看似简单,但对自定义类型变量可调用相应构造函数与析构函数,作用显著,如含自定义类型成员的类实例化或销毁时。