C++程式設計允許程式設計師使用類別(class)定義特定程式中的資料類型。這些資料類型的實例被稱為物件 ,這些實例可以包含程式設計師定義的成員變數常數成員函數,以及多載的運算子。語法上,類似C中結構體(struct)的擴充,C中結構體不能包含函數以及多載的運算子。

C 結構體與C++ 類別的對比

在 C++ 中,結構體 是由關鍵詞 struct 定義的一種資料類型[1]。他的成員和基礎類別預設為公有的(public)。由關鍵詞 class 定義的成員和基礎類別預設為私有的(private)。這是C++中結構體和類別僅有的區別。

聚合類別

聚合類別是一種沒有用戶定義的建構函式,沒有私有(private)和保護(protected)非靜態數據成員,沒有基礎類別,沒有虛擬函式[2]。這樣的類別可以由封閉的大括號用逗號分隔開初始化列表[3]。下列的代碼在 C 和 C++ 具有相同的語法:

struct C
{
  int a;
  double b;
};

struct D
{
  int a; 
  double b;
  C c;
};

// initialize an object of type C with an initializer-list
C c = { 1, 2 };

// D has a sub-aggregate of type C. In such cases initializer-clauses can be nested
D d = { 10, 20, { 1, 2 } };

POD 結構

一個POD結構(普通舊式數據結構)是一個不包含非POD結構、非POD聯合(或者這些類別型的陣列)或參照的非靜態成員變數(靜態成員沒有限制),並且沒有用戶定義的設定運算子解構器的聚合類別。[1] 一個POD結構可以說是C struct在C++中的等價物。在大多數情況下,一個POD結構擁有和一個在C中聲明的對應的結構相同的主記憶體佈局。[4]因此,POD結構有時不正式地被稱為「C風格結構」(C-style struct)。 [5]

C結構與C++ POD結構共有的屬性

  • 數據成員被分配使得一個物件中之後的成員有着更高的地址,除非跨越了一個訪問描述符[6]
  • 兩個POD結構類別型是佈局相容的如果它們有相同數量的非靜態數據成員,而且對應的非靜態數據成員(按照順序)是佈局相容的[7]
  • 一個POD結構可以包含未命名的填充[8]
  • 一個指向POD結構物件的指標適合使用reinterpret_cast,指向其初始成員而且反之亦然,說明在POD結構的頭部不存在填充[8]
  • 一個POD結構可以被offsetof巨集使用[9]

聲明和使用

C++ 的結構體和類別具有他們自己的成員。這些成員包括變數(包括其他結構體和類別),被看做方法的函數(特定的標示符或多載的運算子),建構函式以及解構函式。成員被聲明成為公共或私有使用說明符public:private:來區分。說明符後出現的任何成員會獲得相應的訪問權限直到下一個說明符的出現。對於繼承的類別能夠使用protected:說明符。

基本聲明和成員變數

類別和結構體的聲明使用關鍵詞classstruct。成員在類別和結構體的內部聲明。

下面的代碼段實例了結構體和類別的聲明:


struct person
{
  string name;
  int age;

};
class person
{
public:
  string name;
  int age;
};

以上兩個聲明在功能上是等價的。每一段代碼都定義了一個類別型person,其含有兩個成員變數:nameage。注意,大括號後面的分號是必需的。

在其中一個聲明之後(不能同時使用兩個),person可以被用來定義新的person類別型的變數:

#include <iostream>
#include <string>
using namespace std;

class person
{
public:
  string name;
  int age;
};

int main ()
{
  person a, b;
  a.name = "Calvin";
  b.name = "Hobbes";
  a.age = 30;
  b.age = 20;
  cout << a.name << ": " << a.age << endl;
  cout << b.name << ": " << b.age << endl;
  return 0;
}

執行以上代碼將會輸出

Calvin: 30
Hobbes: 20

成員函數

成員函數是C++ 的類別和結構體的一個重要特性。這些資料類型可以包含作為其成員的函數。成員函數分為靜態成員函數與非靜態成員函數。靜態成員函數只能訪問該資料類型的物件的靜態成員。而非靜態成員函數能夠訪問物件的所有成員。在非靜態成員函數的函數體內,關鍵詞this指向了呼叫該函數的物件。這通常是通過thiscall呼叫協定,將物件的地址作為隱含的第一個參數傳遞給成員函數。[10]再次以之前的person類別型作為例子:

class person
{
  std::string name;
  int age;
public:
  person() : age(5) { }
  void print() const;
};

void person::print() const
{
  cout << name << ";" << this->age << endl;
  /* "name"和"age"是成员变量。
     "this"关键字的值是被调用对象的地址。其类型为
     const person*,原因是该函数被声明为const。
  */
}

在上面的例子中print()函數在類別中聲明,並在類別的名稱後加上::來限定它後加以定義。nameage是私有的(類別的預設修飾詞),print()被聲明為公有,由於一個被用於類別外的成員需要被申明為公有的。

通過使用成員函數print(),輸出可以被簡化為:

a.print();
b.print();

上述的ab被稱為呼叫者(sender),當print()函數被執行時每一個都參照自己的成員變數。 將類別或結構的申明(稱做介面)和定義(稱作實現)放入分開的單元是常見的做法。用戶需要的介面被放入一個標頭檔中而實現則獨立地放入原始碼或者編譯後的形式。

非靜態成員函數,可以用const或volatile關鍵詞限定。const限定的成員函數不能修改其他數據成員(除了具有mutable的例外),也不能呼叫非const限定的其他成員函數。編譯實現時,通常是在const限定的成員函數體內,this所指向的數據成員自動具有const限定,因此是唯讀的。const物件只能呼叫const成員函數;volatile物件只能呼叫volatile限定的成員函數。反之,沒有受到限定的普通物件可以呼叫所有的成員函數,不論它是否為cv限定。建構函式、解構函式不能cv限定。

繼承

非POD類別的主記憶體佈局沒有被C++標準規定。例如,許多流行的C++編譯器通過將父類別的欄位和子類別的欄位並置來實現單繼承,但是這並不被標準所需求。這種佈局的選擇使得將父類別的指標指向子類別的操作是平凡的(trivial)。

例如,考慮:

class P 
{
    int x;
};
class C : public P 
{
    int y;
};

一個P的實例和P* p指向它,在主記憶體中可能看起來像這樣:

+----+
|P::x|
+----+
↑
p

一個C的實例和P* p指向它,在主記憶體中可能看起來像這樣:

+----+----+
|P::x|C::y|
+----+----+
↑
p

因此,任何操縱P物件的欄位的代碼都可以操縱在C物件中的P欄位而不需要考慮任何關於C欄位的定義。一個正確書寫的C++程式在任何情況下都不應該對被繼承欄位的佈局有任何假定。使用static_cast或者dynamic_cast類別型轉換運算子會確保指標正確的從一個類別型轉換為另一個。

多重繼承並不那麼簡單。如果一個類別D繼承了P1P2,那麼兩個父類別的欄位需要被按照某種順序儲存,但是(在大多數情況下)只有一個父類別可以被放在子類別的頭部。每當編譯器需要將一個指向D的指標轉換為P1P2中的任一個,編譯器需要提供一個自動轉換從子類別的地址轉換為父類別欄位的地址(典型地,這是一個簡單的偏移量計算)。

關於多重繼承的更多資訊,參看虛繼承

多載運算子

C++容許程式設計師重載某些運算子,目的是補充庫中未能提供的針對特定類的運算子。同理,很多時自訂類也因為內建庫不能提供指定運算子而需要重載。

另外,當程式設計師沒有重載或定義某些運算子時,編譯器會自動地建立它們,例如三法則中的複製指定運算子(=)。

依照慣例,重載運算子時應模擬運算子本身意義的功能,例如重載運算子「*」時,程式設計師義務重載為兩數之乘法(或其他,視數學或程式上的意義)。另外,宣告一結構如integer類,當重載運算子如「*」就要回傳integer類:

struct integer 
{
    int i;
    integer(int j = 0) : i(j) {}
    integer operator*(const integer &k) const 
    {
        return integer (i * k.i);
    }
};

struct integer 
{
    int i;
   
    integer(int j = 0) : i(j) {}
 
