C++类与对象(二)超详细

news2024/11/15 19:52:38

目录

1.类的6个默认成员函数

2..构造函数

2.1概念

2.2 特征

3.析构函数

3.1 概念

3.2 特性

4.拷贝构造函数

4.1 概念

4.2 特征

5.赋值运算符重载函数

5.1 运算符重载(是否重载这个运算符是看这个运算符对这个类是否有意义)

5.2 赋值运算符重载

6.const成员

7.取地址以及const取地址操作符重载

8.日期类的实现

Date.h

Date.cpp

 9 流插入和流提取运算符重载

9.1自定义类型流插入的实现

9.2 自定义类型流提取的实现


本次大纲内容:

1.类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员 函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

class Date {};

2..构造函数

2.1概念

对于以下Date类:

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;
}

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

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

2.2 特征

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

其特征如下:

1. 函数名与类名相同。

2. 无返回值(也不需要写void)。

3. 对象实例化时编译器自动调用对应的构造函数。

struct Stack
{
public:
	Stack(int capacity = 4)
	{
        cout << "Stack" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("Stack:;malloc");
			return;
		}

		_top = 0;
		_capacity = capacity;
	}

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

int main()
{
    Stack st1;
    
    return 0;
}

测试运行:

可以看到,确实是自动调用了

注:

4. 构造函数可以重载。

class Date
{
public:
	Date()
	{}

	Date(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.Print();
	Date d2;
	d2.Print();
	return 0;
}

可以看到,构造函数可以构成重载

5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦 用户显式定义编译器将不再生成。

class Date
{
public:

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:

	int _year;
	int _month;
	int _day;
};

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

可以看到函数还是随机值

这是因为C++规定编译器生成的默认构造函数,只会初始化构造类型,而不会初始化内置类型

6.关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会 生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默 认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的 默认构造函数并没有什么用??

解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类 型,如:int/char/double...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看 下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员 函数。

我们不写,编译器默认生成构造函数,内置类型不做处理,自定义类型会去调用它的默认构造函数

有些编译器也会处理内置函数,但是那是个性化处理,不是所有编译器都会处理

注:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

class Date
{
public:
	void Print()
	{
		cout << _year <<' ' << _month << ' ' << _day << endl;
	}
private:
	// 基本类型(内置类型)
	int _year = 1;
	int _month = 1;
	int _day = 1;
};
int main()
{
	Date d;
	d.Print();
	return 0;
}

tips:

 在类中给声明赋初始值,不是给它们初始化的意思,是给默认构造函数的形参赋予缺省值 

7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。

所以这两个只能留一个

结论:

  • 一般情况下,有内置类型成员的,就需要自己些构造函数,不能使用编译器生成的
  • 全部都是自定义类型可以考虑让编译器自己生成

3.析构函数

3.1 概念

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

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

3.2 特性

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

1. 析构函数名是在类名前加上字符 ~

2. 无参数无返回值类型。

struct Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("Stack:;malloc");
			return;
		}

		_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;
	return 0;
}

运行测试:

3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数(内置类型不做处理,自定义类型会去调用它的析构函数)。

注:析构函数不能重载

4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

结论:

  • 一般情况下,有动态申请资源,就需要显示写析构函数释放资源
  • 没有动态申请资源,不需要写析构函数
  • 需要释放资源的成员都是自定义类型,不需要写析构

4.拷贝构造函数

4.1 概念

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

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

4.2 特征

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

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

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

class Date
{
public:
	Date(const Date& d)
	{
		cout << "const Date& d" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

这里解释一下为什么拷贝构造函数中需要使用const Date& d这种形式来接收形参

为什么不能使用Date d呢?

  • 内置类型,不会去调用拷贝构造函数拷贝
  • 自定义类型,必须借用拷贝构造函数来完成拷贝

看图:

因为我们传的是自定义类型,当我们将d1传到函数Date的途中,d1就会被调用到新的拷贝构造函数中,重复前面的步骤,不断递归,最后肯定会栈溢出,但是编译器不允许这样传参,会直接报错

解决方法:

  • 传指针:任何类型的指针都是内置类型,拷贝时不会调用拷贝构造函数   
  • 传引用:直接使用d1的别名

这里建议使用引用,并且加个const(类中不受访问限定符的影响)

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

  • 编译器自动生成的默认拷贝构造函数,会对内置类型完成值拷贝/浅拷贝
  • 自定义类型,调用它的拷贝构造函数
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 9, 7);
	Date d2(d1);
	return 0;
}

