跳轉到內容

C++ 程式設計/類

來自 Wikibooks,開放世界中的開放書籍

類用於建立使用者定義型別。類的一個例項稱為物件,程式可以包含任意數量的類。與其他型別一樣,物件型別區分大小寫。

類提供封裝,如面向物件程式設計 (OOP) 正規化中所定義的那樣。類可以與其關聯資料成員和函式成員。與內建型別不同,類可以包含多個變數和函式,這些稱為成員。

類還在程式編寫中提供“分而治之”方案的靈活性。換句話說,一位程式設計師可以編寫一個類並保證一個介面。另一個程式設計師可以使用該預期介面編寫主程式。這兩部分組合在一起並編譯以供使用。

注意
從技術角度來看,結構體和類實際上是一樣的。結構體可以在任何可以放置類的任何地方使用,反之亦然,唯一的技術差異是,類成員預設設定為private,而結構體成員預設設定為public。結構體可以透過在結構體開頭新增關鍵字 private 來使其像類一樣工作。除此之外,這主要是一種約定差異。

C++ 標準沒有對方法的定義。在與其他語言使用者討論時,使用方法來表示成員函式有時會讓人感到困惑或出現解釋問題,例如將靜態成員函式稱為靜態方法。一些 C++ 程式設計師在非正式場合甚至經常使用方法一詞來專門指代虛擬成員函式。

定義

class MyClass
{
 /* public, protected and private
 variables, constants, and functions */
};

MyClass 型別的物件(區分大小寫)宣告使用

MyClass object;
  • 預設情況下,所有類成員最初都是private
  • 關鍵字publicprotected 允許訪問類成員。
  • 類不僅包含資料成員,還包含用於操作這些資料的函式。
  • 類用作 OOP 的基本構建塊(這是約定上的區別,而不是語言強制語義的區別)。
可以建立類
  • 在呼叫 main() 之前。
  • 在呼叫宣告物件的函式時。
  • 當使用“new”運算子時。
類名
  • 根據類本身的名稱為類命名。如果無法確定名稱,則設計系統不夠完善。
  • 超過三個詞的複合名稱表明您的設計可能會混淆系統中的各種實體。重新審視您的設計。嘗試使用 CRC 卡會話來檢視您的物件是否承擔了超出其應有範圍的責任。
  • 避免將類命名為類似於其派生類的名稱的誘惑。類應獨立存在。宣告具有類型別的物件不依賴於類的派生位置。
  • 字尾或字首有時很有用。例如,如果您的系統使用代理,則將某物命名為 DownloadAgent 會傳達真實資訊。
資料抽象

面向物件 (OO) 的基本概念建議物件不應該暴露其任何實現細節。這樣,您可以在不更改使用該物件的程式碼的情況下更改實現。類透過設計允許其程式設計師隱藏(以及防止更改)類的實現方式。此強大的工具允許程式設計師構建“預防性”措施。類中的變數在類的功能中經常發揮著非常重要的作用,因此變數可以在類的private 部分中得到保護。

訪問標籤

[編輯 | 編輯原始碼]

訪問標籤PublicProtectedPrivate 用於類中,用於設定該部分中成員的訪問許可權。預設情況下,所有類成員最初都是private。標籤可以以任何順序排列。這些標籤可以在類宣告中多次使用,適用於需要多個此類型別組的情況。訪問標籤將保持活動狀態,直到使用另一個訪問標籤更改許可權。

我們已經提到類可以在其“內部”包含成員函式;我們將在後面詳細瞭解它們。這些成員函式可以訪問和修改類中所有的資料和成員函式。因此,許可權標籤用於限制對位於類外部的成員函式以及其他類的訪問許可權。

例如,類“Bottle”可以包含一個私有變數fill,表示 0-3 dl 的液體水平。fill 無法直接修改(編譯器錯誤),但Bottle 提供成員函式 sip() 以將液體水平降低 1。Mywaterbottle 可以是該類的例項,即物件。

/* Bottle - Class and Object Example */
#include <iostream>
#include <iomanip>
 
using namespace std;
 
class Bottle
{
  private:      // variables are modified by member functions of class
  int iFill;    // dl of liquid
     
  public:
    Bottle()    // Default Constructor
    : iFill(3)  // They start with 3 dl of liquid
      {
        // More constructor code would go here if needed.
      }
     
     bool sip() // return true if liquid was available
     {

       if (iFill > 0)
       {
         --iFill;
         return true;
       }
       else
       {
         return false;
       }

     }
 
     int level() const  // return level of liquid dl
     {
         return iFill;
     }
 };  // Class declaration has a trailing semicolon
 
int main()
{
  // terosbottle object is an instance of class Bottle
  Bottle terosbottle;
  cout << "In the beginning, mybottle has "
       << terosbottle.level()
       << "  dl of liquid"
       << endl;
   
  while (terosbottle.sip())
  {
     cout << "Mybottle has "
          << terosbottle.level()
          << " dl of liquid"
          << endl;
  }
   
  return 0;
}

這些關鍵字private、public 和 protected 影響成員的許可權——無論是函式還是變數。

此標籤表示“public”部分中的任何成員可以在宣告物件的任何範圍內自由訪問。

注意
避免宣告公共資料成員,因為這樣做會導致不可預見的問題。

定義為私有的成員只能在定義它們的類或友元類中訪問。通常是成員變數和輔助函式的領域。通常將函式放在這裡,然後根據需要將它們移動到更高的訪問級別,以降低複雜性。

注意
經常被忽視的是,同一個類的不同例項可以訪問彼此的私有或受保護的變數。這種情況在複製建構函式中很常見。

(這是一個預設複製建構函式將執行相同操作的示例。)

class Foo
{
 public:
   Foo(const Foo &f)
   {
     m_iValue = f.m_iValue; // perfectly legal
   }
 
 private:
   int m_iValue;
};

protected

[編輯 | 編輯原始碼]

受保護的標籤對繼承具有特殊含義,受保護的成員在定義它們的類中以及在從該基類繼承的類或它的友元中都可以訪問。在關於繼承的部分,我們將看到更多關於它的內容。

注意
同一個類的其他例項可以訪問受保護的欄位 - 只要兩個類是相同型別。但是,子類的例項不能訪問父類例項的受保護欄位或方法。

