類 (I)

資料結構概念的擴充套件:與資料結構一樣,類可以包含資料成員,但它們也可以包含函式作為成員。

一個物件是類的一個例項。從變數的角度來看,類就是型別,而物件就是變數。

類使用關鍵字class或關鍵字struct來定義,語法如下:

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


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

類具有與普通資料結構相同的格式,不同之處在於它們還可以包含函式,並且具有稱為訪問說明符的新內容。訪問說明符是以下三個關鍵字之一:privatepublicprotected。這些說明符會修改它們後面的成員的訪問許可權。

  • 類的private成員只能從同一類的其他成員(或其“朋友”)內部訪問。
  • protected成員可以從同一類的其他成員(或其“朋友”)訪問,也可以從其派生類的成員訪問。
  • 最後,public成員可以在任何可見物件的地方訪問。

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

1
2
3
4
5
6
class Rectangle {
    int width, height;
  public:
    void set_values (int,int);
    int area (void);
} rect;

聲明瞭一個名為Rectangle的類(即型別),以及該類的一個名為rect的物件(即變數)。該類包含四個成員:兩個int型別的資料成員(成員width和成員height),具有私有訪問許可權(因為私有是預設的訪問級別),以及兩個具有公共訪問許可權成員函式:函式set_valuesarea。目前我們只包含了它們的宣告,而沒有包含它們的定義。

請注意類名物件名之間的區別:在前面的示例中,Rectangle類名(即型別),而rectRectangle型別的物件。這與以下宣告中的inta的關係相同:

1
int a;

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

在聲明瞭Rectanglerect之後,物件rect的任何公共成員都可以像訪問普通函式或普通變數一樣進行訪問,只需在物件名成員名之間插入一個點(.)即可。這與訪問普通資料結構的成員的語法相同。例如:

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

rect中唯一不能從類外部訪問的成員是widthheight,因為它們具有私有訪問許可權,只能在該類的其他成員內部引用。

以下是Rectangle類的完整示例:
// classes example
#include <iostream>
using namespace std;

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

void Rectangle::set_values (int x, int y) {
  width = x;
  height = y;
}

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

此示例重新引入了作用域運算子::,兩個冒號),該運算子在前面章節中與名稱空間相關。此處用於在類外部定義類的成員函式set_values

請注意,成員函式area的定義已直接包含在類Rectangle的定義中,因為其極其簡單。相反,set_values僅在類中用其原型宣告,但其定義在類外部。在此外部定義中,使用作用域運算子(::)來指定正在定義的函式是Rectangle類的成員,而不是常規的非成員函式。

作用域運算子(::)指定了正在定義的成員所屬的類,提供了與將此函式定義直接包含在類定義中完全相同的範圍屬性。例如,上例中的函式set_values可以訪問變數widthheight,它們是Rectangle類的私有成員,因此只能從該類的其他成員(例如這個函式)訪問。

在類定義中完全定義成員函式與僅在函式中包含其宣告並在類外部稍後定義它的唯一區別是,第一種情況下編譯器自動認為該函式是內聯成員函式,而第二種情況則是普通(非內聯)類成員函式。這不會造成行為上的差異,只會影響可能的編譯器最佳化。

成員widthheight具有私有訪問許可權(請記住,如果沒有指定其他內容,使用class關鍵字定義的類的所有成員都具有私有訪問許可權)。透過將它們宣告為私有,不允許從類外部訪問。這是有意義的,因為我們已經定義了一個成員函式來為物件中的這些成員設定值:成員函式set_values。因此,程式的其餘部分不需要直接訪問它們。也許在這個如此簡單的例子中,很難看出限制對這些變數的訪問有何用處,但在更大的專案中,防止值以意外的方式(從物件的角度來看是意外的)修改可能非常重要。

類的最重要屬性是它是一個型別,因此我們可以宣告它的多個物件。例如,以Rectangle類的先前示例為例,我們可以除了物件rect之外,還可以宣告物件rectb

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

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

void Rectangle::set_values (int x, int y) {
  width = x;
  height = y;
}

