C++---类与对象(二)

类的六个默认成员函数

在类中,用户没有实现时,编译器会自动生成的成员函数被称之为默认成员函数。

一、构造函数

构造函数是特殊的成员函数,主要任务是初始化对象,而不是开空间创建对象。

如上所说,构造函数的作用是保证对象中每个成员变量有合适的初始值,在对象整个生命周期中只调用一次。

当我们显式实现构造函数时,编译器就不会自动生成默认的构造函数了。

1、语法

①构造函数的函数名与类名相同;

②构造函数无返回值,即不写返回类型,而不是void;

③对象实例化/定义时,编译器自动调用对应的构造函数;

④构造函数可以重载。

显式实现构造函数的几种方式:

传参的构造函数:

无参但初始化:

全缺省构造函数

无参构造函数

注:在使用无参构造函数时,主函数中对象实例化时不能加括号,原因是如果加上括号,无法与函数的声明区分开:

那为什么全缺省和半缺省构造函数时,对象实例化时括号没有问题呢? 

因为全缺省和半缺省时,对象实例化括号中会有值,既然有值,那么编译器就能够分辨出这个不是声明,函数声明的参数部分,要么没有,要么是类型加形参,不可能是直接的值。

半缺省构造函数:

C++11打了补丁:可以在声明位置写入缺省值:

只是在声明处赋予缺省值,如果显式构造函数并直接赋值的话,并不会用到缺省值: 

2、特性

①构造函数属于默认成员函数,用户不显式写出,那么就使用编译器默认生成的构造函数;若用户显式写出,就使用用户写出的构造函数。那么编译器就不会自动生成默认的构造函数。

如下图我们使用编译器默认生成的构造函数,打印的是随机值:

使用编译器默认生成的构造函数,为什么没有进行初始化呢?

②编译器默认生成的构造函数,对于内置类型不会进行处理;对于自定义类型,会去调用它的默认构造函数。

也就是说,内置类型,若使用编译器默认生成的构造函数,不会对其初始化。这也就解释了上图中为什么是随机值。新的一些编译器的生成的构造函数可能会初始化内置类型,但是这并不是C++规定的要求。

对于自定义类型,我们可以看到如下图,Date类中含有Time类的一个成员变量_t,在Date类的一个对象实例化时,编译器调用默认的Date类的构造函数,遇到了自定义类型Time的变量_t,因此编译器就会去调用Time类的默认构造函数。 

自定义类型说到底,尽头仍然是内置类型,因此最后还是需要我们自己处理内置类型的初始化。 

那么我们在实际过程中,应该如何去选择构造函数呢?

我们需要分析类型成员和初始化需求,需要写构造函数就手写,不需要就使用编译器生成的,一般在绝大多数场景下都需要我们自己实现构造函数。

3、默认构造函数

用户显式写的无参构造函数、全缺省构造函数,还有编译器自动生成的构造函数,三者都被称为默认构造函数。

就是说,不需要传参能直接调用的构造函数,都属于默认构造函数。

对于默认构造函数的三种而言,三者只能存在其一,有显式构造函数,编译器就不会自动生成构造函数,倘若用户既写了无参构造函数又写了全缺省构造函数,会导致调用冲突,编译器不知道到底调用哪一个默认构造函数,因此默认构造函数只会存在一个。

注意区分默认成员函数默认构造函数

默认成员函数是,用户不显式写出时,编译器自动生成的6个成员函数

默认构造函数是指无参构造函数全缺省构造函数编译器生成的构造函数

构造函数的初始化列表部分在后面会讲到。

二、析构函数

构造函数是用来初始化定义的对象的,析构函数则是用来清理的。

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

1、语法

①析构函数名 == ~ + 类名;

即在类名前加~。

②析构函数无参数无返回值;

③一个类只有一个析构函数,用户没有显式实现析构函数时,编译器默认自动生成析构函数;

④对象生命周期到头,编译器自动调用对应析构函数。

2、特性

①编译器默认生成的析构函数同构造函数类似,对于内置类型而言,编译器不作处置;对于自定义类型而言,编译器调用自定义类型的析构函数进行处理。

