目錄

引言

介紹

一、類的設計

 二、成員函數的實現

 🍃構造函數、析構函數、拷貝構造函數和賦值運算符重載

🍃友元函數:重載>>和<<

 🍃日期合法性檢查函數

🍃獲取某年某月的天數

🍃比較運算符重載

🍃 日期加減操作

🍃自增自減操作

🍃日期差計算

結語


引言

在編程的世界裏,類和對象是面向對象編程(OOP)的核心概念。它們為我們提供了一種組織和封裝數據及其相關操作的強大機制。類可以被視為一種藍圖或模板,它定義了對象的屬性和方法。而對象則是根據這些類創建的具體實例,它們具有自己的狀態(通過屬性表示)和行為(通過方法實現)。


日期處理是編程中常見且重要的一個領域。無論是在日誌記錄、事件管理,還是在日程安排中,日期都扮演着至關重要的角色。通過實現一個日期類,我們不僅可以深入理解類和對象的概念,還能將這些理論知識應用於解決實際問題。

介紹

本實踐將圍繞實現一個日期類展開,旨在通過這一具體案例來教授類和對象的基礎知識

我們將從定義類的基本結構開始,逐步添加屬性和方法,以構建一個功能完備的日期類。

首先,我們需要明確日期類應該具備哪些基本屬性。顯然,一個日期應該包含年、月和日這三個關鍵信息。這些屬性將用於表示日期的狀態。


接下來,我們將為日期類定義一系列方法。這些方法將實現日期的各種操作,如設置日期、獲取日期、計算兩個日期之間的天數差、判斷一個日期是否是閏年等。通過這些方法,日期類將具有自己的行為,能夠根據需要進行各種計算和操作。


在實現日期類的過程中,我們還將涉及到一些面向對象編程的高級概念,如封裝、繼承和多態。封裝將幫助我們隱藏類的內部實現細節,只暴露必要的接口給外部使用。雖然在這個簡單的日期類示例中可能不會直接用到繼承和多態,但瞭解這些概念將有助於我們更深入地理解面向對象編程的精髓。

一、類的設計

首先,我們需要設計一個日期類Date,包含年、月、日三個私有成員變量,並定義多個成員函數來實現日期的各種操作。

以下是類的聲明部分:

#include<iostream>  
#include<stdbool.h>  
using namespace std;  
  
class Date {  
public:  
    // 友元函數聲明  
    friend ostream& operator<<(ostream& out, const Date& d);  
    friend istream& operator>>(istream& in, Date& d);  
  
    // 檢查日期合法性  
    bool CheckDate() const;  
  
    // 獲取某年某月的天數  
    int GetMonthDay(int year, int month) const;  
  
    // 構造函數  
    Date(int year = 1900, int month = 1, int day = 1);  
  
    // 拷貝構造函數  
    Date(const Date& d);  
  
    // 賦值運算符重載  
    Date& operator=(const Date& d);  
  
    // 析構函數  
    ~Date();  
  
    // 日期+=天數  
    Date& operator+=(int day);  
  
    // 日期+天數  
    Date operator+(int day) const;  
  
    // 日期-天數  
    Date operator-(int day) const;  
  
    // 日期-=天數  
    Date& operator-=(int day);  
  
    // 前置++  
    Date& operator++();  
  
    // 後置++  
    Date operator++(int);  
  
    // 後置--  
    Date operator--(int);  
  
    // 前置--  
    Date& operator--();  
  
    // >運算符重載  
    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::Date(int year, int month, int day) {  
    _year = year;  
    _month = month;  
    _day = day;  
}  
  
Date::Date(const Date& d) {  
    _year = d._year;  
    _month = d._month;  
    _day = d._day;  
}  
  
Date& Date::operator=(const Date& d) {  
    if (this != &d) {  
        _year = d._year;  
        _month = d._month;  
        _day = d._day;  
    }  
    return *this;  
}  
  
Date::~Date() {}

解析:

