官术网_书友最值得收藏!

2.4 全功能的C++類

POD類只包含數(shù)據(jù)成員,有時這就是你對類的全部要求。然而,只用POD類設(shè)計程序會很復(fù)雜。我們可以用封裝來處理這種復(fù)雜性,封裝是一種設(shè)計模式,它可以將數(shù)據(jù)與操作它的函數(shù)結(jié)合起來。將相關(guān)的函數(shù)和數(shù)據(jù)放在一起,至少在兩個方面有助于簡化代碼。首先,可以把相關(guān)的代碼放在一個地方,這有助于對程序進行推理。它有助于對代碼工作原理的理解,因為它在一個地方同時描述了程序狀態(tài)以及代碼如何修改該狀態(tài)。其次,可以通過一種叫作信息隱藏的做法將類的一些代碼和數(shù)據(jù)相對程序的其他部分隱藏起來。

在C++中,向類定義添加方法和訪問控制即可實現(xiàn)封裝。

2.4.1 方法

方法就是成員函數(shù)。它們在類、其數(shù)據(jù)成員和一些代碼之間建立了明確的聯(lián)系。定義方法就像在類的定義中加入函數(shù)一樣簡單。方法可以訪問類的所有成員。

考慮一個記錄年份的示例類ClockOfTheLongNow。以下代碼定義了一個int類型的year成員和一個遞增它的add_year方法:

add_year方法的聲明?看起來像其他不需要參數(shù)也不返回值的函數(shù)聲明一樣。在這個方法中,我們增加了成員year的值?。代碼清單2-19顯示了如何使用這個類來跟蹤年份。

代碼清單2-19 一個使用ClockOfTheLongNow的程序

我們聲明了一個ClockOfTheLongNow實例clock?,然后將clockyear設(shè)置為2017?。接著,調(diào)用clockadd_year方法?,然后打印clock.year的值?。遞增年份?并再次打印年份?,這樣就完成了這個程序。

2.4.2 訪問控制

訪問控制可以限制類成員的訪問。公有和私有是兩個主要的訪問控制。任何人都可以訪問公有成員,但只有類自身可以訪問其私有成員。所有的struct成員默認都是公有的。

私有成員在封裝中發(fā)揮著重要作用。再次考慮ClockOfTheLongNow類。目前,year成員可以從任何地方訪問,包括讀訪問和寫訪問。假設(shè)我們想防止year的值小于2019,那么可以通過兩個步驟來實現(xiàn):將year設(shè)為私有,并要求使用該類的任何人(消費者)只能通過該結(jié)構(gòu)的方法與year進行交互。代碼清單2-20說明了這種方法。

代碼清單2-20 從代碼清單2-19更新的ClockOfTheLongNow,它封裝了year

我們向ClockOfTheLongNow添加了兩個方法:setter?和getter?,用于設(shè)置或獲取year。我們沒有讓ClockOfTheLongNow的用戶直接修改year,而是用set_year設(shè)置year。這個新增的輸入驗證可以確保new_year永遠不會小于2019?。如果小于2019,代碼就會返回false,保持year不被修改。否則,year會被更新并返回true。為了獲得year的值,用戶需調(diào)用get_year

我們已經(jīng)使用了訪問控制標簽private?來禁止消費者訪問year。現(xiàn)在,用戶只能從ClockOfTheLongNow內(nèi)部訪問year

1.關(guān)鍵字class

我們可以用class關(guān)鍵字代替struct關(guān)鍵字,class關(guān)鍵字默認成員聲明為private。除了默認的訪問控制外,用structclass關(guān)鍵字聲明的類是一樣的。例如,可以用下列方式聲明ClockOfTheLongNow

以何種方式聲明類只是個人風(fēng)格問題。除了默認的訪問控制外,structclass之間完全沒有區(qū)別。我更喜歡使用struct關(guān)鍵字,因為我喜歡把公有成員列在前面。但每個程序員都有自己的風(fēng)格,培養(yǎng)一種風(fēng)格并堅持下去。

2.初始化成員

在封裝了year之后,我們現(xiàn)在必須使用方法來與ClockOfTheLongNow交互。代碼清單2-21顯示了如何將這些方法拼接到一個試圖將年份設(shè)置為2018的程序中。程序沒有設(shè)置成功,然后程序?qū)?b>year設(shè)置為2019,遞增年份,并打印其最終值。

