编程就像搭积木:构造函数教你建,析构函数帮你拆

160 阅读7分钟

前言

在C嘎嘎中一个很重要的概念就是“类和对象”,而在一个类中比较重要的就是构造与析构,这篇文章便是讲解构造函数与析构函数的概念。

1. 类的默认成员函数

默认成员函数就是我们没有显示实现,但编译器会自动生成的成员函数被称为默认成员函数

image.png

一个类编译器会默认生成6个默认成员函数,本篇文章主要讲述前4个。

针对于此需要从两个方面来了解:

  1. 当我们没有显示写的时候,编译器默认生成的函数行为是什么,能否满足我们的要求?
  2. 当不能满足我们要求时,该如何自己实现。

2. 构造函数

构造函数虽然名称叫构造,但主要任务并非是开空间创建对象,我们常使用的局部对象在函数栈帧创建时空间便已开辟好了。构造函数的主要功能是在实例化对象时,对对象进行初始化。

在C语言中我们实现一个数据结构,例如栈,队列这些一般需要写一个初始化的Init函数,而构造函数的本质就是代替了这些函数,使得在创建对象时便能完成初始化。

构造函数特点:

  • 函数名与类名相同。
  • 无返回值。
  • 对象实例化时会自动调用对应的构造函数。
  • 构造函数可以重载。
  • 如果没有显示定义,则编译器会默认生成一个无参构造函数,当用户显示定义后编译器不再生成。
  • 无参构造、全缺省构造、编译器默认生成的构造,都叫做默认构造函数,但是只能有一个存在。无参构造和全缺省构造虽然构成函数重载,但在调用时存在歧义。总结一下不传实参就可以调用的构造函数就叫做默认构造。
  • 编译器默认生成的构造,对成员变量的初始化是不确定的,取决于编译器。对于内置类型成员变量初始化没有要求,但自定义成员变量没有构造函数便会报错。
#include<iostream>
using namespace std;

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

	// 带参构造
	Data(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	// 全缺省构造
	//Data(int year = 2000, int month = 12, int day = 31)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	// 无参构造
	Data d1;
	// 有参构造
	Data d2(2025, 3, 7);

	d1.Print();
	d2.Print();
  
  return 0;
}

image.png

两个栈实现一个队列:

#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;
		}

		_top = 0;
		_capacity = n;
	}

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

// 两个stack实现一个队列
class Queue
{
public:
	// 编译器默认生成的构造调用了Stack中的构造,完成了两个成员的初始化
private:
	Stack s1;
	Stack s2;
};

int main()
{
	Queue q1;
	return 0;
}

3. 析构函数

析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,因为局部对象是存在于函数栈帧中的,函数结束栈帧销毁,对象自然释放。而析构函数会在对象销毁时自动调用,目的是完成对象中资源的清理释放。功能类似于C语言中数据结构手动实现的Destroy函数,而在上面的Data类中,没有动态开辟等向操作系统申请的资源,所以不需要释放,也就不需要析构函数。

析构函数的特点:

  • 析构函数命名是在类名前加字符~。
  • 无参无返回值。
  • 一个类中只能有一个析构函数,若未显示定义,编译器自动生成的。
  • 对象生命周期结束后,自动调用析构函数。
  • 编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用其自己的析构函数。
  • 如类中没有申请的资源时,可以不写析构函数,如有申请的资源,必须写析构函数,否则会造成内存泄漏。
  • 一个局部域中,多个对象时,先定义的对象后析构,后定义的对象先析构。
#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;
		}

		_top = 0;
		_capacity = n;
	}
        
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

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

// 两个stack实现一个队列
class Queue
{
public:
	// 编译器默认生成的构造调用了Stack中的构造,完成了两个成员的初始化
	// 显示写析构,也会自动调用Stack的析构
private:
	Stack s1;
	Stack s2;
};

int main()
{
	Queue q1;
	return 0;
}

4. 拷贝构造函数

当一个构造函数的第一个参数是自身类型的引用,并且其他参数都有缺省值,则这个构造函数叫做拷贝构造函数,也就是说拷贝构造函数是一个特殊的构造函数。

拷贝构造的特点:

  • 拷贝构造函数是构造函数的重载。
  • 第一个参数必须是类类型对象的引用,使用传值方式编译器会报错,因为会引发无穷递归调用。
  • 拷贝构造可以多个参数,但是第一个必须是类对象引用,后面的参数必须有缺省值。
  • C++规定自定义类型对象进行拷贝必须调用拷贝构造,所以自定义类型传值传参和传值返回都会调用拷贝构造。
  • 若未显示定义拷贝构造,编译器会自动生成。自动生成的会对内置类型成员变量进行浅拷贝,对自定义类型成员变量会调用其自身的拷贝构造。
  • 像上文中Data类中的成员变量全部为内置类型且没有指向资源,所以编译器默认生成的拷贝构造就可以完成需要的拷贝。
  • 像Stack这样的类虽然也都是内置类型,但_a指向了申请的资源,编译器实现的浅拷贝不符合需求,所以需要自己实现拷贝构造。
  • Queue的内部主要是自定义类型,但编译器生成的拷贝构造会调用Stack的拷贝构造,所以也不需要显示实现拷贝构造。
  • 传值返回会产生一个临时对象调用拷贝构造,如果是引用返回则不会产生拷贝。
  • 如果函数返回对象是一个当前函数局部域中的对象,函数结束后系统回收函数栈帧,那么使用引用返回就会出现问题:相当于返回了一个野引用,类似野指针。所以虽然引用返回可以减少拷贝,但要确保返回对象在当前函数结束后还在。
#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;
		}

		_top = 0;
		_capacity = n;
	}

	Stack(const Stack& st)
	{
		// 需要对_a指向的资源创建同样大的空间,再拷贝值
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail!");
			return;
		}

		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

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

// 两个stack实现一个队列
class Queue
{
public:
	// 编译器默认生成的构造调用了Stack中的构造,完成了两个成员的初始化
	// 显示写析构,也会自动调用Stack的析构
private:
	Stack s1;
	Stack s2;
};

int main()
{
	Queue q1;
	return 0;
}