一.前言
通過前面對類和對象的學習,現在我們可以開始實踐日期類的代碼編寫。在實際操作過程中,我會補充之前文章中未提及的相關知識點。
二.正文?
1. 日期類代碼實現
我們先來看看要實現什么功能吧,把他放在Date.h中
#pragma once
#include<iostream>
using namespace std;class Date
{
public:// 全缺省的構造函數Date(int year = 2025, int month = 7, int day = 7);void Print(){cout << _year << "-" << _month << "-" << _day << endl;}// 拷貝構造函數Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}// 運算符重載bool operator<(const Date& x);bool operator==(const Date& x);bool operator<=(const Date& x);bool operator>(const Date& x);bool operator>=(const Date& x);bool operator!=(const Date& x);// 獲取某年某月的天數int GetMonthDay(int year, int month);// 日期+=天數Date& operator+=(int day);// 日期+天數Date operator+(int day);// 日期-=天數Date& operator-=(int day);// 日期-天數Date operator-(int day);// 日期-日期 返回天數int operator-(const Date& x);// 前置++Date& operator++();// 后置++Date operator++(int);// 前置--Date& operator--();// 后置--Date operator--(int);private:int _year;int _month;int _day;
};
繼續像之前一下在準備兩個文件,Date.cpp和test.cpp
?1.構造函數
類的對象在創建時會自動調用構造函數,若未顯式定義,編譯器會生成默認構造函數( Date() ),但默認構造函數不會初始化成員變量。若成員變量未初始化,會出現年月日隨機值,會導致對象狀態無意義,后續操作必然出錯。
Date::Date(int year, int month, int day)
{if (month >= 1 && month <= 12 && day >= 1 && day <= GetMonthDay(year, month)){_year = year;_month = month;_day = day;}else{cout << "非法日期" << endl;}
}
為什么博主選擇全缺省參數的構造函數了?
1.構造對象更靈活:支持多種初始化方式?
Date d1; Date d2(2025); Date d3(2025, 7); Date d4(2025, 7, 8);
都可以初始化
2.簡化接口設計:減少構造函數重載
非全缺省構造函數需要重載
Date();//無參構造(默認日期) Date(int year);//僅傳年份 Date(int year, int month);//傳年份和月份 Date(int year, int month, int day); //全參數構造
而全缺省構造函數只需要一個
3.增強代碼可維護性:默認值統一管理
這時候有人好奇了,那析構函數要嗎?
不需要的,原因如下:
成員變量無動態資源
存儲在棧上或類對象的內存空間中,生命周期結束時會被系統自動釋放,無需手動處理。但是當遇到開辟了動態空間,則需要大家自己寫析構函數了。?
?2.打印?
為了方便后續的檢查代碼正確性,寫一個打印更方便看到結果。又因為他很簡短可以直接放在類里面。
void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
3.拷貝構造函數?
可以直接寫在類里面
// 拷貝構造函數Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}
或者類里面聲明,類外面實現如下:
class Date
{
public://省略其他的功能Date(const Date& d);
private:int _year;int _month;int _day;
};Date::Date(const Date& d)
{_year = d._year;_month = d._month;_day = d._day;
}
來看看運行結果吧:
4.關系運算符重載?
要寫一系列 >? >=? < <= ==? !=關系運算符,我們可以先寫一組 < 和 == ,或者 > 和 ==,然后就可以用復用。
// <運算符重載
bool Date::operator<(const Date& x)
{if (_year < x._year)return true;else if (_year == x._year && _month < x._month)return true;else if (_year == x._year && _month == x._month && _day < x._day)return true;return false;
}
//==運算符重載
bool Date::operator==(const Date& x)
{return _year == x._year&& _month == x._month&& _day == x._day;
}
然后就可以復用了
// <=運算符重載:直接調用上面寫的那兩個,滿足其中一個就好了,所以 ||
bool Date::operator<=(const Date& x)
{return *this < x || *this == x;
}
// < 運算符重載:可直接調用上一個復用
bool Date::operator>(const Date& x)
{return !(*this <= x);
}
// >=運算符重載
bool Date::operator>=(const Date& x)
{return !(*this < x);
}
// !=運算符重載
bool Date::operator!=(const Date& x)
{return !(*this == x);
}
看看運行結果:?
?5.算數運算符
日期加天數
Date& Date::operator+=(int day)
{if (day < 0){return *this -= -day;}_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;if (_month == 13){_year++;_month = 1;}}return *this;
}
Date Date::operator+(int day)
{Date tmp(*this);tmp += day;return tmp;
}
因為可以連續賦值,所以要有返回值,又因為+=改變了本身,所以用Date&,返回*this就好了。 而+不改變本身,所以要先定義一個tmp然后返回tmp。
?+的代碼不調用一個為
Date Date::operator+(int day) {Date tmp(*this);if (day < 0){return tmp - (-day);}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; }
為什么是+調用+=而不是+=調用加啦?
1.使用 operator+= ?調用 operator+ ?形成 operator+= ?函數
Date& Date::operator+=(int day) {*this = *this + day;return *this; }
在 Date& Date::operator+=(int day) ?函數中,調用 operator+ ?時會創建一個臨時對象(因為 operator+ ?通常會返回一個新對象),然后將這個臨時對象賦值給 *this 。*this + day ?會創建一個臨時對象,之后再將其內容復制給當前對象,這就產生了額外的對象創建和銷毀開銷。建立了兩個臨時變量。
2.使用 operator+ ?調用 operator+= ?形成 operator+ ?函數
Date Date::operator+(int day) {Date tmp(*this);tmp += day;return tmp; }
在 Date Date::operator+(int day) ?函數中,僅創建了一個臨時對象 tmp ,然后調用 operator+= ?直接在這個臨時對象上進行日期累加操作。這里只創建了一個 tmp ?對象用于存儲計算結果。后續不需要再創建額外的臨時對象來完成加法操作。
總結:
"使用 operator+ 調用 operator+= 形成 operator+ "函數的方式更好,它能減少臨時對象創建、避免多次內存分配與釋放、利用編譯器優化,提高代碼執行效率。
?日期減天數
Date& Date::operator-=(int day)
{if (day < 0){return *this += -day;}_day -= day;while (_day <= 0){--_month;_day += GetMonthDay(_year, _month);if (_month == 0){--_year;_month = 12;}}return *this;
}
Date Date::operator-(int day)
{Date tmp(*this);tmp -= day;return tmp;
}
因為可以連續賦值,所以要有返回值,又因為-=改變了本身,所以用Date&,返回*this就好了。 而-不改變本身,所以要先定義一個tmp然后返回tmp。?
? -的代碼不調用一個為
Date Date::operator-(int day) {Date tmp(*this);if (day < 0){return tmp + (-day);}tmp._day -= day;while (tmp._day <= 0){--tmp._month;if (tmp._month == 0){--tmp._year;tmp._month = 12;}tmp._day += GetMonthDay(tmp._year, tmp._month);}return tmp; }
1.使用 operator-= ?調用 operator-? 形成 operator-= ?函數
Date& Date::operator-=(int day) {*this = *this - day;return *this; }
2.使用 operator-? 調用 operator-= ?形成 operator-? 函數
Date Date::operator-(int day) {Date tmp(*this);tmp -= day;return tmp; }
為什么博主使用"使用 operator-?調用 operator-= 形成 operator-?"呢?理由也就是上面一樣了。
日期減日期?
int Date::operator-(const Date& x)
{Date min = *this;Date max = x;int flag = 1;if (*this > x){min = x;max = *this;flag = -1;}int dayCount = 0;while (min < max){min++;dayCount++;}return dayCount * flag;
}
這時候有人可能好奇為什么要定義一個flag?
因為如果當前對象(*this)小于x則flag為正,然后乘dayCount則為正,即表示x在當前對象之后,相差dayCount天
而若當前對象比x大則flag為負,然后乘dayCount則為負,即表示當前對象在x之后,相差dayCount天
看看運行結果:??
6.自增自減運算符?
前置++
Date& Date::operator++()
{*this += 1;return *this;
}
?后置++
Date Date::operator++(int)
{Date tmp(*this);*this += 1;return tmp;
}
前置--
Date& Date::operator--()
{*this -= 1;return *this;
}
后置--?
Date Date::operator--(int)
{Date tmp(*this);*this -= 1;return tmp;
}
后置++和后置--括號中的int無實際意義只是為了構成重載,易于區分
前置版本(?operator++()?、?operator--()?):先加加后使用,先修改當前對象,再返回自身引用(?*this?)
后置版本(?operator++(int)?、?operator--(int)?):先使用后加加,先復制當前對象(?tmp?),再修改自身,最后返回修改前的副本。?
運行結果:
上述就是日期類的大致代碼,但是我們還能進行優化,讓我們繼續學習吧。
2.日期類代碼的優化
1.const成員?
首先來學習一下const成員
const成員函數:用const修飾的“成員函數”,const修飾類成員函數,實際修飾該成員函數隱含的this指針,表明在該成員函數中不能對類的任何成員進行修改。
因為Print不改變任何成員使用可以加,大家伙可以想想我們上面的日期類代碼還有哪里可以加?
若聲明和定義分開 ,兩個都得寫,如下:
我給大家吧要加的寫出來
void Print() const
{cout << _year << "-" << _month << "-" << _day << endl;
}bool operator<(const Date& x) const;
bool operator==(const Date& x) const;
bool operator<=(const Date& x) const;
bool operator>(const Date& x) const;
bool operator>=(const Date& x) const;
bool operator!=(const Date& x) const;Date operator+(int day) const;
Date operator-(int day) const;
int operator-(const Date& x) const;
?因為他們不改變任何成員使用可以加。
可能有人好奇為什么要加?又得判斷還可能加錯。
因為有些使用的人初始化會寫成 const Date d2(2025, 11, 22);
使用const是有一定好處的。
只要成員內部不修改成員變量,都應該加const,這樣const對象和普通對象都可以調用。權限可以縮小和轉移,不能放大。
來看看幾題思考題
1. const對象可以調用非const成員函數嗎?
不可以。
const對象表示其狀態不能被改變。非const成員函數有可能會修改對象的數據成員(因為其沒有承諾不修改對象狀態),若允許const對象調用非const成員函數,就可能違背const對象不可變的特性。
class MyClass { public:void nonConstFunc() {data = 10; // 可能修改對象數據成員} private:int data; }; const MyClass obj; obj.nonConstFunc(); // 編譯錯誤,const對象不能調用非const成員函數
2. 非const對象可以調用const成員函數嗎?
可以。
const成員函數承諾不會修改對象的數據成員,對于非const對象而言,調用const成員函數不會有違背其可變性的問題,而且這也提供了一種在不同場景下靈活調用函數的方式。
class MyClass { public:void constFunc() const {// 這里不會修改對象數據成員} }; MyClass obj; obj.constFunc(); // 合法,非const對象可以調用const成員函數
3. const成員函數內可以調用其它的非const成員函數嗎?
不可以。
const成員函數保證不會修改對象狀態,而調用非const成員函數可能會改變對象的數據成員,這就破壞了const成員函數的承諾。
class MyClass { public:void nonConstFunc() {data = 10;}void constFunc() const {nonConstFunc(); // 編譯錯誤,const成員函數不能調用非const成員函數} private:int data; };
4. 非const成員函數內可以調用其它的const成員函數嗎??
可以。
非const成員函數本身就可以修改對象狀態,但調用const成員函數不會有問題,因為const成員函數不會改變對象狀態,不會破壞非const成員函數對對象狀態修改的靈活性 。
class MyClass { public:void constFunc() const {// 不修改對象數據成員}void nonConstFunc() {constFunc(); // 合法,非const成員函數可以調用const成員函數} };
總結:
- const 對象調用非 const 成員函數:禁止。非 const 成員函數可能修改對象狀態,與 const 對象的只讀特性相沖突。
- 非 const 對象調用 const 成員函數:允許。const 成員函數保證不修改對象狀態,與非 const 對象兼容,且可復用只讀邏輯。
- const 成員函數調用非 const 成員函數:禁止。非 const 成員函數可能修改對象狀態,違反 const 成員函數的只讀約束。
- 非 const 成員函數調用 const 成員函數:允許。非 const 函數可以安全調用只讀的 const 函數,既不影響自身邏輯,又能實現代碼復用。
2.取地址及const取地址操作符重載
這兩個默認成員函數一般不用重新定義 ,編譯器默認會生成。這兩個也比較少用。這兩個運算符一般不需要重載,使用編譯器生成的默認取地址的重載即可,只有特殊情況,才需要重載,比如想讓別人獲取到指定的內容或者不想讓人取到普通對象的地址。
1.想讓別人獲取到指定的內容
class Point {
private:int x, y;
public://下面是初始化列表可以先不要太在意,下一篇會介紹Point(int a, int b):x(a), y(b){}// 取地址時返回x的地址(而非Point對象地址)int* operator&(){return &x;}
};// 使用:
Point p(10, 20);
int* px = &p; // px指向p.x,而非p本身
2.不想讓人取到普通對象的地址?
class NoNormalAddr {
public:// 普通對象取地址返回空NoNormalAddr* operator&(){return nullptr;}const NoNormalAddr* operator&() const{return this;}
};// 使用:
NoNormalAddr obj;
const NoNormalAddr c_obj;
&obj; // 得到nullptr
&c_obj; // 得到實際地址
可能有人那代碼去VS中嘗試了,發現會紅為什么了?
簡單說:取地址操作本身不報錯,但返回的 nullptr ?是無效地址,用它做后續操作時才會因“訪問無效內存”而報錯。這是一種通過返回無效結果間接阻止濫用地址的方式。
3.流插入和流提取?
先來看看這個圖片簡單了解一下
流插入<<?
先看看我們用operator的常規思想?
void Date::operator<<(ostream& out)
{out << _year << "-" << _month << "-" << _day << endl;
}void test7()
{Date d1(2025, 1, 25);d1 << cout;//d1.operator<<(cout);
}
發現調用的時候不符合我們常規調用,因為成員函數第一個參數是隱藏的this,使用調用就成這樣了。為了正常,我們要將他寫在全局,這樣this就不占用參數了。且我們還可能連續插入,修改后的:
糟糕爆紅了,發現_year,_month,_day 位于私有調用不到怎么辦了?
方法一:友元函數,在類里面加入
friend ostream& operator<<(ostream& out, const Date& d);
此時就可以調用了,調用結果:
此時就符合習慣了。
友元
我們來學一下友元
友元提供了一種突破封裝的方式,有時提供了便利。但是友元會增加耦合度,破壞了封裝,所以友元不宜多用。
友元分為:友元函數和友元類
?友元函數
友元函數可以直接訪問類的私有成員,它是定義在類外部的普通函數,不屬于任何類,但需要在類的內部聲明,聲明時需要加friend關鍵字
//frinnd+函數名 friend ostream& operator<<(ostream& out, const Date& d);
注意:
- 友元函數可訪問類的私有和保護成員,但不是類的成員函數
- 友元函數不能用const修飾
- 友元函數可以在類定義的任何地方聲明,不受類訪問限定符限制
- 一個函數可以是多個類的友元函數
- 友元函數的調用與普通函數的調用原理相同
友元類
友元類的所有成員函數都可以是另一個類的友元函數,都可以訪問另一個類中的非公有成員。
class Time {friend class Date;// 聲明日期類為時間類的友元類,// 則在日期類中就直接訪問Time類中的私有成員變量 public:...;//博主懶了 private:int _hour;int _minute;int _second; };class Date { public:...;//博主懶了void SetTimeOfDate(int hour, int minute, int second){// 直接訪問時間類私有的成員變量_t._hour = hour;_t._minute = minute;_t._second = second;} private:int _year;int _month;int _day;Time _t; };
注意:
- 友元關系是單向的,不具有交換性。 比如上述Time類和Date類,在Time類中聲明Date類為其友元類,那么可以在Date類中直接訪問Time 類的私有成員變量,但想在Time類中訪問Date類中私有的成員變量則不行。
- 友元關系不能傳遞。如果B是A的友元,C是B的友元,則不能說明C時A的友元。
- 友元關系不能繼承,在繼承位置再給大家詳細介紹。
?方法二:定義一個函數用來獲取_year,_month,_day
//寫在類里面
int GetYear() const
{return _year;
}int GetMonth() const
{return _month;
}int GetDay() const
{return _day;
}
流提取>>
?按照上面的學習:
//類里面
friend istream& operator>>(istream& cin, Date& d);istream& operator>>(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;}else{cout << "非法日期" << endl;assert(false);}return in;
}
?為什么沒const了?
因為兩個都要修改。流提取要改變里面的狀態值,使用不用。
調用結果:
三.總結
希望這個日期類的知識能對你有所幫助!如果覺得實用,歡迎點贊支持~ 要是發現任何問題或有改進建議,也請隨時告訴我。感謝閱讀!