类中没有申请资源时,可以不写析构函数,直接使用编译器生成的默认析构函数,如Date类;有资源申请时,一定要写析构函数否则会造成资源泄露。

并不是所有类都需要资源清理,即并不是所有类都刚需显式实现析构函数。 

编译器生成的默认析构函数是不会帮你去释放堆区空间的。

③与栈的先进后出类似,先定义的对象后析构,后定义的对象先析构,若多个对象不处于同一存储区域(生命周期不同),则根据实际情况处理。

对于不同对象的析构顺序解析:

均在局部域中创建对象D1、D2、D3:

D3存储于静态区,D1、D2在局部域/栈区,优先D1、D2,D2后构造,因此D2先析构,D1后析构,最后D3析构。

将D3与D1、D2交换顺序也没有影响,因为处于不同区域:

函数中与main函数中,以及全局域中创建类对象:

那么再来一个例子:

多个对象析构顺序总结:

局部域(后构造的先析构)--->局部域的静态区对象析构(多个静态区对象,同样先构造的后析构)--->

全局域(均处于静态区,后构造的先析构)

三、拷贝构造函数

只有一个形参,这个形参是对本类类型对象的引用,拷贝构造函数是对已经存在的类类型对象进行拷贝创建相同的新对象,在进行此过程时由编译器自动调用。

特性

①拷贝构造函数是构造函数的一个重载形式。

什么意思呢?

就是说,类中显式实现了拷贝构造函数,这个也算构造函数,编译器就不会自动生成默认的构造函数。

这个时候如果我们需要编译器的自动生成默认构造函数,可以使用如下代码强制编译器生成:

Time()=default;

②拷贝构造函数只有一个形参,这个形参必须是类类型对象的引用不能传值,传值会造成无穷递归,编译器强制检查,避免程序发生无穷递归因此进行报错。

如果传值,会报错:

那么为什么传值不行呢?传值为什么会造成无穷递归的问题?

这就涉及到了C++中自定义类型的拷贝规则:对于自定义类型的拷贝,都会自动去调用对应的拷贝构造函数,传值,本质上是形参对实参的临时拷贝,那么C++中编译器就会去调用拷贝构造函数,但是我们写的拷贝构造函数是传值传参,因此每一次还没有进入拷贝构造函数时,在参数部分由于每次都是传值,就会导致传值传参需要拷贝--->调用拷贝构造函数--->传值传参需要拷贝--->调用拷贝构造函数--->......。就形成了无穷递归:

在使用引用传参时,通常习惯在其前加上const,避免在拷贝构造函数实现时,写反代码:

但你写反了拷贝构造函数内部的实现时,最后执行程序,非但不会拷贝D1的成员变量数据,反而变本加厉,将D2的随机值全数赋给了D1。 

因此这个时候,就能够体现出const修饰的好处了:

const修饰能够保护拷贝的原对象的内容不被修改。 

③用户没有显式实现拷贝函数,那么编译器会生成默认的拷贝构造函数。

默认的拷贝构造函数对象内置类型成员按照内存存储字节序完成拷贝--->浅拷贝/值拷贝

默认的拷贝构造函数对象自定义类型成员调用该类类型的拷贝构造函数。

也就是说,默认拷贝构造函数不同于默认构造函数和默认析构函数对内置类型的不作为,对内置类型会采取值拷贝/浅拷贝;对于自定义类型成员,三者是类似的。

深拷贝:对于动态开辟空间的成员,需要使用深拷贝,自己实现拷贝函数,不能使用编译器生成的默认拷贝构造函数。

为什么涉及到动态开辟空间,需要深拷贝不能使用编译器自动生成的拷贝构造函数的浅拷贝?

举例说明,我们实现一个栈的类:

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	Stack s2(s1);
	return 0;
}

如果使用默认拷贝构造函数:

我们发现,_size和_capacity确实没有任何问题,完美地被拷贝到了s2中,但是两个对象的数组指针同时指向了一块动态开辟的空间,这就有问题了,这意味着,在最后的析构环节,当编译器对s2析构后,相当于已经释放掉了s2对象中数组指针指向的这块空间,但是s1数组指针指向的也是这块空间,这意味着s1对象的数组指针成为了野指针!同时后续析构s1对象时,会将野指针进行释放,也是一块空间的二次释放!这是不被允许的!

