天天看點

C++ 類和對象(下)

寫在前面

這個大概就是類和對象最後的一篇部落格了,算是初階的一個結尾吧,裡面涉及到的内容還是挺多的,我們還是需要靜下心來看看.

指派運算符重載

這個是一個很大的内容,我們可以自己創造指派運算符的實作規則,我們先來看看什麼是指派運算符重載.

C++為了增強代碼的可讀性引入了運算符重載,運算符重載是具有特殊函數名的函數,也具有其傳回值類型,函數名字以及參數清單,其傳回值類型與參數清單與普通的函數類似。

函數原型:傳回值類型 operator操作符(參數清單)

為何出現指派運算符重載

我們先來看一種請況.

為啥會報錯,我就想讓他們比較一下,我有什麼錯?可編譯器卻不允許,今天我必須讓它給我允許了,這就是指派運算符重載 為何會出現的原因

class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};

int main()
{
  Date d1(2022, 5, 18);
  Date d2(2022, 5, 18);

  if (d1 == d2)
  {
    cout << "==" << endl;
  }
  return 0;
}      
C++ 類和對象(下)

指派運算符重載

多的不說,我們現在就來看看如何使它變得合理.

我們來看看這個函數,現在出現了一個問題,我們得不到類的屬性,它被封裝了,我們先把屬性改成public,後面在解決這個問題.

bool operator==(Date d1, Date d2)
{
  //在這裡 年月日都相等  才是 相等
  return d1._year == d2._year
    && d1._month == d2._month
    && d1._day == d2._day;
}      

這裡就可以了,我們調用一下這個函數

int main()
{
  Date d1(2022, 5, 18);
  Date d2(2022, 5, 18);

  if (operator==(d1,d2))
  {
    cout << "==" << endl;
  }
  return 0;
}      
C++ 類和對象(下)

我們可能會疑惑,我随便取一個函數名就可以把這個函數的功能給寫出來,還用弄得這樣花裡胡哨,但是你寫的函數可以被這樣調用嗎?但是我的就可以.

if (d1 == d2)
{
    cout << "==" << endl;
}      
C++ 類和對象(下)

這個就是運算符重載的魅力,現在我需要把這個函數給完善下,傳引用,沒必要在開辟空間了,用const修飾,避免被不小心修改

bool operator==(const Date& d1, const Date& d2)   
{
  //在這裡 年月日都相等  才是 相等
  return d1._year == d2._year
    && d1._month == d2._month
    && d1._day == d2._day;
}      

解決不能得到類内封裝屬性的問題

這個我給兩個解決方法,一個是在類裡面寫一些get函數,得到這些屬性的值,另一個是使用友元,但是這種方法破壞了封裝,不太建議.

  • 在類内寫一些 get 方法 這是Java經常用的
  • 使用友元 會破壞類的封裝,這裡我放在下面談

在類裡面寫運算符重載

我們還不如直接在類裡面寫這個函數呢,簡單快捷,這樣就可以避免破壞封裝.

class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  bool operator==(const Date& d1, const Date& d2)
  {
    //在這裡 年月日都相等  才是 相等
    return d1._year == d2._year
      && d1._month == d2._month
      && d1._day == d2._day;
  }
public:
  int _year;
  int _month;
  int _day;
};      
C++ 類和對象(下)
請問為什麼會報這個錯誤?參數不是很對嗎?我們在前面都說過,編譯器會預設添加一個this指針類型的參數,而==就是兩個操作數,是以報參數過多.我們減少一個參數就可以了.
bool operator==(const Date& d) //預設添加一個  const this  指針
{
    //在這裡 年月日都相等  才是 相等
    return _year == d._year
        && _month == d._month
        && _day == d._day;
}      

這樣函數的調用就變成這樣

int main()
{
  Date d1(2022, 5, 18);
  Date d2(2022, 5, 18);

  if (d1 == d2)    //   d1 == d2 預設  變成   d1.operator==(d2) 
  {
    cout << "==" << endl;
  }
  
  return 0;
}      
C++ 類和對象(下)
代碼裡面你說變成 d1.operator==(d2) 這樣,就變成這樣?有什麼可以證明的,這裡用對象的位址證明一下吧.
class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  bool operator==(const Date& d)
  {
    cout << "this" << this << endl;
    return true;
  }
public:
  int _year;
  int _month;
  int _day;
};

int main()
{
  Date d1(2022, 5, 18);
  Date d2(2022, 5, 18);
  cout << "d1" << &d1 << endl;
  cout << "d2" << &d2 << endl;
  if (d1 == d2)
  {

  }
  return 0;
}      
C++ 類和對象(下)

