類 (I)

是資料結構的擴充套件概念:它不僅可以儲存資料,還可以儲存函式。

物件是類的例項化。就變數而言,類是型別,物件是變數。

類通常使用關鍵字宣告,格式如下

class class_name {
access_specifier_1:
member1;
access_specifier_2:
member2;
...
} object_names;

其中class_name是類的有效識別符號,object_names是此類的物件的可選名稱列表。 宣告的主體可以包含成員,成員可以是資料或函式宣告,以及可選的訪問說明符。

所有這些都與資料結構的宣告非常相似,只不過我們現在還可以包含函式和成員,以及一個名為訪問說明符的新事物。 訪問說明符是以下三個關鍵字之一private, public或者protected。 這些說明符修改了其後成員獲得的訪問許可權

  • private類的成員只能從同一類的其他成員或其友元中訪問。
  • protected成員可以從其同一類的成員及其友元訪問,也可以從其派生類的成員訪問。
  • 最後,public成員可以從物件可見的任何位置訪問。

預設情況下,使用關鍵字宣告的類的所有成員都對其所有成員具有私有訪問許可權。 因此,在另一個類說明符之前宣告的任何成員都會自動具有私有訪問許可權。 例如

1
2
3
4
5
6
class CRectangle {
    int x, y;
  public:
    void set_values (int,int);
    int area (void);
  } rect;

宣告一個名為CRectangle的類(即,一種型別)和一個名為rect的此類的物件(即,一個變數)。 此類包含四個成員:兩個型別為int的資料成員(成員x和成員y),具有私有訪問許可權(因為 private 是預設訪問級別),以及兩個具有公共訪問許可權的成員函式set_values()area(),目前我們僅包含它們的宣告,而沒有包含它們的定義。

請注意類名和物件名之間的區別:在前面的示例中,CRectangle是類名(即,型別),而rectCRectangle型別的物件。 它們的關係與inta在以下宣告中具有的關係相同

1
int a;

,其中int是型別名稱(類),a是變數名稱(物件)。

在前面的CRectanglerect宣告之後,我們可以在程式主體中引用物件rect的任何公共成員,就像它們是普通函式或普通變數一樣,只需放置物件的名稱,後跟一個點(.),然後是成員的名稱。 這與我們之前對普通資料結構所做的非常相似。 例如

1
2
rect.set_values (3,4);
myarea = rect.area();

我們無法從類外部的程式主體訪問 rect 的唯一成員是xy,因為它們具有私有訪問許可權,並且只能從同一類的其他成員中引用它們。

這是類 CRectangle 的完整示例

// classes example
#include <iostream>
using namespace std;

class CRectangle {
    int x, y;
  public:
    void set_values (int,int);
    int area () {return (x*y);}
};

void CRectangle::set_values (int a, int b) {
  x = a;
  y = b;
}

int main () {
  CRectangle rect;
  rect.set_values (3,4);
  cout << "area: " << rect.area();
  return 0;
}
area: 12

此程式碼中最重要的新的內容是作用域運算子(::,兩個冒號),包含在set_values()的定義中。 它用於從類定義本身之外定義類的成員。

您可能會注意到,成員函式area()的定義已直接包含在CRectangle類的定義中,因為它非常簡單,而set_values()僅在類中聲明瞭其原型,但其定義在類之外。 在此外部定義中,我們必須使用作用域運算子(::)來指定我們正在定義一個函式,該函式是類CRectangle的成員,而不是常規的全域性函式。

作用域運算子(::)指定宣告的成員所屬的類,從而授予與此函式定義直接包含在類定義中完全相同的作用域屬性。 例如,在前一個程式碼的函式set_values()中,我們能夠使用變數xy,它們是類CRectangle的私有成員,這意味著它們只能從其類的其他成員訪問。

在類中完全定義一個類成員函式,還是僅包含原型並在以後包含其定義之間的唯一區別是,在第一種情況下,該函式將自動被編譯器視為內聯成員函式,而在第二種情況下,它將是一個普通的(非內聯)類成員函式,實際上這不會在行為上產生任何差異。

成員xy具有私有訪問許可權(請記住,如果沒有其他說明,則使用關鍵字 class 定義的類的所有成員都具有私有訪問許可權)。 透過將它們宣告為私有,我們拒絕從類外部的任何位置訪問它們。 這是有道理的,因為我們已經定義了一個成員函式來為物件內的這些成員設定值:成員函式set_values()。 因此,程式的其餘部分不需要直接訪問它們。 也許在這樣一個簡單的例子中,很難看到保護這兩個變數的任何效用,但是在更大的專案中,重要的是不能以意外的方式(從物件的角度來看是意外的)修改值。

類的一大優勢是,與任何其他型別一樣,我們可以宣告它的多個物件。 例如,繼續前面的類CRectangle的示例,除了物件rectb之外,我們還可以宣告物件rect:

// example: one class, two objects
#include <iostream>
using namespace std;

class CRectangle {
    int x, y;
  public:
    void set_values (int,int);
    int area () {return (x*y);}
};

void CRectangle::set_values (int a, int b) {
  x = a;
  y = b;
}