    integer operator*(const integer &k) const;
};
 
integer integer::operator*(const integer &k) const 
{
    return integer(i * k.i);
}

在這裏,const關鍵字出現兩次。表達式const integer &k中的const關鍵字代表函數不能修改此常數值,而第二個const關鍵字代表此函數不會修改類物件本身(*this)。

integer &k之中,符號(&)表示以參照形式呼叫。當呼叫函數時會直接傳遞變數地址,並以變數本身取代這裏的變數k[11]

二元可多載運算子

二元運算子會用函數方式並以「operator 運算符」識別來進行重載,這裏的參數會是單一參數。實際使用時,二元運算子左方的變數會成為類物件本身(*this),而右方變數則成為傳入參數。

integer a = 1; 
/* 這裡的等號是其中一種二元運算符,
   我們可利用重載運算符(=)的方式
   來提供初始化功能,而左方的變數i
   就是類物件本身,右方的數字1則是
   傳入參數。 */
integer b = 3;
/* 變數名字跟類物件內的變數無關 */
integer k = a * b;
cout << k.i << endl;  //輸出3

以下是二元可重載運算子列表:

算術運算子

運算子名稱 語法
加法(總和) a + b
以加法賦值 a += b
減法(差) a - b
以減法賦值 a -= b
乘法(乘積) a * b
以乘法賦值 a *= b
除法(分之) a / b
以除法賦值 a /= b
模數(餘數) a % b
以模數賦值 a %= b

比較運算子

運算子名稱 語法
小於 a < b
小於或等於 a <= b
大於 a > b
大於或等於 a >= b
不等於 a != b
等於 a == b
邏輯 AND a && b
邏輯 OR a || b

位元運算子

運算子名稱 語法
位元左移 a << b
以位元左移賦值 a <<= b
位元右移 a >> b
以位元右移賦值 a >>= b
位元AND a & b
以位元AND賦值 a &= b
位元OR a | b
以位元OR賦值 a |= b
位元XOR a ^ b
以位元XOR賦值 a ^= b

其它運算子

運算子名稱 語法
基本賦值 a = b
逗號 a , b

運算子(=)可以被用作賦值,意思是原定功能是由右方變數抄寫內部資料到左方變數,但視乎需要也可以被用作其他用途。

每個運算子是互相獨立存在,並不依賴其他運算子。例如運算子(<)並不需要運算子(>)存在從而運作。

一元可多載運算子

一元運算子跟上述的運算子相似,只是一元運算子只會載入類物件本身(*this),而不接受其他參數。另外,一元運算子有分前置運算子和後置運算子,分別在於前置運算子會放到變數前方,後置運算子則是後方。例如負值運算子(-)和邏輯取反運算子(!)都是一元前置運算子。

以下是一元可重載運算子列表:

算術運算子

運算子名稱 語法 類型 備注
一元正號 +a 前置
前綴遞增 ++a 前置 先加1並回傳加1後的值
後綴遞增 a++ 後置 加1但回傳加1前的值
一元負號(取反) -a 前置
前綴遞減 --a 前置 先減1並回傳減1後的值
後綴遞減 a-- 後置 減1但回傳減1前的值

比較運算子

運算子名稱 語法 類型
邏輯取反 !a 前置

位元運算子

運算子名稱 語法 類型
位元一的補碼 ~a 前置

其它運算子

運算子名稱 語法 類型
間接(向下參考) *a 前置
的地址(參考) &a 前置
轉換 (type) a 前置

重載一元運算子時有區分前置和後置式,一元前置運算子按以下格式編寫:

回傳資料型態 operator 運算符 ()

而後置運算子按以下格式編寫:

回傳資料型態 operator 運算符 (參數)

括號多載

括號運算子有兩種,分別是方形括號運算子([])和圓形括號運算子(())。方形括號運算子又名陣列運算子,只能傳入單一參數,而圓形括號運算子卻可以傳入任意數量的參數。

方形括號運算子按以下格式重載:

回傳資料型態 operator[] (參數)

圓形括號運算子按以下格式重載:

回傳資料型態 operator() (參數1, 參數2, ...)

