Coursera課程筆記----C++程式設計----Week3

maimai_d發表於2020-05-15

類和物件(Week 3)

內聯成員函式和過載成員函式

內聯成員函式

  • inline + 成員函式
  • 整個函式題出現在類定義內部
class B{
  inline void func1(); //方式1
  void func2() //方式2
  {
    
  };
};

void B::func1(){}

成員函式的過載及引數預設

  • 過載成員函式
  • 成員函式——帶預設引數
#include<iostream>
using namespace std;
class Location{
  private:
  	intx,y;
  public:
  	void init(int x=0,int y=0); //存在2個預設引數
  	void valueX(int val) {x = val;}//1
  	int valueX(){return x;}//2
  //1和2是過載函式
}
  • 使用預設引數要注意避免有函式過載時的二義性

建構函式

基本概念

  • 成員函式的一種
    • 名字與類名相同,可以有引數,不能有返回值(void也不行)
    • 作用時對物件進行初始化,如給成員變數賦初值
    • 如果定義類的時候沒寫建構函式,則編譯器生成一個預設的無引數的建構函式
      • 預設建構函式無引數,無任何操作
    • 如果定義了建構函式,則編譯器不生成磨人的無引數的建構函式
    • 物件生成時建構函式自動被呼叫。物件一旦生成,就再也不能在其上執行建構函式
    • 一個類可以有多個建構函式
  • 為什麼需要建構函式
    1. 執行必要初始化工作,不需要專門再寫初始化函式
    2. 有時物件沒被初始化就使用,會導致程式出錯
class Complex{
  private:
  	double real,imgae;
  public:
  	void Set(double r, double i);
};//編譯器自動生成預設的建構函式

Complex c1;//預設的建構函式被呼叫
Complex* pc = new Complex;//預設的建構函式被呼叫

class Complex{
  private:
  	double real,imgae;
  public:
  	Complex(double r, double i = 0);
};
Complex::Complex(double r, double i){
  real = r;
  imag = i;
}

Complex c1; //error,缺少建構函式的引數
Complex *pc = new Complex;//error,沒有引數
Complex c1(2); //OK
Complex c1(2,4),c2(3,5);
Complex *pc = new Complex(3,4);
  • 可以有多個建構函式,引數個數或型別不同
class Complex{
  private:
  	double real,imgae;
  public:
  	void Set(double r, double i);
  	Complex(double r,double i);
  	Complex(double r);
  	Complex(Complex c1,Complexc2);
};

Complex::Complex(double r, double i)
{
  real = r;imag = i;
}

Complex::Complex(double r)
{
  real = r; imag = 0;
}
Complex::Complex(Complex c1, Complex c2)
{
  real = c1.real+c2.real;
  imag = c1.imag+c2.imag;
}
Complex c1(3),c2(1,0),c3(c1,c2);
//c1={3,0} c2={1,0} c3={4,0};

建構函式在陣列中的使用

class CSample{
  	int x;
  public:
  	CSample(){
      cout<<"Constructor 1 Called"<<endl;
    }
  	CSample(int n){
      x = n;
      cout<<"Constructor 2 Called"<<endl;
    }
};

int main()
{
  CSample array1[2]; // 1 1
  cout<<"step1"<<endl;
  CSample array2[2] = {4,5};//2 2
  cout<<"step2"<<endl;
  CSample array3[2] = {3};//2 1
  cout<<"step3"<<endl;
  CSample *array4 = new CSample[2];//1 1
  delete []array4; // 收回空間
  return 0;
}
class Test{
  public:
  	Test(int n){ }//(1)
  	Test(int n, int m){ }//(2)
  	Test(){ }//(3)
};
Test array1[3] = {1,Test(1,2)} //三個元素分別用(1),(2),(3)進行初始化
Test array2[3] = {Test(2,3),Test(1,2),1};//2 2 1
Test * pArray[3] = {new Test(4), new Test(1,2)};//1 2 ❌

複製建構函式(copy constructor)

  • 基本概念
    • 只有一個引數,即對同類物件的引用
    • 形如 X::X( X&)或X::X(const X &),二選一,後者能以常量物件作為引數
    • 如果沒有定義複製建構函式,那麼編譯器生成預設複製建構函式,預設的複製建構函式完成複製功能
    • 如果定義了複製建構函式,預設的複製建構函式將不存在。