实际运行我们也会发现,程序会在free处报错。

因此对于有动态空间开辟的情况,我们需要使用深拷贝,需要手写,不能使用默认拷贝构造函数。

④拷贝构造函数典型调用场景   

使用已存在对象创建新对象;

函数参数类型为类类型对象;

函数返回值类型为类类型对象; 

四、运算符重载

赋值运算符重载属于默认成员函数,运算符重载不等于默认成员函数。

1、引言

对于一个日期类而言,如果我们想要比较两个日期的大小,那么之前的方式我们可以通过全局中邪函数来完成:

日期类如下:

class Date
{
public:
	//显式实现构造函数 --- 全缺省的默认构造函数
	Date(int year = 2025, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
//private:
	int _year;
	int _month;
	int _day;

};

全局中实现的日期相等DateEqual、判断较小日期DateLess的函数如下:

//实现两个日期的比较
//相等
bool DateEqual(const Date& x,const Date& y)
{
	return x._year == y._year
		&& x._month == y._month
		&& x._day == y._day;
}
//x<y输出true,反之输出false
bool DateLess(const Date& x, const Date& y)
{
	if (x._year < y._year)
		return true;
	else if (x._year == y._year)
	{
		if (x._month < y._month)
			return true;
		else if (x._month == y._month)
			return x._day < y._day;
	}
	return false;
}

形参传引用,就可以不用调用拷贝构造函数进行参数的拷贝,使用const修饰确保不会改变原日期 

2、概念

运算符重载---对运算符的行为进行按需求重新控制。

目的是使自定义类型的对象能够使用运算符。

函数名:

Operator+需要重载的运算符作函数名。

函数原型:

返回值类型 operator运算符(参数列表)

函数重载与运算符重载间的区别?

函数重载是函数的参数不同但是能够同名,运算符重载能够让自定义类型对象可以使用运算符,二者是不同的概念。

3、运算符重载的注意事项

①operator后连接的运算符,必须是C/C++语言中存在的运算符,不能连接其他的符号如operator@等。

②重载操作符,必须有一个类类型对象的参数。简单理解就是,重载操作符是针对于自定义类型的操作符,不能对内置类型对象进行修改。

③用于内置类型的运算符,尽量不去改变其含义。如整型+,就是+的含义,不要在重载操作符中实现整型-的指令。

④重载操作符作为类中成员函数时,与在全局域实现有所区别,参数由于包含隐藏的this指针,因此形参看起来比实际操作符少一个。

⑤以下5个操作符不能进行重载:

4、运算符重载举例 

在C++中,如上实现日期的比较时,我们可以使用Operator 加 运算符来代替函数名DateEqual、DateLess,操作如下:

//实现两个日期的比较
//相等
bool operator==(const Date& x,const Date& y)
{
	return x._year == y._year
		&& x._month == y._month
		&& x._day == y._day;
}
//x<y输出true,反之输出false
bool operator<(const Date& x, const Date& y)
{
	if (x._year < y._year)
		return true;
	else if (x._year == y._year)
	{
		if (x._month < y._month)
			return true;
		else if (x._month == y._month)
			return x._day < y._day;
	}
	return false;
}

在日期类的实现中,如果采取这种比较两个日期的方式,我们需要公有public修饰类的年_year、月_month、日_day成员,这样在类外的DateEqual、DateLess函数才能够访问类成员。这也就解释了为什么上面Date类的private访问限定符被去除。

但是通常情况下,我们一般会将类中的成员变量私有,即private修饰,那么我们就可以将Operator==和Operator<两个函数放入类中以解决无法类外访问成员的问题:

但是当我们放入类中,将Operator==和Operator<作为成员函数时,却调试报错,提示我们参数过多,这是为什么呢?

原因其实很简单,因为类的成员函数默认有一个隐含参数this指针,这就意味着,放入类中,编译器会使用this指针帮我们偷偷调用其中一个对象,因此我们需要对类中的Operator==和Operator<函数进行一定的修改,同时在主函数中的比较上也需要做出一些调整:

调用D1对象的operator==、operator<函数,那么需要我们手动传入的就是D2对象。

D1<D2 <===> D1.operator<(D2);

D1==D2 <===> D1.operator==(D2);

5、赋值运算符重载

①重载格式

假设类类型为D,参数中const D& d。

返回值类型:类类型的引用D&;

参数:类类型的引用const D&;

参数和返回值的类型采取传引用的方式均是为了提高效率。

检测是否自己赋值给自己;if(this != &d);

返回*this。

为什么需要返回值,因为赋值运算符允许连续赋值,那么重载也需要满足连续赋值的需求:

那么首先我们要理解清楚,连续赋值到底是怎么赋值的:

有两个要点:从右往左依次赋值赋值表达式的值为其左操作数

所以,如果不要求返回值,确实能够两个存在的对象间进行赋值,但是却无法满足多个对象的赋值要求。

那么为什么要传引用而不传值呢?

原因很简单,为了提高效率。前面在拷贝构造函数学习时提到过,传值并不会直接传对象,而会额外调用对应的拷贝构造。

②特性

[I]赋值运算符只能重载成类的成员函数,不能够重载成全局函数。其他运算符能重载成全局函数。

原因:

赋值运算符重载属于默认成员函数,这就意味着,用户没有显式实现,编译器会默认生成,如果重载成全局函数,此时类中没有赋值运算符重载,那么编译器会自动生成一个默认的赋值运算符重载函数,那么全局和类域中各有一个赋值运算符重载函数,就会发生冲突。因此赋值运算符重载只能是类的成员函数,不能是静态成员。

[II]赋值运算符重载作为一个默认成员函数,用户没有显式实现时,编译器会自动生成默认的赋值运算符重载,以值的方式逐字节拷贝(值拷贝/浅拷贝)。内置类型成员变量直接赋值,自定义类型成员变量则调用对应的赋值运算符重载进行赋值。遇见资源管理、动态空间开辟方面的成员变量,就需要自己设计进行深拷贝。

五、日期类的实现

我们实现一个日期类,功能包含获取某年某月的天数、判断两个对象日期的大小、日期加减天数、两个对象日期的天数差、对象日期自增等。

日期类的创建

class Date
{
public:
	//全缺省构造函数声明
	//注意:声明与定义分离时,缺省值给在声明处
	Date(int year = 1, int month = 1, int day = 1);

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