  • 這四個成員函數都屬於類的默認成員函數,是實現類的第一步
  • 日期類中除了構造函數需要自己單獨實現之外,其他三個可使用編譯器實現的默認成員函數即可。
  • 示例代碼中在聲明中給出了全缺省構造函數(全缺省構造函數是最穩妥的選擇,可以適應各種情況),要注意帶缺省參數聲明和定義分離時,只在聲明處給出缺省值
  • 更多默認成員函數知識可參考類和對象系列文章

🍃友元函數:重載>>和<<

//運算符重載
ostream& operator<<(ostream& out,const Date& d)
{
 out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
 return out;//方便連續使用
}

//運算符重載
istream& operator>>(istream& in, Date& d) 
{
 cout << "請輸入年 月 日" << endl;
 while (1)
 {
	 in >> d._year >> d._month >> d._day;
	 if (d.CheckDate())
		 break;
	 cout << "輸入的日期非法,請重新輸入!!!" << endl;
 }
 return in;
}

為什麼要重載成友元函數(全局函數)而不是成員函數?

void Date:: operator<<(ostream& out)//錯誤示範
 {
	 out << this->_year << "-" << this->_month << "-" << this->_day;
}

如果我們重載成成員函數,運算符重載默認第一個參數為隱藏的this指針指向對象,第二個參數才是cout對象。那麼想要使用cout打印對象內容時,就變成了下面這樣的形式,這明顯不符合我們的要求 

Date() << cout  ;

 只有改變兩個對象的位置,讓std::ostream對象的引用(std::ostream&)作為第一個參數才能解決,但this指針是隱藏的,我們無法修改。所有該問題只有通過重載成全局函數才能解決

為什麼要有返回值?

對於輸出運算符(<<),重載函數通常返回一個對std::ostream對象的引用(std::ostream&)。這樣做允許我們進行鏈式操作,即將多個輸出操作連接在一起,而不需要在每個操作後都調用std::endl或類似的分隔符。例如,std::cout << obj1 << obj2; 將首先輸出obj1的狀態,然後緊接着輸出obj2的狀態

 🍃日期合法性檢查函數

bool Date::CheckDate()const
{
	if (_day > GetMonthDay(this->_year, this->_month) || _month > 12)
		return false;
	return true;
}

數據合法性檢查是十分必要的 

🍃獲取某年某月的天數

int Date:: GetMonthDay(int year, int month)const
{
	static int getdays[] = { 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 getdays[month] + 1;//閏年二月天數+1
	}
	return getdays[month];
}

  • 考慮到天數的複雜性,每個月的天數都不一樣,並且還存在閏年的2月份有29,邏輯複雜必須封裝成函數實現
  • 將每個月的天數寫在數組裏,並空出第一個位置來,這樣數組下標就能與月份一一對應
  • 獲取月份的天數之後,再判斷是否是2月以及是否是閏年再做進一步處理
  • 注意要先判斷是否是2月,再判斷是否是閏年,如果左側不成立右側就不會計算 
  • 獲取天數的函數這種短小但頻繁調用的函數更建議寫成內聯函數,也就是在類內定義
  • 另外數組getdays建議寫成靜態,可以省去每次調用函數都要創建數組,提高效率

🍃比較運算符重載

// >運算符重載
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;//上述條件都沒有執行,就返回false
}

// ==運算符重載
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 || *this == d;//複用大於和等於
}

// <運算符重載
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對象之間的“大於”比較。如果當前對象在任何一級比較中大於參數對象,就返回true;否則,返回false
  • 比較運算符中只需要實現一個大於或者小於運算符,和一個==運算符,其他的函數皆可複用代碼

🍃 日期加減操作

// 日期+=天數
Date& Date::operator+=(int day)
{
 _day += day;
 int tmp = GetMonthDay(this->_year, this->_month);
 while (_day > tmp)//日期大於本月天數
 {
	 _day -= tmp;//就減去本月天數
	 ++_month;//月份+1
	 if (_month > 12)//月份大於12
	 {
		 ++_year;//年份+1,月份置為1
		 _month = 1;
	 }
	 tmp = GetMonthDay(this->_year, this->_month);
 }
 return *this;
}

// 日期+天數
Date Date::operator+(int day)const
{
 Date tmp = *this;//複用+=代碼
 tmp += day;
 return tmp;
}

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

