【是C++,不是C艹】 类与对象 | 默认成员函数 | 构造函数 | 析构函数

欢迎来到 Claffic 的博客

  专栏:《是C++,不是C艹》

前言:

在完成类与对象的认识后,我们接着学习类与对象的第二部分:默认成员函数,它包括构造函数,析构函数,拷贝构造,赋值重载,普通对象取地址和const对象取地址重载,放心,这一期不会都讲给你的,让我们来慢慢研究构造函数和析构函数:

注:

你最好是学完了C语言,并学过一些初阶的数据结构。


(没有目录) ヽ( ̄ω ̄( ̄ω ̄〃)ゝ 

Part1:默认成员函数

上一次我们提到了空类,里面没有成员,编译器给了它一个字节表示它存在,

class Date {};

❓那空类中真的什么也没有吗?

并不是的,编译器可是让你省心的存在:

类为空时,编译器会自动生成6个默认成员函数

【是C++,不是C艹】 类与对象 | 默认成员函数 | 构造函数 | 析构函数_第1张图片

❓生成是会生成,那它们有什么用呢?

有个例子:

上次我们提到了 Stack 的实现,其中有初始化函数 StackInit 和销毁函数 StackDestroy 

现在告诉你个好事:构造函数可以代替 StackInit ,析构函数可以代替 StackDestroy 。 

重点:它们都是编译器自动生成的,也就是说 像这种初始化函数和销毁函数不需要自己造了

当然不止这些,还有拷贝赋值和取地址重载等,大大方便了我们。

那么接下来,就由“构造函数”讲起:

Part2:构造函数

1.一个引子

为了更方便地讲解,我们先定义一个 Date 类:

#include
using namespace std;

class Date
{
public:
	void SetDate(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.SetDate(2023, 6, 1);
	d1.Print();

	Date d2;
	d2.SetDate(2023, 6, 4);
	d2.Print();

	return 0;
}

对于 Date 类来说,我每次要初始化设置信息,就要调用一次 SetDate 函数,那岂不是很烦?

❓那有没有一种办法,自动将我要传递的数值传递进去呢? 

❗还真有,那就是构造函数,让我们有请构造函数登场!!!

2.构造函数的概念

构造函数 是一个 特殊的成员函数,名字与类名相同 ,创建类类型对象时由编译器自动调用

能保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次 

构造函数的意义在于初始化对象,而不是给对象开辟空间(虽然名字叫做构造)

3.构造函数的特性

构造函数是特殊的成员函数,其特性如下: 

① 函数名与类名相同;

② 没有返回值;

③ 构造函数可以重载;

④ 对象实例化时编译器自动调用对应的构造函数:

如上方代码中的 Date d1

构造函数就在这个时候被调用了

让我们康康它具体是怎么使用的:

#include
using namespace std;

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

	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(2023, 6, 4); // 调用带参构造函数
	d2.Print();

	return 0;
}

️‍️输出结果:

【是C++,不是C艹】 类与对象 | 默认成员函数 | 构造函数 | 析构函数_第2张图片

不给参数就调用无参构造,给参数就调用带参构造

你以为你会了?其实有很多需要注意的地方:

注意:

构造函数是特殊的函数,不是普通的成员函数,所以不可以这样调用

#include
using namespace std;

class Date
{
public:
	Date()
	{
		_year = 1;
		_month = 0;
		_day = 0;
	}

	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.Date;    // 不可以这样调用
	d1.Print();

	return 0;
}

️‍️输出结果:

无参构造对象,对象后面不用跟括号,否则就成了函数的声明

#include
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

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

int main()
{
	Date d1(2023, 6, 5); // 带参就这样,括号加参数
	d1.Print();

	Date d2();  // 调用不带参构造函数,不要加括号
	d2.Print(); // 实际上这个类没有创建出来

	return 0;
}

报错: 

 【是C++,不是C艹】 类与对象 | 默认成员函数 | 构造函数 | 析构函数_第3张图片

带参构造,该传递多少参数就传递多少参数

【是C++,不是C艹】 类与对象 | 默认成员函数 | 构造函数 | 析构函数_第4张图片

如上三点要注意!

4.默认构造函数

我们聊完了构造函数,就要说说默认构造函数了:

如果你没有在类中定义构造函数(类中未显式定义),那么C++编译器就会自动生成一个无参的默认构造函数

#include
using namespace std;

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

️‍️输出结果:

没有定义构造函数,也会成功创建对象,只不过用随机值来进行初始化

❓你是否和我有着同样的困惑:既然构造函数是用来初始化的,调用默认构造函数之后,确实是把类的成员参数初始化了,but 是用随机值初始化的,有一种没有初始化的赶脚... ...

默认构造函数没卵用吗?

这里我不说,等到后面再解答(哎呦,不就是想要骗你把文章读完嘛~)(狗头

继续回到默认构造函数:

无参构造函数、全缺省构造函数都被称为默认构造函数。

并且默认构造函数只能有一个!

class Date
{
public:
	 // 全缺省的默认构造函数
	Date(int year = 2023, int month = 6, int day = 5)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

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

注意:

① 无参构造函数、全缺省构造函数、什么没写时编译器默认生成的构造函数,可以认为是默认构造函数(并不只有编译器默认生成的构造函数才叫做默认构造函数

② 无参构造和全缺省构造同时存在时会引发歧义:

#include
using namespace std;

class Date
{
public:
	// 无参的默认构造函数
	Date()
	{
		_year = 2023;
		_month = 6;
		_day = 5;
	}

	 // 全缺省的默认构造函数
	Date(int year = 2023, int month = 6, int day = 5)
	{
		_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++,不是C艹】 类与对象 | 默认成员函数 | 构造函数 | 析构函数_第5张图片

存在两个默认构造函数:无参的和全缺省的,但默认构造函数只能有一个,当类 d1 创建好后,编译器不知道用哪个构造函数来初始化。

  图源:柠檬叶子C

这种全缺省/半缺省的格外好用:

#include
using namespace std;

class Date
{
public:
	// 全缺省的默认构造函数
	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();

	Date d2(2023, 7, 14);
	d2.Print();

	Date d3(2023, 7);
	d3.Print();

	Date d4(2023);
	d4.Print();

	return 0;
}

️‍️输出结果: 

5.进一步探讨构造函数

这一部分主要解决上面留下的一个疑问:

如果你没有在类中定义构造函数(类中未显式定义),那么C++编译器就会自动生成一个无参的默认构造函数

#include
using namespace std;

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

️‍️输出结果:

没有定义构造函数,也会成功创建对象,只不过用随机值来进行初始化

❓你是否和我有着同样的困惑:既然构造函数是用来初始化的,调用默认构造函数之后,确实是把类的成员参数初始化了,but 是用随机值初始化的,有一种没有初始化的赶脚... ...

默认构造函数没卵用吗?

解答:

C++把类型分成内置类型(基本类型)和自定义类型

内置类型:语言本身提供的数据类型,如 int / char / float

自定义类型:我们使用 class / struct / union 等自己定义的类型。

我们一起来看看下面的程序: 

#include
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
{
// 这里没有显式定义构造函数
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;

	return 0;
}

️‍️输出结果: 

️我们可以看出,编译器生成的 Date 默认构造函数会对自定义 Time 类型成员 _t 调用它的默认成员函数。

编译器默认生成构造函数:

对于内置类型的成员变量,会用随机值进行“处理”。

对于自定义类型的成员变量,会去调用它的默认构造函数(不用参数就可以调的)初始化。

❓随机值果然很挫,有没有一种办法,即能用到编译器默认生成的构造函数,又能使内置类型不是随机值?

这里提供一种方法:就是在定义内置类型的时候就给一个初始值(C++11 内置类型成员变量在类中声明时可以给默认值

#include
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 = 1;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	d.Print();

	return 0;
}

️‍️输出结果:

是不是爽歪歪? 

Part3:析构函数

1.一个引子

❓在前面构造函数的学习之后,我们知道了一个对象是怎么来的,那么一个对象是怎么没的呢?

构造函数的使命是初始化,那么谁来做清理工作?

❗那就是 -- 析构函数

2.析构函数的概念

对象在销毁时调用析构函数,完成对象中资源的清理工作

注意:析构函数不是完成对象本身的销毁,局部对象销毁工作是由编译器完成的

3.析构函数的特性

析构函数也是特殊的成员函数,其特征如下:

① 析构函数名是在类名前加上字符 ~

② 无返回类型,也无参数

③ 一个类只能有一个析构函数,若无显式定义,系统会自动生成默认的析构函数。

注意:析构函数不能重载

④ 对象的声明周期结束时,C++编译系统自动调用析构函数:

【是C++,不是C艹】 类与对象 | 默认成员函数 | 构造函数 | 析构函数_第6张图片 为了演示自动调用,就让析构函数被调用时 “仙逝” 一波 ~ 

#include 
using namespace std;

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

    ~Date() 
    {
        cout << "~Date() 仙逝~ " << endl;  // 测试一波
    }

    void Print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

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

int main()
{
    Date d1;
    Date d2(2023, 6, 5);

    return 0;
}

️‍️输出结果:

因为创建了两个 Dated1 , d2 ,所以都会自动销毁

析构函数的魅力不止,为了更好的演示,下面就采用 Stack :

之前实现 Stack ,最后需要 StackDestroy 来清理栈中的数据,

现在可以让析构函数来干这个活,泰爽辣:

#include 
#include 
using namespace std;

typedef int DataType;
class Stack
{
public:
	// 构造函数 默认容量为4
	Stack(size_t capacity = 4)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc failed");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	// 析构函数 清理开辟的空间,防止内存泄露
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

void TestStack()
{
	Stack s1; // 默认容量为4
	Stack s2(10); // 设置初始容量为10
}

缺省了一个初始容量,也可以自己定义   另外不需要手动调用析构函数,都交给编译器。

继续回到析构函数的特性:

⑤ 编译器默认生成的析构函数,对自定义类型成员调用它的析构函数:

这一点与构造函数相同,下面来测试

#include 
using namespace std;

class Time
{
public:
	~Time()
	{
		cout << "~Time() 仙逝~" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

️‍️输出结果:

❓明明没有创建 Time 类的对象,为什么最后会调用 Time 类的析构函数?

解释:

编译器默认生成了 Date 类的析构函数,Date 类中含有自定义类型 Time ,于是 Date 类的析构函数调用了 Time 类的析构函数,做到当 Date 对象销毁时,保证其内部每个对象都正确销毁。

注意:创建哪个类的对象则调用该类的析构函数,销毁哪个类的对象则调用该类的析构函数

⑥ 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如 Date      类;

     如果有资源申请,一定要写,否则会造成内存泄漏,如 Stack 类,其中含有动态内存申请,所       以一定要写。

4.进一步探讨析构函数

我们知道了如果你不写析构函数,编译器会自动生成一个默认析构函数,这个默认析构函数会做些什么呢?

先来串联一下过来的知识:

如果你不写构造函数,编译器会自动生成,这个自动生成的 默认构造函数

• "内置类型" 成员变量:不会做初始化处理
• "自定义类型" 成员变量:会调用它的默认构造函数初始化

对应的,析构函数也是这样: 

如果你不写析构函数,编译器会自动生成,这个自动生成的 默认析构函数

• "内置类型" 的成员变量:不作处理,也不需要处理,系统将其内存回收
• "自定义类型" 的成员变量:会调用它对应的析构函数

就如特性⑤所提到的:

编译器默认生成的析构函数,对自定义类型成员调用它的析构函数:

#include 
using namespace std;

class Time
{
public:
	~Time()
	{
		cout << "~Time() 仙逝~" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

️‍️输出结果:


总结: 

这篇带大家认识了6个默认成员函数,再给大家详细讲解了构造函数和析构函数,思维不要停留在C语言阶段啦,C++编译器可以帮你做很多事情的!

码文不易 

如果你觉得这篇文章还不错并且对你有帮助,不妨支持一波哦  

你可能感兴趣的:(是C++,不是C艹,c++)