对于这种日期类还是可以使用默认构造函数的

但是有另一种情况

因为默认拷贝构造函数是浅拷贝,遇到动态申请空间的情况就会出现问题

struct Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("Stack:;malloc");
			return;
		}

		_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;
	Stack st2(st1);
	return 0;
}

这里的问题:

(1)析构函数对同一个空间清理了两次,会报错

(2)改变一个空间的内容会对另外一个空间造成影响

如图:

指向同一块空间

这里使用浅拷贝肯定不行

这里栈得我们手动写一个深拷贝,后期我们会学到深拷贝来解决这里的问题

这里给大家浅浅讲一下深拷贝

深拷贝就是为了弥补浅拷贝的缺点而存在的

如图:

代码实现:

struct Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("Stack:;malloc");
			return;
		}

		_top = 0;
		_capacity = capacity;
	}

	//深拷贝需要自己手动实现
	Stack(const Stack& st1)
	{
		_a = (int*)malloc(sizeof(int) * st1._capacity);
		if (nullptr == _a)
		{
			perror("Stack::malloc");
			return;
		}

		memcpy(_a, st1._a, sizeof(int) * st1._top);
		_capacity = st1._capacity;
		_top = st1._top;
	}

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

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


int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

这里看几个情况:

(1).

1. void func1(Date& d);
2. void func2(Date d);
  • 如果没有动态资源的类类型,这上面两种形式肯定是第一个好,因为使用引用为形参在语法上是直接使用d的空间,不会去额外开辟栈帧,这样会减少消耗,加快程序运行效率

(2).

1.
Stack& func1()
{
    static Stack st;
    return st;
}

2.
Stack func1()
{
    Stack st;
    return st;
}

3.
Stcak& func1()
{
    Stack st;
    return st;
}

int main()
{
    Stack ret = func1();
    return 0;
}

(1).st出栈帧后不会被销毁,因为st存放在静态区,故而可以使用引用作为返回值

(2)(3).在2,3中只能使用3这种形式

  • 解释(2):调用func1函数,在func1函数中创建一个变量st,然后返回st变量,返回时编译器会产生一个临时变量来保存st的内容,这时就需要调用拷贝构造函数,将st的内容拷贝给这个临时变量,通过这个临时变量将st的内容给ret,因为是深拷贝,所以会增加空间的使用
  • 解释(3):因为st是临时变量出栈帧结束就销毁了,销毁之后st会调用析构函数来清理它的资源,因为是引用,所以不会产生临时变量,这时ret和st指向的内容相同,即编译器报错

5.赋值运算符重载函数

5.1 运算符重载(是否重载这个运算符是看这个运算符对这个类是否有意义)

C++为什么要加入运算符重载函数呢?

1. C++提供的一些运算符只能供 内置类型/基本类型使用,自定义类型使用不了,因为内置类型原本就是C++本来就有的,创造C++的大佬知道怎么实现它们的运算符,而自定义类型是我们自己定义的所以,需要我们自己实现,运算符重载就是 让自定义类型支持运算符

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

d1 == d2; //书写简单,可读性高
assignment(d1, d2); //书写复杂,可读性低

函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型 operator操作符(参数列表)

注:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数(自定义类型)
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .*  ::  sizeof  ?:  . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

下面我通过创建日期类来了解运算符重载函数

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

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

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

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

int main()
{
	Date d1(2024, 9, 10);
	Date d2(2024, 9, 10);

	//operator==(d1, d2);与下面那个效果一样
	int ret = d1 == d2;//自定义类型使用运算符
	cout << ret << endl;
	return 0;
}

这里我使用的是全局的运算符重载函数,但是有一定的缺陷,保证不了类的封装性,类中的对象全部公开了

注:operator==(d1, d2);和d1 == d2是一样的

我们看看底层实现(汇编代码):

这里有两个办法

1.将运算符重载函数写成日期类成员函数,写到类中不会受访问限定符的影响

