天天看點

類和對象·日期類的實作

類和對象·日期類的實作
你好,我是安然無虞。

文章目錄

  • ​​自學網站​​
  • ​​寫在前面​​
  • ​​日期類的實作​​
  • ​​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。比如日期類裡面的++, --不可以加,而+, -可以。

好的,有了前面的基礎,請看下面幾個問題:

  1. const對象可以調用非const成員函數嗎?
  2. 非const對象可以調用const成員函數嗎?
  3. const成員函數内可以調用其他的非const成員函數嗎?
  4. 非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 以及.*

繼續閱讀