代碼清單2-21 使用ClockOfTheLongNow說明方法用法的程序

首先,我們聲明了clock?,并試圖把它的年份設(shè)置為2018?。沒有設(shè)置成功,因為2018小于2019,然后程序?qū)⒛攴菰O(shè)置為2019?。把年份遞增一次?,然后打印它的值。

ClockOfTheLongNow有一個問題:當(dāng)clock被聲明?時,year是未初始化的。而我們想保證year在任何情況下都不會小于2019。這樣的要求被稱為類不變量:一個總是真的類特性(也就是說,它從不改變)。

在這個程序中,clock最終會進入一個良好的狀態(tài)?,但通過構(gòu)造函數(shù)可以做得更好。構(gòu)造函數(shù)會初始化對象,并在對象的生命周期之初就強制執(zhí)行類不變量。

2.4.3 構(gòu)造函數(shù)

構(gòu)造函數(shù)是具有特殊聲明的特殊方法。構(gòu)造函數(shù)聲明不包含返回類型,其名稱與類的名稱一致。例如,代碼清單2-22中的構(gòu)造函數(shù)不需要任何參數(shù),并將year設(shè)置為2019,這將導(dǎo)致year默認為2019。

代碼清單2-22 使用無參數(shù)構(gòu)造函數(shù)對代碼清單2-21進行改進

該構(gòu)造函數(shù)不需要參數(shù)?并將year設(shè)置為2019?。當(dāng)聲明新的ClockOfThe-LongNow時?,year默認為2019。我們可以使用get_year訪問year,并把它打印到控制臺?。

如果想用其他年份來初始化ClockOfTheLongNow,該怎么辦?構(gòu)造函數(shù)也可以接受任何數(shù)量的參數(shù)。我們可以實現(xiàn)任意多的構(gòu)造函數(shù),只要它們的參數(shù)類型不同。

考慮代碼清單2-23中的例子,它添加了一個接受int類型參數(shù)的構(gòu)造函數(shù)。該構(gòu)造函數(shù)可將year初始化為參數(shù)的值。

代碼清單2-23 用另一個構(gòu)造函數(shù)對代碼清單2-22進行擴展

新的構(gòu)造函數(shù)?接受一個int類型的year_in參數(shù)。我們可以用year_in?調(diào)用set_year。如果set_year返回false,說明調(diào)用者提供了錯誤的輸入,程序會用默認值2019覆寫year_in?。在main中,我們用新的構(gòu)造函數(shù)?聲明一個clock,然后打印結(jié)果?。ClockOfTheLongNow clock{2020};被稱為初始化語句。

注意 你可能不喜歡將無效的year_in實例默默糾正為2019?的做法。我也不喜歡這樣。異常機制(詳見4.3節(jié))可以解決這個問題。

2.4.4 初始化

對象初始化(簡稱初始化)是使對象“活起來”的過程。不幸的是,對象初始化的語法很復(fù)雜。幸運的是,初始化過程是直截了當(dāng)?shù)摹1竟?jié)將對C++對象初始化進行簡單提煉。

1.將基本類型初始化為零

我們先把基本類型的對象初始化為零。有四種方法可以做到這一點:

其中三種是可靠的,分別是使用字面量?明確地設(shè)置對象的值,使用大括號{}?,以及使用等號加大括號(={})的方法?。聲明對象時沒有額外的符號?是不可靠的,它只在某些情況下有效。即使你了解這些情況,也應(yīng)該避免依賴這種行為,因為它會造成混亂。

不出所料,使用大括號{}初始化變量的方法被稱為大括號初始化。C++初始化語法如此混亂的部分原因是,該語言從C語言(對象的生命周期是原始的)發(fā)展到具有健壯和豐富特性的對象生命周期的語言。語言設(shè)計者在現(xiàn)代C++中加入了大括號初始化,以幫助在初始化語法中的各種尖銳沖突中平滑過渡。簡而言之,無論對象的作用域或類型如何,大括號初始化總是適用的,而其他方法則不總適用。本章后面會介紹鼓勵廣泛使用大括號初始化的一般規(guī)則。

2.將基本類型初始化為任意值

初始化為任意值的過程類似于將基本類型初始化為零的過程:

它也有四種方法,分別是使用等號的初始化?,使用大括號初始化?,使用等號加大括號的初始化?,以及使用小括號的初始化?。所有這些都產(chǎn)生相同的代碼。