注意,參數是指定在第二個括號之中,第一個括號只是運算子符號。

建構函式

有時軟件工程師會想要他們的變數在聲明時有一個預設值。這可以通過聲明建構函式做到。

person(string N, int A) 
{
    name = N;
    age = A;
}

成員變數可以像下面的例子一樣,利用一個冒號,通過一個初始化序列初始化。這與上面不同,它進行了初始化(使用建構函式),而不是使用設定運算子。這對類別類別型來說更有效,因為它只需要直接構造;而賦值時,它們必須先使用預設建構函式進行第一次初始化,然後再賦予一個不同的值。而且一些類別型(例如參照和const類別型)不能被賦值,因而必須通過初始化序列進行初始化。

person(std::string N, int A) : name(N), age(A) {}

注意花括號不能被省略,即使為裏面為空。

預設值可以給予最後的幾個參數類別幫助初始化預設值。

person(std::string N = "", int A = 0) : name(N), age(A) {}

在上面的例子中,當沒有參數給予建構函式時,等價於呼叫以下的無參建構函式(一個預設建構函式):

person() : name(""), age(0) {}

建構函式的聲明看起來像一個名字和資料類型相同的函數。事實上,我們的確可以用函數呼叫的形式呼叫建構函式。在這種情況下一個person類別型的變數會成為返回值:

int main() 
{
    person r = person("Wales", 40);
    r.print();
}

以上的例子建立了一個臨時的person物件,然後使用複製建構函式將其賦予r。一個更好的建立物件的方式(沒有不需要的拷貝):

int main() 
{
    person r ("Wales", 40);
    r.print ();
}

具體的程式行為,可以也可以不和變數有關係,可以被作為一部分加入建構函式。

person() 
{
    std::cout << "Hello!" << endl;
}

通過以上的建構函式,當一個person變數沒有被具體的值初始化時,「Hello!」會被列印。

預設建構函式

當類別沒有定義建構函式時,預設建構函式將被呼叫。

class A { int b;};
//使用括号创建对象
A *a = new A(); //调用默认构造函数,b会被初始化为'0'
//不使用括号创建对象
A *a = new A; //仅分配内存,不调用默认构造函数,b会有一个未知值

然而如果用戶定義了這個類別的建構函數,兩個聲明都會呼叫用戶定義的建構函式。而在用戶定義的建構函式中的代碼會被執行,並且不會賦予b預設值。

解構函式

一個解構函式是一個建構函式的逆,當一個類別的一個實例被銷毀時會被呼叫,例如當一個類別在塊(一組花括號「{}」)中被構造的一個物件會在關閉括號後刪除,之後解構函式被自動呼叫。它會在清空儲存變數的主記憶體位置時被呼叫。解構函式可以在類別被銷毀時用來釋放資源,例如堆分配的主記憶體和打開的檔案。

聲明一個解構函式的符號類別似於建構函式。它沒有返回值而且方法的名稱和在類別的名稱前加上波浪線(~)相同。

~person() 
{
    cout << "I'm deleting " << name << " with age " << age << endl;
}

另外要注意的是,析構函數是不容許參數傳遞。然而,與構造函數一樣,析構函數可以被顯式調用:

int main() 
{
    person someone("Wales", 40);
    someone.~person();  //此時會輸出一次"I'm deleting Wales with age 40"

    return 0;  //第二次輸出"I'm deleting  with age 40"
}
/* 在這裡,程式結束時會自動調用析構函數,
   而person.name在第一次調用析構函數時已被清除,
   但person.age會按編譯器而定,
   沒能在第一次調用析構函數時清零。 */

建構函式與解構函式的相似點

  • 兩者都和聲明所在的類別有相同的名字。
  • 若未宣告,兩者都會執行預設的行為。意即類別在建立或刪除時,會一同分配或刪除記憶體。
  • 對衍生類別(subclass)而言,在基礎類別(superclass)的建構函式執行期間,衍生類別的建構函式還未執行;反之,在基礎類別的解構函式執行期間,衍生類別的解構函式已執行完畢。兩種情況下,都無法使用在衍生類別宣告的變數。

類別模板