class Complex{
  private:
  	double real,imag;
};
Complex c1;//呼叫預設無參建構函式
Complex c2(c1);//呼叫預設的複製建構函式,將c2初始化成和c1一樣
class Complex{
  public:
  	double real,imag;
  Complex(){ }
  Complex(const Complex & c){
    real = c.real;
    imag = c.imag;
    cout<<"Copy Constructor called";
  }
};
Complex c1;
Complex c2(c1);
  • 注意
    • 不允許有形如X::X(X)的建構函式
class CSample{
  CSample(CSample c){
    //error,不允許這樣的建構函式
  }
}
  • 複製建構函式起作用的三種情況

    1. 當用一個物件去初始化同類的另一個物件時

      Complex c2(c1);
      
      Complex c2 = c1; //初始化語句,非賦值語句
      
    2. 如果某函式有一個引數是類A的物件,那麼該函式被呼叫時,類A的複製建構函式將被呼叫

      class A
      {
        public:
        A(){ };
        A(A & a){
          cout<<"Copy constructor called"<<endl;
        }
      }
      
      void Func(A a1){ }
      int main(){
        A a2;
        Func(a2);
        return 0;
      }
      
    3. 如果函式的返回值時類A的物件時,則函式返回時,A的複製建構函式將被呼叫

  • 為什麼要自己寫複製建構函式

    • 後續會講解

型別轉換建構函式

  • 目的
    • 實現型別的自動轉換
  • 特點
    • 只有一個引數
    • 不是複製建構函式
  • 編譯系統會自動呼叫➡️轉換建構函式➡️建立一個臨時物件/臨時變數
class Complex{
  public:
  	double real,imag;
  	Complex(int i){//型別轉換建構函式
      cout<<"IntConstructor called"<<endl;
      real = i;
      imag = 0;
    }
  	Complex(double r, double i) //傳統建構函式
    {
      real = r;
      imag = i;
    }
};

iint main()
{
  Complex c1(7,8);//對c1進行初始化,呼叫傳統建構函式
  Complex c2 = 12; //對c2進行初始化,呼叫型別轉換建構函式,不會生成一個臨時物件
  c1 = 9; // 賦值語句,雖然賦值號兩邊型別不同,但是編譯器沒有報錯。
  //編譯器以9作為實參,呼叫型別轉換建構函式,9被自動轉換成一個臨時Complex物件,賦值給c1
}

解構函式(Destructor)

  • 回顧:建構函式
    • 成員函式的一種
    • 名字與類名相同
    • 可以有引數,不能有返回值
    • 可以有多個建構函式
    • 用來初始化物件
  • 解構函式
    • 成員函式的一種
    • 名字與類名相同(在函式名之前加'~')
    • 無引數,無返回值
    • 一個類最多隻能有一個解構函式
    • 在物件消亡時,自動被呼叫
      • 在物件消亡前做善後工作
        • 釋放分配的空間等
    • 定義類時沒寫解構函式,則編譯器生成預設解構函式
      • 不涉及釋放使用者申請的記憶體釋放等清理工作
    • 定義了解構函式,則編譯器不生成預設解構函式
class String{
  private:
  	char *p;
  public:
  String(){
    p = new char[10];
  }
  ~String();
};

String::~String(){
  delete [] p;
}
  • 解構函式和陣列
    • 物件陣列生命週期結束時,物件陣列的每個元素的解構函式都會被呼叫
  • 解構函式和運算子delete
    • delete運算導致解構函式呼叫
  • 例題總結
    • 先被構造的物件,最後被析構掉(平級的情況下)
    • 建構函式和解構函式在不同編譯器中
      • 個別呼叫情況不一致
        • 編譯器有bug
        • 程式碼優化措施
      • 課程中討論的是C++標準的規定,不牽扯編譯器的問題

靜態成員變數和靜態成員函式

基本概念

  • 靜態成員:在說明前面+static關鍵字的成員,有靜態成員變數和靜態成員函式
  • 普通成員變數每個物件有各自的一份,而靜態成員變數一共一份,所有物件共享
  • sizeof運算子不會計算靜態成員變數
  • 普通成員函式必須具體作用於某個物件,而靜態成員函式並不具體作用於某個物件
  • 靜態成員不需要通過物件就能訪問
  • 靜態成員變數本質上是全域性變數
  • 靜態成員函式本質上是全域性函式
  • 設定靜態成員這種機制,目的是將和某些類緊密相關的全域性變數和函式寫進類中,看上去像一個整體,易於維護和理解

如何訪問靜態成員

  1. 類名::成員名
  2. 物件名.成員名
  3. 指標->成員名
  4. 引用.成員名

