C++基础精讲篇第6讲:类中构造函数和析构函数特性详解

本文详细剖析C++中类的默认构造函数、析构函数及其特性,包括它们的自动调用、参数处理和资源管理,以及在对象创建和销毁过程中的行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C++基础精讲篇第4讲:类和对象的初步认识_King_lm_Guard的博客-优快云博客https://blog.youkuaiyun.com/King_lm_Guard/article/details/125930940?spm=1001.2014.3001.5502C++基础精讲篇第5讲:this指针特性研究_King_lm_Guard的博客-优快云博客https://blog.youkuaiyun.com/King_lm_Guard/article/details/125942676?spm=1001.2014.3001.5502       今天这一讲主要承接以上两讲的内容展开,带着大家详细分析关于类中6个默认成员函数原理讲解,下面跟着博主一起深入学习吧。

目录

1、6个默认成员函数组成

2、构造函数

2.1 基本概念

2.2 构造函数特性分析

2.4 小结

3、析构函数

3.1 基本概念

3.2 析构函数特性分析

3.3 析构函数调用特性

3.4 举例说明

4、构造函数和析构函数在执行和销毁时的特性研究(非常值得一看)

5、结语


1、6个默认成员函数组成

       在C++中,如果一个类中什么成员都没有,则简称为空类。但我们需要注意:所谓的空类中并不是真的什么都没有。

      当用户在创建一个类时,如果里面什么内容都没有,编译器会自动生成以下6个默认成员函数。

class Date{};

 对于默认成员函数:用户没有显示实现,编译器会生成的成员函数称之为默认成员函数。

2、构造函数

2.1 基本概念

        在理解构造函数之前,我们再以打印时间的函数为例展开说明:

#define _CRT_SECURE_NO_WARNINGS 
#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 << "-" << _month << "-" << _day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
	int a;
};
 
int main()
{
	Date d1, d2;
	d1.Init(2022, 7, 28);
	d2.Init(2022, 7, 29);
	d1.Print();
	d2.Print();
	return 0;
}

       在上面的代码中,我们在定义两个日期类变量时,需要通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

       针对上面的这类需求,类中的构造函数就能解决这一类问题。构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。

2.2 构造函数特性分析

       构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。


构造函数特性如下:

1、函数名与类名相同;

2、无返回值(即连空返回void都没有);

3、对象实例化时编译器自动调用对于的构造函数;

4、构造函数可以重载;

示例:

#define _CRT_SECURE_NO_WARNINGS 
#include<iostream>
using namespace std;

class Date
{
public:
    //无参构造函数
    Date()
    {

    }
    //带参数构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
	int a;
};
 
int main()
{
	Date d1;
	Date d2(2022, 7, 28);
	return 0;
}

补充:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明。


5、如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

分析:

5.1 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数;

5.2 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成


6、编译器自动生成的默认构造函数对内置类型不做处理,对自定义类型会去调用它(这个自自定义类型成员变量)的默认构造函数。

     补充1:C++把类型分成内置类型(基本类型)和自定义类型内置类型就是语言提供的数据类型,如:int/char/double/指针等,而自定义类型就是使用class/struct/union等自己定义的类型。

      所以我们接着来补充分析特性5中的具体性质:在特性5中的代码中,当用户没有显示创建构造函数时,编译通过,但调试会发现,此时生成的变量d中的值仍然是随机值,如下所示:

      这是因为对于编译器而言,变量_year、_month、_day的类型是int类型,也就是内置类型,所以C++中的编译器中的默认构造函数对其不做处理,所以这也解释了为什么还是随机值的原因。

       在下面演示的代码中,我在原来Date类的基础上,增加了一个Time类,然后在Date类中增加成员变量Time  _t ,调试结果发现,Date类中属于内置类型的成员变量和前面一样,不会被处理,而增加的成员变量Time  _t由于其是属于Time类,也就是前面分析的属于C++中的自定义类型,所以编译器会对自定义类型_t调用它的默认构造函数。


补充2:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

     如下所示:当在Date类中对内置类型声明时给默认值,即给缺省值,所以在调试过程中,内置类型同自定义类型均默认初始化。

#define _CRT_SECURE_NO_WARNINGS 
#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()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year=2022; // 年
	int _month=7; // 月
	int _day=28; // 日
	Time _t;
};

int main()
{
	Date d1;
	return 0;
}

7、无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

        补充:当默认构造函数有无参默认构造函数和全缺省默认构造函数同时存在时,编译能通过,但会提示有多个默认构造函数,调用的时候会存在歧义,又叫二一性。二者同时存在时,语法合理,调用存在歧义,为此,我们在书写的时候二者中我们就使用全缺省的。编译器自动生成的默认构造函数不会和上面无参和全缺省同时存在。

