你好,我是安然無虞。 |
文章目錄
- 自學網站
- 寫在前面
- 日期類的實作
- cin和cout重載問題
- const成員函數
- 大廠面試真題
自學網站
推薦給老鐵們兩款學習網站:
面試利器&算法學習:牛客網 風趣幽默的學人工智能:人工智能學習
寫在前面
上一節我們學習了構造函數、析構函數、拷貝構造和指派重載,今天我們就要好好運用前面學習的知識來完成日期類的實作。
日期類的實作
下面先把Date類基本的成員補充完整:
class Date
{
public:
//全預設的構造函數
Date(int year = 2022, int month = 9, int day = 7)
{
_year = year;
_month = month;
_day = day;
}
//拷貝構造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//析構函數
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
//指派重載
//對于指派重載函數需要注意的是連續指派的情況
//至于傳回值是Date還是Date&由傳回對象在函數結束後是否銷毀決定
//這裡因為傳回的是*this,是以可以使用引用
//d1 = d2 = d3
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;
}
OK,是不是很簡單,下面也不難哦。
// ==運算符重載
bool operator==(const Date& d)
{
return (_year == d._year && _month == d._month && _day == d._day);
}
// >運算符重載
bool operator>(const Date& d)
{
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)
{
//可直接複用寫好的 ==運算符和 >運算符
return (*this == d) || (*this > d);
}
// <運算符重載
bool operator<(const Date& d)
{
//可直接複用寫好的>=運算符
return !(*this >= d);
}
// <=運算符重載
bool operator<=(const Date& d)
{
//可直接複用寫好的 <運算符和 ==運算符
return (*this < d) || (*this == d);
}
上面這部分代碼,主要是想讓大家體會代碼的複用性。
我們知道還有++,-- 這類的運算符,它們還分為前置和後置,因為函數名相同,那麼該如何區分呢?
// 如若要區分前置和後置,需構成重載
// 規定:如果是後置,需要多加一個參數,這個形參傳什麼都可以,僅為了區分
// 如:Date operator++();//前置
// Date opeartor++(int);//後置
// 前置++
Date& operator++()
{
*this += 1;
return *this;
}
// 後置++
Date operator++(int)//形參寫成int i也可以,無所謂
{
Date ret(*this);
*this += 1;
return ret;
}
// 這裡需要格外注意:為什麼前置++傳回值是Date&,而後置++傳回值是Date?
// 我們知道傳回值為Date,會調用拷貝構造,這樣會影響效率,而Date&卻不用。
// 那為什麼後置++不用引用傳回呢?
// 因為後置++是先指派再++,傳回的是++前的值,是以首先拷貝構造一個ret,傳回的是ret的值
// 因為ret是局部對象,是以不能傳引用傳回。
好,既然前置++和後置++已經給出了,那麼前置 --和後置-- 就很容易啦。
// 前置--
Date& operator--()
{
*this -= 1;
return *this;
}
// 後置--
Date operator--(int)
{
Date ret(*this);
*this -= 1;
return ret;
}
好的,上面的内容都很簡單,下面我們稍微加大一點難度哦。
為了實作日期±天數,首先實作這兩個函數:
// 判斷閏年
bool isLeapYear(int year)
{
return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
}
//擷取每個月的天數
int GetMonthDay(int year, int month)
{
int MonthDay[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if(isLeapYear(year))
MonthDay[2] = 29;
return MonthDay[month];
}
// 因為GetMonthDay函數可能會被頻繁調用,每次都開數組影響效率,是以這部分代碼還可以優化一下:
static int MonthDay[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};//靜态成員隻會初始化一次
if(isLeapYear(year))
MonthDay[2] = 29;
return MonthDay[month];
//注意哦,這裡我給大家挖了一個坑,上面的寫法有一個bug,你發現了嗎?
//MonthDay[2] = 29;會修改數組裡的資料,導緻MonthDay[2]變成29。你可以調試去感受。
//綜上,下面的代碼才是最完美的。
int GetMonthDay(int year, int month)
{
const static int MonthDay[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if(isLeapYear(year) && month == 2)
return 29;
else
return MonthDay[month];
}
// 日期+=天數
// d1 += 2;
Date& operator+=(int day)
{
//注意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;
}
// 日期+天數
// d1 + 2
Date operator+(int day)
{
//Date ret(*this);
//ret._day += day;
//while (ret._day > GetMonthDay(ret._year, ret._month))
//{
// ret._day -= GetMonthDay(ret._year, ret._month);
// ret._month++;
// if (ret._month == 13)
// {
// ++ret._year;
// ret._month = 1;
// }
//}
//return ret;
//可以用複用+=
Date ret(*this);
ret += day;
return ret;
}
這裡又有一個問題了:+複用+=,+=複用+,哪個更優呢?
首先我們知道實作+運算符本身就要調用兩次拷貝構造,而實作+=本身沒有拷貝構造。如果是+=複用+的話,有4次拷貝構造(+=是用+實作的嘛,是以調用+=本身就有兩次拷貝構造,然而實作部分的+又要調用兩次拷貝構造,攏共4次);如果是+複用+=的話,隻有2次拷貝構造。(日期-=天數同理)
是以說,以後我們隻需要實作+=,-=,其他的直接複用它們即可。
// 日期-=天數
// d1 -= 2;
Date& operator-=(int day)
{
if (day < 0)
return *this += -day;
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
_month = 12;
--_year;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// 日期-天數
// d1 - 2
Date operator-(int day)
{
Date ret = *this;
ret -= day;
return ret;
}
在計算日期±天數的時候,大家先在草稿紙上面計算哦,想好了再寫代碼。OK,下面講解最後一部分,日期-日期,下面給出的方法挺妙的哦,不信你看看:
// 日期-日期
int operator-(const Date& d)
{
//假設較大值是*this
Date max = *this;
Date min = d;
int flag = 1;
int count = 0;
if(max < min)
{
max = d;
min = *this;
flag = -1;
}
while(min != max)
{
min++;
count++;
}
return flag * count;
}
cin和cout重載問題
哈哈,你不會真的以為沒有了吧,怎麼會呢,還有補充部分。
對日期類的補充:
上面我們已經對日期類的大部分功能實作完畢了,但是如果直接執行這段代碼還是存在問題:
void TestDate()
{
Date d1;
//對于自定義類型的對象流提取操作符>>和流插入操作符<<需要自己實作
cin >> d1;// >>是流提取操作符
cout << d1;// <<是流插入操作符
}
注意哦:
cin 是istream類型的對象,cout是ostream類型的對象,它們都是iostream頭檔案裡面全局屬性的對象。
int i = 1;
double d = 2.2;
cout << i << d << endl;
從上面的代碼中我們可以看到,相比于C語言,不需要%d,%lf這些指定的輸入輸出格式,這是為什麼呢?其實又是函數重載,C++庫裡面已經給了内置類型的<<,>>也一樣。
是以這裡說明了什麼問題呢,說明了自定義類型的流插入和流提取需要我們自己實作。
如果我們在類中實作:
class Date
{
public:
...
//成員函數裡的第一個形參是隐藏的this指針
void operator<<(ostream& out)
{
out << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
如果像上面這樣,将operator<<重載到類中實作,那麼調用的時候會變成下面這樣:
Date d1(2022, 9, 29);
d1.operator<<(cout);// 即d1 << cout;
// 這樣是不是很别扭,但是沒辦法,成員函數的第一個參數預設是隐藏的this指針
因為将<<運算符重載成成員函數後,第一個參數預設是隐藏的this指針,是以我們調用的時候會很怪,這個時候需要将cout 調成第一個參數才可以,也就是說不能把<<重載成成員函數,那我們該怎麼辦呢?
可以實作成全局函數:
void operator<<(ostream& out, const Date& d)
{
// 如果重載成全局函數的話,還有一個問題就是不能通路類中的私有成員
out << d._year << " " << d._month << " " << d._day << endl;//報錯
}
class Date
{
public:
...
private:
int _year;
int _month;
int _day;
};
解決将<<運算符重載成全局函數,不能通路類中私有成員的問題,有兩種解決方法:
方法一:
這種方法Java語言喜歡使用,就是在類中定義GetYear(), SetYear()…這些public成員函數,以便在類外進行讀寫類中私有成員,但是這樣實作的話,跟将私有成員變成公有成員的差距不大了,因為這個時候既能讀私有成員,又能寫成員變量,影響封裝,是以不好,C++不推薦這種寫法;
方法二:
提供友元,就是在類中聲明這個全局變量是該類的友元函數(我是你的朋友,我就可以通路你的私有了),具體關于友元的細節将在下一節進行講解。
void operator<<(ostream& out, const Date& d)
{
// 如果重載成全局函數的話,還有一個問題就是不能通路類中的私有成員
out << d._year << " " << d._month << " " << d._day << endl;//報錯
}
class Date
{
//友元函數
void operator<<(ostream& out, const Date& d);
public:
...
private:
int _year;
int _month;
int _day;
};
代碼像上面這樣實作之後,就可以正常使用了:
Date d1(2022, 9, 29);
cout << d1;
但是還存在一個問題就是:
cout << d1 << d2;
這樣就編譯不通過了,原因是沒有給<<運算符重載提供傳回值,導緻其不支援連續指派,是以我們需要給函數提供一個傳回值,以便再去做下一次流插入的左操作數,具體實作如下:
//流插入操作符:<<
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day << endl;
return out;
}
這樣實作之後,<<就可以正常使用啦,當然了,流提取操作符>>原理同上:
//流提取操作符:>>
istream& operator>>(istream& in, Date& d)//注意哦,此時不能加上const,因為d需要修改
{
in >> d._year >> d._month >> d._day;
return in;
}
const成員函數
我們将const修飾類的成員函數叫做const成員函數,const修飾類成員函數,實際上是修飾該成員函數隐藏的this指針,表明在該函數中不能對類的任何成員進行修改操作。
//Date.h檔案
class Date
{
public:
//...
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
//test.cpp檔案
#include"Date.h"
void Func(const Date& d)
{
d.Print();
}
int main()
{
Date d1(2022,9,29);
d1.Print();
Func(d1);
return 0;
}
如果我們直接這樣編譯的話是編譯不通過的,錯誤顯示如下:
咦,這個報錯是什麼鬼?
我們知道執行這一行代碼的時候,編譯器是這樣處理的:
d1.Print(); //d1.Print(&d1);
//由于d1是Date類型,是以&d1是Date*
這樣的話,Print()成員函數第一個隐藏的this指針類型也就是這樣的:
//Date* this,由于this的指向不能發生改變,
//是以編譯器會将成員函數的第一個隐藏的this指針處理成Date* const this;
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
可能有老鐵會說:對呀,這個你說的,我都了解呀,那為什麼會編譯不通過呢?
其實呀,如果僅執行 d1.Print();是不會報錯的,錯就錯在 Func(d1); 這個函數體裡面調用的Print()。為什麼這樣說呢,請繼續看:
#include"Date.h"
void Func(const Date& d)
{
d.Print();//d.Print(&d);
//這裡的&d類型是const Date*
}
//而我們知道成員函數Print()第一個隐藏的this指針類型是Date*
//将const Date* 傳遞給 Date* const(本質還是Date*)屬于權限放大,這個是不允許的,是以編譯報錯
//隻允許權限不變或者權限縮小
//d1.Print();//即d1.Print(&d1);Date*傳遞給Date*屬于權限不變
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
那麼我們該如何修改呢?我們知道如果要解決Func()函數裡面的錯誤的話,這需要将成員函數Print()裡面的第一個隐藏的this指針類型改成const Date* const即可,但是由于它是隐藏的,不能顯示修改,那我們該怎麼辦呢?不要擔心,很簡單,像下面這樣即可:
//在函數後面加上const,相當于在第一個參數(也就是隐藏的this指針)的前面加上const
//這個時候this指針的類型就變成了const Date* const類型
void Print() const
{
cout << _year << " " << _month << " " << _day << endl;
}
在成員函數的後面加上const之後,this類型就變成了const Date* const類型,屬于權限很低的類型,這樣的好處是不管傳遞過來的變量類型是Date*(權限縮小),還是const Date*(權限不變)編譯都能通過,不會存在權限放大這麼一說了。
可能有老鐵會問了,既然給成員函數後面加上const之後可以有效避免權限放大的問題,那麼我們為什麼不在每個成員函數的後面都加上一個呢?
我要說的是,加上const之後,this指針變成const Date* const類型,是以此時this指向不能修改,this指向的内容同樣也不能修改,是以說隻有成員變量不用修改的成員函數才可以加上const。比如日期類裡面的++, --不可以加,而+, -可以。
好的,有了前面的基礎,請看下面幾個問題:
- const對象可以調用非const成員函數嗎?
- 非const對象可以調用const成員函數嗎?
- const成員函數内可以調用其他的非const成員函數嗎?
- 非const成員函數内可以調用其他的const成員函數嗎?
這就很簡單啦,問題1不可以,權限放大;問題2可以,權限縮小;問題3不可以,權限放大;問題4可以,權限縮小。
大廠面試真題
1.已知表達式++a中的"++"是作為成員函數重載的運算符,則與++a等效的運算符函數調用形式為( )
A.a.operator++()
B.a.operator++(0)
C.a.operator++(int)
D.operator++(a,0)
解析:這道題很簡單,前置++嘛
2.在重載一個運算符為成員函數時,其參數表中沒有任何參數,這說明該運算符是( )
A.無操作數的運算符
B.二進制運算符
C.字首一進制運算符
D.字尾一進制運算符
解析:重載為成員函數時,其函數的參數個數與真實的函數參數個數會減少1個,減少的則通過this指針進行傳遞,是以無參則說明有一個參數,然後對比前置後置++,就很容易解決本題啦。
3.哪個操作符不能被重載 ( )
A.*
B.()
C… (點)
D.[]
E.->
解析:不能被重載的運算符隻有5個, 點号. 三目運算?: 作用域訪 問符:: 運算符sizeof 以及.*