繼承(派生)

[edit | edit source]

如早前介紹程式設計正規化時所見,繼承是一種描述物件型別或類之間關係的屬性。它是 OOP 的一個特性,在 C++ 中,類共享此屬性。

派生是使用繼承屬性建立新類的行為。可以從另一個類甚至多個類派生一個類(多重繼承),就像一棵樹,我們可以將基類稱為根,將子類稱為任何葉子;在任何其他情況下,派生自另一個類的每個類都將存在父子關係。

基類

基類是一個類,它是在有意地從它派生其他類的情況下建立的。

子類

子類是從另一個類派生的類,現在將成為它的父類。

父類

父類是我們用來建立作為子類引用的類的最近類。

例如,假設您正在建立一個遊戲,其中使用不同的汽車,並且您需要為警察和玩家使用特定型別的汽車。兩種汽車型別都具有類似的屬性。主要區別(在本示例情況下)是警車的頂部會有警笛,而玩家的汽車不會。

準備警車和玩家車的其中一種方法是為警車和玩家車建立單獨的類,如下所示

class PlayerCar {
   private:
     int color;
  
   public:
     void driveAtFullSpeed(int mph){
       // code for moving the car ahead
     }
};

class PoliceCar {
private:
  int color;
  bool sirenOn;  // identifies whether the siren is on or not
  bool inAction; // identifies whether the police is in action (following the player) or not
  
public:
  bool isInAction(){
    return this->inAction;
  }

  void driveAtFullSpeed(int mph){
    // code for moving the car ahead
  }
  
};

然後為兩輛車建立單獨的物件,如下所示

PlayerCar player1;
PoliceCar policemen1;

所以,除了您很容易注意到的一個方面:上述兩個類中有一些程式碼部分非常相似(如果不是完全相同)。本質上,您必須在兩個不同的位置輸入相同的程式碼!當您更新程式碼以包含用於handBrake()pressHorn() 的方法(函式)時,您必須在上面的兩個類中都這樣做。

因此,為了避免在單個專案中的多個位置編寫相同程式碼的這種令人沮喪(且令人困惑)的任務,您使用繼承。

現在您已經瞭解了繼承在 C++ 中解決哪些問題,讓我們檢查如何在程式中實現繼承。顧名思義,繼承允許我們建立新的類,這些類自動具有現有類中的所有程式碼。這意味著,如果有一個名為MyClass 的類,就可以建立一個名為MyNewClass 的新類,該類將具有MyClass 類中存在的所有程式碼。以下程式碼段顯示了所有內容

class MyClass {
  protected:
         int age;
  public:
         void sayAge(){
             this->age = 20;
             cout << age;
         }
};

class MyNewClass : public MyClass {

};

int main() {
  
  MyNewClass *a = new MyNewClass();
  a->sayAge();
  
  return 0;
  
}

如您所見,使用冒號“:”我們可以從現有類中繼承一個新類。就這麼簡單!MyClass 類中的所有程式碼現在都可供MyNewClass 類使用。如果您足夠聰明,您已經可以看出它提供的優勢。如果您像我一樣(即不太聰明),您可以檢視以下程式碼段來了解我的意思

class Car {
  protected:
         int color;
         int currentSpeed;
         int maxSpeed;
  public:
         void applyHandBrake(){
             this->currentSpeed = 0;
         }
         void pressHorn(){
             cout << "Teeeeeeeeeeeeent"; // funny noise for a horn
         }
         void driveAtFullSpeed(int mph){
              // code for moving the car ahead;
         }
};

class PlayerCar : public Car {

};

class PoliceCar : public Car {
  private:
         bool sirenOn;  // identifies whether the siren is on or not
         bool inAction; // identifies whether the police is in action (following the player) or not
  public:
         bool isInAction(){
             return this->inAction;
         }
};

在上面的程式碼中,新建立的兩個類PlayerCarPoliceCar 都是從Car 類繼承的。因此,Car 類中的所有方法和屬性(變數)都可供玩家車和警車的這兩個新建立的類使用。從技術上講,在本例中,C++ 中的Car 類是我們的“基類”,因為它是其他兩個類所基於(或繼承自)的類。

這裡需要注意的一件事是關鍵字protected 而不是通常的private 關鍵字。這沒什麼大不了的:當我們想確保我們在基類中定義的變數應該在從該基類繼承的類中可用時,我們使用protected。如果您在Car 類的類定義中使用private,則您將無法在繼承的類中繼承這些變數。

類繼承有三種類型:public、private 和 protected。我們使用關鍵字public 來實現公有繼承。使用關鍵字 public 從基類繼承的類將所有公有成員作為公有成員繼承,受保護的資料作為受保護資料繼承,私有資料被繼承,但不能直接被類訪問。

以下示例顯示了從基類 Form “公開”繼承的 Circle 類

class Form {
private:
  double area;

public:
  int color;

  double getArea(){
    return this->area;
  }

  void setArea(double area){
    this->area = area;
  }

};

class Circle : public Form {
public:
  double getRatio() {
    double a;
    a = getArea();
    return sqrt(a / 2 * 3.14);
  }

  void setRatio(double diameter) {
    setArea( pow(diameter * 0.5, 2) * 3.14 );
  }

  bool isDark() {
    return (color > 10);
  }

};

新類 Circle 繼承了來自基類 Form 的屬性 area(屬性 area 隱式地是 Circle 類的屬性),但不能直接訪問它。它是透過函式 getArea 和 setArea 來實現的(這些函式在基類中是公有的,並且在派生類中保持為公有)。然而,color 屬性被繼承為一個公有屬性,並且該類可以直接訪問它。

下表顯示了三種不同型別的繼承中屬性的繼承方式

基類中的訪問說明符
private protected public
私有繼承 該成員不可訪問。 該成員是私有的。 該成員是私有的。
受保護繼承 該成員不可訪問。 該成員是受保護的。 該成員是受保護的。
公有繼承 該成員不可訪問。 該成員是受保護的。 該成員是公有的。

如上表所示,受保護的成員在公有繼承中被繼承為受保護的方法。因此,當我們想要宣告一個方法在類外部不可訪問,並且不希望在派生類中丟失對它的訪問許可權時,我們應該使用受保護標籤。但是,有時失去可訪問性可能很有用,因為我們正在封裝基類中的詳細資訊。

