【C++类与对象(中)】

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

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

class Student{};	//空类

在这里插入图片描述
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。(这里的“默认”和“缺省”的意思差不多,也就是你不写这6个函数,编译器会自动生成,你若是写了,则编译器就不生成了。)

2. 构造函数

2.1概念

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

 例如,以下日期类中的成员函数Date就是一个构造函数。当你用该日期类创建一个对象时,编译器会自动调用该构造函数对新创建的变量进行初始化。

class Date
{
public :
	Date(int year = 2024, 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;
};

 顺便也打印一下:
在这里插入图片描述

2.2特性

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

2.2.1语法特性:

在这里插入图片描述

 下面我们来认识一下无参构造函数有参构造函数以及一些细节。

class Date
{
public:
    // 1.无参构造函数
    Date()
    {}

    // 2.带参构造函数
    Date(int year, int month, int day)
    {
        _year = year;
        month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};

void TestDate()
{
    Date d1; // 调用无参构造函数
    Date d2(2015, 1, 1); // 调用带参的构造函数

    // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
    // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
    // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
    Date d3();
}

这里首先d1调用了无参构造函数
其次d2调用了带参构造函数

一般无参构造函数的调用样式如:Date d1对象后面不用跟括号!!!
有参函数的调用样式如:Date d2(2015, 1, 1)需要在对象的后面带上括号和参数。

Date d3() 这样的调用方式是错误的。
在C++中,如果使用圆括号,编译器会将其解释为函数声明而不是对象的创建
为了避免这种混淆,建议在创建对象时省略括号,或者使用等号进行直接初始化,如下所示:


Date d3;  // 推荐的方式,使用默认构造函数

Date d3{};  // 使用大括号初始化,C++11及更新的标准支持

2.2.2用法特性

在这里插入图片描述
1.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。

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

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

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

代码可以通过编译,因为编译器生成了一个无参的默认构造函数

class Date
{
public:
    
    // 如果用户显式定义了构造函数,编译器将不再生成
    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()
{  
        // 代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成无参构造函数,
        // 报错:error C2512: “Date”: 没有合适的默认构造函数可用
     Date d1;
    return 0;
}

在该 代码中,由于显式定义了一个有参数的构造函数,编译器将不再为你生成默认的无参构造函数。因此,当你尝试创建一个没有提供构造函数参数的Date对象时,编译器会报错,因为此时没有可用的默认构造函数。

你可以这样修改:

class Date
{
public:
    // 默认构造函数
    Date() : _year(0), _month(0), _day(0) {}

    // 有参数的构造函数
    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;
};

提供了一个默认构造函数,使得你可以创建没有参数的Date对象,同时也提供了有参数的构造函数用于进行对象的初始化。

2.不实现构造函数的情况下,编译器会生成默认的构造函数,其对内置类型不执行初始化操作,对自定义类型变量调用并执行初始化

 首先我们先了解一下:在C++中,数据类型可以分为内置类型(也称为基本类型)和自定义类型。