靜態成員示例

  • 考慮一個需要隨時知道矩形總數和總面積的圖形處理程式
  • 可以用全域性變數來記錄總數和總面積(造成變數和類之間的關係不直觀,且變數能夠被其他類訪問,存在一定風險)
  • 用靜態成員將這兩個變數封裝進類中,更容易理解和維護
  • 必須在定義類的檔案中對靜態成員變數進行一次說明or初始化,否則編譯能通過,連結不能通過

注意事項

  • 在靜態成員函式中,不能訪問非靜態成員變數,也不能呼叫非靜態成員函式
  • 考慮到複製建構函式的影響

成員物件和封閉類

成員物件

  • 一個類的成員變數是另一個類的物件
  • 包含成員物件的類叫封閉類(Enclosing)
class CTyre{ //輪胎類
  private:
  	int radius;
  	int width;
  public:
  	CTyre(int r,int w):radius(r),width(w){ } //這種風格看起來更好一些
};
class CEngine{ //引擎類
}

class CCar{ //汽車類➡️“封閉類”
  private:
  	int price;//價格
  	CTyre tyre;
  	CEngine engine;
  public:
  	CCar{int p, int tr, int tw};
};
CCar::CCar(int p, int tr,int w):price(p),tyre(tr,w){ };

int main()
{
  CCar car(20000,17,225);
  return 0;
}
  • 如果CCar類不定義建構函式,則:

    • CCar car;//error➡️編譯錯誤
      
    • 編譯器不知道car.type該如何初始化

    • car.engine的初始化沒有問題,可以用預設建構函式

  • 生成封閉類物件的語句➡️明確“物件中的成員物件”➡️如何初始化

封閉類建構函式的初始化列表

  • 定義封閉類的建構函式時,新增初始化列表

    • 類名::建構函式(參數列):成員變數1(參數列),成員變數2(參數列),...

      {

      ......

      }

    • 成員物件初始化列表中的引數

      • 任意複雜的表示式
      • 函式/變數/表示式中的函式,變數有定義

呼叫順序

  • 當封閉類物件生成
    • 執行所有成員物件的建構函式
    • 執行封閉類的建構函式
  • 成員物件的建構函式呼叫順序
    • 和成員物件在類中的說明順序一致
    • 與在成員初始化列表中出現的順序無關
  • 當封閉類物件消亡
    • 執行封閉類的解構函式
    • 執行成員物件的解構函式
  • 先構造的後析構,後構造的先析構

友元

友元函式

  • 一個類的友元函式可以訪問該類的私有成員
class CCar; // 提前宣告CCar類,以便後面CDriver類使用
class CDriver{
  public:
  void ModifyCar(CCar* pCar); //改裝汽車
};
class CCar{
  private:
  	int price;
  friend int MostExpensiveCar(CCar cars[],int total); //宣告友元
  friend void CDriver::ModifyCar(CCar *pCar);
}

void CDriver::ModifyCar(CCar *pCar)
{
  pCar->price +=1000; //汽車改裝後價值增加
}
int MostExpensiveCar(CCar cars[],int total)//求最貴的汽車的價格
{
  int tmpMax = -1;
  for(int i = 0; i < total;++i)
    if(cars[i].price > tmpMax)
      tmpMax = cars[i].price;
  return tmpMax;
}
int main()
{
  return 0;
}
  • 可以將一個類的成員函式(包括構造,解構函式)定義成另一個類的友元
class B{
  public:
  	void function();
};

class A{
  friend void B::function();
};

友元類

  • A是B的友元類➡️A的成員函式可以訪問B的私有成員
    • 友元類之間的關係,不能傳遞,不能繼承
class CCar{
  private:
  	int price;
  friend class CDriver; //宣告CDriver為友元類
};
class CDriver{
  public:
  	CCar myCar;
  void ModifyCar(){
    myCar.price += 1000; //CDriver是CCar的友元類➡️可以訪問其私有成員
  }
};
int main(){return 0;}

this指標

this指標作用

  • 指向成員函式所作用的物件
  • 非靜態成員函式中可以直接使用this來代表指向該函式作用的物件的指標
class Complex{
  public:
  	double real,imag;
  	void Print(){
      cout<<real<<","<<imag;
    }
  Complex(double r, double i):real(r),imag(i){ }
  Complex AddOne(){
    this->real++; //=real++
    this->Print();//=Print()
    return *this;
  }
};

int main()
{
  Complex c1(1,1),c2(0,0);
  c2 = c1.AddOne();
  return 0;
}
class A{
  int i;
  public:
  	void Hello(){cout<<"hello"<<endl;}
};//編譯器把該成員函式編譯成機器指令後,會變成
//void Hello(A *this){cout<<"hello"<<endl;}
//如果Hello函式變成 void Hello(){cout<<i<<"hello"<<endl;}
//就會出錯