2.在日期类中声明operator==为友元函数

第二个办法我们后面在讲,这里说明第一个方法

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

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

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

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

int main()
{
	Date d1(2024, 9, 10);
	Date d2(2024, 9, 10);

	//d1.operator==(d2);与下面那个效果一样
	int ret = d1 == d2;、
	cout << ret << endl;
	return 0;
}

:写成成员函数要注意operator==的有操作数是this指针,所以operator==函数参数只有一个,但实际上有两个,只是this指针不能显示在参数表和函数调用中(形参和实参)

5.2 赋值运算符重载

1. 赋值运算符重载格式

参数类型:const T&,传递引用可以提高传参效率

返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

检测是否自己给自己赋值

返回*this :要复合连续赋值的含义

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

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

		return *this;
	}

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

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

int main()
{
	Date d1(2024, 9, 10);
	Date d2;

	d2 = d1;
	d2.Print();

	return 0;
}

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数

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

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

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

// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& d1, const Date& d2)
{
	if (&d1 != &d2)
	{
		d1._year = d2._year;
		d1._month = d2._month;
		d1._day = d2._day;
	}

	return d1;
}

int main()
{
	Date d1(2024, 9, 10);
	Date d2;

	d2 = d1;
	d2.Print();

	return 0;
}

编译失败:error C2801: “operator =”必须是非静态成员

原因:赋值运算符如果不显式实现注:只是赋值运算符),编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注 意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符 重载完成赋值。

默认生成的赋值重载函数的作用和拷贝构造一样:

  1. 内置类型进行值拷贝/深拷贝
  2. 自定义类型调用它的赋值重载函数
class Time //我这里将std全展开了,如果写time会和库中的time冲突
{
public:
	Time(int hour = 1, int minute = 1, int second = 1)
	{
		_hour = hour;
		_minute = minute;
		_second = second;
	}
	Time& operator=(const Time& T)
	{
		if (this != &T)
		{
			_hour = T._hour;
			_minute = T._minute;
			_second = T._second;
		}
		
		return *this;
	}

//private:
	int _hour;
	int _minute;
	int _second;
};

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

	void Print()
	{
		cout << _year << ' ' << _month << ' ' << _day << endl;
		cout << _T._hour << ' ' << _T._minute << ' ' << _T._second << endl;
	}

private:
	int _year;
	int _month;
	int _day;

	Time _T;
};

int main()
{
	Date d1(2024, 9, 10);
	Date d2;

	d2 = d1;
	d2.Print();

	return 0;
}

  总结:

1.用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

2.赋值前检查是否在给自己赋值

C++支持d1 = d1这样赋值的情况,这种赋值是没有意义的,这种操作只会增加空间复杂度,所以我们可以在赋值函数中增加一个判断语句,如果是d1 = d1这种情况,返回d1就可以了

3.函数的返回值使用引用返回

实际上若是出现d2 = d1;这种情况我们没有必要有返回值,因为在函数内已经通过this指针改变了对象d2,但是为了支持连续赋值d3 = d2 = d1 ,我们就需要为函数设置一共返回值,显而易见的我们应该返回运算符的左操作数,即*this

如果返回的*this出作用域不会被销毁,那么为了避免不必要的消耗,最好使用引用返回

4.参数类型设置为引用,并使用const修饰

 赋值运算符重载函数第一个参数默认是this指针,第二个参数是运算符的右操作数

由于是自定义类型,我们若使用传值传参,这里系统就会自动调用一次拷贝构造函数,所以我们这里使用引用传递(第一个参数this是不显示的,我们管不了)

第二个操作数原本就是给第一个操作数赋值的,所以这里我们还能在它前面加上一个const防止它的数据被篡改

5.拷贝构造函数和赋值运算符

拷贝构造函数:用一个已经存在的对象初始化另一个对象

赋值运算符重载:两个存在对象之间的赋值拷贝

例:

int main()
{
    Date d1(2024, 9, 11);
    Date d2(d1);  //或 d2 = d1; 拷贝构造函数
    Date d3(2024, 9, 12);
    d3 = d1; // 或d3.operator(d1); 赋值运算符重载函数
    return 0;
}

6.const成员

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

 

