吃透C++类和对象(中):构造函数与析构函数深度解析

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》

《C++入门到进阶&自我学习过程记录》

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一、类的默认成员函数

二、构造函数

三、析构函数

结束语


前言

在上一篇文章吃透C++类和对象(上):封装、实例化与 this 指针详解我们讲解了C++中类的定义、实例化以及 this 指针,这些知识都是为后面的学习做铺垫,接下来我们将会学习类中的6个默认成员函数,由于这些函数的学习量很大所以我将会分成三篇来进行讲解,本篇文章主要讲解的是构造函数和析构函数。

一、类的默认成员函数

默认成员函数就是用户没有显式实现 ,编译器会自动生成的成员函数 称为默认成员函数 。一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解一下即可。其次就是C++11以后还会增加两个默认成员函数,移动构造移动赋值 ,这个由于涉及到C++后面的知识我们之后再进行讲解。默认成员函数很重要,但也比较复杂,我们要从两个方面去学习:

第一:当我们不写 时,编译器默认生成的函数行为是什么 ,这是否满足我们的需求

第二:当编译器默认生成的函数不满足我们的需求 时,我们就需要自己实现,那么如何自己实现?

二、构造函数

构造函数特殊的成员函数 ,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象 (我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象 。构造函数的本质 是要替代 我们以前 Stack 和 Date 类中写的Init 函数的功能 ,构造函数自动调用的特点就完美的替代的了 Init 。

构造函数的特点

1.函数名与类名相同
2.无返回值 。(返回值啥都不需要给,也不需要写 void ,不要纠结,C++规定如此)
3.对象实例化 时系统会自动调用 对应的构造函数

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	//无参构造函数
	Date() //函数名与类名相同,无返回值(也不需要写void)
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1; //对象实例化时系统就会自动调用对应的构造函数
	d1.Print();
    //注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,
    //否则编译器无法区分这里是函数声明还是实例化对象
    //Date d3(); //err
    //d3.Print();
    //warningC4930:"Date d3(void)" : 未调用原型函数(是否是有意用变量定义的 ? )
	return 0;
}

4.构造函数 可以重载

cpp 复制代码
class Date
{
public:
	//无参构造函数
	Date() //函数名与类名相同,无返回值(也不需要写void)
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

	//带参构造函数
	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; //对象实例化时系统就会自动调用对应的构造函数
	d1.Print();
	
	Date d2(2025, 12, 18);
	d2.Print();
	return 0;
}

如果类中没有显式定义构造函数 ,则 C++ 编译器会自动生成 一个无参的默认构造函数 ,一旦用户显式定义 编译器将不再生成
无参构造函数全缺省构造函数 、我们不写构造时编译器默认生成的构造函数 ,都叫做默认构造函数 。但是这三个函数有且只有一个存在,不能同时存在无参构造函数全缺省构造函数 虽然构成函数重载 ,但是调用时会存在歧义 。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结一下就是不传实参就可以调用的构造就叫默认构造

这里的确是刚接触构造函数时很多人的误区,默认构造函数其实包含了以上三种函数而并非只有编译器默认生成的。我们也可以利用代码来进行证明:

cpp 复制代码
class Date
{
public:
	//带参构造函数
	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;
	d1.Print();
	return 0;
}

由于此时我们写了带参的构造函数,所以编译器不会默认生成构造函数,但由于带参的构造函数并不是默认构造函数,所以就会报这样的错误。而通过前面的代码我们就知道当写了无参构造函数后程序可以运行,所以能说明无参构造函数是默认构造函数。而全缺省构造函数也是如此,这里就不重复证明了。

有个需要注意的点就是:全缺省构造函数与带参构造函数同时存在时不一定没问题 ,如果当全缺省构造函数的形参与带参完全一致时就会出现问题。

