类的默认成员函数

类的默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。
在这里插入图片描述

1.构造函数

通过Date类来说明:

对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。

#include<iostream>
using namespace std;
class Date
{
public:
	void SetDate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Display()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.SetDate(2021, 1, 1);
	d1.Display();

	Date d2;
	d2.SetDate(2021, 5, 1);
	d2.Display();
	return 0;
}

对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。

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

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。
#include<iostream>
using namespace std;
class Date
{
public:
	Date()//无参构造函数
	{
		cout << "Date()" << endl;
	}
	Date(int year, int month, int day)//有参构造函数
	{
		cout << "Date(int year, int month, int day)" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date a;//当调用无参构造时后面不用加括号
	Date b(2021, 5, 14);
	return 0;
}

上述Date类的的有参和无参构造函数构成了重载,构造函数虽然可以重载,但是对于同一个定义的对象只能使用一个,否则有歧义。

当然如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
编译器生成的默认构造函数做了啥,他是有区别对待的:
对于成员变量中的基本类型,他什么也不会做。
对于成员变量中的自定义类型,他会去调用他会去调他的默认构造函数(不用参数就可以调用的:无参构造函数、全缺省构造函数、编译器生成的构造函数)
如下:

#include<iostream>
using namespace std;
class A
{
public:
	A()
	{
		cout << "A()"<<endl;
	}
private:
	int a;
};
class Date
{
private:
	int _year;
	int _month;
	int _day;
	A a;
};
int main()
{
	Date a;
	return 0;
}

在这里插入图片描述

2.析构函数

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作,析构函数是特殊的成员函数。
特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值。
  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
#include<iostream>
using namespace std;
class A
{
public:
	void create(int n)
	{
		cout << "create(int a)" << endl;
		_a = (int*)malloc(sizeof(int)*n);
	}
	~A()
	{
		if (_a)
		{
			cout << "~A()" << endl;
			free(_a);
		}
	}
public:
	int* _a;
};
int main()
{
	A a;
	//a.create(1);
	return 0;
}

在这里插入图片描述
当解除a.create的屏蔽后
在这里插入图片描述
想Date类,没有要释放的的资源,所以不需要自己写析构函数,编译器自动生成的就够了。但是只要我们的类中动态开辟了空间如stack这种,一定要自己实现析构函数。
构造函数和析构函数的最大特点就是会自动调用

class A
{
public:
	A(int a = 1)
	{
		cout <<this<< ":构造函数" << endl;
	}
	~A()
	{
		cout <<this<< ":析构函数" << endl;
	}
private:
	int _a;
};
int main()
{
	A a1;
	A a2;
	cout << "&a1:" << &a1 << " &a2:" << &a2 << endl;
}

在这里插入图片描述
从上面这代码和运行结果看出来什么?
定义对象性时构造函数和析构函数的最大特点就是会自动调用,并且后定义的先析构,构造函数在定义时调用,析构函数在生命周期结束时调用。

3.拷贝构造函数

在创建对象时,可否创建一个与一个对象一某一样的新对象呢?
比如:

int a=10;
int b=a;

就像上述代码这样在创建时可以用同类型去初始化
所以有了拷贝构造函数
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特征
拷贝构造函数也是特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
  3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

来一段代码演示:(暂时不考虑日期是否正确)

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 2021, int month = 5, int day = 14)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2021, 5, 15);
	Date d2(d1);
	Date d3 = d1;
	d1.print();
	d2.print();
	d3.print();
	return 0;
}

结果:
在这里插入图片描述

从这里看到

	Date d2(d1);
	Date d3 = d1;

这两句代码效果一样
看一下上述代码拷贝构造函数的结构

	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

定义规定参数就只能有一个,这没问题。
但是为什么他的参数是引用类型的呢?如果就是普通的自定义类型呢
思考一下
是不是会有下面这种情况
在这里插入图片描述
所以这里我们不妨试试用引用类型,为什么用可以用引用呢?
引用就是一个变量的别名,所以用引用时在上图中传参时就不会调用拷贝构造,这里就直接相当于把实参(因为引用定义的时候就是相当于那个数,只是名字不同)直接拿过来,而不会产生临时变量来存储实参
在这里插入图片描述
当然这里加const是为了安全考虑,前面讲引用的文章也讲过,引用传参时若不改变实参的值就尽量加const,不然有时候误操作或者不小心写错代码会改变实参的值。