讓我們假設我們有一個類,其中包含一個非常複雜的方法“m”,它呼叫在類中宣告為私有的許多輔助方法。如果我們從它派生一個類,我們不應該理會那些方法,因為它們在派生類中不可訪問。如果一個不同的程式設計師負責派生類的設計,允許訪問這些方法可能會導致錯誤和混亂。因此,每當我們可以使用私有標籤來設計具有相同結果的設計時,最好避免使用受保護標籤。

現在還有一個額外的“語法技巧”。如果基/父類有一個需要引數的建構函式,那麼我們遇到了麻煩,你可能會認為。當然,直接呼叫建構函式是被禁止的,但是我們有一種特殊的語法用於此目的。方式就是當您定義傳遞類的建構函式時,您像這樣呼叫父建構函式

ChildClass::ChildClass(int a, int b) : ParentClass(a, b)
{
  //Child constructor here
}

注意
避免在父建構函式呼叫中引用子類內部,因為關於類建立順序沒有保證,並且父類仍然需要初始化。一種解決方法是在父類中建立一個“初始化器”方法,這樣任何對其的呼叫都將提供這些保證。這不是最好的解決方案,通常表明設計中存在錯誤,但有時是必需的。

多重繼承

[edit | edit source]

多重繼承允許構建從多個型別或類繼承的類。這與單繼承形成對比,在單繼承中,一個類只從一個型別或類繼承。

多重繼承可能會導致一些令人困惑的情況,並且比單繼承複雜得多,因此關於其優勢是否超過其風險存在一些爭論。多年來,多重繼承一直是一個敏感問題,反對者指出其複雜性增加以及在“菱形問題”等情況下的模糊性。大多數現代 OOP 語言不允許多重繼承。

宣告的派生順序與確定建構函式的預設初始化順序和解構函式清理順序相關。

class One
{
  // class internals
};

class Two
{
  // class internals
};

class MultipleInheritance : public One, public Two
{
  // class internals
};

注意
請記住,在建立將被派生的類時,解構函式可能需要進一步考慮。

資料成員

[edit | edit source]

資料成員在類定義中以與全域性變數或函式變數相同的方式宣告。它們的作用是儲存該類的資訊,並且可以包括任何型別的成員,甚至其他使用者定義的型別。它們通常對外部使用隱藏,具體取決於所採用的編碼風格,外部使用通常透過特殊成員函式來完成。

注意
在類定義中不允許顯式初始化程式,除非它們是const static int 或列舉型別,這些型別可以具有顯式初始化程式。


Clipboard

待辦事項
新增更多資訊


this 指標

[edit | edit source]

this 關鍵字充當指向所引用類的指標。this 指標的行為類似於任何其他指標,儘管您無法更改指標本身。請閱讀有關指標和引用的部分,以瞭解有關一般指標的更多資訊。

this 指標只能在聯合體結構體的非靜態成員函式中訪問,在靜態成員函式中不可用。不需要為this 指標編寫程式碼,因為編譯器會隱式地執行此操作。在使用偵錯程式時,在程式單步執行到非靜態類函式時,您可以在某些變數列表中看到this 指標。

在以下示例中,編譯器在非靜態成員函式 int getData() 中插入了一個隱式引數this。此外,啟動呼叫的程式碼傳遞了一個隱式引數(由編譯器提供)。

class Foo
{
private:
    int iX;
public:
    Foo(){ iX = 5; };

    int getData() 
    {   
        return this->iX;  // this is provided by the compiler at compile time
    }
};

int main()
{
    Foo Example;
    int iTemp;

    iTemp = Example.getData(&Example);  // compiler adds the &Example reference at compile time

    return 0;
}

在某些情況下,程式設計師應該瞭解並使用this 指標。在過載賦值運算子時,應使用this 指標來防止災難。例如,在上面的程式碼中新增一個賦值運算子。

class Foo
{
private:
    int iX;
public:
    Foo() { iX = 5; };

    int getData()          
    {                            
        return iX;  
    }

    Foo& operator=(const Foo &RHS);
};

Foo& Foo::operator=(const Foo &RHS)
{
    if(this != &RHS)
    {    // the if this test prevents an object from copying to itself (ie. RHS = RHS;)
        this->iX = RHS.iX;     // this is suitable for this class, but can be more complex when
                               // copying an object in a different much larger class
    }

    return (*this);            // returning an object allows chaining, like a = b = c; statements
}

無論您對this 的瞭解多麼少,它在實現任何類中都很重要。

靜態資料成員

[edit | edit source]

在資料成員中使用static 說明符,將導致該成員被所有擁有者類的例項和派生類共享。要使用靜態資料成員,必須將資料成員宣告為 static 並在類宣告之外的 檔案範圍 內初始化它。

在類資料成員中使用時,該類的所有例項共享該變數的一個副本。

class Foo {
public:
  Foo() {
    ++iNumFoos;
    cout << "We have now created " << iNumFoos << " instances of the Foo class\n";
  }
private:
  static int iNumFoos;
};

int Foo::iNumFoos = 0;  // allocate memory for numFoos, and initialize it

int main() {
  Foo f1;
  Foo f2;
  Foo f3;
}

在上面的示例中,靜態類變數 numFoos 在Foo 類的所有三個例項(f1f2f3)之間共享,並跟蹤Foo 類例項化的次數。


成員函式

[edit | edit source]

成員函式可以(也應該)用於與使用者定義型別中包含的資料進行互動。使用者定義型別在程式編寫中的"分而治之" 方案中提供靈活性。換句話說,一個程式設計師可以編寫一個使用者定義型別並保證一個介面。另一個程式設計師可以用該預期的介面編寫主程式。這兩部分被放在一起並編譯以供使用。使用者定義型別提供了面向物件程式設計 (OOP) 正規化中定義的封裝