類内運算符重載和類外運算符重載的優先級

如果類内和類外都有一樣的,編譯器優先調用哪一個重載呢?我們通過現象得知優先調用類裡面的重載.

class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  bool operator==(const Date& d)
  {
    cout << "類裡面" << endl;
    return true;
  }
public:
  int _year;
  int _month;
  int _day;
};

bool operator==(const Date& d1,const Date& d2)
{
  cout << "類外面" << endl;
  return true;
}



int main()
{
  Date d1(2022, 5, 18);
  Date d2(2022, 5, 18);
  if (d1 == d2)
  {

  }
  return 0;
}      
C++ 類和對象(下)

重載 運算符 “=”

本來我想和大家分享一個日期類,但是如果現在這這裡寫了,至少還需要幾千字,我把它單獨放到了一個部落格,作為我們這些天學習類一個小總結,在這個日期類裡面,你會發現我們上面談的所有的知識點,這裡先來點小菜,這個很簡單,目的是為了引出下面的知識點.

Date& operator=(const Date& d)
{
    if (this != &d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    return *this;
}      

我們先來調用一下.

int main()
{
  Date d1(2022, 10, 18);
  Date d2;
  d2 = d1;
  d2.Print();
  return 0;
}      
C++ 類和對象(下)

d2 = d1

細心的朋友可能發現我們寫的是d2 = d1;而不是在開辟d2的時候給他指派,這裡我要重點談下.我們通過調試來看看吧.

這個調用的是運算符重載.

int main()
{
  Date d1(2022, 10, 18);
  Date d2;
  d2 = d1;
  d2.Print();
  return 0;
}      
C++ 類和對象(下)

Date d2 = d1

這個調用的是拷貝構造,不是那個運算符重載.

int main()
{
  Date d1(2022, 10, 18);
  Date d2 = d1;
  d2.Print();
  return 0;
}      
C++ 類和對象(下)
從這裡就可以下一個結論了,如果我們給定義變量并初始化的時候,調用拷貝構造,如果指派的時候兩個變量已經被定義過了,就是調用運算符重載.

如何構造前置++和後置++

這個很重要的,我們如何讓編譯器知道我們想要的是前置++還是後置++,這個我們不知道該如何辦,但是C++已經規定了我們如何做,這樣就可很好的重載這個運算符了.

C++規定,我們可以通過給後置++加上一個int類型的參數來區分前置++和後置++,這裡面–也是一樣的,我們也可以這麼重載.這裡我先說一個規則,具體的可以去看我的Date類部落格,裡面有詳細的實作.

  • 前置 尋常寫法
  • 後置 參數加一個 int 類型的參數

前置

前置是什麼都不用做,直接寫就可以了.

Date& Date::operator++()
{
    // 假設我們已經重載了  =  和 +
    *this = *this + 1;
    return *this;
}      

後置

後置的化是我們給一個參數,我們不用關心編譯器是怎麼做的,隻需要知道用法就可以了,

Date Date:: operator++(int) // 可以這麼做 告訴編譯器參數不接受 隻是為了差別
{
    // 假設我們已經重載了  =  和 +
    Date ret(*this);
    *this = *this + 1;
    return ret;
}      

重載流提取 & 流插入

有的時候我們也可能通過這樣的方法來進行輸入日期,也就是流提取和流插入,我們也要重載它們.

int main()
{
  Date d;
  cin >> d;   // 流提取
  cout << d;  // 流插入
  return 0;
}      
C++ 類和對象(下)

流提取 >>

這裡面我們詳細分享一下流提取,把衆多情況給考慮到,最後把流插入給寫一下就可以了.我們可以在直接在類内重載我們想要的運算符,不過這裡的參數一個是 std裡面的istream,我們可以這麼了解,打卡一個螢幕,看作檔案,我們從鍵盤上輸入資料,遇到空格算一個,遇到換行結束.

void operator>>(std::istream& in)
{
    in >> _year >> _month >> _day;
}      

我們試試代碼可以嗎?

int main()
{
  Date d1;
  cin >> d1;
  return 0;
}      
C++ 類和對象(下)
我們在C++中一直提到過一個東西,對于重載的運算符實際上是調用函數,cin >> d1調用的函數就是 operator>>(cin,d1),那麼我們在類内重載的運算符可不是這樣的,就會報錯,那麼我們該如何正确的調用呢?
int main()
{
    Date d1;
    d1 >> cin;
    d1.Pinrt();
    return 0;
}      
C++ 類和對象(下)
友元

你是不是感覺有點怪,我們是想吧螢幕上的資料放到 d1 中,但是這種調用的方式有點讓我難以接受,不妥,很是不妥.我們怎麼修改這個函數呢?這裡,我們想到了使用 全局的函數,也就是在類外定義這個函數.但是這裡就有問題了,我們類的成員是封裝的,也就意味者外部的函數是無法得到和修改(或者使用 set,get函數)對象的.

class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day;
    cout << endl;
  }
  