cpp 复制代码
class Date
{
public:
	//带参构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//全缺省构造函数
	Date(int year = 1, int month = 1, int day = 1) 
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

不管是对象实例化带不带实参都会出现这样的报错,如果是带实参我们很好理解就是因为编译器无法判断是调用哪个构造函数;但是不带实参的话,虽然我们理解是会调用全缺省构造函数(因为全缺省构造函数不传实参也能调用),但是在编译阶段就会出现上述的错误 而等不到调用的时候。

所以想要避免这种情况的做法就是:全缺省构造函数与带参函数的形参不同(个数或者类型),这样就可以避免重复声明的问题了。但是一般来说全缺省构造函数相当于就是无参和带参的结合,所以写了全缺省构造函数就不需要再写带参的了。

我们不写,编译器默认生成的构造,对于内置类型成员变量 的初始化没有要求 ,也就是说是否初始化是不确定 的,看编译器;对于自定义类型成员变量要求调用这个成员变量的默认构造函数初始化 。如果这个成员变量没有默认构造函数,那么就会报错,我们要初始化这个成员变量,就需要用初始化列表才能解决,初始化列表我们在类和对象的收尾时再细细讲解。

(说明:C++把类型 分成内置类型 (基本类型)和自定义类型内置类型 就是语言提供的原生数据类型 ,如:int / char / double / 指针 等,自定义类型 就是我们使用class / struct等关键字自己定义的类型。)

我们以之前在数据结构中用两个栈实现队列为例:

cpp 复制代码
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4) //全缺省构造函数为默认构造函数,对象 mq 的自定义成员变量能够找到进行初始化
	{
		_arr = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _arr)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

private:
	STDataType* _arr;
	size_t _capacity;
	size_t _top;
};

// 两个Stack实现队列 
class MyQueue
{
public:
	//编译器默认生成 MyQueue 的构造函数调用了Stack的构造,完成了两个成员的初始化 
private:
	Stack pushst; 
    //为自定义类型成员变量(隐含自带对象实例化的逻辑),需要存在对应的默认构造函数,否则会报错
	Stack popst;
};

int main()
{
	MyQueue mq;
	return 0;
}

但是当 Stack pushst; 自定义成员变量没有找到 对应的默认构造函数,则会报错:

cpp 复制代码
typedef int STDataType;
class Stack
{
public:
	Stack(int n) 
    //如果只有带参构造函数这种不是默认构造函数,
    //则 Stack pushst;无法找到对应的默认构造函数,也就会报错
	{
		_arr = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _arr)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

private:
	STDataType* _arr;
	size_t _capacity;
	size_t _top;
};

// 两个Stack实现队列 
class MyQueue
{
public:
	//编译器默认生成 MyQueue 的构造函数调用了Stack的构造,完成了两个成员的初始化 
private:
	Stack pushst; 
    //为自定义类型成员变量(隐含自带对象实例化的逻辑),需要存在对应的默认构造函数,否则会报错
	Stack popst;
};

int main()
{
	MyQueue mq;
	return 0;
}

三、析构函数

析构函数构造函数功能相反 ,析构函数不是完成对对象本身的销毁 ,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用 析构函数,完成对象中资源的清理释放工作 。析构函数的功能 类比我们之前 Stack 实现的 Destroy 功能,而像 Date 没有 Destroy ,其实就是没有资源需要释放,所以严格说 Date 是不需要析构函数的。

析构函数的特点

1.析构函数名 是在类名前加上字符 ~ 。(在C语言中我们就接触过这个符号,按位取反的符号就是 ~ ,在逻辑上和析构函数与构造函数功能相反有异曲同工之妙,可以加以理解)
2.无参数无返回值。(这里跟构造类似,也不需要加void,而由于没有参数,所以就没有重构的意义,因为重构的条件是形参不同,没有参数则一定是相同的)
3.一个类 只能有一个析构函数 。若未显式定义 ,系统会自动生成 默认的析构函数 。(只有一个析构函数就是因为没有重构的意义,当出现第二个析构 就会出现重复声明的报错)
4. 对象生命周期结束 时,系统会自动调用析构函数

cpp 复制代码
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_arr = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _arr)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	~Stack() //析构函数,一个类有且仅有一个,由于没有形参,则说明没有重构
		     //在对象生命周期结束时,系统会自动调用该析构函数
	{
		free(_arr);
		_arr = nullptr;
		_capacity = 0;
		_top = 0;
	}

