【C++】类和对象 — 默认成员函数(中篇)

📖前言

前情回顾:
👉 类和对象(上篇)

类和对象中最烧脑的就是默认成员函数这一节,本篇我们来好好讲一讲

类的6个默认成员函数:

  • 如果一个类中什么成员都没有,简称为空类
  • 空类中真的什么都没有吗?
  • 并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数
  • 默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数

在这里插入图片描述


1. 构造函数

1.1 构造函数的引入:

在上一节中讲到我们用C++实现一个栈的话,可以将其封装成一个类,这样使用起来既方便又规范。
在C语言中我们要对栈初始化的时候是要调用一个(Inint)函数的,而C++在设计的时候就考虑到了这一点,在创建对象的时候自动初始化,这就是构造函数要做的事情。

class Date
{
public:
	void Init(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.Init(2022, 7, 5);
	d1.Print();
	
	Date d2;
	d2.Init(2022, 7, 6);
	d2.Print();
	
	return 0;
}

1.对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

  • 构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用
  • 以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次

2.再来说明一下,私有成员变量命名前带_是为了区分,下面举个栗子:

  • 在构造函数中,如果成员函数中的形参和私有成员变量名字一样的话
  • year = year这句指令,year找值是就近原则,找year是先在这个函数里面找
  • 如果在函数里面找不到就会在类里面找,如果发现是类里面定义的,那么一定是属于某个对象的
  • 因为类里面的只是声明,找到的话会在前面加上一个this->

3.要通过this指针去对象里面找:

左值year是不会加上this指针的,因为在当前这个局部的作用域(函数)找year已经找到了,所以就无法区分了
year = year;
this->year = year;如果这样显式的去对象里面找,就可以完成赋值了
this->_month = month;
this->_day = day;

但是最好还是区分一下,将_加上,不写this指针

4.举个栗子:

_month = month;这条语句中,_month在构造函数中找不到,只能在类中找,如果发现是类里面定义的,那么_month一定是属于类实例化的某个对象的,要是想找到这个对象中的_month,可以使用隐含的this指针来找到该对象中_monht的空间,对其用形参传来的month来赋值


1.2 构造函数的特性和使用:

1.构造函数是特殊的成员函数,其特征如下:

  • 构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造
  • 但是构造函数的主要任务并不是开空间创建对象,而是初始化对象

使用特性:

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

(1)代码演示:

class Date
{
public:
	//Date() 
	//{
	//	_year = 1;
	//	_month = 1;
	//	_day = 1;
	//}
	
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	//一般情况下更喜欢的是全缺省,达到的作用是,可以传参数,也可不传参数
	//作用还可以多样化,参数可以传一个,传两个,传三个
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
private:
	int _year; //年
	int _month; //月
	int _day; //日
};

int main()
{
	//Date d1(); //没有参数的时候不能这么写
	//编译器直接将这一行忽略掉了,认为没有定义出对象
	//因为编译器无法区分这是一个函数的声明还是对象的定义

	//无参的时候这样写
	Date d1;
	d1.Print();

	//带参的时候这样写
	Date d2(2022, 5, 15);//构造函数参数在对象名的后面,在实例化对象的时候自动调用
	d2.Print(); //普通调用函数,参数都在函数名的后面

	return 0;
}

(2)注意点:

  • 构造函数参数在对象名的后面,在实例化对象的时候自动调用
  • 构造函数对参数没有要求,可以有参数,也可以没有
  • 构造函数形参最好全都带缺省值,可以传参数,也可不传参数
  • 作用还可以多样化,参数可以传一个,传两个,传三个等

(3)特殊情况:(解释)

  • Date()Date(int year = 1, int month = 1, int day = 1) 是可以同时存在的
  • 编译时通过的,因为它们构成函数重载
  • 虽然它们俩在语法上可以同时存在,但是在使用上是不能同时存在的
  • 因为会存在调用的歧义,不知道调用哪一个

(4)当没不传参数的时候不可以这样写Date d1()

  • 因为编译器无法区分这是一个函数的声明还是对象的定义
  • 编译器会认为没有定义出对象

1.3 默认生成构造函数:

先来补充几个知识点:(补充)

C++将变量分为两种:

  1. 内置类型/基本类型: int/char/double/任意类型的指针…
  2. 自定义类型: class/struct/union去定义类型对象…

1.默认生成构造函数:

规则:

  • 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
  • C++规定:默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理

2.对于自定义类型成员变量才会处理: (解释)

  • 内置类型的成员不做处理,不进行初始化
  • 对自定义类型的成员变量进行处理,去调用该自定义类型的构造函数
  • 如果该自定类型显式写了构造函数,就去调用那个构造函数
  • 如果没有则该自定义类型会生成默认构造函数
  • 该默认构造会依旧会对其所在类实例化的对象中进行
  • 内置类型不处理,自定义类型调用它的构造函数
  • 若类嵌套的话将会一直如此往复调用…

(1)代码演示:

在这里插入图片描述
(2)代码演示:

class Stack
{
public:
	
	Stack(int capacity = 10)  
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};


class MyQueue 
{
public:

	//这样写是不对的,因为这样a是Stack类中私有的,要在Stack类中初始化才行
	/*MyQueue()
	{
		_st1.a = 0;
	}*/

	//默认生成构造函数就可以用了,因为只有自定义的成员
	void push(int x)
	 {}

	int pop() 
	{}

private:
	//构造函数会对自定的义类型进行初始化,自定定义类型会调用自己的构造函数
	//栈需要写构造函数,队列就不需要写构造函数了
	Stack _st1;
	Stack _st2;
};

3.我们以之前我们在数据结构中用栈实现队列为例:

  • 队列这个类中没有自定义类型,所以可以不用显式去写构造函数
  • 因为队列这个类实例化的对象生成的默认构造会被调用
  • 内置类型不处理,自定义类型去调用其构造函数
  • 队列成员变量只有栈类实例化的对象,于是就去调用该对象的构造函数
  • 栈的构造函数是要显式写的,因为栈类中只有内置类型
  • 需要我们显式写一个才能将栈类型的成员变量初始化

4.总结:

  • 默认构造函数:1、自己写的无参的 2、自己写的全缺省的 3、我们不写编译器自动生成的
  • 如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数
  • 如果有内置类型的成员,或者需要显式传参初始化(自己写一个带参数的),那么都要自己实现构造函数
  • 大部分的情况下都要自己去写构造函数的,因为内置类型是默认构造函数初始化不了的
  • 默认构造函数是不好用的

5.补充:

C++的败笔:就是内置类型构造函数初始化不了

  • C++11打的补丁,针对编译器自己生成默认成员函数不初始化的问题
  • 给的缺省值,编译器自己生成默认构造函数使用
  • 不是初始化,是一种声明,没有开空间

代码演示:
在这里插入图片描述

只有新一点的编译器才支持这种用法,要是很老的编译器是不支持的

小结:

一般情况一个C++类,都要自己写构造函数,一般只有少数情况可以让编译器默认生成如下所示:

  • 类里面成员都/是自定义类型成员,并且这些成员都提供了默认构造函数
  • 如果还有内置类型成员,声明时给缺省值,C++11才有的缺成员声明给的缺省值

2. 析构函数

2.1 析构函数的引入:

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

析构函数: 与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作


2.2 析构函数的使用和特性:

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

  • 析构函数名是在类名前加上字符 ~
  • 无参数无返回值类型
  • 一个类只能有一个析构函数,若未显式定义,系统会自动生成默认的析构函数
  • 注意:析构函数不能重载
  • 对象生命周期结束时,C++编译系统系统自动调用析构函数

使用特性:

  • 一个类有且只有一个析构函数,若未显示定义,系统会生成默认的析构函数
  • 内置类型不做处理
  • 自定义类型会去调用他的析构函数

2.3 默认生成析构函数:

若未显式定义,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。

(1)代码演示1:

class B
{
public:
	~B()
	{
		cout << "~B()" << endl;
	}
private:
};

class A
{
private:
	// 基本类型(内置类型)
	int _a = 1;
	int _b = 2;
	int _c = 3;

	// 自定义类型
	B bb;
};

int main()
{
	A aa;
	return 0;
}

运行结果有是~B()

在main方法中根本没有直接创建B类的对象,为什么最后会调用B类的析构函数?

因为:main方法中创建了A对象aa,而aa中包含4个成员变量,其中a, b, c三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而bb是B类对象,所以在aa对象要销毁时,要将其内部包含的B类的bb对象销毁,所以要调用B类的析构函数。但是:main函 数中不能直接调用B类的析构函数,实际要释放的是A类对象,所以编译器会调用A类的析构函数,而A没有显式提供,则编译器会给A类生成一个默认的析构函数,目的是在其内部调用B类的析构函数,即当A对象销毁时,要保证其内部每个自定义对象都可以正确销毁。
main函数中并没有直接调用B类析构函数,而是调用编译器为A类生成的默认析构函数

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

(2)代码演示2:

class Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack(int capacity = 4)" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue
{

private:
	Stack _st1;
	Stack _st2;
};

int main()
{
	MyQueue mq;

	return 0;
}

在这里插入图片描述


2.4 析构的顺序:

类的析构函数调用一般按照构造函数调用的相反顺序进行调用

#include<iostream>
using namespace std;

class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}

