11.C++日期類的實現
日期類的實現
在前面學過預設成員函式後,我們就可以寫一個簡單的日期類了。
如何寫呢?我們可以先分析分析。
日期類的成員變數都是int型別,那麼建構函式是要顯式定義的,成員變數都是int型別,因此淺拷貝即可。
因此拷貝構造、析構、賦值操作符過載都不需要我們顯式定義,使用編譯器生成的就好。
#include<iostream> using namespace std; int MonthDay[13] = { 0, 31,28,31,30,31,30,31,31,30,31,30,31 }; bool IsLeapYear(int year) { if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) return true; else return false; } class Date { public: int GetDay(int year, int month)const { if (IsLeapYear(year) && month == 2) return 29; else return MonthDay[month]; } Date(int year = 1, int month = 1, int day = 1) :_year(year), _month(month), _day(day) { if (year <= 0 || (month < 0 || month >= 13) || day > GetDay(year, month)) { cout << "日期錯誤" << endl; cout << year << "-" << month << "-" << day << endl; exit(-1); } } void swap(Date& d) { ::swap(_year, d._year); ::swap(_month, d._month); ::swap(_day, d._day); } Date& operator+=(int day) { if (day < 0) { return *this -= -day; } _day += day; while (_day > GetDay(_year, _month)) { _day -= GetDay(_year, _month); _month++; if (_month > 12) { _month = 1; _year++; } } return *this; } Date operator+(int day) { return Date(*this) += day; } Date& operator-=(int day) { if (day < 0) { return *this += -day; } _day -= day; while (_day < 1) { _month--; if (_month < 1) { _year--; _month = 12; } _day += GetDay(_year, _month); } return *this; } Date operator-(int day) { return Date(*this) -= day; } bool operator>(const Date& d)const { if (_year > d._year || _year == d._year && _month > d._month || _year == d._year && _month == d._month && _day > d._day) return true; else return false; } bool operator>=(const Date& d)const { return *this > d || *this == d; } bool operator==(const Date& d)const { return _year == d._year && _month == d._month && _day == d._day; } bool operator!=(const Date& d)const { return !(*this == d); } bool operator<(const Date& d)const { return !(*this >= d); } bool operator<=(const Date& d)const { return *this < d || *this == d; } Date& operator++() { *this += 1; return *this; } Date operator++(int) { Date ret(*this); *this += 1; return ret; } Date& operator--() { *this -= 1; return *this; } Date operator--(int) { Date ret(*this); *this -= 1; return ret; } int operator-(const Date& d)const { Date d1(*this); Date d2(d); int flag = -1; int ret = 0; int num1 = 0; int num2 = 0; if (d1 > d2) { d1.swap(d2); flag = 1; } for (int i = 1; i < d1._month; i++) { num1 += GetDay(d1._year, i); } num1 += d1._day; for (int i = 1; i < d2._month; i++) { num2 += GetDay(d2._year, i); } num2 += d2._day; for (int i = d1._year; i < d2._year; i++) { ret += 365; if (IsLeapYear(i)) ret++; } ret = ret + num2 - num1; return ret * flag; } ostream& operator<<(ostream& out,const Date& d)//定義在外部的 { out << d._year << "-" << d._month << "-" << d._day << endl; return out; } istream& operator>>(istream& in, const Date& d)//定義在外部的 { in >> d._year >> d._month >> d._day; return in; } private: int _year; int _month; int _day; };
程式碼不難,但可能會遇到一些細節處理上的問題。
一些問題
前置++與後置++的過載
C++中如何區分兩者呢,兩者的函式名(操作符)作用物件(形參)都是一樣的;
因此C++規定了讓後置++多一個int引數,用於與前置++構成過載得以區分。
Date& operator++();
前置++
Date operator++(int);
後置++
所以呼叫後置++操作符過載時,會多傳一個int值,用於區分前置++,這個引數不需要我們顯式去傳,編譯器幫我們處理。
但注意後置++的操作符過載不能寫成Date operator++(int a = 0);
,這是不被編譯器所允許的。
因為這樣會導致呼叫不明確,我們呼叫前置++過載時是該呼叫無參的還是預設呢?這裡就會產生歧義,因此不能給後置++過載的int引數預設值。
臨時變數與const的衝突
cout << d1++ << endl;
這段程式碼實際上就會出錯了,會報錯:
二元“<<”: 沒有找到接受“Date”型別的右運算元的運算子(或沒有可接受的轉換)
說明上述程式碼沒有找到對應的函式,可是明明引數什麼的都是一致的啊?
d1++呼叫的是d1.operator++(int)函式,而此函式返回值是一個傳值返回,而傳遞的過程實際上是在呼叫函式棧幀中建立了一個臨時變數來接收被調函式的返回值,因此d1++是一個臨時變數,具有常性,但我們的ostream& operator<<(ostream& out,Date& d)
cout << ++d1 << endl;
會不會報錯呢?這裡返回的是d1的引用,而不是一個臨時變數的拷貝,對++d1的訪問實際上就是對d1的訪問,因此不會報錯。
流提取&流插入
我們沒有將這兩個函式寫成成員函式,而是設定為友元函式。
其實可以是可以,如下:
ostream& operator<<(ostream& out)const
{
out << _year << "-" << _month << "-" << _day << endl;
return out;
}
istream& operator>>(istream& in)
{
in >> _year >> _month >> _day;
return in;
}
設定為成員函式,那麼第一個引數就是隱含的this指標,第二個引數是輸入輸出流的物件。
呼叫方式:d1 << cout;
呼叫時物件就只能作為左運算元,cout或者cin只能作為右運算元,與我們的使用習慣不符合,因此這裡採用友元的方式處理流插入和流提取。
宣告和定義分離
首先我們要知道全域性函式我們是不能放在標頭檔案中的,因為標頭檔案中得函式定義會在各個原始檔展開並編譯,同一個函式在兩個不同的原始檔有了不同的地址產生衝突,造成重定義。
如果想要成員函式的宣告和定義分離,那成員函式的定義也一定得放在.cpp檔案中而不是標頭檔案,原因同上,同一個函式在多個原始檔內被編譯,造成重定義。
那為什麼函式定義在類中,能正常編譯呢?
在類中定義得函式是預設內聯的,不會放入符號表,即使標頭檔案在多個原始檔展開,也不會有重定義錯誤,因為它們都單獨作用於所存在的原始檔(沒有外部連結屬性)。
那可能又會問,放在類的函式不一定會被編譯器當做內聯,那這些沒有被當做內聯的函式會有函式地址,為什麼也不會有重定義錯誤?
答案是即使沒有被編譯器當做內聯,即不會在呼叫的地方被展開,但是其是具有內聯屬性的,而無法被外部連結,就不會發生多個原始檔相同函式有衝突的情況。
例如下面這種:
//func.h
inline void print(const int a);
//func.cpp
#include"date.h"
inline void Test(const int a)//編譯器不會採用我們的建議使其內聯
{
for(int i = 0; i < 100; i++)
{
cout << a << endl;
}
}
//main.cpp
#include"date.h"
int main()
{
const int a = 1;
Test(a);
return 0;
}
連結錯誤:LNK2019: 無法解析的外部符號 "void __cdecl print(int)" (?print@@YAXH@Z),函式 main 中引用了該符號
則表明在main.cpp中找不到func.cpp中的func函式,可以這樣理解——func函式雖然有地址,但是由於我們的inline宣告,它無論如何都有內聯屬性,無法被外部連結。
結論:
- 公有的函式(包括友元)都不能定義在標頭檔案,否則會有函式重定義的錯誤;
- 類的成員函式如果不是行內函數,也不能定義在標頭檔案,否則會有函式重定義的錯誤;
- 若函式聲明瞭是內聯的,編譯器也採納了,那麼行內函數沒有地址,無法被外部連結;
- 若函式聲明瞭是內聯的,但編譯器沒有采納,雖然產生了函式地址,但有內聯屬性,也無法被外部連結;
濃縮成一句話,被宣告為內聯的函式,即使編譯器沒有采納內聯意見,但仍保留內聯屬性(沒有外部連線屬性),必須沒有外部連結屬性的函式才能定義在標頭檔案中。
說到外部連結屬性,那麼static修飾全域性函式也是改變其連線屬性,讓函式只能在本原始檔內被呼叫(無法被外部連結)即使其他原始檔有一個完全相同的函式定義,但我連結此函式時只會在本原始檔找,就不會有函式地址的衝突,因此static修飾全域性函式時,這個函式也可以定義在標頭檔案中而不發生重定義的錯誤。
當然也可以說它們沒有了外部連結屬性,就不會被放入符號表,也不會有符號表的合併中同一個函式卻有多個地址的衝突,因此多個原始檔連結到一起時不會報錯。(我猜這個是主要原因)
關於程式碼複用
我最開始覺得程式碼複用很不好,因為一個函式的函式體內要呼叫另一個函式來實現部分功能,這就需要有壓棧的開銷,這不是浪費資源嗎?因此我帶著懷疑在日期類的+運算子過載的過載中複用了+=運算子的過載。
但是當我做修改和新增功能時,我才發現程式碼複用的好處:
這降低了我們維護程式碼的難度,相似但細節上有差異的函式我們不需要一個地方出問題了就去全盤修改,而是隻需要修改被複用了的函式,最重要的是我們省事啊!