C++修炼之筑基期第二层——构造函数与析构函数

Posted 花想云

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++修炼之筑基期第二层——构造函数与析构函数相关的知识,希望对你有一定的参考价值。


文章目录

💐专栏导读

🌸作者简介:花想云,在读本科生一枚,致力于 C/C++、Linux 学习。

🌸本文收录于 C++系列,本专栏主要内容为 C++ 初阶、C++ 进阶、STL 详解等,专为大学生打造全套 C++ 学习教程,持续更新!

🌸相关专栏推荐:C语言初阶系列C语言进阶系列 数据结构与算法

💐文章导读

本章节我们将学习类的6个默认成员函数中的构造函数析构函数,并对比C语言阶段的内容来学习它们的各自的特性。

🌷默认成员函数

上一章中我们谈到,如果一个类中什么成员也没有,那么这个类就叫作空类。其实这么说是不太严谨的,因为一个类不可能什么都没有

当我们定义好一个类,不做任何处理时,编译器会自动生成以下6个默认成员函数

  • 默认成员函数:如果用户没有手动实现,则编译器会自动生成的成员函数。

  • 构造函数:主要完成初始化工作;

  • 析构函数:主要完成清理工作;

  • 拷贝构造:使用一个同类的对象初始化创建一个对象;

  • 赋值重载:把一个对象赋值给另一个对象;

  • 取地址重载普通对象取地址操作;

  • 取地址重载(const):const对象取地址操作;

本章我们将学习两个默认成员函数——构造函数析构函数

🌷构造函数

🌺引例

在C语言阶段,我们实现的数据结构时,有一件事很苦恼,就是每当创建一个stack对象(之前叫作定义一个stack类型的变量)后,首先得调用它的专属初始化函数StackInit来初始化对象。

typedef int dataOfStackType;

typedef struct stack

	dataOfStackType* a;
	int top;
	int capacity;
stack;

void StackInit(stack* ps);
//...

 int main()
 
	 stack s;
	 StackInit(&s);
	 //...
	 return 0;
 

这不免让人觉得有点麻烦。在C++中,构造函数为我们很好的解决了这一问题。

🌺构造函数的概念及特性

构造函数是一个特殊的成员函数。构造函数虽然叫作构造,但是其主要作用并不是开辟空间创建对象,而是初始化对象

构造函数之所以特殊,是因为相比于其它成员函数,它具有如下特性

  1. 函数名与类名相同
  2. 无返回值
  3. 对象实例化时,编译器自动调用对应的构造函数
  4. 构造函数可以重载

🌼举例🌼

class Date

public:
	//无参的构造函数
	Date()
	;
	//带参的构造函数
	Date(int year,int month,int day)
	
		_year = year;
		_month = month;
		_day = day;
	

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

void TestDate()

	Date d1;//调用无参构造函数(自动调用)
	Date d2(2023, 3, 29);//调用带参构造函数(自动调用)

🌼特别注意🌼

  • 创建对象时编译器会自动调用构造函数,若是调用无参构造函数,则无需在对象后面使用()。否则会产生歧义:编译器无法确定你是在声明函数还是在创建对象

🌼错误示例🌼

//错位示例
Date d3();
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
class Date

public:
	//若用户没有显示定义,则编译器自动生成。
	/*Date(int year,int month,int day)
	
		_year = year;
		_month = month;
		_day = day;
	*/

private:
	int _year;
	int _month;
	int _day;
;
  1. 默认生成构造函数,对内置类型成员不作处理;对自定义类型成员,会调用它的默认构造函数
  • C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int、char、double…,自定义类型就是我们使用class、struct、union等自己定义的类型。

🌼举例🌼

🌼默认构造函数对内置类型🌼

class Date

public:
	//此处不对构造函数做显示定义,测试默认构造函数
	/*Date()
	*/

	void print()
	
		cout << _year << "-" << _month << "-" << _day << endl;
	

private:
	int _year;
	int _month;
	int _day;
;
void TestDate1()

	Date d1;
	d1.print();

  • 如图所示,默认构造函数的确未对内置类型做处理。