  • 内置类型(基本类型):这些是由语言本身提供的数据类型,如int、char、double等。这些类型的对象可以直接创建并使用,而不需要进行特殊的定义。
  • 自定义类型:这包括通过使用class、struct、union等关键字定义的用户自己创建的数据类型。这些类型允许程序员组织和封装数据定义自己的数据结构,以及添加成员函数来操作这些数据。
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

在这里插入图片描述
在这里插入图片描述

这里的Time _t是一个自定义类型,并且在Date d时程序对自定义类型变量调用并执行初始化

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

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

在这里插入图片描述

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

class Date
{
public:
	Date()
	{
		_year = 1900;
		_month = 1;
		_day = 1;
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
// 以下测试函数能通过编译吗?
void Test()
{
	Date d1;
}

在这里插入图片描述

当你在Test函数中创建Date对象 d1时,没有提供具体的参数。由于存在两个构造函数,编译器无法确定应该调用哪一个构造函数,故无法编译!


总结一下:
 在C++中,构造函数的概念可能在初学者阶段显得复杂,但理解构造函数的重要性和灵活性对于编写有效的类和对象是至关重要的。构造函数负责对象的初始化工作,确保对象在被创建时处于一个合适的状态。

虽然在我们不写的情况下,编译器会自动生成构造函数,但是编译器自动生成的构造函数可能达不到我们想要的效果,所以大多数情况下都需要我们自己写构造函数

默认构造函数是一个没有任何参数的构造函数。如果用户没有提供任何构造函数的话,C++ 编译器会为您自动生成一个默认构造函数。然而,一旦您提供了自定义的构造函数(无论是否有参数),编译器就不再生成默认构造函数。

3. 析构函数

3.1概念:

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

在我们上述的例子中,使用Date d1 实例化了一个对象,其中d1_year _month _day也相对于的进行了初始化。
d1这个对象生命周期结束时,这些局部变量 _year _month _day也会被编译器销毁。

但是对于像这样的数据结构,其在堆区开辟的空间无法在对象被销毁的同时被销毁,即当对象被销毁了,其动态开辟的空间仍然存在,这样就无法找到这块空间,从而造成了内存泄漏,这是不被允许,所以这就是析构函数存在的意义。

typedef int DataType;
class Stack
{
public :
	Stack(int capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * 3);
		if (_array == nullptr)
		{
			perror("malloc fail\n");
			return;
		}

		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
		_array[_size++] = data;
	}

	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = _size = 0;
			cout << this <<"~Stack()" << endl;
		}
	}

private:
	DataType* _array;
	int _size;
	int _capacity;
};

void TestStack()
{
	Stack s1;
	Stack s2(2);

	s1.Push(1);
	s1.Push(2);
	s1.Push(3);

	s2.Push(3);
}

int main()
{
	TestStack();
	return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

由上面的调试我们也可以知道:先构造的后析构,后构造的先析构
因为对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合先进后出的原则。
在这里插入图片描述

3.2特性

析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。

class Date
{
public:
	Date()// 构造函数
	{}
	~Date()// 析构函数
	{}
private:
	int _year;
	int _month;
	int _day;
};

2. 无参数无返回值类型。
 析构函数所谓的无返回值也是真的无返回值,而不是返回值为void。

3. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
 这就大大降低了C语言中栈空间忘记释放问题的发生,因为当栈对象生命周期结束时,C++编译器会自动调用析构函数对其栈空间进行释放。

4. 一个类有且只有一个析构函数。若未显示定义,系统会自动生成默认的析构函数编译器自动生成的析构函数机制:

  • 编译器自动生成的析构函数对内置类型不做处理。
  • 对于自定义类型,编译器会再去调用它们自己的默认析构函数

5.先构造的后析构,后构造的先析构

4. 拷贝构造函数

4.1概念

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

class Date
{
public :
	Date(int year = 2024, 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;
	}
	void Print()
	{
		cout<<_year<<'/'<<_month<< '/' << _day <<endl;

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

int main()
{
	Date d1;
	Date d2(d1);
	d1.Print();
	d2.Print();
	return 0;
}

4.2特征

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

  1. 拷贝构造函数是构造函数的一个重载形式。
    因为拷贝构造函数的函数名也与类名相同。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
class Date
{
public:
    Date(int year = 1900, 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;
    Date d2(d1);
    return 0;
}

如果写成Date d2(d1) 则会陷入死循环,无限递归。

在这里插入图片描述

在这里插入图片描述
注意自定义类型的对象进行函数传参时,一般推荐使用引用传参。使用传值传参也可以,但每次传参时都会调用拷贝构造函数。

  1. 若未显示定义拷贝构造函数,系统将生成默认的拷贝构造函数
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0, 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(2021, 5, 30);
	Date d2(d1); // 用已存在的对象d1创建对象d2
	d1.Print();
	d2.Print();
	return 0;
}

注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

  1. 编译器自动生成的拷贝构造函数不能实现深拷贝

 由上面的代码可知:我们在没有显示实现的情况下完成了拷贝,那我们还需要手动来显示实现拷贝函数吗?

先说明结论,是需要的。
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们将其称作浅拷贝(值拷贝)。
但某些场景下浅拷贝并不能达到我们想要的效果。

下面这个代码会出现崩溃:

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;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}

在这里插入图片描述

在这里插入图片描述

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

5. 赋值运算符重载

5.1运算符重载

 C++中的运算符重载是一种特性,允许程序员重新定义或重载已经存在的运算符,以适应自定义类型的操作。
 运算符重载是具有特殊函数名的函数,也具有其返回值类型函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。其目的就是让自定义类型可以像内置类型一样可以直接使用运算符进行操作。

a1 < a2;	//可读性高(书写简单)
CompareLess(a1,a2);	//可读性差(书写麻烦)
  1. 操作符重载的形式: 在C++中,通过使用关键字 operator,后接要重载的运算符,来定义重载函数。例如,operator+表示对加法运算符的重载。
  2. 语法格式: 通常,运算符重载函数是类的成员函数或友元函数。成员函数的格式如下:
      returnType operator OperatorSymbol (parameters);
	  bool operator==(const Date& d)	//返回值为bool
  1. 参数: 运算符重载函数的参数通常包括需要操作的对象及其他必要的参数。
  2. 返回类型: 运算符重载函数的返回类型通常是与运算符执行结果相关的类型。例如,对于加法运算符,通常返回相加后的结果。.

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this

这里以重载 == 运算符作为例子:
 我们可以将该运算符重载函数作为类的一个成员函数,此时该函数的第一个形参默认为this指针。

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
	bool operator==(const Date& d)// 运算符重载函数
	   {						  //该函数的第一个形参默认为this指针。
		return _year == d._year
			&&_month == d._month
			&&_day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

 同时当我们将该运算符重载函数放在类外面,但此时外部无法访问类中的成员变量,这时我们可以将类中的成员变量设置为共有(public),这样外部就可以访问该类的成员变量了(也可以用友元函数解决该问题)。并且在类外没有this指针,所以此时函数的形参我们必须显示的设置两个。

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
	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;
}

5.2赋值运算符重载

下面我们来看第一个赋值运算符函数:

class Date
{ 
public :
 Date(int year = 1900, 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;
   }
 
 Date& operator=(const Date& d)   //传递引用可以提高传参效率
 {
 if(this != &d)
       {
            _year = d._year;
            _month = d._month;
            _day = d._day;
       }
        
        return *this;
 }
private:
	int _year ;
	int _month ; 
	int _day ;
};

对于上面的代码,我们应该注意一下几点:

  1. Date& operator=(const Date& d) 这里使用了引用调用,引用调用用一下几个好处:
    (1). 避免拷贝开销: 传递引用而不是值可以避免在函数调用时进行对象的拷贝操作。如果使用 Date 而不是 const Date&,每次调用运算符重载函数时都会发生一次复制,而引用避免了这种开销。
    (2).不修改原对象: 使用 const 修饰的引用表示运算符重载函数不会修改传入的对象。这是一种良好的设计,因赋值操作本身不应该修改值。这符合良好的编程实践,即通过常量引用传递参数,确保函数不会对传入的参数进行修改
    (3).允许使用常量对象: 通过使用 const Date& d,你可以传递常量对象给运算符重载函数。这使得可以对常量对象进行加法操作,而不仅仅局限于非常量对象。

  2. if(this != &d) 这个的作用是检测是否自己给自己赋值,如果是自我赋值,就直接返回 *this,避免了不必要的操作。如果不是自我赋值,则执行成员变量的赋值操作。使用这样的检查可以提高代码的健壮性,防止在自我赋值时出现问题。

  3. return *this,返回 *this 是为了支持连续赋值,也被称为“链式赋值”。在函数体内我们只能通过this指针访问到左操作数。
    比如:Date d1, d2, d3; d1 = d2 = d3在这里,d3 被赋值给 d2,然后 d2 的值再赋值给 d1。这种形式的赋值表达式是通过返回 *this 实现的。

  4. 返回值类型:Date&,结合上面,return *this; 语句返回当前对象的引用,与return *this共同作用,完成连续赋值。

  5. 一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝

那通过上述的学习,下面这个代码可以正常运行吗?

#define _CRT_SECURE_NO_WARNINGS 1
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
	if (&left != &right)
	{
		left._year = right._year;
		left._month = right._month;
		left._day = right._day;
	}
	return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员

int main()
{

	return 0;
}

在这里插入图片描述

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

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

同样面临于析构函数一样的问题:如果类中未涉及到资源管理,赋值运算符是否需要显示实现都可以;一旦涉及到资源管理则必须要显示实现。

假设讲一个Stack类的对象s1赋值给s2,那么就会出现问题:
在这里插入图片描述
所以,如果类中涉及到资源管理,那么就需要程序员自己显示实现赋值运算符,否则将会出现上述的错误。


下面我们对这三行代码进行区别:

	Date d1(2021, 6, 1);
	Date d2(d1);
	Date d3 = d1;

这里一个三句代码,我们现在都知道第二句代码调用的是拷贝构造函数,那么第三句代码呢?调用的是哪一个函数?是赋值运算符重载函数吗?
其实第三句代码调用的也是拷贝构造函数,注意区分拷贝构造函数和赋值运算符重载函数的使用场景:
拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。
赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。


5.3前置++和后置++重载

 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载,
C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 前置++:返回+1之后的结果
	// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
	Date& operator++()
	{
		_day += 1;
		return *this;
	}
	// 后置++:
	// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
	// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
		// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
		//       而temp是临时对象,因此只能以值的方式返回,不能返回引用
		Date operator++(int)
	{
		Date temp(*this);
		_day += 1;
		return temp;
	}
		void Print()
		{
			cout << _year << '/' << _month << '/' << _day << endl;
		}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d;
	Date d1(2022, 1, 13);
	d = d1++;    // d: 2022,1,13   d1:2022,1,14
	d.Print();
	d1.Print();
	d = ++d1;    // d: 2022,1,15   d1:2022,1,15
	d.Print();
	d1.Print();
	
	return 0;
}

在这里插入图片描述

6.日期类的实现

在学习了以上知识之后,我们需要巩固下知识,所以下面我们来实现一个日期类,类似下图功能:
在这里插入图片描述

我们需要实现的功能有:日期加等天数、日期加天数、日期减等天数、日期减天数、日期前置自加、日期后置自加、日期前置自减、日期后置自减、已经各种的日期比较。

好了现在已经知道了要实现的功能,那如何实现呢?
我们是否需要显示的实现构造、析构、拷贝构造和赋值重载函数呢?

为了方便的在创建对象的时候完成实例化,那我们可以使用带有参数的构造函数创建对象。
其次我们这个类的成员都是基本数据类型,并不涉及资源管理,那么编译器默认生成的析构函数、拷贝构造函数和赋值运算符重载可以完成相对应的销毁,拷贝和赋值工作。故不显示实现这三个函数

class Date
{
public:
	// 构造函数
	Date(int year = 0, int month = 1, int day = 1);
	// 打印函数
	void Print() const;
	// 日期+=天数
	Date& operator+=(int day);
	// 日期+天数
	Date operator+(int day) const;
	// 日期-=天数
	Date& operator-=(int day);
	// 日期-天数
	Date operator-(int day) const;
	// 前置++
	Date& operator++();
	// 后置++
	Date operator++(int);
	// 前置--
	Date& operator--();
	// 后置--
	Date operator--(int);
	// 日期的大小关系比较
	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;
	// 日期-日期
	int operator-(const Date& d) const;

	// 析构,拷贝构造,赋值重载可以不写,使用默认生成的即可

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

我们首先来实现加等,即Date& operator+=(int day)

在实现之前,我们需要知道日期的相关规则,比如:每个月有多少天,闰年的相关规则等等
还有在计算加日期的时候需要考虑,相加后的day是否已经超过当月的day上限,若超过则需要month++,直至day满足要求,当month超过上限的时候year++…

这是一个有点复杂的过程,所以我们可以将其封装成一个函数,每次计算的时候调用该函数来匹配计算最终的日期。

int GetMonthDay(int year, int month)
	{
		assert(month > 0 && month < 13);
		static int monthDays[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 monthDays[month];
	}

GetMonthDay函数中的三个细节:
 1.该函数可能被多次调用,所以我们最好将其设置为内联函数。
 2.函数中存储每月天数的数组最好是用static修饰,存储在静态区,避免每次调用该函数都需要重新开辟数组。
 3.逻辑与应该先判断month == 2是否为真,因为当不是2月的时候我们不必判断是不是闰年。


下面来实现一下这个全缺省的构造函数:

	//Date.h
// 构造函数
	Date(int year = 1, int month = 1, int day = 1);

	//Date.cpp
	Date::Date(int year , int month , int day )
{
	_year = year;
	_month = month;
	_day = day;
}

注意:当函数声明和定义分开时,在声明时注明缺省参数,定义时不标出缺省参数。

6.1日期 += 天数

对于这个函数的实现,我们首先给_day加上day,然后通过判断GetMonthDay,最终返回该类对象。

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

6.2日期 + 天数

这个函数可以复用上面的+=,但是此对象的值不应该被改变,所以创建了一个临时变量,实现计算,最终返回这个临时变量的临时拷贝。

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

	tmp += day;

	return tmp;
}

在这里插入图片描述

我们在上述的代码中是通过复用+=来实现+,那么同样也可以通过复用+来实现+=:
在这里插入图片描述

所以,由上图可知通过复用+=来实现+的效率更好,所以后续也同样通过复用-=来实现-


6.3日期-=天数

与日期+=天数相类似,首先给_day减去day,再循环判断,得出结果并返回

// 日期-=天数
Date& Date::operator-=(int day)
{
	_day -= day;
	while (_day <= 0)
	{
		_month--;
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
		// 将 _day 的增加移到 while 循环之前
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}

6.4日期-天数

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

	return tmp;
}	

在这里插入图片描述

6.5前置++

直接复用+=

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

6.6后置++

复用前置++

// 后置++
Date Date::operator++(int)
{
	Date tmp = *this;
	++*this ;
	return tmp;
}

在这里插入图片描述

6.7前置–

复用-=

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

6.8后置–

复用前置–

// 后置--
Date Date::operator--(int)
{
	Date tmp = *this;
	--*this;
	return tmp;
}

在这里插入图片描述

6.9 日期的大小关系比较

日期类的大小关系比较需要重载的运算符看起来有6个,实际上我们只用实现两个就可以了,然后其他的通过复用这两个就可以实现。

注意:进行日期的大小比较,我们并不会改变传入对象的值,所以这6个运算符重载函数都应该被const所修饰。

逻辑比较简单,就不做过多的赘述了:

// 日期的大小关系比较
//  < 重载
bool Date::operator<(const Date& d) const
{
	if (_year < d._year)
	{
		return true;
	}
	else if (_year == d._year)
	{
		if (_month < d._month)
		{
			return true;
		}
		else if (_month == d._month)
		{
			if (_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);
}

//  == 重载
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);
}

6.10日期 - 日期

 日期 - 日期,即计算传入的两个日期相差的天数。我们只需要让较小的日期的天数一直加一,直到最后和较大的日期相等即可,这个过程中较小日期所加的总天数便是这两个日期之间差值的绝对值。若是第一个日期大于第二个日期,则返回这个差值的正值,若第一个日期小于第二个日期,则返回这个差值的负值。

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

	if (*this < d)
	{
		int flag = -1;
		max = d;
		min = *this;
	}
	int n = 0;
	while (min != max)
	{
		min++; 
		n++;
	}
	return n * flag;
}

在这里插入图片描述

7. const成员函数

 我们将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改。

例如,我们可以对类成员函数中的打印函数进行const修饰,避免在函数体内不小心修改了对象:

void Print()const// cosnt修饰的打印函数
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

思考下面几个问题(经典面试题):
1.const对象可以调用非const成员函数吗?
2.非const对象可以调用const成员函数吗?
3.const成员函数内可以调用其他的非const成员函数吗?
4.非cosnt成员函数内可以调用其他的cosnt成员函数吗?

答案是:不可以、可以、不可以、可以

解释如下:
 1.非const成员函数,即成员函数的this指针没有被const所修饰,我们传入一个被const修饰的对象,用没有被const修饰的this指针进行接收,属于权限的放大,函数调用失败。
 2.const成员函数,即成员函数的this指针被const所修饰,我们传入一个没有被const修饰的对象,用被const修饰的this指针进行接收,属于权限的缩小,函数调用成功。
 3.在一个被const所修饰的成员函数中调用其他没有被const所修饰的成员函数,也就是将一个被const修饰的this指针的值赋值给一个没有被const修饰的this指针,属于权限的放大,函数调用失败。
 4.在一个没有被const所修饰的成员函数中调用其他被const所修饰的成员函数,也就是将一个没有被const修饰的this指针的值赋值给一个被const修饰的this指针,属于权限的缩小,函数调用成功。

8.取地址及const取地址操作符重载

 取地址操作符重载和const取地址操作符重载,这两个默认成员函数一般不用自己重新定义,使用编译器自动生成的就行了:

class Date
{
public:
	Date* operator&()// 取地址操作符重载
	{
		return this;
	}
	const Date* operator&()const// const取地址操作符重载
	{
		return this;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值