類別模板,是對一批僅僅成員資料類型不同的類別的抽象,程式設計師只要為這一批類別所組成的整個類別家族建立一個類別模板,給出一套程式碼,就可以用來生成多種具體的類別,這類別可以看作是類別模板的實例,從而大大提高程式設計的效率。

屬性

C++語法試圖使一個結構的所有方面看起來像一個基本資料類型。因此,運算子多載允許結構像整數和浮點數一樣操作,結構的陣列可以通過方括號聲明(some_structure variable_name[size]),而且指向結構的指標可以通過和指向內建類別型的指標通用的方法解除參照。

主記憶體消耗

結構的主記憶體消耗至少是組成變數的主記憶體大小的總和。參考如下twonums 結構例子。

struct twonums 
{
    int a;
    int b;
};

這個結構包含兩個整型。在當前許多 C++ 編譯器中,整型預設32 位整型, 所以每個成員變數消耗 4 個位元組的主記憶體.因而整個結構至少(或者正好)消耗 8 個位元組的主記憶體,見下圖。

+----+----+
| a  | b  |
+----+----+

然而,編譯器可能在變數或者結構的結尾添加空的位, 這樣可以保證和給定的電腦結構匹配,通常是把變數添加到 32 位。如下例子所示的結構:

struct bytes_and_such
{ 
   char c;
   char C;
   short int s;
   int i;
   double d;
};

可看成

+-+-+--+--+--+--+--------+
|c|C|XX|s |  i  |   d    |
+-+-+--+--+--+--+--------+

在主記憶體中, XX 表示兩個未被使用的空位元。

因為結構可能會使用指標和陣列去聲明 或者初始化變數,結構的主記憶體消耗不一定是固定的。另外一個主記憶體消耗不固定的例子是模板結構。

位欄位

位欄位(Bit field)可以被用來定義比內建類別型還要小的類別成員變數。通過這個欄位定義的變數,只可以像使用內建的整數類別型(例如int, char, short, long...)那樣子使用。

struct A
{ 
	unsigned a:2; // 可以存储0-3的数字,占据一个int前2 bit的空间.
	unsigned b:3; // 可以存储0-7的数字,占据之后3 bit的空间.
	unsigned :0;  // 移动到下一个内置类型的末尾
	unsigned c:2; 
	unsigned :4;  // 在c和d中间加4bit的空白
	unsigned d:1;
	unsigned e:3;
};

// 内存结构
/*	4 byte int   4 byte int
	[1][2][3][4] [5][6][7][8]
	[1]                   [2]              [3]              [4]
	[a][a][b][b][b][][][] [][][][][][][][] [][][][][][][][] [][][][][][][][]

	[5]                  [6]                [7]              [8]
	[c][c][][][][][d][e] [e][e][][][][][][] [][][][][][][][] [][][][][][][][]
*/

位欄位不能在結構體中使用,它只能在使用struct或者class關鍵字定義的類別中使用。

按參照傳參

this關鍵字

complex& operator+=(const complex & c) 
{
    realPart += c.realPart;
    imagPart += c.imagPart;
    return *this;
}

參見

參考

  1. ^ 1.0 1.1 ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §9 Classes [class] para. 4
  2. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §8.5.1 Aggregates [dcl.init.aggr] para. 1
  3. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §8.5.1 Aggregates [dcl.init.aggr] para. 2
  4. ^ What's this "POD" thing in C++ I keep hearing about?. Comeau Computing. [2009-01-20]. (原始內容存檔於2009-01-19). 
  5. ^ Henricson, Mats; Nyquist, Erik. Industrial Strength C++. Prentice Hall. 1997. ISBN 0-13-120965-5. 
  6. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §9.2 Class members [class.mem] para. 12
  7. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §9.2 Class members [class.mem] para. 14
  8. ^ 8.0 8.1 ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §9.2 Class members [class.mem] para. 17
  9. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §18.1 Types [lib.support.types] para. 5
  10. ^ thiscall (C++). [2009-01-26]. (原始內容存檔於2008-12-10). 
  11. ^ integer *k不同,使用符號(*)只會傳遞變數地址,而且只能利用變數地址操作變數本身。

資料來源