?? 06章 類與數據抽象(一).txt
字號:
65 << "\nMilitary time: ";
66 t.printMilitary();
67 cout << "\nStandard time: ";
68 t.printStandard();
69 cout << endl;
70 return 0;
71 }
輸出結果:
The initial military time is 00::00
The initial standard time is 12:00:00 AM
Military time after setTime is 13:27
Standard time after setTime is 1:27:06 PM
After attemping invalid settings:
Military time: 00::00
Standard time: 12:00:00 AM
圖6.3 用類實現抽象數據類型Time
注意數據成員hour、minute和second前面使用成員訪問說明符private。類的private數據成員通常只能在類中訪問(下一章會介紹,還可由類的友元訪問)。從本例中可以看出,類的客戶不關心類中的實際數據表達。例如,類完全可以用從午夜算起的秒數表示時間,這時客戶可以用相同的publie成員函數取得相同的結果而并不注意類中的變化。從這種意義上說,類的實現是向客戶隱藏起來的。這種信息隱藏提高了程序的可修改性,簡化了客戶對類的理解。
軟件工程視點6.3
類的客戶使用類時不必知道類的內部實現細節。如果類的內部實現細節改變(例如為了提高性能),只要 類的接口保持不變,類的客戶源代碼就不必改變(但客戶可能需要重新編譯),這樣就更容易修改系統。
在這個程序中,Time構造函數只是將數據成員初始化為0(即上午12時的軍用時間格式),因此就保證對象生成時具有一致狀態。Time對象的數據成員中不可能保存無效值,因為生成Time對象時自動調用構造函數,后面客戶對數據成員的修改都是由setTime函數完成的。
軟件工程視點6.4
成員函數通常比非面向對象編程中的函數更短,因為數據成員中存放的數據已由構造函數和保存新數據的成員函數驗證。由于數據已經是對象,成員函數調用通常沒有參數或比非面向對象語言中調用的典型函數的參數更少。這樣,調用簡化了,函數定義簡化了,函數原型也簡化了。
注意,類的數據成員無法在類體中聲明時初始化,而要用類的構造函數初始化,也可以用給它們設值的函數賦值。
常見編程錯誤6.4
想在類定義中顯式地將類的數據成員初始化是個語法錯誤。
與類同名而前面加上代字符(~)的函數稱為類的析構函數(destructor)(本例沒有顯式地加上析構函數,系統會插入一個析構函數)。析構函數在系統收回對象的內存之前對每個類對象進行清理工作。析構函數不帶參數,無法重載。本章稍后和第7章將詳細介紹構造函數與析構函數。
注意,類向外部提供的函數要加上public標號。public函數實現類向客戶提供的行為或服務,通常稱為類的接口或Public接口。
軟件工程視點6.5
客戶能訪問類的接口,但不能訪問類的實現方法。
類定義包含類的數據成員和成員函數的聲明。成員函數的聲明就是本書前面介紹的函數原型。
成員函數可以在類的內部定義,但在類的外部定義函數是個良好的習慣。
軟件工程視點6.6
在類定義中(通過函數原型)聲明成員函數而在類定義外定義這些成員函數,可以區分類的接口與實現方法。這樣可以實現良好的軟件工程,類的客戶不能看到類成員函數的實現方法。
注意圖6.3類定義中每個成員函數定義使用的二元作用域運算符(::)。定義類和聲明成員函數后,就要定義成員函數。類的每個成員函數可以直接在類定義體中定義(而不是包括類的函數原型),也可以在類定義體之后定義成員函數。在類定義體之后定義成員函數時,函數名前面要加上類名和二元作用域運算符(::)。由于不同類可能有同名成員,因此要用二元作用域運算符將成員名與類名聯系起來,惟一標識某個類的成員函數。
常見編程錯誤6.5
在類的外部定義成員函數時,省略函數名中的類名和二元作用域運算符是個語法錯誤。
盡管類定義中聲明的成員函數可以在類定義之外定義,但成員函數仍然在類范圍(class'sscope)中,即只有該類的其他成員知道它的名稱,除非通過類對象、引用類對象或類對象指針進行引用。稍后將詳細介紹類范圍。
如果在類定義中定義成員函數,則該成員函數自動成為內聯函數。在類體之后定義成員函數時,可以用關鍵字inline指定其為內聯函數。記住,編譯器有權不把內聯函數放進程序塊中。
性能提示6.2
在類定義內定義小的成員函數將自動使該函數成為內聯函數(如果編譯器選擇這么做),這樣雖然可以提
高性能,但不能提高軟件工程質量,因為類的客戶能看到函數實現方法。
軟件工程視點6. 7
只有最簡單的成員函數才能在類的首部中定義。
有趣的是printMilitary和printStandard成員函數沒有參數。這是因為成員函數隱式知道對調用 的特定Time對象打印數據成員。這樣就使成員函數調用比過程式編程中的傳統函數調用更為簡練。
測試與調試提示6. 1
成員函數調用通常不帶參數或比非面向對象語言中的傳統函數調用參數少得多,從而減少傳遞錯誤謾參數、
錯誤參數類型或錯誤參數個數的機會。
軟件工程視點6.8
利用面向對象編程方法通常能減少傳遞的參數個數,從而簡化函數調用。這個面向對象編程好處是由于
在對象中封裝數據成員和成員函數之后,成員函數有權訪問數據成員。
類能簡化編程,因為客戶(或類對象用戶)僅需關心對象中封裝或嵌入的操作。這種操作通常是面向客戶的,而不是面向實現方法的。客戶不必關心類的實現方法(當然客戶需要正確和有效的實現方法)。接口不是沒有改變,只是不像實現方法那樣經常改變而已。實現方法改變時,與實現方法有關的代碼也要相應改變。通過隱藏實現方法,可以消除程序中與實現方法有關的代碼。
軟件工程視點6.9
本書的中心主題是“復用、復用、再復用”。我們將認真介紹幾個提高復用性的技術,著重介紹”建立寶
貴的類”和建立寶貴的”軟件資產”。
類通常不需要從頭生成,可以從其他提供新類可用的屬性和行為的類派生而來,類中可以包括其他類對象作為成員。這種軟件復用可以大大提高程序員的工作效率。從現有類派生新類稱為繼承(inheritance),將在第9章介紹。把其他類對象作為類的成員稱為復合(composition),將在第7章介紹。
不熟悉面向對象編程的人常常擔心對象會很大,因為它們要包含數據和函數。邏輯上的確如此,程序員可以把對象看成要包含數據和函數,但實際中并不是這樣。
性能提示6.3
實際對象只包含數據,因此要比包含函數的對象小得多。對類名或該類的對象來用sizeof運算符時,只得到該類的數據長度。編譯器生成獨立于所有類對象的類成員函數副本(只有一份)。自然,因為每個對象的數據是不同的,所以每個對象需要自已的類數據副本。該函數代碼是不變的(或稱為可重入碼或純過程),因此可以在一個類的所有對象之間的共享。
6.6 類范圍與訪問類成員
類的數據成員(類定義中聲明的變量)和成員函數(類定義中聲明的函數)屬于該類的類范圍(class's scope)。非成員函數在文件范圍(file scope)中定義。
在類范圍中,類成員可由該類的所有成員函數直接訪問,也可以用名稱引用。在類范圍外,類成員是通過一個對象的句柄引用,可以是對象名、對象引用或對象指針(第7章將介紹,每次引用對象中的數據成員和成員函數時,編譯器插入一個隱式句柄)。
類的成員函數可以重載,但只能由這個類的其他成員函數重載。要重載成員函數,只要在類定義中提供該重載函數每個版本的原型,并對該重載函數每個版本提供不同的函數定義。
成員函數在類中有函數范圍(function scope),成員函數內定義的變量只能在該函數內訪問。如果成員函數定義與類范圍內的變量同名的變量,則在函數范圍內,函數范圍內的變量掩蓋類范圍內的變量。這種隱藏變量可以通過在前面加上類名和作用域運算符(::)而訪問。隱藏的全局變量可以用一元作用域運算符訪問(見第3章)。
訪問類成員的運算符與訪問結構成員的運算符是相同的。圓點成員選擇運算符(.)與對象名或對象引用組合,用于訪問對象成員。箭頭成員選擇運算符(->)與對象指針組合,用于訪問對象成員。
圖6.4的程序用簡單的Count,類和public數據成員x(int類型)以及public成員函數print演示如何用成員選擇運算符訪問類成員。程序實例化三個Count類型的變量--counter、counterRef(Count對象的引用)和counterPtr(Count對象的指針)。變量counterRef定義為引用Counter,變量countcrPtr定義為指向counter。注意,這里將數據成員x設置為public,只是為了演示public成員利用句柄(如名稱、引用或指針)即可訪問。前面曾介紹過,數據通常指定為private,第9章“繼承”中將介紹有時可以將數據指定為protected。
1 // Fig. 6.4: fig06_04.cpp
2 // Demonstrating the class member access operators . and ->
3 //
4 // CAUTION: IN FUTURE EXAMPLES WE AVOID PUBLIC DATA!
5 #include <iostream.h>
6
7 // Simple class Count
8 class Count {
9 public:
10 int x;
11 void print() { cout << x << endl; }
12 };
13
14 int main()
15 {
16 Count counter, // create counter object
17 *counterPtr = &counter, // pointer to counter
18 &counterRef = counter; // reference to counter
19
20 cout << "Assign 7 to x and print using the object's name: ";
21 counter.x = 7; // assign 7 to data member x
22 counter.print(); // call member function print
23
24 cout << "Assign 8 to x and print using a reference: ";
25 counterRef.x = 8; // assign 8 to data member x
26 counterRef.print(); // call member ~unction print
27
28 cout << "Assign 10 to x and print using a pointer: ";
29 counterPtr->x = 10; // assign 10 to data member ~
30 counterPtr->print(); // call member function print
31 return 0;
32 }
輸出結果:
Assign 7 to x and print using the object's name: 7
Assign 8 to x and print using a reference: 8
Assign 10 to x and pring using a pointer: 10
圖 6.4 通過各種句柄訪問對象的數據成員和成員函數
6.7 接口與實現方法的分離
良好軟件工程的一個基本原則是將接口與實現方法分離,這樣可以更容易修改程序。就類的客戶而言,類實現方法的改變并不影響客戶,只要類的接口保持不變即可(類的功能可能擴展到原接口以外)。
軟件工程視點6.10
將類聲明放在使用該類的任何客戶的頭文件中,這就形成類的Public接口(并向客戶提供調用類成員函數所需的函數原型)。將類成員函數的定義放在源文件中,這就形成類的實現方法。
軟件工程視點6.11
類的客戶使用類時不需要訪問類的源代碼,但客戶需要連掛類的目標碼。這樣就可以由獨立軟件供應商(ISV)提供類庫進行銷售和發放許可證。ISV只在產品中提供頭文件和目標模塊,不提供專屬信息(例如源代碼)。C++用戶可以享用更多的ISV生產的類庫。
實際上,任何事情都不是十全十美的。頭文件中包含一些實現部分,并隱藏了實現方法的其他函數定義。private成員列在頭文件的類定義中.因此客戶雖然無法訪問private成員,但能看到這些成員。第7章將介紹如何用代理類從類的客戶中隱藏類的private數據。
軟件工程視點6.12
對類接口很重要的信息應放在頭文件中。只在類內部使用而類的客戶不需要的信息應放在不發表的源文件中。這是最低權限原則的又一個例子。
圖6.5將圖6.3的程序分解為多個文件。建立C++程序時,每個類定義通常放在頭文件中,類的成員函數定義放在相同基本名字的源代碼文件(source-code file)中。在使用類的每個文件中包含頭文件(通過#include),而源代碼文件編譯并連接包含主程序的文件。編譯器文檔中介紹了如何編譯和連接由多個源文件組成的程序。
圖6.5包含聲明Time類的time1.h頭文件、定義Time類成員函數的Time1.cpp文件和定義main函數的fig06_05.cpp文件。這個程序的輸出與圖6.3的輸出相同。
1 // Fig. 6.5: timel.h
2 // Declaration of the Time class.
3 // Member functions are defined in timel.cpp
4
5 // prevent multiple inclusions of header file
6 #ifndef TIME1_H
7 #define TIME1_H
8
9 // Time abstract data type definition
10 class Time {
11 public:
12 Time(); // constructor
13 void setTime( int, int, int ); // set hour, minute, second
14 void printMilitary(); // print military time format
15 void printStandard(); // print standard time format
16 private:
17 int hour; // 0 - 23
16 int minute; // 0 59
19 int second; // 0 - 59
20 };
21
22 #endif
23 // Fig. 6.5: timel.cpp
24 // Member function definitions for Time class.
25 #include <iostream.h>
26 #include "time1.h"
27
28 // Time constructor initializes each data member to zero.
29 // Ensures all Time objects start in a consistent state.
30 Time::Time() { hour = minute = second = 0; }
31
32 // Set a new Time value using military time. Perform validity
33 // checks on the data values. Set invalid values to zero.
34 void Time::setTimm( int b, int m, int s )
35 {
36 hour = ( h >= 0 && h < 24 ) ? h : 0;
37 minute ( m >= 0 && m < 60 ) ? m : 0;
38 second = ( s >= 0 && s < 60 ) ? s : 0;
39 }
40
41 // Print Time in military format
42 void Time::printMilitary()
43 {
44 cout << (hourt< 10 ? "0" : "" ) << hour << ":"
45 ( minute < l0 ? "0" : "" ) << minute;
46 }
47
48 // Print time in standard format
49 void Time::printStandard()
50 {
51 cout << ( ( hour( == 0 || hour == 12 ) ? 12 : hourt% 12 )
52 << ":" << minute < l0 ? "0" : "" ) << mlnute
53 << ":" << ( second < l0 ? "0" : "" ) << second
54 << ( hour < 12 ? "AM" : "PM" );
55 }
56 // Fig. 6.5: fig06_05.cpp
57 // Driver for Timel class
58 // NOTE: Compile with timel.cpp
59 #include <iostream.h>
60 #include "time1.h"
61
62 // Driver to in main( test simple class Time
63 int main()
64 {
65 Time t; // instantiate object t of class time
66
67 cout << "The initial military time is";
68 t.printMilitary();
69 cout << "\nThe initial standard time is";
70 t.printStandardO;
71
72 t.setTime( 13, 27, 6 );
73 cout << "\n\nMilitary time after setTime is";
74 t.printMilitary();
75 cout << "%nStandard time after setTime is";
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -