程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++拾遺(五)——類,拾遺

C++拾遺(五)——類,拾遺

編輯:C++入門知識

C++拾遺(五)——類,拾遺


  類是 C++ 中最重要的特征。C++ 語言的早期版本被命名為“帶類的 C(Cwith Classes)”,以強調類機制的中心作用。隨著語言的演變,創建類的配套支持也在不斷增加。語言設計的主要目標也變成提供這樣一些特性:允許程序定義自己的類型,它們用起來與內置類型一樣容易和直觀。

類的定義和聲明

  • 類背後蘊涵的基本思想是數據抽象封裝
  • 數據抽象是一種依賴於接口和實現分離的編程(和設計)技術。類設計者必須關心類是如何實現的,但使用該類的程序員不必了解這些細節。相反,使用一個類型的程序員僅需了解類型的接口,他們可以抽象地考慮該類型做什麼,而不必具體地考慮該類型如何工作。
  • 封裝是一項低層次的元素組合起來的形成新的、高層次實體珠技術。函數是封裝的一種形式:函數所執行的細節行為被封裝在函數本身這個更大的實體中。被封裝的元素隱藏了它們的實現細節——可以調用一個函數但不能訪問它所執行的語句。同樣地,類也是一個封裝的實體:它代表若干成員的聚焦,大多數(良好設計的)類類型隱藏了實現該類型的成員。
  • 標准庫類型 vector 同時具備數據抽象和封裝的特性。在使用方面它是抽象的,只需考慮它的接口,即它能執行的操作。它又是封裝的,因為我們既無法了解該類型如何表示的細節,也無法訪問其任意的實現制品。另一方面,數組在概念上類似於 vector,但既不是抽象的,也不是封裝的。可以通過訪問存放數組的內存來直接操縱數組。
  • 並非所有類型都必須是抽象的。標准庫中的 pair 類就是一個實用的、設計良好的具體類而不是抽象類。具體類會暴露而非隱藏其實現細節。一些類,例如 pair,確實沒有抽象接口。pair 類型只是將兩個數據成員捆綁成單個對象。在這種情況下,隱藏數據成員沒有必要也沒有明顯的好處。在像 pair 這樣的類中隱藏數據成員只會造成類型使用的復雜化。
  • 數據抽象和封裝提供了兩個重要優點:1.避免類內部出現無意的、可能破壞對象狀態的用戶級錯誤。2.隨時間推移可以根據需求改變或缺陷(bug)報告來完美類實現,而無須改變用戶級代碼。

隱含的this指針

  • 成員函數具有一個附加的隱含形參,即指向該類對象的一個指針。這個隱含形參命名為 this,與調用成員函數的對象綁定在一起。成員函數不能定義 this 形參,而是由編譯器隱含地定義。成員函數的函數體可以顯式使用 this 指針,但不是必須這麼做。如果對類成員的引用沒有限定,編譯器會將這種引用處理成通過 this 指針的引用。
  • 盡管在成員函數內部顯式引用 this 通常是不必要的,但有一種情況下必須這樣做:當我們需要將一個對象作為整體引用而不是引用對象的一個成員時。最常見的情況是在這樣的函數中使用 this:該函數返回對調用該函數的對象的引用。
  • 在普通的非 const 成員函數中,this 的類型是一個指向類類型的 const 指針。可以改變 this 所指向的值,但不能改變 this 所保存的地址。在 const 成員函數中,this 的類型是一個指向 const 類類型對象的const 指針。既不能改變 this 所指向的對象,也不能改變 this 所保存的地址。注意不能從 const 成員函數返回指向類對象的普通引用。const 成員函數只能返回 *this 作為一個 const 引用。.
  • 有時,我們希望類的數據成員(甚至在 const 成員函數內)可以修改。這可以通過將它們聲明為 mutable 來實現。可變數據成員(mutable data member)永遠都不能為 const,甚至當它是const 對象的成員時也如此。因此,const 成員函數可以改變 mutable 成員。要將數據成員聲明為可變的,必須將關鍵字 mutable 放在成員聲明之前。