int main () {
  Rectangle 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  

在這種特定情況下,類(物件的型別)是Rectangle,它有兩個例項(即物件):rectrectb。它們中的每一個都有自己的成員變數和成員函式。

請注意,呼叫rect.area()的結果與呼叫rectb.area()的結果不同。這是因為Rectangle類的每個物件都有自己的變數widthheight,因為它們(以某種方式)也有自己的函式成員set_valuearea,這些函式作用於物件自己的成員變數。

類允許使用面向物件正規化進行程式設計:資料和函式都是物件的成員,從而減少了將處理程式或其他狀態變數作為引數傳遞給函式的需要,因為它們是呼叫其成員的物件的一部分。請注意,在呼叫rect.arearectb.area時沒有傳遞任何引數。這些成員函式直接使用了它們各自物件rectrectb的資料成員。

建構函式

在前面的示例中,如果我們先呼叫set_values之前呼叫成員函式area會發生什麼?結果不確定,因為成員widthheight從未被賦值。

為了避免這種情況,類可以包含一個稱為其建構函式的特殊函式,當建立該類的新物件時,該函式會自動呼叫,允許類初始化成員變數或分配儲存空間。

此建構函式宣告方式與常規成員函式一樣,但函式名與類名匹配,並且沒有任何返回型別;甚至不是void

上述Rectangle類可以透過實現建構函式來輕鬆改進:

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

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

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

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

此示例的結果與先前示例的結果相同。但現在,Rectangle類沒有成員函式set_values,而是有一個建構函式執行類似的操作:它使用傳遞給它的引數來初始化widthheight的值。

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

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

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

請注意,建構函式的原型宣告(在類內)和後續的建構函式定義都沒有返回值;甚至不是void:建構函式從不返回值,它們只是初始化物件。

過載建構函式

與任何其他函式一樣,建構函式也可以透過採用不同引數的不同版本來過載:引數的數量不同和/或引數的型別不同。編譯器會自動呼叫引數匹配的那個:

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

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

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

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

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

在上面的示例中,構造了兩個Rectangle類的物件:rectrectbrect使用兩個引數構造,如之前的示例所示。

但此示例還引入了一種特殊的建構函式:預設建構函式預設建構函式是沒有引數的建構函式,它之所以特殊,是因為當一個物件被宣告但沒有用任何引數進行初始化時,它就會被呼叫。在上面的示例中,為rectb呼叫了預設建構函式。請注意,rectb甚至沒有用一對空括號構造——實際上,空括號不能用於呼叫預設建構函式。

1
2
Rectangle rectb;   // ok, default constructor called
Rectangle rectc(); // oops, default constructor NOT called 

這是因為一對空括號會將rectc宣告為函式,而不是物件宣告:它將是一個不接受任何引數並返回Rectangle型別值的函式。

統一初始化

如上所示,透過將引數括在括號中呼叫建構函式的方式被稱為函式式。但建構函式也可以使用其他語法呼叫:

首先,帶單個引數的建構函式可以使用變數初始化語法(等號後跟引數)呼叫:

class_name object_name = initialization_value;

最近,C++引入了使用統一初始化呼叫建構函式的方式,這本質上與函式式相同,但使用大括號({})而不是圓括號(()):

class_name object_name { value, value, value, ... }

可選地,最後一種語法可以在大括號前加上等號。

這裡有一個建構函式接受單個引數的類的物件構造的四種方法的示例:

// classes and uniform initialization
#include <iostream>
using namespace std;

class Circle {
    double radius;
  public:
    Circle(double r) { radius = r; }
    double circum() {return 2*radius*3.14159265;}
};

int main () {
  Circle foo (10.0);   // functional form
  Circle bar = 20.0;   // assignment init.
  Circle baz {30.0};   // uniform init.
  Circle qux = {40.0}; // POD-like

  cout << "foo's circumference: " << foo.circum() << '\n';
  return 0;
}
foo's circumference: 62.8319

統一初始化相對於函式式的優勢在於,與圓括號不同,大括號不會與函式宣告混淆,因此可以用於顯式呼叫預設建構函式:

1
2
3
Rectangle rectb;   // default constructor called
Rectangle rectc(); // function declaration (default constructor NOT called)
Rectangle rectd{}; // default constructor called 

呼叫建構函式的語法選擇很大程度上是風格問題。大多數現有程式碼目前使用函式式,一些較新的風格指南建議優先選擇統一初始化,儘管它也有其潛在的陷阱,因為其對initializer_list作為其型別的偏好。

建構函式中的成員初始化

當建構函式用於初始化其他成員時,這些其他成員可以直接初始化,而無需使用其體內的語句。這是透過在建構函式體之前插入冒號(:)和類成員的初始化列表來完成的。例如,考慮一個具有以下宣告的類:

1
2
3
4
5
6
class Rectangle {
    int width,height;
  public:
    Rectangle(int,int);
    int area() {return width*height;}
};

該類的建構函式可以像往常一樣定義為:

1
Rectangle::Rectangle (int x, int y) { width=x; height=y; }

但也可以使用成員初始化定義為:

1
Rectangle::Rectangle (int x, int y) : width(x) { height=y; }

甚至:

1
Rectangle::Rectangle (int x, int y) : width(x), height(y) { }

請注意,在最後一種情況下,建構函式除了初始化其成員外,什麼也不做,因此它有一個空的函式體。

對於基本型別的成員,以上任何一種建構函式定義方式都沒有區別,因為它們預設不初始化,但對於成員物件(型別是類的那些),如果它們沒有在冒號後面初始化,則它們會被預設構造。

預設構造類的所有成員可能方便也可能不方便:在某些情況下,這是浪費(當成員之後在建構函式中重新初始化時),但在其他情況下,預設構造甚至不可行(當類沒有預設建構函式時)。在這些情況下,成員應在成員初始化列表中初始化。例如:

// member initialization
#include <iostream>
using namespace std;

class Circle {
    double radius;
  public:
    Circle(double r) : radius(r) { }
    double area() {return radius*radius*3.14159265;}
};

class Cylinder {
    Circle base;
    double height;
  public:
    Cylinder(double r, double h) : base (r), height(h) {}
    double volume() {return base.area() * height;}
};

int main () {
  Cylinder foo (10,20);

  cout << "foo's volume: " << foo.volume() << '\n';
  return 0;
}
foo's volume: 6283.19

在此示例中,Cylinder類有一個成員物件,其型別是另一個類(base的型別是Circle)。由於Circle類的物件只能透過引數構造,因此Cylinder的建構函式需要呼叫base的建構函式,而唯一的方法是在成員初始化列表中進行。

這些初始化也可以使用統一初始化語法,使用大括號({})而不是圓括號(()):

1
Cylinder::Cylinder (double r, double h) : base{r}, height{h} { }

類指標

物件也可以被指標指向:一旦宣告,類就成為一個有效的型別,因此它可以被用作指標指向的型別。例如:

1
Rectangle * prect;

是指向Rectangle類物件的指標。

與普通資料結構類似,可以透過使用箭頭運算子(->)從指標直接訪問物件成員。以下是一個包含一些可能組合的示例:

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
// pointer to classes example
#include <iostream>
using namespace std;

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


int main() {
  Rectangle obj (3, 4);
  Rectangle * foo, * bar, * baz;
  foo = &obj;
  bar = new Rectangle (5, 6);
  baz = new Rectangle[2] { {2,5}, {3,6} };
  cout << "obj's area: " << obj.area() << '\n';
  cout << "*foo's area: " << foo->area() << '\n';
  cout << "*bar's area: " << bar->area() << '\n';
  cout << "baz[0]'s area:" << baz[0].area() << '\n';
  cout << "baz[1]'s area:" << baz[1].area() << '\n';       
  delete bar;
  delete[] baz;
  return 0;
}	

此示例使用了幾個運算子來操作物件和指標(運算子*&.->[])。它們可以解釋為:

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

這些表示式中的大多數都在前面的章節中介紹過。最值得注意的是,陣列章節介紹了偏移運算子([]),而普通資料結構章節介紹了箭頭運算子(->)。

使用struct和union定義的類

類不僅可以使用關鍵字class定義,還可以使用關鍵字structunion定義。

關鍵字struct通常用於宣告普通資料結構,但也可以用於宣告具有成員函式的類,語法與關鍵字class相同。兩者之間的唯一區別是,使用關鍵字struct宣告的類的成員預設具有public訪問許可權,而使用關鍵字class宣告的類的成員預設具有private訪問許可權。在所有其他方面,在這方面這兩個關鍵字是等效的。

相反,聯合的概念與使用structclass定義的類不同,因為聯合一次只能儲存一個數據成員,但儘管如此,它們也是類,因此也可以包含成員函式。聯合類中的預設訪問許可權是public
Index
目錄