在類中,為了保護資料成員,程式設計師可以定義函式來對這些資料成員執行操作。成員函式和函式是用於引用類的名稱。函式原型在類定義中宣告。這些原型可以採用非類函式的形式,也可以採用類適用的原型。函式可以在類定義中宣告和定義。但是,大多數函式可能具有非常大的定義,並且會使類非常難以閱讀。因此,可以使用作用域解析運算子“::”在類定義之外定義函式。此作用域解析運算子允許程式設計師在其他地方定義函式。這可以使程式設計師提供一個定義類的標頭檔案.h 和一個從編譯後的.cpp 檔案構建的.obj 檔案,該檔案包含函式定義。這可以隱藏實現並防止篡改。使用者將必須重新定義每個函式才能更改實現。類中的函式可以訪問和修改(除非該函式是常量)資料成員,而無需宣告它們,因為資料成員已經在類中宣告。

簡單示例

檔案:Foo.h

// the header file named the same as the class helps locate classes within a project
// one class per header file makes it easier to keep the 
// header file readable (some classes can become large)
// each programmer should determine what style works for them or what programming standards their
// teacher/professor/employer has
 
#ifndef FOO_H
#define FOO_H
 
class Foo{
public:
  Foo();                  // function called the default constructor
  Foo( int a, int b );    // function called the overloaded constructor
  int Manipulate( int g, int h );
 
private:
  int x;
  int y;
};
 
#endif

檔案:Foo.cpp

#include "Foo.h"
 
/* these constructors should really show use of initialization lists
Foo::Foo() : x(5), y(10)
{
}
Foo::Foo(int a, int b) : x(a), y(b)
{
}
*/
Foo::Foo(){
  x = 5;
  y = 10;
}
Foo::Foo( int a, int b ){
  x = a;
  y = b;
}

int Foo::Manipulate( int g, int h ){
  x = h + g*x;
  y = g + h*y;
}

過載

[edit | edit source]

成員函式可以被過載。這意味著多個成員函式可以在同一個作用域記憶體在相同的名稱,但必須具有不同的簽名。成員函式的簽名由成員函式的名稱、成員函式的引數型別和順序組成。

由於名稱隱藏,如果派生類中的成員與基類中的成員共享相同的名稱,它們將對編譯器隱藏。要使這些成員可見,可以使用宣告從基類作用域中引入它們。

建構函式和其他類成員函式(解構函式除外)可以被過載。

建構函式

[edit | edit source]

建構函式是一個特殊的成員函式,它在每次建立類的新的例項時都會被呼叫。編譯器在新的物件已在記憶體中分配並且將該“原始”記憶體轉換為適當的型別化物件後呼叫建構函式。建構函式的宣告方式與普通成員函式非常相似,但它將共享類名並且沒有返回值。

建構函式負責類操作所需的幾乎所有執行時設定。它的一般主要目的是在物件例項化(宣告物件時)定義資料成員,它們也可以具有引數,如果程式設計師選擇的話。如果建構函式具有引數,那麼在使用new 運算子時,也應將它們新增到該類任何其他物件的宣告中。建構函式也可以被過載。

Foo myTest;                 // essentially what happens is:  Foo myTest = Foo();
Foo myTest( 3, 54 );        // accessing the overloaded constructor
Foo myTest = Foo( 20, 45 ); // although a new object is created, there are some extra function calls involved
                            // with more complex classes, an assignment operator should
                            // be defined to ensure a proper copy (includes ''deep copy'')
                            // myTest would be constructed with the default constructor, and then the
                            // assignment operator copies the unnamed Foo( 20, 45 ) object to myTest

使用new 以及建構函式

Foo* myTest = new Foo();           // this defines a pointer to a dynamically allocated object
Foo* myTest = new Foo( 40, 34 );   // constructed with Foo( 40, 34 )
// be sure to use delete to avoid memory leaks

注意

雖然使用new 建立物件沒有風險,但通常最好避免在物件的建構函式中使用記憶體分配函式。具體來說,使用 new 建立一個物件陣列,其中每個物件也使用 new 在其構造期間分配記憶體,通常會導致執行時錯誤。如果類或結構包含必須指向動態建立物件的成員,最好依次初始化這些父物件的陣列,而不是將任務留給它們的建構函式。
這在使用異常(在異常處理中)編寫程式碼時尤其重要,如果在建構函式完成之前丟擲異常,則不會為該物件呼叫關聯的解構函式。

建構函式可以委託給另一個(在 C++ 11 中引入)。也認為減少預設引數的使用是可取的,如果維護者必須編寫和維護多個建構函式,這會導致程式碼重複,從而降低可維護性,因為可能會引入不一致甚至導致程式碼膨脹。

預設建構函式

預設建構函式是可以不帶任何引數呼叫的建構函式。最常見的情況是,預設建構函式在沒有引數的情況下宣告,但如果所有這些引數都被賦予預設值,則具有引數的建構函式也可以是預設建構函式。

為了建立類型別的物件陣列,該類必須有一個可訪問的預設建構函式;C++ 沒有語法指定陣列元素的建構函式引數。

過載建構函式

[edit | edit source]

當類的物件被例項化時,類編寫者可以提供各種建構函式,每個建構函式都有不同的目的。一個大型類將具有許多資料成員,其中一些資料成員在物件被例項化時可能被定義,也可能不被定義。無論如何,每個專案都會有所不同,因此程式設計師在提供建構函式時應該調查各種可能性。

這些都是類 myFoo 的建構函式。

 
myFoo(); // default constructor, the user has no control over initial values
         // overloaded constructors

myFoo( int a, int b=0 ); // allows construction with a certain 'a' value, but accepts 'b' as 0
                         // or allows the user to provide both 'a' and 'b' values
 // or
 
myFoo( int a, int b ); // overloaded constructor, the user must specify both values

class myFoo {
private:
  int Useful1;
  int Useful2;

public:
  myFoo(){                     // default constructor
           Useful1 = 5;
           Useful2 = 10;  
          };

  myFoo( int a, int b = 0 ) { // two possible cases when invoked
         Useful1 = a;
         Useful2 = b;
  };

};
 
myFoo Find;           // default constructor, private member values Useful1 = 5, Useful2 = 10
myFoo Find( 8 );      // overloaded constructor case 1, private member values Useful1 = 8, Useful2 = 0
myFoo Find( 8, 256 ); // overloaded constructor case 2, private member values Useful1 = 8, Useful2 = 256

建構函式初始化列表

[edit | edit source]

建構函式初始化列表(或成員初始化列表)是使用非預設建構函式初始化資料成員和基類的唯一方法。成員的建構函式包含在引數列表和建構函式主體之間(由冒號與引數列表隔開)。使用初始化列表不僅在效率方面更好,而且是保證所有資料成員初始化在進入建構函式主體之前完成的最簡單方法。