    //检查初始化日期的合理性
	bool CheckDate();
    
	//获取某年某月的天数
	//由于使用频繁,我们直接在类中定义---本质内联,需要频繁展开
	int GetMonthDay()
	{
		assert(_month > 0);
		assert(_month < 13);
		//最简便的方法:定义一个数组
		int monthday[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (_month == 2 && (_year % 4 == 0 && _year % 100 != 0) || _year % 400 == 0)
			return 29;
		return monthday[_month];
	}

	//赋值运算符重载
	Date& operator=(const Date& d);
	//拷贝构造函数,同赋值重载一样,可以不写直接使用编译器默认生成的
	//析构与构造函数同上,日期类不涉及动态空间开辟

	//两个日期的比较
	bool operator==(const Date& d);
	bool operator<(const Date& d);
	bool operator>(const Date& d);
	bool operator<=(const Date& d);
	bool operator>=(const Date& d);
	bool operator!=(const Date& d);



	//日期+=天数
	Date& operator+=(int day);
	//日期+天数 ===>与+=的区别是,不改变原来的对象
	Date operator+(int day);

	//日期-=天数
	Date& operator-=(int day);
	//日期-天数
	Date operator-(int day);

	//前置++
	Date& operator++();
	//后置++
	Date operator++(int);//int作用,构成函数重载,区分前置后置
	//前置--
	Date& operator--();
	//后置--
	Date operator--(int);//int作用,构成函数重载,区分前置后置

	//日期-日期===>返回天数求解
	int operator-(const Date& d);

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

//日期类的流插入 
//写成全局函数
ostream& operator<<(ostream& out, const Date& d);

//日期类的流提取
//同上全局函数
istream& operator>>(istream& in, Date& d);

//--- 流插入能够支持自定义类型!而C语言printf搞不定!
//--- 流插入的本质是解决所有类型的打印/输出问题!

获取某年某月的天数 

直接在类中定义。 

由于年份与月份都会影响天数,因此我们需要实现一个函数来获取到每个对象对应月份的对应天数,定义一个数组是最简单的方式,当然也可以多重if进行条件限制。

	//获取某年某月的天数
	//由于使用频繁,我们直接在类中定义---本质内联,需要频繁展开
	int GetMonthDay()
	{
		assert(_month > 0);
		assert(_month < 13);
		//最简便的方法:定义一个数组
		int monthday[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (_month == 2 && (_year % 4 == 0 && _year % 100 != 0) || _year % 400 == 0)
			return 29;
		return monthday[_month];
	}

同时这个函数会频繁的调用,在日期与天数的计算过程中,都少不了它,所以将其放入类中直接定义,而不用声明与定义分离,这本质上是内联。

两个日期的比较

    bool operator==(const Date& d);
    bool operator<(const Date& d);
    bool operator>(const Date& d);
    bool operator<=(const Date& d);
    bool operator>=(const Date& d);
    bool operator!=(const Date& d); 

对于两个日期的比较而言,看着要实现的接口多,实际上却比较方便,我们实现==与<的接口函数后,就能够调用这两个运算符重载对其他的日期比较接口进行实现,非常便捷。 

//两个日期的比较
bool Date::operator==(const Date& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}
bool Date::operator<(const Date& d)
{
	if (_year < d._year)
		return true;
	else if (_year == d._year)
	{
		if (_month < d._month)
			return true;
		else if (_month == d._month)
			return _day < d._day;
	}
	return false;
}
bool Date::operator>(const Date& d)
{
	return !(*this <= d);
}
bool Date::operator<=(const Date& d)
{
	return *this < d || *this == d;
}
bool Date::operator>=(const Date& d)
{
	return !(*this < d);
}
bool Date::operator!=(const Date& d)
{
	return !(*this == d);
}

日期+=天数、日期+天数

    Date& operator+=(int day);
    Date operator+(int day); 

主要是要分清楚二者的区别,+=是改变原对象,修改原对象日期;+是不改变原对象,创建一个临时对象接受原对象日期,然后改变这个临时对象。

1、日期+=天数
//日期+=天数
Date& Date::operator+=(int day)
{
	_day += day;
	while (_day > GetMonthDay())
	{
		_day -= GetMonthDay();
		_month++;
		if (_month > 12)
		{
			_month = 1;
			_year++;
		}
	}
	return *this;
}

2、日期+天数 
//日期+天数 (不会改变原对象日期)
Date Date::operator+(int day)
{
	//对一个正在创建的对象tmp使用已有对象*this进行初始化的过程--->拷贝构造的过程,不是赋值重载!
	Date tmp = *this;//拷贝构造
	tmp += day;
	//tmp是一个局部对象,函数终止tmp销毁,因此传值返回,传的是tmp的拷贝
	//如果传引用,tmp销毁就会成为野引用
	return tmp;
}

日期+=天数,实际上是一个对象的日期更迭,而日期+天数,是将基于对象1修改后的日期给予对象2。因此在日期+天数的操作中,我们不能够改变对象1的日期,但是需要能够返回对象1修改后的日期给对象2,因此函数内需要创建一个临时变量接受对象1日期然后修改,最后返回临时变量的拷贝,即传值,若传引用,会导致野引用。

+=实现+与+实现+=的优劣对比

日期-=天数、日期-天数 

    Date& operator-=(int day);
    Date operator-(int day); 

主要是要分清楚二者的区别,-=是改变原对象,修改原对象日期;-是不改变原对象,创建一个临时对象接受原对象日期,然后改变这个临时对象。

//日期-=天数
Date& Date::operator-=(int day)
{
	_day -= day;
	while (_day <= 0)
	{
		_month--;
		if (_month <= 0)
		{
			_month = 12;
			_year--;
		}
		_day += GetMonthDay();
	}
	return *this;
}
//日期-天数
Date Date::operator-(int day)
{
	Date tmp = *this;
	tmp -= day;

	return tmp;
}

前置/后置++/-- 

这里主要的注意事项是如何构造运算符重载来区分前置与后置,因为没有++operator的说法。

因此采用函数重载的方式对前置后置进行区分:通过多增加一个类型参数来区分前后置。

    //前置++
    Date& operator++();
    //后置++
    Date operator++(int);//int作用,构成函数重载,区分前置后置 

    //前置--
    Date& operator--();
    //后置--
    Date operator--(int);//int作用,构成函数重载,区分前置后置 

后置需要创建临时变量接收没有增1的对象日期并返回该临时变量,因此不能传引用返回,否则野引用。 

//前置++
Date& Date::operator++()
{
	*this += 1;
	return *this;
}
//后置++
Date Date::operator++(int)//int作用,构成函数重载,区分前置后置
{
	//创建临时对象tmp接受原对象日期,然后返回临时对象的值,不能传引用
	Date tmp = *this;//拷贝构造
	*this += 1;
	return tmp;
}
//前置--
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}
//后置--
Date Date::operator--(int)//int作用,构成函数重载,区分前置后置
{
	//创建临时对象tmp接受原对象日期,然后返回临时对象的值,不能传引用
	Date tmp = *this;//拷贝构造
	*this -= 1;
	return tmp;
}

日期-日期 

int operator-(const Date& d);

有关于日期-日期,其实我们不容易通过直接相减的方式进行实现,因为每年每月的天数其实是有差别的,我们可以转换思维,可以想象为小日期追大日期,追上所用的天数即为二者的天数差。

通过假设法,创建类类型的对象min与max表示小日期的对象与大日期的对象:

	//月份天数不定、年也对月份天数有影响,直接减法计算不佳
	//思路:后者追前者
	//假设法:创建小日期与大日期
	Date min = *this;
	Date max = d;
	int flag = -1;

	if (*this >= d)
	{
		min = d;
		max = *this;
		flag = 1;
	}

flag的作用是,求出天数差而非天数差的绝对值。

小日期追大日期:定义一个整型n变量记录追的天数。

	int n = 0;
	//小追大 --- 同时计天数
	while (min != max)
	{
		min++;
		n++;
	}
    return n * flag;

检查初始化日期的合理性 

bool CheckDate(); 

//检查初始化日期的合理性
bool Date::CheckDate()
{
	if (_year <= 0 || _month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay())
		return false;
	return true;
}

初始化时,可能会初始化某个月的天数超出了该有的天数,那么就应该进行检查。

因此在Date的构造函数定义中,就应该加上对于初始化日期的检查:

//全缺省构造函数定义
Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;

	if (!CheckDate())
	{
		cout << "构造日期" << _year << "/" << _month << "/" << _day <<"非法" << endl;
	}
}

日期类对象的流插入

ostream& operator<<(ostream& out ,const Date& d);

实现自定义类型对象的输出/打印。

如果在类中声明为成员函数,那么函数第一个参数就是隐含的this指针,那么这意味着,在流插入的过程中,与我们平时使用的流插入是反着的,这很不合适,因此我们最好在全局中声明该函数。

类中声明为成员函数:

//类中声明
ostream& operator<<(ostream& out);

ostream& Date::operator<<(ostream& out)
{
    out << _year << "/" << _month << "/" << _day << endl;
    return out;
}

int main()
{
    Date D1(2025 ,1 ,25);
    Date D2(2024 ,12 ,25);
    D1 << cout;//与平时的流插入倒反天罡了
    D2 << (D1 << cout); //连续的输出也很奇怪 ---D1->D2
    //对比普通的连续输出---cout << a1 << a2; ---a1->a2
}

我们平时都是cout << a ; 的形式,这里显然不太顺眼,问题就在于成员函数隐藏的第一个参数this

因此我们最好在全局中声明流插入函数,流提取也是一样。

全局中声明(最佳)

//全局中声明
ostream& operator<<(ostream& out ,const Date& d);

ostream& operator<<(ostream& out ,const Date& d)
{
    out << d._year << "/" << d._month << "/" << d._day << endl;
    return out;
}

int main()
{
    Date D1(2025 ,1 ,25);
    Date D2(2025 ,1 ,25);
    cout << D1 << D2;
}

全局中声明的好处是,没有隐藏参数this的限制,可以直接将左操作数设置为流插入类型的符号。这样的话就符合我们所学的流插入的顺序。如果是类中的成员函数,左操作数就定死了为this指针,这就导致了流插入的顺序倒转,虽然不会使结果出现问题,但在连续输出上也很别扭。

注:cout为ostream类型。

日期类对象的流提取