class Date
{
public:
    void Print() const
    {
        cout << _year << "年" << _month 
        << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
}

实际编译器的处理,这段代码只是给予理解的,实际上this指针是不能显现的

这里其实是const Date* const this,但是为了好理解,写成 const Date* this

class Date
{
public:
    void Print(const Date *this) 
    {
        cout << _year << "年" << _month 
        << "月" << _day << "日" << endl;  
    }
private:
    int _year;
    int _month;
    int _day;
}

请思考下面的几个问题:

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

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

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

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

解释:

1. 我们将const对象传进非const函数中,函数中的this指针没有被const修饰,而我们传进来的形参是被const所修饰的不能随意篡改内容,但是一旦传进函数中,形参的权限就被放大了,不受const的限制了

2.将非const对象传进const成员函数中,属于权限的缩小,这样是可行的

3.在const成员函数内调用非const成员函数,是将被const修饰的指针this的值赋值给另一个非const成员函数,这属于权限的放大,不可行

4.在非const成员函数内调用const成员函数,是将指针this的值赋值给另一个const成员函数,这属于权限的缩小,是可行的

7.取地址以及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

class Date
{ 
public:
    Date* operator&()
    {
        return this ;
    }
    const Date* operator&()const
    {
         return this ;
    }
private :
    int _year ; // 年
    int _month ; // 月
    int _day ; // 日
};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需 要重载,比如想让别人获取到指定的内容!

特殊使用场景,让别人取不到对象的地址:

class Date
{ 
public:
    Date* operator&()
    {
        //return this;
          return nullptr;
    }

    const Date* operator&()const
    {
         return this;
    }
private :
    int _year ; // 年
    int _month ; // 月
    int _day ; // 日
};

8.日期类的实现

Date.h

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1);
    //获取这个月的天数
	int GetMonthDay(int _year, int _month);
    //日期 += 天数
	Date& operator+=(int day);
    //日期 -= 天数
	Date& operator-=(int day);
    //日期 + 天数
	Date operator+(int day) const;
    //日期 - 天数
	Date operator-(int day) const;
    //日期 == 日期
	bool operator==(const Date& d) const;
    //日期 >= 日期
	bool operator>=(const Date& d) const;
    //日期 > 日期
	bool operator>(const Date& d) const;
    //日期 <= 日期
	bool operator<=(const Date& d) const;
    //日期 < 日期
	bool operator<(const Date& d) const;
    //日期 != 日期
	bool operator!=(const Date& d) const;
    //日期 - 日期
    Date operator-(const Date& d);
    //前置++
    Date& operator++();
    //后置++
    Date operator++(int);
	void Print();

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

Date.cpp

Date::Date(int year, int month, int day)
{
	if (month > 0 && month < 13 
		&& day > 0 && day <= GetMonthDay(month, day))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "无效天数,请重新输入" << endl;
		assert(false);
	}
}

//获取这个月的天数
int Date::GetMonthDay(int year, int month)
{
	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& Date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= -day;
	}

	_day += day;
	while (_day > GetMonthDay(_year,_month))
	{
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
		_day -= GetMonthDay(_year, _month);
		++_month;
	}

	return *this;
}

//日期 -= 天数
Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += -day;
	}

	_day -= day;
	while (_day < 0)
	{
		_day += GetMonthDay(_year, _month);
		--_month;
		if (_month == 0)
		{
			--_year;
			_month = 12;
		}
	}

	return *this;
}

//日期 + 天数
Date Date::operator+(int day) const
{
	//Date tmp = *this;
	//tmp += 100;
	//return tmp;

	Date tmp = *this;
	tmp._day += day;
	while (tmp._day > GetMonthDay(tmp._year, tmp._month))
	{
		tmp._day -= GetMonthDay(tmp._year, tmp._month);
		++tmp._month;
		if (tmp._month == 13)
		{
			++tmp._year;
			tmp._month = 1;
		}
	}

	return tmp;

}

//日期 - 天数
Date Date::operator-(int day) const
{
	//Date tmp = *this;
	//tmp -= 100;
	//return tmp;

	Date tmp = *this;
	tmp._day -= day;

	while (tmp._day < 0)
	{
		tmp._day += GetMonthDay(tmp._year, tmp._month);
		--tmp._month;
		if (tmp._month == 0)
		{
			--tmp._year;
			tmp._month = 12;
		}
	}
	return tmp;
}

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

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