int main () {
  CRectangle rect, rectb;
  rect.set_values (3,4);
  rectb.set_values (5,6);
  cout << "rect area: " << rect.area() << endl;
  cout << "rectb area: " << rectb.area() << endl;
  return 0;
}
rect area: 12
rectb area: 30  

。 在這種具體情況下,我們正在談論的類(物件的型別)是CRectangle,它有兩個例項或物件rectrectb。 它們中的每一個都有自己的成員變數和成員函式。

請注意,對rect.area()的呼叫與對rectb.area()的呼叫給出的結果不同。 這是因為 CRectangle 類的每個物件都有自己的變數xy,就像它們在某種程度上也有自己的函式成員set_value()area(),每個函式都使用其物件自己的變數進行操作。

這就是面向物件程式設計的基本概念:資料和函式都是物件的成員。 我們不再使用作為引數從一個函式傳遞到另一個函式的全域性變數集,而是處理具有嵌入為其成員的自有資料和函式的物件。 請注意,我們不必在對rect.area或者rectb.area的任何呼叫中提供任何引數。 這些成員函式直接使用其各自物件的資料成員rectrectb.

建構函式和解構函式

物件通常需要在建立過程中初始化變數或分配動態記憶體,才能變得可操作並避免在執行過程中返回意外的值。 例如,如果在之前的示例中,我們在呼叫函式area()之前呼叫成員函式set_values()會發生什麼? 可能會得到一個不確定的結果,因為成員xy將永遠不會被賦值。

為了避免這種情況,類可以包含一個特殊的函式,稱為建構函式,每當建立該類的新物件時,都會自動呼叫該函式。 此建構函式函式必須與類具有相同的名稱,並且不能具有任何返回型別; 甚至不能是void.

我們將要實現CRectangle,包括一個建構函式

// example: class constructor
#include <iostream>
using namespace std;

class CRectangle {
    int width, height;
  public:
    CRectangle (int,int);
    int area () {return (width*height);}
};

CRectangle::CRectangle (int a, int b) {
  width = a;
  height = b;
}

int main () {
  CRectangle rect (3,4);
  CRectangle rectb (5,6);
  cout << "rect area: " << rect.area() << endl;
  cout << "rectb area: " << rectb.area() << endl;
  return 0;
}
rect area: 12
rectb area: 30  

如您所見,此示例的結果與上一個示例相同。 但是現在我們刪除了成員函式set_values(),而是包含了一個建構函式,該建構函式執行類似的操作:它使用傳遞給它的引數來初始化寬度height的值。

請注意,在建立該類的物件時,這些引數是如何傳遞給建構函式的

1
2
CRectangle rect (3,4);
CRectangle rectb (5,6);

不能像常規成員函式一樣顯式呼叫建構函式。 它們僅在建立該類的新物件時執行。

您還可以看到,建構函式原型宣告(在類中)和後面的建構函式定義都沒有包括返回值; 甚至沒有void.

解構函式實現了相反的功能。 當物件被銷燬時,它會自動被呼叫,或者是因為它的存在範圍已經結束(例如,如果它被定義為函式中的區域性物件並且函式結束了),或者是因為它是一個動態分配的物件,並且使用運算子 delete 釋放了它。

解構函式必須與類具有相同的名稱,但在前面有一個波浪線符號(~),並且它也必須不返回值。

當物件在其生命週期內分配動態記憶體,並且在銷燬時我們想要釋放物件分配的記憶體時,解構函式的使用尤其適合。

// example on constructors and destructors
#include <iostream>
using namespace std;

class CRectangle {
    int *width, *height;
  public:
    CRectangle (int,int);
    ~CRectangle ();
    int area () {return (*width * *height);}
};

CRectangle::CRectangle (int a, int b) {
  width = new int;
  height = new int;
  *width = a;
  *height = b;
}

CRectangle::~CRectangle () {
  delete width;
  delete height;
}

int main () {
  CRectangle rect (3,4), rectb (5,6);
  cout << "rect area: " << rect.area() << endl;
  cout << "rectb area: " << rectb.area() << endl;
  return 0;
}
rect area: 12
rectb area: 30  

過載建構函式

與任何其他函式一樣,建構函式也可以過載為具有相同名稱但型別或引數數量不同的多個函式。 請記住,對於過載函式,編譯器將呼叫其引數與函式呼叫中使用的引數匹配的函式。 對於在建立物件時自動呼叫的建構函式,執行的建構函式是與物件宣告中傳遞的引數匹配的建構函式

// overloading class constructors
#include <iostream>
using namespace std;

class CRectangle {
    int width, height;
  public:
    CRectangle ();
    CRectangle (int,int);
    int area (void) {return (width*height);}
};

CRectangle::CRectangle () {
  width = 5;
  height = 5;
}

CRectangle::CRectangle (int a, int b) {
  width = a;
  height = b;
}

int main () {
  CRectangle rect (3,4);
  CRectangle rectb;
  cout << "rect area: " << rect.area() << endl;
  cout << "rectb area: " << rectb.area() << endl;
  return 0;
}
rect area: 12
rectb area: 25  

