跳转至

類(class)是結構體的拓展,不僅能夠擁有成員元素,還擁有成員函數。

在面向對象編程(OOP)中,對象就是類的實例,也就是變量。

C++ 中 struct 關鍵字定義的也是類,上文中的 結構體 的定義來自 C。因為某些歷史原因,C++ 保留並拓展了 struct

定義類

類使用關鍵字 class 或者 struct 定義,下文以 class 舉例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class ClassName {
  ...
};

// Example:
class Object {
 public:
  int weight;
  int value;
} e[array_length];

const Object a;
Object b, B[array_length];
Object *c;

與使用 struct 大同小異。該例定義了一個名為 Object 的類。該類擁有兩個成員元素,分別為 weight,value;並在 } 後使用該類型定義了一個數組 e

定義類的指針形同 struct

訪問説明符

不同於 struct 中的舉例,本例中出現了 public,這屬於訪問説明符。

  • public:該訪問説明符之後的各個成員都可以被公開訪問,簡單來説就是無論 類內 還是 類外 都可以訪問。
  • protected:該訪問説明符之後的各個成員可以被 類內、派生類或者友元的成員訪問,但類外 不能訪問
  • private:該訪問説明符之後的各個成員 只能類內 成員或者友元的成員訪問,不能 被從類外或者派生類中訪問。

對於 struct,它的所有成員都是默認 public。對於 class,它的所有成員都是默認 private

關於 "友元" 和 "派生類",可以參考下方摺疊框,或者查詢網絡資料進行詳細瞭解。

對於算法競賽來説,友元和派生類並不是必須要掌握的知識點。

關於友元以及派生類的基本概念

友元(friend):使用 friend 關鍵字修飾某個函數或者類。可以使得在 被修飾者 在不成為成員函數或者成員類的情況下,訪問該類的私有(private)或者受保護(protected)成員。簡單來説就是隻要帶有這個類的 friend 標記,就可以訪問私有或受保護的成員元素。

派生類(derived class):C++ 允許使用一個類作為 基類,並通過基類 派生派生類。其中派生類(根據特定規則)繼承基類中的成員變量和成員函數。可以提高代碼的複用率。

派生類似 "is" 的關係。如貓(派生類)"is" 哺乳動物(基類)。

對於上面 privateprotected 的區別,可以看做派生類可以訪問基類的 protected 的元素(public 同),但不能訪問 private 元素。

訪問與修改成員元素的值

方法形同 struct

  • 對於變量,使用 . 符號。
  • 對於指針,使用 -> 符號。

成員函數

成員函數,顧名思義。就是類中所包含的函數。

常見成員函數舉例
1
2
3
vector.push_back();
set.insert();
queue.empty();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Class_Name {
  ... type Function_Name(...) { ... }
};

// Example:
class Object {
 public:
  int weight;
  int value;

  void print() {
    cout << weight << endl;
    return;
  }

  void change_w(int);
};

void Object::change_w(int _weight) { weight = _weight; }

Object var;

該類有一個打印 Object 成員元素的函數,以及更改成員元素 weight 的函數。

和函數類似,對於成員函數,也可以先聲明,在定義,如第十四行(聲明處)以及十七行後(定義處)。

如果想要調用 varprint 成員函數,可以使用 var.print() 進行調用。

重載運算符

何為重載

C++ 允許編寫者為名稱相同的函數或者運算符指定不同的定義。這稱為 重載(overload)。

如果同名函數的參數種類、數量中的一者或多者兩兩不相同,則這些同名函數被看做是不同的。

需要注意的是:如果兩個同名函數的區別僅僅是返回值的類型不同則無法進行重載,此時編譯器會拒絕編譯!

如果在調用時不會出現混淆(指調用某些同名函數時,無法根據所填參數種類和數量唯一地判斷出被調用函數。常發生在具有默認參數的函數中),則編譯器會根據調用時所填參數判斷應調用函數。

而上述過程被稱作重載解析。

重載運算符,可以部分程度上代替函數,簡化代碼。

下面給出重載運算符的例子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Vector {
 public:
  int x, y;

  Vector() : x(0), y(0) {}

  Vector(int _x, int _y) : x(_x), y(_y) {}

  int operator*(const Vector& other) { return x * other.x + y * other.y; }

  Vector operator+(const Vector&);
  Vector operator-(const Vector&);
};

Vector Vector::operator+(const Vector& other) {
  return Vector(x + other.x, y + other.y);
}

Vector Vector::operator-(const Vector& other) {
  return Vector(x - other.x, y - other.y);
}

// 關於4,5行表示為x,y賦值,具體實現參見後文。

該例定義了一個向量類,並重載了 * + - 運算符,並分別代表向量內積,向量加,向量減。

重載運算符的模板大致可分為下面幾部分。

1
2
3
/*類定義內重載*/ 返回類型 operator符號(參數){...}

/*類定義內聲明,在外部定義*/ 返回類型 類名稱::operator符號(參數){...}

對於自定義的類,如果重載了某些運算符(一般來説只需要重載 < 這個比較運算符),便可以使用相應的 STL 容器或算法,如 sort

如要了解更多,可參見「參考資料」第四條。

可以被重載的運算符
1
2
3
4
5
6
+       -       *       /       %       ^       &
|       ~       !       =       <       >       +=
-=      *=      /=      %=      ^=      &=      |=
<<      >>      >>=     <<=     ==      !=      <=
>=      &&      ||      ++      --      ,       ->*
->      ()      []      new     new []  delete  delete []