class Date
{
public:
    //屏蔽一个就可以编译通过

	//Date()
	//{
	//	_year = 1900;
	//	_month = 1; 
	//	_day = 1;
	//}

	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

void Test()
{
	Date d1;
}

2.4 小结

1、一般的类都不会让编译器默认生成构造函数,都会自己写,推荐显示写一个全缺省的默认构造函数,非常好用的;

2、特殊情况才会使用默认构造函数;

3、一个类只能有一个构造函数。

3、析构函数

3.1 基本概念

      与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

补充:

1、这里的清理工作可以理解为:系统资源的释放;

2、对象的销毁和作用域有关,出了作用域,对象生命周期结束,对象就被编译器销毁;

3、其实经过前面概念的描述,我们也应该能知道,析构函数作用的对象一般是诸如malloc、realloc这类函数,会在堆上开辟空间,需要用户手动释放空间。

3.2 析构函数特性分析

1、析构函数名是在类名前加上字符 ~。可以和"!"理解就是按位取反;

2、无参数无返回值;

3、一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载;

4、对象生命周期结束时,C++编译系统系统自动调用析构函数;
5、默认生成的析构函数的特点:跟构造函数类似,对内置类型不处理,对自定义类型成员会去调用它的析构函数。

6、如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

3.3 析构函数调用特性

1、诸如日期等编译器的内置类型,编译器默认对其不处理,因为也没必要处理,出了作用域此时变量已经销毁了,不用再进一步清理;
2、对自定义类型,用户可以在类中显示定义了析构函数,则编译器在对象销毁时,会自动调用显示定义的析构函数;
3、对自定义类型,如果没有显示定义,则编译器在对象销毁时,会自动默认生成析构函数,然后对自定义类型进行处理,其实这里的操作和默认构造函数对自定义类型的处理思想是一致的。

4、一般情况下,有动态开辟的资源交给类中的变量存储时,需要利用显示的析构函数清理。一般在栈、队列、顺序表、链表、二叉树等,可以这样理解:动态开辟的空间是放在堆上,所以函数栈帧销毁时,并不影响,需要手动释放。所以需要析构函数。

3.4 举例说明

        同样以日期Date类为例说明:下面的程序补充了析构函数,程序运行结果如上所示:可以看出:编译的时候分别调用了Time类中的自定义类型变量_t对应的默认构造函数和析构函数。在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?

 这是因为:

1、main中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。

2、但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁。
补充:

1、在这里示例的例子中,Time类中创建的析构函数相对于Date类而言,就是编译器提供的默默人析构函数;

2、创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。

#define _CRT_SECURE_NO_WARNINGS 
#include<iostream>
using namespace std;

class Time
{
public:
    //构造函数
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
    //析构函数
	~Time()
	{
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
    //内置类型
	int _year=2022; // 年
	int _month=7; // 月
	int _day=28; // 日
    //自定义类型
	Time _t;
};

int main()
{
	Date d1;
	return 0;
}

4、构造函数和析构函数在执行和销毁时的特性研究

补充知识:

程序内存组成:

内存地址区域作用及概述
栈区        存放函数内的局部变量,形参和函数返回值。栈区之中的数据的作用范围过了之后,系统就会回收自动管理栈区的内存(分配内存 , 回收内存),不需要开发人员来手动管理。
堆区        由程序员调用malloc、realloc等函数来主动申请的,需使用free函数来主动释放内存,若申请了堆区内存,之后忘记释放内存,很容易造成内存泄漏。
静态区/全局区        静态变量和全局变量的存储区域是一起的,一旦静态区的内存被分配,静态区的内存直到程序全部结束之后才会被释放。
字符常量区        存放常量(程序在运行的期间不能够被改变的量,例如: 数字,字符串常量, 数组的名字等)。
代码区        存放程序的代码,一般认为是二进制文件,即CPU执行的机器指令,并且是只读的。

情况1:

首先,我们先看下面这一段代码:

class A
{
public:
	//构造函数
	A(int a = 0)
	{
		_a = a;
		cout << "构造函数:A(int a = 0)-> " << _a << endl;
	}

	//析构函数
	~A()
	{
		cout << "析构函数:~A()-> " << _a << endl;

	}

private:
	int _a;
};


int main()
{
	A a1(1);
	A a2(2);
	A a3(3);
	A a4(4);
    A a5(5);

	return 0;
}

创建了一个类A,其内部定义了成员变量_a,以及自己显示定义的构造函数(全缺省)和析构函数,在main中定义了五个变量a1、a2、a3、a4、a5,我想问读者这个代码执行的结果是什么呢?