//日期 >= 日期
bool Date::operator>=(const Date& d) const
{
	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;
	}

	return false;
}

//日期 <= 日期
bool Date::operator<=(const Date& d) const
{
	return *this < d || *this == d;
}

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

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

//前置++,没有创建新的变量
Date& Date::operator++() 
{
	*this += 1;
	return *this;
}

//后置++
//创建了两个新对象,因为需要构成重载,所以在后置++上的参数表中需要带上int
//一般前置++用的多一些,所以在后置上加int
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;

	return tmp;
}

//日期 - 日期
Date Date::operator-(const Date& d)
{
	Date max = *this;
	Date min = d;
	int flag = 1;

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

	int n = 0;
	while (max != min)
	{
		++min;
		++n;
	}

	return n * flag;
}

void Date::Print()
{
	cout << _year << "年" << _month << "月" << _day << "日" << endl;
}

 9 流插入和流提取运算符重载

我们知道cin和cout是在iostream库中的

通过这张图可知cin是定义istream中,而cout定义在osteam中

看图:

这张图解释了

cout会自动识别内置类型,是因为它会去调用与之对应的运算符重载函数。

cout可以直接支持内置类型,是因为它已经在库中实现了

9.1自定义类型流插入的实现

class Date
{
public:
    void operator<<(ostream& out)
    {
	    out << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2024, 9, 10);
    //d1.operator<<(cout);
    d1 << cout;
    return 0;
}

但是这样看起来是不是很奇怪我们能不能将d1<<cout 改为 cout<<d1;

将operator<<函数改为全局函数

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

void operator<<(ostream& out,const Date& d)
{
	out << _year << "年" << _month << "月" << _day << "日" << endl;
}

int main()
{
    Date d1(2024, 9, 10);
    //d1.operator<<(cout);
    d1 << cout;
    return 0;
}

这里写成全局函数有两个要注意的点

1. 因为日期类中的成员变量是私有的,不能在类外使用

2. 写成全局函数,参数需要都显示出来

这里有两个解决方法:

(1). 我们可以将类成员变量写成函数 

例:

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

    int getyear()
    {
        return _year;
    }

    int getmonth()
    {
        return _month;
    }

    int getday()
    {
        return _day;
    }
private:
    int _year;
    int _month;
    int _day;
};

void operator<<(ostream& out,const Date& d)
{
	out << d.getyear() << "年" << d.getmonth() << "月" << d.getday() << "日" << endl;
}

int main()
{
    Date d1(2024, 9, 10);
    //cout.operator<<(d1);
    cout << d1;
    return 0;
}

(2).在类中声明友元函数

class Date
{
//声明友元函数
friend void operator<<(ostream& out, const Date& d);
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
		_year = year;
		_month = month;
		_day = day;
	}
private:
    int _year;
    int _month;
    int _day;
};

void operator<<(ostream& out,const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}

int main()
{
    Date d1(2024, 9, 10);
    //cout.operator<<(d1);
    cout << d1;
    return 0;
}

声明成友元函数就可以使用类中的资源了

cout连续打印:cout << d1 << d2 << d3; ,这时重载函数就需要一个返回值来完成这一操作

cout << d1 << d2 << d3;的顺序是  d1先输出,然后d2,d3。它们的返回值是cout 即:ostream

连续赋值的函数实现:

class Date
{
//声明友元函数,在类中哪定义都可以
friend ostream& operator<<(ostream& out, const Date& d);
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
		_year = year;
		_month = month;
		_day = day;
	}
private:
    int _year;
    int _month;
    int _day;
};

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

int main()
{
    Date d1(2024, 9, 10);
    Date d2, d3;
    cout << d1 << d2 << d3;

    return 0;
}

9.2 自定义类型流提取的实现

class Date
{
//声明友元函数,在类中哪定义都可以
friend istream operator>>(istream& in, Date& d);
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
		_year = year;
		_month = month;
		_day = day;
	}
private:
    int _year;
    int _month;
    int _day;
};