int main()
{
  A *p = NULL;
  p->Hello(); //結果會怎樣?
}//輸出:hello
//編譯器把該成員函式編譯成機器指令後,會變成
//hello(p)

注意事項

  • 靜態成員函式中不能使用this指標
  • 因為靜態成員函式並不具體作用於某個物件
  • 因此,靜態成員函式的真實的引數的個數,就是程式中寫出的引數的個數

常量物件、常量成員函式和常引用

常量物件

  • 如果不希望某個物件的值被改變,則定義該物件的時候可以在前面加const關鍵字

常量成員函式

  • 在類的成員函式說明後面可以加const關鍵字,則該成員函式成為常量成員函式
  • 常量成員函式執行期間不應修改其所作用的物件。因此,在常量成員函式中不能修改成員變數的值(靜態變數除外),也不能呼叫同類的非常量成員函式(靜態成員函式除外)
class Sample
{
  public:
  	int value;
  	void GetValue() const;
  	void func(){};
  	Sample(){}
};
void Sample::GetValue() const
{
  value = 0;//wrong
  func();//wrong
}

int main(){
  const Sample o;
  o.value = 100; //err,常量物件不可以被修改
  o.func();//err常量物件上面不能執行非常量成員函式
  o.GetValue();//ok
  return 0;
}

常量成員函式的過載

  • 兩個成員函式,名字和參數列都一樣,但是一個是const一個不是,算過載

常引用

  • 引用前面可以加const關鍵字,成為常引用。不能通過常引用,修改其引用的變數
const int & r = n;
r = 5;//error
n = 4;//ok
  • 物件作為函式的引數時,生成該引數需要呼叫複製建構函式,效率比較低。用指標做引數,會讓程式碼的可讀性變差
  • 所以可以用物件的引用作為引數。
  • 但物件引用作為引數有一定的風險,若函式中不小心修改了形參,則實參也會跟著改變,如何避免?
  • 所以可以用物件的常引用作為引數
  • 這樣函式中就能確保不會出現無意中更改形參值的語句了

練習題

注:填空題在Coursera提交時,檔案中只需出現填進去的內容即可

Quiz 1

#include<iostream>
#include<stdio.h>
#include<cstring>
#include<string>
#include<string.h>
using namespace std;
class A {
public:
    int val;
    A (int n = 0){val = n;}
    A & GetObj(){
        return *this;
    }
};
int main() {
    A a;
    cout << a.val << endl;
    a.GetObj() = 5;
    cout << a.val << endl;
}

Quiz 2

#include<iostream>
#include<stdio.h>
#include<cstring>
#include<string>
#include<string.h>
using namespace std;
class Sample{
public:
    int v;
    Sample(int n):v(n) { }
    Sample(const Sample &a)
    {
        v = a.v*2;
    }
};
int main() {
    Sample a(5);
    Sample b = a;
    cout << b.v;
    return 0;
}

Quiz 3

#include<iostream>
#include<stdio.h>
#include<cstring>
#include<string>
#include<string.h>
using namespace std;
class Base {
public:
    int k;
    Base(int n):k(n) { }
};
class Big {
public:
    int v;
    Base b;
    Big(int n = 5):v(n),b(n){ };
};
int main() {
    Big a1(5); Big a2 = a1;
    cout << a1.v << "," << a1.b.k << endl;
    cout << a2.v << "," << a2.b.k << endl;
    return 0;
}

Quiz 4 魔獸世界之一:備戰

#include <iostream>
#include <cstdio>
#include <string>
using namespace std;
const int WARRIOR_NUM = 5;
/*
string Warrior::names[WARRIOR_NUM] = { "dragon","ninja","iceman","lion","wolf" };
紅方司令部按照 iceman、lion、wolf、ninja、dragon 的順序製造武士。
藍方司令部按照 lion、dragon、ninja、iceman、wolf 的順序製造武士。
*/

class Headquarter;
class Warrior
{
private:
    Headquarter * pHeadquarter; //指向英雄所屬陣營的指標
    int kindNo;//武士的種類編號 0 dragon 1 ninja 2 iceman 3 lion 4 wolf
    int no;//英雄編號
public:
    static string names[WARRIOR_NUM]; //存放5種職業名字的陣列
    static int initialLifeValue [WARRIOR_NUM]; //存放不同英雄的起始生命值(從輸入中採集)
    Warrior( Headquarter *p,int no_,int kindNo_);//建構函式
    void PrintResult(int nTime); //執行列印資料的工作,若無法繼續建立則輸出結束並停止
};