private:
  int _year;
  int _month;
  int _day;
};

void operator>>(std::istream& in, Date& d)
{
  in >> d._year >> d._month >> d._day;
}      
C++ 類和對象(下)

我們還有另外一種方法,使用友元,一般我是不推薦的,但是這裡我們不得不用,我們想來看用法,後面會專門來說.

我們在類裡面聲明這個函數是我們的朋友,既然是朋友,那麼你就可以修改我們的變量了

C++ 類和對象(下)

如果你要是覺得到這裡就可以了,那麼就大錯特錯了,我們發現标準庫裡面的流提取支援連續提取,也就是這樣的

int main()
{

  int a;
  int b;
  cin >> a >> b; //連續 流提取
  printf("a = %d b = %d\n", a, b);
  return 0;
}      

我們也要有一個傳回值,這裡就直接修改了,類裡面這裡就不修改了

std::istream& operator>>(std::istream& in, Date& d)
{
  in >> d._year >> d._month >> d._day;
  return in;
}      

流插入 <<

這裡流提取差不多,這裡直接給代碼了.

std::ostream& operator<<(std::ostream& out, Date& d)
{
  out << d._year << "-" << d._month << "-" << d._day;
  return out;
}      
C++ 類和對象(下)

那些不能運算符重載

我們可以重載很多運算符,但是還有5個運算符不能重載.

分别是 .* 、:: 、sizeof 、?: 、. 注意以上5個運算符不能重載。這個經常在筆試選擇題中出現,大家記住就可以了.

再析權限問題

權限問題是我們的重中之重,現在需要把這個問題再拿出來往深處談談

const

我們需要知道是什麼造成權限的擴大,也就是在C++中,我們通過const來修飾,那麼C++中的const有什麼特殊之處,是不是和C++完全一樣呢?這裡我們隻談一點const修飾的位址可以解引用嗎?可以的.

int main()
{
  int a = 10;
  const int* pa = &a;
  cout << *pa << endl;
  return 0;
}      
C++ 類和對象(下)
那麼它可以修改嗎?
C++ 類和對象(下)

權限

現在就可以來談權限了,我遇到了一個問題,大家看看

class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day;
    cout << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};

void func(const Date& d)
{
  d.Print();
}

int main()
{
  Date d1(2022, 7, 10);
  func(d1);
  return 0;
}      
C++ 類和對象(下)

這裡我們可以通過 報錯的資訊知道,我們在調用d.Print();時,由于d是 一個const修飾的引用

它的調用編譯器自動修改成 Print(const Date* const this),但是 我們類裡面的Print是Print( Date* const this),權限不比對,這就是我們遇到的問題,當我們知道問題,這裡有又有一個,我們該怎麼辦?

C++規定了,如果我們想要修改this指針的權限,隻需要在函數後面加上一個const就可以了,如果生命和定義分離,那麼都加

C++ 類和對象(下)
這裡我們又有了另外一個問題,是不是每一個函數都要加上const呢?不是的,如果我們明确了不修改原來的變量,最好都加上const,并且這裡還有一個好處,就是普通的變量調用const修飾的函數也不會報錯,因為權限問題不會存在.

const成員函數

将const修飾的類成員函數稱之為const成員函數,const修飾類成員函數,實際修飾該成員函數隐含的this指針,表明在該成員函數中不能對類的任何成員進行修改 .

這裡有一個問題,const成員函數可以可以調用非const的成員函數嗎? 答案是 不可以,我們知道,const成員裡面的this都是const 類型* const this,但是普通成員函數的是 類型* const this,也會發生權限問題.

C++ 類和對象(下)

取位址運算符重載

這個就是六個預設函數的另外兩個,比較簡單,一般的話,我們是不寫出來的,沒有必要,除非你不想讓任何人(包括你自己)得到位址,可以寫出來,傳回一個nullptr.

Date* operator&()
{
    return this;
}

const Date* operator&() const  // 這個二  就是  const 成員函數
{
    return this;
}      

再談構造函數