在這種情況下,rectb宣告時沒有任何引數,因此它已使用沒有引數的建構函式進行初始化,該建構函式將寬度height都初始化為值 5。

重要提示:請注意,如果我們宣告一個新物件並且想要使用其預設建構函式(沒有引數的建構函式),我們不要包含括號():

1
2
CRectangle rectb;   // right
CRectangle rectb(); // wrong! 

預設建構函式

如果您未在類定義中宣告任何建構函式,則編譯器假定該類具有一個沒有引數的預設建構函式。 因此,在聲明瞭像這樣的類之後

1
2
3
4
5
class CExample {
  public:
    int a,b,c;
    void multiply (int n, int m) { a=n; b=m; c=a*b; }
  };

編譯器假設CExample具有預設建構函式,因此您可以透過簡單地宣告它們而不帶任何引數來宣告此類的物件

1
CExample ex;

但是,只要您為類聲明瞭自己的建構函式,編譯器就不再提供隱式預設建構函式。 因此,您必須根據您為類定義的建構函式原型來宣告該類的所有物件

1
2
3
4
5
6
class CExample {
  public:
    int a,b,c;
    CExample (int n, int m) { a=n; b=m; };
    void multiply () { c=a*b; };
  };

在這裡,我們聲明瞭一個接受兩個 int 型別引數的建構函式。 因此,以下物件宣告將是正確的

1
CExample ex (2,3);
但是,
1
CExample ex;

正確,因為我們已宣告該類具有顯式建構函式,從而替換了預設建構函式。

但是,如果您沒有指定自己的建構函式,則編譯器不僅會為您建立一個預設建構函式。 它總共提供了三個特殊的成員函式,如果您沒有宣告自己的建構函式,則會隱式宣告這些函式。 這些是複製建構函式複製賦值運算子和預設解構函式。

複製建構函式和複製賦值運算子將另一個物件中包含的所有資料複製到當前物件的資料成員中。 對於CExample,編譯器隱式宣告的複製建構函式將類似於

1
2
3
CExample::CExample (const CExample& rv) {
  a=rv.a;  b=rv.b;  c=rv.c;
  }

因此,以下兩個物件宣告將是正確的

1
2
CExample ex (2,3);
CExample ex2 (ex);   // copy constructor (data copied from ex) 

指向類的指標

建立指向類的指標是完全有效的。 我們只需考慮一旦宣告,一個類就變成了一個有效的型別,因此我們可以使用類名作為指標的型別。 例如

1
CRectangle * prect;

是指向類CRectangle.

的物件的指標。 與資料結構的情況一樣,為了直接引用指標指向的物件的成員,我們可以使用間接定址的箭頭運算子(->)。 這是一個包含一些可能組合的示例

// pointer to classes example
#include <iostream>
using namespace std;

class CRectangle {
    int width, height;
  public:
    void set_values (int, int);
    int area (void) {return (width * height);}
};

void CRectangle::set_values (int a, int b) {
  width = a;
  height = b;
}

int main () {
  CRectangle a, *b, *c;
  CRectangle * d = new CRectangle[2];
  b= new CRectangle;
  c= &a;
  a.set_values (1,2);
  b->set_values (3,4);
  d->set_values (5,6);
  d[1].set_values (7,8);
  cout << "a area: " << a.area() << endl;
  cout << "*b area: " << b->area() << endl;
  cout << "*c area: " << c->area() << endl;
  cout << "d[0] area: " << d[0].area() << endl;
  cout << "d[1] area: " << d[1].area() << endl;
  delete[] d;
  delete b;
  return 0;
}
a area: 2
*b area: 12
*c area: 2
d[0] area: 30
d[1] area: 56

接下來,您將獲得一個關於如何讀取先前示例中出現的一些指標和類運算子(*, &, ., ->, [ ])的摘要

表示式可以讀作
*x由 x 指向
&xx 的地址
x.y物件 x 的成員 y
x->yx 指向的物件的成員 y
(*x).yx 指向的物件的成員 y(與上一個等效)
x[0]x 指向的第一個物件
x[1]x 指向的第二個物件
x[n]x 指向的第 (n+1) 個物件

在繼續下一節之前,請務必瞭解所有這些表示式下的邏輯。 如果您有疑問,請再次閱讀本節和/或查閱有關指標和資料結構的前幾節。

使用 struct 和 union 定義的類

類不僅可以使用關鍵字定義,還可以使用關鍵字struct聯合體.

類和資料結構的概念非常相似,因此這兩個關鍵字(struct)都可以在 C++ 中用於宣告類(即,structs 也可以在 C++ 中具有函式成員,而不僅僅是資料成員)。 兩者之間的唯一區別是,使用關鍵字struct宣告的類的成員預設具有公共訪問許可權,而使用關鍵字宣告的類的成員具有私有訪問許可權。 對於所有其他目的,這兩個關鍵字都是等效的。

聯合的概念與使用struct宣告的類的概念不同,因為聯合一次只儲存一個數據成員,但儘管如此,它們也是類,因此也可以容納函式成員。 聯合類中的預設訪問許可權是公共的。
Index
目錄