// 日期-=天數
Date& Date::operator-=(int day)
{
 _day -= day;
 while (_day < 1)
 {
	 --_month;//向前借位
	 if (_month == 0)
	 {
		 --_year;
		 _month = 12;
	 }
	 _day+= GetMonthDay(this->_year, this->_month);

 }
 return *this;
}

+=實現思路:

  1. 增加天數:將傳入的天數day加到當前日期的天數_day上。
  2. 檢查天數是否超出本月:使用GetMonthDay函數獲取當前年份和月份的天數tmp。如果增加後的天數_day大於tmp,説明天數超出了當前月份的天數。
  3. 調整日期:如果天數超出,則減去當前月份的天數,並將月份_month加1。如果月份超過12(即一年結束),則將年份_year加1,並將月份重置為1。
  4. 重複檢查:重複上述步驟,直到_day不大於當前月份的天數為止。
  5. 返回當前對象:返回對當前對象的引用,以支持鏈式操作。

+可以直接複用+=的代碼 

注意:

為什麼不是先實現+ 再讓+=複用代碼呢?

+=的獨立實現不需要拷貝,而+複用+=需要兩次拷貝

+的獨立實現需要兩次拷貝,而+=複用需要一次自己的拷貝再加上+函數的兩次拷貝

相比之下,先實現+=再實現+,具有更高的效率

-=實現思路:

  1. 減少天數:將傳入的天數day從當前日期的天數_day中減去。
  2. 檢查天數是否小於1:如果減少後的天數_day小於1,説明天數不足以滿足當前月份,需要向前借位。
  3. 調整日期:如果天數小於1,則月份_month減1。如果月份減到0(即一年開始之前),則將年份_year減1,並將月份重置為12。然後,使用GetMonthDay函數獲取新的月份的天數,並加到_day上。
  4. 重複檢查:重複上述步驟,直到_day不小於1為止。
  5. 返回當前對象:返回對當前對象的引用,以支持鏈式操作。

-可以直接複用-=的代碼

🍃自增自減操作

可以直接複用+=和-=的代碼完成++和--函數

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

🍃日期差計算

// 日期-日期 返回天數
int Date::operator-(const Date& d)const
{
 int flag = 1;
 int count = 0;
 Date max = *this;//假設this大,d小
 Date min = d;

 if (max < min)//假設不對,則更改
 {
	 max = d;
	 min = *this;
	 flag = -1;
 }
 while (min != max)//使用計數器計算
 {
	 ++min;
	 ++count;
 }
 return flag * count;
}

 思路解析:

  1. 變量初始化
  • flag:用於標記日期的先後順序,以便確定最終結果的符號(正或負)。
  • count:用於累加兩個日期之間的天數差。
  • maxmin:分別用於存儲較大的日期和較小的日期,以便後續計算天數差。
  1. 假設與賦值
  • 假設當前對象(*this)表示的日期大於傳入的日期d,因此將當前對象賦值給max,將d賦值給min
  • 如果假設不成立(即*this表示的日期小於d),則交換maxmin的賦值,並將flag設置為-1,表示最終的天數差應為負數。
  1. 計算天數差
  • 使用一個循環,每次循環將min表示的日期加1天,並累加count的值。
  • 循環繼續,直到minmax表示的日期相等為止。此時,count的值即為兩個日期之間的天數差。
  1. 返回結果
  • 根據flag的值,返回count-count作為兩個日期之間的天數差。如果flag為1(即初始假設成立),則返回count;如果flag為-1(即初始假設不成立),則返回-count
  • 這種實現方式在日期相差較大時可能效率較低,因為它通過逐天累加來計算天數差
  • 但相對來説邏輯是比較簡單的,直接計算的方法都比較複雜,涉及到不同的月份天數和閏年,因為天數的計算相對來説數據是比較小的,這些計算量在CPU面前還是小意思的

日期+日期沒有實際意義,所有這裏不進行運算符重載

結語

通過實現一個日期類,我們不僅能夠掌握類和對象的基礎知識,還能將這些知識應用於解決實際問題。同時,這一實踐還將為我們後續學習更復雜的面向對象編程概念打下堅實的基礎。