這裡,我們要把構造函數好好的在深入的談一下,另外不加了一些内容,我們還是先來看看那個最基本的Date類,以它為例.

class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};      

構造函數時對内置類型不做任何處理嗎

這個是肯定的,大家不要糾結,但是這裡我發現了課一個現象,可以作為特立,我用的時VS2013,其他的編譯器我不确定是不是.

我們的成員變量要是存在一個指針,所有的成員變量被初始化成0
C++ 類和對象(下)

初始化清單

在構造函數中,我們可以通過下面的方式來給成員變量進行指派.

C++ 類和對象(下)
我們在剛開始的時候就說過,在類裡面的的變量隻是一個聲明,隻有當我們進行對象的執行個體化的時候才會建立,這一點我們時已經知道的,但是這裡面說在構造函數裡面進行再次指派是什麼意思?我們該如何判斷成員變量是在哪個過程進行的對象的執行個體化?這裡就要談到初始化清單.

我麼先來看看初始化清單如何使用的,我們在構造函數後面跟上黃色方框裡面的内容,格式是 成員變量後面的括号裡面表示要初始化的值,它的作用和我們在函數裡内結果是一樣的.

C++ 類和對象(下)
int main()
{
  Date d1;
  d1.Print();
  return 0;
}      
C++ 類和對象(下)
這裡還要和大家提一個醒,這兩種方法是可以混合這個用的.
C++ 類和對象(下)

為何出現初始清單

這個就要談談類裡面是如何"定義"成員變量的?我們這麼認為,成員變量是在初始化清單進行定義的,也就是初始化清單的首要作用就是定義變量,那麼這裡就有一個疑問了,如果我們不寫,是不是也會存在初始化清單?是的,編譯器對于内置類型進行初始化清單事會給一個随機值,就是我們之前說的,對内置内置類型不做處理,如果我們寫了,就按照我們的來.

大家可能還存在另外一個疑問,那就是初始化清單和函數體内指派好象沒有什麼差別啊,這兩個的作用是不是重複了?我想說的是沒有,初始化清單能做的遠比函數體内的要多.

這裡就有一個問題對于那些定義和初始化必須在一個起的變量你該怎麼辦?這裡就不能在函數體内進行再次指派.這裡也進一步證明了函數體内是指派,而不是定義.

C++ 類和對象(下)

這裡我們隻能通過初始化清單來進行幫忙,

C++ 類和對象(下)

成員變量預設值的實質

我們之前說過成員變量是可以預設的,它的是指就是給初始化變量時候把随機值給替換掉,如果我們傳參了,就按傳參的的來,和預設函數差不多.

C++ 類和對象(下)

初始化清單的初始化順序

我們想問,初始化清單的順序是不是和我們寫的一樣,還是有固定的順序呢?這個我們按照例題的形式來解釋.

class A
{
public:
  A(int a)
    :_a1(a)
    , _a2(_a1)
  {}
  void Print() {
    cout << _a1 << " " << _a2 << endl;
  }
private:
  int _a2;
  int _a1;
};
int main() 
{
  A aa(1);
  aa.Print();
}      

A.輸出1 1

B.程式崩潰

C.編譯不通過

D.輸出1 随機值

按照我們想的,我們把1給了_a1,後面又把 _a1 給了 _a2,也就是我們選A,但真的是是這樣嗎?

C++ 類和對象(下)
這就和我麼想的不同了,但是要是這麼想就可以了,初始化是按照變量的聲明來的,先初始化 _a2,把 _a1這個随機值 _a2,在初始化 _a1,把1給 _a1,這就和結果一樣了.

小結

  • 盡量使用初始化清單初始化,因為不管你是否使用初始化清單,對于自定義類型成員變量,一定會先使用初始化清單初始化
  • 初始化順序是按照申明的定義走的

explicit 關鍵字

class A
{
public:
  A(int a = 0)
    :_a(a)
  {
    cout << "A(int a = 0)" << endl;
  }
  A(const A& d)
  {
    cout << "A(const A& d)" << endl;
  }
private:
  int _a;
};

int main()
{
  A aa = 10;

  return 0;
}      

我麼可以接受其他的執行個體化對象的方式,但是上面的對我來說就有點難以接受了,我們把一個内置類型給了一個自定類型,這是開什麼玩笑,但是,在C++中這裡還真是允許的,C++中構造函數不僅可以構造與初始化對象,對于單個參數的構造函數,還具有類型轉換的作用,也就是說,編譯器會先把10作為參數執行個體化一個臨時對象,有通過拷貝構造把這個臨時對象給拷貝一份.