        不知道这个运行结果是不是和读者想的一样的呢。下面我详细解释为什么是这样的结果:从程序运行的结果中我们可以看到在main中定义了五个A类类型的变量,然后在执行过程中,变量之间执行的顺序都是先执行初始化:a1、a2、a3、a4、a5,然后销毁的时候,析构函数作为清理执行的顺序是:a5、a4、a3、a2、a1。之所以有这样的结果是因为:

        当定义的五个类类型的变量在执行过程中,在调用构造函数时,因为会在栈上开辟空间,也就是建立函数栈帧,所以首先依次建立函数栈帧,然后当程序运行结束时,此时系统需要回收在栈上开辟的空间,也就是所谓的销毁栈帧,销毁的顺序符合栈的后进先出的顺序,因为当销毁的时候,会做一次清理工作,所以执行析构函数的顺序是:后定义的先销毁先清理。


情况2:

理解了上面程序的执行过程,我们来看看下面这种情况:

class A
{
public:
	//构造函数
	A(int a = 0)
	{
		_a = a;
		cout << "构造函数:A(int a = 0)-> " << _a << endl;
	}

	//析构函数
	~A()
	{
		cout << "析构函数:~A()-> " << _a << endl;

	}

private:
	int _a;
};

A a0(0);

int main()
{
	A a1(1);
	static A a2(2);
	A a3(3);
	A a4(4);
	static A a5(5);

	return 0;
}

上面这段程序相比情况1增加了全局变量和静态变量,那执行的结果又会是怎样的呢?

构造函数执行顺序:a0、a1、a2、a3、a4、a5;

析构函数执行顺序:a4、a3、a1、a5、a2、a0;

        分析:在对象实例化时就会调用构造函数,所以执行顺序不考虑是全局、静态或者一般的局部变量,而是依次执行。但当程序结束,此时需要销毁空间,利用析构函数执行清理工作,所以执行析构函数的顺序会有所不同,根据内存空间的分区并且根据后进先出的规则,栈区相比于静态区/全局区会先销毁,而在栈区中后构造的同样先析构,所以析构的执行顺序就是:先执行栈区(在栈区中先析构后构造的变量),然后再执行静态区/全局区(在其内部先析构后构造的变量)。


情况3:

理解了上面情况1和情况2程序的执行过程,我们来看看下面这种情况:

class A
{
public:
	//构造函数
	A(int a = 0)
	{
		_a = a;
		cout << "构造函数:A(int a = 0)-> " << _a << endl;
	}
	//析构函数
	~A()
	{
		cout << "析构函数:~A()-> " << _a << endl;
	}
private:
	int _a;
};
void fun1()
{
	A a0(0);
	A a1(1);
	static A a2(2);
	A a3(3);
	A a4(4);
	static A a5(5);
}
int main()
{
	fun1();
	fun1();
	return 0;
}

        上面这种情况我就直接分析了:将创建的6个变量共同在函数fun中定义,当我们在main函数中执行fun函数第一次时,执行结果如上左图所示:

构造函数执行顺序:a0、a1、a2、a3、a4、a5;

析构函数执行顺序:a4、a3、a1、a0、a5、a2;

        这里执行结果同情况1和情况2分析可以理解,不在赘述。

        当在执行fu函数一次的基础上,再执行一次fun函数,我们会发现,程序输出的结果变化好像是有点大,但其实按照前面分析的两种情况也能解释:

分析:当执行第一个fun函数时:

                                        构造函数顺序:a0、a1、a2、a3、a4、a5;

                        ​​​​​​​        ​​​​​​​        析构函数顺序:a4、a3、a1、a0;

        析构函数出现这种情况是因为,变量a2和a5是静态变量,所以在第一次执行也就是初始化以后,就放在静态区了,而其他的a0、a1、a3和a4则是放在栈区,当程序要执行第二次fun函数时,在栈区中的变量需要销毁,根据后构造先析构的原则,所以程序执行的结果就是a4、a3、a1、a0,因为要第二次执行fun函数,因为变量a2和a5在第一次执行fun函数时已经定义好了,也就是初始化好了,所以就不用再次初始化,所以此时执行程序时的构造函数顺序是:a0、a1、a3、a4。当第二次执行的fun函数需要销毁时,此时同样的先销毁在栈中定义的变量,所以析构函数执行顺序是:a4、a3、a1、a0,然后程序往后执行发现没有其他需要执行的命令时,此时系统才会考虑销毁在静态区定义的变量,所以此时的析构函数执行顺序是:a5、a2。所以第二次调用fun函数的析构函数执行顺序就是:a4、a3、a1、a0、a5、a2。

5、结语

        今天这一讲为大家介绍了C++类中6个默认成员函数组成情况,并针对构造函数和析构函数展开详细分析,剩下的成员函数详解将在下一讲中为大家带来详细分析。欢迎大家点赞、关注、支持!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值