C++类和对象(中)

目录

类的6个默认成员函数

第一个默认成员函数———构造函数

第二个默认成员函数———析构函数

构造函数与析构函数小结

第三个默认成员函数———拷贝构造函数

第四个默认成员函数———赋值运算符重载

第五、六个成员函数————取地址重载


类的6个默认成员函数

如果一个类里面什么都没有(没有成员对象及函数),那称其为空类。但是空类也不是真的什么都没有,编译器会默认自动生成6个成员函数(隐式的),如果我们自己在类中实现了这些默认成员函数,编译器就不会生成。

这段话隐含的意思就是:有的类需要我们自己写默认成员函数(因为有些情况编译器自动生成的不方便使用),有的类则不需要,编译器默认生成的就很方便。

第一个默认成员函数———构造函数

声明一下:关于构造函数的初始化列表在类和对象(下)中讲解。

在之前用C语言写栈的时候,我们经常会犯一个错误:忘记调用  初始化函数Init  和   销毁函数Destory。不仅是我们,设计该语言的大佬也会,所以在构架C++的时候,就引入了构造函数和析构函数

构造函数是在我们创建变量的时候调用的函数,帮助我们自动实现初始化。注意:构造函数不是创建变量,变量还是交给我们自己创建,它只是自动完成变量初始化。

构造函数的特征

1、构造函数名与类名相同。

2、编译器在对象创建时自动调用,上面讲了。

3、在对象生命周期只调用一次,初始化只会有一次。

4、无返回值!注意:这里无返回值不是指void,而是不写返回值!

5、支持函数重载。

还是以日期类为例:

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1) //函数名与类名相同
	{
		_year = year;                  //没有返回值
		_month = month;
		_day = day;
	}
	void Print(/*Date* const this*/)
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022, 1, 1);
	Date d2(2022, 1, 1);
	return 0;
}

构造函数调用的时候如上所示,参数写在对象后面。(C++规定)

如果是无参的,写成:

	Date d1;

不能写成:

	Date d1();

下面这种写法,编译器容易认为Date是返回类型,d1是函数名,()里是调用,会报警告

前面说了,如果我们没有写构造函数,那么编译器就会自己实现一个,那它是如何实现的呢?

我们来看一下。

 其实这是C++设计的一个失败之处,是一个比较麻烦的坑。

还是以上面的日期类为例,如果不写构造函数,看看编译器默认生成什么样:

 我们发现,编译器自动调用的构造函数生成的是随机值

我们来看看这是什么情况。

C++将类型分为两种,内置类型和自定义类型

内置类型:int、char、double.......指针(自定义类型指针也算,如:struct node*)

自定义类型:class、struct、stack(栈)、queue(队列)、自定义结构

对于内置类型,C++是不处理的,也就是说初始化为随机值。(像上面例子一样)

对于自定义类型,C++会调用它的默认构造函数。(自定义类型也有自己的构造函数)

class A
{
public:
	A()
	{
		_a = 0;
	}
private:
	int _a;
};
class Date
{
public:
	void Print(/*Date* const this*/)
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	A _aa;
};
int main()
{
	Date d1;

	return 0;
}

这里_year , _month, _day 都是内置类型,所以初始化为随机值;而_aa是自定义类型,调用它的构造函数,初始化为0 。

这里再说一个容易被误解的概念:

很多人认为默认构造函数是编译器自动生成的构造函数,其实不是。

有3种构造函数都属于默认构造函数:

1、上面说的编译器自动生成的。

2、无参的。

3、参数全缺省的。(全缺省和无参的不能一起出现,会有二义性错误)

来看下面场景:

 这里为什么报错报的是不存在默认构造函数,因为我们自己写了构造函数,编译器不会自动生成;并且没有全缺省和无参的构造函数。

第二个默认成员函数———析构函数

 析构函数比较简单,它与构造函数相对,一个初始化,一个完成清理。

析构函数类似于之前学的free,它不是销毁对象,而是清理对象的空间。

与构造函数一样,对于内置类型,编译器自动调用的析构函数也不处理。对于自定义类型,则会调用它的默认析构。

在对象生命周期结束时,编译器会自动调用析构函数。如下这种情况需要注意:

typedef int DataType;
class Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack( )" << endl;
		_a = (DataType*)malloc(sizeof(DataType) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		cout << "~Stack( )" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

	void StackPush(){}
private:
	int _top;
	int _capacity;
	DataType* _a;
};

void func()
{
	Stack n;
}

int main()
{
	func();//对象生命周期到了,就调用析构函数
	Stack d1;
	return 0;
}

.如果将对象创建在func函数内部,随着调用func函数结束,对象生命周期结束,那么编译器就自动调用析构函数。

所以说,什么时候编译器调用析构函数,关键是看对象的生命周期结束。

构造函数与析构函数小结

一、

构造函数和析构函数需不需要我们自己写,取决于具体情况(编译器自动生成的方不方便使用)。

就算类里面都是自定义类型,编译器自动生成的构造函数会处理,有的情况下也需要我们自己写。

具体写不写,是看有没有需求。

二、

之前讲了,C++编译器对内置类型不处理是早期遗留下来的坑,但是因为语言向前兼容的特性,只能选择后期弥补而不好修改以前的东西。

因此,在C++11针对内置类型不处理问题发了一个补丁。

可以在类里面声明成员变量类型的时候,给成员变量一个缺省值(类似函数参数缺省值)

这样在编译器在调用自动生成的构造函数时,就会按照这个缺省值来初始化。

class Date
{
private:
	int _year =0;
	int _month = 0;
	int _day = 0;
};

int main()
{
	Date d1;
	return 0;
}

注意:这里是给缺省值,而不是直接初始化,不要将概念混淆了。

在自己写了构造函数的情况下,如果有的成员变量没有初始化,那也可以用缺省值初始化。

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

int main()
{
	Date d1;
	return 0;
}

 缺省值是好用,可这里毕竟是声明,不会开空间,它只会初始化,开空间还需要创建对象。

既然如此,那能不能这样:

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

    int *  _a = (int*) malloc( sizeof (int) );    能不能这样给缺省值?

有人说,既然是声明,不能开空间,那肯定不能这么写。

恰恰这里是可以的,这确实也是不太好理解的一个点。  但这里实质上还是给缺省值,不是在这个位置开空间。当对象创建后_a变量初始化时才会调用这里的malloc开空间,而没创建对象时只是给一个形式,说明一下给的缺省值的大小。

第三个默认成员函数———拷贝构造函数

构造函数是创建对象时的初始化,而拷贝构造函数可以理解为拷贝一个对象初始化的值。

 拷贝构造是构造函数的重载形式,并且它的参数必须是同类类型对象的引用

以日期类为例:

class Date
	{
	public:
		Date(int year = 1, int month = 1, int day = 1)
		{
			_year = year;
			_month = month;
			_day = day;
		}
		Date(const Date& d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
	private:
		int _year;
		int _month;
		int _day;
	};
	
	int main()
	{
		Date d1(2022,1,1);
		Date d2(d1);
		return 0;
	}

如上是日期类的写法。看一下细节:

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

		Date d1(2022,1,1);
        Date d2(d1);

创建d1对象后,想拷贝一份给d2,因为d1是自定义类型,调用拷贝构造函数Date,d是d1的别名,直接访问成员变量赋值给d2的成员变量。

这里都好理解,问题是如果不用引用,直接传值为什么不行?

 看报错:非法的复制(拷贝)构造函数。 如果不加引用,直接传值的话,会出现无限递归拷贝的情况。

将实参d1传值给形参d,需要拷贝一份d1给临时变量,临时变量再给到d,而由于d1是自定义类型, 拷贝d1又需要拷贝构造,那不成了无限递归了吗?会一直套娃下去。

 为什么传值就需要拷贝构造呢?以前写的很多函数都有传值传参啊。

这是因为内置类型(int、char)编译器很熟悉如何拷贝,需要拷贝多少字节。

但是这里是自定义类型,有很多难以直接拷贝的类型(如:链表、树),所以需要用到拷贝构造。

每一次实参传值给形参,都需要拷贝构造一份实参再继续下去,就形成了无限递归,因此编译器禁止这种行为。

用引用则不会产生这种情况,因为引用d相当于是d1的别名,直接操纵d1的成员变量。

此外,在引用前加const是为了防止前后位置写反(d._year = _year的情况)。这样编译器能检查出来。

拷贝构造函数也属于默认成员函数,自然也存在编译器自动生成的情况。

还是分内置类型和自定义类型讲解:

一、内置类型

内置类型是按字节方式拷贝的,类似memcpy函数。

以日期类为例:

class Date
{
public:
	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(2022, 1, 1);
	Date d2(d1);
	return 0;
}

我们没写拷贝构造函数,编译器自动生成的可以使用。

但是下面这种情况,就不行了:

typedef int DataType;
class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (DataType*)malloc(sizeof(DataType) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_top = 0;
		_capacity = capacity;
	}

	void Push(DataType x)
	{
		_a[_top++] = x;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	int _top;
	int _capacity;
	DataType* _a;
};

int main()
{
	Stack st1;
	st1.Push(1);
	st1.Push(2);

	Stack st2(st1);
	return 0;
}

 栈里面不写拷贝构造,编译器默认生成的就不行。

有的人会说,这里不是完成拷贝了吗,好像也没问题啊?

 当程序调试到free的时候,程序会崩溃,这里存在一大问题,拷贝的指针_a和st1的_a地址相同,

这意味着它们指向同一个地方。

 当对象生命周期结束,编译器调用析构函数~Stack() 清理空间时,先free的是st2,也就是st2指向的空间,此时还没有问题,

 _a确实置空了,free没问题。但是调用~Stack( )清理st1的空间时,因为st1,st2指向同一块空间,已经被清理过了,所以再清理的时候就崩溃了。

日期类的那种拷贝方式,叫做浅拷贝。它只拷贝表层的东西,也就是只将数值完全拷贝过去,而不管两个地方是否存在联系。

而栈这块需要的拷贝叫做深拷贝,它虽然拷贝了数值,但是拷贝者与被拷贝者是独立、互不干扰的,所指向的位置也是不一样的。

下面是深拷贝的实现:

Stack(const Stack& st)
	{
		_a = (DataType*)malloc(sizeof(DataType) * st._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		memcpy(_a, st._a, sizeof(DataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}

 深拷贝下,两者地址就不同了,不会产生上述错误。

根据经验总结:需要写析构函数~Stack( )的,需要深拷贝;

                         不需要写析构函数~Stack( )的,编译器默认的浅拷贝即可。

二、自定义类型

 自定义类型调用其自身拷贝构造函数(这里Stack的拷贝构造函数)完成拷贝,是不需要我们自己写的。

用自定义类型验证一下:

typedef int DataType;
class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (DataType*)malloc(sizeof(DataType) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_top = 0;
		_capacity = capacity;
	}
    

Stack(const Stack& st)
	{
		cout << "stack(const stack& st)" << endl;
		_a = (int*)malloc(sizeof(int) * st._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top = st._top;
		_capacity = st._capacity;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	int _top;
	int _capacity;
	DataType* _a;
};

class MyQueue {
public:
	void push(int x)
	{
		_pushST.Push(x);
	}
private:
	Stack _pushST;
	Stack _popST;
	size_t _size = 0;
};

int main()
{
	MyQueue q1;
	MyQueue q2(q1);

	return 0;
}

如果没有Stack的拷贝构造函数就会出错。

 MyQueue不需要写析构函数,因此也没必要写拷贝构造函数,编译器默认的足够。

所以,是否需要写拷贝构造函数,一是看是否需要写析构函数,而是看对象类型和具体场景。

第四个默认成员函数———赋值运算符重载

在看这个默认成员函数之前,先来理解一下运算符重载。

运算符重载

 对于内置类型,运算符可以直接使用(如:int a = 1,b = 2; a+b;)

如果是自定义类型想要直接使用运算符,就需要用到运算符重载(与函数重载没有关系)

以日期类为例,如果想比较创建的两个日期对象的年月日大小,或者计算10天以后的日期,就可以用这一块的运算符重载,不仅高效而且代码阅读性较强。

#include<iostream>
using namespace std;

class Date
{
public:
	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(2022,1,1);
	Date d2(2022,1,1);

	return 0;
}

一个熟悉的日期类,先从判断两个日期对象相等开始,d1 == d2,要写出它的运算符重载函数,

要先明确它的性质:

 判断是否相等,返回值类型可以用bool,函数名是operator后的运算符,参数必须是类类型的。

代码如下:

#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, 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;
}
int main()
{
	Date d1(2022,1,1);
	Date d2(2022,1,1);
	d1 == d2;
	return 0;
}

为了便于观察结果,可以写成打印的形式:

	cout<<(d1 == d2)<<endl;

 

 注意:这里一定要加括号(),如果不加,<<也是运算符,优先级高于 == ,cout会先和d1结合。

这里本质上是将d1 == d2转化成函数  operator==(d1, d2);

	cout << operator==(d1, d2) << endl;

所以可以写成显示调用,但是一般不这么写,为了增强代码阅读性,还是写成d1 == d2

运行程序,后会发现一个问题,

 成员函数是私有的,无法访问。将成员变量改为公有显然不是一个好方法。

这时我们可以将函数放到类里面,这样就不存在公有私有的问题了,但是此时又报错了,

 显示函数的参数多了,我们传参传两个,用两个参数接收怎么会多呢?————因为有隐含的this指针。这里正因为有这个隐含的this指针作了一个参数,所以会显示参数过多。

因此,我们拿掉一个参数,就传一个,this指针相当于d1,d就是d2的别名,代码如下:

	bool operator == (const Date& d)
	{
		return _year == d._year &&
			   _month == d._month &&
			   _day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1(2022,1,1);
	Date d2(2022,1,1);
	cout<<(d1 == d2)<<endl;     转化成 d1.operator==(d2)
	cout << d1.operator==(d2) << endl;     显示调用写法
	return 0;
}

PS:这里传参要引用传参,因为传值传参会多一个拷贝构造(自定义类型默认),并且比较两个对象不做修改,用const修饰更好。

等于写好了,看一下大于的写法吧(大同小异):

	bool operator>(const Date& d)
	{
		if (_year > d._year)
			return true;
		else if (_year == d._year && _month > d._month)
			return true;
		else if (_year == d._year && _month == d._month && _day > d._day)
			return true;
		else
		return false;
	}
private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1(2022,1,3);
	Date d2(2022,1,2);
	cout<<(d1 == d2)<<endl;

	cout << (d1 > d2) << endl;
	return 0;
}

类似的,也可以完成其他运算符重载。

注意:这里有一种简便写法,以大于等于>=为例,既然上面已经分别写好了等于和大于的重载,那么大于等于可以直接复用:

	bool operator>=(const Date& d)
	{
		return *this > d || *this == d;
	}

*this 相当于d1,d相当于d2,return d1> d2 || d1 == d2 直接调用了上面两个重载,用operator调用也可以。

比较的运算符重载完成后,我们来看一下 " +=" 是如何操作的。

首先,是函数名:operator+=     参数是int day     返回类型则是Date(目的是得到N天后的日期)

这里情况比较多,要分类讨论了:

 大致是这三种:1、只需改变日    2、日满进月    3、日满进月、月满进年

在日满进月的时候有个问题,每个月天数不一样,特别是2月还有平年闰年之分,因此,我们先写一个能得到月份天数的函数:

class Date
{
public:
	int GetMonthday(int year, int month)
	{
		static 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;
		else
			return monthday[month];
	}

这有很多写法,这种还是比较简单的。

接下来就是实现图中的逻辑了,如何进位:

Date& operator+=(int day)
	{
		_day += day;
		while (_day > GetMonthday(_year,_month))
		{
			_day -= GetMonthday(_year, _month);
			_month++;
			if (_month > 12)
			{
				_year++;
				_month -= 12;
			}
		}
		return *this;
	}

注意这里是引用返回,因为我们写的是 += ,会改变日期,如果不用引用返回,*this出作用域之前会先拷贝,用引用返回就省了拷贝。

那不改变日期的显然就是 " + " 了,那么就要用到拷贝构造,不需要引用返回了。

还是一样,复用+=的函数:

	Date operator+(int day)
	{
		Date ret(*this);
		ret += day;
		return ret;
	}

 int main()
{
	Date d2(2022,1,2);
	d2 + 1000;

	return 0;
}

拷贝构造一份d2给ret,调用上面的+=重载,返回ret,ret出了作用域就销毁,不改变d2.

其他运算符重载也是一样,重点是活用复用!没必要都写,比如写了 >= 就可以用 ' ! ' 得出 <

此外,因为在类里的成员函数都默认是内联函数,所以我们可以将不频繁调用的以及较长的函数声明和定义分离,以此避免内联。

以下是对一些常用运算符的重载————

Date.h

#pragma once

#include<iostream>
using namespace std;

class Date
{
//友元声明
friend istream& operator>>(istream& in, Date& d);
friend inline ostream& operator<<(ostream& out, const Date& d);
public:
	void PrintDate()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	// 赋值运算符重载
  // d3 = d2 -> d3.operator=(&d3, d2)
	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);

		// 获取某年某月的天数
		int GetMonthDay(int year, int month);

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

		// 日期+天数
		Date operator+(int day);

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

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

		// 前置++
		Date& operator++();

		// 后置++
		Date operator++(int);

		// 后置--
		Date operator--(int);

		// 前置--
		Date& operator--();

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

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


//输入日期
inline istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

//打印日期
inline ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

Date.cpp

#include"Test.h"

Date& Date::operator=(const Date& d)
{
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this;
	}
	return *this;
}

bool Date::operator==(const Date& d)
{
	return _year == d._year &&
		 _month == d._month &&
		 _day == d._day;
}

bool Date::operator != (const Date& d)
{
	//复用
	return !(*this == d);
}

bool Date::operator>(const Date& d)
{
		if (_year > d._year)
			return true;
		else if (_year == d._year && _month > d._month)
			return true;
		else if (_year == d._year && _month == d._month && _day > d._day)
			return true;
		else
		return false;
}

bool Date::operator >= (const Date& d)
{
	return *this > d || *this == d;
}

bool Date::operator < (const Date& d)
{
	return !(*this > d || *this == d);
}

bool Date::operator <= (const Date& d)
{
	return !(*this > d);
}

int Date::GetMonthDay(int year, int month)
{
	static 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& Date::operator+=(int day)
{
	_day += day;
	while(_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		++_month;
		if (_month > 12)
		{
			++_year;
			_month -= 12;
		}
	}
	return *this;
}

Date Date::operator+(int day)
{
	Date ret(*this);
	ret += day;
	return ret;
}

Date& Date::operator-=(int day)
{
	_day -= day;
	while (_day <= 0)
	{
		if(_month >= 2)
		_day += GetMonthDay(_year, _month-1);
		else
		{
			_day += GetMonthDay(_year-1, 12);
		}
		--_month;
		if (_month <= 0)
		{
			--_year;
			_month = 12;
		}
	}
	return *this;
}

Date Date::operator-(int day)
{
	Date ret(*this);
	ret -= day;
	return ret;
}

Date& Date::operator++()
{
	*this += 1;
	return *this;
}

Date Date::operator++(int)
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}

Date Date::operator--(int)
{
	Date tmp(*this);
	*this -= 1;
	return tmp;
}

Date& Date::operator--()
{
	*this -= 1;
	return *this;
}


//日期 - 日期
//方法一 :
int Date::operator-(const Date& d)
{
	Date max = d;
	Date min = *this;
	int flag = 1;
	if (max < min)
	{
		max = *this;
		min = d;
		flag = -1;
	}
	int tmon_day = 0;
	int subyear = d._year - (*this)._year;
	for (int i = min._year; i <= max._year; ++i)
	{
		if ((((i % 4 == 0) && (i % 100 != 0))
			|| (i % 400 == 0)) && flag == 1)
		{
			tmon_day++;
		}
		else if ((((i % 4 == 0) && (i % 100 != 0))
			|| (i % 400 == 0)) && flag == -1)
		{
			tmon_day--;
		}
	}
	int total_day = subyear * 365 + tmon_day;
	int MonthDay[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	for (int i = 1; i <= (*this)._month; ++i)
	{
		total_day -= MonthDay[i];
	}
	if ((*this)._month >= 2 &&
		(((*this)._year % 4 == 0) &&
		((*this)._year % 100 != 0))||
		((*this)._year % 400 == 0))
	{
		total_day -= 1;
	}
	total_day -= (*this)._day;

	for (int i = 1; i <= d._month; ++i)
	{
		total_day += MonthDay[i];
	}
	if (d._month >= 2 &&
		((d._year % 4 == 0) &&
			(d._year % 100 != 0)) ||
		(d._year % 400 == 0))
	{
		total_day -= 1;
	}
	total_day += d._day;
	int x = total_day * flag;
	return x;
}

//方法二 :
int Date::operator-(const Date& d)
{
	Date max = d;
	Date min = *this;
	int flag = 1;
	if (max < min)
	{
		max = *this;
		min = d;
		flag = -1;
	}

	int n = 0;
	while (min != max)
	{
		++n;
		++min;
	}
	return n * abs(flag);
}

 注意:有5个运算符不能重载 :   1 .*     2  ::       3  sizeof        4  ? :(三目操作符)      5  .

特别是第一个 .*   这不是只有一个 * 而是 .*   笔试中可能会问到,不要和解引用符号*混淆了。

还要注意一下前置++与后置++,为了区分它们,规定将其设置为函数重载,后置多给一个int参数,但是不使用,仅做区分。

后置++会比前置多拷贝两次,所以自定义类型应尽量使用前置++,内置类型则不会存在该问题,用两个中哪一个差别不大。

运算符重载讲完其实这个默认成员函数也基本讲完了,因为本质是一样的。

下面说一下赋值重载(d1=d2):

Date& Date::operator=(const Date& d)
{
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this;
	}
	return *this;
}

注意这里和拷贝构造的区别:

拷贝构造(Date d2(d1)) 是拷贝已经存在的d1,用d1去初始化d2,也就是说是在d2创建的过程中拷贝;   而赋值重载(d1=d2),d1 和 d2都是原本就存在已经创建好的,用d2的内容覆盖d1,不是初始化。

那 Date d1 =d2是属于哪一种呢?

————是拷贝构造!不能光看符号形式,要理解它的意义更符合哪一种。很明显这里是在初始化的过程(Date d1),所以是拷贝构造。

赋值重载一定要给返回值,不能是void,因为有可能会连续赋值,d1=d2=d3这种,将d2=d3的返回值再作为 = 的两个操作数,所以必须是Date&。

既然赋值重载是默认成员函数,那么也有编译器默认实现的情况,我们不写,会默认生成一个,也和拷贝构造一样,如果没有动态开辟空间,那么默认生成的赋值重载函数就可以使用,但是如果有动态开辟空间,那么就需要自己写了。

还是用栈那块来举例:

typedef int DataType;
class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (DataType*)malloc(sizeof(DataType) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_top = 0;
		_capacity = capacity;
	}

	void Push(DataType x)
	{
		_a[_top++] = x;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	int _top;
	int _capacity;
	DataType* _a;
};

现在我们没有写赋值重载函数,看看编译器自动生成的会怎样吧:

#include"stack.h"

int main()
{
	Stack st1;
	Stack st2;

	st2 = st1;
	return 0;
}

 

这里崩溃了,存在两大问题:

1、内存泄漏        2、free重复清理同一块区域

 这里的情况和拷贝构造类似:

首先创建出st1和st2 ,此时它们各自指向各自的空间,st1=st2,赋值重载,st1指向st2,st1自己的空间最后没人释放,st2的空间被自己和st1free了两次。

要解决这个问题,最好的方式是先free st1的空间,在重新按st2的空间大小开辟一块新空间拷贝。

	Stack& operator=(const Stack& st)
	{
		free(_a);
		_a = (DataType*)malloc(sizeof(DataType) * st._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_top = 0;
		_capacity = st._capacity;
	}

和拷贝构造代码几乎一样,就多了一个free st1的空间。

这样还有一个问题,当自己给自己赋值,st1=st1的时候,会出现拷贝随机值的问题。

因为开头的free会free  st1的空间(包括里面的东西),因此拷贝会出现随机值。

总结:与拷贝构造一样,赋值重载函数需要我们自己写的情况是:有开辟动态空间(malloc、free),需要写析构函数的情况下。

可以用编译器默认的情况则相反。

第五、六个成员函数————取地址重载

这两个函数非常简单,用的也很少,并且一般来说编译器自动会生成,很少存在需要我们自己实现的情况,这一块主要结合权限的放大缩小来理解。

日期类中,用print函数打印一个对象时,如果在初始化时加了const修饰,就将对象属性修改成了只读(原本是可读可写的)。

const Date d1;
d1.Print();

这时候传参类型就是 const Date*,而Print函数接收则是Date*  this,前者是只读,后者是可读可写,也就是权限扩大了,因此编译器会报错。

 此时想修改权限调整this指针,但是this指针规定是隐含的,无法显示操作,所以规定在函数后面加const表示对this指针的修饰。

	void PrintDate() const
	{
		cout << _year << " " << _month << " " << _day << endl;
	}

总而言之:权限可以平移和缩小,但是不能扩大。

                  调用函数的时候,const修饰的可以被const和非const调用,反过来就不行。

                  凡是内部不改变成员变量的(也就是*this对象数据的),这些成员函数都应该加const

像日期类中的比较类成员函数,+   -   ==    != 都可以加, 要改变成员变量的不能加,如 += , -=

取地址重载函数

这了解上面的基础上,我们来看最后两个默认成员函数,取地址重载。

	Date* operator&()
	{
		return this;
	}

	const Date* operator&() const
	{
		return this;
	}

非常简单,其实就是对取地址&运算符重载,上面的是没有加const修饰的对象,下面则是加了const修饰的对象。

 当然,一般不需要我们自己实现,编译器会实现的。

如果有特殊场景特别需求(比如不允许返回地址)需要我们自己实现一下。

以上就是关于6个默认成员函数的分享,谢谢大家支持!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值