 istream& operator>>(istream& in, Date& d);

实现自定义类型对象的输入。

与日期类的流提取类似。

在全局中声明:

//日期类的流提取
//同上全局函数
istream& operator>>(istream& in, Date& d);

istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

int main()
{
    Date D1;
    Date D2;
    cin >> D1 >> D2;
}

在流提取的接口中,我们也可以对其进行合理性检查,因为是我们自己输入的日期,需要判断是否合理。

//日期类的流提取
istream& operator>>(istream& in, Date& d)
{
	while (1)
	{
		cout << "请依次输入年月日:>";
		in >> d._year >> d._month >> d._day;

		if (!d.CheckDate())
		{
			cout << "输入日期非法,请重新输入" << endl;
		}
		else
		{
			break;
		}
	}
	return in;
}

注:cin为istream类型。

对于全局中实现日期类的流插入和流提取,那么就无法访问日期类中的私有成员变量_year/_month/_day,因此我们在不更改成员变量的访问权限基础上,可以通过友元声明来解决:

在日期类之中对这些需要访问私有成员变量的函数进行一次友元声明。如上图所示。

Extra---流插入流提取与C语言printf、scanf的本质区别

流插入与流提取,与C语言printf、scanf的最大区别在于,流插入与流提取能够支持自定义类型!

它们被设计出来的本质是解决所有类型的输出与输入问题!

六、const成员

概念

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

日期类const成员说明

如果我们const Date创建一个日期类对象D1,那么D1不能够直接调用Print函数打印其日期。

具体原因在于权限的放大:D1的创建规定只读,但是Print函数却没有限制。

那么为了解决这种问题,C++规定能够在成员函数的声明定义的")"后添加const来限制对成员变量的访问权限,如下图所示:

对于const成员使用的总结

只对成员变量进行读访问的成员函数,建议加const,这样const对象与非const对象都能访问;

对成员变量进行读写访问的成员函数,不能加const,否则无法写访问。

那么据上述:我们应该对日期类的两个日期比较、检查合理性、日期-日期、日期+天数、日期-天数这些接口进行const优化。

注意:如只有定义,那么定义的)后加const;若声明与定义分离,声明和定义处均需要加const。

以检查合理性的接口作为示例,其余具体优化不作展示:

声明如下:

//检查初始化日期的合理性
bool CheckDate() const;

定义如下:

//检查初始化日期的合理性
bool Date::CheckDate() const
{
	if (_year <= 0 || _month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay())
		return false;
	return true;
}

日期类的流插入和流提取加const吗?

不加,const成员只针对于成员函数,而日期类的流插入和流提取虽然不对成员变量进行写访问,但是属于全局函数,不能加const成员修饰。

关于const的理解:

对于权限的放大,只会存在于指针和引用中。

指针的解引用和引用的别名,都会影响到原变量,因此可能存在指针权限与原变量权限冲突,就会导致权限放大的报错。如int* 的指针指向const int 的变量地址,int& 的引用,创建const int变量的别名。

临时变量的产生情况

对于类型转换、表达式的值、返回值传值(传值返回),都会产生临时变量。

有关const成员的提问

1. const对象可以调用非const成员函数吗?×

const对象不能调用非const成员函数===>权限的放大。

2. 非const对象可以调用const成员函数吗?

非const对象可以调用const成员函数===>权限的缩小。

3. const成员函数内可以调用其它的非const成员函数吗?×

const成员函数内不能调用其他非const成员函数===>权限的放大。

4. 非const成员函数内可以调用其它的const成员函数吗?

非const成员函数可以调用const成员函数===>权限的缩小。

七、取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义,编译器默认会生成,日常不需要我们写。

当然,如果我们不想别人获取到地址,那么可以重载这俩函数:

六个默认成员函数:构造/析构、拷贝构造/赋值重载、取地址/const取地址重载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值