構造函數

  • 構造函數是特殊的成員函數,只要創建類類型的新對象,都要執行構造函數。構造函數的工作是保證每個對象的數據成員具有合適的初始值。構造函數的名字與類的名字相同,並且不能指定返回類型。像其他任何函數一樣,它們可以沒有形參,也可以定義多個形參。構造函數可以被重載,實參決定使用哪個構造函數。構造函數自動執行。
  • 構造函數不能聲明為 const,創建類類型的 const 對象時,運行一個普通構造函數來初始化該 const 對象。構造函數的工作是初始化對象。不管對象是否為 const,都用一個構造函數來初始化化該對象。
  • 構造函數初始化列表以一個冒號開始,接著是一個以逗號分隔的數據成員列表,每個數據成員後面跟一個放在圓括號中的初始化式。與任意的成員函數一樣,構造函數可以定義在類的內部或外部。構造函數初始化只在構造函數的定義中而不是聲明中指定。
  • 如果沒有為類成員提供初始化式,則編譯器會隱式地使用成員類型的默認構造函數。如果那個類沒有默認構造函數,則編譯器嘗試使用默認構造函數將會失敗。在這種情況下,為了初始化數據成員,必須提供初始化式。有些成員必須在構造函數初始化列表中進行初始化。對於這樣的成員,在構造函數函數體中對它們賦值不起作用。沒有默認構造函數的類類型的成員,以及 const 或引用類型的成員,不管是哪種類型,都必須在構造函數初始化列表中進行初始化。
  • 只要定義一個對象時沒有提供初始化式,就使用默認構造函數。為所有形參提供默認實參的構造函數也定義了默認構造函數。一個類哪怕只定義了一個構造函數,編譯器也不會再生成默認構造函數。這條規則的根據是,如果一個類在某種情況下需要控制對象初始化,則該類很可能在所有情況下都需要控制。只有當一個類沒有定義構造函數時,編譯器才會自動生成一個默認構造函數。內置和復合類型的成員,如指針和數組,只對定義在全局作用域中的對象才初始化。當對象定義在局部作用域中時,內置或復合類型的成員不進行初始化。如果類包含內置或復合類型的成員,則該類不應該依賴於合成的默認構造函數。它應該定義自己的構造函數來初始化這些成員。

static類成員

  • 不像普通的數據成員,static 數據成員獨立於該類的任意對象而存在;每個 static 數據成員是與類關聯的對象,並不與該類的對象相關聯。正如類可以定義共享的 static 數據成員一樣,類也可以定義 static 成員函數。static 成員函數沒有 this 形參,它可以直接訪問所屬類的 static 成員,但不能直接使用非 static 成員。
  • 使用static 成員而不是全局對象有三個優點。1. static 成員的名字是在類的作用域中,因此可以避免與其他類的成員或全局對象名字沖突。2. 可以實施封裝。static 成員可以是私有成員,而全局對象不可以。3. 通過閱讀程序容易看出 static 成員是與特定類關聯的。這種可見性可清晰地顯示程序員的意圖。

一個實例

  為了增進讀者對上述文字的理解,這裡給出一個實例,源自《C++ Primer》習題12.13:擴展Screen類以包含move、set和display操作通過執行如下表達式來測試類:

// 將光標移至指定位置,設置字符並顯示屏幕內容 
myScreen.move(4,0).set('#').display(cout);

 

  答案如下:

1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 class Screen { 7 public: 8 typedef string::size_type index; 9 char get() const { return contents[cursor]; } 10 inline char get(index ht, index wd) const; 11 index get_cursor() const; 12 Screen(index hght, index wdth, const string &cntnts); 13 14 // 增加三個成員函數 15 Screen& move(index r, index c); 16 Screen& set(char); 17 Screen& display(ostream &os); 18 19 private: 20 std::string contents; 21 index cursor; 22 index height, width; 23 }; 24 25 Screen::Screen(index hght, index wdth, const string &cntnts) : 26 contents(cntnts), cursor(0), height(hght), width(wdth) { } 27 28 char Screen::get(index r, index c) const 29 { 30 index row = r * width; 31 return contents[row + c]; 32 } 33 34 inline Screen::index Screen::get_cursor() const 35 { 36 return cursor; 37 } 38 39 // 增加的三個成員函數的定義 40 Screen& Screen::set(char c) 41 { 42 contents[cursor] = c; 43 return *this; 44 } 45 46 Screen& Screen::move(index r, index c) 47 { 48 index row = r * width; 49 cursor = row + c; 50 return *this; 51 } 52 53 Screen& Screen::display(ostream &os) 54 { 55 os << contents; 56 return *this; 57 } 58 59 int main() 60 { 61 // 根據屏幕的高度、寬度和內容的值來創建 62 Screen Screen myScreen(5, 6, "aaaaa\naaaaa\naaaaa\naaaaa\naaaaa\n"); 63 64 // 將光標移至指定位置,設置字符並顯示屏幕內容 65 myScreen.move(4, 0).set('#').display(cout); 66 67 return 0; 68 } View Code

 

  這個解決方法已滿足了題目提出的要求,但存在一些缺陷:

  (1) 創建Screen對象時必須給出表示整個屏幕內容的字符串,即使有些位置上沒有內容。

  (2) 顯示的屏幕內容沒有恰當地分行,而是連續顯示,因此(4,0)位置上的'#',在實際顯示時 不一定正好在屏幕的(4,0)位置,顯示效果較差。

  (3) 如果創建的Screen對象是一個const對象,則不能使用display函數進行顯示(因為const對 象只能使用const成員)。

  (4) 如果move操作的目的位置超出了屏幕的邊界,會出現運行時錯誤。 

  要解決第一個缺陷,可以如下修改構造函數:

1 Screen::Screen(index hght, index wdth, const string &cntnts = " "): cursor(0), height(hght), width(wdth)
2  { 
3     // 將整個屏幕內容置為空格 
4     contents.assign(hght*wdth, ' ');
5      // 用形參string對象的內容設置屏幕的相應字符 
6     if (cntnts.size() != 0) 
7       contents.replace(0, cntnts.size(), cntnts); 
8 }    


  要解決第二個缺陷,可以如下修改display函數:

 1 Screen& Screen::display(ostream &os)
 2 { 
 3     string::size_type index = 0; 
 4     while (index != contents.size()) 
 5     { 
 6         os << contents[index];
 7          if ((index+1) % width == 0) 
 8         { 
 9             os << '\n'; 
10         } 
11         ++index; 
12     } 
13     return *this; 
14 }        


  要解決第三個缺陷,可以在Screen類定義體中增加如下函數聲明: const Screen& display(ostream &os) const; 聲明display函數的一個重載版本,供 const對象使用。
  要解決第四個缺陷,可以如下修改move函數:

 1 Screen& Screen::move(index r, index c) 
 2 { 
 3     // 行、列號均從0開始
 4     if (r >= height c >= width) 
 5     { 
 6         cerr << "invalid row or column" << endl; 
 7         throw EXIT_FAILURE;
 8     }
 9 
10     index row = r * width;
11     cursor = row + c; 
12     return *this;
13 }         

  經過如上述幾處修改,整個程序的健壯性,魯棒性都得到了改善。全部代碼如下:

1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 class Screen { 7 public: 8 typedef string::size_type index; 9 char get() const { return contents[cursor]; } 10 inline char get(index ht, index wd) const; 11 index get_cursor() const; 12 13 Screen(index hght, index wdth, const string &cntnts); 14 15 Screen& move(index r, index c); 16 Screen& set(char); 17 Screen& display(ostream &os); 18 const Screen& display(ostream &os) const; 19 20 private: 21 std::string contents; 22 index cursor; 23 index height, width; 24 }; 25 26 Screen::Screen(index hght, index wdth, const string &cntnts = "1"): 27 cursor(0), height(hght), width(wdth) 28 { 29 contents.assign(hght*wdth, '1'); 30 31 if (cntnts.size() != 0) 32 contents.replace(0, cntnts.size(), cntnts); 33 } 34 35 36 char Screen::get(index r, index c) const 37 { 38 index row = r * width; 39 return contents[row + c]; 40 } 41 42 inline Screen::index Screen::get_cursor() const 43 { 44 return cursor; 45 } 46 47 Screen& Screen::set(char c) 48 { 49 contents[cursor] = c; 50 return *this; 51 } 52 53 Screen& Screen::move(index r, index c) 54 { 55 if (r >= height || c >= width) 56 { 57 cerr << "invalid row or column" << endl; 58 throw EXIT_FAILURE; 59 } 60 61 index row = r * width; 62 cursor = row + c; 63 64 return *this; 65 } 66 67 Screen& Screen::display(ostream &os) 68 { 69 string::size_type index = 0; 70 71 while (index != contents.size()) 72 { 73 os << contents[index]; 74 if ((index + 1) % width == 0) 75 { 76 os << '\n'; 77 } 78 ++index; 79 } 80 return *this; 81 } 82 83 const Screen& Screen::display(ostream &os) const 84 { 85 string::size_type index = 0; 86 87 while (index != contents.size()) 88 { 89 os << contents[index]; 90 if ((index + 1) % width == 0) 91 { 92 os << '\n'; 93 } 94 ++index; 95 } 96 return *this; 97 } 98 99 int main() 100 { 101 Screen myScreen(10,30); 102 //Screen myScreen(5, 6, "aaaaa\naaaaa\naaaaa\naaaaa\naaaaa\n"); 103 myScreen.move(4, 0).set('#').display(cout); 104 105 system("pause"); 106 107 return 0; 108 } View Code

 



 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved