【构造函数】?【析构函数】?

1. 类的默认成员函数

默认成员函数就是**⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数**

如图 有 构造函数 和 析构函数 ,拷贝复制函数 , 赋值重载函数,这四个最为重要。

c++98 下,对于一个类,我们不显示写的情况下编译器默认会生成6个默认成员函数,其中前4个为我们重点学习对象,后面两个 取地址重载不重要,我们稍微了解⼀下即可。c++11 以后还会增加两个默认成员函数,我们以后在继续学习。

我们需要明确默认成员函数很重要,也⽐较复杂,我们要从两个⽅⾯ 去学习:

• 第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。

• 第⼆:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?

下面对默认成员函数的讲解都是从这两个方面展开学习

1.1 构造函数

1.11 定义

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

1.12 构造函数的特点

先看前四点

  1. 函数名与类名相同。

  2. ⽆返回值。(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)

3.对象实例化时系统会⾃动调⽤对应的构造函数。

  1. 构造函数可以重载

先来看第一种构造函数,无参构造函数,即没有形参,满足上述特点,函数名与类名相同,无返回值。调试发现,当对象实例化时会自动调用 该构造函数初始化。

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

class Date
{
public:
	//无参构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _year << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

构造函数可以重载 ,如图,带参构造函数,实例化时后面要加括号和传实参,创建对象d2时,会自动调用带参的构造函数。

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

class Date
{
public:
	//无参构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

	// 2.带参构造函数 
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}


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

	Date d2(2025,5,21);
	return 0;
}

注意不能这样实例化,因为编译器分不清这是函数声明,还是实例化。记住就行

cpp 复制代码
Date d3();
cpp 复制代码
	// 3.全缺省的构造函数 
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

全缺省的构造函数,显然比带参的构造函数更加方便,我们不传参时会以缺省参数为默认值,需要传参时也可以继续传。

那无参构造函数还需不需要呢,首先我们之前讲过,对于全缺省的构造函数和无参函数,再不给全缺省函数传实参时,会存在调用歧义,并且这里的全缺省的构造函数显然比无参构造函数更加实用,所以我们也不需要 无参构造函数了。

上面我们讲述了构造函数的前四个特点,接下来我们来进入第二关

  1. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显 式定义编译器将不再⽣成。

  2. ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函 数。(注意并不是只有编译器默认生成构造函数才叫默认构造函数)

但是这三个函数有且只有⼀个存在,不能同时存在 。(当我们不写时,编译器默认生成,但是当我们显示实现时,编译器就不会生成了,以及全缺省构造函数和无参函数也不能同时存在,所以这三个函数有且只有一个存在)⽆参构造函数和全缺省构造函数虽然构成 函数重载,但是调⽤时会存在歧义

要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫 默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调 ⽤的构造就叫默认构造。

  1. 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求也就是说是是否初始 化是不确定的,看编译器。( 大多数编译器对内置成员变量的初始化没有要求,为了方便,建议以后不管编译器对内置成员函数的初始化有没有要求,我们都自己实现**)对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始 化** 。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤ 初始化列表才能解决,初始化列表,我们下个章节再细细讲解。

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

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

class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return ;
		}
		_capacity = n;
		_top = 0;
	}
private:
	STDataType* _a;
	int _capacity;
	int _top;
};
// 两个Stack实现队列 
class MyQueue
{
public:
	//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化 


private:
	Stack pushst;
	Stack popst;
};

int main()
{
	MyQueue q;
	return 0;

对于MyQueue这个类,我们没有写构造函数,编译器自动生成的构造函数,对于自定义类型 Stack

回去调用它的默认构造函数进行初始化。所以说编译器默认生成的构造函数并不是一点作用都没有

2. 析构函数

2.1 定义

析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁 ,⽐如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对 象中资源的清理释放⼯作析构函数的功能类⽐我们之前Stack实现的Destroy功能,⽽像Date类中(没有对资源的申请)即也没有 Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。

2.2 特点

同样先看前四点

  1. 析构函数名是在类名前加上字符**~**。

  2. ⽆参数⽆返回值。 (这⾥跟构造类似,也不需要加void)

  3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。

  4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数

  5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理⾃定类型成员会 调⽤他的析构函数。

6. 还要注意的是,即使我们显示写了自定义类型的析构函数,它还是会去调用自定义成员变量的析构函数

7. 如果类中没有申请资源时,可以不需要自己写析构函数, 直接使⽤编译器⽣成的默认析构函数,如Date类,

如果默认生成的析构函数就可以使用,也不用自己写析构函数,如MyQueue,

如果涉及资源申请,一定要自己手动实现析构函数,否则会造成资源泄露。如stack

8.****⼀个局部域的多个对象,C++规定后定义的先析构,如stack的特点 后进先出,即后进来的先出去,也就是后定义的先释放先析构。

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

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_capacity = _top = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

int main()
{
	Stack s;

	return 0;
}

当对象的生命周期结束时,会自动调用析构函数,即上面,当mian函数结束时,会自动调用析构函数。

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

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_capacity = _top = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

class MyQueue
{

private:
	Stack Pushst;
	Stack Popst;
};

int main()
{
	MyQueue mq;

	return 0;
}

我们不写时,编译器对于内置类型不做处理,对自定义类型,**如上图的MyQueue,编译器默认生成的MyQueue的析构函数会去调用 Stack的析构函数,**和上面的构造函数一样。

6.还要注意的是,即使我们显示写了自定义类型的析构函数,它还是会去调用自定义成员变量的析构函数如图,也就是说⾃定义类 型成员⽆论什么情况都会⾃动调⽤析构函数

显示实现了,但是还是会去调用Stack的析构函数。先调用自己的,再调用成员变量的。

也就是说⾃定义类 型成员⽆论什么情况都会⾃动调⽤析构函数

8.****⼀个局部域的多个对象,C++规定后定义的先析构,如stack的特点 后进先出,即后进来的先出去,也就是后定义的先释放先析构。

如图依次创建对象 s1,s2,s3,但是调用析构函数,是先s3,s2,再s1.

3. 对比c 和 c++ 实现的栈

对⽐⼀下⽤C++和C实现的Stack解决之前括号匹配问题isValid,我们发现有了构造函数和析构函数确 (不需要手动初始化栈以及每次返回前,都要Destory)实⽅便了很多,不会再忘记调⽤Init和Destory函数了,也⽅便了不少。

c版本

cpp 复制代码
// ⽤之前C版本Stack实现 
bool isValid(const char* s) {
 ST st;
 STInit(&st);
 while (*s)
 {
 // 左括号⼊栈 
 if (*s == '(' || *s == '[' || *s == '{')
 {
 STPush(&st, *s);
 }
 else // 右括号取栈顶左括号尝试匹配 
 {
 if (STEmpty(&st))
 {
 STDestroy(&st);
 return false;
 }
 char top = STTop(&st);
 STPop(&st);
 // 不匹配 
 if ((top == '(' && *s != ')')
 || (top == '{' && *s != '}')
 || (top == '[' && *s != ']'))
 {
 STDestroy(&st);
 return false;
 }
}
 ++s;
 }
 // 栈不为空,说明左括号⽐右括号多,数量不匹配 
 bool ret = STEmpty(&st);
 STDestroy(&st);
 return ret;
}

c++版本

cpp 复制代码
#include<iostream>
using namespace std;
// ⽤最新加了构造和析构的C++版本Stack实现 
bool isValid(const char* s) {
     Stack st;
     while (*s)
     {
             if (*s == '[' || *s == '(' || *s == '{')
             {
                  st.Push(*s);
             }
             else
             {
                   // 右括号⽐左括号多,数量匹配问题 
                   if (st.Empty())
                   {
                       return false;
                   }
                  // 栈⾥⾯取左括号 
                   char top = st.Top();
                   st.Pop();
                   // 顺序不匹配 
                  if ((*s == ']' && top != '[')
                       || (*s == '}' && top != '{')
                       || (*s == ')' && top != '('))
                  {
                         return false;
                  }
             }
            ++s;
     }
              // 栈为空,返回真,说明数量都匹配 左括号多,右括号少匹配问题 
               return st.Empty();
}

由于析构函数会帮我们释放申请的资源,即替代了Destory,且会自动调用,相比c语言中,每次return前都要手动释放资源,方便很多,也不用担心会忘记释放资源,

c++中stack 对象创建时会自动调用构造函数初始化,也不需要我们手动初始化了。

相关推荐
玖玥拾11 小时前
C/C++ 数据结构(六)链表迭代器与底层
c语言·数据结构·c++·链表·stl库
牛油果子哥q12 小时前
AVL平衡树与红黑树深度精讲对比,平衡因子、四大旋转原理、着色规则、平衡策略、性能差异与面试手撕全解
数据结构·c++·面试
汉克老师12 小时前
GESP7级C++考试语法知识(二、指数函数(3、综合练习)
c++·算法·数学建模·指数函数·gesp7级·复利
C++ 老炮儿的技术栈12 小时前
Ubuntu root账号自动登陆
linux·运维·服务器·c语言·c++·ubuntu·visual studio
Irissgwe12 小时前
map/set/multimap/multiset 的底层逻辑与实现
数据结构·c++·算法·二叉树·stl·c·红黑树
凡人叶枫13 小时前
Effective C++ 条款39:明智而审慎地使用 private 继承
java·数据库·c++·嵌入式开发
不想写代码的星星13 小时前
伪共享:逻辑无共享,物理打成狗
c++
noipp14 小时前
【无标题】
c语言·数据结构·c++·算法
森G14 小时前
64、完善聊天室程序(TLV拓展)---------网络编程
网络·c++·tcp/ip
郝学胜-神的一滴14 小时前
完全二叉树与堆底层原理深度剖析 | 手写C++大顶堆实现
java·开发语言·数据结构·c++·python·算法