C++ 類和對象(下)

按理說,我們是需要調用構造函數和拷貝構造的,但是這裡編譯器直接給優化了,直接把這個臨時變量給了aa,把拷貝構造這一步給省了

C++ 類和對象(下)

explicit 關鍵字

如果我們不想讓單參數的構造函數發生這樣的轉變,我們可以在這個構造函數前面加上一個explicit關鍵字,這樣編譯器就不會這麼做了

C++ 類和對象(下)

static 成員

現在我們談點東西了,C++類裡面支援static修飾的成員變量和方法,它們普通的方法是不一樣的,屬于所有的對象,也就是說屬于類.我麼先來見識一下它們的特性

static 成員變量

和普通成員變量一樣,在類裡面申明,但是在類外定義,如果不定義,編譯器編不過去

class A
{
public:
    A()
    {
    }
public:
    static int a; // 存在 靜态區
};
int A::a = 10;      
C++ 類和對象(下)

通路手段

如果是public的,我們可以通過下面的兩種方式來訪,如果不是就隻能通過用函數來通路了

int main()
{
  A aa;
  cout << aa.a << endl; //  它們的存在 隻是告訴編譯器 去這個 裡面去找,沒其他作用
  cout << A::a << endl;
  return 0;
}      

static修飾的方法

這個更加簡單,隻要記住,static修飾的方法裡面沒有this指針就可以了,也就是說,不能在這函數内調用非static修飾的函數,除非你在這個函數内執行個體化一個對象,通過它來簡潔調用,這裡就不談了.

class A
{
public:
  A()
  {
  }
  static void func()
  {
    cout << "static void func" << endl;
  }
};      

通路手段

這個和static修飾的變量一樣,都是告訴編譯器去類裡面找就行了

int main()
{
  A a;
  a.func();
  A::func();
  return 0;
}      
C++ 類和對象(下)

友元

我們前面是見過友元的,就在那類Date的流提取和流插入,那是友元函數.

友元函數

問題:現在我們嘗試去重載operator<<,然後發現我們沒辦法将operator<<重載成成員函數。因為cout 的

輸出流對象和隐含的this**指針在搶占第一個參數的位置。this指針預設是第一個參數也就是左操作數了。但是

實際使用中cout需要是第一個形參對象,才能正常使用。是以我們要将operator<<重載成全局函數。但是這

樣的話,又會導緻類外沒辦法通路成員,那麼這裡就需要友元來解決。operator>>同理

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    
    friend std::istream& operator>>(std::istream& in, Date& d);  // 友元函數
private:
    int _year;
    int _month;
    int _day;
};

std::istream& operator>>(std::istream& in, Date& d)
{
    in >> d._year >> d._month >> d._day;
    return in;
}      
  • 友元函數可以直接通路類的私有成員,它是定義在類外部的普通函數,不屬于任何類,但需要在類的内部聲明,聲明時需要加friend關鍵字
  • 友元函數不能用const修飾沒有this
  • 一個函數可以是多個類的友元函數

友元類

這裡我要見識一下有友元類.

從這裡我們可以知道,B是A的友元,是以通路和修改A類型對象的中成員變量.

class A
{
  friend class B;  // B是我們的朋友
public:
  A();
  

private:
  int _a;
  int _b;
};

class B
{
public:
  B();
  void fun()
  {
    cout << _a._a << _a._b << endl;
  }

private:
  A _a;
};      
這裡也有一點總結
  • 友元關系是單向的,不具有交換性 上面 B是A的友元,但是 A不是B的友元
  • 友元關系不能傳遞 如果B是A的友元,C是B的友元,則不能說明C時A的友元。

内部類

所謂的内部類就是類裡面是否可以定義一個類.

class A
{
public:
  A()
  {}
  class B
  {
  public:
    B()
    {}

  private:
    int _a; // 可以同名
  };

private:a
  int _a;
};      

内部類的大小

我們計算類的大小實際上是計算的是對象的大小,也就是裡面的成員變量,與裡面是不是有内部類是無關的的

int main()
{
  cout << sizeof(A) << endl;
  return 0;
}      
C++ 類和對象(下)

内部類的特性

内部類B是是外部類A的友元,B直接可以通路A的私有,但是A不能通路B的.

執行個體化内部類

如果内部類是public的,這裡可以通過類域來進行執行個體化,如果是private,就是天生不讓外界來看到的内部類

int main()
{
  A::B b;  //  通過類域
  return 0;
}      

再談封裝

繼續閱讀