🌼默认构造函数对自定义类型🌼

class stack

public:
	//此处对stack构造函数做显示定义
	stack()
	
		cout <<"stack()" << endl;
		_a = nullptr;
		_top = _capacity = 0;
	
private:
	int* _a;
	int _top;
	int _capacity;
;
class queue

public:
	//此处不对queue构造函数做显示定义,测试默认构造函数
	/*queue()
	*/
private:
	//自定义类型成员
	stack _s;
;

void TestQueue()

	queue q;

  • 如图所示,在创建queue对象时,默认构造函数对自定义成员_s做了处理,调用了它的默认构造函数stack()

这一波蜜汁操作让很多C++使用者感到困惑与不满,为什么要针对内置类型和自定义类型做不同的处理呢?终于,在C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:

  1. 内置类型成员变量在类中声明时可以给默认值

🌼举例🌼

class Date

public:
//...
	void print()
	
		cout << _year << "-" << _month << "-" << _day << endl;
	
private:
	//使用默认值
	int _year = 0;
	int _month = 0;
	int _day = 0;
;
void TestDate2()

	Date d2;
	d2.print();

  • 默认值:若不对成员变量做处理,则使用默认值。
  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个

🌼举例🌼

class Date

public:
	//无参的默认构造函数
	//Date()
	//

	//

	//全缺省的默认构造函数
	Date(int year = 0, int month = 0, int day = 0)
	
		_year = year;
		_month = month;
		_day = day;
	
	void print()
	
		cout << _year << "-" << _month << "-" << _day << endl;
	

private:
	int _year = 0;
	int _month = 0;
	int _day = 0;
;

🌷析构函数

析构函数构造函数的特性相似,但功能有恰好相反。构造函数是用来初始化对象的,析构函数是用来销毁对象的。

  • 需要注意的是,析构函数并不是对对象本身进行销毁(因为局部对象出了作用域会自行销毁,由编译器来完成),而是在对象销毁时会自动调用析构函数,对对象内部的资源做清理(例如stack _s中的int* a)。

同样,有了析构函数,我们再也不用担心创建对象(或定义变量)后由于忘记释放内存而造成内存泄漏了。

🌼举例🌼

class Stack

public:
	Stack()
	
		//...
	
	void Push(int x)
	
		//...

	
	bool Empty()
	
		// ...

	
	int Top()
	
		//...
	
	void Destory()
	
		//...
	
private:
	// 成员变量
	int* _a;
	int _top;
	int _capacity;
;

void TestStack()

	Stack s;
	st.Push(1);
	st.Push(2);
	//过去需要手动释放
	st.Destroy();

🌺析构函数的特性

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数
  3. 无返回值
  4. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
  5. 析构函数不能重载

🌼举例🌼

class Date

public:
	Date()
	
		cout << "Date()" << endl;
	
	~Date()
	
		cout << "~Date()" << endl;
	
private:
	int _year = 0;
	int _month = 0;
	int _day = 0;
;
void TestDate3()

	Date d3;
	//d3生命周期结束时自动调用构造函数


为便于观察,我们可以在析构函数内部写点儿东西。

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

🌼举例🌼

class stack

public:
	//此处对stack构造函数做显示定义
	stack()
	
		cout <<"stack()" << endl;
		_a = nullptr;
		_top = _capacity = 0;
	
	~stack()
	
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	
private:
	int* _a;
	int _top;
	int _capacity;
;
class queue

public:
	//此处不对queue构造函数做显示定义,测试默认构造函数
	/*queue()
	*/
private:
	//自定义类型成员
	stack _s;
;

void TestQueue1()

	queue q;

  • 这里可能有小伙伴会好奇:为什么析构函数不像构造函数那样区分内置类型与自定义类型呢
    答案是:因为内置类型压根不需要我们担心清理工作,在其生命周期结束时会自动销毁。而自定义类型需要担心,因为自定义类型里可能含有申请资源(例如:malloc申请内存须手动释放)。
  1. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如stack

本章到这里就结束了,下一章节我们将学习剩余的4个默认成员函数~

点击下方个人名片,可添加博主的个人QQ,交流会更方便哦~
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓

C++:对象和类|| 类的构造函数与析构函数

类在构造时,会默认调用类的构造函数;类在销毁时,又会默认调用类的析构函数,所以,这两函数无论是否预先定义,类在定义时都会自动执行,未定义时也会默认补足。巧妙使用构造函数与析构函数,可以实现类的初始化或其他特殊的功能。


析构函数与构造函数在内存分配上也具有重要的意义,如果构造函数使用new关键字申请了类空间,则在该类生命周期结束时,系统会自动调用析构函数使用delete释放空间,完成清理工作。


构造函数

声明和定义一个构造函数:

构造函数三无:无返回值,无类型,无自己的名字

Stock::Stock(const string & co, long n, double pr)由函数重载,可以多次定义Stock从而适应不同初始值的自动匹配

成员名和参数名

在构造函数中参数名不能与类成员名相同!

//No,该函数目的是为shares和share_val赋初值//由于构造函数可以直接访问数据这样操作会出问题Stock::Stock(const string& company, long shares, double share_val){    shares = shares??? //写到这里就会发现问题解决方法1:在定义时 修改数据前后缀Stock::Stock(const string& company, long shares, double share_val){    m_shares = shares;//前缀加m_    share_val_ = share_val; //后缀加_}解决方法2:修改形式参数名Stock::Stock(const string& company, long sh, double sh_val){    shares = sh;    share_val = sh_val;}


使用构造函数:


a.显式使用构造函数(构造函数返回相当于一个类)

Stock food = Stock(...)//food不存在,使用构造函数,可能无副本food = Stock(...)//food已经存在,赋初值,必有副本,若均可,往往第一种方法效率更高

b.隐式调用,直接加括号

Stock food(...)

对于a,部分编译器会直接当作b处理,也有部分编译器会创建一个副本,这使得会多产生一次构造与析构。

Stock *pstock = new Stock()...

d.构造函数不能被对象自身调用,因为没有构造函数就没有对象,对象不能自己构造自己


默认构造函数:

不带参数的构造函数,有构造函数就必须有默认构造函数,没有构造函数就可以没有默认构造函数,因为默认有了默认构造函数,这句话有些拗口,它的意思是,如果定义了含参的构造函数,则必须定义默认构造函数,否则会报错,这是想要避免创建未初始化的对象,确保类被定义时就有一个确定的已知值,未知的东西往往会带来想不到的后果。


为了省去麻烦,多打一行总是好的。


析构函数

析构函数同样满足三无:无返回值,无类型,无自己的名字

析构函数的命名是在类名前加上一个 非 符号~;

~Stock();Stock::~Stock(){    cout << "bye" << name << endl;}加上上面一句输出后运行,你会看见,每当一个类生命周期结束内存被释放时,它会给你说一句拜拜

注意,不同编译器对于析构函数的执行可能会有不同处理。


const:

这里的const是一种新语法,保证函数不会修改调用对象

倘若有例子:

const Stock land = Stock(...);land.show();//deny 程序将拒绝执行第二行,因为没有机制保证show()函数不会修改land

倘若在show函数声明时:多加一个关键字const,就像程序保证不会修改调用对象C++就会继续执行第二条语句了。

void show() const;对应函数定义的开头:void Stock::show() const{}


小结:

构造函数与析构函数是一类特殊的类成员函数,在创建和销毁对象时被调用。

构造函数与析构函数都是三无函数,区别在于~。

如果构造函数使用了new,那析构函数中应该体现delete。

以上是关于C++修炼之筑基期第二层——构造函数与析构函数的主要内容,如果未能解决你的问题,请参考以下文章

C++提高 --- 构造函数与析构函数

C++:对象和类|| 类的构造函数与析构函数

C++:对象和类|| 类的构造函数与析构函数

构造函数与析构函数的起源

C++ - 构造函数与析构函数的使用

C++ 类的继承三(继承中的构造与析构)