private:
};

class B
{
public:
	B()
	{
		cout << "B()" << endl;

	}
	~B()
	{
		cout << "~B()" << endl;
	}

private:
};

int main()
{
	A aa;
	B bb;

	return 0;
}

在这里插入图片描述
如图解释:

在这里插入图片描述


3. 拷贝构造函数

3.1 拷贝构造的引入:

那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

3.2 拷贝构造的特性和使用:

1.拷贝构造函数也是特殊的成员函数,其特征如下:

  • 拷贝构造函数是构造函数的一个重载形式
  • 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用

使用特性:

  • 我们不写编译器会默认生成一个拷贝构造
  • 同类型对象初始化规定了要调用拷贝构造
  • 内置类型的成员会完成值拷贝,浅拷贝
  • 自定义类型的成员,去调用它的拷贝构造

(1)代码演示:

#include<iostream>
using namespace std;

class A
{
public:
	A(int a = 1, int b = 2)
	{
		_a = a;
		_b = b;
	}

	A(const A& a)//如果不改变a尽量加上const - 形成一个保护
	{
		_a = a._a;
		_b = a._b;
	}

	void Print()
	{
		cout << _a << " " << _b << endl;
	}
private:
	int _a;
	int _b;
};

int main()
{
	A aa;
	A bb(aa);

	aa.Print();
	bb.Print();

	return 0;
}

在这里插入图片描述
(2)无穷递归解释:(注意)

  • 拷贝构造函数是构造函数的重载
  • 只能有一个形参,并且必须是类型对象的引用,否则会无穷递归

(3)原因如下:

因为内置类型是直接值拷贝而,自定义类型是要调用该类型实例化的对象拷贝构造函数,调用拷贝构造如果是传值的话,那么就是,调用拷贝构造就要传值传参,传值传参就要赋值,赋值就要调用拷贝构造,调用拷贝构造就要传值传参,传值传参就要赋值,赋值就要调用拷贝构造…,一直无限递归下去……
在这里插入图片描述


3.3 默认生成拷贝构造:

若未显式定义,编译器会生成默认的拷贝构造函数。
默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

  • 内置类型的成员会完成值拷贝,浅拷贝
  • 自定义类型的成员,去调用它的拷贝构造

注意:这里刚好和构造函数处理方式相反

1.默认生成拷贝构造:

(1)代码演示:

编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,不需要自己显式实现。

#include<iostream>
using namespace std;

class A
{
public:
	A(int a = 1, int b = 2)
	{
		_a = a;
		_b = b;
	}

	void Print()
	{
		cout << _a << " " << _b << endl;
	}
private:
	int _a;
	int _b;
};

int main()
{
	A aa;
	A bb(aa);

	aa.Print();
	bb.Print();

	return 0;
}

运训结果如下:

在这里插入图片描述
这里没有显式写拷贝构造,编译器就默认生成了一个拷贝构造
因为只有内置类型所以直接值拷贝,如果有自定义类型则去调用该自定义类型的拷贝构造

(2)代码演示:

类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构
造函数是一定要写的,否则就是浅拷贝。

#include<iostream>
#include<assert.h>
using namespace std;

class Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack(int capacity = 4)" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}
	
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

int main()
{	
	//浅拷贝的时候,增删查改时会彼此修改,同时会析构两次
	//解决方案:要自己实现拷贝构造,--深拷贝
	Stack st1(10); 
	Stack st2(st1);

	return 0;
}

对于栈这个类来说,要显式写构造函数因为要初始化,要显式写析构函数因为要动态内存管理释放空间,要显式写拷贝构造,因为不显式写默认自动生成的话,会进行值拷贝,就会出问题:

如下图:
在这里插入图片描述
问题一:

  • 对象生命周期结束时,C++编译系统系统自动调用析构函数
  • 执行完Stack st2(st1); 之后,两个栈对象是指在同一个位置的
  • 当最后要调用析构函数时,同一块空间要释放两次肯定是错的

问题二:

  • 栈s2在对自己增删查改的时候会将s1的内容修改掉,也是不合理的

方法:这里很显然对栈的不能只是简单的值拷贝,也就是不能让编译器默认生成拷贝构造,因为默认生成的只能值拷贝(浅拷贝),我们要手写一个显式的拷贝构造)—— 深拷贝

结论:一般的类,自己生成拷贝构造就够用了,只有像Stack这样类,直接自己管理资源,需要自己实现拷贝。


深拷贝将在之后的博客中呈现,敬请期待……

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yy_上上谦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值