在實例化變量時設定初始值

為完成這種操作,需要定義 默認構造函數(Default constructor)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class ClassName {
  ... ClassName(...)... { ... }
};

// Example:
class Object {
 public:
  int weight;
  int value;

  Object() {
    weight = 0;
    value = 0;
  }
};

該例定義了 Object 的默認構造函數,該函數能夠在我們實例化 Object 類型變量時,將所有的成員元素初始化為 0

若無顯式的構造函數,則編譯器認為該類有隱式的默認構造函數。換言之,若無定義任何構造函數,則編譯器會自動生成一個默認構造函數,並會根據成員元素的類型進行初始化(與定義 內置類型 變量相同)。

在這種情況下,成員元素都是未初始化的,訪問未初始化的變量的結果是未定義的(也就是説並不知道會返回和值)。

如果需要自定義初始化的值,可以再定義(或重載)構造函數。

關於定義(或重載)構造函數

一般來説,默認構造函數是不帶參數的,這區別於構造函數。構造函數和默認構造函數的定義大同小異,只是參數數量上的不同。

構造函數可以被重載(當然首次被叫做定義)。需要注意的是,如果已經定義了構造函數,那麼編譯器便不會再生成無參數的默認構造函數。這會可能會使試圖以默認方法構造變量的行為編譯失敗(指不填入初始化參數)。

使用 C++11 或以上時,可以使用 {} 進行變量的初始化。

關於 {}

使用 {} 進行初始化,會用到 std::initializer_list 這一個輕量代理對象進行初始化。

初始化步驟大概如下

  1. 嘗試尋找參數中有 std::initializer_list 的默認構造函數,如果有則調用(調用完後不再進行下面的查找,下同)。
  2. 嘗試將 {} 中的元素填入其他構造參數,如果能將參數按照順序填滿(默認參數也算在內),則調用該默認構造函數。
  3. 若無 private 成員元素,則嘗試在 類外 按照元素定義順序或者下標順序依次賦值。

上述過程只是完整過程的簡化版本,詳細內容參見 "參考資料九"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Object {
 public:
  int weight;
  int value;

  Object() {
    weight = 0;
    value = 0;
  }

  Object(int _weight = 0, int _value = 0) {
    weight = _weight;
    value = _value;
  }

  // the same as
  // Object(int _weight,int _value):weight(_weight),value(_value) {}
};

// the same as
// Object::Object(int _weight,int _value){
//   weight = _weight;
//   value = _value;
// }
//}

Object A;        // ok
Object B(1, 2);  // ok
Object C{1, 2};  // ok,(C++11)
關於隱式類型轉換

有時候會寫出如下的代碼

1
2
3
4
5
6
7
8
class Node {
 public:
  int var;

  Node(int _var) : var(_var) {}
};

Node a = 1;

看上去十分不符合邏輯,一個 int 類型不可能轉化為 node 類型。但是編譯器不會進行 error 提示。

原因是在進行賦值時,首先會將 1 作為參數調用 node::node(int),然後調用默認的複製函數進行賦值。

但大多數情況下,編寫者會希望編譯器進行報錯。這時便可以在構造函數前追加 explicit 關鍵字。這會告訴編譯器必須顯式進行調用。

1
2
3
4
5
6
class Node {
 public:
  int var;

  explicit Node(int _var) : var(_var) {}
};

也就是説 node a=1 將會報錯,但 node a=node(1) 不會。因為後者顯式調用了構造函數。當然大多數人不會寫出後者的代碼,但此例足以説明 explicit 的作用。

不過在算法競賽中,為了避免此類情況常用的是 "加強對代碼的規範程度",從源頭上避免

銷燬

這是不可避免的問題。每一個變量都將在作用範圍結束走向銷燬。

但對於已經指向了動態申請的內存的指針來説,該指針在銷燬時不會自動釋放所指向的內存,需要手動釋放動態內存。

如果結構體的成員元素包含指針,同樣會遇到這種問題。需要用到析構函數來手動釋放動態內存。

析構 函數(Destructor)將會在該變量被銷燬時被調用。重載的方法形同構造函數,但需要在前加 ~

默認定義的析構函數通常對於算法競賽已經足夠使用,通常我們只有在成員元素包含指針時才會重載析構函數。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Object {
 public:
  int weight;
  int value;
  int* ned;

  Object() {
    weight = 0;
    value = 0;
  }

  ~Object() { delete ned; }
};

為類變量賦值

默認情況下,賦值時會按照對應成員元素賦值的規則進行。也可以使用 類名稱()類名稱{} 作為臨時變量來進行賦值。

前者只是調用了複製構造函數(copy constructor),而後者在調用複製構造函數前會調用默認構造函數。

另外默認情況下,進行的賦值都是對應元素間進行 淺拷貝,如果成員元素中有指針,則在賦值完成後,兩個變量的成員指針具有相同的地址。

1
2
3
4
// A,tmp1,tmp2,tmp3類型為Object
tmp1 = A;
tmp2 = Object(...);
tmp3 = {...};

如需解決指針問題或更多操作,需要重載相應的構造函數。

更多 構造函數(constructor)內容,參見「參考資料」第六條。

參考資料

  1. cppreference class
  2. cppreference access
  3. cppreference default_constructor
  4. cppreference operator
  5. cplusplus Data structures
  6. cplusplus Special members
  7. C++11 FAQ
  8. cppreference Friendship and inheritance
  9. cppreference value initialization