istream& operato>>(istream& in, Date& d)
{
    int year, month, day;
	in >> year >> month >> day;
    
    if(month > 0 && month < 13
    && day > 0 && day < d.GetMonthDay(year, month))
    {
        d._year = year;
        d._month = month;
        d._day = day;
    }

    return in;
}

int main()
{
    Date d1(2024, 9, 10);
    Date d2, d3;
    cin >> d1 >> d2 >> d3;
    cout << d1 < d2 << d3;

    return 0;
}

istream& operato>>(istream& in, Date& d);这条语句中的参数都不可以加const

in是向对象d中写数据,d不能加const

写数据的过程中会改变in中的一些状态值,in不能加const

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2138657.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

嵌入式单片机程序运行基本机理

1. 程序各种要素说明 大家好,今天用一个最简单的程序跟大家讲清楚程序的构成。 1.1. 概述 硬件首先要知道硬件的组成。 在前面章节我们说过,芯片包含Flash和RAM。 他们虽然不是相同的东西,但是都属于同一个地址空间,32位芯片的地址空间大小是4G。 比如ST32,FLASH通常从…

在 FlexSim 中使用 OpenUSD 分析、可视化和优化现实世界的流程

对于制造和工业企业而言&#xff0c;效率和精度至关重要。为了简化运营、降低成本和提高生产力&#xff0c;各公司正在转向数字孪生和离散事件模拟。 离散事件模拟使制造商能够通过试验不同的输入和行为来优化流程&#xff0c;这些输入和行为可以逐步进行建模和测试。 FlexSi…

基于Python实现的一个电影知识库QA系统

1. 实现效果 1. 图形展示 这是使用echarts.js 来实现的自定义页面的图谱展示&#xff0c;当然还有其他的库也能实现类似的效果&#xff0c;这里看各位的选择。 这里我在每个实体之间都实现了双层关系的绑定&#xff0c;这对于后面实现检索会有点帮助 2. 实体搜索展示 这里…

中断门+陷阱门

中断门&#xff1a; 中断描述符在IDT表里面 kd> dq idtr 80b95400 83e48e000008bfc0 83e48e000008c150 80b95410 0000850000580000 83e4ee000008c5c0 80b95420 83e4ee000008c748 83e48e000008c8a8 80b95430 83e48e000008ca1c 83e48e000008d018 80b95440 000085000050…

回溯-重新安排行程

1.排序 Collections.sort(list,(o1, o2)-> o1.get(0).compareTo(o2.get(0))); 2.返回值 3.往集合添加元素 Arrays.asList(元素) List<List<String>> list new ArrayList<>();List<String> path new ArrayList<>();// 将[["JFK"…

沉浸式体验和评测Meta最新超级大语言模型405B

2024年7月23日&#xff0c; 亚马逊云科技的AI模型托管平台Amazon Bedrock正式上线了Meta推出的超级参数量大语言模型 - Llama 3.1模型&#xff0c;小李哥也迫不及待去体验和试用了该模型&#xff0c;那这么多参数量的AI模型究竟强在哪里呢&#xff1f;Llama 3.1模型是Meta&…

idea激活页面怎么打开

打开Help------选择Register 然后就可以选择激活方式了

Vue2学习笔记(01计算属性和监视属性)

1、事件修饰符 2、计算属性-computed 要显示的数据不存在&#xff0c;要通过计算得来。在computed对象中定义计算属性。在页面中使用{{方法名}}来显示计算的结果。 3、监视属性-watch 通过vm对象的$watch()或watch配置来监视指定的属性当属性变化时,回调函数自动调用,在函数内…

Games101图形学笔记——光栅化

这里写目录标题 Rasterization光栅化屏幕空间隔行扫描三角形采样采样产生的问题反走样处理方法&#xff1a;采样前模糊 频率&#xff0c;时域傅里叶级数展开傅里叶变换 滤波高通滤波低通滤波 卷积卷积的一些定理 反走样MSAA&#xff08;Multisample Anti-Aliasing&#xff09;多…

C++_20_多态

多继承会造成 菱形继承** 使用虚继承来解决 不是给爷爷类加 也不是给子类加 是给父类加 虚基指针和虚基表 多态 概念&#xff1a; 概念&#xff1a; 一个事物的多种形态&#xff0c;简称多态 如&#xff1a; 对象的多态 ​ 张三 ​ 在对象面前 怂 ​ 在朋友面前 谄媚 ​ 在父…