private:
	STDataType* _arr;
	int _top;
	int _capacity;
};

int main()
{
	Stack st1;
	return 0;
}

5. 跟构造函数类似,我们不写编译器自动生成的析构函数内置类型成员不做处理自定类型成员调用他的析构函数 。但是自定义成员 存在管理了动态资源 的情况时,如果没有定义析构函数 则会出现内存泄漏的风险!

cpp 复制代码
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_arr = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _arr)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	~Stack() //析构函数,一个类有且仅有一个,由于没有形参,则说明没有重构
		     //在对象生命周期结束前,系统会自动调用该析构函数
	{
		free(_arr);
		_arr = nullptr;
		_capacity = 0;
		_top = 0;
	}

private:
	STDataType* _arr;
	int _top;
	int _capacity;
};

class MyQueue
{
public:
	//编译器默认生成 MyQueue 的析构函数调用了 Stack 的析构,释放的 Stack 内部的资源
private:
	Stack pushst; 
//由于 Stack 类存在动态开辟空间的情况,此时必须定义析构函数,否则就会内存泄漏
//因为我们不定义析构函数时系统默认生成的析构函数只会调用成员变量而并没有释放动态分配内存的功能
	Stack popst;
};

int main()
{
	//Stack st1;
	MyQueue mq;
	return 0;
}

6. 还需要注意的是:我们显示 写析构函数,对于自定义 类型成员也会调用他的析构 ,也就是说自定义类型成员无论什么情况 都会自动调用析构函数
7.如果类中没有申请资源 时,析构函数可以不写 ,直接使用编译器生成的默认析构函数,如 Date ;如果默认生成的析构就可以用,也就不需要显示写析构,如 MyQueue ;但是有资源申请 时,一定要自己写析构 ,否则会造成资源泄漏,如 Stack 。
8. 一个局部域的多个对象 ,C++ 规定后定义的先析构

那我们怎么证明呢?这就需要用到前面学习到的 this 指针了,由于析构函数也隐含有 this 指针,所以我们可以通过 this 指针在调试中查看谁先被释放空间:

cpp 复制代码
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_arr = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _arr)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	~Stack() //析构函数,一个类有且仅有一个,由于没有形参,则说明没有重构
		     //在对象生命周期结束时,系统会自动调用该析构函数
	{
		free(_arr);
		_arr = nullptr;
		_capacity = 0;
		_top = 0;
	}

private:
	STDataType* _arr;
	int _top;
	int _capacity;
};

int main()
{
	Stack st1;
	Stack st2;
	return 0;
}

结束语

到此,C++类的默认成员函数中的构造函数和析构函数就讲解完了,下一篇文章我们将继续讲解拷贝构造函数以及赋值运算符重载的知识,希望对大家学习C++有所帮助!

C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/

相关推荐
AA陈超2 小时前
Lyra Starter Game 中 GameFeature 类(如 ShooterCore)的加载流程
c++·笔记·学习·ue5·虚幻引擎
加成BUFF2 小时前
C++入门讲解3:数组与指针全面详解
开发语言·c++·算法·指针·数组
天若有情6733 小时前
我发明的PROTO_V4协议:一个让数据“穿上迷彩服”的发明(整数传输协议)
网络·c++·后端·安全·密码学·密码·数据
加油=^_^=3 小时前
【C++11】特殊类设计 | 类型转换
c++·单例模式·类型转换
加成BUFF3 小时前
C++入门详解2:数据类型、运算符与表达式
c语言·c++·计算机
徐行code3 小时前
std::bind()和lambda的区别
c++
小老鼠不吃猫3 小时前
C++20 STL <numbers> 数学常量库
开发语言·c++·c++20
程序员zgh3 小时前
C++常用设计模式
c语言·数据结构·c++·设计模式
im_AMBER3 小时前
Leetcode 80 统计一个数组中好对子的数目
数据结构·c++·笔记·学习·算法·leetcode