当然拷贝构造函数我们不写编译器也会自动生成,向上面这种日期类编译器自动生成的就够了,编译器生成的拷贝构造能完成值拷贝也称浅拷贝。
但是当我们在写像stack这种类时就得自己实现了,举个栗子:

class Stack
{
public:
	Stack(int capacity=1)
	{
		_size = 0;
		_capacity=capacity;
		_a = (int*)malloc(sizeof(int)*_capacity);
	}
private:
	int _size;
	int _capacity;
	int *_a;
};
int main()
{
	Stack s1;
	Stack s2(s1);
}

在这里插入图片描述
从调试中能不能看到什么问题
发现s1和s2两个对象中的_a也值变一样了,那么当我们改变s2的的时候,s1是不是也会受到改变,这就会出问题了。
所以像这种类型的类千万不能用编译器默认生成的拷贝构造函数,得自己写,自己实现深拷贝。
总之,视情况而定。

4.赋值运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型或者枚举类型的操作数
3.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4.作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
操作符有一个默认的形参this,限定为第一个形参
5.(.* 、:: 、sizeof 、?: 、.) 注意以上5个运算符不能重载。

当运算符重载在全局:

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	private:
	int _year;
	int _month;
	int _day;
};
// 这里会发现运算符重载成全局的就需要成员变量是共有的,那么问题来了,封装性如何保证?
// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2) 
{
	return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}
void main()
{
	Date d1(2021, 5, 15);
	Date d2(2021, 6, 16);
	cout << (d1 == d2) << endl;
}

显然上面这段代码会有错误,因为在在类外是不能直接访问私有域的数据的,若是将私有域改为公有这样到时可以解决问题了,那么问题来了,封装性如何保证?
所以,我们可以将运算符重载放在类里面。

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator==(const Date& d1, const Date& d2)
	{
		return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
	}
	private:
	int _year;
	int _month;
	int _day;
};
void main()
{
	Date d1(2021, 5, 15);
	Date d2(2021, 6, 16);
	cout << (d1 == d2) << endl;
}

但是这样编译器又会报错了
在这里插入图片描述
为什么?
this指针,编译器会自动向类里面的函数添加一个隐含的this指针,前面也有讲过,就不在这里细讲了。
在这里插入图片描述
所以优化后会有三个参数,与函数调用的参数数量不符。
this指针指向的就是这个类,所以在类里重载时只需要另一个类传过来就行了,用引用传参可以减少拷贝。所以改一下有:

	bool operator==( Date& d2)
	{
		return (*this)._year == d2._year&& (*this)._month == d2._month&& (*this)._day == d2._day;
	}

*this就是当前类。
运行结果
在这里插入图片描述
调用时注意参数位置就行了。
赋值运算符主要有四点:

  1. 参数类型
  2. 返回值
  3. 检测是否自己给自己赋值
  4. 返回*this
  5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。

若是遇到下面这种情况呢?
当我们重载了=用来赋值

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void operator=(const Date& d2)
	{
		this->_year = d2._year;
		this->_month = d2._month;
		this->_day = d2._day;
	}
	void print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	private:
	int _year;
	int _month;
	int _day;
};
void main()
{
	Date d1(2021, 5, 15);
	Date d2(2021, 6, 16);
	Date d3, d4;
	d2 = d1;
	d4 = d3 = d2;
	d1.print();
	d2.print();
}

d4 = d3 = d2;
向上面这种连续赋值,我们就得改代码了,若是重载后的运算符是无返回值类型,那么上面这个语句就会出错,得返回一个和d3、d4一样类型的值。

	Date operator=(const Date& d2)
	{
		this->_year = d2._year;
		this->_month = d2._month;
		this->_day = d2._day;
		return d2;
	}

直接返回d2可以,那这里是不是就要调拷贝构造函数了,还是像前面常用的引用一样,直接传引用就不用拷贝构造了。改为

	Date& operator=(const Date& d2)
	{
		this->_year = d2._year;
		this->_month = d2._month;
		this->_day = d2._day;
		return *this;
	}

但是前面将引用的时候讲过,引用返回的时候要保证这个值的生命周期,这里可以成功返回码?
在这里插入图片描述
就算出来这个作用域销毁了this,但是Date&确已是d3的别名了,d3要销毁也得等程序结束了。

赋值运算符重载(d2=d1这种)和拷贝构造一样,不写的话编译器也会自动生成,他会完成值拷贝,但是像stack这样的类就不能用默认生成的,跟默认生成的拷贝构造一样,他只会完成浅拷贝,会影响到他前面的对象,要用深拷贝就得自己去实现了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值