class Headquarter
{
private:
    int totalLifeValue;
    bool stopped;
    int totalWarriorNum;
    int color;
    int curMakingSeqIdx; //當前要製造的武士是製造序列中的第幾個
    int warriorNum[WARRIOR_NUM]; //存放每種武士的數量
    Warrior * pWarriors[1000];//和每個建立的英雄建立連結
public:
    friend class Warrior;
    static int makingSeq[2][WARRIOR_NUM];//武士的製作序列,按陣營的不同分成兩個
    void Init(int color_, int lv); //初始化陣營需要顏色和總血量
    ~Headquarter();
    int Produce(int nTime); //建立英雄,輸入時間
    string GetColor();
};

Warrior::Warrior(Headquarter *p, int no_, int kindNo_) {
    no = no_;
    kindNo = kindNo_;
    pHeadquarter = p;
}

void Warrior::PrintResult(int nTime) {
    string color = pHeadquarter->GetColor();
    printf("%03d %s %s %d born with strength %d,%d %s in %s headquarter\n",
            nTime, color.c_str(), names[kindNo].c_str(),no,initialLifeValue[kindNo],
            pHeadquarter->warriorNum[kindNo],names[kindNo].c_str(),color.c_str()); // string 在printf中輸出的函式呼叫c_str()
}

void Headquarter::Init(int color_, int lv) {
    color = color_;
    totalLifeValue = lv;
    totalWarriorNum = 0;
    stopped = false;
    curMakingSeqIdx = 0;
    for (int i = 0; i < WARRIOR_NUM; i++) {
        warriorNum[i] = 0;
    }
}

Headquarter::~Headquarter() {
    for (int i = 0; i < totalWarriorNum; i++) {
        delete pWarriors[i];
    }
}

int Headquarter::Produce(int nTime) {
    if(stopped)
        return 0;
    int searchingTimes = 0;
    while(Warrior::initialLifeValue[makingSeq[color][curMakingSeqIdx]] > totalLifeValue &&
            searchingTimes < WARRIOR_NUM)
    {
        curMakingSeqIdx = (curMakingSeqIdx + 1) % WARRIOR_NUM;
        searchingTimes++;
    }
    int kindNo = makingSeq[color][curMakingSeqIdx];
    if(Warrior::initialLifeValue[kindNo] > totalLifeValue)
    {
        stopped = true;
        if(color == 0)
            printf("%03d red headquarter stops making warriors\n",nTime);
        else
            printf("%03d blue headquarter stops making warriors\n",nTime);
        return 0;
    }
    //排除所有其他條件後,開始製作士兵
    totalLifeValue -= Warrior::initialLifeValue[kindNo];
    curMakingSeqIdx =( curMakingSeqIdx + 1) % WARRIOR_NUM;
    pWarriors[totalWarriorNum] = new Warrior(this,totalWarriorNum+1,kindNo);
    warriorNum[kindNo]++;
    pWarriors[totalWarriorNum]->PrintResult(nTime);
    totalWarriorNum++;
    return 1;
}

string Headquarter::GetColor() {
    if(color == 0)
        return "red";
    else
        return "blue";
}

string Warrior::names[WARRIOR_NUM] = {"dragon","ninja","iceman","lion","wolf"};
int Warrior::initialLifeValue[WARRIOR_NUM];
int Headquarter::makingSeq[2][WARRIOR_NUM]={{2,3,4,1,0},{3,0,1,2,4}};//兩個司令部武士的製作順序序列

int main()
{
    int t;
    int m;
    Headquarter RedHead,BlueHead;
    scanf("%d", &t); //讀取case數
    int nCaseNo = 1;
    while(t--){
        printf("Case:%d\n",nCaseNo++);
        scanf("%d",&m);//讀取基地總血量
        for (int i = 0; i < WARRIOR_NUM; i++) {
            scanf("%d",&Warrior::initialLifeValue[i]);
        }
        RedHead.Init(0,m);
        BlueHead.Init(1,m);
        int nTime = 0;
        while (true){
            int tmp1 = RedHead.Produce(nTime);
            int tmp2 = BlueHead.Produce(nTime);
            if( tmp1 == 0 && tmp2 == 0)
                break;
            nTime++;
        }
    }
    return 0;
}
//老師給的答案讀了好幾遍,大概捋順了……
//現階段自己根本寫不出來這種程式,在第一步抽象出兩個類這塊就感覺很困難
//慢慢加油吧……

相關文章