Axure科技感大屏系统设计:智慧农场管理平台

在数字化转型的浪潮中&#xff0c;数据可视化作为连接现实世界与数字世界的桥梁&#xff0c;正以前所未有的速度改变着各行各业的面貌。智慧农业作为现代农业的重要发展方向&#xff0c;其管理平台的数据大屏设计尤为重要&#xff0c;它不仅是农场运营状况的直接展示窗口&#…

3. Python计算水仙花数

Python计算水仙花数 一、什么是水仙花数&#xff1f; 百度答案 二、怎样使用Python计算水仙花数&#xff1f; 这里需要for循环&#xff0c;if判断&#xff0c;需要range()函数&#xff0c;需要知道怎么求个位数&#xff0c;十位数&#xff0c;百位数… 1. For循环 语句结…

【Android Studio】API 29(即Android 10)或更高版本,在程序启动时检查相机权限,并在未获取该权限时请求它

文章目录 1. 在AndroidManifest.xml文件中&#xff0c;声明相机权限&#xff1a;2. 在你的Activity中&#xff08;例如MainActivity&#xff09;测试 1. 在AndroidManifest.xml文件中&#xff0c;声明相机权限&#xff1a; <uses-feature android:name"android.hardwar…

OS:初识操作系统——邂逅与启航

✨ Blog’s 主页: 白乐天_ξ( ✿&#xff1e;◡❛) &#x1f308; 个人Motto&#xff1a;实践是检验真理的唯一标准&#xff01;&#xff01;&#xff01; &#x1f4ab; 欢迎来到我的学习笔记&#xff01; 前言 各位uu好&#xff0c;现在我们要开始一个新的篇章——操作…

信息安全:守护数字世界的坚固堡垒

信息安全&#xff1a;守护数字世界的坚固堡垒 一、信息安全的重要性与意义 信息安全在个人、企业和社会层面都具有至关重要的意义。 在个人层面&#xff0c;信息安全保护隐私至关重要。在数字化时代&#xff0c;我们的个人信息如银行账户、社交媒体账户等容易受到威胁。一旦…

【代码随想录训练营第42期 续Day58打卡 - 图论Part8 - Dijkstra算法

目录 一、Dijkstra算法 实现方式 1、使用优先队列&#xff08;最小堆&#xff09; 2、朴素法&#xff08;简单数组&#xff09; 二、经典例题 题目&#xff1a;卡码网 47. 参加科学大会 题目链接 题解&#xff1a;朴素Dijkstra 三、小结 一、Dijkstra算法 刚入门Dijks…

【Git】常见命令(仅笔记)

文章目录 创建/初始化本地仓库添加本地仓库配置项提交文件查看仓库状态回退仓库查看日志分支删除文件暂存工作区代码远程仓库使用 .gitigore 文件让 git 不追踪一些文件标签 创建/初始化本地仓库 git init添加本地仓库配置项 git config -l #以列表形式显示配置项git config …

【免费】CISSP官方习题集第4版

伴随2004年4月CISSP新大纲发布&#xff0c;CISSP官方习题集第4版(OPT v4)已于2024年5月出版&#xff1a; 本人维护的中英对照8个知识域分章节练习已同步更新完成&#xff0c;在保持v3版内容基础上&#xff0c;增补了所有v4新内容&#xff0c;免费供考友们使用&#xff0c;访问方…

Requests-HTML模块怎样安装和使用?

要安装和使用Requests-HTML模块&#xff0c;您可以按照以下步骤进行操作&#xff1a; 打开命令行界面&#xff08;如Windows的命令提示符或Mac的终端&#xff09;。 使用pip命令安装Requests-HTML模块。在命令行中输入以下命令并按回车键执行&#xff1a; pip install request…

mp4文件解析

mp4格式与Annexb格式不同 实际送到GPU中解码数据是 00 00 01 + 编码数据,三个字节标识加解码数据 解析开始: for (uint32_t sampleIndex = 0; sampleIndex < track.sample_count; ++sampleIndex) {//----------------------------------------------------------…