// Using the initialization list for myComplexMember_ 
MyClass::MyClass(int mySimpleMember, MyComplexClass myComplexMember)
: myComplexMember_(myComplexMember) // only 1 call, to the copy constructor
{
 mySimpleMember_=mySimpleMember; // uses 2 calls, one for the constructor of the mySimpleMember class
                                 // and a second for the assignment operator of the MyComplexClass class
}

這比在建構函式主體內部為複雜資料成員賦值更有效,因為在這種情況下,變數將使用其對應的建構函式進行初始化。

請注意,提供給成員建構函式的引數不需要是類建構函式的引數;它們也可以是常量。因此,您可以為包含具有非預設建構函式的成員的類建立預設建構函式。

示例

MyClass::MyClass() : myComplexMember_(0) { }

在建構函式中使用初始化列表初始化成員非常有用。這使得讀者很清楚地看到建構函式沒有執行邏輯。初始化的順序應該與定義基類和成員的順序一致,否則編譯時可能會出現警告。一旦開始初始化成員,請確保在所有建構函式中都保持一致,以避免混淆和潛在的 0xbaadfood 錯誤。

使用與成員同名的建構函式引數是安全的。

示例

class MyClass : public MyBaseClassA, public MyBaseClassB {
  private:
    int c;
    void *pointerMember;
  public:
    MyClass(int,int,int);
};
/*...*/
MyClass::MyClass(int a, int b, int c):
 MyBaseClassA(a)
,MyBaseClassB(b)
,c(c)
,pointerMember(NULL)
,referenceMember()
{
 //logic
}

請注意,此技術也適用於普通函式,但現在已過時,在這種情況下被視為錯誤。

注意
一個常見的誤解是,資料成員的初始化可以在建構函式體中完成。所有這種所謂的“初始化”實際上都是賦值。C++ 標準規定,所有資料成員的初始化都必須在進入建構函式體之前完成。這就是為什麼某些型別(const 型別和引用)不能被賦值,而必須在建構函式初始化列表中初始化的原因。

還應記住,類成員的初始化順序是按照它們宣告的順序,而不是在初始化列表中出現的順序。避免雞生蛋還是蛋生雞的悖論的一種方法是始終按照宣告順序將成員新增到初始化列表中。

解構函式

[edit | edit source]

解構函式與建構函式一樣,被宣告為任何正常的成員函式,但會使用與類相同的名稱,區別在於解構函式的名稱前面帶有“~”,它不能帶引數,也不能被過載。

解構函式在銷燬類物件時被呼叫。解構函式在避免資源洩漏(透過釋放記憶體)和實現 RAII 慣用法方面至關重要。在類建構函式中分配的資源通常在該類的解構函式中釋放,以便在類不再存在後將系統恢復到已知或穩定的狀態。

當物件被銷燬時,解構函式被呼叫,在它們被宣告的函式返回後,在使用delete運算子或程式結束時被呼叫。如果派生型別的物件被銷燬,首先執行最派生物件的解構函式。然後,成員物件和基類物件以其對應建構函式完成的相反順序遞迴銷燬。與結構體一樣,如果類沒有使用者宣告的解構函式,編譯器會隱式地將解構函式宣告為其類的內聯公共成員。

物件的動態型別將從最派生型別變為解構函式執行,與建構函式執行時的變化對稱。這會影響構造和銷燬過程中虛擬呼叫呼叫的函式,並導致常見的(並且合理的)建議,避免直接或間接地從物件的建構函式或解構函式呼叫物件的虛擬函式。

在關於行內函數的介紹中,我們已經看到了大部分概念,在處理成員函式時,這些概念得到了擴充套件,並有一些額外的注意事項。

如果成員函式的定義包含在類的宣告中,則該函式預設情況下被隱式地設為內聯。編譯器選項可能會覆蓋此行為。

如果在編譯時不知道物件的型別,則不能內聯對虛擬函式的呼叫,因為我們不知道要內聯哪個函式。

static關鍵字可以用四種不同的方式使用


Clipboard

待辦事項
在結構固定後,將上述連結從子部分更改為書籍位置。


靜態成員函式
[edit | edit source]

宣告為靜態的成員函式或變數在物件型別的所有例項之間共享。這意味著對於任何物件型別,成員函式或變數都只存在一個副本。

無需物件即可呼叫的成員函式

在類函式成員中使用時,該函式不接受例項作為隱式的this引數,而是像自由函式一樣執行。這意味著可以呼叫靜態類函式而不建立類的例項。

class Foo {
public:
  Foo() {
    ++numFoos;
    cout << "We have now created " << numFoos << " instances of the Foo class\n";
  }
  static int getNumFoos() {
    return numFoos;
  }
private:
  static int numFoos;
};

int Foo::numFoos = 0;  // allocate memory for numFoos, and initialize it

int main() {
  Foo f1;
  Foo f2;
  Foo f3;
  cout << "So far, we've made " << Foo::getNumFoos() << " instances of the Foo class\n";
}
命名建構函式
[edit | edit source]

命名建構函式是使用靜態成員函式的一個很好的例子。命名建構函式是用來建立類物件而不(直接)使用其建構函式的函式的名稱。這可能用於以下情況

  1. 繞過建構函式只能在簽名不同時過載的限制。
  2. 透過將建構函式設為私有,使類不可繼承。
  3. 透過將建構函式設為私有,防止堆疊分配

宣告一個使用私有建構函式建立物件並返回它的靜態成員函式。(它也可以返回指標或引用,但這種複雜性似乎毫無用處,並將此轉變為工廠模式,而不是傳統的命名建構函式。)

以下是一個儲存可以在任何溫度刻度中指定的溫度的類的示例。

class Temperature
{
    public:
        static Temperature Fahrenheit (double f);
        static Temperature Celsius (double c);
        static Temperature Kelvin (double k);
    private:
        Temperature (double temp);
        double _temp;
};

Temperature::Temperature (double temp):_temp (temp) {}

Temperature Temperature::Fahrenheit (double f)
{
    return Temperature ((f + 459.67) / 1.8);
}

Temperature Temperature::Celsius (double c)
{
    return Temperature (c + 273.15);
}