3.初始化POD

初始化POD的語法大多遵循基本類型的初始化語法。代碼清單2-24通過聲明一個包含三個成員的POD類型并用不同的值初始化它的實例來說明這種相似性。

代碼清單2-24 一個說明初始化POD的各種方法的程序

將POD對象初始化為零與將基本類型的對象初始化為零類似。大括號初始化?和等號加大括號的初始化?產(chǎn)生相同的代碼:字段初始化為零。

警告 不能對POD使用“等于0”的初始化方法。以下代碼不會被編譯,因為它在語言規(guī)則中被明確禁止了:

4.將POD初始化為任意值

我們可以使用括號內(nèi)的初始化列表將字段初始化為任意值。大括號初始化列表中的參數(shù)必須與POD成員的類型相匹配。從左到右的參數(shù)順序與從上到下的成員順序一致。任何省略的成員都被設(shè)置為零。成員abinitialized_pod3?中被初始化為42Helloc被清零(設(shè)置為false),因為我們在括號內(nèi)的初始化中省略了它。initialized_pod4?的初始化包括了c的參數(shù)(true),所以它的值在初始化后被設(shè)置為true

等號加大括號的初始化工作方式與此相同。例如,我們可以用以下代碼來代替?:

因為只能從右到左省略字段,所以下面的代碼不會被編譯:

警告 不能使用小括號來初始化POD。以下代碼將不會被編譯:

5.初始化數(shù)組

我們可以像初始化POD一樣初始化數(shù)組。數(shù)組聲明和POD聲明的主要區(qū)別是,數(shù)組指定了長度,這個長度參數(shù)在方括號[]中。

當(dāng)使用大括號初始化列表來初始化數(shù)組時,長度參數(shù)變得可有可無,因為編譯器可以從初始化列表參數(shù)的數(shù)量推斷出長度參數(shù)。

代碼清單2-25演示了一些初始化數(shù)組的方法。

代碼清單2-25 一個列出各種初始化數(shù)組方法的程序

數(shù)組array_1的長度為3,其元素為1、2和3?。array_2的長度是5,因為它指定了長度參數(shù)?。括號內(nèi)的初始化列表是空的,所以五個元素都初始化為零。array_3的長度也是5,但括號內(nèi)的初始化列表不是空的。它包含三個元素,所以剩下的兩個元素初始化為零?。array_4沒有大括號初始化列表,所以它包含未初始化的對象?。

警告 array_5是否被初始化實際上取決于與初始化基本類型相同的規(guī)則。對象的存儲期(見4.1節(jié))決定了這些規(guī)則。如果你對初始化有明確的要求,你就不必記住這些規(guī)則。

6.全功能類的初始化

與基本類型和POD不同,全功能類總是被初始化。換句話說,一個全功能類的構(gòu)造函數(shù)總是在初始化時被調(diào)用。具體是哪個構(gòu)造函數(shù)被調(diào)用取決于初始化時給出的參數(shù)。

代碼清單2-26中的類有助于闡明全功能類的用法。

代碼清單2-26 一個宣布調(diào)用了哪個構(gòu)造函數(shù)的類

Taxonomist類有四個構(gòu)造函數(shù)。如果初始化時沒有提供參數(shù),無參數(shù)的構(gòu)造函數(shù)被調(diào)用?。如果初始化時提供charintfloat,相應(yīng)的構(gòu)造函數(shù)???會分別被調(diào)用。在每一種情況下,構(gòu)造函數(shù)都會用printf語句給出提示。

代碼清單2-27使用不同的語法和參數(shù)初始化了幾個Taxonomist

代碼清單2-27 一個通過各種初始化語法初始化Taxonomist類的程序

沒有任何大括號或小括號時,無參數(shù)構(gòu)造函數(shù)被調(diào)用?。與POD和基本類型不同,無論在哪里聲明對象,都可以依賴這種初始化。使用大括號初始化列表,char?、int?和float?構(gòu)造函數(shù)會如預(yù)期那樣被調(diào)用。我們也可以使用小括號?和等號加大括號的語法?,這些都會調(diào)用預(yù)期的構(gòu)造函數(shù)。

雖然全功能類總是被初始化,但有些程序員喜歡對所有對象使用相同的初始化語法。這對于大括號初始化來說是沒有問題的,因為默認構(gòu)造函數(shù)會按照預(yù)期被調(diào)用?。

