🔥个人主页:@草莓熊Lotso
🎬作者简介:C++研发方向学习者
📖个人专栏: 《C语言》 《数据结构与算法》《C语言刷题集》《Leetcode刷题指南》
⭐️人生格言:生活是默默的坚持,毅力是永久的享受。
前言:在上篇博客中我们初始学习了类和对象的一些知识,今天这篇博客主要是给大家分享一下类中的几个默认成员函数,第一部分我们会先学习构造函数和析构函数。这里的内容还是有点难的,建议大家可以多去实现一下,多看几遍 。
目录
一.类的默认成员函数
二.构造函数
构造函数的特点:
举例说明:(注意看注释)
三.析构函数
析构函数的特点:
举例说明:(注意看注释)
四.括号匹配问题的优化(利用当前阶段所学知识)
一.类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数。一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个我们后面再分享。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:
- 第一:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
- 第二:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?
二.构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init。
构造函数的特点:
特别说明:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型, 如:int/char/double/指针等,自定义类型就是我们使用class/struct等关键字自己定义的类型。
–上述的这些特点在后续的举例说明中都会体现出来的
举例说明:(注意看注释)
#include<iostream>
using namespace std;
class Date
{
public:
//如果这里不自己显示构造函数,编译器会自动生成一个无参的默认构造函数
//默认构造函数:无参构造函数,全缺省构造函数,编译器默认生成的构造函数
//三种默认构造函数只能同时存在一个(第三个的原因不用多说,前两个是因为构成重载但传参啥都不传时会歧义)
//1.自己实现的无参数的构造函数(默认构造函数的一种)
Date()//函数名和类名相同,无返回值
{
_year = 1;
_month = 1;
_day = 1;
}
//2.带参构造函数(可以和1同时存在)(不能和3同时存在,函数签名相同,构成了重定义)(不属于默认构造函数)
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
////3.全缺省构造函数(很好用,结合了1,2的功能)(但是不能和1或者2同时存在)(默认构造函数的一种)
//Date(int year = 1, int month = 1, int day = 1)
//{
//_year = year;
//_month = month;
//_day = day;
//}
//总结:两种使用方案:1+2或者直接用3
void Print()
{
cout << _year << "/" << _month << "/" << _day << '\\n';
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; //会自动初始化,调用1或者3,不能同时存在(我上面给3注释掉了,因为2和3也不能同时存在)
Date d2(2025, 7, 31);//会调用3,不会有歧义
//Date d3();//这种写法是错误的
// 参考一下 Data func();你觉得它是对象还是函数呢?
//注意:如果通过无参构造函数创建对象时,对象后面不用跟括号。
//否则编译器无法区分这里是函数声明还是实例化对象
d1.Print();//1,1,1
d2.Print();//2025,7,31
return 0;
}
我们不写,编译器默认生成的构造对内置类型初始化没要求:(看注释)
//我们不写,编译器默认生成的构造对内置类型初始化没要求
#include<iostream>
using namespace std;
class Date
{
public:
//如果这里不自己显示构造函数,编译器会自动生成一个无参的默认构造函数
void Print()
{
cout << _year << "/" << _month << "/" << _day << '\\n';
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();//打印出来结果是随机值
return 0;
}
–这个地方很奇怪,会让很多人怀疑这里编译器到底有没有自动生成一个默认构造函数,但是肯定是生成了的
我们不写,编译器默认生成的构造,对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化:(注意注释)
//我们不写,编译器默认生成的构造,对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化
//比如之前我们写过的两个栈实现队列
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(int n=4)//在这里还是要自己实现一个的,不然会出跟上面一样的问题,我们来定义一个全缺省的
{
_a = (int*)malloc(n * sizeof(int));
if (_a == nullptr)
{
perror("malloc fail!");
exit(1);
}
_top = 0;
_capacity = n;
}
private:
//内置类型
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
//编译器默认生成MyQueue的构造函数调用了Stack的构造函数,完成了两个成员的初始化
private:
//自定义类型
Stack _pushst;
Stack _popst;
//内置类型,但很奇怪,混在这里它却能处理,这里大家可以自己去试试
//int size = 0;
};
int main()
{
MyQueue q;
return 0;
}
调试可以看出全部都初始化了,而且我还发现这里自定义类型和内置类型混在一起时,将内置类型也处理了,大家可以自己试试。但我们还是严格按照定义来。
三.析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能,就像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
析构函数的特点:
–上述这些特点在后续的举例说明中都会讲述
举例说明:(注意看注释)
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(int n=4)//在这里还是要自己实现一个的,不然会出跟上面一样的问题,我们来定义一个全缺省的
{
_a = (int*)malloc(n * sizeof(int));
if (_a == nullptr)
{
perror("malloc fail!");
exit(1);
}
_top = 0;
_capacity = n;
}
//析构函数(跟构造函数写法类似,前面加个~)
//在这里也需要自己定义,不然他也不处理内置类型,会造成内存泄漏
~Stack()
{
if (_a)
{
free(_a);
_a = nullptr;
}
_top = 0;
_capacity = 0;
}
private:
//内置类型
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
//编译器默认生成MyQueue的构造函数调用了Stack的构造函数,完成了两个成员的初始化
//编译器默认生成MyQueue的析构函数调用了Stack的析构函数,释放的Stack内部的资源
//显示写析构,也会自动调用Stack的析构
/*~MyQueue()
{}*/
private:
//自定义类型
Stack _pushst;
Stack _popst;
//内置类型,但很奇怪,混在这里它却能处理,这里大家可以自己去试试
//int size = 0;
};
int main()
{
Stack s;
MyQueue q;
//析构不用显示写出来,对象生命周期结束时自动调用
//后定义的先析构,所以这里先析构q,再析构s.可以调试观察
return 0;
}
–调试观察到当对象生命周期结束时确实都销毁掉了,另外大家可以自己调试去观察一下后定义的先析构这个特点(其实跟栈有点像)
感兴趣的朋友可以写一下下面这道题:
这题答案是B,解析如下:
四.括号匹配问题的优化(利用当前阶段所学知识)
对比一下用C++和C实现的Stack解决之前括号匹配问题isValid,我们会发现有了构造函数和析构函数确实方便了很多,不会再忘记调用Init和Destory函数了(下面的两个实现都需要借用一下栈这个数据结构先,我这里就不展现出来了)。
以前用C语言实现的:
bool isValid(char* s) {
ST st;
STInit(&st);
char* pi = s;
while (*pi != '\\0')
{
if (*pi == '(' || *pi == '[' || *pi == '{')
{
STPush(&st, *pi);
}
else {
//右括号取栈顶元素进行匹配
//栈不为空才能取
if (STEmpty(&st))
{
STDestory(&st);
return false;
}
char top = STTop(&st);
if ((top == '(' && *pi != ')')
|| (top == '[' && *pi != ']')
|| (top == '{' && *pi != '}'))
{
STDestory(&st);
return false;
}
//本次匹配就出栈
STPop(&st);
}
pi++;
}
//为空有效,非空无效
bool ret = STEmpty(&st) ? true : false;
STDestory(&st);
return ret;
}
用最新加了构造和析构的C++版本Stack实现:
bool isValid(char* s) {
Stack st;
char* pi = s;
while (*pi != '\\0')
{
if (*pi == '(' || *pi == '[' || *pi == '{')
{
st.Push(&st, *pi);
}
else {
//右括号取栈顶元素进行匹配
//栈不为空才能取
if (st.Empty(&st))
{
return false;
}
char top = st.Top(&st);
if ((top == '(' && *pi != ')')
|| (top == '[' && *pi != ']')
|| (top == '{' && *pi != '}'))
{
return false;
}
//本次匹配就出栈
st.Pop(&st);
}
pi++;
}
//栈为空,返回真,说明数量都匹配 左括号多,右括号少匹配问题
return st.Empty();
}
对比图示如下:
本篇博客的完整源代码:
cpp-exclusive-warehouse: 【CPP知识学习仓库】 – Gitee.com
往期回顾:
《解锁 C++ 起源与核心:命名空间用法 + 版本演进全知道》
《解锁 C++ 基础密码:输入输出、缺省参数,函数重载与引用的精髓》
《解锁 C++ 进阶密码:引用补充与内联函数、nullptr 核心用法》
《吃透 C++ 类和对象(上):封装、实例化与 this 指针详解》
结语:在本篇博客中我们学习了构造函数和析构函数这两个类的默认成员函数,整体来看还是比较有难度的,但其实只要理解了构造函数之后,析构函数也就很好理解了,有很多相似的地方。类的前四个默认成员函数是一定要掌握的。如果文章对你有帮助的话,欢迎评论,点赞,收藏加关注,感谢大家的支持。
评论前必须登录!
注册