Temperature Temperature::Kelvin (double k)
{
    return Temperature (k);
}

const

[edit | edit source]

這種型別的成員函式不能修改類的成員變數。它既是給程式設計師,又是給編譯器的一個提示,表明給定的成員函式不會改變類的內部狀態;但是,任何宣告為mutable的變數仍然可以修改。

例如

 class Foo
 {
  public:
    int value() const
    {
      return m_value;
    }
 
    void setValue( int i )
    {
      m_value = i;
    }
 
  private:
    int m_value;
 };

這裡value()顯然不會改變 m_value,因此可以並且應該設為 const。但是setValue()確實修改了 m_value,因此不能設為 const。

另一個經常被忽略的微妙之處是const成員函式不能呼叫非 const 成員函式(如果你嘗試,編譯器會抱怨)。該const成員函式不能改變成員變數,而非 const 成員函式可以改變成員變數。由於我們假設非 const 成員函式會改變成員變數,const成員函式被認為永遠不會改變成員變數,並且不能呼叫改變成員變數的函式。

下面的程式碼示例解釋了const根據放置位置的不同,可以做什麼。

 class Foo
 {
 public:
    /*
     * Modifies m_widget and the user
     * may modify the returned widget.
     */
    Widget *widget();
 
    /*
     * Does not modify m_widget but the
     * user may modify the returned widget.
     */
    Widget *widget() const;
 
    /*
     * Modifies m_widget, but the user
     * may not modify the returned widget.
     */
    const Widget *cWidget();
 
    /*
     * Does not modify m_widget and the user
     * may not modify the returned widget.
     */
    const Widget *cWidget() const;
 
 private:
    Widget *m_widget;
 };

訪問器和修改器(Setter/Getter)

[edit | edit source]
什麼是訪問器?
訪問器是一個不修改物件狀態的成員函式。訪問器函式應宣告為const
Getter是訪問器的另一個常見定義,因為這種型別的成員函式的命名(GetSize())。
什麼是修改器?
修改器,也稱為修改函式,是一個至少更改一個數據成員值的成員函式。換句話說,修改物件狀態的操作。修改器也稱為“變異器”。
Setter是修改器的另一個常見定義,因為這種型別的成員函式的命名(SetSize( int a_Size ))。

注意
這些是常用的引用標籤(在標準語言中沒有定義)。

動態多型性(覆蓋)

[edit | edit source]

到目前為止,我們已經瞭解到可以透過繼承向類新增新的資料和函式。但是,如果我們想讓派生類繼承基類的方法,但要使用不同的實現呢?這就是我們談論多型性的時候,它是 OOP 程式設計中的一個基本概念。

如之前在程式設計正規化部分中所見,多型性細分為兩個概念靜態多型性動態多型性。本節重點介紹動態多型性,它在派生類覆蓋基類中宣告的函式時適用於 C++。

我們在派生類中重新定義方法來實現此概念。但是,我們在這樣做時需要考慮一些因素,因此現在必須介紹動態繫結、靜態繫結和虛擬方法的概念。

假設我們有兩個類,ABB派生自A,並重新定義了類A中存在的方法c()的實現。現在假設我們有一個類B的物件b。如何解釋指令b.c()

如果b在堆疊中宣告(沒有宣告為指標或引用),編譯器將應用靜態繫結,這意味著它會(在編譯時)解釋我們指的是B中存在c()的實現。

然而,如果我們宣告b為類A的指標或引用,編譯器在編譯時將無法知道呼叫哪個方法,因為b可以是AB型別。如果在執行時解決這個問題,將呼叫B中駐留的方法。這稱為動態繫結。如果在編譯時解決這個問題,將呼叫A中駐留的方法。這同樣是靜態繫結。

虛擬成員函式

[編輯 | 編輯原始碼]

虛擬成員函式相對簡單,但經常被誤解。該概念是設計類層次結構的必要部分,涉及子類化類,因為它決定了特定上下文中重寫方法的行為。

虛擬成員函式是類成員函式,可以在從宣告它們的類派生的任何類中重寫。然後用派生類中的一組新的實現替換成員函式體。

注意
重寫虛擬函式時,可以更改派生類成員函式的私有、受保護或公共狀態訪問狀態。

透過在方法宣告之前放置關鍵字虛擬,我們指示當編譯器必須決定是應用靜態繫結還是動態繫結時,它將應用動態繫結。否則,將應用靜態繫結。

注意
雖然在子類定義中使用虛擬關鍵字不是必需的(因為如果基類函式是虛擬的,那麼它所有子類的覆蓋也將是虛擬的),但在為將來的重複使用(用於同一專案之外)生成程式碼時,這樣做是一種良好的風格。

再說一次,透過一個例子應該更清楚

class Foo
{
public:
  void f()
  {
    std::cout << "Foo::f()" << std::endl;
  }
  virtual void g()
  {
    std::cout << "Foo::g()" << std::endl;
  }
};
 
class Bar : public Foo
{
public:
  void f()
  {
    std::cout << "Bar::f()" << std::endl;
  }
  virtual void g()
  {
    std::cout << "Bar::g()" << std::endl;
  }
};
 
int main()
{
  Foo foo;
  Bar bar;

  Foo *baz = &bar;
  Bar *quux = &bar;

  foo.f(); // "Foo::f()"
  foo.g(); // "Foo::g()"
 
  bar.f(); // "Bar::f()"
  bar.g(); // "Bar::g()"

  // So far everything we would expect...
 
  baz->f();  // "Foo::f()"
  baz->g();  // "Bar::g()"

  quux->f(); // "Bar::f()"
  quux->g(); // "Bar::g()"
 
  return 0;
}

我們對f()g()在兩個物件上的第一次呼叫很簡單。然而,事情在我們的 baz 指標變得有趣,它是一個指向 Foo 型別的指標。

f()不是虛擬因此,對f()的呼叫將始終呼叫與指標型別關聯的實現——在本例中是從 Foo 的實現。

注意
請記住,過載重寫是不同的概念。

虛擬函式呼叫在計算上比常規函式呼叫更昂貴。虛擬函式使用指標間接定址、呼叫,並且需要比普通成員函式多執行幾條指令。它們還要求包含虛擬函式的任何類/結構的建構函式初始化一個指向其虛擬成員函式的指標表。

所有這些特性都將表明效能和設計之間的權衡。應該避免在沒有現有結構需求的情況下先發制人地宣告函式為虛擬。請記住,只有在執行時才能解析的虛擬函式無法內聯。


Clipboard

待辦事項
虛擬和內聯問題示例。


注意
使用類模板可以解決使用虛擬函式的一些需求。當我們介紹模板時,將涵蓋這一點。

純虛擬成員函式

[編輯 | 編輯原始碼]

還有一種有趣的可能性。有時我們根本不想提供函式的實現,而是想要求對我們的類進行子類化的人自己提供實現。這就是純虛擬的情況。

為了指示純虛擬函式而不是實現,我們只需在函式聲明後新增一個“= 0”。

再說一次——一個例子

class Widget
{
public:
   virtual void paint() = 0;
};

class Button : public Widget
{
public:
   void paint() // is virtual because it is an override
   {
       // do some stuff to draw a button
   }
};

因為paint()是純虛擬函式在 Widget類中,我們必須在所有具體子類中提供實現。如果我們不這樣做,編譯器將在構建時給出錯誤。

這對於提供介面很有幫助——我們期望從基於特定層次結構的所有物件中獲得的東西,但當我們想要忽略實現細節時。

那麼,這有什麼用呢?

讓我們以我們上面的例子為例,我們有一個純虛擬用於繪畫。在很多情況下,我們希望能夠使用小部件而不必擔心它是什麼型別的小部件。繪畫是一個簡單的例子。

假設我們的應用程式中有一些東西會在小部件變為活動時重新繪製它們。它只會使用指向小部件的指標——即Widget *activeWidget() const可能是一個可能的函式簽名。因此,我們可能會做類似的事情

Widget *w = window->activeWidget();
w->paint();

我們實際上想要呼叫適合“真實”小部件型別的 paint 成員函式——而不是Widget::paint()(這是一個“純”虛擬,如果使用虛擬分派呼叫,會導致程式崩潰)。透過使用虛擬函式,我們確保呼叫子類的成員函式實現——Button::paint()在本例中——將被呼叫。


Clipboard

待辦事項
提到介面類


協變返回型別

[編輯 | 編輯原始碼]

協變返回型別是派生類中的虛擬函式能夠返回指向自身例項的指標或引用,如果基類中的方法版本也這樣做。例如

class base
{
public:
  virtual base* create() const;
};

class derived : public base
{
public:
  virtual derived* create() const;
};

這允許避免強制轉換。

注意
一些較舊的編譯器不支援協變返回型別。對於此類編譯器,存在解決方法。

虛擬建構函式

[編輯 | 編輯原始碼]

有一個以基類Foo為基礎的類層次結構。給定一個屬於該層次結構的物件bar,希望能夠執行以下操作

  1. 建立一個物件baz,該物件與bar(例如,類Bar)相同,使用該類的預設建構函式進行初始化。通常使用的語法是
    Bar* baz = bar.create();
  2. 建立一個物件baz,該物件與bar,它是bar的副本。通常使用的語法是
    Bar* baz = bar.clone();

在類Foo中,方法Foo::create()Foo::clone()宣告如下

class Foo
{
    // ...

    public:
        // Virtual default constructor
        virtual Foo* create() const;

        // Virtual copy constructor
        virtual Foo* clone() const;
};

如果Foo將用作抽象類,則這些函式可以被設定為純虛擬

class Foo
{
   // ...

    public:
        virtual Foo* create() const = 0;
        virtual Foo* clone() const = 0;
};

為了支援建立預設初始化的物件和建立副本物件,每個類Bar在層次結構中都必須具有公有的預設建構函式和複製建構函式。Bar的虛擬建構函式定義如下

class Bar : ... // Bar is a descendant of Foo
{
    // ...

    public:
    // Non-virtual default constructor
    Bar ();
    // Non-virtual copy constructor
    Bar (const Bar&);

    // Virtual default constructor, inline implementation
    Bar* create() const { return new Foo (); }
    // Virtual copy constructor, inline implementation
    Bar* clone() const { return new Foo (*this); }
};

上面的程式碼使用協變返回型別。如果您的編譯器不支援Bar* Bar::create(),請使用Foo* Bar::create()代替,對於clone().

也是如此。在使用這些虛擬建構函式時,您必須透過呼叫delete baz;手動釋放建立的物件。如果在返回型別中使用智慧指標(例如std::unique_ptr<Foo>)而不是普通的Foo*.

,可以避免這種麻煩。請記住,無論Foo是否使用動態分配的記憶體,您必須定義解構函式virtual ~Foo ()並將其設定為虛擬,以處理使用指向祖先型別的指標的物件的釋放。

虛擬解構函式

[編輯 | 編輯原始碼]

尤其要記住在任何基類中定義虛擬解構函式,即使它為空,因為如果不這樣做,將導致編譯器生成的預設析構函數出現問題,該解構函式將不是虛擬的。

在派生類中重新定義時,不會重寫虛擬解構函式,每個解構函式的定義是累積的,它們從最後一個派生類開始,一直到第一個基類。

純虛擬解構函式

[編輯 | 編輯原始碼]

每個抽象類都應包含純虛擬解構函式的宣告。

純虛擬解構函式是純虛擬函式(旨在在派生類中重寫)的特例。它們必須始終被定義,並且該定義應始終為空。

class Interface {
public:
  virtual ~Interface() = 0; //declaration of a pure virtual destructor 
};

Interface::~Interface(){} //pure virtual destructor definition (should always be empty)

三法則

[編輯 | 編輯原始碼]

“三法則”並不真正是一條法則,而是一項準則:如果類需要顯式宣告的複製建構函式、複製賦值運算子或解構函式,那麼它通常需要這三者。

這個規則有一些例外(或者,換個角度來說,是改進)。例如,有時會顯式宣告解構函式只是為了使其成為virtual;在這種情況下,不一定需要宣告或實現複製建構函式和複製賦值運算子。

大多數類不應該宣告任何“三大”操作;管理資源的類通常需要這三者。

子包容屬性

[編輯 | 編輯原始碼]

子型別化是所有位於類層次結構中的物件都必須滿足的屬性:基類的物件可以被從它派生的物件替換(直接或間接)。所有哺乳動物都是動物(它們是從動物派生的),所有貓都是哺乳動物。因此,由於子型別化屬性,我們可以將任何哺乳動物“視為”動物,將任何貓“視為”哺乳動物。這意味著抽象,因為當我們將哺乳動物“視為”動物時,我們應該知道的唯一資訊是它活著,它生長等等,但與哺乳動物無關的資訊除外。

當我們使用指向位於類層次結構中的物件的指標或引用時,將在 C++ 中應用此屬性。換句話說,指向動物類的指標可以指向動物類、哺乳動物類或貓類的物件。

讓我們繼續我們的例子

 //needs to be corrected
  enum AnimalType {
       Herbivore,
       Carnivore,
       Omnivore,
  };
 
  class Animal {
        public:
               AnimalType Type;
               bool bIsAlive;
               int iNumberOfChildren;
  };
 
  class Mammal : public Animal{
        public:
               int iNumberOfTeats;
  };
 
  class Cat : public Mammal{
        public:
              bool bLikesFish;  // probably true
  };
 
  int main() {
      Animal* pA1 = new Animal;
      Animal* pA2 = new Mammal;
      Animal* pA3 = new Cat;
      Mammal* pM  = new Cat;
      
      pA2->bIsAlive = true;     // Correct
      pA2->Type = Herbivore;    // Correct
      pM->iNumberOfTeats = 2;   // Correct
 
      pA2->iNumberOfTeats = 6;  // Incorrect
      pA3->bLikesFish = true;   // Incorrect
      
      Cat* pC = (Cat*)pA3;      // Downcast, correct (but very poor practice, see later)
      pC->bLikesFish = false;   // Correct (although it is a very awkward cat)
   }

在示例的最後幾行中,有一個指向動物的指標強制轉換為指向的指標。這被稱為“向下強制轉換”。向下強制轉換是有用的,應該使用,但首先我們必須確保我們正在強制轉換的物件確實是我們要強制轉換到的型別。將基類向下強制轉換到不相關的類是一個錯誤。為了解決這個問題,應該使用強制轉換運算子dynamic_cast<> 或 static_cast<>。它們會正確地將物件從一個類強制轉換為另一個類,如果類型別不相關,則會丟擲異常。例如,如果你嘗試

Cat* pC = new Cat;

motorbike* pM = dynamic_cast<motorbike*>(pC);

那麼應用程式將丟擲異常,因為貓不是摩托車。Static_cast 非常相似,只是它會在編譯時執行型別檢查。如果您有一個不確定其型別的物件,那麼您應該使用dynamic_cast,並準備好處理強制轉換時的錯誤。如果您正在向下強制轉換您知道型別的物件,那麼您應該使用static_cast。不要使用舊式的 C 強制轉換,因為如果強制轉換的型別不相關,它們只會給你一個訪問衝突。

區域性類

[編輯 | 編輯原始碼]

區域性類是指在特定語句塊中定義的任何類,例如在函式中。這就像定義任何其他類一樣,但是區域性類不能訪問非靜態區域性變數,也不能用於定義靜態資料成員。這些型別的類在模板函式中特別有用,我們將在後面看到。

void MyFunction()
{
   class LocalClass
   {
   // ... members definitions ...
   };
   
   // ... any code that needs the class ...

}

使用者定義的自動型別轉換

[編輯 | 編輯原始碼]

我們已經介紹了自動型別轉換(隱式轉換),並提到了一些可以由使用者定義。

從一個類到另一個類的使用者定義的轉換可以透過在目標類中提供一個將源類作為引數的建構函式來完成,Target(const Source& a_Class) 或者透過為目標類提供一個轉換運算子,如 operator Source()

確保類的物件永遠不會被複制

[編輯 | 編輯原始碼]

這需要例如防止記憶體相關問題,這些問題會在預設的複製建構函式或預設的賦值運算子意外應用於類的情況下導致。C它使用動態分配的記憶體,其中複製建構函式和賦值運算子可能是一種過度設計,因為它們不會經常被使用。

一些樣式指南建議預設情況下使所有類不可複製,並且只有在有意義的情況下才啟用複製。其他(不好的)指南說你應該始終明確編寫複製建構函式和複製賦值運算子;這實際上是一個糟糕的主意,因為它增加了維護工作量,增加了閱讀類的工作量,比使用隱式宣告的運算子更容易引入錯誤,並且對大多數物件型別來說沒有意義。一個明智的指南是思考複製對型別是否有意義;如果有意義,那麼首先更喜歡安排編譯器生成的複製操作能做正確的事情(例如,透過資源管理類而不是透過原始指標或控制代碼來儲存所有資源),如果這不可行,那麼遵守三法則。如果複製沒有意義,您可以透過以下兩種慣用法中的任何一種來禁止它。

只需聲明覆制建構函式和賦值運算子,並使它們private. 不要定義它們。由於它們不是protectedpublic,因此它們在類外部不可訪問。在類內部使用它們會給出連結器錯誤,因為它們沒有定義。

class C
{
  ...
 
  private:
    // Not defined anywhere
    C (const C&);
    C& operator= (const C&);
};

請記住,如果類為資料成員使用動態分配的記憶體,則必須在解構函式中定義記憶體釋放過程~C ()來釋放已分配的記憶體。

一個只聲明瞭這兩個函式的類可以用作私有基類,因此所有私有繼承該類的類都將禁止複製。

注意
Boost 庫的一部分,實用類boost:noncopyable執行類似的功能,更易於使用,但由於需要派生,會增加成本。

容器類

[編輯 | 編輯原始碼]

一個用於在記憶體或外部儲存中儲存物件的類通常被稱為容器類。容器類充當通用持有者,具有預定義的行為和眾所周知的介面。它也是一個支援類,其目的是隱藏用於維護記憶體中物件列表的拓撲。當它包含一組混合物件時,容器被稱為異構容器;當容器儲存一組完全相同的物件時,容器被稱為同構容器。

介面類

[編輯 | 編輯原始碼]
Clipboard

待辦事項
完整


單例類

[編輯 | 編輯原始碼]

單例 類是一個只能例項化一次的類(類似於靜態變數或函式的使用)。它是建立模式的可能實現之一,在本書的設計模式部分中得到了全面介紹。

華夏公益教科書