不幸的是,使用小括號的初始化?會導(dǎo)致一些令人驚訝的行為。它不會給出任何輸出。

乍看起來,最后一個初始化語句?像函數(shù)聲明,這是因為它就是。由于一些神秘的語言解析規(guī)則,我們向編譯器聲明的是一個尚未定義的函數(shù)t8,它沒有任何參數(shù),返回一個類型為Taxonomist的對象。

注意 9.1節(jié)詳細地介紹了函數(shù)聲明。但是現(xiàn)在,你只需要知道你可以提供一個函數(shù)聲明,在聲明中定義函數(shù)的修飾符、名稱、參數(shù)和返回類型,然后再在函數(shù)定義中提供函數(shù)體。

這個廣為人知的問題被稱為“最令人頭疼的解析”(most vexing parse),這也是C++社區(qū)在語言中引入大括號初始化語法的一個主要原因。縮小轉(zhuǎn)換(narrowing conversion)是另一個問題。

7.縮小轉(zhuǎn)換

每當(dāng)遇到隱式縮小轉(zhuǎn)換時,大括號初始化將產(chǎn)生警告。這是一個很棒的功能,可以讓我們避免一些討厭的bug。考慮下面的例子:

兩個float字面量的除法結(jié)果仍是一個浮點數(shù)。當(dāng)初始化narrowed_result?時,編譯器默默地將a/b(0.5)的結(jié)果縮小到0,因為我們使用小括號( )來初始化。當(dāng)使用大括號初始化時,編譯器會產(chǎn)生一個警告?。

8.初始化類成員

我們可以使用大括號初始化來初始化類的成員,正如這里所演示的:

gold成員使用等號初始化方法初始化?,year_of_smelting_accident使用大括號初始化方法初始化?,key_location使用等號加大括號的方法初始化?。不能使用小括號來初始化成員變量。

9.打起精神來

初始化對象的各種方法甚至讓有經(jīng)驗的C++程序員都感到困惑。這里有一條使初始化變得簡單的一般規(guī)則:在任何地方都使用大括號初始化方法。大括號初始化方法幾乎在任何地方都能正常工作,而且它們引起的意外也最少。由于這個原因,大括號初始化也被稱為“統(tǒng)一初始化”。本書的其余部分都遵循這一指導(dǎo)。

警告 對于C++標準庫中的某些類,你可能需要打破在任何地方都使用大括號初始化方法的規(guī)則。第二部分會把這些例外情況講解清楚。

2.4.5 析構(gòu)函數(shù)

對象的析構(gòu)函數(shù)是其清理函數(shù)。析構(gòu)函數(shù)在銷毀對象之前被調(diào)用。析構(gòu)函數(shù)幾乎不會被明確地調(diào)用:編譯器將確保每個對象的析構(gòu)函數(shù)在適當(dāng)?shù)臅r機被調(diào)用。我們可以在類的名稱前加上“~”來聲明該類的析構(gòu)函數(shù)。

下面的Earth類有一個析構(gòu)函數(shù),該析構(gòu)函數(shù)會打印Making way for hyperspace bypass

析構(gòu)函數(shù)的定義是可選的。如果決定實現(xiàn)一個析構(gòu)函數(shù),它不能接受任何參數(shù)。我們想在析構(gòu)函數(shù)中執(zhí)行的操作包括釋放文件句柄、刷新網(wǎng)絡(luò)套接字(socket)和釋放動態(tài)對象。

如果沒有定義析構(gòu)函數(shù),則會自動生成默認的析構(gòu)函數(shù)。默認析構(gòu)函數(shù)的行為是不執(zhí)行任何操作。

更多關(guān)于析構(gòu)函數(shù)的信息請見4.2節(jié)。

主站蜘蛛池模板: 平舆县| 大埔县| 深水埗区| 永泰县| 吴川市| 平原县| 手游| 延寿县| 安化县| 安龙县| 泗水县| 德安县| 温州市| 土默特左旗| 鹿邑县| 荣成市| 正蓝旗| 陆川县| 武宣县| 无锡市| 榕江县| 胶南市| 常熟市| 怀安县| 甘泉县| 阜康市| 抚远县| 富平县| 宁津县| 永昌县| 周宁县| 当雄县| 景泰县| 兴化市| 巩留县| 边坝县| 乐东| 宁南县| 赤峰市| 东山县| 泸溪县|