Thinking In C++中文版

----------------------- Page 1-----------------------前端

 下載 ios

                      第1章  對象的演化 程序員

    計算機革命起源於一臺機器,程序設計語言也源於一臺機器。 算法

    然而計算機並不只僅是一臺機器,它是心智放大器和另外一種有表述能力的媒體。這一點
使它不很像機器,而更像咱們大腦的一部分,更像其餘有表述能力的手段,例如寫做、繪畫、
雕刻、動畫製做或電影製做。面向對象的程序設計是計算機向有表述能力的媒體發展中的一
部分。
    本章將介紹面向對象程序設計(O O P )的基本概念,而後討論O O P開發方法,最後介紹使 數據庫

程序員、項目和公司使用面向對象程序設計方法而採用的策略。
    本章是一些背景材料,若是讀者急於學習這門語言的具體內容,能夠跳到第 2章,而後再
回過頭來學習本章。 編程

1.1  基本概念 小程序

    C + +包含了比面向對象程序設計基本概念更多的內容,讀者應當在學習設計和開發程序之
前先理解該語言所包含的基本概念。 後端

1.1.1 對象:特性+行爲[1] 設計模式

    第一個面向對象的程序設計語言是6 0年代開發的S i m u l a - 6 7。其目的是爲了解決模擬問題。
典型的模擬問題是銀行出納業務,包括出納部門、顧客、業務、貨幣的單位等大量的「對象」。
把那些在程序執行期間除了狀態以外其餘方面都同樣的對象歸在一塊兒,構成對象的「類」,這
就是「類」一詞的來源。
    類描述了一組有相同特性(數據元素)和相同行爲(函數)的對象。類實際上就是數據類 數組

型,例如,浮點數也有一組特性和行爲。區別在於程序員定義類是爲了與具體問題相適應,而
不是被迫使用已存在的數據類型。這些已存在的數據類型的設計動機僅僅是爲了描述機器的存
儲單元。程序員能夠經過增添他所須要的新數據類型來擴展這個程序設計語言。該程序設計系
統歡迎建立、關注新的類,對它們進行與內部類型同樣的類型檢查。
    這種方法並不限於去模擬具體問題。儘管不是全部的人都贊成,但大部分人相信,任何程

序都模擬所設計系統。O O P技術能很容易地將大量問題概括成爲一個簡單的解,這一發現產生
了大量的O O P語言,其中最著名的是S m a l l t a l k—C++ 以前最成功的O O P語言。
    抽象數據類型的建立是面向對象程序設計中的一個基本概念。抽象數據類型幾乎能像內部類
型同樣準確工做。程序員能夠建立類型的變量(在面向對象程序設計中稱爲「對象」或「實例」)
並操縱這些變量(稱爲發送「消息」或「請求」,對象根據發來的消息知道須要作什麼事情)。

1.1.2 繼承:類型關係

    類型不只僅說明一組對象上的約束,還說明與其餘類型之間的關係。兩個類型能夠有共同
的特性和行爲,可是,一個類型可能包括比另外一個類型更多的特性,也能夠處理更多的消息

  [1] 這一描述部分引自我對《The Tao of Objects》(Gary Entsminger著)一書的介紹。

----------------------- Page 2-----------------------

  2     C + +編程思想
                                                                      下載

(或對消息進行不一樣的處理)。繼承表示了基本類型和派生類型之間的類似性。一個基本類型具
有全部由它派生出來的類型所共有的特性和行爲。程序員建立一個基本類型以描述系統中一些

對象的思想核心。由這個基本類型派生出其餘類型,表達了認識該核心的不一樣途徑。
    例如,垃圾再生機要對垃圾進行分類。這裏基本類型是「垃圾」, 每件垃圾有重量、價值
等等,而且能夠被破碎、融化或分解。這樣,能夠派生出更特殊的垃圾類型,它們能夠有另外
的特性(瓶子有顏色)或行爲(鋁能夠被壓碎,鋼能夠被磁化)。另外,有些行爲能夠不一樣
(紙的價值取決於它的種類和狀態)。程序員能夠用繼承創建類的層次結構,在該層次結構中用

類型術語來表述他須要解決的問題。
    第二個例子是經典的形體問題,能夠用於計算機輔助設計系統或遊戲模擬中。這裏基本類
型是「形體」,每一個形體有大小、顏色、位置等。每一個形體能被繪製、擦除、移動、着色等。
由此,能夠派生出特殊類型的形體:圓、正方形、三角形等,它們中的每個都有另外的特性
和行爲,例如,某些形體能夠翻轉。有些行爲能夠不一樣(計算形體的面積)。類型層次結構既

體現了形體間的相似,又體現了它們之間的區別。
    用與問題相同的術語描述問題的解是很是有益的,這樣,從問題描述到解的描述之間就不
須要不少中間模型(程序語言解決大型問題,就須要中間模型)。面向對象以前的語言,描述
問題的解不可避免地要用計算機術語。使用對象術語,類型層次結構是主要模型,因此能夠從
現實世界中的系統描述直接進入代碼中的系統描述。實際上,使用面向對象設計,人們的困難

之一是從開始到結束過於簡單。一個已經習慣於尋找複雜解的、訓練有素的頭腦,每每會被問
題的簡單性難住。

1.1.3 多態性

    當處理類型層次結構時,程序員經常但願不把對象看做是某一特殊類型的成員,而把它看
做基本類型成員,這樣就能夠編寫不依賴於特殊類型的代碼。在形體例子中,函數能夠對通常
形體進行操做,而不關心它們是圓、正方形仍是三角形。全部的形體都能被繪製、擦除和移動,

因此這些函數能簡單地發送消息給一個形體對象,而不考慮這個對象如何處理這個消息。
    這樣,新添類型不影響原來的代碼,這是擴展面向對象程序以處理新狀況的最普通的方法。
例如,能夠派生出形體的一個新的子類,稱爲五邊形,而沒必要修改那些處理通常形體的函數。
經過派生新子類,很容易擴展程序,這個能力很重要,由於它極大地減小了軟件維護的花費。
(所謂「軟件危機」正是由軟件的實際花費遠遠超出人們的想象而產生的。)

    若是試圖把派生類型的對象看做它們的基本類型(圓看做形體,自行車看做車輛,鸕鶿看做
鳥),就有一個問題:若是一個函數告訴一個通常形體去繪製它本身,或者告訴一個通常的車輛去
行駛,或者告訴一隻通常的鳥去飛,則編譯器在編譯時就不能確切地知道應當執行哪段代碼。同
樣的問題是,消息發送時,程序員並不想知道將執行哪段代碼。繪圖函數能等同地應用於圓、正
方形或三角形,對象根據它的特殊類型來執行合適的代碼。若是增長一個新的子類,不用修改函

數調用,就能夠執行不一樣的代碼。編譯器不能確切地知道執行哪段代碼,那麼它應該怎麼辦呢?
    在面向對象的程序設計中,答案是巧妙的。編譯器並不作傳統意義上的函數調用。由非
O O P編譯器產生的函數調用會引發與被調用代碼的「早捆綁」,對於這一術語,讀者可能還沒
有據說過,由於歷來沒有想到過它。早捆綁意味着編譯器對特定的函數名產生調用,而鏈接器
肯定調用執行代碼的絕對地址。對於 O O P,在程序運行以前,編譯器不肯定執行代碼的地址,

因此,當消息發送給通常對象時,須要採用其餘的方案。
    爲了解決這一問題,面嚮對象語言採用「晚捆綁」的思想。當給對象發送消息時,在程序

----------------------- Page 3-----------------------

                                                      第1章 對象的演化         3
 下載

運行以前不去肯定被調用的代碼。編譯器保證這個被調用的函數存在,並完成參數和返回值的
類型檢查,可是它不知道將執行的準確代碼。

    爲了實現晚捆綁,編譯器在真正調用的地方插入一段特殊的二進制代碼。經過使用存放在
對象自身中的信息,這段代碼在運行時計算被調用函數的地址(這一問題將在第 1 4章中詳細介
紹)。這樣,每一個對象就能根據一個指針的內容有不一樣的行爲。當一個對象接收到消息時,它
根據這個消息判斷應當作什麼。
    程序員能夠用關鍵字 v i r t u a l代表他但願某個函數有晚捆綁的靈活性,而並不須要懂得

v i r t u a l 的使用機制。沒有它,就不能用C + +作面向對象的程序設計。Vi r t u a l函數(虛函數)表
示容許在相同家族中的類有不一樣的行爲。這些不一樣是引發多態行爲的緣由。

1.1.4 操做概念:OOP程序像什麼

    咱們已經知道,用C  語言編寫的過程程序就是一些數據定義和函數調用。要理解這種程序
的含義,程序員必須掌握函數調用和函數實現的自己。這就是過程程序須要中間表示的緣由。
中間表示容易引發混淆,由於中間表示的表述是原始的,更偏向於計算機,而不偏向於所解決

的問題。
    由於C++  向C 語言增長了許多新概念,因此程序員很天然地認爲,C + +程序中的m a i n ( )會
比功能相同的 C 程序更復雜。但使人吃驚的是,一個寫得很好的C + +程序通常要比功能相同的
C程序更簡單和容易理解。程序員只會看到一些描述問題空間對象的定義(而不是計算機的描
述),發送給這些對象的消息。這些消息表示了在這個空間的活動。面向對象程序設計的優勢

之一是經過閱讀,很容易理解代碼。一般,面向對象程序須要較少的代碼,由於問題中的許多
部分均可以用已存在的庫代碼。

1.2  爲何C++會成功

    C + +可以如此成功,部分緣由是它的目標不僅是爲了將 C語言轉變成O O P語言(雖然這是
最初的目的),並且還爲了解決當今程序員,特別是那些在 C語言中已經大量投資的程序員所
面臨的許多問題。人們已經對O O P語言有了這樣傳統的見解:程序員應當拋棄所知道的每件事
情而且從一組新概念和新文法從新開始,他應當相信,最好丟掉全部來自過程語言的老行裝。
從長遠角度看,這是對的。但從短時間角度看,這些行裝仍是有價值的。最有價值的可能不是那

些已存在的代碼庫(給出合適的工具,能夠轉變它),而是已存在的頭腦庫。做爲一個職業 C
程序員,若是讓他丟掉他知道的關於 C的每一件事,以適應新的語言,那麼,幾個月內,他將
毫無成果,直到他的頭腦適應了這一新範例爲止。若是他能調整已有的 C知識,並在這個基礎
上擴展,那麼他就能夠繼續保持高效率,帶着已有的知識,進入面向對象程序設計的世界。因
爲每一個人有他本身的程序設計模型,因此這個轉變是很混亂的。所以, C + +成功的緣由是經濟

上的:轉變到O O P須要代價,而轉變到C + +所花的代價較小。
    C + +的目的是提升效率。效率取決於不少東西,而語言是爲了儘量地幫助使用者,儘可
能不用武斷的規則或特殊的性能妨礙使用者。C + +成功是由於它立足於實際:儘量地爲程序
員提供最大便利。

1.2.1 較好的C

    即使程序員在C + +環境下繼續寫C代碼,也能直接獲得好處,由於C + +堵塞了C語言中的一

----------------------- Page 4-----------------------

  4     C + +編程思想
                                                                       下載

些漏洞,並提供更好的類型檢查和編譯時的分析。程序員必須先說明函數,使編譯器能檢查它
們的使用狀況。預處理器虛擬刪除值替換和宏,這就減小了查找疵點的困難。C + +有一個性能,

稱爲r e f e r e n c e s (引用) ,它容許對函數參數和返回值的地址進行更方便的處理。函數重載改進了
對名字的處理,使程序員能對不一樣的函數使用相同的名字。另外,名字空間也增強了名字的控
制。許多性能使C的更安全。

1.2.2 採用漸進的學習方式

    與學習新語言有關的問題是效率的問題。全部公司都不可避免地因軟件工程師學習新語言
而忽然下降了效率。C + +是對C 的擴充,而不是新的文法和新的程序設計模型。程序員學習和

理解這些性能,逐漸應用並繼續建立有用的代碼。這是C + +成功的最重要的緣由之一。
    另外,已有的C代碼在C + +中仍然是有用的,但由於C + +編譯器更嚴格,因此,從新編譯
這些代碼時,經常會發現隱藏的錯誤。

1.2.3 運行效率

    有時,以程序執行速度換取程序員的效率是值得的。假如一個金融模型僅在短時間內有用,
那麼快速建立這個模型比所寫程序能更快速執行重要。不少應用程序都要求有必定的運行效率,

因此C + +在更高運行效率時老是犯錯。C程序員很是重視運行效率,這讓他們認爲這個語言不
太龐大,也不太慢。產生的代碼運行效率不夠時,程序員能夠用C + +的一些性能作一些調整。
    C + +不只有與C相同的基本控制能力(和C + +程序中直接寫彙編語言的能力),非正式的證
據指出,面向對象的C + +程序的速度與用C寫的程序速度相差在±1 0 %以內,並且經常更接近。
用O O P方法設計的程序可能比C的對應版本更有效。

1.2.4 系統更容易表達和理解

    爲適合於某問題而設計的類固然能更好地表達這個問題。這意味着寫代碼時,程序員是在
用問題空間的術語描述問題的解(例如「把鎖鏈放在箱子裏」),而不是用計算機的術語,也就
是解空間的術語,描述問題的解(例如「設置芯片的一位即合上繼電器」)。程序員所涉及的是
較高層的概念,一行代碼能作更多的事情。
    易於表達所帶來的另外一個好處是易於維護。據報道,在程序的整個生命週期中,維護佔了

花費的很大部分。若是程序容易理解,那麼它就更容易維護,還能減小建立和維護文檔的花
費。

1.2.5 「庫」使你事半功倍

    建立程序的最快方法是使用已經寫好的代碼:庫。 C + +的主要目標是讓程序員能更容易地
使用庫,這是經過將庫轉換爲新數據類型(類)來完成的。引入一個庫,就是向該語言增長一
個新類型。編譯器負責這個庫如何使用,保證適當的初始化和清除,保證函數被正確地調用,

所以程序員的精力能夠集中在他想要這個庫作什麼,而不是如何作上。
    由於程序的各部分之間名字是隔離的,因此程序員想用多少庫就用多少庫,不會有像 C語
言那樣的名字衝突。
    • 模板的源代碼重用
    一些重要的類型要求修改源代碼以便有效地重用。模板能夠自動完成對代碼的修改,於是

----------------------- Page 5-----------------------

                                                      第1章 對象的演化         5
 下載

是重用庫代碼特別有用的工具。用模板設計的類型很容易與其餘類型一塊兒工做。由於模板對程
序員隱藏了這類代碼重用的複雜性,因此特別好用。

1.2.6 錯誤處理

    在C語言中,錯誤處理聲名狼藉。程序員經常忽視它們,對它們一籌莫展。若是正在建大
而複雜的程序,沒有什麼比讓錯誤隱藏在某處,且不能指出它來自何處更糟的了。 C + +的異常
處理(見第1 7章的內容)保證能檢查到錯誤並進行處理。

1.2.7 大程序設計

    許多傳統語言對程序的規模和複雜性有自身的限制。例如, B A S I C對於某些類型的問題能
很快解決,可是若是這個程序有幾頁紙長,或者超出該語言的正常解題範圍,那麼它可能永遠

算不出結果。C語言一樣有這樣的限制,例如當程序超過 50 000行時,名字衝突就開始成爲問
題。簡言之,程序員用光了函數和變量名。另外一個特別糟糕的問題是若是 C語言中存在一些小
漏洞—錯誤藏在大程序中,要找出它們是極其困難的。
    沒有清楚的文字告訴程序員,何時他的語言會失效,即使有,他也會忽視它們。他不
說「個人B A S I C程序太大,我必須用 C重寫」,而是試圖硬塞進另外幾行,增長額外的性能。

因此額外的花費就悄悄增長了。
    設計C + +的目的是爲了輔助大程序設計,也就是說,去掉小程序和大程序之間複雜性的分
界。當程序員寫h e l l o - w o r l d類實用程序時,他確實不須要用O O P、模板、名字空間和異常處理,
但當他須要的時候,這些性能就有用了。並且,編譯器在排除錯誤方面,對於小程序和大程序
同樣有效。

1.3  方法學介紹

    所謂方法學是指一組過程和啓發式,用以減小程序設計問題的複雜性。在 O O P中,方法學
是一個有許多實踐的領域。所以,在程序員考慮採用某一方法以前,瞭解該方法將要解決的問
題是很重要的。對於C + +,有一點是確實的:它自己就是但願減小程序表達的複雜性。從而不
必用更復雜方法學。對於用過程語言的簡單方法所不能處理的大型問題,在 C + +中用一些簡單
的方法就足夠了。

    認識到「方法學」一詞含義太廣是很重要的。實際上,設計和編寫程序時,不管作什麼都
在使用一種方法。只不過由於它是程序員本身的方法而沒有意識到。可是,它是程序員編程中
的一個過程。若是過程是有效的,只須要用C + +作很小的調整。若是程序員對他的效率和調整
程序的方法不滿意,他能夠考慮採用一種更正式的方法。

1.3.1 複雜性

    爲了分析複雜性,先假設:程序設計制定原則來對付複雜性。

    原則以兩種方式出現,每種方式都被單獨檢查。
    1)  內部原則體如今程序自身的結構中,機靈而有看法的程序員能夠經過程序設計語言的表
達方式瞭解這種內部原則。
    2) 外部原則體如今程序的源信息中,通常被描述爲「設計文檔」(不要與產品文檔混淆)。
    我認爲,這兩種形式的原則互相不一致:一個是程序的本質,是爲了讓程序能工做而產生

----------------------- Page 6-----------------------

  6  C + +編程思想
                                                                      下載

的,另外一個是程序的分析,爲了未來理解和維護程序而產生的。建立和維護都是程序生命期的
基本組成部分。有用的程序設計方法把二者綜合爲最合適的方式,而不偏向任何一方。

1.3.2 內部原則

    程序設計的演化(C + +只是其中的一步)從程序設計模型強加於內部開始,也就是容許程
序員爲內存位置和機器指令取別名。這是數字機器程序設計的一次飛躍,帶動了其餘方面的發
展,包括從初級機器中抽象出來,向更方便地解決手邊問題的模型發展。不是全部這些發展都
能流行,起源於學術界並延伸進計算機世界的思想經常依賴於所適應的問題。
    命名子程序的建立和支持子程序庫的鏈接技術在5 0年代向前飛躍發展,而且孕育出了兩個

語言,它們在當時產生了巨大沖擊,這就是爲科學工做者使用的                            F O RT R A N (F O R m u l a -
T R A N s l a t i o n )和爲商業者使用的C O B O L (COmmon Business-Oriented Language )。純計算機
科學中很成功的語言是L i s p      (L i s t - P r o c e s s i n g),而面向數學的語言應當是A P L (A Programming
L a n g u a g e )。
    這些語言的共同特色是對過程的使用。 L i s p和A P L 的創造專一於語言的高雅—語言的

「m i s s i o n語句」嵌入在處理全部任務狀況的引擎中。F O RT R A N和C O B O L的創造是爲了解決專
門的問題,當這些問題變得更復雜,有新的問題出現時,它們又獲得了發展。甚至它們進入衰
退期後,仍在發展:F O RT R A N和C O B O L的版本都面向對象進行了擴充(後時髦哲學的基本原
則是:任何具備本身獨特生活方式的組織,其主要目標就是使這種生活方式永存)。
    命名子程序在程序設計中起了重要做用,語言的設計圍繞着這一原則,特別是                                A l g o l和

P a s c a l 。同時另一些語言也出現了,它們成功地解決了程序設計的一些子集問題,並將它們
有序排列。最有趣的兩個語言是P r o l o g和F O RT H 。前者是圍繞着推理機而創建的(在其餘語言
中經常稱做庫)。後者是一個可擴充語言,容許程序員從新造成這個語言,以適應所解決的問
題,觀念上相似於面向對象程序設計。 F O RT H還能夠改變語言,於是很難維護,而且是內部
原則概念最純正的表達,它強調的是問題一時的解,而不是對這個解的維護。

    人們還創造了其餘許多語言,以解決某一部分的程序設計問題。一般,這些語言以特定的
目標開始。例如,B A S I C     (Beginners All-purpose Symbolic Instruction Code )是在6 0年代設計
的,目的是使程序設計對初學者更簡單。A P L的設計是爲了數學處理。兩種語言都可以解決其
他問題,而關鍵在於它們是不是這些問題集合最理想的解。有一句笑話是,「帶着錘子三年,
看什麼都是釘子」。這反映了根本的經濟學真理:若是咱們只有 B A S I C或A P L語言,特別是,

當最終期限很短且這個解的生命期有限時,它就是咱們問題最好的解。
    然而,最終考慮兩個因素:複雜性的管理和維護(將在下一部分討論)。即這種語言首先
是爲某一領域開發的,而程序員又不肯花很長時間來熟悉這門語言,其結果只能使程序愈來愈
長,使手頭的問題屈服於語言。界限是模糊的:誰能說何時您的語言會使您失望呢?這不
是立刻就出現的。

    問題的解開始變長,而且對於程序員更具挑戰性。爲了知道語言大概的限制,你得更聰明,
這種聰明變成了一種標準,也就是「爲了使該語言工做而努力」。這彷佛是人類的操做方式,
而不是遇到缺陷就抱怨,而且再也不稱它爲缺陷。
    最終,程序設計問題對於求解和維護變得太困難了,即求得的解太昂貴了。人們最終明白
了,程序的複雜性超出了咱們可以處理的程度。儘管一大類程序設計要求開發期間去作大部分

工做並建立要求最小維護的解(或者簡單地丟掉這個解,或者用不一樣的解替換它),但這只是
問題的一部分。通常狀況是,咱們把軟件看做是爲人們提供服務的工具。若是用戶的須要變化

----------------------- Page 7-----------------------

                                                      第1章 對象的演化          7
 下載

了,服務就必須隨着變化。這樣,當初版本開始運行時,項目並無結束。項目是一個不斷
進化的生命體。程序的更新變成了通常程序設計問題的一個部分。

1.3.3 外部原則

    爲了更新和改善程序,須要更新思考問題的方法。它不僅是「咱們如何讓程序工做」,而
是「咱們如何讓程序工做而且使它容易改變」。這裏就有一個新問題:當咱們只是試圖讓程序
工做時,咱們能夠假設開發組是穩定的(總之,咱們能夠但願這樣),可是,若是咱們正在考
慮程序的整個生命期,就必須假設開發組成員會改變。這意味着,新組員必須以某種方式學習
原程序的要點,並與老組員互相通信(也許經過對話)。這樣,該程序就須要某種形式的設計

文檔。
    由於只想讓程序工做,文檔並非必需的,因此尚未像由程序設計語言強加於程序那樣
的、強加於建立文檔的規則。這樣,若是要求文檔知足特定的須要,就必須對文檔強加外部原
則。文檔是否「工做」,這很難肯定(而且須要在程序一輩子中驗證),所以,對外部原則「最好」
形式的爭論.比對「最好」程序設計語言的爭論更激烈。

    決定外部原則時,頭腦中的重要問題是「我準備解決什麼問題」。問題的根本就是上面所
說的「咱們如何讓它工做和使它容易改變」。然而,這個問題經常有多種解釋:它變成了「我
如何才能與F o o b l e B l a h文檔規範說明一致,以使政府會爲此給我撥款」。這樣,外部原則的目
的是爲了創建文檔,而不是爲了設計好的、可維護的程序。文檔居然變得比程序自己更重要
了。

    被問到將來通常和特殊的計算的方向時,我會從這樣的問題開始:哪一種解花費較少?假設
這個解知足須要,價格的不一樣足以使程序員放棄他當前作事情的習慣方式嗎?若是他的方法包
括存儲在項目分析和設計過程當中所建立的每一個文檔,而且包括當項目進化時維護這些文檔,那
麼當項目更新時,他的系統將花費很大,可是它能使新組員容易理解(假設沒有那麼多的令人
懼怕閱讀的文檔)。這樣建立和維護方法的花費會和它打算替代方法的花費同樣多。

    外部結構系列的另外一個極端是最小化方法。爲完成設計而進行足夠的分析,而後丟掉它們,
使得程序員再也不花時間和錢去維護它;爲開始編碼而作足夠的設計,而後丟掉這個設計,使得
程序員再也不花時間和錢去維護這些文檔;而後使得代碼是一流的和清晰的,代碼中只須要最少
的註釋。爲了使新組員快速參與項目,代碼連同註釋就足夠了。由於在全部這些乏味的文檔上,
新組員只需花費不多的時間(總之,沒有人真地理解它們),因此他能較快地參與工做。

    即使不維護文檔,丟掉文檔也不是最好的辦法,由於這畢竟是程序員所作的有效工做。某
些形式的文檔一般是必須的(參看本章後面的描述)。

    1. 通信
    對於較大的項目,指望代碼像文檔同樣充分是不合理的,儘管咱們在實際中經常這樣指望。
可是,代碼包含了咱們實際上但願外部原則所產生的事物的本質:通信。咱們只是但願能與改
進這個程序的新組員通信就足夠了。可是,咱們還想使花費在外部原則上的錢最少,由於最終

人們只爲這個程序所提供的服務付錢,而不是爲它後面的設計文檔付錢。爲了真正有用,外部
原則應當作比只產生文檔更多的事情—它應當是項目組成員在建立設計時爲了討論問題而採
用的通信方法。理想的外部原則目標是使關於程序分析和設計的通信更容易。這對於如今爲這
個程序而工做的人們和未來爲這個程序而工做的人們是有幫助的。中心問題不僅是爲了能通信,
而爲了產生好的設計。

    人們(特別是程序員)被計算機吸引(因爲機器爲他們作工做),出於經濟緣由,要求開

----------------------- Page 8-----------------------

  8     C + +編程思想
                                                                      下載

發者爲機器作大量工做的外部原則彷佛從一開始就註定要失敗。成功的方法(也就是人們習慣
的方法)有兩個重要的特徵:

    1) 它幫助人們進行分析和設計。這就是,用這種方法比用別的方法對分析和設計中的思考
和通信要容易得多。目前的效率和採用這種方法後的效率應當明顯不一樣。不然,人們可能還留
在原地。還有,它的使用必須足夠簡單,不需用手冊。當程序員正在解決問題時,要考慮簡單
性,而無論他適用於符號仍是技術。
    2)  沒有短時間回報,就不會增強投資。在通向目標的可見的進展中,沒有短時間回報,人們就

不會感到採用一種方法能使效率提升,就會迴避它。不能把這個進展誤認爲是從一種中間形式
到另外一種中間形式的變換。程序員能夠看到他的類,連同類之間互相發送的消息一塊兒出現。爲
某人創造一種方法,就像武斷的約束,由於它是簡單的心理狀態:人們但願感到他們正在作創
造性的工做,若是某種方法妨礙他們而不是幫助他們飛快地接近目標,他們將設法繞過這種方
法。

    2. 量級

    在方法學上反對個人觀點之一是:「好了,您可以僥倖成功是由於您正在作的小項目很短。」
聽衆對「小」的理解因人而異。雖然這種見解並不全對,但它包含一個正確的核心:咱們所需
要的原則與咱們正在努力解決問題的量級有關。小項目徹底不須要外部原則,這不一樣於個別程
序員正在解的生命期問題的模式。涉及不少人的大項目會令人們之間有一些通信,因此必須使
通信具備形式化方法,以使通信有效和準確。

    麻煩的是介於它們之間的項目。它們對外部原則的須要程度可能在很大程度上依賴於項目
的複雜性和開發者的經驗。確實,全部中等規模的項目都不須要忠實於成熟的方法,即產生許
多報告和不少文檔。一些項目也許這樣作,但許多項目能夠僥倖成功於「方法學簡化」(代碼
多而文檔少)。咱們面前的全部方法學的複雜性能夠減小到 8 0 %~2 0 % 的(或更少的)規則。
咱們正在被方法學的細節淹沒,在所解決的程序設計問題中,可能只有不足 2 0 % 的問題須要這

些方法學。若是咱們的設計是充分的,而且維護也不可怕,那麼咱們也許不須要方法學或不全
部須要它。

    3. OOP是結構化的嗎
    如今提出一個更有意義的問題。爲了使通信方便,假設方法學是須要的。這種關於程序的
元通信是必須的,由於程序設計語言是不充分的—它太原始,趨向於機器範例,對於談論問
題不頗有用。例如,過程程序設計方法要求用數據和變換數據的函數做爲術語談論程序。由於

這不是咱們討論實際問題的方法,因此必須在問題描述和解描述之間翻譯來翻譯去。一旦獲得
了一個解描述而且實現了它,之後不管什麼時候對這個解描述作改變就要對問題描述作改變。這意
味着必須從機器模式返回問題空間。爲了獲得真正可維護的程序,而且可以適應問題空間上的
改變,這種翻譯是必須的。投資和組織的須要彷佛要求某種外部原則。過程程序的最重要的方
法學是結構化技術。

    如今考慮,是否解空間上的語言能夠徹底脫離機器模式?是否能夠強迫解空間使用與問題
空間相同的術語?
    例如,在氣候控制大樓中的空氣調節器就變成了氣候調節程序的空氣調節器,自動調溫器
變成了自動調溫程序,等等。(這是按直覺作的,與O O P不一致)。忽然,從問題空間到解空間
的翻譯變成了次要問題。能夠想象,在程序分析、設計和實現的每一階段,能使用相同的術語

學、相同的描述,這樣,這個問題就變成了「若是文檔(程序)可以充分地描述它自身,咱們
仍然須要關於這個文檔的文檔嗎?」若是O O P作它所主張的事情,程序設計的形式就變成了這

----------------------- Page 9-----------------------

                                                      第1章 對象的演化         9
 下載

樣:在結構化技術中所遇到的困難在新領域中可能不復存在了。
    這個論點也爲一個思想實驗所揭示。假設程序員須要寫一些小實用程序,例如能在文本文

件上完成一個操做的程序(就像在第6章的後幾頁上可找到的那樣),它們要程序員花費幾分鐘,
最困難的要花費幾小時去寫。如今假設回到5 0年代,這個項目必須用機器語言或彙編語言來寫,
使用最少的庫函數,它須要許多人幾星期或幾個月的時間。在 5 0年代須要大量的外部原則和管
理,如今不須要了。顯然,工具的發展已經極大地增長了咱們不用外部原則解決問題的複雜性
(一樣很顯然,咱們將發現的問題也更加複雜)。

    這並非說能夠不須要外部原則,有用的O O P外部原則解決的問題與有用的過程程序設計
外部原則所解決的問題不一樣,特別是,O O P方法的目標首先必須是產生好的設計。好設計不只
要促進重用,並且它與項目的各級開發者的須要是一致的。這樣,開發者就會更喜歡採用這樣
的系統。讓咱們基於這些觀點考慮O O P設計方法中的一些問題。

1.3.4 對象設計的五個階段

    對象的設計不限於寫程序的時期,它出如今一系列階段。有這種觀點頗有好處,由於咱們

再也不指望設計馬上盡善盡美,而是認識到,對對象作什麼和它應當像什麼的理解是隨着時間的
推移而產生的。這個觀點也適用於不一樣類型程序的設計。特殊類型程序的模式是經過一次又一
次地求解問題而造成的 [ 1 ] 。一樣,對象有本身的模式,經過理解、使用和重用而造成。

    下面是描述,不是方法。它簡直就是對象指望的設計出現時的觀察結果。
    1) 對象發現    這個階段出如今程序的最初分析期間。能夠經過尋找外部因素與界線、系統

中的元素副本和最小概念單元而發現對象。若是已經有了一組類庫,某些對象是很明顯的。類
之間的共同性(暗示了基類和繼承類),能夠馬上出現或在設計過程的後期出現。
    2)  對象裝配   咱們在創建對象時會發現須要一些新成員,這些新成員在對象發現時期未出
現過。對象的這種內部須要可能要用新類去支持它。
    3)  系統構造   對對象的更多要求可能出如今之後階段。隨着不斷的學習,咱們會改進咱們

的對象。與系統中其它對象通信和互相鏈接的須要,可能改變已有的類或要求新類。
    4)  系統擴充   當咱們向系統增添新的性能時,可能發現咱們先前的設計不容易支持系統擴
充。這時,咱們能夠從新構造部分系統,並極可能要增長新類。
    5) 對象重用    這是對類的真正的重點測試。若是某些人試圖在全新的狀況下重用它,他們
會發現一些缺點。當咱們修改一個類以適應更新的程序時,類的通常原則將變得更清楚,直到

咱們有了一個真正可重用的對象。
    對象開發原則
    在這些階段中,提出考慮開發類時所須要的一些原則:
    1) 讓特殊問題生成一個類,而後在解其餘問題時讓這個類生長和成熟。
    2) 記住,發現所須要的類,是設計系統的主要內容。若是已經有了那些類,這個項目就不

困難了。
    3) 不要強迫本身在一開始就知道每一件事情,應當不斷地學習。
    4)  開始編程,讓一部分可以運行,這樣就能夠證實或反駁已生成的設計。不要懼怕過程語
言風格的細麪條式的代碼—類分割能夠控制它們。壞的類不會破壞好的類。
    5) 儘可能保持簡單。具備明顯用途的不太清楚的對象比很複雜的接口好。咱們總可以從小的

  [1] 參看Design Patterns:Elements of Reusable Object-Oriented Software by Erich Gamma et al., Addison-We s l e y, 1995 。

----------------------- Page 10-----------------------

  10      C + +編程思想
                                                                      下載

和簡單的類開始,當咱們對它有了較好地理解時再擴展這個類接口,但不可能簡化已存在的類
接口。

1.3.5 方法承諾什麼

    因爲不一樣的緣由,方法承諾的東西每每比它們可以提供的東西多得多。這是不幸的,由於
當策略和不實際的指望同時出現時程序員會疑神疑鬼。一些方法的壞名聲,使得程序員丟棄了
手上的其餘方法,忽視了一些有價值的技術。

    1. 管理者的銀子彈
    最壞的許諾是「這個方法將解決您的全部問題」。這一許諾也極可能用這樣的思想表達,
即一個方法將解決實際上不存在解的問題,或者至少在程序的設計領域內沒有解的問題:一個

貧窮的社團文化,疲憊的、互相疏遠或敵對的項目組成員;不充分的時間和資源;或試圖解決
一個實際上不能解的問題(資源不足)。最好的方法學,無論它許諾什麼,都不解決這些或類
似的任何問題。不管O O P仍是C + +,都無助於這樣的問題。不幸的是,在這種狀況下管理員是
這樣的人:對於銀子彈的警報 [ 1 ] ,他是最易動搖的。

    2. 提升效率的工具

    這就是方法應當成爲的東西。提升生產效率不只取決於管理容易和花費不大,並且取決於
一開始就建立好的設計。因爲一些方法學的創造動機是爲了改善維護,因此它們就片面地強調
維護問題,而忽視了設計的漂亮和完整。實際上,好的設計應當是首要的目標,好的 O O P設計
也應當容易維護,但這是它的附加做用。

1.3.6 方法應當提供什麼

    無論爲特殊方法提出什麼要求,它都應當提供這一節所列出的基本功能:容許爲討論這個

項目將完成什麼和如何作而進行通信的約定;支持項目結構化的系統;能用某抽象形式描述項
目的一組工具(使得程序員能容易地觀察和操做項目)。如過去介紹過的,一個更微妙的問題
是該方法對待最寶貴資源—組員積極性的「態度」。

    1. 通信約定
    對於很小的項目組,能夠用緊密接觸的方式天然維持通信。這是理想的請況。 C + +的最大
的好處之一是它能夠使項目由不多的項目組成員創建,所以,明白表示的通信能使維護變得容

易,於是通信費用低,項目組能更快地創建。
    狀況並不老是這樣理想,有可能項目組成員不少,項目很複雜,這就須要某種形式的通
訊原則。方法提供一種在項目組成員之間造成「約定」的辦法。能夠用兩種方式看待這樣的
約定:
    1) 敵對的   約定基於參與的當事人之間互有疑問,以使得沒有人出格且每一個人都作應該作

的事情。約定清楚地說明,若是他們不作這些事,會出現壞的結果。這樣看待任何約定,咱們
就已經輸了,由於咱們已經認爲其餘人是不可信賴的了。若是不能信任某人,約定並不能確保
好的行爲。
    2)  信息的  約定是一種努力,使每一個人都知道咱們已經在哪些方面取得了一致的意見。這
是對通信的輔助,使得每一個人能看到它並說,「是的,這是我認爲咱們將要作的事情」。它是協

議做出後對協議的表述,只是消除誤解。這類約定能最小化,並容易讀。

  [1] A reference to vampires made in The Mythical Man-Month, by Fred Brooks, Addision-We s l e y, 1975 。

----------------------- Page 11-----------------------

                                                    第1章 對象的演化         11
 下載

    有用的方法不鼓勵敵對的約定,而重點是在通信上。
    2. 使系統結構化

    結構化是系統的核心。若是一個方法能作些事情,那麼它就必須可以告訴程序員:
    1) 須要什麼類
    2) 如何把它們鏈接在一塊兒,造成一個工做系統。
    一個方法產生這些回答須要一個過程,即首先對問題進行分析,最終對類、系統和類之間
傳遞的消息進行某種表述。

    3. 描述工具
    模型不該當比它描述的系統更復雜。一種好的模型僅提供一種抽象。
    程序員必定不會使用只對特殊方法有用的描述工具。他能使本身的工具適合本身的各類需
要。(例如在本章後面,建議一種符號,用於商業字處理機。)下面是有用的符號原則:
    1) 方法中不含有不須要的東西。記住,「七加減二」規則的複雜性。(瞬間,人們只能在頭

腦中存放這麼多的條目。)額外的細節就變成了負擔,必須維護它,爲它花錢。
    2)  經過深刻到描述層,人們應當可以獲得所須要的信息。即咱們能夠在較高的抽象層上創
建一些隱藏的層,僅在須要時,它們纔可見。
    3) 符號應當儘量少。「過多的噱頭使得軟件變壞」。
    4)  系統設計和類設計是互相隔離的問題。類是可重用工具,而系統是對特殊問題的解(雖

然系統設計也是可重用的)。符號應當首先集中在系統設計方面。
    5)  類設計符號是必須的嗎?由C + +語言提供的類表達對大多數狀況是足夠的。若是符號在
描述類方面不能比用O O P語言描述有一個明顯的推動,那麼就不要用它。
    6) 符號應當在隱藏對象的內部實現。在設計期間,這些內部實現通常不重要。
    7)  保持符號簡單。咱們想用咱們的方法作的全部事情基本上就是發現對象及其如何互相連

接以造成系統。若是一個方法或符號要求更多的東西,則應當問一問,該方法花費咱們的時間
是否合理。
    4. 不要耗盡最重要的資源
    個人朋友Michael Wi l k來自學術界,也許並不具有作評判的資格(從某我的那裏據說的新
觀點),但他觀察到,項目、開發組或公司擁有的最重要的資源是積極性。無論問題如何困難,

過去失敗多麼嚴重,工具多麼原始或不成套,積極性都能克服這些障礙。
    不幸的是,各類管理技術經常徹底不考慮積極性,或由於不容易度量它,就認爲它是「不
重要」的因素,他們認爲,若是管理完善,項目就能強制完成。這種認識有壓制開發組積極性
的做用,由於他們會感到公司除了利益動機之外就沒有感興趣的東西了。一旦發生這種現象,
組員就變成了「僱員」,看着鍾,想着感興趣的、分心的事情。

    方法和管理技術是創建在動機和積極性的基礎上的,最寶貴的資源應當是對項目真正感興
趣。至少,應當考慮O O P設計方法對開發組士氣起的做用。
    5. 「必」讀
    在選擇任何方法以前,從並不想出售方法的人那兒獲得意見是有幫助的。不真正地理解我
們想要一種方法是爲了作什麼或它能爲咱們作什麼,那麼採用一種方法很容易。其餘人正在用

它,這彷佛是很充足的理由。可是,人們有一種奇怪的心理:若是他們相信某件事能解決他們
的問題,他們就將試用它(這是經驗,是好的)。可是,若是它不能解決他們的問題,他們可
能加倍地努力,而且開始大聲宣佈他們已經發現了偉大的東西(這是否認,是很差的)。這個
假設是,若是見到同一條船上有其餘人,就不感到孤單,即使這條船哪兒也不去。

----------------------- Page 12-----------------------

  12      C + +編程思想
                                                                      下載

    這並非說全部的方法學都什麼也不作,而是用精神方法使程序員武裝到牙齒,這些方法
能使程序員保持實驗精神(「它不工做,讓咱們再試其它方法」)和跳出否認方式(「不,這根

本不是問題,每樣東西都很好,咱們不須要改變」)。我認爲,在選擇方法以前讀下面的書,會
爲咱們提供這些精神武器。
    《軟件的創造力》(Software Creativity,Robert Glass編,P r e n t i c e - H a l l , 1 9 9 5 )。這是我所看
到的討論整個方法學前景最好的書。它是由 G l a s s 已經寫過的短文和文章以及收集到的東西組
成的(P. J . P l a u g e r是一個撰稿者),反映出他在該主題上多年的思考和研究。他們愉快地說明什

麼是必須的,並不東拉西扯和掃咱們的興,不說空話,並且,這裏有幾百篇參考文獻。全部的
程序員和管理者在陷入方法學泥潭以前應當讀這本書 [ 1 ] 。

    《人件》(Pe o p l e w a r e,Tom Demarco  和Timothy Lister  編,Dorset House,1987 )。雖然他們
有軟件開發方面的背景,並且本書大致上是針對項目和開發組的,可是這本書的重點是人和他
們的須要方面,而不是技術和技術的須要方面。他們談論創造一個環境,在其中人們是幸福和

高效率的,而不是決定人們應當遵照什麼樣的規則,以使得他們成爲機器的合適部件。我認爲,
這本書後面的觀點是對程序員在採用X Y Z方法,而後平靜地作他們老是作的事情時微笑和點頭
的最大貢獻。
    《複雜性》(C o m p l e x i t y, M. Mitchell Wa l d r o p編,Simon & Schuster, 1992 )。此書集中了
Santa Fe, New Mexico的一組持不一樣觀點的科學家,討論單個原則不能解決的實際問題(經濟

學中的股票市場、生物學的生命原始形式、爲何人們作他們在社會中應作的事情,等等)。
經過物理學、經濟學、化學、數學、計算機科學、社會學和其餘學科的交叉,對這些問題的一
種多原則途徑正在發展。更重要的是,思考這些極其複雜問題的不一樣方法正在造成。拋開數學
的肯定性,人們想寫一個預言全部行爲的方程,首先觀察和尋找一個模式,用任何可能的手段
模擬這個模式(例如,這本書編入了遺傳算法)。我相信,這種思惟是有用的,由於咱們正在

對管理愈來愈複雜軟件項目的方法作科學觀察。

1.4  起草:最小的方法

    我首先聲明,這一點沒有證實過。我並不準諾—起草是起點,是其餘思想的種子,是思
想試驗,儘管這是我在大量思考、適量閱讀和在開發過程當中對本身和其餘人觀察以後造成的看
法。這是受我稱之爲「小說結構」的寫做類的啓示。「小說結構」出如今Robert McKee  [2]                        的教

學中,最初針對熱心熟練的電影劇做家們,也針對小說家和編劇。後來我發現,程序員與這個
人羣有大量的共同點:他們的思想最終用某類文本形式表達,表達的結構能肯定產品成功與否。
有少許使人拍案稱奇的上口小說,其餘許多小說都很平庸,但有技巧,獲得發行,大量不上口
的小說得不到發表。固然,小說要描述,而程序要編寫。

    做家還有一些在程序設計中不太出現的約束:他們通常單獨工做或可能在兩我的的組中工
做。這樣,他們必須很是經濟地使用他們的時間,放棄不能帶來重要成果的方法。 M c K e e 的兩
個目標是將花費在電影編劇上的時間從一年減小到六個月,在這個過程當中極大地提升電影編劇
的質量。軟件開發者能夠有相似的目標。
    使每一個人都贊成某些事情是項目啓動過程當中最艱苦的部分。系統的最小性應當能得到最獨

立的程序員的支持。

  [1] 另一本好「前景」的書是Object Lessons Tom Love著, SIGS Books, 1993。

  [2] Through Two Arts, Inc.,12021 Wilshire Blvd. Suite 868, Los Angeles, CA 90025。

----------------------- Page 13-----------------------

                                                    第1章 對象的演化         13
 下載

1.4.1 前提

    該方法的描述是創建在兩個重要前提的基礎上的,在咱們採用該思想的其餘部分時必須仔

細考慮這兩個前提:
    1)  與典型的過程語言(和大量已存在的語言)不一樣, C + +語言和語言性能中有許多防禦,
程序員能創建本身的防禦。這些防禦意在防止程序員建立的程序破壞它的結構,不管在建立它
的整個期間仍是在程序維護期間。
    2)  無論分析得如何透徹,這裏還有一些有關係統的事直到設計時尚未揭示出來,更多的

直到程序完成和運行時尚未揭示出來。所以,快速經過分析過程和設計過程以實現目標系統
的測試是重要的。根據第一點,這比用過程語言更安全,由於C + +中的防禦有助於防止「麪條」
代碼的建立。
    着重強調第二點。因爲歷史緣由,咱們已經用了過程語言,所以在開始設計和實現以前開
發組但願仔細地處理和了解每一微小的細節,這是值得表揚的。的確,當建立 D B M S時,完全

地瞭解消費者的須要是值得的。可是,D B M S是一類具備良好形式且容易理解的問題。在這一
章中討論的這類程序設計問題是w i l d - c a r d變體問題,它不僅簡單地從新造成已知解,而是包括

                                                                      [ 1 ]
一個或多個w i l d - c a r d 因素—元素,在這裏沒有容易理解的先前的解,而必須進行研究  。在着
手設計和實現以前完全地分析w i l d - c a r d 問題會致使分析癱瘓,由於在分析階段沒有足夠的信息
解決這類問題。解這樣的問題要求在整個週期中反覆,要冒險(以產生感性認識,由於程序員

正在作新事情,而且有較高的潛在回報)。結果是,危險由盲目「闖入」預備性實現而產生,但
是它反而能減小在w i l d - c a r d項目中的危險,由於人們能較早地發現一個特殊的設計是否可行。
    這個方法的目標是經過建議解處理w i l d - c a r d 問題,獲得最快的開發結果,使設計能儘早地
被證實或反證。這些努力不會白費。這個方法經常建議,「創建一個解,而後再丟掉它」。用
O O P,可能仍然丟掉一部分,可是由於代碼被封裝成類,不可避免地生產一些有用的類設計和

第一次反覆中發展一些對系統設計有價值的思想,它們不須要丟掉。這樣,對某一問題快速地
過一遍不只產生對下一次分析、設計和實現的重複重要的信息,並且也爲下一次的重複過程創
建了代碼基礎。
    這個方法的另外一性能是能對項目早期部分的集體討論提供支持。因爲保持最初的文檔小而
簡明,因此最初的文檔能夠由小組與動態建立該描述的領導經過幾回集體討論而建立,這不只

要求每一個人的投入,並且還鼓勵開發組中的每一個人意見一致。也許更重要的是,它能在較高的
積極性下完成一個項目(如先前注意到的,這是最基本的資源)。
    表示法
    做家的最有價值的計算機工具是字處理器,由於它容易支持文檔的結構。對於程序設計項
目,程序的結構一般是由某形式的分離的文檔來表述的。由於項目變得更復雜,因此文檔是必
需的。這就出現了一類問題,如 B r o o k s[2]  所說:「數據處理的基本原則引出了試圖用同步化方

法維護獨立文檔的傻念頭—而咱們在程序設計文檔方面的實踐違反了咱們本身的教義。咱們
典型的努力是維護程序的機器可讀形式和一組獨立可讀的文檔……。」
    好的工具應當將代碼和它的文檔聯繫起來。
    咱們認爲,使用熟悉的工具和思惟模式是很是重要的,O O P的改變正面臨着由它本身引發

  [1] 我估計這樣的項目的主要規則:若是多於一張w i l d - c a r d ,則不計劃它要費多長時間和它花費多少。這裏有太多

     的自由度。

  [2] The Mythical Man-Month, 出處同上。

----------------------- Page 14-----------------------

  14      C + +編程思想
                                                                      下載

的挑戰。較早的O O P方法學已經由於使用精細的圖形符號方案而受挫了。咱們不可避免地要大
量改變設計,由於咱們必須改變設計以免棘手的問題,因此用很難修改的符號表示設計是不

好的。只是最近,纔有處理這些圖形符號的工具出現。容易使用設計符號的工具必須在但願人
們使用這種方法以前就有了。把這種觀點與在軟件設計過程當中要求文檔這一事實結合,能夠看
出,最符合邏輯的工具是一個性能全面的字處理器 [ 1 ] 。事實上,每一個公司都有了這些工具(所

以試用這種方法不須要花費),大多數程序員熟悉它們,程序員習慣於用它們建立基本宏語言。
這符合C + +的精神,咱們是創建在已有的知識和工具基礎上的,而不是把它們丟掉。
    這個方法所用的思惟模式也符合這種精神。雖然圖形符號在報告中表示設計是有用的                                  [ 2 ] ,

但它不能緊密地支持集體討論。可是,每一個人都懂得畫輪廓,並且不少字處理器有一些畫輪廓
的方法,容許抓取幾塊輪廓,很快地移動它們。這使交互集體討論會上快速設計很完美。另外,
人們能夠擴展和推倒輪廓,決定於系統中粒度的不一樣層次。(如後描述)由於程序員建立了設
計,建立了設計文檔,因此關於項目狀態的報告能用一個像運行編譯器同樣的過程產生。

1.4.2 高概念

    創建的系統,不管如何複雜,都有一個基本的目的,它服務於哪一行業和它應知足什以基
本須要。若是咱們看看用戶界面、硬件、系統特殊的細節、編碼算法和效率問題,那麼咱們最
終會發現它有簡單和直接的核心。就像好萊塢電影中所謂的高概念,咱們能用一兩句話描述它。
這種純描述是起點。
    高概念至關重要,由於它爲咱們的項目定調。它是委派語句,不須要一開始就正確(能夠

在徹底清楚它以前完善這個論述或構思設計),它只是嘗試,直到它正確。例如,在空中交通
控制系統中,咱們能夠從系統的一個高概念開始,即準備創建「控制塔跟蹤飛機。」可是當將
該系統用於很是小的飛機場時,也許只有一個導航員或無人導航。更有用的模型不會使得正在
建立的解像問題描述那麼多:「飛機到達、卸貨、服務和從新裝貨、離去。」

1.4.3 論述(treatment)

    劇本的論述是用一兩頁紙寫的故事概要,即高概念的外層。計算機系統發展高概念和論述

的最好的途徑多是組成一個小組,該小組有一個具備寫能力的輔助工具。在集體討論中能提
出建議,輔助工具在與小組相連的網絡計算機上或在屏幕上表達這些思想。輔助工具只起捉刀
人的做用,不評價這些思想,只是簡單地使它們清楚和保持它們通順。
    論述變成了初始對象發現的起點和設計的第一個雛形,它也能在擁有輔助工具的小組內完
成。

1.4.4 結構化

    對於系統,結構是關鍵。沒有結構,就會任意收集無心義事件。有告終構,就有了故事。
故事的結構經過特徵表示,特徵對應於對象,情節結構對應於系統設計。
    1. 組織系統
    如前所述,對於這種方法,最主要的描述工具是具備歸納功能的高級字處理器。

  [1] 個人觀察是基於我最熟悉的:Microsoft Wo r d的擴展功能,它已被用於產生了這本書的照相機準備的頁。

  [2] 我鼓勵這種選擇,即用簡單的方框、線和符號,它們在畫字處理的包時是可用的,而不是很難產生的無定型的

     形狀。

----------------------- Page 15-----------------------

                                                    第1章 對象的演化         15
 下載

    從「高概念」、「論述」、「對象」和「設計」的第一層開始。當對象被發現時它們就被放在
第二層子段中,放在「對象」下面。增長對象接口做爲第三層子段,放在對象的特殊類下面。

若是基本表述文本產生,就在相應的子段下面做爲標準文本。
    由於這個技術包括鍵入和寫大綱,不依靠圖畫,因此會議討論過程不受創做該描述的速度
限制。
    2. 特徵:發現初始對象
    論述包括名詞和動詞。當咱們列出它們後,通常將名詞做爲類,動詞或者變爲這些類的方

法或者變爲系統設計的進程。雖然在第一遍以後程序員可能對他找出的結構不滿意,但請記住,
這是一個反覆的過程。在未來的階段和後面的設計中能夠增長另外的類和方法,由於那時程序
員對問題會有更清晰的認識。這種構造方法的要點是不須要程序員當前徹底理解問題,因此不
指望設計一會兒展示在程序員的面前。
    從簡單的論述檢查開始,爲每一個已找出的惟一名稱建立「對象」中的第二層子段。取那些

很顯然做用於對象的動詞,置它們於相應名詞下面的第三層方法子段。對每一個方法增長參數表
(即便它最初是空的)和返回類型。這就給程序員一個雛形以供討論與完善。
    若是一個類是從另外一個類繼承來的,則它的第二層子段應當儘量靠近地放在這個基類之
後,它的子段名應當顯示這個繼承關係,就像寫代碼:derived:public base時應當作的。這容許
適當地產生代碼。

    雖然能設置系統去表示從公共接口繼承來的方法,但目的是隻建立類和它們的公共接口,
其餘元素都被認爲是下面實現的部分,不是高層設計。若是要表示它們,它們應看成爲相應類
下面的文本層註解出現。
    當決策點肯定後,使用修改的 O c c a m ’s Razor辦法:考慮這個選擇並選擇最簡單的一個,
由於簡單的類幾乎老是最好的。向類增長元素很容易,可是隨着時間的推移,丟掉元素就困

難了。
    若是須要培植這一過程,請看一個懶程序員的觀點:您應當但願哪些對象魔術般地出現,
用以解決您的問題?讓一些能夠用的類和各類系統設計模式做爲手頭的參考是有用的。
    咱們不要老是在對象段裏,分析這個論述時應當在對象和系統設計之間來回運動。任什麼時候
候,咱們均可能想在任何子段下面寫一些普通文本,例若有關特殊類或方法的思想或註解。

    3. 情節:初始系統設計
    從高概念和論述開始,會出現一些子情節。一般,它們就像「輸入、過程、輸出」或「用
戶界面、活動」同樣簡單。在「設計」下面,每一個子情節有它本身的第二層子段。大多數故事
沿用一組公共情節。在o o p中,這種相似被稱爲「模式」。查閱在o o p設計模式上的資源,能夠
幫助對情節的搜索。

    咱們如今正在建立系統的粗略草圖。在集體討論會上,小組中的人們對他們認爲應該出現
在系統中的活動提出建議,而且分別記錄,不須要爲將它與整個系統相連而工做。讓整個項目
組,包括機械設計人員(若是須要)、市場人員、管理人員,都出席會議。這是特別重要的,
這不只使得每一個人心情舒暢,由於他們的意見已經被考慮,並且每個人的參加對會議都是有
價值的。

    子情節有一組變化的階段或狀態,有在階段之間變化的條件,有包含在每一個過渡中的活動。
在特殊的子情節下面,每一個階段都給出它本身的第三層子段。條件和過渡被描寫爲文本,放在
這個階段的標題下。狀況若是理想,咱們最終可以寫出每一個子情節的基本的東西(由於設計是
反覆過程),做爲對象的建立並向它們發送的消息。這就變成了這個子情節的最初代碼。

----------------------- Page 16-----------------------

  16      C + +編程思想
                                                                      下載

    設計發現和對象發現過程相似,所以咱們能夠在會議過程當中將子條目增長到這兩個段中。

1.4.5 開發

    這是粗設計到編譯代碼的最初轉換,編譯代碼能被測試,特別是,它將證實或者反證咱們

的設計。這不僅是一遍處理,而是一系列寫和重寫的開始,因此,重點是從文檔到代碼的轉換,
該轉換用這樣一種方法,即經過對代碼中的結構或有關文字的改變從新產生文檔。這樣,在編
碼開始後(和不可避免的改變出現後)產生設計文檔就變得很是容易了,而設計文檔能變成項
目進展的報告工具。

    1. 初始翻譯
    經過在第一層的標題中使用標準段名「對象」和「設計」,咱們就能運行咱們的工具以突

出這些段,並由它們產生文件頭。依據咱們所在的主段和正在工做的子段層,咱們將完成不一樣
的工做。最容易的辦法多是讓咱們的工具或宏把文檔分紅小塊而且相應地在每一小塊上工
做。
    對於「對象」中的每一個第二層段,在段名(類名和它的基類名,若是有的話)中會有足夠
的信息用以自動地產生類聲明,在這個類名下面的每一個第三層子段,段名(成員函數名、參數

表和返回類型)中會有足夠的信息用以產生這個成員函數的聲明。咱們的工具將簡單地管理這
些並建立類聲明。
    爲了使問題簡單,單個類聲明將出如今每一個頭文件中。命名這些頭文件的最好辦法,也許
是包括這個文件名,以做爲這個類的第二層段名中的標記信息。
    編制情節能夠更精細。每一個子情節能夠產生一個獨立的函數,由內部main( )調用,或者就

是main( ) 中的一段。從一個能完成咱們的工做的事情開始,更好的模式可能在之後的反覆中形
成。

    2. 代碼產生
    使用自動工具(大部分字處理工具對此是合適的):
    1)  爲  「對象」段中描述的每一個類產生一個頭文件,也就是爲每個類建立一個類聲明,
帶有公共接口函數和與它們相聯繫的描述塊;對每一個類附上在之後很容易分析的專門標號。

    2)  爲每一個子情節產生頭文件,而且將它的描述拷貝在文件的開頭,做爲註釋塊,接下來跟
隨函數聲明。
    3)  用它的概要標題層標記每一個子情節、類和方法,造成加標號的、被註釋的標識符:
/ / # [ 1 ] 、// #[2]等等。全部產生的文件都有文檔註釋,放在帶有標號的專門標識塊中。類名和函
數聲明也保留註釋標記。這樣,轉換工具能檢查、提取全部信息,並用文檔描述語言更好地重

新產生源文檔,例如用Rich Text Format(RT F )描述語言。
    4)  接口和情節在這時應當是可編譯的(但不可鏈接),所以能夠作語法檢查。這將保證設
計的高層完整性。文檔能由當前正在編譯的文件從新產生。
    5) 在這個階段,有兩件事會發生。若是設計是在早期,那麼咱們可能須要繼續加工處理集
體討論會的文檔(而不是代碼)或小組負責的那部分文檔。然而,若是設計是徹底充足的,那

麼咱們就能夠開始編碼。若是在編碼階段增長接口元素,則這些元素必須連同加過標號的註釋
一塊兒由程序員加標號,因此從新產生的程序能用新信息產生文檔。
    若是咱們擁有前端編譯器,咱們確實能夠對類和函數自動進行編譯,可是它是大做業,並
且這個語言正在演化。使用明確的標號,是至關不安全的,商業瀏覽工具能用以檢驗是否全部
的公共函數都已經造成文檔了(也就是,它們已加標號了)。

----------------------- Page 17-----------------------

                                                    第1章 對象的演化         17
 下載

1.4.6 重寫

    這相似於重寫電影劇本以完善它,使它更好。在程序設計中,這是重複過程,咱們的程序

從好到更好,在第一遍中尚未真正理解的問題變得清楚了。咱們的類從在單個項目中使用進
化爲可重用的資源。
    從工具的觀點看,轉換該過程略微複雜,咱們但願能分解頭文件,使咱們能從新整理這些
文件使它們成爲設計文檔,包括在編碼過程當中已經作過的所有改變。在設計文檔中對設計有任
何改變,頭文件也必須徹底重建,這樣就不會丟失爲了獲得在第一遍反覆中編譯所須要的頭文

件而作的任何工做。所以,咱們的工具不只應當能找出加標號的信息,將它們變成段層和文本
層,並且還應當能發現其餘信息,爲其餘信息加標號和存放其餘信息,例如在每一個文件開頭的
# i n c l u d e 。咱們應當記住,頭文件表達了類設計而咱們必須可以由設計文檔從新產生頭文件。
    咱們還應當注意文本層註解和討論,它們最初產生時被轉換爲加標號的註釋,比在設計演
化過程當中程序員修改的內容更多。基本上,這些註解和討論被收集並放在各自的地方,所以設

計文檔反映了新的信息。這就容許咱們去改變這些信息,而且返還到已產生的頭文件中。
    對於系統設計(m a i n ( )和任何支持函數),咱們也可能想得到整個文件,添加段標識符,
例如A 、B、C等等,做爲加標號的註釋(不用行號,由於行號會改變),並附上段描述(而後
返還m a i n ( )文件,做爲加標號的文本)。
    咱們必須知道何時中止,何時重複設計。理想的狀況是,咱們達到了目標功能,

處在完善和增長新功能的過程當中,最後期限到來,強迫咱們中止併發出咱們的版本(記住,軟
件是預定業務)。

1.4.7 邏輯

    咱們會週期性地但願知道項目在什麼地方須要從新整理文檔。若是是在網上使用自動化工
具,這個過程可有可無。常常地整集和維護設計文檔是項目領導者或管理者的責任,而項目組
或我的只對文檔的一小部分負責(也就是他們的代碼和註釋)。

    對於補充功能,例如類圖表,能夠用第三方工具生成,並自動地添加到文檔中。
    在任什麼時候候,均可以經過簡單地「刷新」文檔生成當前的報告。這樣能夠看到程序各部分
的狀況,也支援了項目組,併爲最終用戶文檔提供了直接的更新。這些文檔對於使新組員儘快
參加工做也頗有價值。
    單個文檔比由某些分析、設計和實現方法而產生的全部文檔更合理。雖然一個較小的文檔

缺少印象,但它是「活的」。相反,例如一個分析文檔只是對於項目的特殊階段有價值,而後
很快變得陳舊了。對於知道將被丟掉的文檔,人們很難對它再做更大的努力。

1.5  其餘方法

                                                     [ 1 ]
    當前有大量的形式化方法(不下2 0種)可用,由程序員選擇  。有一些並不徹底獨立,它
們有共同的思想基礎,在某個更高層上,它們都是相同的。在最低層,不少方法都受語言的缺
省表現約束,因此每一個方法大概只能知足簡單的項目。真正的好處是在較高層上,一個方法可

用於實時硬件控制器,但可能不容易適合檔案數據庫的設計。
    每種方法都有它的啦啦隊,在咱們對大規模方法採用以前應當更好地懂得它的語言基礎,

  [1] 這些是下書中的綜述:Object Analysis and Design:Description of Methods, edited by Andrew T. F.Hutt of Object

     Management Group(OMG),john Wiley & Sons, 1994 。

----------------------- Page 18-----------------------

  18      C + +編程思想
                                                                      下載

以此來認識一個方法是否適合咱們的特殊風格,或者咱們是否真須要一個方法。下面是對最流
行的三種方法的描述,主要是供參考,不是購買比較。若是讀者想了解更多的方法,有許多書

籍能夠參考,還有許多學習班能夠參加。

1.5.1 Booch

    B o o c h方法 [1]  是最先、最基本和最普遍被引用的一個方法。由於它是較早發展的,因此對

各類程序設計問題都有意義。它的焦點是在O O P的類、方法和繼承的單獨性能上,步驟以下:
    1. 在抽象的特定層上肯定類和對象
    這是可預見的一小步。咱們用天然語言聲明問題和解,而且肯定關鍵特性,例如造成類的

基本名詞。若是咱們在煙花行業,可能想肯定工人、鞭炮、顧客,進而,可能須要肯定化學藥
劑師、組裝者、處理者,業餘鞭炮愛好者和專業鞭炮者、購買者和觀衆。甚至更專門的,能確
定年輕觀衆、老年觀衆、少年觀衆和父母觀衆。
    2. 肯定它們的語義
    這是在相應的抽象層上定義類。若是咱們計劃建立一個類,咱們應當肯定類的相應觀衆。

例如,若是建立鞭炮類,那麼誰將觀察它,化學藥劑師仍是觀衆?前者想知道在結構中有什麼
化學藥劑,然後者將對鞭炮爆炸時釋放的顏色和形狀感興趣。若是化學藥劑師問起一個鞭炮產
生主顏色的化學藥劑,最好不要回答「有點冷綠和紅」。一樣,觀衆會對鞭炮點火後噴出的只
是化學方程式感到疑惑不解。也許,咱們的程序是爲了眼前的市場,化學藥劑師和觀衆都會用
它,在這種狀況下,鞭炮應當有主題屬性和客體屬性,而且能以和觀察者相應的外觀出現。

    3. 肯定它們之間的關係(CRC 卡片)
    定義一個類如何與其餘類互相做用。將關於每一個類的信息製成表格的常見的方法是使用類,
責任,協做(Class, Responsibility, Collaboration, CRC)卡片。這是一種小卡片(一般是一個索
引卡),在它上面寫上這個類的狀態變量、它的責任(也就是它發送和接受的消息)和對與它
互相做用的其餘類的引用。爲何須要索引卡片?理由是咱們應當能在一張小卡片上存放咱們

須要知道的關於一個類的全部內容,若是不能知足這點,那麼這個類就太複雜了。理想的類應
當能在掃視間被理解,索引卡片不只實際可行,並且恰好符合大多數人思考問題合理的信息量。
一種不涉及主要技術改革的解決辦法對於每一個人均可用的(就像本章前面描述的草稿方法中的
文檔結構化同樣)。
    4. 實現類

    如今咱們知道作什麼了,投入精力進行編碼。在大多數項目中, 編碼將影響設計。
    5. 反覆設計
    這樣的設計過程給人一種相似著名的程序開發瀑布流方法的感受。如今對這種方法的見解
是有分歧的。在第一遍查看主要的抽象是否能夠將類清晰地分離以後,可能須要重複前三個步
驟。B o o c h寫了一個「粗糙旅程完型設計過程」。若是這些類真正反映了這個解的天然語言描

述,那麼程序有完型的觀點應當是可能的。也許要記住的最重要的事情是,經過缺省—實際
上是經過定義,若是修改了一個類,它的超類和子類應當仍然工做。不須要懼怕修改,它不會
破壞程序,對結果的任何改變將限制在子類和 /或被改變的這個類的特定協做者中。爲了這個
類而對C R C卡片的掃視也許是咱們須要檢驗新版本的惟一線索。

    [1] 參看Object-Oriented Design with Applications by Grady Booch, Benjamin Cummings, 1991.有關C + +更新的版

       本。

----------------------- Page 19-----------------------

                                                    第1章 對象的演化         19
 下載

1.5.2 責任驅動的設計(RDD)

    這個方法 [1]  也用C R C卡片。在這裏,正如名稱蘊涵,卡片的重點在於責任的受權,而不是

外觀。這可由下面例子說明:B o o c h方法能夠產生僱員—銀行僱員—銀行經理的繼承,而
在R D D中,這可能出現經理—金融經理—銀行經理的繼承。銀行經理的主要責任是經理的
責任,因此這種繼承反映了這種關係。
    更形式化地說,R D D包含以下內容:
    1) 數據或狀態: 對每一個類的數據或狀態變量的描述。

    2) 池和源: 數據池和源的標識;處理或產生數據的類。
    3) 觀察者或觀點: 觀點或觀察者類,用以隔離硬件依賴。
    4)  輔助工具或幫助器: 輔助工具或幫助器類,例如鏈接表,它們包含不多的狀態信息並
簡單地幫助其餘類工做。

1.5.3 對象建模技術(OMT)

    對象建模技術 [ 2 ]  (O M T )對過程增長一個或多個複雜層。B o o c h方法強調類的功能表現,

簡單地定義它們做爲天然語言解的輪廓。 R D D進了一步,強調類的責任超過強調類的表現。
O M T用詳細地繪製圖表的方法,不只描述類,並且還描述系統的各類狀態,以下所述:
    1) 對象模型,「什麼」,對象圖表: 該對象模型相似於由B o o c h方法和R D D產生的那些模
型;對象的類經過責任相關聯。
    2)  動態模型,「什麼時候」,狀態圖表: 該動態模型描寫了系統的與時間有關的狀態。不一樣的狀

態是經過轉變相關聯的;包含時間有關狀態的一個例子是實時傳感器,它從外部世界收集數據。
    3)  功能模型,「如何」,數據流程表: 該功能模型跟蹤數據流。它的理論是,在程序的最
低層上,實際的工做是經過使用過程而完成的,所以程序的低層行爲最好經過畫數據流來理解,
而不是經過畫它的對象來理解。

1.6  爲向OOP轉變而採起的策略

    若是咱們決定採用O O P,咱們的下一個問題多是「我如何才能使得管理員、同事、部門、

夥伴開始使用O O P?」想想,做爲獨立的程序員,應當如何學習使用新語言和新程序設計。
正如咱們之前所作的,首先訓練和作例子,再經過一個試驗項目獲得一個基本的感受,不要作
太混亂的事情,而後嘗試作一個「真實世界」的實際有用的項目。在咱們的第一個項目中,我
們經過讀、向上司問問題、與朋友切磋等方式,繼續咱們的訓練。基本上,這就是許多做者建
議的從C轉到C + +的方法。轉變整個公司固然應當採用某個動態的小組,但回憶我的是如何作

這件事的,能在轉變的每一步中起到有益的做用。

1.6.1 逐步進入OOP

    當向O O P和C + +轉變時,有一些方針要考慮:
    1. 訓練

    第一步是一些形式的培訓。記住公司在日常的 C代碼上的投資,而且當每一個人都在爲這些
遺留的東西而爲難時,應努力在6到9個月內不使公司徹底陷入混亂。對一個小組進行培訓,更

    [1] 參看Designing Object-Oriented Software by Rebecca Wirfs-Brock et al., Prentice Hall, 1990 。

    [2] 參看Object-Oriented Modeling and Design by James Rumbaugh et al., Prentice Hall,1991。

----------------------- Page 20-----------------------

  20      C + +編程思想
                                                                      下載

適宜的狀況是,這個小組成員是一些勤奮好學、能很好地在一塊兒工做的人們,當他們學習 C + +
時,能造成他們本身的支持網。

    有時建議採用另外一種方法,即在整個公司層一塊兒訓練,包括爲策略管理員而開設的概論課
程,以及爲項目建設者而開設的設計課程和編程課程。對於較小的公司或較大公司的部門,用
他們作事情的方法去作基本的改變是很是好的。然而代價較高,因此一些公司可能選擇以項目
層訓練開始,作飛行員式的項目(可能請一個外面的導師),而後讓這個項目組變成公司其餘
人的老師。

    2. 低風險項目

    首先嚐試一個低風險項目,並容許出錯。一旦咱們已經獲得了一些經驗,咱們就將這第一
個小組的成員安插到其餘項目組中,或者用這個組的成員做爲 O O P的技術支柱。第一個項目可
能不能正確工做,因此該項目在事情的安排上應當不是很是重要的。它應當是簡單的、自包含
的和有教學意義的。這意味着它應當包括建立對公司的其餘程序員學習C + +有意義的類。

    3. 來自成功的模型
    從草稿開始以前,挑一些好的面向對象設計的例子。極可能有些人已經解決過咱們的問題,

若是他們尚未正確地解決它,咱們能夠應用已經學到的關於封裝的知識,來修改存在的設計,
以適合咱們本身的須要。這是設計模式 [1]             的通常概念。

    4. 使用已存在的類庫
    轉變爲C + +的主要經濟動機是容易使用以類庫形式存在的代碼,最短的應用開發週期是除
了m a i n ( ) 之外沒必要本身寫任何東西。然而,一些新程序員不理解這個,不知道已存在的類庫,

或因爲對語言的迷戀但願寫可能已經存在的類。若是咱們努力查找和重用其餘人在轉變過程當中
的早期代碼,那麼咱們在O O P和C + +方面將是最優的。
    5. 不要用C + +重寫已存在的代碼
    雖然用C + +編譯C代碼一般會有(有時是很大的)好處,它能發現老代碼中的問題,可是
把時間花在對已存在的功能代碼進行C + +重寫上,一般不是時間的最佳利用。若是代碼是爲重

用而編寫的,會有很大的好處。可是,有可能出現這種狀況:在最初的幾個項目中,並不能看
到效率如您夢想的同樣增加,除非這是新項目。固然,若是項目是從頭開始的, C + +和O O P最
好。

1.6.2 管理障礙

    對於管理員,他的任務就是爲他的小組得到資源,使他的小組克服通往成功路上的障礙。
而且,他應當努力創造高產的和使人愉快的環境,以使得他的小組最大可能完成他所要求的奇

跡。轉變到C + + 的過程包含有下面的三類障礙,若是不花費何代價,那真是奇怪的。雖然對於
C程序員(也許對於其餘過程語言的程序員)小組,可證實選擇 C + + 比選擇其餘O O P語言代價
低,但這也不是免費的。咱們應當認識到,在咱們試圖動員咱們的公司轉移到 C + +和着手轉移
以前,還有一些障礙。

    1. 啓動代價
    這個代價要比獲得C + +編譯器大得多。若是投資培訓(也許爲了指導第一個項目),而且

若是選購解決問題的類庫,而不是試圖本身創建這些類庫,那麼能夠將中長期代價減到最低。
這些都是很花錢的,必須在制定計劃時充分考慮。另外,當學習新語言連同程序設計環境時,

  [1] 參看Camma et al., 出處同上。

----------------------- Page 21-----------------------

                                                    第1章 對象的演化         21
 下載

還會有損失效率的隱含代價。培訓和指導確實能使這些代價降到最低,可是組員們必須克服他
們本身的困惑去理解這些問題。在這個過程當中,他們將會犯更多的錯誤(這是一個特徵,由於

失敗是成功之母)和有更低的效率。儘管如此,對於一些類型的程序設計問題、正確的類和正
確的開發環境,即便咱們還在學習C + +,也可能比咱們仍然用C語言時有更高的效率(即使考
慮到咱們正在犯更多的錯誤和天天寫更少行的代碼)。

    2. 性能問題
    一個共同的問題是「O O P不會使個人程序更大且更慢嗎?」回答是「不必定」。大多數傳
統的O O P語言是在實驗和在頭腦中快速創建原型的狀況下設計的,而不側重於普通操做。這樣,

它們就本質地決定了在規模上的明顯增加和在速度上的明顯下降。然而, C + +是在已有生產程
序的狀況下設計的。當咱們的焦點是創建快速原型時,咱們能夠儘快地把構件拉在一塊兒,而忽
略效率問題。若是咱們正在使用任何第三方庫,這些庫一般已經被廠商優化過了,咱們用快速
開發方法時,不管如何這都不成問題。咱們有一個咱們喜歡的系統時,若是它足夠小和足夠快,
這就好了。若是達不到這種要求,咱們就開始用一個有益的工具進行調整,首先尋求加速,這

能夠經過簡單地運用創建在C + +中的一些特性作到。若是這還不行,應當尋求修改低層來實現,
因此使用特定類的原有代碼不須要改變。只有這一切都沒法解決這個問題時才須要改變設計。
性能在設計中的地位如此重要,以至於這一因素必然是主要設計標準的指標之一。經過快速原
型較早地發現這一點是有好處的。
    如本章較早所述,C和C + +之間在規模和速度上的差距經常是± 1 0 %,而且一般更接近。

實際上咱們能夠用C + +獲得在規模和速度上超過C的系統,由於咱們爲C++  所作的設計能夠完
全不一樣於爲C所作的設計。
    在C和C + +之間比較規模和速度的證據至今還只是估計,也許還會繼續如此。儘管一些人建議
對相同的項目用C和C + +同時作,但也許不會有公司把錢浪費在這裏,除非它很是大而且對這個研
究項目感興趣。即使如此,它也但願錢花得更好。已經從C                     (或其餘過程語言)轉到C + +的程序員

幾乎一致都有在程序設計效率上很大提升的我的經驗,這是咱們能找到的最引人注目的證據。

    3. 廣泛的設計錯誤
    當項目組開始使用O O P和C + +時,程序員們將會出現一系列廣泛的設計錯誤。這常常會發
生,由於在早期項目的設計和實現過程當中從專家們那裏獲得的反饋太少,公司中沒有專家。程
序員彷佛會以爲,在這個週期中,他懂得O O P太早了並開始了一條很差的道路。有時,對這個
語言有經驗的人認爲顯而易見的事情多是新手們在內部激烈爭論的主題。大量的這類問題都

能經過聘用外部專家培訓和指導來避免。

1.7  小結

    這一章但願讀者對面向對象程序設計和 C + + 的普遍問題有必定的感性認識,包括爲何
O O P是不一樣的;爲何C + +特別不一樣;什麼是O O P方法的概念和爲何應當(或不該當)使用
一個概念;提出最小化方法的建議(這是我發展的方法),它容許以最小費用開始O O P項目;
對其餘方法的討論;最後當公司轉到O O P和C + +時會遇到的各類問題。

    O O P和C + +可能不會對每一個人都適合。對本身的須要作出估計,並決定 C + +是否能很好地
知足本身的要求,或者是否能很好地離開別的程序設計系統,這是很重要的。若是程序員知道,
他的須要對能夠預見的將來是很是特別的,若是他有特殊的約束,不能由 C + +知足,那麼能夠
用它研究替代物。即使他最終選擇了C + +做爲他的語言,他至少應當懂得這些選項是什麼,並
應當對爲何取這個方向有清晰的見解。

----------------------- Page 22-----------------------

                                                                      下載

                      第2章  數 據 抽 象

    C + +是一個能提升效率的工具。爲何咱們還要努力(這是努力,無論咱們試圖作的轉變

多麼容易)使咱們從已經熟悉且效率高的語言(在這裏是 C語言)轉到另外一種新的語言上?而
且使用這種新語言,咱們會在確實掌握它以前的一段時間內下降效率。這歸因於咱們確信經過
使用新工具將會獲得更大的好處。
    用程序設計術語,多產意味着用較少的人在較少的時間內完成更復雜和更重要的程序。然
而,選擇語言時確實還有其餘問題,例如運行效率(該語言的性質引發代碼臃腫嗎?)、安全性

(該語言能有助於咱們的程序作咱們計劃的事情並具備很強的糾錯能力嗎?)、可維護性(該語
言能幫助咱們建立易理解、易修改和易擴展的代碼嗎?)。這些都是本書要考察的重要因素。
    簡單地講,提升生產效率,意味着本應花費三我的一星期的程序,如今只須要花費一我的
一兩天的時間。這會涉及到經濟學的多層次問題。生產效率提升了,咱們很高興,由於咱們正
在建造的東西功能將會更強;咱們的客戶(或老闆)很高興,由於產品生產又快,用人又少;

咱們的顧客很高興,由於他們獲得的產品更便宜。而極大提升效率的惟一辦法是使用其餘人的
代碼,即便用庫。
    庫,簡單地說就是一些人已經寫的代碼,按某種方式包裝在一塊兒。一般,最小的包是帶有
擴展名如L I B 的文件和向編譯器聲明庫中有什麼的一個或多個頭文件。鏈接器知道如何在 L I B
文件中搜索和提取相應的已編譯的代碼。可是,這只是提供庫的一種方法。在跨越多種體系結

構的平臺上,例如U N I X ,一般,提供庫的最明智的方法是用源代碼,這樣在新的目標機上它
能被從新編譯。而在微軟Wi n d o w s上,動態鏈接庫是最明智的方法,這使得咱們可以利用新發
布的D D L常常修改咱們的程序,咱們的庫函數銷售商可能已經將新D D L發送給咱們了。
    因此,庫大概是改進效率的最重要的方法。 C + +的主要設計目標之一是使庫容易使用。這
意味着,在C 中使用庫有困難。懂得這一點就對C + +設計有了初步的瞭解,從而對如何使用它

有了更深刻的認識。

2.1  聲明與定義

    首先,必須知道「聲明」和「定義」之間的區別,由於這兩個術語在全書中會被確切地使
用。「聲明」向計算機介紹名字,它說,「這個名字是什麼意思」。而「定義」爲這個名字分配

存儲空間。不管涉及到變量時仍是函數時含義都同樣。不管在哪一種狀況下,編譯器都在「定義」
處分配存儲空間。對於變量,編譯器肯定這個變量佔多少存儲單元,並在內存中產生存放它們
的空間。對於函數,編譯器產生代碼,併爲之分配存儲空間。函數的存儲空間中有一個由使用
不帶參數表或帶地址操做符的函數名產生的指針。
    定義也能夠是聲明。若是該編譯器尚未看到過名字A ,程序員定義int A ,則編譯器立刻

爲這個名字分配存儲地址。
    聲明經常使用於e x t e r n關鍵字。若是咱們只是聲明變量而不是定義它,則要求使用 e x t e r n。
對於函數聲明,e x t e r n是可選的,不帶函數體的函數名連同參數表或返回值,自動地做爲一個
聲明。

----------------------- Page 23-----------------------

                                                    第2章  數據 抽 象       23
 下載

    函數原型包括關於參數類型和返回值的所有信息。int f(float,char);是一個函數原型,由於
它不只介紹f這個函數的名字,並且告訴編譯器這個函數有什麼樣的參數和返回值,使得編譯

器能對參數和返回值作適當的處理。C + +要求必須寫出函數原型,由於它增長了一個重要的安
全層。下面是一些聲明的例子。

    在函數聲明時,參數名可給出也可不給出。而在定義時,它們是必需的。這在 C語言中確

實如此,但在C + +中並不必定。

    全書中,咱們會注意到,每一個文件的第一行是一個註釋,它以註釋符開始,後面跟冒號。

這是我用的技術,能夠利用諸如「 g r e p」和「a w k」這樣的文本處理工具從代碼文件中提取信

息。在第一行中還包含有文件名,所以能在文本和其餘文件中查閱這個文件,本書的代碼磁盤

中也很容易定義這個文件。

2.2  一個袖珍C庫

    一個小型庫一般以一組函數開始,可是,已經用過別的C  庫的程序員知道,這裏一般有更

多的東西,有比行爲、動做和函數更多的東西。還有一些特性(顏色、重量、紋理、亮度),

它們都由數據表示。在C語言中,當咱們處理一組特性時,能夠方便地把它們放在一塊兒,造成

一個s t r u c t 。特別是,若是咱們想表示咱們的問題空間中的多個相似的事情,則能夠對每件事

情建立這個s t r u c t的一個變量。

    這樣,在大多數C庫中都有一組s t r u c t和一組活動在這些s t r u c t上的函數。如今看一個這樣

的例子。假設有一個程序設計工具,當建立時它的表現像一個數組,但它的長度能在運行時建

立。我稱它爲s t a s h。

----------------------- Page 24-----------------------

  24      C + +編程思想
                                                                      下載

    在結構內部須要引用這個結構時能夠使用這個 s t r u c t的別名,例如,建立一個鏈表,須要
指向下一個s t r u c t的指針。在C庫中,幾乎能夠在整個庫的每一個結構上看到如上所示的 t y p e d e f 。

這樣作使得咱們能把s t r u c t做爲一個新類型處理,而且能夠定義這個s t r u c t的變量,例如:

    stash A, B, C;

    注意,這些函數聲明用標準 C  風格的函數原型,標準 C  風格比「老」C  風格更安全和更
清楚。咱們不只介紹了函數名,並且還告訴編譯器參數表和返回值的形式。
    s t o r a g e指針是一個unsigned char* 。這是 C  編譯器支持的最小的存儲片,儘管在某些機器
上它可能與最大的通常大,這依賴於具體實現。人們可能認爲,由於 s t a s h被設計用於存聽任何
類型的變量,因此v o i d *在這裏應當更合適。然而,咱們的目的並非把它看成某個未知類型

的塊處理,而是做爲連續的字節塊。
    這個執行文件的源代碼(若是咱們買了一個商品化的庫,咱們可能獲得的只是編譯好的
O B J或L I B或D D L等)以下:

----------------------- Page 25-----------------------

                                                     第2章  數據 抽 象          25
 下載

    注意本地的 #include  風格,儘管這個頭文件在本地目錄下,但仍然以相對於本書的根目錄
給出。這樣作,能夠建立不一樣於這本書根目錄的另外的目錄,很容易拷貝文件到這個新目錄下

去實驗,而沒必要擔憂改變 #include  中的路徑。

----------------------- Page 26-----------------------

  26      C + +編程思想
                                                                      下載

    initialize( )完成對 struct stash 的必要的設置,即設置內部變量爲適當的值。最初,設置
s t o r a g e指針爲零,設置size 指示器也爲零,表示初始存儲未被分配。

    add( ) 函數在s t a s h的下一個可用位子上插入一個元素。首先,它檢查是否有可用空間,如
果沒有,它就用後面介紹的 inflate( )  函數擴展存儲空間。
    由於編譯器並不知道被存放的特定變量的類型(函數返回的都是v o i d * ),因此咱們不能只
作賦值,雖然這的確是很方便的事情。代之,咱們必須用標準 C  庫函數memcpy( )一個字節一
個字節地拷貝這個變量,第一個參數是 memcpy( )  開始拷貝字節的目的地址,由下面表達式產

生:

    &(S->storage[S->next * S->size])

    它指示從存儲塊開始的第n e x t個可用單元結束。這個數實際上就是已經用過的單元號加一

的計數,它必須乘上每一個單元擁有的字節數,產生按字節計算的偏移量。這不產生地址,而是
產生處於這個地址的字節,爲了產生地址,必須使用地址操做符 &。
    memcpy( ) 的第二和第三個參數分別是被拷貝變量的開始地址和要拷貝的字節數。n e x t計數
器加一,並返回被存值的索引。這樣,程序員能夠在後面調用fetch( ) 時用它來取得這個元素。
    fetch( )首先看索引是否越界,若是沒有越界,返回所但願的變量地址,地址的計算採用與

add( ) 中相同的方法。
    對於有經驗的C程序員count( )乍看上去可能有點奇怪,它好像是自找麻煩,作手工很容易
作的事情。例如,若是咱們有一個 struct stash,例假設稱爲i n t S t a s h,那麼經過用i n t S t a s h . n e x t
找出它已經有多少個元素的方法彷佛更直接,而不是去作c o u n t ( & i n t S t a s h )函數調用(它有更多
的花費)。可是,若是咱們想改變s t a s h的內部表示和計數計算方法,那麼這個函數調用接口就

容許必要的靈活性。而且,不少程序員不會爲找出庫的「更好」的設計而操心。若是他們能着
眼於s t r u c t和直接取n e x t 的值,那麼可能不經容許就改變n e x t 。是否是能有一些方法使得庫設計
者能更好地控制像這樣的問題呢?(是的,這是可預見的)。

動態存儲分配

    咱們不可能預先知道一個s t a s h須要的最大存儲量是多少,因此由s t o r a g e指向的內存從堆中
分配。堆是很大的內存塊,用以在運行時分一些小單元。在咱們寫程序時,若是咱們還不知道

所需內存的大小,就能夠使用堆。這樣,咱們能夠直到運行時才知道須要存放 2 0 0個a i r p l a n e變
量,而不只是2 0個。動態內存分配函數是標準  C  庫的一部分,包括 malloc( ) 、 calloc( ) 、
realloc( )和free( ) 。
    inflate( )函數使用realloc( )爲s t a s h獲得更大的空間塊。realloc( )把已經分配而又但願重分配
的存儲單元首地址做爲它的第一個參數(若是這個參數爲零,例如 initialize( ) 剛剛被調用時,

realloc( )分配一個新塊)。第二個參數是這個塊新的長度,若是這個長度比原來的小,這個塊
將不須要做拷貝,簡單地告訴堆管理器剩下的空間是空閒的。若是這個長度比原來的大,在堆
中沒有足夠的相臨空間,因此要分配新塊,而且要拷貝內存。 assert( )檢查以確信這個操做成
功。(若是這個堆用光了,malloc( )、calloc( )和realloc( )都返回零。)
    注意,C  堆管理器至關重要,它給出內存塊,對它們使用 free( ) 時就回收它們。沒有對堆

進行合併的工具,若是能合併就能夠提供更大的空閒塊。若是程序屢次分配和釋放堆存儲,最
終會致使這個堆有大量的空閒塊,但沒有足夠大且連續的空間能知足咱們對內存分配的須要。
可是,若是用堆合併器移動內存塊,又會使得指針保存的不是相應的值。一些操做環境,例如
Microsoft Wi n d o w s有內置的合併,但它們要求咱們使用專門的內存句柄(它們能臨時地翻轉爲

----------------------- Page 27-----------------------

                                                    第2章  數據 抽 象       27
 下載

指針,鎖住內存後,堆壓緊器不能移動它),而不是使用指針。
    assert( )是在A S S E RT. H中的預處理宏。assert( )取單個參數,它能夠是能求得真或假值的任

何表達式。這個宏表示:「我斷言這是真的,若是不是,這個程序將打印出錯信息,而後退出。」
再也不調試時,咱們能夠用一個標誌使得這個斷言被忽略。在調試期間,這是很是清楚和簡便的
測試錯誤的方法。不過,在出錯處理時,它有點生硬:「對不起,請進行控制。咱們的C  程序
對一個斷言失敗,而且跳出去。」在第1 7章中,咱們將會看到,C + +是如何用出錯處理來處理
重要錯誤的。

    編譯時,若是在棧上建立一個變量,那麼這個變量的存儲單元由編譯器自動開闢和釋放。
編譯器準確地知道須要多少存儲容量,根據這個變量的活動範圍知道這個變量的生命期。而對
動態內存分配,編譯器不知道須要多少存儲單元,不知道它們的生命期,不能自動清除。所以,
程序員應負責用 free( )釋放這塊存儲, free( ) 告訴堆管理器,這個存儲能夠被下一次調用的
malloc( ) 、calloc( )或realloc( )重用。合理的方法是使用庫中cleanup( )函數,由於在這裏,該函

數作全部相似的事情。
    爲了測試這個庫,讓咱們建立兩個s t a s h。第一個存放i n t,第二個存放8 0個字符的數組(我
們能夠把它看做新數據類型)。

----------------------- Page 28-----------------------

  28      C + +編程思想
                                                                      下載

    在main( ) 的開頭定義了一些變量,其中包括兩個s t a s h結構變量,固然。稍後咱們必須在這

個程序塊的對它們初始化。庫的問題之一是咱們必須向用戶認真地說明初始化和清除函數的重
要性,若是這些函數未被調用,就會出現許多問題。遺憾的是,用戶不老是記得初始化和清除
是必須的。他們只知道他們想完成什麼,並不關心咱們反覆說的:「喂,等一等,您必須首先
作這件事。」一些用戶甚至認爲初始化這些元素是自動完成的。的確沒有機制能防止這種狀況
的發生(只有多預示)。

    i n t S t a s h適合於整型, s t r i n g S t a s h適合於字符串。這些字符串是經過打開源代碼文件
L I B T E S T.C  和把這些行讀到 stringStash  而產生的。注意一些有趣的地方:標準 C  庫函數打開
和讀文件所使用的技術與在s t a s h中使用的技術相似。fopen( )返回一個指向 FILE struct 的指針,
這個 FILE struct是在堆上建立的,而且能將這個指針傳給涉及到這個文件的任何函數。(在這
裏是fgets( ) )。fclose( )所作的事情之一是向堆釋放這個FILE struct 。一旦咱們開始注意到這種

模式的,包含着s t r u c t和有關函數的 C 庫後,咱們就能處處看到它。
    裝載了這兩個s t a s h以後,能夠打印出它們。i n t S t a c h的打印用一個f o r循環,用count( )肯定
它的限度。s t r i n g S t a s h的打印用一個w h i l e語句,若是fetch( )返回零則表示打印越界,這時跳出循
環。
    在咱們考慮有關 C  庫建立的問題以前,應當瞭解另一些事情(咱們可能已經知道這些,

由於咱們是 C 程序員)。第一,雖然這裏用頭文件,並且實際上用得很好,但它們不是必須的。
在C中可能會調用還未聲明的函數。好的編譯器會告誡咱們應當首先聲明函數,但不強迫這樣
作。這是很危險的,由於編譯器能假設以 i n t參數調用的函數有包含i n t 的參數表,並據此處理
它,這是很難發現的錯誤。
    注意,頭文件 LIB.H  必須包含在涉及 stash         的全部文件中,由於編譯器不可能猜出這個結

構是什麼樣子的。它能猜出函數,即使它可能不該當這樣,但這是 C  的一部分。
    每一個獨立的C文件就是一個處理單元。就是說,編譯器在每一個處理單元上單獨運行,而編
譯器在運行時只知道這個單元。這樣,用包含頭文件提供信息是至關重要的,由於它爲編譯器
提供了對程序其餘部分的理解。在頭文件中的聲明特別重要,由於不管是在哪裏包含這個頭文
件,編譯器都會知道要作什麼。例如,若在一個頭文件中聲明void foo(float) ,編譯器就會知道,

若是咱們用整型參數調用它,它會自動把i n t轉變爲f l o a t。若是沒有聲明,這個編譯器就會簡單
地猜想,有一個函數存在,而不會作這個轉變。
    對於每一個處理單元,編譯器建立一個目標文件,帶有擴展名  .o  或 .obj  或相似的名字。必
須再用鏈接器將這些目標文件連同必要的啓動代碼鏈接成可執行程序。在鏈接期間,全部的外
部引用都必須肯定。例如在 L I B T E S T.C  中,聲明並使用函數 initialize( )  和fetch( ) ,(也就是,

編譯器被告知它們像什麼,)但未定義。它們是在 LIB.C  中定義的,這樣,在 L I B T E S T.C  中的這
些調用都是外部引用。當鏈接器將目標文件鏈接在一塊兒時,它找出未肯定的引用並尋找這些引

----------------------- Page 29-----------------------

                                                    第2章  數據 抽 象       29
 下載

用對應的實際地址,用這些地址替換這些外部引用。
    重要的是認識到,在 C 中,引用就是函數名,一般在它們前面加上下劃線。因此,鏈接器

所要作的就是讓被調用的函數名與在目標文件中的函數體匹配起來。若是咱們偶然作了一個調
用,編譯器解釋爲f o o ( i n t ) ,而在其餘目標文件中有 f o o ( f l o a t ) 的函數體,鏈接器將認爲一個
_ f o o在一處而另外一個_ f o o在另外一處,它會認爲這都是對的。在調用foo( )處將一個i n t放進棧中,
而foo( ) 函數體指望在這個棧中的是一個f l o a t 。若是這個函數只讀這個值而不對它寫,尚不會
破壞這個棧。但從這個棧中讀出的f l o a t值可能會有另外的某種理解。這是最壞的狀況,由於很

難發現這個錯誤。

2.3  放在一塊兒:項目建立工具

    分別編譯時(把代碼分紅多個處理單元),咱們須要一些方法去編譯全部的代碼,並告訴
鏈接器把它們與相應的庫和啓動代碼放在一塊兒,造成一個可執行文件。大部分編譯器容許用一
條命令行語句。例如編譯器命名爲c p p,可寫:

    cpp libtest.c lib.c

    這個方法帶來的問題是,編譯器必須首先編譯每一個處理單元,而無論這個單元是否須要重
建。雖然咱們只改變了一個文件,但卻須要耗費時間來對項目中的每個文件進行從新編譯。
    對這個問題的第一種解決辦法,已由 U N I X            (C的誕生地)提出,是一個被稱爲make               的

程序。m a k e 比較源代碼文件的日期和目標文件的日期,若是目標文件的日期比源代碼文件的
早,make   就調用這個編譯器對這個單元進行處理。咱們能夠從編譯器文檔  [1]                      中學到更多的關

於make  的知識。
    make 是有用的,但學習和配置 makefile  有點乏味。makefile  是描述項目中全部文件之間
關係的文本文件。所以,編譯器銷售商發行它們本身的項目建立工具。這些工具向咱們詢問項

目中有哪些處理單元,並肯定它們的關係。這些關係有些相似於 m a k e f i l e文件,一般稱爲項目
文件。程序設計環境維護這個文件,因此咱們沒必要爲它擔憂。項目文件的配置和使用隨系統而
異,假設咱們正在使用咱們選擇的項目建立工具來建立程序,咱們會發現如何使用它們的相應
文檔(雖然由編譯器銷售商提供的項目文件工具一般是很是簡單的,能夠不費勁地學會它們)。

文件名

    應當注意的另外一個問題是文件命名。在 C中,慣例是以擴展名. h命名頭文件(包含聲明),

以.c 命名實現文件(它引發內存分配和代碼生成)。C++ 繼續演化。它首先是在Unix 上開發的,
這個操做系統能識別文件名的大小寫。原來的文件名簡單地變爲大寫,造成 .H 和 .C 版本。這
樣對於不區分大小寫的操做系統,例如 M S - D O S,就行不通了。DOS C++  廠商對於頭文件和實
現文件分別使用擴展名.hxx  和 . c x x。後來,有人分析出,須要不一樣擴展名的惟一緣由是使得編
譯器能肯定編譯C仍是C + +文件。由於編譯器不直接編譯頭文件,因此只有實現文件的擴展名需

要改變。如今人們已經習慣於在各類系統上對於實現文件都使用 . c p p而對於頭文件使用 . h。

2.4  什麼是非正常

    咱們一般有特別的適應能力,即便是對本不該該適應的事情。 stash  庫的風格對於 C  程序
員已是經常使用的了,可是若是觀察它一下子,就會發現它是至關笨拙的。由於在使用它時,必

  [1] 參看由做者編寫的C++ Inside & Out, (Osborne/McGraw-Hill,1993)。

----------------------- Page 30-----------------------

  30      C + +編程思想
                                                                      下載

須向這個庫中的每個函數傳遞這個結構的地址。而當讀這些代碼時,這種庫機制會和函數調
用的含義相混淆,試圖理解這些代碼時也會引發混亂。

    然而在 C  中,使用庫的最大的障礙是名字衝突問題。C 對於函數使用單個名字空間,因此
當鏈接器找一個函數名時,它在一個單獨的主表中查找,而當編譯器在單個處理單元上工做時,
它只能對帶有某些特定名字的函數進行處理工做。
    如今假設要支持從不一樣的廠商購買的兩個庫,而且每一個庫都有一個必須被初始化和清除的
結構。兩個廠商都認爲initialize( )和cleanup( )是好名字。若是在某個處理單元中同時包含了這

兩個庫文件,C 編譯器怎麼辦呢?幸虧,標準 C 給出一個出錯,告訴在聲明函數的兩個不一樣的
參數表中類型不匹配。即使不把它們包含在同一個處理單元中,鏈接器也會有問題。好的鏈接
器會發現這裏有名字衝突,但有些編譯器僅僅經過查找目標文件表,按照在鏈接表中給出的次
序,取第一個找到的函數名(實際上,這能夠看做是一種功能,由於能夠用本身的版本替換一
個庫函數)。

    不管哪一種狀況,都不容許使用包含具備同名函數的兩個庫。爲了解決這個問題, C  庫廠商
經常會在它們的全部函數名前加上一個獨特字符串。因此, initialize( )和cleanup( )可能變爲
stash_initialize( )和stash_cleanup( ) 。這是合乎邏輯的,由於它「分解了」 這個struct        的名字,
而該函數以這樣的函數名對這個s t r u c t操做。
    如今,邁向 C + +第一步的時候到了。咱們知道, struct             內部的變量名不會與全局變量名衝

突。而當一些函數在特定s t r u c t上運算時,爲何不把這一優勢擴展到這些函數名上呢?也就
是,爲何不讓函數是 struct  的成員呢?

2.5  基本對象

    C + +的第一步正是這樣。函數能夠放在結構內部,做爲「成員函數」。在這裏stash 是:

    首先注意到的多是新的註釋文法/ / 。這是對 C  風格註釋的補充,C  原來的註釋文法仍然
能用。C + +註釋直到該行的結尾,它有時很是方便。另外,咱們會在這本書中,在文件的第一

行的/ /以後加一個冒號,後面跟的是這個文件名和簡要描述。這就能夠了解代碼所在的文件。
而且還能夠很容易地用本書列出的名字從電子源代碼中識別出這個文件。

----------------------- Page 31-----------------------

                                                    第2章  數據 抽 象       31
 下載

    其次,注意到這裏沒有t y p e d e f 。在C + +中,編譯器不要求咱們建立t y p e d e f,而是直接把結
構名轉變爲這個程序的新類型名(就像i n t、c h a r、f l o a t、d o u b l e同樣)。stash  的用法仍然相同。

    全部的數據成員與之前徹底相同,但如今這些函數在 s t r u c t的內部了。另外,注意到,對
應於這個庫的 C 版本中第一個參數已經去掉了。在C + +中,不是硬性傳遞這個結構的地址做爲
在這個結構上運算的全部函數的一個參數,而是編譯器背地裏作了這件事。如今,這些函數僅
有的參數與這些函數所作的事情有關,而不與這些函數運算的機制有關。
    認識到這些函數代碼與在 C 庫中的那些一樣有效,是很重要的。參數的個數是相同的(即

便咱們尚未看到這個結構地址被傳進來,實際上它在這裏),而且每一個函數只有一個函數體。
正由於如此,寫:

    stash A, B, C;

並不意味着每一個變量獲得不一樣的add( ) 函數。
    被產生的代碼幾乎和咱們已經爲               C 庫寫的同樣。有趣的是,這同時就包括了爲過程
stash_initialize( )、stash_cleanup( )等所作的「名字分解」。當函數在 struct  內時,編譯器有效地
作了相同的事情。所以,在 stash          內部的initialize( ) 將不會與任何其餘結構中的 initialize( ) 相
抵觸。大部分時間都沒必要爲函數名字分解而擔憂—即便使用未分解的函數名。但有時還必須

可以指出這個initialize( )屬於這個 struct stash 而不屬於任何其餘的s t r u c t。特別是,定義這個函
數時,須要徹底指定它是哪個。爲了完成這個指定任務, C + +有一個新的運算符: :,即範圍
分解運算符(這樣命名是由於名字如今能在不一樣的範圍:在全局範圍或在這個 struct                             的範圍)。
例如,若是但願指定initialize( )屬於 s t a s h,就寫stash::initialize(int Size, int Quantity) 。對於
stash  的C + +版本,咱們能夠看到,在函數定義中如何使用範圍分解運算符:

----------------------- Page 32-----------------------

  32      C + +編程思想
                                                                      下載

    這個文件有幾個其餘的事項要注意。首先,在頭文件中的聲明是由編譯器要求的。在 C + +
中,不能調用未事先聲明的函數,不然編譯器將報告一個出錯信息。這是確保這些函數調用在
被調用點和被定義點之間一致的重要方法。經過強迫在調用以前必須聲明, C++  編譯器能夠保
證咱們用包含這個頭文件的方式完成這個聲明。若是咱們在這個函數被定義的地方還包含有同

樣的頭文件,則編譯器將做一些檢查以保證在這個頭文件中的聲明和這個定義匹配。這意味着,
這個頭文件變成了函數聲明的有效的倉庫,而且保證這些函數在項目中的全部處理單元中使用
一致。
    固然,全局函數仍然能夠在定義和使用它的每一個地方用手工方式聲明(這是很乏味的,以
致於變得不太可能)。然而,結構的聲明必須在它們定義和使用以前,而放置結構定義的最習

慣的位置是在頭文件中,除非咱們有意把它藏在代碼文件中。
    能夠看到,除了範圍分解和來自這個庫的C版本的第一個參數再也不是顯式的這一事實之外,
全部這些成員函數實際上都是同樣的。固然,這個參數仍然存在,由於這個函數必須工做在一
個特定的struct 變量上。可是,在成員函數內部,成員選擇照常使用。這樣,不寫s->size = size,
而寫size = size 。這就去除了並不能在此增長信息的冗餘s - >。固然,C++  編譯器必須爲咱們作

這些事情。實際上,它取「祕密」的第一個參數,而且當提到類的數據成員的任什麼時候候,必須

----------------------- Page 33-----------------------

                                                    第2章  數據 抽 象       33
 下載

應用成員選擇器。這意味着,不管什麼時候,當在另外一個類的成員函數中時,咱們能夠經過簡單地
給出它的名字,說起任何成員(包括成員函數)。編譯器在找出這個名字的全局版本以前先在

局部結構的名字中搜索。這樣這個性能意味着不只代碼更容易寫,並且更容易閱讀。
    可是,若是由於某種緣由,咱們但願可以處理這個結構的地址,狀況會怎麼樣呢?在這個
庫的 C  版本中,這是很容易的,由於每一個函數的第一個參數是稱做 S  的一個 stash*                       。在 C + +
中,事情是更一致的。這裏有一個特殊的關鍵字,稱爲 t h i s ,它產生這個s t r u c t的地址。它等價
於這個庫的 C 版本的 S 。因此咱們能夠用下面語句恢復成C風格。

    this->size = Si z e ;

    對這種書寫形式進行編譯所產生的代碼是徹底同樣的。一般,不常常用 t h i s,只是須要時
才使用。在這些定義中還有最後一個變化。在 C  庫中的 inflate( ) 中,能夠將v o i d *賦給其餘任

何指針,例如

    S->storage = v;

並且編譯器可以經過。但在 C++            中,這個語句是不容許的,爲何呢?由於在 C  中,能夠給
任何指針賦一個 v o i d *  (它是malloc( ) 、calloc( )和 realloc( ) 的返回),而不需計算。C  對於類
型信息不挑剔,因此它容許這種事情。C + +不一樣,類型在C + +中是嚴格的,當類型信息有任何
違例時,編譯器就不容許。這一點一直是很重要的,而對於C + +尤爲重要,由於在s t r u c t中有成
員函數。若是咱們可以在 C++           中向s t r u c t傳遞指針而不被阻止,那麼咱們就能最終調用對於

s t r u c t邏輯上並不存在的成員函數。這是防止災難的一個實際的辦法。所以, C + +容許將任何類
型的指針賦給 v o i d * (這是void*   的最初的意圖,它要求 void*  足夠大,以存聽任何類型的指
針),但不容許將void*      指針賦給任何其餘類型的指針。 一項基本的要求是告訴讀者和編譯器,
咱們知道正在用的類型。這樣,咱們能夠看到  calloc( )  和 realloc( )             的返回值嚴格地指派爲
(unsigned char* )。

    這就帶來了一個有趣的問題,C + +的最重要的目的之一是能編譯儘量多的已存在的 C 代
碼,以便容易地向這個新語言過渡。那麼在上述例子中如何使用標準  C  庫函數?另外,全部
的C 運算符和表達式在 C++  中均可用。可是,這並不意味着 C 容許的全部代碼在 C++  中也允
許。有一些C編譯器容許的東西是危險的和易出錯的(本書中還會看到它們)。C++  編譯器對
於這些狀況產生警告和出錯信息,這樣將更有好處。實際上, C中有許多咱們知道的錯誤只是

不能找出它的緣由,可是一旦用C + +重編譯這個程序,編譯器就能指出這些問題。在 C  中,我
們經常發現能使程序經過編譯,而後咱們必須再花力氣使它工做。在 C + +中,經常是,程序編
譯正確了,它也就能工做了。這是由於該語言對類型要求更嚴格的緣故。
    在下面的測試程序中,能夠看到 stash  的C + +版本所使用的另外一些東西。

----------------------- Page 34-----------------------

  34      C + +編程思想
                                                                       下載

    這些代碼與原來的代碼至關相似,但在調用成員函數時,在函數名字以前使用成員運算符

「.」。這是一個傳統的文法,它模仿告終構數據成員的使用。所不一樣的是這裏是函數成員,有
一個參數表。
    固然,該編譯器實際產生的調用,看上去更像原來的庫函數。若是咱們考慮名字分解和
this  傳遞,C + +函數調用 intStash.initialize(sizeof(int), 100) 就和 s t a s h _ i n i t i a l i z e ( & i n t S t a s h ,
sizeof(int), 100) 同樣了。若是咱們想知道在內部所進行的工做,能夠回憶最先的 C + +編譯器

c f r o n t,它由AT & T開發,它輸出的是C代碼,而後再由C  編譯器編譯。這個方法意味着c f r o n t
能使C + +很快地轉到有 C 編譯器的機器上,有助於傳播C++ 編譯器技術。
    在下面語句中咱們還會注意到類型轉化的狀況。

    while(cp = (char*)stringStash.fetch(i++))

這是因爲在 C++  中有嚴格類型檢查而致使的結果。

2.6  什麼是對象

    咱們已經看到了一個最初的例子,如今回過頭來看一些術語。把函數放進結構是  C++                                中
的根本改變,而且這引發咱們將結構做爲新概念去思考。在  C  中,結構是數據的凝聚,它將

數據捆綁在一塊兒,使得咱們能夠將它們看做一個包。但這除了能使程序設計方便以外,別無其
他好處。這些結構上的運算能夠用在別處。然而將函數也放在這個包內,結構就變成了新的創

----------------------- Page 35-----------------------

                                                    第2章  數據 抽 象      35
 下載

造物,它既能描述屬性(就像 C中的 struct  能作的同樣),又能描述行爲,這就造成了對象的
概念。對象是一個獨立的有約束的實體,有本身的記憶和活動。

    「對象」和「面向對象的程序設計」(O O P )術語不是新的。第一個O O P語言是 S i m u l a - 6 7,
於1 9 6 7年由 S c a n d i n a v i a發明,用於輔助解決建模問題。這些問題彷佛老是包括一束相同的實
體(諸如人、細菌、小汽車),它們爲互相交互而忙碌。 S i m u l a容許對實體建立通常的描述,
描寫它的屬性和行爲,而後取總的一束。在 Simula  中,這種「通常的描述」稱爲 class                          (類)
(一個將在後面章節中看到的術語)。由類產生的大量的項稱爲對象。在 C++                           中,對象只是一

個變量,最純的定義是「存儲的一個區域」。它是能存放數據的空間,並隱含着還有在這些數
據上的運算。
    不幸的是,對於各類語言,涉及這些術語時,並不徹底一致,儘管它們是能夠接受的。我
們有時還會遇到面嚮對象語言是什麼的爭論,雖然到目前爲止這已被認爲是至關好的選擇。還
有一些語言是o b j e c t - b a s e d (基於對象的),意味着它們有像 C++  的結構加函數這樣的對象,正

如咱們已經看到的。然而,這只是到達面嚮對象語言歷程中的一部分,停留在把函數捆綁在結
構內部的語言是基於對象的,而不是面向對象的。

2.7  抽象數據類型

    將數據連同函數捆綁在一塊兒,這一點就容許建立新的類型。這經常被稱爲封裝  [ 1 ] 。一個已

存在的數據類型,例如 f l o a t,有幾個數據塊,一個指數,一個尾數和一個符號位。咱們可以告
訴它:與另外一個 float 或 int 相加,等等。它有屬性和行爲。

    stash  也是一個新數據類型,能夠add( )、fetch( )和 inflate( ) 。由說明stash S建立一個 s t a s h
就像由說明 float f 建立一個 float  同樣。一個 stash  也有屬性和行爲,甚至它的活動就像一個實
數—一個內建的數據類型。咱們稱s t a s h爲抽象數據類型(abstract dada type ),也許這是由於
它能容許咱們從問題空間把概念抽象到解空間。另外,C + +編譯器把它看做一個新的數據類型,
而且若是說一個函數須要一個 s t a s h,編譯器就確保傳遞了一個 stash  給這個函數。對抽象數據

類型(有時稱爲用戶定義類型)的類型檢查就像對內建類型的類型檢查同樣嚴格。
    然而,咱們會看到在對象上完成運算的方法有所不一樣。 o b j e c t . m e m b e r _ f u n c t i o n ( a rg l i s t )是
對一個對象「調用一個成員函數」。而在面向對象的用法中,也稱之爲「向一個對象發送消息」。
這樣,對於 stash S,語句 S . a d d ( & i )  「發送消息給S」,也就是說,「對本身add( ) 」。事實上,
面向對象程序設計能夠總結爲一句話,「向對象發送消息」。須要作的全部事情就是建立一束對

象而且給它們發送消息。固然,問題是勾畫出咱們的對象和消息是什麼,但若是完成了這些,
C++  的實現就直截了當了。

2.8  對象細節

    這時咱們大概和大多數 C 程序員同樣會感到有點困惑,由於原有的 C 是很是低層和麪向效
率的語言。在研討會上常常提出的一個問題是「對象應當多大和它應當像什麼」。回答是「最好
莫過於和咱們但願來自 C的 s t r u c t同樣」。事實上,C struct       (不帶有C++ 裝飾)在由 C 和 C + +

編譯器產生的代碼上徹底相同,這能夠使那些關心代碼的安排和大小細節的 C程序員放心了。
而且,因爲某種緣由,直接訪問結構的字節,而不是使用標識符,不必定是效率更高的辦法。

  [1] 應當知道,這個術語彷佛是有爭議的題目。一些人就像這裏的用法同樣用它,而另外一些人用它描述隱藏的實現,

     這將在第三章中討論。

----------------------- Page 36-----------------------

  36      C + +編程思想
                                                                      下載

    一個結構的大小是它的全部成員大小的和。有時,當一個 struct  被編譯器處理時,會增長
額外的字節以使得捆綁更整齊,這主要是爲了提升執行效率。在第 1 4章和第1 6章中,將會看到

如何在結構中增長「祕密」指針,可是如今沒必要關心這些。
    用s i z e o f運算能夠肯定 struct  的長度。這裏有一個小例子:

    第一個打印語句產生的結果是2 0 0,由於每一個 int             佔二個字節。struct B  是奇異的,由於它

是沒有數據成員的 s t r u c t。在 C   中,這是不合法的,但在 C++           中,以這種選擇方式建立一個
s t r u c t, 惟一的目的就是劃定函數名的範圍,因此這是容許的。儘管如此,由第二個 printf ()語
句產生的結果是一個有點奇怪的非零值。在該語言較早的版本中,這個長度是零,可是,當創
建這樣的對象時出現了笨拙的狀況:它們與緊跟着它們建立的對象有相同的地址,沒有區別。
這樣,無數據成員的結構總應當有最小的非零長度。

    最後兩個 sizeof  語句代表在 C++  中的結構長度與 C  中等價版本的長度相同。C++  盡力不
增長任何花費。

2.9  頭文件形式

    當我第一次學習用 C  編程時,頭文件對我是神祕的。許多有關 C語言的書彷佛不強調它,
而且編譯器也並不強調函數聲明,因此它在大部分時間內彷佛是可要可不要的,除非要聲明結
構時。在 C++     中,頭文件的使用變得很是清楚。它們對於每一個程序開發是強制的,在它們中

放入很是特殊的信息:聲明。頭文件告訴編譯器在咱們的庫中哪些是可用的。由於對於                                    C P P
文件可以不要源代碼而使用庫(只須要對象文件或庫文件),因此頭文件是存放接口規範的惟

----------------------- Page 37-----------------------

                                                     第2章  數據 抽 象       37
 下載

一地方。
    頭文件是庫的開發者與它的用戶之間的合同。它說:「這裏描述的是庫能作什麼。」它不說

如何作,由於如何作存放在C P P文件中,開發者不須要分發這些描述「如何作」的源代碼給用
戶。
    該合同描述數據結構,並說明函數調用的參數和返回值。用戶須要這些信息來開發應用程
序,編譯器須要它們來產生相應的代碼。
    編譯器強迫執行這一合同,也就是要求全部的結構和函數在它們使用以前被聲明,當它們

是成員函數時,在它們被定義以前被聲明。這樣,就強制把聲明放在頭文件中並把這個頭文件
包含在定義成員函數的文件和使用它們的文件中。由於描述庫的單個頭文件被包括在整個系統
中,因此編譯器能保證一致和避免錯誤。
    爲了恰當地組織代碼和寫有效的頭文件,有一些問題必須知道。第一個問題是將什麼放進
頭文件中。基本規則是「只聲明」,也就是說,對於編譯器只須要一些信息以產生代碼或建立

變量分配內存。這是由於,在一個項目中,頭文件也許會包含在幾個處理單元中,而若是內存
分配不止一個地方,則鏈接器會產生多重定義錯誤。
    這個規則不是很是嚴格的。若是在頭文件中定義「靜態文件」的一段數據(只在文件內可
視),在這個項目中將有這個數據的多個實例,編譯器不會報錯。基本上,不要在頭文件中作
在鏈接時會引發混淆的任何事情。

    關於頭文件的第二個問題是重複聲明。 C  和 C++  都容許對函數重複聲明,只要這些重複
聲明匹配,但決不容許對結構重複聲明。在 C++                   中,這個規則特別重要,由於,若是編譯器
容許對結構重複聲明並且這兩個重複聲明又不同,那麼應當使用哪個呢?
    重複聲明問題在 C++       中不多出現,由於每一個數據類型(帶有函數的結構)通常有本身的
頭文件。但咱們若是但願建立使用某個數據類型的另外一個數據類型,必須在另外一個頭文件中包

含它的頭文件。在整個項目中,極可能有幾個文件包含同一個頭文件。在編譯期間,編譯器會
幾回看到同一個頭文件。除非作適當的處理,不然編譯器將認爲是結構重複聲明。
    典型的防止方法是使用預處理器隔離這個頭文件。若是有一個頭文件名爲  F O O . H,通常
用「名字分解」產生預處理名,以防止屢次包含這個頭文件。FOO.H  的內部能夠以下:

    #ifndef FOO_H_

    #define FOO_H_

    // Rest of header here ...

    #endif // FOO_H_

    注意:不用前導下劃線,由於標準 C 用前導下劃線指明保留標識符。

在項目中使用頭文件

    用 C++  創建項目時,咱們一般要聚集大量不一樣的類型(帶有相關函數的數據結構)。通常
將每一個類型或一組相關類型放在一個單獨的頭文件中,而後在一個處理單元中定義這個類型的

函數。當使用這個類型時必須包含這個頭文件,造成適當的聲明。
    有時這個模式會在本書中使用,但若是例子很小,結構聲明、函數定義和 main( )                            函數可
以出如今同一個文件中。應當記住,在實際上使用的是隔離的文件和頭文件。

2.10  嵌套結構

    在全局名字空間以外爲數據和函數取名字是有好處的,能夠將這種方式推廣到對結構的處理

----------------------- Page 38-----------------------

  38      C + +編程思想
                                                                      下載

中。咱們能夠將一個結構嵌套在另外一箇中,這就能夠將相關聯的元素放在一塊兒。聲明文法在下面
結構中能夠看到,這個結構用很是簡單的連接表方式實現了一個棧,因此它決不會運行越界。

    這個嵌套 struct 稱爲 l i n k,它包括指向這個表中的下一個 link  的指針和指向存放在 link  中
的數據的指針,若是next 指針是零,意味着表尾。
    注意,head 指針緊接在 struct link 聲明以後定義,而不是單獨定義 link* head 。這是來自C
的文法,但它強調在結構聲明以後的分號的重要性,分號代表這個結構類型的定義表結束(通

常這個定義表是空的)。
    正如到目前爲止全部描述的結構同樣,嵌套結構有它本身的 initialize( )  函數。爲了確保正
確地初始化,stack  既有 initialize( )  又有 cleanup( )  函數。此外還有push( )  函數,它取一個指
向但願存放數據的存儲單元(假設已經分配在堆中);還有 pop( ) ,它返回棧頂的d a t a指針,並
去除棧頂元素(注意,咱們對破壞 data  指針負有責任);peek( ) 函數也從棧頂返回 data  指針,

可是它在棧中保留這個棧頂元素。
    cleanup  去除每一個棧元素,並釋放data 指針。
    下面是一些函數的定義。

----------------------- Page 39-----------------------

                                                    第2章  數據 抽 象      39
 下載

    第一個定義特別有趣,由於它代表如何定義嵌套結構的成員。簡單地兩次使用範圍分解運算
符,以指明這個嵌套struct 的名字。stack::link::initialize( ) 函數取參數並把參數賦給它的成員們。雖
然用手工作這些事情至關容易,可是,咱們未來會看到這個函數的不一樣的形式,因此它更有意義。

    stack::initialize( ) 函數置head  爲零,使得這個對象知道它有一個空表。
    stack::push( )  取參數,也就是一個指向但願用這個 stack  保存的一塊數據的指針,而且把
這個指針放在棧頂。首先,使用  malloc( )            爲 link 分配空間,l i n k將插入棧頂。而後調用
initialize( )  函數對這個 link  的成員賦適當的值。注意,next  指針賦爲當前的 h e a d ,而 head  賦
爲新link 指針。這就有效地將 link 放在這個表的頂部了。

    stack::pop( )取出當前在該棧頂部的 data  指針,而後向下移 head  指針,刪除該棧老的棧頂
元素。stack::cleanup( )建立cursor  在整個棧上移動,用free( )釋放每一個l i n k的d a t a和link 自己。
    下面是測試這個棧的例子。

----------------------- Page 40-----------------------

  40      C + +編程思想
                                                                       下載

    這個例子與前面一個很是相似,這些行存放到這個棧中,而後彈出它們,這會使這個文件
被反序打印出。另外,要打開文件的文件名是從命令行中取出的。

全局範圍分解

    編譯器經過缺省選擇的名字(「最近」的名字)可能不是咱們所但願的,範圍分解運算符
能夠避免這種狀況。例如,假設有一個結構,它的局域標識符爲  A                         ,但願在成員函數內選全

局標識符 A     。編譯器會缺省地選擇局域的那一個,因此必須另外告訴編譯器。但願用範圍分
解指定一個全局名字時,應當使用前面不帶任何東西的運算符。這裏有一個例子,代表對變量
和函數的全局範圍分解。

----------------------- Page 41-----------------------

                                                    第2章  數據 抽 象       41
 下載

    若是在 S::f( )  中沒有範圍分解,編譯器會缺省地選擇 f( ) 和A  的成員版本。

2.11  小結

    在這一章中,咱們已經學會了使用C++              的基本方法,也就是在結構的內部放入函數。這種
新類型被稱爲抽象數據類型,用這種結構建立的變量被稱爲這個類型的對象或實例。向對象調
用成員函數被稱爲向這個對象發消息。面向對象的程序設計中的主要活動就是向對象發消息。
    雖然將數據和函數捆綁在一塊兒頗有好處,並使得庫更容易使用,由於這能夠經過隱藏名字

防止名字衝突,可是,還有大量的工做能夠使C + +程序設計更安全。在下面一章中,將學習如
何保護 struct  的一些成員,以使得只有咱們能對它們操做。這就在什麼是結構的用戶能夠改動
的和什麼只是程序員能夠改動的之間造成了明確的界線。

2.12  練習

    1)  建立一個 struct 聲明,它有單個成員函數,而後對這個成員函數建立定義。建立這個新
數據類型的對象,再調用這個成員函數。

    2) 編寫而且編譯一段代碼,這段代碼完成數據成員選擇和函數調用。
    3)  寫一個在另外一個結構中的被聲明結構的例子(嵌套結構)。並說明如何定義這個結構的
成員。
    4) 結構有多大?寫一段能打印各個結構的長度的代碼。建立一些結構,它們只有數據成員,
另外一些有數據成員和函數成員。而後建立一個結構,它根本沒有成員。打印出全部這些結構的

長度。對於根本沒有成員的結構的結果做出解釋。
    5) C++對於枚舉、聯合和struct     自動建立typedef    的等價物,正如在本章中看到的。寫一個
能說明這一點的小程序。

----------------------- Page 42-----------------------

                                                                      下載

                      第3章  隱藏 實 現

    一個典型的C語言庫一般包含一個結構和一組運行於該結構之上的相關函數。前面咱們已

經看到C + +是怎樣處理那些在概念上和語法上相關聯的函數的,那就是:
    把函數的聲明放在一個s t r u c t內,改變這些函數的調用方法,在調用過程當中再也不把 s t r u c t的
地址做爲第一個參數傳遞,在程序中增長一個新的數據類型(這樣就沒必要在 s t r u c t關鍵字前加
一個t y p e d e f之類的聲明瞭)。
    這樣作帶來不少方便—有助於組織代碼,使程序易於編寫和閱讀。然而,在使得 C + +庫

比之前更容易的同時,存在一些其餘問題,特別是在安全與控制方面。本章重點討論 s t r u c t中
的邊界問題。

3.1  設置限制

    在任何關係中,存在相關各方都聽從的邊界是很重要的。當咱們創建了一個庫以後,咱們
就與該庫的用戶(也能夠叫用戶程序員)創建了一種關係,他是另外的程序員,但他須要用我
們的庫來編寫一個應用程序或用咱們的庫來創建更大的庫。

    在C語言中,s t r u c t同其餘數據結構同樣,沒有任何規則,用戶能夠在 s t r u c t 中作他們想作
的任何事情,沒有辦法來強制任何特殊的行爲。好比,即便咱們已經看到了上一章中提到的
i n i t i a l i z e ( )函數和c l e a n u p ( )函數的重要性,但用戶有權決定是否調用它們(咱們將在下一章看到
更好的方法)。再好比,咱們可能不肯意讓用戶去直接處理 s t r u c t中的某些成員,但在C語言中
沒有任何方法能夠阻止用戶。一切都是暴露無遺的。

    須要控制對結構成員的存取有兩個理由:一是讓用戶避開一些他們不須要使用的工具,這
些工具對數據類型內部的處理來講是必須的,但對用戶特定問題的接口來講卻不是必須的。這
其實是爲用戶提供了方便,由於他們能夠很容易地知道,對他們來講哪些是重要的,哪些是
能夠忽略的。
    二是設計者能夠改變 s t r u c t 的內部實現,而沒必要擔憂對用戶程序員產生影響。在上一章

s t a c k的例子中,咱們想以大塊的方式來分配存儲空間,提升速度,而不是在每次增長成員時調
用malloc() 函數來從新分配內存。若是這些庫的接口部分與實現部分是清楚地分開的,並做了
保護,那麼咱們只須要讓用戶從新鏈接一遍就能夠了。

3.2  C++的存取控制

    C + +語言引進了三個新的關鍵字,用於在 s t r u c t中設置邊界:p u b l i c 、p r i v a t e和p r o t e c t e d 。
它們的使用和含義從字面上就能理解。這些存取指定符只在 s t r u c t聲明中使用,它們能夠改變
在它們以後的全部聲明的邊界。使用存取指定符,後面必須跟上一個冒號。
    p u b l i c意味着在其後聲明的全部成員對全部的人均可以存取。  p u b l i c成員就如同通常的
s t r u c t成員。好比,下面的s t r u c t聲明是相同的:

----------------------- Page 43-----------------------

                                                    第3章  隱藏 實現           43
 下載

    p r i v a t e關鍵字則意味着,除了該類型的建立者和類的內部成員函數以外,任何人都不能存
取這些成員。p r i v a t e在設計者與用戶之間築起了一道牆。若是有人試圖存取一個私有成員,就
會產生一個編譯錯誤。在上面的例子中,咱們可能想讓 struct B 中的部分數據成員隱藏起來,

只有咱們本身能存取它們:

----------------------- Page 44-----------------------

  44      C + +編程思想
                                                                       下載

    雖然f o o ( )函數能夠訪問B 的全部成員,但通常的全局函數如m a i n ( )卻不能,固然其餘s t r u c t
中的成員函數一樣也不能。只有那些在這個s t r u c t中明確聲明瞭的函數才能訪問這些私有成員。

    對存取指定符的順序沒有特別的要求,它們能夠不止一次出現,它們影響在它們以後和下
一個存取指定符以前聲明的全部成員。

保護(protected)

    最後一種存取指定符是p r o t e c t e d 。p r o t e c t e d與p r i v a t e基本類似,只有一點不一樣:繼承的結
構能夠訪問p r o t e c t e d成員,但不能訪問p r i v a t e成員。但咱們要到第1 3章才討論繼承。如今就把
這兩種指定符當成同樣來看待,直到介紹了繼承後再區分這兩類指定符。

3.3  友元

    若是程序員想容許不屬於當前結構的一個成員函數存取結構中的數據,那該怎麼辦呢?他
能夠在s t r u c t內部聲明這個函數爲友元。注意,一個友元必須在一個 s t r u c t 內聲明,這一點很重
要,由於他(和編譯器)必須能讀取這個結構的聲明以理解這個數據類型的大小、行爲等方面
的規則。有一條規則在任何關係中都很重要,那就是「誰能夠訪問個人私有實現部分」。
    類控制着哪些代碼能夠存取它的成員。讓程序員沒有辦法「破門而入」,他不能聲明一個

新類而後說「嘿,我是鮑勃的朋友」,不能期望這樣就能夠訪問鮑勃的私有成員和保護成員。
    程序員能夠把一個全局函數聲明爲友元類,也能夠把另外一個 s t r u c t中的成員函數甚至整個
s t r u c t都聲明爲友元類,請看下面的例子:

----------------------- Page 45-----------------------

                                                    第3章  隱藏 實現        45
 下載

    struct Y有一個成員函數f( ),它將修改X類型的對象。這裏有一個難題,由於C + +的編譯器
要求在引用任一變量以前必須聲明,因此struct Y必須在它的成員Y :: f(X*)被聲明爲struct X的
一個友元以前聲明 ,但Y :: f(X*)要被聲明,struct X又必須先聲明。

    解決的辦法是:注意到Y :: f(X*) 引用了一個X對象的地址。這一點很關鍵,由於編譯器知
道如何傳遞一個地址,這一地址大小是必定的,而無論被傳遞的對象類型大小。若是試圖傳遞
整個對象,編譯器就必須知道X 的所有定義以肯定它的大小以及如何傳遞它,這就使程序員無
法聲明一個相似於Y :: g(X) 的函數。
    經過傳遞X的地址,編譯器容許程序員在聲明Y :: f(X*)以前作一個不徹底的類型指定。這

一點是在struct X 的聲明時完成的,這兒僅僅是告訴編譯器,有一個叫X 的s t r u c t ,因此當它被引
用時不會產生錯誤,只要程序員的引用不涉及名字之外的其餘信息。
    這樣,在struct X 中,Y :: f(X*) 就能夠成功地聲明爲一個友元函數,若是程序員在編譯器
得到對Y 的所有指定信息以前聲明它,就會產生一條錯誤,這種安全措施保證了數據的一致性,
同時減小了錯誤的發生。

    再來看看其餘兩個友元函數,第一個聲明將一個全局函數g( )做爲一個友元,但g( )在這之
前並無在全局範圍內做過聲明,這代表f r i e n d能夠在聲明函數的同時又將它做爲s t r u c t的友元。
這種聲明對整個s t r u c t一樣有效:friend struct Z是一個不徹底的類型說明,並把整個s t r u c t都看成
一個友元。

3.3.1 嵌套友元

    一個嵌套的s t r u c t並不能自動地得到存取私有成員的權限。要得到存取私有成員的權限,

必須遵照特定的規則:首先聲明一個嵌套的 s t r u c t,而後聲明它是全局範圍使用的一個友元。

----------------------- Page 46-----------------------

     46   C + +編程思想
                                                                        下載

s t r u c t的聲明必須與f r i e n d聲明分開,不然編譯器將不把它看做成員。請看下面的例子:

----------------------- Page 47-----------------------

                                                     第3章  隱藏 實現        47
 下載

    struct holder包含一個整型數組和一個p o i n t e r,咱們能夠經過p o i n t e r來存取這些整數。由於

p o i n t e r與h o l d e r緊密相連,因此有必要將它做爲s t r u c t 中的一個成員。一旦p o i n t e r被定義,它就
能夠經過下面的聲明來得到存取h o l d e r的私有成員的權限:

    friend holder : :pointer ;

    注意,這裏s t r u c t關鍵字並非必須的,由於編譯器已經知道p o i n t e r是什麼了。
    由於p o i n t e r是同h o l d e r分開的,因此程序員能夠在main( )塊中定義它的多個實例,而後用
它們來選擇數組的不一樣部分。因爲p o i n t e r是C語言中指針的替代,所以程序員能夠保證它老是
安全地指向h o l d e r 的內部。

----------------------- Page 48-----------------------

  48      C + +編程思想
                                                                       下載

3.3.2 它是純的嗎

    這種類的定義提供了有關權限的信息,咱們能夠知道哪些函數能夠改變類的私有部分。如

果一個函數被聲明爲f r i e n d,就意味着它不是這個類的成員函數,但卻能夠修改類的私有成員,
並且它必須被列在類的定義中,所以咱們能夠認爲它是一個特權函數。
    C + +不是徹底的面嚮對象語言,它只是一個混合產品。 f r i e n d關鍵字就是用來解決部分的
突發問題。它也說明了這種語言是不純的。畢竟 C + +語言的設計是爲了實用,而不是追求理想
的抽象。

3.4  對象佈局

    第2章講述了爲C編譯器而寫的一個s t r u c t,而後一字不動地用C + +編譯器進行編譯。這裏咱們
就來分析s t r u c t的佈局,也就是,各自的變量放在內存的什麼位置?若是C + +編譯器改變了C struct
中的佈局,在C語言代碼中若是使用了s t r u c t中變量的位置信息的話,那麼在C + +中就會出錯。
    當咱們開始使用一個存取指定符時,咱們就已經徹底進入了 C + +的世界,狀況開始有所改
變。在一個特定的「存取塊」(被存取指定符限定的一組聲明)內,這些變量在內存中確定是

相鄰的,這和C語言中同樣,然而這些「存取塊」自己能夠不按定義的順序在對象中出現。
    雖然編譯器一般都是按存取塊出現的順序給它們分配內存,但並非必定要這樣,由於部
分機器的結構或操做環境可對私有成員和保護成員提供明確的支持,將其放在特定的內存位置
上。C + +語言的存取指定並不想限制這種好處。
    存取指定符是s t r u c t的一部分,它並不影響從這個s t r u c t產生的對象,程序開始運行時,所

有的存取指定信息都消失了。存取指定信息一般是在編譯期間消失的。在程序運行期間,對象
變成了一個存儲區域,別無他物,所以,若是有人真的想破壞這些規則而且直接存取內存中的
數據,就如在C中所作的那樣,那麼C + +並不能防止他作這種不明智的事,它只是提供給人們
一個更容易、更方便的方法。
    通常說來,程序員寫程序時,依賴特定實現的任何東西都是不合適的。如確有必要,這些

指定應封裝在一個s t r u c t以內,這樣當環境改變時,他只需修改一個地方就好了。

3.5  類

    存取控制一般是指實現細節的隱藏。將函數包含到一個 s t r u c t 內(封裝)來產生一種帶數
據和操做的數據類型,但由存取控制在該數據類型以內肯定邊界。這樣作的緣由有兩個:首先
是決定哪些用戶能夠用,哪些用戶不能用。咱們能夠創建內部的數據結構,而用戶只能用接口
部分的數據,咱們沒必要擔憂用戶會把內部的數據看成接口數據來存取。

    這就直接導出第二個緣由,那就是將具體實現與接口分離開來。若是該結構被用在一系列
的程序中,而用戶只是對公共的接口發送消息,這樣程序員就能夠改變全部聲明爲 p r i v a t e 的成
員而沒必要去修改用戶的代碼。
    封裝和實現細節的隱藏能防止一些狀況的發生,而這在 C語言的s t r u c t類型中是作不到的。
咱們如今已經處在面向對象編程的世界中,在這裏,結構就是一個對象的類,就像人們能夠描

述一個魚類或一個鳥類,任何屬於該類的對象都共享這些特徵和行爲。也就是說,結構的聲明
開始描述該類型的全部對象及其行爲。
    在最初的面向對象編程語言 S i m u l a - 6 7中,關鍵字c l a s s被用來描述一個新的數據類型。這
顯然激發了S t r o u s t r u p在C + +中選用一樣的關鍵字,以強調這是整個語言的關鍵所在。新的數據

----------------------- Page 49-----------------------

                                                    第3章  隱藏 實現        49
 下載

類型並不是只是C中的帶有函數的s t r u c t,這固然須要用一個新的關鍵字。
    然而c l a s s在C + +中的使用逐漸變成了一個非必要的關鍵字。它和s t r u c t的每一個方面都是同樣

的,除了c l a s s中的成員缺省爲私有的,而s t r u c t中的成員缺省爲p u b l i c 。下面有兩個結構,它們
將產生相同的結果。

    在C + +中,c l a s s是面嚮對象語言的基本概念,它是一個關鍵字,本書將不用粗體字來表示。
因爲要常常用到c l a s s ,這樣作很麻煩。但轉換到類是如此重要,我懷疑 S t r o u s t r u p偏向於將
s t r u c t從新定義,但考慮到向後兼容性而沒有這樣作。

    許多人喜歡用一種更像s t r u c t的風格去建立一個類,由於能夠經過以p u b l i c開頭來重載  「缺
省爲私有」的類行爲。

----------------------- Page 50-----------------------

  50      C + +編程思想
                                                                       下載

    之因此這樣作,是由於這樣能夠讓讀者更清楚地看到他們的成員是與什麼限定符相連的,
這樣他們能夠忽略全部聲明爲私有成員。事實上,全部其餘成員都必須在類中聲明的緣由僅僅

是讓編譯器知道對象有多大,以便爲它們分配合適的存儲空間,並保證它們的一致性。
    但本書中仍採用首先聲明私有成員的方法,以下例:

    有些人甚至不厭其煩地在他們的私有成員名字前加上私有標誌:

    由於m X 已經隱藏於Y 的範圍內,因此在x以前加m並非必須的。然而在一個有許多全局
變量的項目中(有些事雖然咱們想極力避免,但有時仍不可避免地出現),它有助於在一個成
員函數的定義體內識別出哪些是全局變量,哪些是成員變量。

3.5.1 用存取控制來修改stash

    如今咱們把第2章的例子用類及存取控制來改寫一下。請注意用戶的接口部分如今已經很
清楚地區分開了,徹底不用擔憂用戶會偶然地訪問他們不應訪問的內容了。

----------------------- Page 51-----------------------

                                                     第3章  隱藏 實現        51
 下載

    i n f l a t e ( )函數聲明爲私有,由於它只被a d d ( )函數調用,因此它屬於內在實現部分,不屬於
接口部分。這就意味着之後咱們能夠調整這些實現的細節,用不一樣的系統來管理內存。在此例

中除了包含文件的名字以外,只有上面的頭文件須要更改,實現文件和測試文件是相同的。

3.5.2 用存取控制來修改stack

    對於第二個例子,咱們把s t a c k改寫成一個類。如今嵌套的數據結構是私有的。這樣作的好
處是能夠確保用戶既看不到它,也不能依賴s t a c k的內部表示:

    和上例同樣,實現部分無需改動,這裏再也不贅述。測試部分也同樣,惟一改動的地方是類
的接口部分的健壯性。存取控制的真正價值體如今開發階段,防止越界。事實上,只有編譯器
知道類成員的保護級別,並無將此類的信息傳遞給鏈接器。全部的存取保護檢查都是由編譯
器來完成的,在運行期間再也不檢查。
    注意面向用戶的接口部分如今是一個壓入堆棧。它是用一個鏈表結構來實現的,但能夠換

成其餘的形式,而不會影響用戶處理問題,更重要的是,無需改動用戶的代碼。

3.6  句柄類(handle classes )

    C + +中的存取控制容許將實現與接口部分分開,但實現的隱藏是不徹底的。編譯器必須知
道一個對象的全部部分的聲明,以便建立和管理它。咱們能夠想象一種只需聲明一個對象的公

共接口部分的編程語言,而將私有的實現部分隱藏起來。但 C + +在編譯期間要儘量多地作靜
態類型檢查。這意味着儘早捕獲錯誤,也意味着程序具備更高的效率。然而這對私有的實現部
分來講帶來兩個影響:一是即便程序員不能輕易地訪問實現部分,但他能夠看到它;二是形成
一些沒必要要的重複編譯。

3.6.1 可見的實現部分

    有些項目不可以讓最終用戶看到其實現部分。例如可能在一個庫的頭文件中顯示一些策略

----------------------- Page 52-----------------------

  52      C + +編程思想
                                                                       下載

信息,但公司不想讓這些信息被競爭對手得到。好比從事一個安全性很重要的系統 (如加密算
法),咱們不想在文件中暴露任何線索,以防有人破譯咱們的代碼。或許咱們把庫放在了一個

「有敵意」的環境中,在那裏程序員會不顧一切地用指針和類型轉換存取咱們的私有成員。在
全部這些狀況下,就有必要把一個編譯好的實際結構放在實現文件中,而不是讓其暴露在頭
文件中。

3.6.2 減小重複編譯

    在咱們的編程環境中,當一個文件被修改,或它所依賴的文件包含的頭文件被修改時,項
 目負責人須要重複編譯這些文件。這意味着不管什麼時候程序員修改了一個類,不管是修改公共的
接口部分,仍是私有的實現部分,他都得再次編譯包含頭文件的全部文件。對於一個大的項目
而言,在開發初期這可能很是難以處理,由於實現部分可能須要常常改動;若是這個項目很是
大,用於編譯的時間過多就可能妨礙項目的完成。

                                                                 [ 1 ]
    解決這個問題的技術有時叫句柄類(handle classes )或叫「Cheshire Cat 」 。有關實現的
任何東西都消失了,只剩一個單一的指針「 s m i l e」。該指針指向一個結構,該結構的定義與其
全部的成員函數的定義同樣出如今實現文件中。這樣,只要接口部分不改變,頭文件就不需變
動。而實現部分能夠按須要任意更動,完成後只要對實現文件進行從新編譯,而後再鏈接到項
 目中。
    這裏有個說明這一技術的簡單例子。頭文件中只包含公共的接口和一個簡單的沒有徹底指
定的類指針。

    這是全部客戶程序員都能看到的。這行

    struct cheshire;

是一個沒有徹底指定的類型說明或類聲明(一個類的定義包含類的主體)。它告訴編譯器,

cheshire  是一個結構的名字,但沒有提供有關該結構的任何東西。這對產生一個指向結構的指
針來講已經足夠了。但咱們在提供一個結構的主體部分以前不能建立一個對象。在這種技術裏,
包含具體實現的結構主體被隱藏在實現文件中。

  [1] 這個名字是歸屬於John Carolan和Lewis Carroll ,前者是C + +最先的開創者之一。

----------------------- Page 53-----------------------

                                                     第3章  隱藏 實現        53
 下載

    cheshire 是一個嵌套結構,因此它必須用範圍分解符定義

      struct handle::cheshire {

在h a n d l e : : i n i t i a l i z e ( ) 中,爲cheshire struct分配存儲空間 [ 1 ] ,在h a n d l e : : c l e a n u p ( )中這些空間被釋

放。這些內存被用來代替類的全部私有部分。當編譯 H A N D L E . C P P時,這個結構的定義被隱

藏在目標文件中,沒有人能看到它。若是改變了                      c h e s h i r e 的組成,惟一要從新編譯的是
H A N D L E . C P P,由於頭文件並無改動。
    句柄(h a n d l e )的使用就像任何類的使用同樣,包含頭文件、建立對象、發送信息。

  [1] 在第1 2章咱們將看到建立對象更好的方法:用n e w在堆中分配內存。

----------------------- Page 54-----------------------

  54      C + +編程思想
                                                                       下載

    客戶程序員惟一能存取的就是公共的接口部分,所以,只是修改了在實現中的部分,這些
文件就不須從新編譯。雖然這並非完美的信息隱藏,但畢竟是一大進步。

3.7  小結

    在C + +中,存取控制並非面向對象的特徵,但它爲類的建立者提供了頗有價值的訪問控
制。類的用戶能夠清楚地看到,什麼能夠用,什麼應該忽略。更重要的是,它保證了類的用戶
不會依賴任何類的實現細節。有了這些,咱們就能更改類的實現部分,沒有人會所以而受到影

響,由於他們並不能訪問類的這一部分。
    一旦咱們有了更改實現部分的自由,就能夠在之後的時間裏改進咱們的設計,並且容許犯
錯誤。要知道,不管咱們如何當心地計劃和設計,均可能犯錯誤。知道犯些錯誤也是相對安全
的,這意味着咱們會變得更有經驗,會學得更快,就會更早完成項目。
    一個類的公共接口部分是用戶能看到的。因此在分析設計階段,保證接口的正確性更加劇

要。但這並非說接口不能做修改。若是咱們第一次沒有正確地設計接口部分,咱們能夠再增
加函數,這樣就不須要刪除那些已使用該類的程序代碼。

3.8  練習

    1) 建立一個類,具備p u b l i c、private  和p r o t e c t e d數據成員和函數成員。建立該類的一個對
象,看看當試圖存取全部的類成員時會獲得一些什麼編譯信息。
    2) 建立一個類和一個全局f r i e n d函數來處理類的私有數據。

    3) 修改 H A N D L E . C P P 中的c h e s h i r e ,從新編譯和鏈接這一文件,但不從新編譯
U S E H A N D L . C P P 。

----------------------- Page 55-----------------------

 下載

                    第4章  初始化與清除

    第2章利用了一些分散的典型C語言庫的構件,並把它們封裝在一個 s t r u c t中,從而在庫的

應用方面作了有意義的改進。(從如今起,這個抽象數據類型稱爲類)。
    這樣不只爲庫構件提供了單一一致的入口指針,也用類名隱藏了類內部的函數名。在第 3
章中,咱們介紹了存取控制(隱藏實現),這就爲類的設計者提供了一種設立界線的途徑,通
過界線的設立來決定哪些是用戶能夠處理的,哪些是禁止的。這意味着數據類型的內部機制
對設計者來講是可控的和能自行處理的。這樣讓用戶也清楚哪些成員是他們可以使用並加以

注意的。
    封裝和實現的隱藏大大地改善了庫的使用。它們提供的新的數據類型的概念在某些方面比
從C 中繼承的嵌入式數據類型要好。如今 C + +編譯器能夠爲這種新的數據類型提供類型檢查,
這樣在使用這種數據類型時就確保了必定的安全性。
    固然,說到安全性,C + +的編譯器能比C編譯器提供更多的功能。在本章及之後的章節中,

咱們將看到許多C + +的另一些性能。它們能夠讓咱們程序中的錯誤暴露無遺,有時甚至在我
們編譯這個程序以前,幫咱們查出錯誤,但一般是編譯器的警告和出錯信息。因此咱們不久就
會習慣:在第一次編譯時總聽不到編譯器那意味着正確的提示音。
    安全性包括初始化和清除兩個方面。在C語言中,若是程序員忘記了初始化或清除一個變
量,就會致使一大段程序錯誤。這在一個庫中尤爲如此,特別是當用戶不知如何對一個 s t r u c t

初始化,甚至不知道必需要初始化時。(庫中一般不包含初始化函數,因此用戶不得不手工初
始化s t r u c t)。清除是一個特殊問題,由於C程序員一旦用過了一個變量後就把它忘記了,因此
對一個庫的s t r u c t來講,必要的清除工做每每被遺忘了。
    在C + +中,初始化和清除的概念是簡化類庫使用的關鍵所在,並能夠減小那些因爲用戶忘
記這些操做而引發的許多細微錯誤。本章就來討論C + + 的這些特徵。

4.1  用構造函數確保初始化

    在s t a s h和s t a c k類中都曾調用i n i t i a l i z e ( )函數,這暗示不管用什麼方法使用這些類的對象,

在使用以前都應當調用這一函數。很不幸的是,這要求用戶必須正確地初始化。而用戶在專一

於用那使人驚奇的庫來解決他們的問題的時候,每每忽視了這些細節。在 C + + 中,初始化實在
過重要了,因此不能留給用戶來完成。類的設計者能夠經過提供一個叫作構造函數的特殊函數

來保證每一個對象都正確的初始化。若是一個類有構造函數,編譯器在建立對象時就自動調用這
一函數,這一切在用戶使用他們的對象以前就已經完成了。對用戶來講,是否調用構造函數並

不是可選的,它是由編譯器在對象定義時完成的。

    接下來的問題是這個函數叫什麼名字。這必須考慮兩點,首先這個名字不能與類的其餘成
員函數衝突,其次,由於該函數是由編譯器調用的,因此編譯器必須總能知道調用哪一個函數。

S t r o u s t r u p的方法彷佛是最容易也是最符合邏輯的:構造函數的名字與類的名字同樣。這使得
這樣的函數在初始化時自動被調用。

    下面是一個帶構造函數的類的簡單例子:

----------------------- Page 56-----------------------

  56      C + +編程思想
                                                                      下載

    如今當一個對象被定義時:

    這時就好像a是一個整數同樣:爲這個對象分配內存。可是當程序執行到 a的定義點時,構
造函數自動被調用,由於編譯器已悄悄地在 a 的定義點處插入了一個X : : X ( ) 的調用。就像其餘
成員函數被調用同樣。傳遞到構造函數的第一個參數(隱含)是調用這一函數對象的地址。
    像其餘函數同樣,咱們也能夠經過構造函數傳遞參數,指定對象該如何建立,設定對象初
始值等等。構造函數的參數保證對象的全部部分都被初始化成合適的值。舉例來講:若是類
t r e e有一個帶整型參數的構造函數,用以指定樹的高度,那麼咱們就必須這樣來建立一個對象:

    tree t(12); // 12英尺高的樹

    若是t r e e ( i n t )是惟一的構造函數,編譯器將不會用其餘方法來建立一個對象(在下一章我
們將看到多個構造函數以及調用它們的不一樣方法)。
    關於構造函數,咱們就所有介紹完了。構造函數是一個有着特殊名字,由編譯器自動爲每
個對象調用的函數,然而它解決了類的不少問題,並使得代碼更容易閱讀。例如在上一個代碼
段中,對有些i n i t i a l i z e ( )函數咱們並無看到顯式的調用,這些函數從概念上說是與定義分開
的。在C + +中,定義和初始化是同一律念,不能只取其中之一。
    構造函數和析構函數是兩個很是特殊的函數:它們沒有返回值。這與返回值爲 v o i d 的函數
顯然不一樣。後者雖然也不返回任何值,但咱們還能夠讓它作點別的。而構造函數和析構函數則
不容許。在程序中建立和消除一個對象的行爲很是特殊,就像出生和死亡,並且老是由編譯器
來調用這些函數以確保它們被執行。若是它們有返回值,要麼編譯器必須知道如何處理返回值,
要麼就只能由用戶本身來顯式地調用構造函數與析構函數,這樣一來,安全性就被破壞了。

4.2  用析構函數確保清除

    做爲一個C程序員,咱們可能常常想到初始化的重要性,但不多想到清除的重要性。畢竟,
清除一個整型變量時須要做什麼 ?只須要忘記它。然而,在一個庫中,對於一個曾經用過的對
象,僅僅「忘記它」是不安全的。若是它修改了某些硬件參數,或者在屏幕上顯示了一些字符,
或在堆中分配了一些內存,那麼將會發生什麼呢 ?  若是咱們只是「忘記它」,咱們的對象就永
遠不會消失。在C + +中,清除就像初始化同樣重要。經過析構函數來保證清除的執行。
    析構函數的語法與構造函數同樣,用類的名字做函數名。然而析構函數前面加上一個  ~,
以和構造函數區別。另外,析構函數不帶任何參數,由於析構不需任何選項。下面是一個析構
函數的聲明:

    class Y {

    p u b l i c :
     ~ Y ( ) ;

    } ;

----------------------- Page 57-----------------------

                                                  第4章 初始化與清除          57
 下載

    當對象超出它的定義範圍時,編譯器自動調用析構函數。咱們能夠看到,在對象的定義點
處構造函數被調用,但析構函數調用的惟一根據是包含該對象的右括號,即便用 g o t o語句跳出

這一程序塊(爲了與C 語言向後兼容,g o t o在C + +中仍然存在,固然也是爲了方便)。咱們應該
注意一些非本地的g o t o語句,它們用標準C語言庫中的setjmp()  和l o n g j m p ( )函數,這些函數將
不會引起析構函數的調用。(這裏做一點說明:有的編譯器可能並不用這種方法來實現。依賴
那些不在說明書中的特徵意味着這樣的代碼是不可移植的)。
    下例說明了構造函數與析構函數的上述特徵:

----------------------- Page 58-----------------------

  58      C + +編程思想
                                                                       下載

    下面是上面程序的輸出結果:

    咱們能夠看到析構函數在包括它的右括號處被調用。

4.3  清除定義塊

    在C中,咱們總要在一個程序塊的左括號一開始就定義好全部的變量,這在程序設計語言

中不算少見(P a s c a l 中例外),其理由無非是由於「這是一個好的編程風格」。在這點上,我有
本身的見解。我認爲它老是給我帶來不便。做爲一個程序員,每當我須要增長一個變量時我都
得跳到塊的開始,我發現若是變量定義緊靠着變量的使用處時,程序的可讀性更強。
    也許這些爭論具備必定的廣泛性。在C + +中,是否必定要在塊的開頭就定義全部變量成了
一個很突出的問題。若是存在構造函數,那麼當對象產生時它必須首先被調用,若是構造函數

帶有一個或者更多個初始化參數,咱們怎麼知道在塊的開頭定義這些初始化信息呢?在通常的
編程狀況下,咱們作不到這點,由於C中沒有私有成員的概念。這樣很容易將定義與初始化部
分分開,然而C + +要保證在一個對象產生時,它同時被初始化。這能夠保證咱們的系統中沒有
未初始化的對象。C並不關心這些。事實上,C要求咱們在塊的開頭就定義全部變量,在咱們
還不知道一些必要的初始化信息時,就要求咱們這樣作是鼓勵咱們不初始化變量。

    一般,在C + +中,在還不擁有構造函數的初始化信息時不能建立一個對象,因此沒必要在塊
的開頭定義全部變量。事實上,這種語言風格彷佛鼓勵咱們把對象的定義放得離使用點儘量
近一點。在C + + 中,對一個對象適用的全部規則,對預約義類型也一樣適用。這意味着任何類
的對象或者預約義類型均可以在塊的任何地點定義。這也意味着咱們能夠等到咱們已經知道一
個變量的必要信息時再去定義它,因此咱們老是能夠同時定義和初始化一個變量。

----------------------- Page 59-----------------------

                                                  第4章 初始化與清除          59
 下載

    咱們能夠看到首先是b u f被定義,而後是一些語句,而後x被定義並用一個函數調用對它初
始化,而後y和g被定義。在C中這些變量都只能在塊的一開始定義。通常說來,應該在儘量
靠近變量的使用點定義變量,並在定義時就初始化(這是對預約義類型的一種建議,但在那裏
能夠不作初始化)。這是出於安全性的考慮,減小變量誤用的可能性。另外,程序的可讀性也

加強了,由於讀者不須要跳到程序頭去肯定變量的類型。

4.3.1 for循環

    在C + +中,咱們將常常看到f o r循環的計數器直接在f o r表達式中定義:

    上述聲明是一種重要的特殊狀況,這可能使那些剛接觸C + +的程序員感到疑惑不解。
    變量i和j 都是在f o r表達式中直接定義的(在C中咱們不能這樣作),而後他們就做爲一個變
量在f o r循環中使用。這給程序員帶來很大的方便,由於從上下文中咱們能夠清楚地知道變量 i、

j 的做用,因此沒必要再用諸如i _ l o o p _ c o u n t e r之類的名字來定義一個變量,以表示這一變量的做
用。
    這裏有一個變量生存期的問題,在之前這是由程序塊的右大括號來肯定的。從編譯器的角
度來看這樣是合理的,由於做爲程序員,咱們顯然想讓 i只在循環內部有效。然而很不幸的是,
若是咱們用這種方法聲明:

    (不管有沒有大括號,)在同一程序塊內,編譯器將給出一個重複定義的錯誤,而新的標準
C + +說明書上說,一個在f o r循環的控制表達式中定義的循環計數器只在該循環內纔有效,因此

上面的聲明是可行的。(固然,並非全部的編譯器都支持這一點,咱們可能會遇到一些老式
風格的編譯器。)若是這種轉變引發一些錯誤的話,編譯器會指出,解決起來也很容易。注意,

----------------------- Page 60-----------------------

  60  C + +編程思想
                                                                       下載

那些局部變量會屏蔽這個封閉範圍中的變量。
    我發現一個在小範圍內設計良好的指示器:若是咱們的一個函數有好幾頁,也許咱們正在

試圖讓這個函數完成太多的工做。用更多的細化的函數不只有用,並且更容易發現錯誤。

4.3.2 空間分配

    如今,一個變量能夠在某個程序範圍內的任何地方定義,因此在這個變量的定義以前是無
法對它分配內存空間的。一般,編譯器更可能像 C編譯器同樣,在一個程序塊的開頭就分配所
有的內存。這些對咱們來講是可有可無的,由於做爲一個程序員,咱們在變量定義以前老是無
法獲得存儲空間。即便存儲空間在塊的一開始就被分配,構造函數也仍然要到對象的定義時才

會被調用,由於標識符只有到此時纔有效。編譯器甚至會檢查咱們有沒有把一個對象的定義放
到一個條件塊中,好比在s w i t c h塊中聲明,或可能被g o t o跳過的地方。
    下例中解除註釋的句子會致使一個警告或一個錯誤。

    在上面的代碼中,g o t o和s w i t c h均可能跳過構造函數的調用點,然而這個對象會在後面的
程序塊中起做用,這樣,構造函數就沒有被調用,因此編譯器給出了一條出錯信息。這就確保

了對象在產生的同時被初始化。

    固然,這裏討論的內存分配都是在一個堆棧中。內存分配是經過編譯器向下移動堆棧指針
來實現的(這只是相對而言,實際指針值可能增長,也可能減小,這依賴於機器)。也能夠在

堆中分配對象的內存,這將在第1 2章中介紹。

----------------------- Page 61-----------------------

                                                   第4章 初始化與清除             61
 下載

4.4  含有構造函數和析構函數的stash

    在前幾章的例子中,都有一些很明顯的函數對應爲構造函數和析構函數:  i n i t i a l i z e ( )和
c l e a n u p ( ) 。下面是帶有構造函數與析構函數的s t a s h頭文件。

    下面是實現文件,這裏只對i n i t i a l i z e ( )和c l e a n u p ( )的定義做了修改,它們分別用構造函數與
析構函數代替。

----------------------- Page 62-----------------------

  62  C + +編程思想
                                                                       下載

    注意,在下面的測試程序中,s t a s h對象的定義放在緊靠對象調用的地方,對象的初始化通
過構造函數的參數列表來實現,而對象的初始化彷佛成了對象定義的一部分。

----------------------- Page 63-----------------------

                                                   第4章 初始化與清除          63
 下載

    再看看c l e a n u p ( )調用已被取消,但當i n t S t a s h和s t r i n g S t a s h越出程序塊的範圍時,析構函數
被自動地調用了。

4.5  含有構造函數和析構函數的stack

    從新實現含有構造函數和析構函數的鏈表(在s t a c k內)。這是修改後的頭文件:

    注意,雖然s t a c k有構造函數與析構函數,但嵌套類l i n k並無,這並非說它不須要。當
它被使用時,問題就來了:

----------------------- Page 64-----------------------

  64  C + +編程思想
                                                                      下載

    l i n k是在s t a c k : : p u s h內部產生的,但它是建立在堆棧上的,這兒就產生了一個疑難問題。

    若是一個對象有構造函數,咱們怎麼建立它呢?到目前爲止,咱們一直這樣說:「好吧,
這是堆中的一塊內存,我想您就假定它是給這個對象的吧。」但構造函數並不容許咱們就這樣
把一個內存地址交給它來建立一個對象 [ 1 ] 。對象的建立很關鍵,C + + 的構造函數想控制整個過

  [1] 實際上,確有容許這麼作的語法。但它是在特殊狀況下使用的,不能解決在此描述的通常問題。

----------------------- Page 65-----------------------

                                                  第4章 初始化與清除          65
 下載

程以保證安全。有個很容易的解決辦法,那就是用n e w操做符,咱們將在第1 2章討論這個問題。
如今,只要用C 中的動態內存分配就好了。由於內存分配與清除都隱藏在s t a c k中,它是實現部

分,咱們在測試程序中看不到它的影響。

    t e x t l i n e s 的構造函數與析構函數都是自動調用的,因此類的用戶只要把精力集中於怎樣使
用這些對象上,而不須要擔憂它們是否已被正確地初始化和清除了。

4.6  集合初始化

    集合,顧名思義,就是多個事物彙集在一塊兒。這個定義包括各類類型的集合:像 s t r u c t和
c l a s s等。數組就是單一類型的集合。
    初始化集合每每既冗長又容易出錯。而C + +中集合的初始化卻變得很方便並且很安全。當
咱們產生一個集合對像時,咱們要作的只是指定初始值就好了,而後初始化工做就由編譯器去

承擔了。這種指定能夠用幾種不一樣的風格,取決於咱們正在處理的集合類型。但無論是哪一種情
況,指定的初值都要用大括號括起來。好比一個預約義類型的數組能夠這樣定義:

    int a[5]={1,2,3,4,5};

    若是給出的初始化值多於數組元素的個數,編譯器就會給出一條出錯信息。但若是給的初

----------------------- Page 66-----------------------

   66  C + +編程思想
                                                                                              下載

始化值少於數組元素的個數,那將會怎麼樣呢?例如:

     int b[6]={0};

這時,編譯器會把第一個初始化值賦給數組的第一個元素,而後用 0賦給其他的元素。注意,
若是咱們定義了一個數組而沒有給出一列初始值時,編譯器並不會去作這些工做。因此上面的
表達式是將一個數組初始化爲零的簡潔方法,它不須要用一個 f o r循環,也避免了「偏移 1位」

錯誤(它可能比f o r循環更有效,這依賴於編譯器)。
     數組還有一種叫自動計數的快速初始化方法,就是讓編譯器按初始化值的個數去決定數組
的大小:

     int c[] = {1,2,3,4};

如今,若是咱們決定增長其餘的元素到這個數組上,只要增長一個初始化值便可,若是以此建

立咱們的代碼,只需在一處做出修改便可,這樣,咱們在修改時出錯的機會就減小了。但怎樣

肯定這個數組的大小呢?用表達式sizeof c/sizeof *c(整個數組的大小除以第一個元素的大小)即
可算出,這樣,當數組大小改變時它無需修改。

     for(int i = 0; i< sizeof c / sizeof *c; i++)

      c [ i ] + + ;

     s t r u c t也是一種集合類型,它們也能夠用一樣的方式初始化。由於 C風格的s t r u c t的全部成
員都是公共型的,因此它們的值能夠直接指定:

     struct X {

      int i;

      float f;

      char c;

     } ;

     X x1 = {1,2.2,'c' };

     若是咱們有一個這種s t r u c t的數組,咱們也能夠用嵌套的大括號來初始化每個對象。

     X x2[3] = {{1,1.1, 'a'},{2,2.2, 'b'}};

這裏,第三個對象被初始化爲零。

     若是s t r u c t 中有私有成員,或即便全部成員都是公共成員,但有一個構造函數,狀況就不
同樣了。在上例中,初始值被直接賦給了集合中的每一個元素,但構造函數是經過外在的接口

來強制初始化的。這裏,構造函數必須被調用來完成初始化,所以,若是有一個下面的 s t r u c t
類型:

     struct Y {

      float f;

      int i;

      Y(int A); // presumably assigned to i

     } ;

咱們必須指示構造函數調用,最好的方法像下面這樣:

     Y y2[] = {Y(1),Y(2),Y(3)};

     這樣咱們就獲得了三個對象和進行了三次構造函數調用。只要有構造函數,不管是全部成

員都是公共的s t r u c t仍是一個帶私有成員的c l a s s ,全部的初始化工做都必須經過構造函數,即便
咱們正在對一個集合初始化。

     下面是構造函數帶多個參數的又一個例子:

----------------------- Page 67-----------------------

                                                  第4章 初始化與清除          67
 下載

    注意,它看上去就像數組中的每一個對象都對一個沒有命名的構造函數調用了一次同樣。

4.7  缺省構造函數

    缺省構造函數就是不帶任何參數的構造函數。缺省的構造函數用來建立一個「香草
(v a n i l l a )對象」,當編譯器須要建立一個對象而又不知任何細節時,缺省的構造函數就顯得非

常重要。好比,咱們有一個類Y ,並用它來定義對象:

    Y y4[2] = {Y(1)}

編譯器就會報告找不到缺省的構造函數,數組中的第二個對象想不帶參數來建立,因此編譯器

就去找缺省的構造函數。實際上,若是咱們只是簡單地定義了一個Y對象的數組:

    Y y5[7] ;

或一個單一的對象

    Y y;

編譯器會報告一樣的錯誤,由於它必須用一個缺省的構造函數去初始化數組中的每一個對象。

(記住,一旦有了一個構造函數,編譯器就會確保無論在什麼狀況下它總會被調用)。
    缺省的構造函數是如此重要,因此在一個構造類型( struct  或c l a s s )中沒有構造函數時,
編譯器會自動建立一個。所以下面例子將會正常運行:

    class Z {

     int i; // private

    }; // no constructor

    Z z,z2[10];

然而,一旦有構造函數而沒有缺省構造函數,上面的對象定義就會產生一個編譯錯誤。
    咱們可能會想,缺省構造函數應該能夠作一些智能化的初始化工做,好比把對象的全部內
存置零。但事實並不是如此。由於這樣會增長額外的負擔,並且使程序員沒法控制。好比,若是
咱們把在C中編譯過的代碼用在C + + 中,就會致使不一樣的結果。若是咱們想把內存初始化爲零,
必須親自去作。

    對一個C + +的新手來講,自動產生的缺省構造函數並不會使編程更容易。它實際上要求與
已有的C代碼保持向後兼容。這是C + +中的一個關鍵問題。在C 中,建立一個s t r u c t數組的狀況

----------------------- Page 68-----------------------

  68  C + +編程思想
                                                                       下載

很常見,而在C + +中,在沒有缺省構造函數時,這會引發一個編譯錯誤。
    若是咱們僅僅由於風格問題就去修改咱們的 C代碼,而後用C + +從新編譯,也許咱們會很

不樂意。當將C代碼在C + +中編譯時,咱們總會遇到這樣、那樣的編譯錯誤,但這些錯誤都是
C + +編譯器所發現的C的不良代碼,由於C + +的規則更嚴格。事實上,用C + +編譯器去編譯C代
碼是一個發現潛在錯誤的很好的方法。

4.8  小結

    由C + +提供的細緻精巧機制應給咱們這樣一個強烈的暗示:在這個語言中,初始化和清除
是多麼相當重要。在S t r o u s t r u p設計C + +時,他所做的第一個有關C語言產品的觀察就是,因爲

沒有適當地初始化變量,從而致使了程序不可移植的問題。這種錯誤很難發現。一樣的問題也
出如今變量的清除上。由於構造函數與析構函數讓咱們保證正確地初始化和清除對象(編譯器
將不容許沒有調用構造函數與析構函數就直接建立與銷燬一個對象),使咱們獲得了徹底的控
制與安全。
    集合的初始化一樣如此——它防止咱們犯那種初始化內部數據類型集合時常犯的錯誤,使

咱們的代碼更簡潔。
    編碼期間的安全性是C + +中的一大問題,初始化和清除是這其中的一個重要部分,隨着本
書的進展,咱們能夠看到其餘的安全性問題。

4.9  練習

    1) 用構造函數與析構函數修改第3章結尾處的HANDLE.H,HANDLE.CPP 和USEHANDL.CPP
文件。

    2)  建立一個帶非缺省構造函數和析構函數的類,這些函數都顯示一些信息來表示它們的存
在。寫一段代碼說明構造函數與析構函數什麼時候被調用。
    3) 用上題中的類建立一個數組來講明自動計數與集合初始化。在這個類中增長一個顯示信
息的成員函數。計算數組的大小並逐個訪問它們,調用新成員函數。
    4) 建立一個沒有任何構造函數的類,顯示咱們能夠用缺省的構造函數建立對象。如今爲這

個類建立一個非缺省的構造函數(帶一個參數),試着再編譯一次。解釋發生的現象。

----------------------- Page 69-----------------------

 下載

              第5章  函數重載與缺省參數

    能使名字方便使用,是任何程序設計語言的一個重要特徵。

    當咱們建立一個對象(即變量)時,要爲這個存儲區取一個名字。一個函數就是一個操做
的名字。正是靠系統描述各類各樣的名字,咱們才能寫出易於人們理解和修改的程序。這在很
大程度上就像是寫散文——目的是與讀者交流。這裏就產生了這樣一個問題:如何把人類天然
語言的有細微差異的概念映射到編程語言中。一般,天然語言中同一個詞能夠表明許多種不一樣
的含義,這要依賴上下文來肯定。這就是所謂的一詞多義——該詞被重載了。這點很是有用,

特別是對於細微的差異。咱們能夠說「洗衣服,洗汽車」。若是非得說成「洗(洗衣服的洗)
衣服,洗(洗汽車的洗)汽車」,那將是很愚蠢的,就好像聽話的人對指定的動做毫無辨別能
力同樣。大多數人類語言都是有冗餘的,因此即便漏掉了幾個詞,咱們仍然能夠知道話的意思。
咱們不須要單一的標識—而能夠從上下文中理解它的含義。
    然而大多數編程語言要求咱們爲每一個函數設定一個惟一的標識符。若是咱們想打印三種不

同類型的數據:整型、字符型和實型,咱們一般不得不用三個不一樣的函數名,如 p r i n t _ i n t ( ) 、
p r i n t _ c h a r ( )和p r i n t _ f l o a t ( ) ,這些既增長了咱們的編程工做量,也給讀者理解程序增長了困難。
    在C + +中,還有另一個緣由須要對函數名重載:構造函數。由於構造函數的名字預先由
類的名字肯定,因此只能有一個構造函數名。但若是咱們想用幾種方法來建立一個對象時該怎
麼辦呢?例如建立一個類,它能夠用標準的方法初始化,也能夠從文件中讀取信息來初始化,

咱們就須要兩個構造函數,一個不帶參數(缺省構造函數),另外一個帶一個字符串做爲參數,
以表示用於初始化對象的文件的名字。因此函數重載的本質就是容許函數同名。在這種狀況下,
構造函數是以不一樣的參數類型被調用的。
    重載不只對構造函數來講是必須的,對其餘函數也提供了很大的方便,包括非成員函數。
另外,函數重載意味着,咱們有兩個庫,它們都有一個同名的函數,只要它們的參數不一樣就不

會發生衝突。咱們將在這一章中詳細討論這些問題。
    這一章的主題就是方便地使用函數名。函數重載容許多個函數同名,但還有另外一種方法使
函數調用更方便。若是咱們想以不一樣的方法調用同一函數,該怎麼辦呢?當函數有一個長長的
參數列表,而大多數參數每次調用都同樣時,書寫這樣的函數調用會令人厭煩,程序可讀性也
差。C + +中有一個很通用的做法叫缺省參數。缺省參數就是在用戶調用一個函數時沒有指定參

數值而由編譯器插入參數值的參數。這樣 f (  「h e l l o 」) , f (  「h i 」, 1 )和f (  「h o w d y 」, 2 , ‘c ’)能夠
用來調用同一函數。它們也多是調用三個已重載的函數,但當參數列表相同時,咱們一般希
望調用同一函數來完成相同的操做。
    函數重載和缺省參數實際上並不複雜。當咱們學習完本章的時候,咱們就會明白何時
用到它們,以及編譯、鏈接時它們是怎樣實現的。

5.1  範圍分解

    在第2章中咱們介紹了名字範圍分解的概念(有時咱們用「修飾」這個更通用的術語)。在
下面的代碼中:

----------------------- Page 70-----------------------

   70  C + +編程思想
                                                                       下載

    void f();

    class x {void f();};

類x 內的函數f    ()不會與全局的f       ()發生衝突,編譯器用不一樣的內部名f                ()(全局)和x : : f ( )
(成員函數)來區分兩個函數。在第2章中,咱們建議在函數名前加類名的方法來命名函數,所
以編譯器使用的內部名字可能就是_ f和_ x _ f 。函數名不只與類名關係密切,並且還跟其餘因素

有關。
    爲何要這樣呢?假設咱們重載了兩個函數名:

      void print(char);

      void print(float);

不管這兩個函數是某個類的成員函數仍是全局函數都可有可無。若是編譯器只使用函數名字的
範圍,編譯器並不能產生單一的內部標識符,這兩種狀況下都得用 _ p r i n t結尾。重載函數雖然
能夠讓咱們有同名的函數,但這些函數的參數列表應該不同。因此,爲了讓重載函數正確工
做,編譯器要用函數名來區分參數類型名。上面的兩個在全局範圍定義的函數,可能會產生類
似於_ p r i n t _ c h a r和_ p r i n t _ f l o a t 的內部名。由於,爲這樣的名字分解規定一個統一的標準毫無心

義,因此不一樣的編譯器可能會產生不一樣的內部名(讓編譯器產生一個彙編語言代碼後咱們就可
以看到這個內部名是個什麼樣子了)。固然,若是咱們想爲特定的編譯器和鏈接器購買編譯過
的庫的話,這就會引發錯誤。另外,編譯器在用不一樣的方式來產生代碼時也可能出現這樣的問
題。
    有關函數重載咱們就講到這裏,咱們能夠對不一樣的函數用一樣的名字,只要函數的參數不

同。編譯器會經過分解這些名字、範圍和參數來產生內部名以供鏈接器使用。

5.1.1 用返回值重載

    讀了上面的介紹,咱們天然會問:「爲何只能經過範圍和參數來重載,爲何不能經過
返回值呢?」乍一聽,彷佛徹底可行,一樣將返回值分解爲內部函數名,而後咱們就能夠用返
回值重載了:

    void f();

    int f();

    當編譯器能從上下文中惟一肯定函數的意思時,如int x = f();這固然沒有問題。然而,在C
中,咱們老是能夠調用一個函數但忽略它的返回值,在這種狀況下,編譯器如何知道調用哪一個

函數呢?更糟的是,讀者怎麼知道哪一個函數會被調用呢?僅僅靠返回值來重載函數實在過於微
妙了,因此在C + +中禁止這樣作。

5.1.2 安全類型鏈接

    對名字的範圍分解還能夠帶來一個額外的好處。這就是,在 C中,若是用戶錯誤地聲明瞭
一個函數,或者更糟糕地,一個函數還沒聲明就調用了,而編譯器則按函數被調用的方式去推
斷函數的聲明。這是一個特別嚴重的問題。有時這種函數是正確的,但若是不正確,就會成爲

一個很難發現的錯誤。
    在C + +中,全部的函數在被使用前都必須事先聲明,出現上述狀況的機會大大減小了。編譯
器不會自動爲咱們添加函數聲明,因此咱們應該包含一個合適的頭文件。然而,假如因爲某種
緣由咱們仍是錯誤地聲明瞭一個函數,多是經過本身手工聲明,或包含了一個錯誤的頭文件
(也許是一個過時的版本),名稱分解會給咱們提供一個安全網,也就是人們常說的安全鏈接。

----------------------- Page 71-----------------------

                                                  第5章  函數重載與缺省參數               71
 下載

    請看下面的幾個例子。在第一個文件中,函數定義是:

    //: DEF.CPP -- Function definition

      void f(int) {}

    在第二個文件中,函數沒有聲明就調用了。

    //: USE.CPP -- Function misdeclaration

    void f(char);

    main() {

    //! f(1); //Causes a linker error

    }

    即便咱們知道函數實際上應該是f ( i n t ) ,但編譯器並不知道,由於它被告知 (經過一個明確的
聲明)  這個函數是f ( c h a r )。所以編譯是成功的,在C中,鏈接也能成功,但在C + +中卻不行。因
爲編譯器會分解這些名字,這個函數的定義變成了諸如 f _ i n t之類的名字,而使用的函數則是

f _ c h a r。當鏈接器試圖引用f _ c h a r時,它只能找到f _ i n t,因此它就會報告一條出錯信息。這就是
安全鏈接。雖然這種問題並不常常出現,但一旦出現就很難發現,尤爲是在一個大項目中。這
是利用C + +編譯器查找C語言程序中很隱蔽的錯誤的一個例子。

5.2   重載的例子

    如今咱們回過頭來看看前面的例子,這裏咱們用重載函數來改寫。如前所述,重載的一個
很重要的應用是構造函數。咱們能夠在下面的s t a s h類中看到這點。

    s t a s h ( )的第一個構造函數與前面同樣,但第二個帶了一個 Q u a n t i t y參數來指明分配內存的
初始大小。在這個定義中,咱們能夠看到q u a n t i t y的內部值與s t o r a g e指針一塊兒被置零。

----------------------- Page 72-----------------------

72  C + +編程思想
                                                                        下載

----------------------- Page 73-----------------------

                                             第5章  函數重載與缺省參數                73
 下載

    當咱們用第一個構造函數時,沒有內存分配給 s t o r a g e , 內存是在第一次調用a d d ( )來增長一
個對象時分配的,另外,執行a d d ( )時,當前的內存塊不夠用時也會分配內存。
    下面的測試程序說明了這點,它檢查第一個構造函數。

----------------------- Page 74-----------------------

   74  C + +編程思想
                                                                      下載

    咱們能夠修改這些代碼,增長其餘參數來調用第二個構造函數。這樣咱們能夠選擇 s t a s h的
初始大小。

5.3  缺省參數

    比較上面兩個s t a s h ()構造函數,它們彷佛並無多大不一樣,對不對?事實上,第一個構
造函數只不過是第二個的一個特例——它的初始大小爲零。在這種狀況下去建立和管理同一函
數的兩個不一樣版本實在是浪費精力。
    C + +中的缺省參數提供了一個補救的方法。缺省參數是在函數聲明時就已給定的一個值,
若是咱們在調用函數時沒有指定這一參數的值,編譯器就會自動地插上這個值。在 s t a s h的例子

中,咱們能夠把:

      stash(int Size);// zero quantity

      stash(int Size,int Quantity);

用一個函數聲明來代替

      stash(int Size, int Quantity=0);

這樣,s t a s h ( i n t )定義就簡化掉了——所須要的是一個單一的s t a s h ( i n t , i n t )定義。

    如今這兩個對象的定義

      stash A(100),B(100,0);

將會產生徹底相同的結果。它們將調用同一個構造函數,但對於 A ,它的第二個參數是由編譯

器在看到第一個參數是整型並且沒有第二個參數時自動加上去的。編譯器能看到缺省參數,所
以它知道應該容許這樣的調用,就好像它提供第二個參數同樣,而這第二個參數值就是咱們已
經告訴編譯器的缺省參數。
    缺省參數同函數重載同樣,給程序員提供了不少方便,它們都使咱們能夠在不一樣的場合使
用同一名字。不一樣之處是,當咱們不想親手提供這些值時,由編譯器提供一個缺省參數。上面

的那個例子就是用缺省參數代替函數重載的一個很好的例子。用函數重載咱們得把一個幾乎同
樣含義、一樣操做的函數寫兩遍甚至更多。固然,若是函數之間的行爲差別較大,用缺省參數
就不合適了。
    在使用缺省參數時必須記住兩條規則。第一,只有參數列表的後部參數才但是缺省的,也
就是說,咱們不能夠在一個缺省參數後面又跟一個非缺省的參數。第二,一旦咱們開始使用缺

省參數,那麼這個參數後面的全部參數都必須是缺省的。(這能夠從第一條中導出。)
    缺省參數只能放在函數聲明中,一般在一個頭文件中。編譯器必須在使用該函數以前知道
缺省值。有時人們爲了閱讀方便在函數定義處放上一些缺省的註釋值。如:

      void fn(int x /* =0*/ ) { //...

    缺省參數能夠讓聲明的參數沒有標識符,這看上去頗有趣。咱們能夠這樣聲明:

      void f(int X, int = 0, float =1.1);

    在C + +中,在函數定義時,咱們並不必定須要標識符,像:

      void f(int X, int,float f) {/*...*/}

    在函數體中,x和f能夠被引用,但中間的這個參數值則不行,由於它沒有名字。這種調用還
必須用一個佔位符(p l a c e h o l d e r ),有f ( 1 )或f ( 1 , 2 , 3 . 0 ) 。這種語法容許咱們把一個參數看成佔位
符而不去用它。其目的在於咱們之後能夠修改函數定義而不須要修改全部的函數調用。固然,
用一個有名字的參數也能達到一樣的目的,但若是咱們定義的這個參數在函數體內沒有使用,

----------------------- Page 75-----------------------

                                             第5章  函數重載與缺省參數            75
 下載

多數編譯器會給出一條警告信息,並認爲咱們犯了一個邏輯錯誤。用這種沒有名字的參數就可
以防止這種警告產生。

    更重要的是,若是咱們開始用了一個函數參數,然後來發現不須要用它,咱們能夠高效地
將它去掉而不會產生警告錯誤,並且不須要改動那些調用該函數之前版本的程序代碼。

位向量類

    這裏咱們進一步看一個操做符重載和缺省參數的例子。考慮一個高效存儲真假標誌集合的
問題。若是咱們有一批數據,這些數據能夠用「 o n 」或「o ff」來表示。用一個叫位向量的類
來存儲它們應該是很方便的。有時,位向量並非做爲應用程序的一個工具來使用,而是做爲

其餘類的一部分。
    固然對一組標誌進行編碼,最容易的方法就是每一個標誌佔一個字節,請看下例:

----------------------- Page 76-----------------------

   76  C + +編程思想
                                                                       下載

    然而,這樣很浪費存儲空間,由於咱們用了八位來表示一個只要一位就可表示的標誌。有
時這種存儲很重要,特別是咱們想用這個類去建其餘的類時。因此下面的 B i t Ve c t o r就用一位表
示一個標誌。函數重載出如今構造函數和b i t s ( ) 函數中。

    第一個構造函數(缺省構造函數)產生了一個大小爲零的 B i t Ve c t o r 。咱們不能在這個向量
中設置任何位,由於它們根本就沒有位。首先咱們必須用重載過的 b i t s ( ) 函數增長這一矢量的
大小。這個不帶參數的版本將返回向量的當前大小,而 b i t s ( i n t )會把向量的大小改爲參數指定

的大小。這樣咱們能夠用一樣的函數名來設置和獲得向量的大小。注意對新的大小並無限制

----------------------- Page 77-----------------------

                                            第5章  函數重載與缺省參數            77
 下載

——咱們能夠增大它,也能夠減小它。
    第二個構造函數要用到一個無符號字符數組的指針,這也是一個原始字節數組,第二個參

數告訴構造函數該數組總共有多少個字節,若是第一個參數是零而不是一個有效的指針,這個
數組被初始化爲零。若是咱們沒有給出第二個參數,其缺省值爲8。
    咱們可能覺得咱們能夠用B i t Vector b(0)這樣的聲明來產生一個8個字節的B i t Vector  對象,
並把它們初始化爲零。若是沒有第三個構造函數,狀況確實如此。第三個構造函數取 char*  做
爲它的惟一的參數。參數0既能夠用於第二個構造函數(第二個參數缺省)也可用於第三個構

造函數。編譯器沒法知道應該選用哪個,因此咱們會獲得一個含義不清的錯誤。爲了正確地
產生這樣一個B i t Ve c t o r對象,咱們必須將零強制轉換成一個適當的指針:B i t Vector b((unsigned
char * )0) 。這的確有些麻煩,因此咱們能夠選用B i t Vector b產生一個空向量,而後把它們擴展
到適當的大小,b . b i t s ( 6 4 ) ,這就獲得8個字節的向量。
    編譯 器 必須把c h a r *和unsigned    char* 看成兩個 數據類 型 ,這 點很重要 ,不然

B i t Vector(unsigned char*,int)在第二個參數缺省時就和B i t Ve c t o r ( c h a r * )如出一轍了,編譯器沒法
肯定調用哪一個函數。
    注意p r i n t ( ) 函數有一個c h a r *型的缺省參數。若是咱們知道編譯器怎麼處理字符串常量,那
麼這個函數可能讓咱們感到有點奇怪。編譯器在咱們每次調用這個函數時都產生一個缺省的字
符串嗎?答案是否認的。它是在一個特定的保留區內產生一個單一的字符串做爲靜態全局數據,

而後把這個字符串的地址做爲缺省值傳遞給函數的。
    一個位串
    B i t Ve c t o r 的第三個構造函數引用了一個指向字符串的指針,這個字符串表明了一個位串。
這樣就給用戶提供了方便,由於它容許向量的初值能夠用天然形式的 0 11 0 0 1 0來表示。對象產
生時會匹配這個串的長度,根據串值來設置或清除每一個位。

    其餘函數還有s e t ( )、c l e a r ( )和r e a d ( ) ,這些函數都很重要。它們都用感興趣的位數做爲參數。
p r i n t ( ) 函數打印一條消息,它的缺省參數是空字符串,而後是 B i t Ve c t o r的比特位模型,又一次
使用 0和1。
    當實現B i t Ve c t o r類時會遇到兩個問題。第一個是若是咱們須要的位數並不剛好是 8的倍數
(或機器的字長),咱們必須取最接近的字節數。第二個是在選擇某個當前位時要注意。比方,

用一個字節數組產生了一個B i t Ve c t o r對象時,數組中的每一個字節必須從左讀到右,以使咱們調
用p r i n t ( )函數時出現咱們預期的樣子。
    下面是一組成員函數的定義:

----------------------- Page 78-----------------------

78  C + +編程思想
                                                                        下載

----------------------- Page 79-----------------------

                                            第5章  函數重載與缺省參數            79
 下載

    第一個構造函數很簡單,就是把全部的變量賦零。第二個構造函數分配內存並初始化位數。
接下來用了一個小技巧。外層的 f o r循環指示字節數組的下標,內層 f o r循環每次指示這個字節

的一位,然而這一位是自左向右用i n i t [ i n d e x ] & ( 0 x 8 0 > > o ff s e t )計算出來的。注意這是按位進行
與運算的,並且1 6進制的0 x 8 0      (最高位爲1,其餘位爲零)右移o ff s e t位,產生一個屏蔽碼。如
果結果不爲零,那麼在這一位上必定是 1,這個s e t ( )函數被用來設置B i t Vector              內部位。注意描
述字節位時應從左到右,只有這樣用p r i n t ( )函數顯示的結果看上去纔是有意義的。
    第三個構造函數把一個二進制0、1序列的字符串轉換成一個B i t Ve c t o r 。位數就取字符串的

----------------------- Page 80-----------------------

  80      C + +編程思想
                                                                       下載

長度。但字符串的長度可能並不正好是8的整數倍,因此字節數n u m B y t e s先將位數除以8,而後
根據餘數是否爲0來調整。這種狀況下,掃描位是在源串中從左到右進行的,這與第二個構造

函數不一樣。
    s e t ( )、c l e a r ( )和r e a d ( )三個函數形式都差很少,開始三行徹底同樣:a s s e r t ( )檢查傳入的參數
是否合法,而後產生指向字節數組的索引和指向被選字節的偏移。 S e t ( )和r e a d ( )用一樣的方法
產生屏蔽字節:將1移位到所要的位置。但s e t ( )是用選定的字節與屏蔽字節相「或」來將該位
置1,而r e a d ( )是用選定的字節與屏蔽字節相「與」來得到該位的狀態。c l e a r ( )是將1移位到指定

位來產生屏蔽字節的,而後將選定的字全部的位求「反」(用~ ),再與屏蔽字節相「與」,這樣
只有指定的位被置爲零。
    注意s e t ( )、r e a d ( )和c l e a r ( )能夠寫得更緊湊些,如c l e a r ( )能夠這樣來寫:

    bytes[bit/CHAR_BIT]&=~(1<<(bit % CHAR_BIT));

這樣寫能夠提升效率,但確定下降了可讀性。
    兩個重載的b i t s ( ) 函數在行爲上差異很大。第一個僅僅是一個存取函數(一種向沒有訪問權
限的人提供私有成員數據的函數),告知數組中共有多少位。第二個函數用它的參數來計算所
需的字節數,而後用r e a l l o c ( )函數從新分配內存(若是b y t e s爲零,它將分配新內存),並對新增

的位置零。注意,若是咱們要求的位數與原有的位數相等,這個函數仍有可能從新分配內存
(這取決於r e a l l o c ( )函數的實現)。但這不會破壞任何東西。
    p r i n t ( ) 函數顯示m s g字串,標準的C庫函數p u t s ( ) 已經加了一個新行,因此對缺省參數將輸
出一個新行。而後它用r e a d ( )讀取每一位的值以肯定顯示什麼字符。爲了閱讀方便,在每讀完 8
位後它顯示一個空格。因爲第二個B i t Ve c t o r構造函數是讀取字節數組的方式,p r i n t ( ) 函數將會

用熟悉的形式顯示結果。
    下面的程序經過檢驗B i t Ve c t o r的全部函數來測試B i t Ve c t o r類。

----------------------- Page 81-----------------------

                                            第5章  函數重載與缺省參數            81
 下載

    對象b v 一、b v 2 、b v 3顯示了三種不一樣的B i t Ve c t o r類和它的構造函數。s e t ( )和c l e a r ( )函數也被
檢驗(r e a d ( )在p r i n t ( ) 內檢驗)。在程序的尾部,b v 2被減小了一半而後又增大,用以說明將
B i t Ve c t o r 的尾部置零的一種方法。
    咱們應該知道在標準的C + +庫中包含着b i t s和b i t s t r i n g類,這些類向位向量提供了一個更完
全(也更標準)的實現。

5.4  小結

    函數重載和缺省參數都爲調用函數提供了方便。有時爲弄清到底哪一個函數會被調用,也讓
人迷惑不清。好比在B i t Ve c t o r類中,下式就彷佛對兩個b i t s ( ) 函數均可以調用:

    int bits(int sz=-1);

    若是調用它時不帶參數,函數就會用缺省的值 - 1,它認爲咱們想知道當前的位數。這種使

用彷佛同前面的同樣,但事實上存在着明顯的不一樣,至少讓咱們感受不舒服。
    在b i t s ()內部咱們得按參數的值做一個判斷,若是咱們必須去找缺省值而不是做爲一個
普通值,根據這一點,咱們就能夠造成兩個不一樣的函數。一個是在通常狀況下,一個是在缺省
狀況下。咱們也能夠把它分割成兩個不一樣的函數體,而後讓編譯器去選擇執行哪個,這能夠
提升一點效率,由於不須要傳遞額外的代碼,由條件決定的額外代碼也不會被執行。若是咱們

須要反覆調用這個函數,這種效率的少量提升就會表現得很明顯。
    在這種狀況下,用缺省參數咱們確實會丟失某些東西。首先,缺省值不能做他用,如本例
中- 1。如今,咱們不能區分一個負數是一個意外仍是一個缺省狀況。第二,因爲在單一參數時
只有一個返回值,因此編譯器就會丟失不少重載函數時能夠獲得的有用信息。好比,咱們定
義:

    int i=bv1.set(10);

編譯器會接受它但再也不告訴咱們其餘東西,但做爲類的設計者,咱們可能認爲是一個錯誤。
    再看看用戶遇到的問題。當用戶讀咱們的頭文件時,哪一種設計更容易理解呢?缺省值 - 1意

味着什麼?沒有人告訴他們。而用兩個分開的函數則很是清楚,由於一個帶有一個參數但不返
回任何值,而另外一個則不帶參數但返回一個值。即便沒有有關的文檔,也很容易猜想這兩個函
數完成什麼功能。
    咱們不能把缺省參數做爲一個標誌去決定執行函數的哪一塊,這是基本原則。在這種狀況
下,只要可以,就應該把函數分解成兩個或多個重載的函數。缺省參數應該是能把它看成變通

值來處理的值,只不過這個值出現的可能比其餘值要大,因此用戶能夠忽略它或只在須要改變
缺省值時纔去用它。
    缺省參數的引用是爲了使函數調用更容易,特別是當這些函數的許多參數都有特定值時。
它不只使書寫函數調用更容易,並且閱讀也更方便,尤爲當用戶是在制定參數過程當中,把那些
最不可能調整的缺省參數放在參數表的最後面時。

    缺省參數的一個重要應用是在開始定義函數時用了一組參數,而使用了一段時間後發現要
增長一些參數。如今咱們只要把這些新增參數都做爲缺省的參數,就能夠保證全部使用這一函

----------------------- Page 82-----------------------

  82      C + +編程思想
                                                                       下載

數的代碼不會遇到麻煩。

5.5  練習

    1)  建立一個m e s s a g e類,其構造函數帶有一個 c h a r *型的缺省參數。建立一個私有成員

c h a r *,並假定構造函數能夠傳遞一個靜態引用串:簡單將指針參數賦給內部指針。建立兩個
重載的成員函數p r i n t ( ) ;一個不帶參數,而只是顯示存儲在對象中的消息,另外一個帶有 c h a r *參
數,它將顯示該字符串加上對象內部消息。比較這種方法和使用構造函數的方法,看哪一種方
法更合理?
    2) 測定您的編譯器是怎樣產生彙編輸出代碼的,並嘗試着減少名字分解表。

    3)  用缺省參數修改S TA S H 4 . H和S TA S H 4 . C P P中的構造函數,建立兩個不一樣的s t a s h對象來
測試構造函數。
    4) 比較f l a g s類與B i t Ve c t o r類的執行速度。爲了保證不會與效率弄混,把 s e t ( )、c l e a r ( )和
r e a d ( ) 中的i n d e x 、o ff s e t和m a s k定義合併成一個單一的聲明來完成適當的操做(測試這個新的
代碼以確保代碼正確)。

    5) 修改F L A G S . C P P 以使它能夠動態地爲標誌分配內存,傳給構造函數的參數是空間 存儲
的大小,其缺省值爲1 0 0。保證在析構函數中清除這些存儲空間。

----------------------- Page 83-----------------------

 下載

                  第6章  輸入輸出流介紹

    到目前爲止,在這本書裏,咱們仍使用之前的可靠的C標準I / O庫,這是一個可變成類的完

美的例子。
    事實上,處理通常的 I / O 問題,比僅僅用標準庫並把它變爲一個類,須要作更多的工做。
若是能使得全部這樣的「容器」—標準I / O 、文件、甚至存儲塊—看上去都同樣,只須記
住一個接口,不是更好嗎?這種思想是創建在輸入輸出流之上的。與標準 C輸入輸出庫的各類
各樣的函數相比,輸入輸出流更容易、安全、有效。

    輸入輸出流一般是C + +初學者學會使用的第一個類庫。在這一章裏,咱們要考察輸入輸出
流的用途,這樣,輸入輸出流要代替這本書剩餘部分的 C I/O                     函數。下一章,咱們會發現如何
創建咱們本身的與輸入輸出流相容的類。

6.1  爲何要用輸入輸出流

    咱們可能想知道之前的C庫有什麼很差。爲何不把C庫封裝成一個類,而後進行處理?
其實,當咱們想使C庫用起來更安全、更容易一點時,在有些狀況下,這樣作很完美。例如,

當咱們想確保一標準輸入輸出文件老是被安全地打開,被正確地關閉,而不依賴用戶是否記得
調用c l o s e ( )函數:

    在C中執行文件I / O時,要用一個沒有保護的指針指向文件結構。而這個類封裝了這個指針,
並用構造函數和析構函數保證它能被正確地初始化和清除。第二個構造函數參數是文件模式,
其缺省值爲「r 」,表明「只讀」。
    在文件I / O 函數中,爲了取指針的值,可以使用fp( )訪問函數。下面是成員函數的定義:

----------------------- Page 84-----------------------

  84      C + +編程思想
                                                                      下載

    就像經常作的同樣,構造函數調用 fopen( ) ,但它還檢查結果,從而確保結果不是零,如
果結果是零意味着打開文件時出錯。若是出錯,這個文件名就被打印,並且函數exit( )被調用。
    析構函數關閉這個文件,存取函數fp( )返回值f。下面是使用類文件的一個簡單的例子:

    在該例子中建立了文件對象,在正常C文件I / O函數中經過調用fp( ) 來使用它。當咱們使用
完畢時,就忘掉它,則這個文件在該範圍的末端由析構函數關閉。

正式的真包裝

    即便文件指針是私有的,但因爲fp( )檢索它,因此也不是特別安全的。僅有的保證做用是
初始化和銷燬。既然這樣,爲何不使文件指針是公有的,或者用結構體來代替?注意使用f p ( )

獲得f的副本時,不能賦值給f—它徹底處於類控制下。固然,一旦用戶使用f p ( )返回的指針值,
他仍能對結構元素賦值。因此安全性是保證一個有效的文件指針而不是結構裏的正確內容。
    若是想絕對安全,必須禁止用戶直接訪問文件指針。這意味着全部正常的文件 I / O 函數的
某些版本將必須做爲類成員表現出來。這樣,經過C途徑所作的每件事,在C + +類中都可作到:

----------------------- Page 85-----------------------

                                                 第6章 輸入輸出流介紹           85
 下載

    這個類包含幾乎全部的來自S T D I O . H文件的I / O函數。沒有v f p r i n t f ( )函數,它只是被用來實

現p r i n t f ( )成員函數。

    F i l e有着與前面例子相同的構造函數,並且也有缺省的構造函數。若是要創建一個 F i l e對象
數組或使用一個F i l e對象做爲另外一個類的成員,在那個類裏,構造函數不發生初始化(但有時

在被包含的對象建立後初始化),那麼缺省構造函數是重要的。
    缺省構造函數設置私有F i l e指針f爲0,可是如今,在f的任何引用以前,它的值必需要被檢

----------------------- Page 86-----------------------

  86      C + +編程思想
                                                                      下載

查以確保它不爲0 。這是由類的最後一個成員函數F                   ()來完成的。F      ()函數僅由其餘成員函
數使用,所以是私有的。(咱們不想讓用戶直接訪問這個類的F i l e結構)[ 1 ]

    從任何意義上講,這不是一個很壞的解決辦法。它是至關有效的,能夠設想爲標準(控制
臺)I / O和內核格式化(讀/寫一個存儲塊而不是一個文件或控制檯)構造相似的類。
    大的障礙是運行期間用做參數表函數的解釋程序。這是在運行期間對格式串作語法分析以

及從參數表中索取並解釋變量的代碼。產生這個問題的四個緣由是:
    1) 即便僅使用解釋程序的一部分功能,全部的東西將得到裝載。假如說:

     p r i n t f ( " % c " , ' x ' ) ;

咱們將獲得整個包,包括打印出浮點數和串的那部分。沒有辦法可減小程序使用的空間。
    2) 因爲解釋發生在運行期間,因此不能終止這個執行。使人灰心的是在編譯時,格式串裏
的全部信息都在這兒,可是直到運行時,才能對其求值。可是,若是能在編譯時分析格式串裏
的變量,就能夠創建硬函數調用,它比運行期間解釋程序快得多(雖然p r i n t f ( )族函數一般已被
優化得很好)。

    3) 因爲直到運行期間纔對格式串求值,一個更糟糕的問題出現了:可能沒有編譯時的錯誤
檢查。若是咱們已經嘗試找出因爲在p r i n t f ( )說明裏使用錯誤的數或變量類型而產生的錯誤,我
們大概對這個問題很熟悉了。 C + +對編譯期間錯誤檢查做了許多工做,使咱們及早發現錯誤,
使工做更容易。特別是由於I / O庫用得不少,若是棄之不用,那是很不明智的。
    4)  對C + +,最重要的問題是函數中的p r i n t f ( )族不是能擴展的。它們被設計是用來處理 C中

四類基本的數據類型(字符,整型,浮點數,雙精度及它們的變形)。咱們可能認爲每增長一
個新類時,都能增長一個重載的p r i n t f ( )和s c a n f ( )函數(以及它們對文件和串的變量)。可是要
記住,重載函數在參數表裏必須有不一樣的類型,p r i n t f ( )族把類型信息隱藏在可變參數表和格式
串中。對一個像C + +這樣其目標是能容易地添加新的數據類型的語言,這是一個笨拙的限制。

6.2  解決輸入輸出流問題

    全部這些問題都清楚地代表: C + +中應該有一個最高級別標準類庫,用以處理 I / O 。因爲

「h e l l o , w o r l d 」差很少是每一個人使用一種新的語言所寫的第一個程序, 並且因爲I / O一般是每一個程
序的一部分,所以C + +中的I / O庫必須特別容易使用。這是一個巨大的挑戰:它不知道必須適
應哪些類,可是它必須能適用於任何新的類。這樣的約束要求這個最高級別的類是一個真正的
有靈感的設計。
    這一章看不到這個設計的細節以及如何向咱們本身的類中加入輸入輸出流功能(在之後的

章節裏將會看到)。首先,咱們必須學會使用輸入輸出流,其次,在處理I / O和格式時,咱們除
了能作大量的調節並提升清晰度外,還會看到,一個真正的、功能強大的C + +庫是如何工做的。

6.2.1 預先了解操做符重載

    在使用輸入輸出流庫前,必須明白這個語言的一個新性能,這一性能的詳細狀況在下一章
介紹。要使用輸入輸出流,必須知道C + +中全部操做符具備不一樣的含義。在這一章,咱們特別
講一下「< <」和「> > 」,咱們說「操做符具備不一樣的含義」,這值得進一步探討。

    在第5章中,咱們已經學到怎樣用不一樣的參數表使用相同的函數名。編譯器在編譯一個變
量後跟一個操做符再後跟一個變量組成的表達式時,它只調用一個函數。那就是說,一個操做

  [1] FULLWRAP test file和其實如今此書的源程序中提供,詳見前言。

----------------------- Page 87-----------------------

                                                第6章 輸入輸出流介紹           87
 下載

符只不過是不一樣語法的函數調用。
    固然,這就是C + +在數據類型方面的特別之處。必須有一個事先說明過的函數來匹配操做

符和那些特別變量類型,不然編譯器不接受這個表達式。
    大多數人發現,操做符重載的麻煩是因爲這樣的想法:即咱們知道 C操做符的全部知識是
錯的。這是不對的。下面是C + +的設計的兩個主要目的:
    1) 一個用C編譯過的程序用C + +也能編譯。C + +編譯器僅有的編譯錯誤和警告源於C語言的
「漏洞」,修正這些須要局部編輯(其實,C + +編譯器的提示通常會使咱們直接查找到 C程序中

未被發現的錯誤)。
    2) 用C + +從新編譯,C ++編譯器不會暗地裏改變C程序的行爲。
    記住這些目的有助於回答許多問題。知道從 C轉向C + +並非無規律的變化,會使這種轉
變動容易。特別是,內部數據類型的操做符將不會以不一樣的方式工做—不能改變它們的意思。
重載的操做符僅能在包含新的數據類型的地方建立。因此能爲一個新類創建一個新的重載操做

符,可是表達式

    1 < < 4;

不會忽然間改變它的意思,並且非法代碼

    1 . 4 1 4 < < 1 ;

也不會忽然能開始工做了。

6.2.2 插入符與提取符

    在輸入輸出流庫裏,兩個操做符被重載,目的是能容易地使用輸入輸出流。操做符「 < < 」
常常做爲輸入輸出流的插入符,操做符「> >」常常做爲提取符。
    一個流是一個格式化並保存字節的對象。能夠有一個輸入流(  i s t r e a m )或一個輸出流
(o s t r e a m)。有不一樣類型的輸入流和輸出流:文件輸入流( i f s t r e a m s )和文件輸出流( o f s t r e a m s )、c h a r *
內存的(內核格式化)輸入流(i s t r s t r e a m s )和輸出流( o s t r s t r e a m s )、以及與標準C + +串(s t r i n g)

類接口的串輸入流(i s t r i n g s t r e a m s)和串輸出流( o s t r i n g s t r e a m s )。無論在操做文件、標準I / O、存
儲塊仍是一串對象中全部這些流對象有相同的接口。單個接口被擴展用於支持新的類。
    若是一個流能產生字節(一個輸入流),能夠用一個提取符從這個流中獲取信息。這個提
取符產生並格式化目的對象所指望的信息類型。能夠使用 c i n對象看一下這個例子。 c i n對象相
當於C中s t d i n的輸入輸出流,也就是可重定向的標準輸入。不論什麼時候包含了I O S T R E A M . H頭文

件,這個對象就被預約義了(這樣,輸入輸出流庫能自動與大部分編譯器鏈接)。

----------------------- Page 88-----------------------

  88      C + +編程思想
                                                                       下載

    每一個數據類型都有重載操做符「> > 」,這些數據類型在一個輸入語句裏做爲「> >」右邊參
數使用(也能夠重載咱們本身的操做符,這一點將在下一章講到)。

    爲了發現各類各樣的變量裏有什麼,咱們能夠用帶插入符「 < < 」的c o u t對象(與標準輸出
相對應,還有一個與標準錯誤相對應的c e r r對象) :

    這是特別乏味的,並且對於 p r i n t f ( ) 函數,看來類型檢查沒有多大或根本沒有改進。幸運
的是,輸入輸出流的重載插入符和提取符,被設計在一塊兒鏈接成一個更容易書寫的複合表達

式:

    在下一章中,咱們將明白這是怎樣發生的,可是如今,以類用戶的態度知道它如何工做也
就足夠了。
    1. 操縱算子
    這裏已經添加了一個新的元素:一個稱做 e n d l的操縱算子。一個操縱算子做用於流上,這
種狀況下,插入一新行並清空流(消除全部存儲在內部流緩衝區裏的尚未輸出的字符)。也

能夠只清空流:

     c o u t < < f l u s h;

    另外有一個基本的操縱算子把基數變爲o c t (八進制),d e c (十進制)或h e x (十六進制) :

    c o u t < < h e x < < " 0 x " < < i < < e n d l;

    有一個用於提取的操縱算子「跳過」空格:

    c i n > > w s;

還有一個叫e n d s 的操縱算子和e n d l操縱算子同樣,僅僅用於 s t r s t r e a m s   (稍後介紹)。這些是
I O S T R E A M . H裏全部的操縱算子,I O M A N I P. H裏會有更多的操縱算子,下一章介紹。

6.2.3 一般用法

    雖然c i n和提取符「> > 」爲c o u t和插入符「< < 」提供了一個良好的平衡,但在使用格式化
的輸入機制,尤爲是標準輸入時,會遇到與s c a n f ( )中一樣的問題。若是輸入一個非指望值,進
程則被偏離,並且它很難恢復。另外,格式化的輸入缺省以空格爲分隔符。這樣,若是把上面

----------------------- Page 89-----------------------

                                                第6章 輸入輸出流介紹           89
 下載

的代碼塊蒐集成一個程序:

給出如下輸入:

咱們應該獲得與輸入相同的輸出

但輸出是:(有點不是所指望的)

    注意到b u f只獲得第一個字,這是因爲輸入機制是經過尋找空格來分隔輸入的,而空格在
「t h i s」的後面。另外,若是連續的輸入串長於爲b u f分配的存儲空間,會發生b u f溢出現象。
    看來提供c i n和提取符只是爲了完整性,這多是查看它的一個好方法。實際上,有時想
獲得列字符的一行一行排列的輸入,而後掃描它們,一旦它們安全地處於緩衝區就執行轉換。
這樣,咱們就沒必要擔憂輸入機制因非指望數據而阻塞了。

----------------------- Page 90-----------------------

  90      C + +編程思想
                                                                      下載

    另外一件要考慮的事是命令行接口的總體概念。當控制檯還比不上一臺玻璃打字機時人們就
對這一點有所認識。世界發展迅速,如今圖形用戶接口(G U I )處於支配地位。這樣的世界裏,

控制檯I / O的意思是什麼呢?除了很簡單的例子或測試外,能夠徹底忽略 c i n,並採起如下更有
意義的步驟:
    1)  若是程序須要輸入,從一個文件中讀取輸入——會發現使用輸入輸出流文件很是容易。
輸入輸出流在G U I    (圖形用戶接口)下仍運行得很好。
    2)  只讀輸入而不想去轉換它。一旦在某處得到輸入,它在轉換時不會影響其餘,那麼咱們

能夠安全地掃描它。
    3)  輸出的狀況有所不一樣。若是正在使用圖形用戶接口,則必須將輸出送到一個文件中(這
與將輸出送到c o u t是相同的)或者使用圖形用戶接口設備顯示數據,c o u t不工做。然而,一般把
輸出送到c o u t是有意義的。在這兩種狀況下,輸入輸出流的輸出格式化函數都是頗有用的。

6.2.4 面向行的輸入

    要獲取一行輸入,有兩種選擇:成員函數 g e t             ()或g e t l i n e ()。兩個函數都有三個參數:

指向存儲結果字符的緩衝區指針、緩衝區大小(不能超過其限度)和知道何時中止讀輸入
的終止符。終止符有一個常常用到的缺省值「 / n 」。兩個函數遇到輸入終止符時,都把零儲存
在結果緩衝區裏。
    其不一樣點是什麼呢?差異雖小但極其重要: g e t              ()遇到輸入流的分隔符時就中止,而不
從輸入流中提取分隔符。若是用一樣的分隔符再調用一次 g e t                    ()函數,它會當即返回而不帶

任何輸入。(要麼在下一個g e t        ()說明裏用一個不一樣的分隔符,要麼用一個不一樣的輸入函數)。
g e t l i n e ()與其相反,它從輸入流中提取分隔符,但仍沒有把它儲存在結果緩衝區裏。
    總之,當咱們在處理文本文件時,不管何時讀出一行,都會想到用g e t l i n e                   ()。
    1. get ()的重載版本
    g e t ()有三種其餘的重載版本:一個沒有參數表,返回下一個字符,用的是一個i n t返回

值;一個把字符填進字符參數表,用一個引用(想當即弄明白這個,要跳到第 1 0章看);一個
直接存儲在另外一個輸入輸出流對象的基本緩衝區結構裏。這一點在本章的後面介紹。
    2. 讀原始字節
    若是想確切知道正在處理什麼,並把字節直接移進內存中的變量、數組或結構中,能夠用
r e a d ()函數。第一個參數是一個指向內存目的地址的指針,第二個參數指明要讀的字節數。

預先將信息存儲在一個文件裏特別有用。例如,在二進制形式裏,對一個輸出流使用相反的
w r i t e ()成員函數。之後咱們會看到全部這些函數的例子。
    3.  出錯處理
    除沒有參數表的g e t     ()外。全部g e t  ()和g e t l i n e ()的版本都返回字符來源的輸入流,
沒有參數表的g e t    ()返回下一個字符或E O F 。若是取回輸入流對象,要詢問一下它是否正確。

事實上,咱們可用成員函數g o o d         ()、e o f ()、f a i l ()和b a d ()詢問任何輸入輸出流是否正
確。這些返回狀態信息基於e o f b i t     (指緩衝位於序列的末尾)、f a i l b i t   (指因爲格式化問題或不
影響緩衝區的其餘問題而使操做失敗)和b a d b i t          (指緩衝區出錯)。
    然而,正如前面提到的,若是想輸入特定類型,並且從輸入中讀出的類型與所指望的不一
致時,輸入流的狀態通常要遭到莫名其妙的破壞。固然可經過處理輸入流來改正這個問題。如

果你們按照個人建議,一次讀入一行或一個大的字符段(用 r e a d                    ()函數),而且除簡單狀況
外不使用輸入格式函數,那麼,所關心的只是讀入位置是否處於輸入的末尾。幸虧這種測試很

----------------------- Page 91-----------------------

                                                第6章 輸入輸出流介紹          91
 下載

簡單,並且能在條件內部完成,如w h i l e ( c i n )和i f ( c i n )等。要接受這樣的事實,當咱們在上下文
環境中使用輸入流對象時,正確值被安全、正確和魔術般地產生出來,以代表對象是否達到輸

入的末尾。咱們也能夠像i f         (!c i n)同樣,使用布爾非運算「!」,代表這個流不正確,即我
們可能已經達到輸入的末尾了,應該中止讀這個流。
    有時候,流處於非正確狀態,咱們知道這個狀況,而且想繼續使用它。例如,若是咱們到
達文件的末尾,e o f b i t和f a i l b i t被設置,這樣,那個流對象的狀況代表:這個流再也不是正確的了。
咱們可能想經過尋找之前的位置並讀出更多的數據而繼續使用這個文件。要改變這個狀況,只
需簡單地調用c l e a r ( )成員函數便可 [ 1 ] 。

6.3  文件輸入輸出流

    用輸入輸出流操做文件比在C中用S T D I O . H要容易得多,安全得多。要打開文件,所作的
就是創建一個對象。構造函數作打開文件的工做。沒必要明確地關閉一個文件(儘管咱們用c l o s e ()
成員函數也能作到)。這是由於當對象超出範圍時,析構函數將其關閉。要創建一缺省輸出文
件,只要建一個o f s t r e a m對象便可。下面這個例子說明到目前爲止已討論的不少特性。注意

F S T R E A M . H文件包含聲明文件I / O類,這也包含I O S T R E A M . H文件。

  [1] 更新的實現將支持輸入輸出流的這種處理錯誤的方式,但某些狀況下也會彈出異常。

----------------------- Page 92-----------------------

  92      C + +編程思想
                                                                      下載

    創建i f s t r e a m和o f s t r e a m,後跟一個a s s e r t ( ) 以保證文件能成功地被打開。還有一個對象,用
在編譯器指望一個整型結果的地方,產生一個代表成功或失敗的值。 (這樣作要調用一個自動
類型轉換成員函數,這一點將在第11章討論) 。
    第一個w h i l e循環代表g e t ( )函數的兩種形式的用法。一是不論讀到第 S Z - 1個字符仍是遇到
第三個參數(缺省值爲「/ n 」),g e t       ()函數把字符取進一個緩衝區內,並放入一個零終止符。
g e t ()把終止符留在輸入流內,這樣,經過使用不帶參數形式的g e t                  (),這個終止符必然經過
i n . g e t ( )而被扔掉,這個g e t ()函數取回一個字節並使它做爲一個i n t類型返回。二是能夠用
i g n o r e ()成員函數,它有兩個缺省的參數,第一個參數是扔掉字符的數目,缺省值是1,第二
參數表示i g n o r e ( )函數退出處的那個字符(在提取後),缺省值是E O F 。
    下面將看到兩個看起來很相似的輸出說明:c o u t和o u t 。注意這是很合適的,咱們沒必要擔憂
正在處理的是哪一種對象,由於格式說明對全部的 o s t r e a m對象一樣有效。第一類輸入回顯行至
標準輸出,第二類寫行至新文件幷包括一個行數目。
    爲說明g e t l i n e (),打開咱們剛剛創建的文件併除去行數是一件頗有趣的事。在打開一個
要讀的文件以前,爲確保文件是正當關閉的,有兩種選擇。能夠用大括號包住程序的第一部分
以迫使o u t對象脫離範圍,這樣,調用析構函數並在這裏關閉這個文件。也能夠爲兩個文件調
用c l o s e (),如想這樣作,能夠調用o p e n   ()成員函數重用i n對象(也能夠像第1 2章講的那樣,
在堆裏動態地建立和消除對象)。
    第二個w h i l e循環說明g e t l i n e ()如何從其遇到的輸入流中移走終止符(它的第三個參數缺
省值是「/ n 」)。然而g e t l i n e ( )像g e t ( )同樣,把零放進緩衝區,並且它一樣不能插入終止符。

打開方式

    能夠經過改變缺省變量來控制文件打開方式。下表列出控制文件打開方式的標誌。

      標   志                                          函   數

    i o s : : i n                   打開一個輸入文件,用這個標誌做爲i f s t r e a m的打開方式,以防

                                  止截斷一個現成的文件

    i o s : : o u t                 打開一個輸出文件,當用於一個沒有 i o s : : a p p、i o s : : a t e或i o s : : i n

                                  的o f s t r e a m時,i o s : : t r u n c被隱含

    i o s : : a p p                 以追加的方式打開一個輸出文件

    i o s : : a t e                 打開一現成文件(不管是輸入仍是輸出)並尋找末尾

    i o s : : n o c r e a t e       僅打開一個存在的文件(不然失敗)

    i o s : : n o r e p l a c e     僅打開一個不存在的文件(不然失敗)

    i o s : : t r u n c             若是一個文件存在,打開它並刪除舊的文件

    i o s : : b i n a r y           打開一個二進制文件,缺省的是文本文件

----------------------- Page 93-----------------------

                                                第6章 輸入輸出流介紹           93
 下載

    這些標誌可用一個「位或」(O R )運算來鏈接。

6.4  輸入輸出流緩衝

    不管何時創建一個新類,都應當儘量努力地對類的用戶隱藏類的基本實現詳情,僅

顯示他們須要知道的東西,把其他的變爲私有以免產生混淆。一般,當咱們使用輸入輸出流
的時候,咱們不知道或不關心在哪一個字節被產生或消耗。其實,不管正在處理的是標準  I / O 、
文件、內存仍是某些新產生的類或設備,狀況都有所不一樣。
    這時,重要的是把消息發送到產生和消耗字節的輸入輸出流。爲了提供通用接口給這些流
而且仍然隱藏其基本的實現,它被抽像成本身的類,叫s t r e a m b u f。每個輸入輸出流都包含一

個指針,指向某種s t r e a m b u f (這依賴於它是否處理標準I / O、文件、內存等等)。咱們能夠直接
訪問s t r e a m b u f。例如,能夠向s t r e a m b u f移進、移出原始字節,而沒必要經過輸入輸出流來格式化
它們。固然,這是經過調用s t r e a m b u f對象的成員函數來完成的。
    當前,咱們要知道的最重要的事是:每一個輸入輸出流對象包含一個指向 s t r e a m b u f的指針,
並且,若是須要調用的話,s t r e a m b u f有咱們能夠調用的成員函數。

    爲了容許咱們訪問s t r e a m b u f,每一個流對象有一個叫作r d b u f ( ) 的成員函數,這個函數返回指
向對象的s t r e a m b u f的指針。這樣,咱們能夠爲下層的s t r e a m b u f調用任何成員函數。然而,對
s t r e a m b u f指針所作的最有興趣的事之一是:使用「< < 」操做符將其與另外一個輸入輸出流聯結。
這使咱們的對象中的全部字節流進「< <」左邊的對象中。這意味着,若是把一個輸入輸出流的
全部字節移到另外一個輸入輸出流,咱們沒必要作讀入它們的一個字節或一行這樣單調的工做。這

是一流的方法。
    例如,下面是打開一個文件並將其內容發送到標準輸出(相似前面的例子)的一個很簡單
的程序:

    在確信命令行有一個參數後,經過使用這個變數創建一個文件輸入流 i f s t r e a m 。若是這個
文件不存在,打開它時將會失敗,這個失敗被a s s e r t ( i n )捕獲。
    全部的工做實際上在這個說明裏完成:

    c o u t < < i n . r d b u f ( );

它把文件的整個內容送到c o u t 。這不只比代碼更簡明扼要,也比在每次移動字節更加有效。

使用帶streambuf的get()函數

    有一種g e t ()形式容許直接向另外一對象的s t r e a m b u f寫入。第一個參數是s t r e a m b u f的目的

----------------------- Page 94-----------------------

  94      C + +編程思想
                                                                      下載

地址(它的地址神祕地由一個引用攜帶,第 1 0章討論這個問題)。第二個參數是終止符,它終
止g e t ()函數。因此,打印一個文件到標準輸出的另外一方法是:

    r d b u f ( )返回一個指針,它必須逆向引用,以知足這個函數看到對象的須要。g e t                 ()函數不
從輸入流中拖出終止符,必須經過調用i g n o r e          ()移走終止符。因此,g e t ( )永遠不會跳到新行
上去。

    咱們可能沒必要常用這樣的技術,但知道它的存在是有用的。

6.5  在輸入輸出流中查找

    每種輸入輸出流都有一個概念:「下一個」字符來自哪裏(如果輸入流)或去哪裏(如果
輸出流)。在某些狀況下,可能須要移動這個流的位置,能夠用兩種方式處理:第一種方式是在
流裏絕對定位,叫流定位(s t r e a m p o s);第二種方式像標準C庫函數f s e e k ( )那樣作,從文件的
開始、結尾或當前位置移動給定數目的字節。

    流定位(s t r e a m p o s )方法要求先調用「t e l l 」函數:對一個輸出流用t e l l p  ()函數,對一
個輸入流用t e l l g ()函數。(「p 」指「放指針」,「g 」指「取指針」)。要返回到流中的那個位置
時,這個函數返回一個s t r e a m p o s,咱們之後能夠在用於輸出流的s e e k p         ()函數或用於輸入流
的s e e k g ()函數的單參數版本里使用這個s t r e a m p o s。
    另外一個方法是相對查找,使用s e e k p        ()和s e e k g ()的重載版本。第一個參數是要移動的

字節數,它能夠是正的或負的。第二個參數是查找方向:

        I o s : : b e g                 從流的開始位置

        I o s : : c u r                從流的當前位置

        I o s : : e n d                 從流的末尾位置

    下面是一個說明在文件中移動的例子,記住,不只限於在文件裏查找,就像在 C和C + +中
的S T D I O . H同樣。在C + +中,咱們能夠在任何類型的流中查找(雖然查找時,c i n和c o u t的方式未
被定義) :

----------------------- Page 95-----------------------

                                                第6章 輸入輸出流介紹           95
 下載

    這個程序從命令行中讀取文件名,並做爲一個文件輸入流( i f s t r e a m )打開。a s s e r t          ()檢

測打開是否失敗。因爲這是一種輸入流,所以用 s e e k g              ()來定位「取指針」,第一次調用從文
件末查找零字節,即到末端。因爲 s t r e a m p o s是一個l o n g的t y p e d e f,那裏調用t e l l g (),返回被
打印文件的大小。而後執行查找,移取指針至文件大小的 1 / 1 0處—注意那是文件尾的反向查
找,因此指針從尾部退回。如咱們想進行從文件尾的正向查找,取指針恰好停在文件尾。那裏
的s t r e a m p o s被讀進s p 2,而後,s e e k g ()會到文件開始處執行,整個過程可經過由r d b u f   ()產

生的s t r e a m b u f指針打印出來。最後,s e e k g ()的重載版與streampos sp2一塊兒使用,移到先前
的位置,文件的最後部分被打印出來。

創建讀/寫文件

    既然瞭解s t r e a m b u f並知道怎樣查找,咱們會明白怎樣創建一個既能讀又能寫文件的流對象。
下面的代碼首先創建一個有標誌的 i f s t r e a m,它既是一個輸入文件又是一個輸出文件,編譯器
不會讓咱們向i f s t r e a m寫,所以,須要創建具備基本流緩衝區的輸出流(o s t r e a m ):

    咱們可能想知道向這樣一個對象寫內容時,會發生什麼。下面是一個例子:

----------------------- Page 96-----------------------

  96      C + +編程思想
                                                                      下載

    前五行把這個程序的源代碼拷貝進一個名叫 i o f i l e . o u t的文件,而後關閉這個文件。這給了
咱們一個可在其周圍操做的安全的文本文件。那麼前面說起的技術被用來創建兩個對象,這兩

個對象向同一個文件讀和寫。在c o u t < < i n 2 . r d b u f ( )裏,可看到「取」指針在文件的開始被初始
化。「放」指針被放到文件的末尾,這是因爲「Where does this end up? 」追加到這個文件裏。
然而,若是「放」指針移到 s e e k p       ()的開始處,全部插入的文本覆蓋現成的文本。當「取」
指針用s e e k g ()移回到開始處時,兩次寫結果都可見到,並且文件被打印出來。固然,當o u t 2
脫離範圍時,析構函數被調用,這個文件被自動保存和關閉。

6.6  strstreams

    第三個標準型輸入輸出流可直接與內存而不是一個文件或標準輸出一塊兒工做。它容許咱們
用一樣的讀函數和格式函數去操做內存裏的字節。舊式的計算機,內存是指內核,因此,這種
功能有時叫內核格式。
    s t r s t r e a m的類名字回顯文件流的類名字。如想創建一個從中提取字符的 s t r s t r e a m,咱們就
創建一個i s t r s t r e a m。如想把字符放進一個s t r s t r e a m,咱們就創建一個o s t r s t r e a m 。

    串流與內存一塊兒工做,因此咱們必須處理這樣的問題:內存來自哪裏又去哪裏。這個問題
並不複雜到使人懼怕的程度,但咱們必須弄懂並注意它。從s t r s t r e a m s中獲得的好處遠大於這一
微小的不利。

6.6.1 爲用戶分配的存儲

    由用戶負責分配存儲空間,剛好是弄懂這個問題的最容易的途徑。用 i s t r s t r e a m s,這是惟
一容許的方法。下面是兩個構造函數:

    第一個構造函數取一個指向零終止符數組的指針;咱們能夠提取字節直到零爲止。第二個
構造函數另外還須要這個數組的大小,這個數組沒必要是零終止的。咱們能夠一直提取字節到
b u f [ s i z e ] ,而無論是否遇到一個零。
    當移交數組地址給一個i s t r s t r e a m構造函數時,這個數組必須已經填充了咱們要提取的而且
假定格式化成某種其餘數據類型的字符。下面是一個簡單的例子 [ 1 ] :

  [1] 注意文件名必須被截斷,以處理D O S對文件名的限制。若是咱們的系統支持長文件名,咱們必須調整頭文件名

     (不然只拷貝頭文件)。

----------------------- Page 97-----------------------

                                                第6章 輸入輸出流介紹           97
 下載

    比起標準C庫裏a t o f ( )、a t o i ( )等等這樣的函數,能夠看到,這是把字符串轉換成類型值更加
靈活和更加通常的途徑。
    編譯器在下面的代碼裏處理這個串的靜態存儲分配:

    istrstream s("1.414 47 This is a test");

咱們還能夠移交一個在棧或堆裏分配的有零終止符的指針給它。
    在s > > i > > f裏,第一個數被提取到i,第二數被提取到f,這不是「字符的第一個空白分隔符」,
這是由於它依賴於它正被提取的數據類型。例如,若是這個串被替換成「1.414 47 This is a test」,
那麼i取值1,這是由於輸入程序停留在小數點上。而後 f取0 . 4 1 4 。把一浮點數分紅整數部分和

小數部分,是有用的。不然看起來彷佛是一個錯誤。
    就像已猜想的同樣,b u f 2沒有取串的其他部分,僅取空白分隔符的下一個字。通常地,使
用輸入輸出流提取符最好是當咱們僅知道輸入流裏的確切的數據序列而且正在轉換某些類型而
不是一字符串。然而,若是咱們想當即提取這個串的其他部分並把它送給另外一個輸入輸出流,
咱們能夠使用顯示過的r d b u f     ()。

    輸出s t r s t r e a m s也容許咱們提供本身的存儲空間。在這種狀況下,字節在內存中被格式化。
相應的構造函數是:

    o s t r s t r e a m : : o s t r s t r e a m ( c h a r * , i n t , i n t = i o s : : o u t );

    第一個參數是預分配的緩衝區,在那裏字符將結束,第二個參數是緩衝區的大小,第三個
參數是模式。若是模式是缺省值,字符從緩衝區的開始地址格式化。若是模式是                                  i o s : : a t e或
i o s : : a p p (效果同樣),字符緩衝區被假定已經包含了一個零終止字符串,而任何新的字符只能
從零終止符開始添加。
    第二個構造函數參數表示數組大小,且被對象用來保證它不覆蓋數組尾。如咱們已填滿數

組而又想添加更多的字節,這些字節是加不進去的。
    關於o s t r s t r e a m s,記住重要的是:沒有爲咱們插入通常在字符數組末尾所須要的零終止符。
當咱們準備好零終止符時,用特別操縱算子e n d s。
    一旦已創建一個o s t r s t r e a m,就能夠插入咱們須要插入的任何東西,並且它將在內存緩衝
區裏完成格式化。下面是一個例子:

----------------------- Page 98-----------------------

  98      C + +編程思想
                                                                      下載

    這相似於前面i n t和f l o a t 的例子。咱們可能認爲取一行其他部分的邏輯方法是使用  r d b u f
( );這個固然能夠,但它很笨拙,由於全部包括回車的輸入一直被收集起來,直到用戶按
c o n t r o l - Z (在u n i x中c o n t r o l - D )代表輸入結束時才停下來。使用g e t l i n e ()所代表的方法,一直取
輸入直到用戶按下回車才停下來。這個輸入被取進 b u f ,b u f用來構造ostrstream os                。若是未提
供第三個參數i o s : : a p p,構造函數缺省地寫在b u f 的開頭,覆蓋剛被收集的行。然而,「追加」標

志使它把被格式化後的信息放在這個串的末尾。
    像其餘的輸出流同樣,能夠用日常的格式化工具發送字節到o s t r s t r e a m。區別是僅用e n d s在
末尾插入零。注意,e n d l是在流中插入一個新行,而不是插入零。
    如今信息在b u f裏格式化,可用c o u t < < b u f直接發送它。然而,也有可能用o s . r d b u f ( )發送它。
當咱們這樣作的時候,在s t r e a m b u f裏的「取」指針隨這字符被輸出而向前移動。正因如此,第

二次用到c o u t < < o s . r d b u f ( )時,什麼也沒有發生—「取」指針已經在末端。

6.6.2 自動存儲分配

    輸出s t r s t r e a m s (但不是i s t r s t r e a m s )是另外一種分配存儲空間的方法:它們本身完成這個操
做,咱們所作的是創建一個不帶構造函數參數的o s t r s t r e a m:

    ostrstream A ;

    如今,A關心它本身在堆中存儲空間的分配,能夠在 A 中想放多少字節就放多少字節,它
用完存儲空間,若有必要,它將移動存儲塊以分配更多的存儲空間。
    若是不知道須要多少空間,這是一個很好的解決辦法,由於它很靈活。格式化數據到
s t r s t r e a m,而後把它的s t r e a m b u f移給另外一個輸入輸出流。這樣作很完美:

    這是全部解決辦法中最好的。可是,若是要 A 的字符被格式化到內存物理地址,會發生什
麼呢?這是很容易作到的——只要調用s t r             ()成員函數便可:

    char* cp=A.str();

    還有一個問題,若是想在A 中放進更多的字符會發生什麼呢?若是咱們知道 A分配的存儲
空間足夠放進更多的字符,就行了,但那是不正確的。通常地,如給  A更多的字符,它將用

完存儲空間。一般A試圖在堆中分配更多的存儲空間,這常常須要移動存儲塊。可是流對象
剛剛把它的存儲塊的地址交給咱們,因此咱們不能很好地移動那個塊,由於咱們指望它處於
特定的位置。
    o s t r s t r e a m處理這個問題的方法是「凍結」它本身。只要不用 s t r      ()請求內部c h a r *,就可

----------------------- Page 99-----------------------

                                                第6章 輸入輸出流介紹          99
 下載

以儘量向串輸出流中追加字符。它將從堆中分配所需的存儲空間,當對象脫離做用域時,堆
存儲空間自動被釋放。

    然而,若是調用s t r    (),o s t r s t r e a m就「凍結」了,就不能再向給它添加字符,無需經過實
現來檢測錯誤。向凍結的o s t r s t r e a m添加字符致使未被定義的行爲。另外, o s t r s t r e a m再也不負責
清理存儲器。當咱們用s t r ( )請求c h a r *時,要負責清除存儲器。
    爲了避免讓內存泄漏,必須清理存儲器。有兩種清理辦法。較普通的辦法是直接釋放要處理
的內存。爲搞懂這個問題,咱們得預習一下C + +中兩個新的關鍵字:n e w和d e l e t e 。就像第1 2章

學到的同樣,這兩個關鍵字用得至關多,但目前可認爲它們是用來替代 C 中m a l l o c ( )和f r e e ( ) 的。
操做符n e w返回一個存儲塊,而d e l e t e釋放它。重要的是必須知道它們,由於實際上C + +中全部
內存分配是由n e w完成的。o s t r s t r e a m也是這樣的。若是內存是由n e w分配的,它必須由d e l e t e釋
放。因此,若是有一個ostrstream A,用s t r ( )取得c h a r *,清理存儲器的辦法是:

    delete A.str();

這能知足大部分須要,但還有另外一個不是很普通的釋放存儲器的辦法:解凍 o s t r s t r e a m,可通
過調用f r e e z e ( )來作。f r e e z e ( )是o s t r s t r e a m的s t r e a m b u f成員函數。f r e e z e有一個缺省參數,這個
缺省參數凍結這個流。用零參數對它解凍:

    A . r d b u f ( ) - > f r e e z e ( 0 );

當A脫離做用域時,存儲被從新分配,並且它的析構函數被調用。另外,可添加更多的字節給

A 。可是這可能引發存儲移動,因此最好不要用之前經過調用 s t r                   ()獲得的指針—在添加更
多的字符後,這個指針將不可靠。
    下面的例子測試在一個流被解凍後追加字符的能力:

    在放第一個串到s後,添加一個e n d s,因此這個串能用由s t r ( )產生的c h a r *打印出來。在這
個意義上,s被凍結了。爲了添更多的字節給 s,「放」指針必須後移一步,這樣下一個字符被
放到由e n d s插入的零的上面。(不然,這個串僅被打印到原來的零的上面)。這是由s e e k p                       ()
完成的。而後經過使用r d b u f ( )和調用f r e e z e ( 0 )取回基本s t r e a m b u f指針,s被解凍。在這個意義上,

----------------------- Page 100-----------------------

  100        C + +編程思想
                                                                      下載

s就像它之前調用s t r ( )同樣:咱們能夠添加更多的字符,清理由析構函數自動完成。
    解凍一個o s t r s t r e a m並繼續添加字符是有可能的,但一般不這樣作。正常的狀況下,若是

咱們得到o s t r s t r e a m的c h a r *時想添加更多的字符,就創建一個新的o s t r s t r e a m,經過使用r d b u f ( )
把舊的流灌到這個新的流中去,並繼續添加新的字符到新的o s t r s t r e a m中。
    1. 檢驗移動
    若是咱們仍不相信調用s t r ( )就得對o s t r s t r e a m的存儲空間負責,下面的例子說明存儲定位被
移動了,於是由s t r    ()返回的舊指針是無效的:

    在插入一個串到s中並用s t r       ()捕獲c h a r *後,這個串被解凍並且有足夠的新字節被插入,
真正確保了內存被從新分配且大多數被移動。在打印出舊的和新的 c h a r *值後,存儲明確地由
d e l e t e釋放,由於第二次調用s t r ( )又凍結了這個串。
    爲了打印出它們指向的串的地址而不是這個串,必須把 c h a r *指派爲v o i d * 。c h a r *的操做符

「< <」打印出它正指向的串,而對應於v o i d * 的操做符「< < 」打印出指針的十六進制表示值。
    有趣的是應注意到:在調用s t r ( )前,如不插一個串到s中,結果則爲0 。這意味着直到第一
次插入字節到o s t r s t r e a m時,存儲才被從新分配。
    2. 一個更好的方法
    標準C++ string類以及與其聯繫在一塊兒工做的s t r i n g s t r e a m類,對解決這個問題作了很大的改進。

                                                                       [ 1 ]
使用這兩個類代替c h a r *和s t r s t r e a m時,沒必要擔憂負責存儲空間的事—一切都被自動清理  。

6.7  輸出流格式化

    所有的努力以及全部這些不一樣類型的輸入輸出流的目的,是讓咱們容易地從一個地方到另
一個地方移動並翻譯字節。若是不能用p r i n t f ( )族函數完成全部的格式化,這固然是沒有用的。
咱們將學到輸入輸出流全部可用的輸出格式化函數,獲得所須要的那些字節。
    輸入輸出流的格式化函數開始有點令人混淆,這是由於常常有一種以上的方式控制格式化:

經過成員函數控制,也能夠經過操縱算子來控制。更容易混淆的是,有一個派生的成員函數設
置控制格式化的狀態標誌,如左對齊或右對齊,對十六進制表示法是否用大寫字母,是否老是
用十進制數表示浮點數的值等等。另外一方面,這裏有特別的成員函數用以設置填充字符、域寬

  [1] 這本書中,這些類僅是草稿,不能在編譯器上實現。

----------------------- Page 101-----------------------

                                              第6章 輸入輸出流介紹           101
 下載

和精度,並讀出它們的值。

6.7.1 內部格式化數據

    i o s類(在頭文件I O S T R E A M . H 中可看到)包含數據成員以存儲屬於那個流的全部格式化

數據。有些數據的值有必定範圍並被儲存在變量裏:浮點精度、輸出域寬度和用來填充輸出
(一般是一空格)的字符。格式化的其他部分是由標誌所決定的,這些標誌一般被連在一塊兒以
節省空間,並一塊兒被指定爲格式標誌。能夠用 i o s : : f l a g s ( )成員函數發現格式化標誌的值,這個
成員函數沒帶參數並返回一個包含當前格式化標誌的 long(typedefed to fmtflags)型值。函數的
全部其他部分使格式化標誌發生改變並返回格式化標誌先前的值。

    有時第一個函數迫使全部的標誌改變。更多的是每次用剩下的三個函數來改變一個標誌。

    s e t f ( )的用法看來更加使人混淆:要想知道用哪一個重載版本,必須知道正要改變的是哪類標
志。這裏有兩類標誌:一類是簡單的 o n或o ff ,一類是與其餘標誌在一個組裏工做的標誌。
o n / o ff標誌理解起來最簡單,由於咱們可用s e t f ( f m t f l a g s )將它們變爲o n,用u n s e t f ( f m t f l a g s )將它
們變爲o ff。這些標誌是:

    o n / o ff標誌                               做            用

    i o s : : s k i p w s          跳過空白字符(對於輸入這是缺省值)

    i o s : : s h o w b a s e      打印一個整數值時標明數值基數(十進制,八進制或十六進制),

                                 使用的格式能被C + +編譯器讀出

    i o s : : s h o w p o i n t    代表浮點數的小數點和後面的零

    i o s : : u p p e r c a s e    顯示十六進制數值的大寫字母A - F和科學記數法中的大寫字母E

    i o s : : s h o w p o s        顯示加號(+ )表明正值

    i o s : : u n i t b u f        「設備緩衝區」;在每次插入操做後,這個流被刷新

    i o s : : s t d i o            使這個流與C標準I / O系統同步

    例如,爲 c o u t 顯示加號,可寫成 c o u t . s e t f ( i o s : : s h o w p o s ) ;中止顯示加號,可寫成
c o u t . u n s e t f ( i o s : : s h o w p o s ) 。應該解釋一下最後兩個標誌。當一個字符一旦被插進一個輸出流,
若是想確信它是一個輸出時,可啓用緩衝設備。也能夠不用緩衝輸出,但用緩衝設備會更好。

    有一個程序用了輸入輸出流和C標準I / O庫(用C庫不是不可能的),標誌i o s : : s t d i o就被採用。
若是發現輸入輸出流的輸出和p r i n t f ( )輸出出現了錯誤的次序,就要設置這個標誌。
    1. 格式域
    第二類格式化標誌在一個組裏工做,一次只能用這些標誌中的一種,就像舊式的汽車收音
機按鈕同樣—按下一個按鈕,其他的彈出。惋惜的是,這是不能自動發生的,咱們必須注意

正在設置的是什麼標誌,這樣就不會偶然調用錯誤的 s e t f                 ()函數。例如,每個數字基數有
一個標誌:十六進制,十進制和八進制。這些標誌一塊兒被指定爲 i o s : : b a s e f i e l d 。若是i o s::d e c
標誌被設置而調用s e t f    (i o s : : h e x ),將設置i o s : : h e x標誌,但不會清除i o s : : d e c位,結果出現未被
定義的方式。適當的方法是像這樣調用 s e t f ( )的第二種形式:s e t f ( i o s : : h e x , i o s : : b a s e f i e l d )。這個
函數首先清除i o s : : b a s e f i e l d裏的全部位,而後設置i o s : : h e x。這樣,s e t f ( )的這個形式保證不管什

麼時候設置一個標誌,這個組裏的其餘標誌都會「彈出」。固然,全部這些操做由h e x ( )操縱算
子自動完成。因此沒必要了解這個類實現的內部細節,甚至沒必要關心它是一個二進制標誌的設置。

----------------------- Page 102-----------------------

   102          C + +編程思想
                                                                                        下載

之後將會看到有一個與s e t ( )有提供一樣的功能操縱算子。
     下面是標誌組和它們的做用:

     i s o : : b a s e f i e l d                            做             用

     i o s : : d e c                           十進制格式整數值(十進制)(缺省基數)

     i o s : : h e x                           十六進制格式整數值(十六進制)

     i o s : : o c t                           八進制格式整數值(八進制)

     i o s : : f l o a t f i e l d                          做             用

     i o s : : s c i e n t i f i c             科學記數法表示浮點數值,精度域指小數點後面的數字數目

     i o s : : f i x e d                       定點格式表示浮點數值,精度域指小數點後面的數字數目

    「a u t o m a t i c」(Neither bit is set)    精度域指整個有效數字的數目

     i o s : : a d j u s t f i e l d                         做            用

     i o s : : l e f t                         左對齊,向右邊填充字符

     i o s : : r i g h t                       右對齊,向左邊填充字符

     i o s : : i n t e r n a l                 在任何引導符或基數指示符以後但在值以前添加填充字符

     2. 域寬、填充字符和精度
     一些內部變量,用於控制輸出域的寬度,或當數據沒有填入時,用做填充的字符,或控制
打印浮點數的精度。它被與變量同名字的成員函數讀和寫。

     f u n c t i o n                                         做            用

     int ios::width()                          讀當前寬度(缺省值爲0 ),用於插入和提取

     int ios::width(int n)                     設置寬度,返回之前的寬度

     int ios::fill()                           讀當前填充字符(缺省值爲空格)

     int ios::fill(int n)                      設置填充字符,返回之前的填充字符

     int ios::precision()                      讀當前浮點數精度(缺省值爲6 )

     int ios::precision(int n)                 設置浮點精度,返回之前的精度;「精度」含義見                        i o s : :

                                             floatfield表

     填充和精度值是至關直觀的,但寬度值須要一些解釋。當寬度爲0時,插入一個值將產生代
表這個值所需字符的最小數。一個正值的寬度意味着插入一個值將產生至少與寬度同樣多的字符。
假如值小於字符寬度,填充字符用來填這個域。然而,這個值決不被截斷。因此,若是打印1 2 3
而寬度爲2,咱們仍將獲得1 2 3。域寬標識了字符的最小數目。沒有標識字符最大數目的辦法。
     寬度也是明顯不一樣的,由於每一個插入符或提取符可能受到它的值的影響,它被每一個插入符

或提取符從新設置爲0 。它不是一個真正的靜態變量,而是插入符和提取符的一個隱含參數。
如咱們想有一個恆定的寬度,得在每個插入或提取它以後調用w i d t h ( ) 。

6.7.2  例子

     爲了確信懂得怎樣調用之前討論過的全部函數,下面舉一個所有調用這些函數的例子:

----------------------- Page 103-----------------------

                                               第6章 輸入輸出流介紹                 103
下載

----------------------- Page 104-----------------------

       104   C + +編程思想
                                                                       下載

    這個例子使用技巧創建一個跟蹤文件,這樣咱們就能監控所發生的事情。宏D                              (a )使用預

處理器「s t r i n g i z i n g 」把a轉變爲一個串打印出來。而後,宏D       (a )反覆處理a,使a產生做用。
宏發送全部的信息到一個叫做T的文件中,這就是那個跟蹤文件。輸出是:

----------------------- Page 105-----------------------

                                               第6章 輸入輸出流介紹                 105
下載

----------------------- Page 106-----------------------

   106            C + +編程思想
                                                                                                  下載

     研究這個輸出會使咱們清楚地理解輸入輸出流格式化成員函數。

6.8    格式化操縱算子

     就像咱們在前面的例子中看到的同樣,調用成員函數有點乏味。爲使讀和寫更容易, C + +

提供了一套操縱算子以起到與成員函數一樣的做用。

     提供在I O S T R E A M . H裏的是不帶參數的操縱算子。這些操縱算子包括 d e c 、o c t和h e x 。它

們各自更簡明扼要地完成與 s e t f ( i o s : : d e c , i o s : : b a s e f i e l d ) 、s e t f ( i o s : : o c t , i o s : : b a s e f i e l d )和
s e t f ( i o s : : h e x , i o s : : b a s e f i e l d )一樣的任務。IOSTREAM.H  [ 1 ] 還包括w s 、e n d l、e n d s和f l u s h 以及如

下所示的其餘操縱算子:

     操縱算子                                                              做      用

     s h o w b a s e                                在打印一整數值時,標明數字基數(十進制,八進制和十

     n o s h o w b a s e                           六進制);所用的格式能被C + +編譯器讀出

     s h o w p o s                                   顯示正值符號加(+ )

     n o s h o w p o s

     u p p e r c a s e                               顯示錶明十六進制值的大寫字母A - F 以及科學記數法中的E

     n o u p p e r c a s e

     s h o w p o i n t                               代表浮點數值的小數點和後面的零

     n o s h o w p o i n t

     s k i p w s                                     跳過輸入中的空白字符

     n o s h i p w s

     l e f t                                         左對齊,右填充

     r i g h t                                       右對齊,左填充

     i n t e r n a l                                 在引導符或基數指示符和值之間填充

     s c i e n t i f i c                             使用科學記數法

     f i x e d                                       s e t p r e c i s i o n ( )或i o s : : p r e c i s i o n ( )設置小數點後面的位數

帶參數的操縱算子

     若是正在使用帶參數的操縱算子,必須也包含頭文件I O M A N I P. H。這包含了解決創建帶參
數操縱算子所遇到的通常問題的代碼。另外,它有六個預約義的操縱算子:

     操縱算子                                                               做     用

     setiosflags(fmtflags n)                         設置由n指定的格式標誌;設置一直起做用直到下一個變化

                                                   爲止,像i o s : : s e t f ( )同樣

     resetiosflags(fmtflags n)                       清除由n指定的格式標誌。設置一直起做用直到下一個變化

                                                   爲止,像i o s : : u n s e t f ( )同樣

     setbase(base n)                                 把基數改爲n ,這裏n取1 0、8或1 6  (任何別的值結果爲0 )。

                                                   若是n是0 ,輸出基數爲1 0,但輸入使用C約定:1 0是1 0,0 1 0

                                                   是8而0 x f是1 5。咱們仍是使用d e c、o c t和h e x輸出爲好

     setfill(char n)                                 把填充字符改爲n ,像i o s ::f i l l   ()同樣

     setprecision(int n)                             把精度改爲n ,像i o s ::p r e c i s i o n ()同樣

     setw(int n)                                     把域寬改爲n ,像i o s ::w i d t h ()同樣

     若是正在使用不少的插入符,咱們會看到這是怎樣清理的。做爲一個例子,下面是用操縱

   [1] 這些僅在修改庫中出現,老的輸入輸出流實現中沒包括它們。

----------------------- Page 107-----------------------

                                               第6章 輸入輸出流介紹                 107
 下載

算子對前面程序的重寫(宏已被去掉以使其更容易閱讀):

----------------------- Page 108-----------------------

  108        C + +編程思想
                                                                      下載

    許多的多重語句已被精簡成單個的鏈插入。注意調用 s e t i o s f l a g s ( )和r e s e t i o s f l a g s ( ) ,在這兩
個函數裏,標誌被按「位O R」運算成一個。在前面的例子裏,這個工做是由s e t f ( )和u n s e t f ( )完
成的。

6.9  創建操縱算子

    (注意:這部分包含一些之後章節中才介紹的內容)有時,咱們可能想創建本身的操縱算
子,這是至關簡單的。一個像e n d l這樣的不帶參數的操縱算子只是一個函數,這個函數把一個

o s t r e a m引用做爲它的參數。(引用是一種不一樣的參數傳送方式,在第1 0章中討論)對e n d l的聲
明是:

    ostream& endl(ostream&) ;

    如今,當咱們寫:

    cout<<"howdy" <<endl ;

e n d l產生函數的地址。這樣,編譯器問:「有能被我調用的把函數的地址做爲它的參數的函數
嗎?」確實有一個這樣的函數,是在I O S T R E A M . H裏預先定義的函數;它被稱做「應用算子」。
這個應用算子調用這個函數,把o s t r e a m對象做爲一個參數傳送給這個函數。
    沒必要知道應用算子怎樣創建咱們本身的操縱算子;咱們只要知道應用算子存在就好了。下
面是創建一個操縱算子的例子,這個操縱算子叫n l,它產生一個換行而不刷新這個流:

----------------------- Page 109-----------------------

                                                  第6章 輸入輸出流介紹             109
 下載

    表達式

    o s < < ' / n ';

                                       [ 1 ]
調用一個返回o s的函數,它是從n l中返回的  。
    人們常常認爲,如上所示的n l 比使用e n d l要好,由於後者老是清空輸出流,這可能引發執
行故障。

效用算子

    正如咱們已看到的,零參數操縱算子至關容易創建。可是若是創建帶參數的操縱算子又怎
樣呢?要這樣作,輸入輸出流有一個至關麻煩且易混淆的方法。可是 Jerry Schwarz ,輸入輸出
流庫的創建者,提出一個方案 [2]            ,他稱之爲效用算子。一個效用算子是一個簡單的類,這個類

的構造函數與工做在這個類裏的一個重載操做符「< < 」一塊兒執行想要的操做。下面是一個有兩
個效用算子的例子。第一個輸出是一個被截斷的字符串,第二個打印出一個二進制數(定義一

個重載操做符「< < 」的過程將在第11章討論):

  [1] 把n 1放入頭文件以前,應該把它變成內聯函數(見第8章)。

  [2] 在私人談話中。

----------------------- Page 110-----------------------

  110        C + +編程思想
                                                                       下載

    f i x w的構造函數產生c h a r *參數的一個縮短的副本,析構函數釋放產生這個副本的內存。重

載操做符「< < 」取第二個參數的內容,即f i x w對象,並把它插入第一個參數 o s t r e a m,而後返
回o s t r e a m,因此它能夠用在一個連接表達式裏。下面的表達式裏用f i x w時:

    cout <<fixw(string,i)<<endl ;

一個臨時對象經過調用f i x w構造函數被創建了,那個臨時對象被傳送給操做符「 < < 」。帶參數
的操縱算子產生做用了。

    bin 效用算子依賴於這樣的事實:對一個無符號數向右移位,把零移成高位。 U L O N G _ M
A X (最大的長型值unsigned long ,來自標準包含文件L I M I T S . H )用來產生一個帶高位設置的

值,這個值移過正被討論的數(經過移位),屏蔽每一位。

    起初,這個技術上的問題是:一旦爲c h a r *創建一個叫f i x w的類,或爲unsign long創建一個
叫b i n的類,沒有別的人能爲他們的類型創建一個不一樣的 f i x w類或b i n類。然而,有了名字空間

(在第9章),這個問題被解決了。

----------------------- Page 111-----------------------

                                              第6章 輸入輸出流介紹          111
 下載

6.10  輸入輸出流實例

    本節,將看到怎樣處理本章學到的全部知識的一些例子。雖然存在能操縱字節的不少工具
(像u n i x中的s e d和a w k這樣的流編輯器多是廣爲人知的,而文本編輯器也屬於此類),但它們
通常都有一些限制。s e d和a w k可能比較慢,並且僅能處理向前序列裏的行。文本編輯器一般需
要人的交互做用或至少要學一種專有的宏語言。用輸入輸出流寫的程序就沒有任何此類限制:

這些程序運行快,可移植並且很靈活。輸入輸出流是工具箱裏的很是有用的一個工具。

6.10.1 代碼生成

    第一個例子涉及程序的產生,比較巧的是,這個程序也符合本書的格式。在開發代碼時保
證供快速和連貫性。第一個程序創建一個文件保存 m a i n                 ()(假定它帶有非命令行參數而且使
用輸入輸出流庫):

    這個文件被打開,使用i o s : : n o r e p l a c e,以保證不會偶然地覆蓋一個現成文件。而後用命令
行中的那個參數來創建一個 i s t r s t r e a m 。這樣,字符每次一個地被提取。有了標準  C庫宏
t o u p p e r ( ) ,這些字符可被轉變成大寫字母。這個變量返回一個i n t,這樣,它必須很明確地轉換

給c h a r。此名字用在頭一行裏,後跟產生文件的餘項。
    1. 維護類庫資源
    第二個例子執行一個更復雜和更有用的任務。總之,創建一個類時,要想一想庫術語,並且

----------------------- Page 112-----------------------

  112        C + +編程思想
                                                                      下載

要在類聲明中建立一個頭文件N A M E . H和一個名叫N A M E . C P P 的文件—成員函數在這個文件
裏實現。這些文件有必定的要求:一個特別的代碼標準(這兒顯示的程序是用這本書裏的代碼

格式),並且這個頭文件的聲明通常被一些預處理器說明所包圍,目的是避免類的多重說明。
(多重說明使編譯器混淆—編譯器不知道咱們要使用哪一個類。這些類多是不一樣的,因此編
譯器發出一個出錯信息)。
    這個例子創建一對新的頭—實現文件,否則就修改現存的文件。若是這對文件已存在,
它檢查這對文件並潛在地修改這對文件,可是若是這對文件不存在,它用合適的格式創建這對

文件:

----------------------- Page 113-----------------------

                                               第6章 輸入輸出流介紹                 113
下載

----------------------- Page 114-----------------------

     114   C + +編程思想
                                                                        下載

----------------------- Page 115-----------------------

                                              第6章 輸入輸出流介紹          115
 下載

    這個例子須要不一樣緩衝區中的串格式化。不是創建單個命名的緩衝區和  o s t r s t r e a m對象,
而是在enum bufs緩衝區中創建 一組名字。於是要創建兩個數組:一個字符緩衝區數組和一個

從字符緩衝區裏創建的o s t r s t r e a m對象數組。注意在c h a r緩衝區b 的二維數組定義裏,c h a r數組
的數目是由b u f n u m決定的,b u f n u m是b u f s 中的最後一個枚舉常量。當創建一個枚舉變量時,編
譯器賦一個整數值給全部那些標明從零開始的 e n u m,因此b u f n u m的惟一目的是成爲統計b u f 中
枚舉常量數目的計數器。b 中每個串的長度是S Z。
    枚舉變量裏的名字是: b a s e ,即大寫字母的不帶擴展名的基文件名; h e a d e r ,即頭文件

名;i m p l e m e n t,即實現文件名(C P P );H l i n e 1 ,即頭文件第一行框架;guard1    、g u a r d 2和
g u a r d 3 ,即頭文件裏的「保護」行(阻止多重包含); C P P l i n e l,即C P P文件的第一行框架;
i n c l u d e,即包含頭文件的C P P文件的行。
    o s a r r a y是一個經過集合初始化和自動計數創建起來的 o s t r s t r e a m對象數組。固然,這是帶
兩個參數(緩衝區地址和大小)形式的 o s t r s t r e a m構造函數,因此構造函數調用必須相應地建

立在集合初始化表裏。利用b u f s枚舉常量,b 的適當數組元素結合到相應的o s a r r a y對象。一旦
數組被創建,利用枚舉常量就可選擇數組裏的對象,做用是填充相應的 b元素。咱們可看到,
每一個串是怎樣創建在o s t r s t r e a m數組定義後面的行裏的。
    一旦串被創建,程序試圖打開頭文件和 C P P文件的現行版本做爲i f s t r e a m s 。若是用操做符
「!」測試對象而這個文件不存在,測試將失敗。若是頭文件或實現文件不存在,利用之前建

立的文本的適當的行來創建它。
    若是文件確實存在,那麼這些文件後面跟着適當的格式,這是有保證的。在這兩種狀況下,
一個s t r s t r e a m被創建並且整個文件被讀進;而後第一行被讀出並被檢查,看看這一行是否包含
一個「/ /:」和文件名以確信它跟在格式的後面。這是由標準 C庫函數s t r s t r ( )完成的。若是第一
行不符合,較早時創建的內容被插進一個已建好的 o s t r s t r e a m中,目的是保存被編輯的那個文

件。
    在頭文件裏,整個文件被搜索(再次使用 s t r s t r ( )函數)以確保它包含三個「保護」行;如
果沒有包含這三行,要插入它們。檢查實現文件,看看包含頭文件那一行是否存在(雖然編譯
器有效地保證它的存在)。
    在兩種狀況下,比較原始文件(在它的 s t r s t r e a m裏)和被編輯的文件(在 o s t r s t r e a m裏),

看看它們是否有變化。若是有變化,現存文件被關閉,一個新的 o f s t r e a m對象被創建,目的是
覆蓋這個現存文件。一個特別的變化標誌被添加到開始處以後, o s t r s t r e a m被輸出到這個文件,
因此可以使用一文本搜索程序,經過檢查快速發現產生另外變化的文件。
    2. 檢測編譯器錯誤
    這本書裏的全部代碼都已經設計好了,沒有編譯錯誤。任何產生編譯錯誤的代碼行都由特

別註釋順序符「/ / !」註解出來。下面的程序將移去這些特別註解並在原處添加一個已編號的
註解。這樣,運行編譯器時,它應該產生出錯信息。當編譯全部文件時,應該看到全部的號碼。
它也能夠添加修改過的行到一個特別文件裏,這樣可容易定位任何不產生錯誤的行:

----------------------- Page 116-----------------------

     116   C + +編程思想
                                                                        下載

----------------------- Page 117-----------------------

                                              第6章 輸入輸出流介紹          117
 下載

    這個m a k e r指針可被咱們的一種選擇所代替。
    每一個文件每次讀出一行,在每一行中搜索出如今行開頭的 m a k e r 。這個行被修改並放進錯
誤行表、放進strstream edited裏。當整個文件被處理完時,它被關閉(經過到達範圍的末端),

從新做爲一個輸出文件打開,e d i t e d被灌進這個文件。注意計數器被保存在內部文件裏。因此
下一次調用這個程序時,繼續由計數器按順序計數。

6.10.2 一個簡單的數據記錄

    這個例子顯示了記錄數據到磁盤上然後檢索數據並做處理的一種途徑。這個例子的意思是
產生一個海洋各處溫度—深度的曲線圖。爲保存數據,要用到一個類:

----------------------- Page 118-----------------------

       118   C + +編程思想
                                                                       下載

    這個存取函數爲每一個數據成員提供受控制的讀和寫。p r i n t f ( )函數用一個可讀的形式格式化
d a t a p o i n t到一個o s t r e a m對象中(p r i n t f ( )的參數),下面是一個定義文件:

----------------------- Page 119-----------------------

                                              第6章 輸入輸出流介紹          119
 下載

    在p r i n t f ( ) 函數裏,調用s e t f ( )引發浮點數以定點精度的形式輸出,而精度函數p r e c i s i o n ( )設

置4位小數。
    域裏的數據缺省向右對齊,時間信息由時、分和秒各兩位數字組成,這樣。在每種狀況下,
寬度由s e t w ( )函數設置爲2 。(記住域寬的任何變化影響下次輸出操做,因此s e t w ( )函數必須配給
每一個輸出)。可是首先若是值小於1 0,填充字符被設置爲「0 」,在值的左邊放一個零。之後它
又被設置爲空格。

    緯度和經度是零終止符域,它保存度(這裏‘*’表示度)、分(`)和秒(` ` )的信息。如
咱們願意的話,固然能設計出一個更有效的存儲方案。
    1. 生成測試數據
    下面是一個程序,這個程序(用 w r i t e ( ) )創建一個二進制形式的測試數據文件 ,而且用

----------------------- Page 120-----------------------

  120        C + +編程思想
                                                                      下載

d a t a p o i n t : : p r i n t ( )創建另外一個A S C Ⅱ形式的文件。咱們也能夠把它打印到屏幕上,可是在文件形
式裏更容易檢測:

    文件D ATA . T X T做爲一個A S C Ⅱ文件用一般的方法創建了。而D ATA . B I N有標誌i o s : : b i n a r y,
用以告訴構造函數把它設置成二進制文件。
    標準C庫函數t i m e ( ) ,當帶一個零參數調用它時,返回當前時間爲一個t i m e _ t值,它是自1 9 7 0
年1月1日0 0:0 0:00 GMT後的秒數。當前時間是用標準C庫函數s c r a n d ( )爲隨機數產生器設置
種子的最方便的方法,這裏就是這樣作的。

    有時候,存儲時間的更方便的方法是一個t m結構,它含有時間和日期的每一個元素,而時間
和日期被分紅以下所示的構成成分:

----------------------- Page 121-----------------------

                                              第6章 輸入輸出流介紹          121
 下載

    爲把以秒計數的時間轉換成t m格式的本地時間,利用標準C庫l o c a l t i m e ( )函數,它取時間
秒數並返回一個指針指向t m中的結果。然而,t m是l o c a l t i m e ( )函數裏面的一個靜態結構,每當

l o c a l t i m e ( )被調用時,l o c a l t i m e ( )被重寫。爲把內容拷進d a t a p o i n t中的tm struct 中,咱們可能認
爲必須逐個拷貝每一個元素。然而,咱們所必須作的是一個結構分配,其他的由編譯器來作,這
意味着右邊的必定是一個結構,不是一個指針,因此, l o c a l t i m e ( )的結果被間接引用。想要得
到的結果是:

    d . Ti m e = * l o c a l t i m e ( & t i m e r ) ;

在這以後,t i m e r增長了5 5秒,與讀之間有一個有趣的間隔。
    使用的緯度和經度是定點值,這個值代表在某一位置的一組讀出值。深度和溫度是由標準
C庫r a n d ( )函數產生的,這個函數返回零和常數R A N D _ M A X之間的一個僞隨機數。爲把這個數

放進但願獲得的範圍裏,使用模操做符 %和範圍的上界。這些數是整型的;爲添加小數部分,
對函數r a n d ( )作第二次調用,獲得的值加一之後取倒數(加一是爲了防止除數爲零的錯誤)。
    事實上,文件D ATA . B I N做爲數據容器在程序裏使用,即便這個容器存在於磁盤上而不存
在R A M 中。爲了以二進制形式發送這些數據到磁盤上,w r i t e ( )被使用。第一個參數是源塊的開
始地址—注意它必須指派爲unsigned char* ,由於這正是這個函數所指望的。第二參數表明

要寫的字節數,就是d a t a p o i n t對象的大小。因爲沒有指針包含在d a t a p o i n t裏,所以向盤中寫入
對象時不會出現問題。若是對象更復雜,咱們必須實現一個順序化方案(大多數類庫廠家已在
裏面創建了某種順序化)。
    2. 檢驗和觀察數據
    爲檢查以二進制格式存儲的數據的有效性,數據從盤中讀出並被放進文本文件

D ATA 2 . T X T 中,因此這個文件可與D ATA . T X T 比較以供檢驗。在下面的程序裏,咱們會看到
這個數據恢復是多麼簡單。在測試文件被創建後,記錄在用戶命令上被讀出。

----------------------- Page 122-----------------------

     122   C + +編程思想
                                                                        下載

  ifstream bindata 是從文件D ATA . B I N 中產生並做爲一個二進制文件創建起來的,帶有

----------------------- Page 123-----------------------

                                             第6章 輸入輸出流介紹           123
 下載

i o s : : n o c r e a t e標誌。若是這個文件不存在,這個標誌被設置致使函數 a s s e r t ( )失敗。函數r e a d ( )說
明讀出一個單個記錄並把它直接放進datapoint d 。(假如d a t a p o i n t包含指針,這將產生無心義的

指針值)。到文件尾時,函數r e a d ( ) 的做用是設置b i n d a t a的f a i l b i t。它將致使w h i l e語句失敗。然
而,在這一點上,不能移回「取」指針並讀更多的記錄,由於流的狀態不容許進一步讀出。所
以c l e a r ( )函數被調用,從新設置f a i l b i t。
    一旦記錄從盤裏讀進,咱們就能夠作咱們想作的任何事情,如執行計算或做圖。這裏,它
被顯示出來以進一步練習關於輸入輸出流格式方面的知識。

    程序的其他部分顯示用戶選擇的一個記錄號(由 r e c n u m表明)。像之前同樣,精度是固定
在4個小數的地方。但這時都是左對齊的。
    這個輸出的格式看來與之前不一樣:

    爲確信標號和數據欄縱向對齊,利用s e t w ( ),標號被放入與欄一樣的寬度域內。經過設置
填字符‘-’,設置但願獲得的行寬並且輸出單個的‘-’,這樣,行間隔符產生了。

    若是r e a d ( )失敗,咱們將在e l s e部分結束,這部分告訴用戶記錄數是無效的。而後,因爲f a i l b i t
被設置,必須調用c l e a r ( )從新設置它。這樣,下一個r e a d ( )是成功的(假定它在正確的範圍內)。
    固然,也能夠打開二進制數據文件來讀和寫。這樣能夠檢索記錄,修改它們並把它們寫回
到一樣的位置,創建了f l a t - f i l e數據庫管理系統。就在我第一次作程序設計工做時,我也不得不
創建乏味的文件數據庫管理系統D B M S——在A p p l e Ⅱ上用B A S I C 。它花費了幾個月時間,然

而,如今這樣作只需幾分鐘。固然,如今使用一個封裝的 D B M S會更有意義,可是用C + +和輸
入輸出流,仍可作在實驗室裏須要作的全部低級操做。

6.11  小結

    這一章給出了關於輸入輸出流類庫的一個至關完整的介紹。極可能這就是用輸入輸出流建

立程序所必需的。(在之後的章節裏,咱們會看到向咱們本身的類裏添加輸入輸出流功能的簡
單例子。)然而,應該知道,輸入輸出流還有一些不常使用的其餘性能,可經過查看輸入輸出
流頭文件和讀咱們本身編譯器中的關於輸入輸出流的文檔來發現這些性能。

6.12  練習

    1) 經過建立一個叫in      的i f s t r e a m對象來打開一個文件。建立一個叫o s的o s t r s t r e a m對象,並

經過r d b u f ( )成員函數把整個內容讀進o s t r s t r e a m 。用s t r ( )函數取出o s的c h a r *地址,並利用標準C
toupper() 宏使文件裏每一個字符大寫。把結果寫到一新的文件中,並刪除由o s分配的內存。
    2)  建立一個能打開文件(命令行中的第一個參數)的程序,並從中搜索一組字中的任何一
個(命令行中其他的參數)。每次,讀入一行輸入並打印出與之匹配的行(帶行數)。
    3) 寫一個在全部源代碼文件的開始處添加版權注意事項的程序。只需對練習 1)稍做修改。

    4) 用你最喜好的文本搜索程序 (如g r e p )輸出包含一特殊模式的全部文件名字 (僅是名字)。
重定向輸出到一個文件中。寫一個用那個文件裏的內容產生批處理文件的程序,這個批處理文
件對每一個由這個搜索程序找到的文件調用你的編輯器。

----------------------- Page 124-----------------------

                                                                      下載

                         第7章  常                  量

    常量概念的創建(由關鍵字c o n s t表示)容許程序員在變化和不變化之間劃一條界線。
    在C + +程序設計項目中提供了安全性和可控性。自從常量問世以來,它就有着不少不一樣的
做用。與此同時,它在C語言中的意義又不同。開始時,看起來容易混淆。在這一章裏,我
們將介紹何時、爲何和怎樣使用關鍵字 c o n s t 。最後,討論v o l a t i l e ,它是c o n s t 的「兄弟」
(由於它們都關係到是否變化,並且語法也同樣)。
    c o n s t的最初動機是取代預處理器# d e f i n e s進行值替代。今後它曾被用於指針、函數變量、
返回類型、類對象及其成員函數。全部這些用法都稍有區別,但它們在概念上是一致的,咱們
將在如下各節中說明這些用法。

7.1  值替代

    用C語言進行程序設計時,預處理器能夠不受限制地創建宏並用它來替代值。由於預處理
器只作文本替代,它既沒有類型檢查思想,也沒有類型檢查工具,因此預處理器的值替代會產
生一些微小的問題,這些問題在C + +中可經過使用c o n s t而避免。
    C語言中預處理器用值替代名字的典型用法是這樣的:

    #define BUFSIZE 100

    B U F S I Z E是一個名字,它不佔用存儲空間且能放在一個頭文件裏,目的是爲使用它的全部
編譯單元提供一個值。用值替代而不是用所謂的「難以想象的數」,這對於支持代碼維護是非
常重要的。若是代碼中用到難以想象的數,讀者不只不清楚這個數字來自哪裏,並且也不知道
它表明什麼,進而,當決定改變一個值時,程序員必須執行手動編輯,並且還不能跟蹤以保證
沒有漏掉其中的一個。
    多數狀況,B U F S I Z E 的工做方式與普通變量同樣但也不都如此;並且這種方法還存在一個
類型問題。這就會隱藏一些很難發現的錯誤。C + +用c o n s t把值替代帶進編譯器領域來解決這些
問題。能夠這樣寫:

    const bufsize=100 ;

或用更清楚的形式:

    const int bufsize=100 ;

這樣就能夠在任何編譯器須要知道這個值的地方使用b u f s i z e,同時它還能夠執行常量摺疊,也
就是說,編譯器在編譯時能夠經過必要的計算把一個複雜的常量表達式縮減成簡單的。這一點
在數組定義裏顯得尤爲重要:

    char buf[bufsize] ;

    咱們能夠爲全部的內部數據類型(c h a r、i n t、f l o a t和d o u b l e型)以及由它們所定義的變量
(也能夠是類的對象,這將在之後章節裏講到)使用限定符c o n s t 。咱們應該徹底用c o n s t取代
# d e f i n e 的值替代。

7.1.1 頭文件裏的const

    與使用# d e f i n e同樣,使用c o n s t必須把c o n s t定義放進頭文件裏。這樣,經過包含頭文件,

----------------------- Page 125-----------------------

                                                    第7章  常     量   125
 下載

可把c o n s t定義單獨放在一個地方並把它分配給一個編譯單元。 C + +中的c o n s t默認爲內部鏈接,
也就是說,c o n s t僅在c o n s t被定義過的文件裏纔是可見的,而在鏈接時不能被其餘編譯單元看

到。當定義一個常量(c o n s t )時,必須賦一個值給它,除非用e x t e r n做了清楚的說明:

    extern const bufsize ;

雖然上面的e x t e r n強制進行了存儲空間分配(另外還有一些狀況,如取一個 c o n s t的地址,也要

進行存儲空間分配),可是C + +編譯器一般並不爲c o n s t分配存儲空間,相反它把這個定義保存
在它的符號表裏。當c o n s t被使用時,它在編譯時會進行常量摺疊。
    固然,絕對不爲任何c o n s t分配存儲是不可能的,尤爲對於複雜的結構。這種狀況下,編譯
器創建存儲,這會阻止常量摺疊。這就是 c o n s t爲何必須默認內部鏈接,即鏈接僅在特別編
譯單元內的緣由;不然,因爲衆多的c o n s t在多個c p p文件內分配存儲,容易引發鏈接錯誤,連

接程序在多個對象文件裏看到一樣的定義就會「抱怨」了。然而,由於  c o n s t默認內部鏈接,
因此鏈接程序不會跨過編譯單元鏈接那些定義,所以不會有衝突。對於在大量場合使用的內部
數據類型,包括常量表達式,編譯器都能執行常量摺疊。

7.1.2 const的安全性

    c o n s t的做用不限於在常數表達式裏代替# d e f i n e s。若是用運行期間產生的值初始化一個變
量並且知道在那個變量壽命期內它是不變的,用 c o n s t限定該變量,程序設計中這是一個很好

的作法。若是偶然改變了它,編譯器會給出一個出錯信息。下面是一個例子:

    咱們會發現,i是一個編譯期間的常量,但j 是從i中計算出來的。然而,因爲i是一個常量,
j 的計算值來自一個常數表達式,而它自身也是一個編譯期間的常量。緊接下面的一行須要 j 的
地址,因此迫使編譯器給j 分配存儲空間。即便分配了存儲空間,把j 值保存在程序的某個地方,
因爲編譯器知道j 是常量,並且知道j 值是有效的,因此,這仍不會妨礙在決定數組b u f 的大小時
使用j 。

    在主函數m a i n ( )裏,對於標識符c中有另外一種c o n s t,由於其值在編譯期間是不知道的。這
意味着須要存儲空間,而編譯器不想在符號表裏保留任何東西(和 C 的方式同樣)。初始化必
鬚髮生在定義的地方,並且一旦初始化,其值不能改變。咱們看到 c 2 由c的值計算出來,也會
看到這類常量的做用域與其餘任何類型常量的做用域是同樣的—這是對# d e f i n e用法的另外一種

----------------------- Page 126-----------------------

  126        C + +編程思想
                                                                      下載

改進。
    實際上,若是想一個值不變,就應該使之成爲常量( c o n s t )。這不只爲防止意外的更改提

供安全措施,也消除了存儲和讀內存操做,使編譯器產生的代碼更有效。

7.1.3 集合

    c o n s t能夠用於集合,但編譯器不能把一個集合存放在它的符號表裏,因此必須分配內存。
在這種狀況下,c o n s t意味着「不能改變的一塊存儲」。然而,其值在編譯時不能被使用,由於
編譯器在編譯時不須要知道存儲的內容。這樣,咱們不能寫:

    在一個數組定義裏,編譯器必須能產生這樣的移動存儲數組的棧指針代碼。在上面這兩種
非法定義裏,編譯器給出「提示」是由於它不能在數組定義裏找到一個常數表達式。

7.1.4 與C語言的區別

    常量引進是在早期的C + +版本中,當時標準C規範正在制訂。那時,常量被看做是一個好
的思想而被包含在C中。可是,C中的c o n s t意思是「一個不能被改變的普通變量」,在C中,它

老是佔用存儲並且它的名字是全局符。C編譯器不能把c o n s t當作一個編譯期間的常量。在C 中,
若是寫:

    const bufsize=100 ;

    char buf[bufsize] ;

儘管看起來好像作了一件合理的事,但這將獲得一個錯誤結果。由於 b u f s i z e 佔用存儲的某個地
方,因此C編譯器不知道它在編譯時的值。在C語言中能夠選擇這樣書寫:

    const bufsize ;

這樣寫在C + +中是不對的,而C編譯器則把它做爲一個聲明,這個聲明指明在別的地方有存儲
分配。由於C默認c o n s t是外部鏈接的,C + +默認c o n s t是內部鏈接的,這樣,若是在C + +中想完
成與C中一樣的事情,必須用e x t e r n把鏈接改爲外部鏈接:

    extern const bufsize;//declaration only

這種方法也可用在C語言中。
    在C語言中使用限定符c o n s t不是頗有用,即便是在常數表達式裏(必須在編譯期間被求出)
想使用一個已命名的值,使用c o n s t也不是頗有用的。C迫使程序員在預處理器裏使用# d e f i n e 。

----------------------- Page 127-----------------------

                                                    第7章  常     量   127
 下載

7.2  指針

    咱們還能夠使指針成爲c o n s t指針。當處理c o n s t指針時,編譯器仍將努力阻止存儲分配並
進行常量摺疊,但在這種狀況下,這些特徵彷佛不多有用。更重要的是,若是程序員之後想在
程序代碼中改變這種指針的使用,編譯器將給出通知。這大大增長了安全性。
    當使用帶有指針的c o n s t時,有兩種選擇:或者c o n s t修飾指針正指向對象,或者c o n s t修飾

存儲在指針自己的地址裏。這些語法在開始時有點令人混淆,但練習以後就行了。

7.2.1 指向const的指針

    使用指針定義的技巧,正如任何複雜的定義同樣,是在標識符的開始處讀它並從裏向外讀。
c o n s t指定那個「最靠近」的。這樣,若是要使正指向的元素不發生改變,咱們得寫一個像這
樣的定義:

    const int* x ;

從標識符開始,是這樣讀的:「x是一個指針,它指向一個const int 。」這裏不須要初始化,由於
說x能夠指向任何東西(那是說,它不是一個c o n s t ),但它所指的東西是不能被改變的。
    這是一個容易混淆的部分。有人可能認爲:要想指針自己不變,即包含在指針 x裏的地址

不變,可簡單地像這樣把c o n s t從一邊移向另外一邊:

    int const* x;

    並不是全部的人都很確定地認爲:應該讀成「 x是一個指向i n t 的c o n s t指針」。然而,實際上
應讀成「x是一個指向剛好是c o n s t的i n t普通指針」。即c o n s t又把它本身與i n t結合在一塊兒,結果
與前面定義同樣。兩個定義是同樣的,這一點容易令人混淆。爲使程序更具備可讀性,咱們應
該堅持用第一種形式。

7.2.2 const指針

    使指針自己成爲一個c o n s t指針,必須把c o n s t標明的部分放在*的右邊,如:

    int d=1;

    int* const x=&d;

如今它讀成「x是一個指針,這個指針是指向i n t的c o n s t指針」。由於如今指針自己是c o n s t指針,
編譯器要求給它一個初始化值,這個值在指針壽命期間不變。然而要改變它所指向的值是能夠

的,能夠寫* x = 2;
    也能夠使用下面兩種合法形式中的任何一種形式把一個c o n s t指針變爲一個c o n s t對象:

    int d=1;

    const int* const x=&d ;// (1)

    int const* const x2=&d;// (2)

    如今,指針和對象都不能改變。
    一些人認爲第二種形式更好。由於c o n s t老是放在被修改者的右邊。但對於特定的代碼類型
來說,程序員得本身決定哪種形式更清楚。
    • 格式

    這本書主張,無論什麼時候在一行裏僅放一個指針定義,且在定義的地方初始化每一個指針。正
由於這一點,才能夠把‘*’「附於」數據類型上:

    int* u=&w;

----------------------- Page 128-----------------------

  128        C + +編程思想
                                                                       下載

i n t *自己好像是離散型的。這使代碼更容易懂,惋惜的是,事情並不是如此。事實上,‘*’與標
識符結合,而不是與類型結合。它能夠被放在類型名和標識符之間的任何地方。因此,能夠這

樣作:

    int* u=&w,v = 0 ;

    它創建一個int* u和一個非指針int v 。因爲讀者時常混淆這一點,因此最好用本書裏所用

的表示形式(即一行裏只定義一個指針)。

7.2.3 賦值和類型檢查

    C + +關於類型檢查有其特別之處,這一點也擴展到指針賦值。咱們能夠把一個非 c o n s t對象
的地址賦給一個c o n s t指針,由於也許有時不想改變某些能夠改變的東西。然而,不能把一個
c o n s t對象的地址賦給一個非c o n s t指針,由於這樣作可能經過被賦值指針改變這個 c o n s t指針。
固然,總能用類型轉換強制進行這樣的賦值,可是,這不是一個好的程序設計習慣,由於這樣

就打破了對象的c o n s t屬性以及由c o n s t提供的安全性。例如:

    雖然C + +有助於防止錯誤發生,但若是程序員本身打破了這種安全機制,它也是無能爲力
的。
    • 串字面值
    限定詞c o n s t是很嚴格的,c o n s t沒被強調的地方是有關串字面值。也許有人寫:

    char* cp="howdy";

    編譯器將接受它而不報告錯誤。從技術上講,這是一個錯誤,由於串字面值(這裏是

「h o w d y 」)是被編譯器做爲一個常量串創建的,所引用串的結果是它在內存裏的首地址。
    因此串字面值其實是常量串。然而,編譯器把它們做爲很是量看待,這是由於有許多現
有的C代碼是這樣作的。固然,改變串字面值的作法還未被定義,雖然可能在不少機器上是這
樣作的。

7.3  函數參數和返回值

    用c o n s t限定函數參數及返回值是常量概念另外一個容易被混淆的地方。若是以值傳遞對象時,

對用戶來說,用c o n s t 限定沒有意義(它意味着傳遞的參數在函數裏是不能被修改的)。若是以
常量返回用戶定義類型的一個對象的值,這意味着返回值不能被修改。若是傳遞並返回地址,
c o n s t將保證該地址內容不會被改變。

7.3.1 傳遞const值

    若是函數是以值傳遞的,可用c o n s t限定函數參數,如:

----------------------- Page 129-----------------------

                                                    第7章  常     量   129
 下載

    這是什麼意思呢?這是做了一個約定:變量初值不會被函數x ( )改變。然而,因爲參數是以
值傳遞的,所以要當即製做原變量的副本,這個約定對用戶來講是隱藏的。

    在函數裏,c o n s t有這樣的意義:參數不能被改變。因此它實際上是函數建立者的工具,而不
是函數調用者的工具。
    爲了避免使調用者混淆,在函數內部用 c o n s t限定參數優於在參數表裏用c o n s t限定參數。可
以用一個指針這樣作,但更好的語法形式是「引用」,這是第1 0章討論的主題。簡言之,引用
像一個被自動逆向引用的常指針,它的做用是成爲對象的別名。爲創建一個引用,在定義裏使

用&。因此,不引發混淆的函數定義看來像這樣的:

    這又會獲得一個錯誤信息,但這時對象的常量性(c o n s t)不是函數特徵標誌的部分;它僅
對函數實現有意義,因此它對用戶來講是不可見的。

7.3.2 返回const值

    對返回值來說,存在一個相似的道理,即若是從一個函數中返回值,這個值做爲一個常
量:

    const int g();

約定了函數框架裏的原變量不會被修改。正如前面講的,返回這個變量的值,由於這個變量被
製成副本,因此初值不會被修改。
    首先,這使c o n s t看起來沒有什麼意義。能夠從這個例子中看到:返回常量值明顯失去意

義:

    對於內部數據類型來講,返回值是不是常量並無關係,因此返回一個內部數據類型的值

時,應該去掉c o n s t從而使用戶程序員不混淆。
    處理用戶定義的類型時,返回值爲常量是很重要的。若是一個函數返回一個類對象的值,
其值是常量,那麼這個函數的返回值不能是一個左值(即它不能被賦值,也不能被修改)。例
如:

----------------------- Page 130-----------------------

  130        C + +編程思想
                                                                       下載

    f 5 ( )返回一個非const X對象,然而f 6 ( )返回一個const X對象。只有非c o n s t返回值能做爲一
個左值使用。換句話說,若是不讓對象的返回值做爲一個左值使用,當返回一個對象的值時,
應該使用c o n s t。
    返回一個內部數據類型的值時,c o n s t沒有意義的緣由是:編譯器已經不讓它成爲一個左值
(由於它老是一個值而不是一個變量)。僅當返回用戶定義的類型對象的值時,纔會出現上述問

題。
    函數f 7 ( )把它的參數做爲一個非c o n s t引用(C + + 中另外一種處理地址的辦法,這是第1 0章討
論的主題)。從效果上講,這與取一個非c o n s t指針同樣,只是語法不一樣。
    • 臨時變量
    有時候,在求表達式值期間,編譯器必須創建臨時對象。像其餘任何對象同樣,它們須要

存儲空間並且必須被構造和刪除。區別是咱們歷來看不到它們—編譯器負責決定它們的去
留以及它們存在的細節。這裏有一個關於臨時變量的狀況:它們自動地成爲常量。由於咱們
一般接觸不到臨時對象,不能使用與之相關的信息,因此告訴臨時對象作一些改變幾乎確定
會出錯。當程序員犯那樣的錯誤時,因爲使全部的臨時變量自動地成爲常量,編譯器會向他
發出錯誤警告。

    類對象常量是怎樣保存起來的,將在這一章的後面介紹。

----------------------- Page 131-----------------------

                                                     第7章  常    量    131
 下載

7.3.3 傳遞和返回地址

    若是傳遞或返回一個指針(或一個引用),用戶取指針並修改初值是可能的。若是使這個

指針成爲常(c o n s t )指針,就會阻止這類事的發生,這是很是重要的。事實上,不管什麼時
候傳遞一個地址給一個函數,咱們都應該儘量用 c o n s t修飾它,若是不這樣作,就使得帶有
指向c o n s t 的指針函數不具有可用性。
    是否選擇返回一個指向c o n s t的指針,取決於咱們想讓用戶用它幹什麼。下面這個例子代表
瞭如何使用c o n s t指針做爲函數參數和返回值:

    函數t ( )把一個普通的非c o n s t指針做爲一個參數,而函數u ( )把一個c o n s t指針做爲參數。在

----------------------- Page 132-----------------------

  132        C + +編程思想
                                                                      下載

函數u ( )裏,咱們會看到試圖修改c o n s t指針的內容是非法的。固然,咱們能夠把信息拷進一個
非c o n s t變量。編譯器也不容許使用存儲在c o n s t指針裏的地址來創建一個非c o n s t指針。
    函數v ( )和w ( )測試返回的語義值。函數v ( )返回一個從串字面值中創建的const char* 。在編
譯器創建了它並把它存儲在靜態存儲區以後,這個聲明實際上產生串字面值的地址。像前面提
到的同樣,從技術上講,這個串是一個常量,這個常量由函數v ( )的返回值正確地表示。
    w ( ) 的返回值要求這個指針及這個指針所指向的對象均爲常量。像函數 v ( )同樣,僅僅由於
它是靜態的,因此在函數返回後由w ( )返回的值是有效的。函數不能返回指向局部棧變量的指
針,這是由於在函數返回後它們是無效的,並且棧也被清理了。可返回的另外一個普通指針是在
堆中分配的存儲地址,在函數返回後它仍然有效。
    在函數m a i n ( ) 中,函數被各類參數測試。函數t ( )將接受一個非c o n s t指針參數。可是,若是
咱們想傳給它一個指向c o n s t的指針,那麼就將不能防止t ( )丟下這個指針所指的內容無論,因此
編譯器會給出一個錯誤信息。函數 u ( )帶一個c o n s t指針,因此它接受兩種類型的參數。這樣,
帶c o n s t指針函數比不帶c o n s t指針函數更具通常性。
    正如所指望的,函數v ( )返回值只能夠被賦給一個c o n s t指針。編譯器拒絕把函數w ( ) 的返回
值賦給一個非c o n s t指針,而接受一個const int* const,但使人吃驚的是它也接受一個const int*,
這與返回類型不匹配。正如前面所講的,由於這個值(包含在指針中的地址)正被拷貝,因此
自動保持這樣的約定:原始變量不能被觸動。所以,只有把const int*const 中的第二個c o n s t當
做一個左值使用時(編譯器會阻止這種狀況),它才能顯示其意義。
    • 標準參數傳遞
    在C語言中,值傳遞是很普通的,可是當咱們想傳遞地址時,只能使用指針。然而,在
C + + 中卻不使用這兩種方法。在C + +中,傳遞一個參數時,先選擇經過引用傳遞,並且是經過
常量(c o n s t )引用。對程序員來講,這樣作的語法與值傳遞是同樣的,因此在指針方面沒有
混淆之處的—他們甚至沒必要考慮這個問題。對於類的建立者來講,傳遞地址總比傳遞整個類
對象更有效,如經過常量( c o n s t )引用來傳遞,這意味着函數將不改變該地址所指的內容,
從用戶程序員的觀點來看,效果剛好與值傳遞同樣。
    因爲引用的語法(看起來像值傳遞)的緣由,傳遞一個臨時對象給帶有一個引用的函數,
是可能的,但不能傳遞一個臨時對象給帶有一個指針的函數—由於它必須清楚地帶有地址。
因此,經過引用傳遞會產生一個在C中不會出現的新情形:一個老是常量的臨時變量,它的地
址能夠被傳遞給一個函數。這就是爲何臨時變量經過引用被傳遞給一個函數時,這個函數的
參數必定是常量(c o n s t )引用。下面的例子說明了這一點:

----------------------- Page 133-----------------------

                                                    第7章  常     量   133
 下載

    函數f ( )返回類X 的一個對象的值。這意味着當即取 f ( ) 的返回值並把它傳遞給其餘函數時
(正如g 1 ( )和g 2 ( )函數的調用),創建了一個臨時變量,那個臨時變量是常量。這樣,函數g 1 ( )中

的調用是錯誤的,由於g 1 ( )不帶一個常量(c o n s t )引用,可是函數g 2 ( )中的調用是正確的。

7.4  類

    這一部分介紹了c o n s t用於類的兩種辦法。程序員可能想在一個類裏創建一個局部常量,將
它用在常數表達式裏,這個常數表達式在編譯期間被求值。然而, c o n s t的意思在類裏是不一樣
的,因此必須使用另外一技術—枚舉,以達到一樣的效果。
    咱們還能夠創建一個類對象常量(c o n s t )(正如咱們剛剛看到的,編譯器老是創建臨時類

對象常量)。可是,要保持類對象爲常量卻比較複雜。編譯器能保證一個內部數據類型爲常量,
但不能控制一個類中錯綜複雜的事物。爲了保證一個類對象爲常量,引進了  c o n s t成員函數:
對於一個常量對象,只能調用c o n s t成員函數。

7.4.1 類裏的const和enum

    常數表達式使用常量的狀況之一是在類裏。典型的例子是在一個類裏創建一個數組,並用
c o n s t代替# d e f i n e創建數組大小以及用於有關數組的計算。並把數組大小一直隱藏在類裏,這

樣,若是用s i z e表示數組大小,就能夠把s i z e這個名字用在另外一個類裏而不發生衝突。然而預
處理器從這些# d e f i n e被定義的時起就把它們當作全程的,因此如用# d e f i n e就不會獲得預期的效
果。
    起初讀者可能認爲合乎邏輯的選擇是把一個c o n s t放在類裏。但這不會產生預期的結果。在
一個類裏,c o n s t恢復它在C中的一部分意思。它在每一個類對象裏分配存儲並表明一個值,這個

值一旦被初始化之後就不能改變。在一個類裏使用 c o n s t的意思是「在這個對象壽命期內,這
是一個常量」。然而,對這個常量來說,每一個不一樣的對象能夠含一個不一樣的值。
    這樣,在一個類裏創建一個c o n s t時,不能給它初值。這個初始化工做必須發生在構造函數
裏,而且,要在構造函數的某個特別的地方。由於 c o n s t必須在創建它的地方被初始化,因此
在構造函數的主體裏,c o n s t必須已初始化了,不然,就只有等待,直到在構造函數主體之後

的某個地方給它初始化,這意味着過一下子纔給 c o n s t初始化。固然,沒法防止在在構造函數
主體的不一樣地方改變c o n s t的值。
    1. 構造函數初始化表達式表
    構造函數有個特殊的初始化方法,稱爲構造函數初始化表達式表,起初用在繼承裏(繼承
是之後章節中有關面向對象的主題)。構造函數初始化表達式表—顧名思義,是出如今構造

函數的定義裏的—是一個出如今函數參數表和冒號後,但在構造函數主體開頭的花括號前的
「函數調用表」。這提醒人們,表裏的初始化發生在構造函數的任何代碼執行以前。這是把全部
的c o n s t初始化的地方,因此類裏的c o n s t正確形式是:

----------------------- Page 134-----------------------

  134        C + +編程思想
                                                                      下載

    開始時,上面顯示的構造函數初始化表達式表的形式容易令人們混淆,由於人們不習慣看
到一個內部數據類型有一個構造函數。

    2.  內部數據類型「構造函數」
    隨着語言的發展和人們爲使用戶定義類型像內部數據類型所做的努力,有時彷佛使內部數
據類型看起來像用戶定義類型更好。在構造函數初始化表達式表裏,能夠把一個內部數據類型
當作好像它有一個構造函數,就像下面這樣:

    這在初始化c o n s t數據成員時尤其典型,由於它們必須在進入函數體前被初始化。
    咱們還能夠把這個內部數據類型的「構造函數」(僅指賦值)擴展爲通常的情形,能夠寫:

    float pi (3.14159) ;

把一個內部數據類型封裝在一個類裏以保證用構造函數初始化,是頗有用的。例如,下面是一

個i n t e g e r類:

    如今,若是創建一個i n t e g e r數組,它們都被自動初始化爲零:

    integer I[100];

與f o r循環和m e m s e t ( )相比,這種初始化沒必要付出更多的開銷。不少編譯器能夠很容易地把它優

化成一個很快的過程。

7.4.2 編譯期間類裏的常量

    由於在類對象裏進行了存儲空間分配,編譯器不能知道c o n s t的內容是什麼,因此不能把它

用做編譯期間的常量。這意味着對於類裏的常數表達式來講,c o n s t就像它在C中同樣沒有做用。
咱們不能這樣寫:

    在類裏的c o n s t意思是「在這個特定對象的壽命期內,而不是對於整個類來講,這個值是不
變的(c o n s t )」。那麼怎樣創建一個能夠用在常數表達式裏的類常量呢?一個普通的辦法是使
用一個不帶實例的無標記的 e n u m 。枚舉的全部值必須在編譯時創建,它對類來講是局部的,
但常數表達式能獲得它的值,這樣,咱們通常會看到:

----------------------- Page 135-----------------------

                                                     第7章  常    量         135
 下載

    使用e n u m是不會佔用對象中的存儲空間的,枚舉常量在編譯時被所有求值。咱們也能夠
明確地創建枚舉常量的值:

    enum { one=1,two=2,three} ;

對於整型e n u m,編譯器從最後一個值繼續計數,因此枚舉常量t h r e e將取值3。
    下面這個例子代表了在一個串指針棧裏的e n u m 的用法:

----------------------- Page 136-----------------------

  136        C + +編程思想
                                                                      下載

    注意p u s h ( )帶一個const char*參數,p o p ( )返回一個const char*,s t a c k保存const char* 。若是

不是這樣,就不能用s t r i n g s t a c k保存i c e C r e a m裏的指針。然而,它不讓程序員作任何事情以改
變包含在S t r i n g s t a c k裏的對象。固然,不是全部的串指針棧都有這個限制。
    雖然會常常在之前的程序代碼裏看到使用e n u m技術,但C + +還有一個靜態常量static const,
它在一個類裏產生一個更靈活的編譯期間的常量。這一點在將第9章描述。
    • 枚舉的類型檢查

    C 中的枚舉是至關原始的,只涉及整型值和名字,但不提供類型檢查。在 C + +裏,正如我
們如今所指望的,類型概念是十分重要的,枚舉正是這樣要求的。咱們創建了一個已命名的枚
舉時,咱們就已經有效地創建了一個新的類型,就像一個類同樣:在編譯單元被翻譯期間,枚
舉名字將成爲一個保留字。
    另外,C + + 中的枚舉有一個比C 中更嚴格的類型檢查。假如咱們有一個稱爲 a 的枚舉類型

c o l o r,就會注意這一點。在C中能夠寫a + +,但在C + +中不能這樣寫。這是由於枚舉自增正在
執行兩個類型轉換,其中一個類型在C + +中是合法的,另外一個是不合法的。首先,枚舉的值隱
蔽地從c o l o r轉換到i n t,而後值增1,而後i n t又轉回到c o l o r 。在C + +中,這樣作是不容許的,因
爲c o l o r是一個與i n t不一樣的類型,沒法知道b l u e加1剛好出如今顏色表裏。若是要對c o l o r加1,那
麼它應該是一個類(有自增操做),而不是一個e n u m。不論何時寫出了隱含對e n u m進行類

型轉換的代碼,編譯器都把它標記成危險的活動。
    共用數據類型有相似的附加類型檢查。

7.4.3 const對象和成員函數

    能夠用c o n s t限定類成員函數,這是什麼意思呢?爲了搞清楚這一點,必須首先掌握 c o n s t
對象的概念。
    用戶定義類型和內部數據類型同樣,均可以定義一個c o n s t對象。例如:

    const int i=1 ;

    const blob B(2);

----------------------- Page 137-----------------------

                                                     第7章  常    量    137
 下載

這裏,B是類型b l o b 的一個c o n s t對象。它的構造函數被調用,且其參數爲「 2 」。因爲編譯器強
調對象爲c o n s t 的,所以它必須保證對象的數據成員在對象壽命期內不被改變。能夠很容易地

保證公有數據不被改變,可是怎麼知道哪一個成員函數會改變數據?又怎麼知道哪一個成員函數對
於c o n s t對象來講是「安全」的呢?
    若是聲明一個成員函數爲c o n s t函數,則等於告訴編譯器能夠爲一個c o n s t對象調用這個函
數。一個沒有被特別聲明爲 c o n s t的成員函數被當作是將要修改對象中數據成員的函數,並且
編譯器不容許爲一個c o n s t對象調用這個函數。

    然而,不能就此爲止。僅僅聲明一個函數在類定義裏是c o n s t的,不能保證成員函數也如此
定義,因此編譯器迫使程序員在定義函數時要重申c o n s t說明。(c o n s t 已成爲函數識別符的一部
分,因此編譯器和鏈接程序都要檢查c o n s t )。爲確保函數的常量性,在函數定義中,若是咱們
改變對象中的任何成員或調用一個非 c o n s t成員函數,編譯器都將發出一個出錯信息,強調在
函數定義期間函數被定義成c o n s t函數。這樣,能夠保證聲明爲c o n s t的任何成員函數可以按定

義方式運行。
    c o n s t放在函數聲明前意味着返回值是常量,但這不合語法。必須把 c o n s t標識符放在參數
表後。例如:

    關鍵字c o n s t必須用一樣的方式重複出如今定義裏,不然編譯器把它當作一個不一樣的函數:

    int X::f() const {return i;}

若是f ( )試圖用任何方式改變i或調用另外一個非c o n s t成員函數,編譯器把它標記成一個錯誤。
    任何不修改爲員數據的函數應該聲明爲c o n s t函數,這樣它能夠由c o n s t對象使用。
    下面是一個比較c o n s t和非c o n s t成員函數的例子:

----------------------- Page 138-----------------------

  138        C + +編程思想
                                                                       下載

    構造函數和析構函數都不是c o n s t成員函數,由於它們在初始化和清理時,老是對對象做些

修改。q u o t e ( )成員函數也不能是c o n s t函數,由於它在返回說明裏修改數據成員l a s t q u o t e 。然而

L a s t q u o t e ( )沒作修改,因此它能夠成爲c o n s t函數,並且也能夠被c o n s t對象c q安全地調用。

    • 按位和與按成員 c o n s t

    若是咱們想要創建一個c o n s t成員函數,但仍然想在對象裏改變某些數據,這時該怎麼辦

呢?這關係到按位c o n s t和按成員c o n s t的區別。按位c o n s t意思是對象中的每一個位是固定的,所

以對象的每一個位映像從不改變。按成員 c o n s t意思是,雖然整個對象從概念上講是不變的,但

是某個成員可能有變化。當編譯器被告知一個對象是 c o n s t對象時,它將保護這個對象。這裏

咱們要介紹在c o n s t成員函數裏改變數據成員的兩種方法。

    第一種方法已成爲過去,稱爲「強制轉換c o n s t」。它以至關奇怪的方式執行。取t h i s                   (這個

關鍵字產生當前對象的地址)並把它強制轉換成指向當前類型對象的指針。看來 t h i s 已是我

們所需的指針,但它是一個 c o n s t指針,因此,還應把它強制轉換成一個普通指針,這樣就可

以在運算中去掉常量性。下面是一個例子:

----------------------- Page 139-----------------------

                                                    第7章  常     量   139
 下載

    這種方法可行,在過去的程序代碼裏能夠看到這種用法,但這不是首選的技術。問題是:
t h i s沒有用c o n s t修飾,這在一個對象的成員函數裏被隱藏,這樣,若是用戶不能見到源代碼
(並找到用這種方法的地方),就不知道發生了什麼。爲解決全部這些問題,應該在類聲明裏使

用關鍵字m u t a b l e ,以指定一個特定的數據成員能夠在一個c o n s t對象裏被改變。

    如今類用戶可從聲明裏看到哪一個成員可以在一個c o n s t成員函數裏被修改。

7.4.4 只讀存儲能力

    若是一個對象被定義成 c o n s t對象,它就成爲被放進只讀存儲器( R O M )中的一個候選,

----------------------- Page 140-----------------------

  140        C + +編程思想
                                                                      下載

這常常是嵌入式程序設計中要考慮的重要事情。然而,只創建一個 c o n s t對象是不夠的—只
讀存儲能力的條件很是嚴格。固然,這個對象還應是按位 c o n s t 的,而不是按成員c o n s t的。如

果只經過關鍵字m u t a b l e實現按成員常量化的話,就容易看出這一點。若是在一個 c o n s t成員函
數裏的c o n s t被強制轉換了,編譯器可能檢測不到這個。另外,
    1) class或s t r u c t必須沒有用戶定義的構造函數或析構函數。
    2)  這裏不能有基類(將在關於繼承的章節裏談到) ,也不能有包含用戶定義的構造函數或
析構函數的成員對象。

    在只讀存儲能力類型的c o n s t對象中的任何部分上,有關寫操作的影響沒有定義。雖然適當
形式的對象可被放進R O M裏,可是目前尚未什麼對象須要放進R O M裏。

7.5  可變的(volatile )

    v o l a t i l e的語法與c o n s t是同樣的,可是v o l a t i l e 的意思是「在編譯器認識的範圍外,這個數
據能夠被改變」。不知何故,環境正在改變數據(可能經過多任務處理),因此,v o l a t i l e告訴編
譯器不要擅自作出有關數據的任何假定—在優化期間這是特別重要的。若是編譯器說:「我

已經把數據讀進寄存器,並且再沒有與寄存器接觸」。通常狀況下,它不須要再讀這個數據。
可是,若是數據是v o l a t i l e修飾的,編譯器不能做出這樣的假定,由於可能被其餘進程改變了,
它必須重讀這個數據而不是優化這個代碼。
    就像創建c o n s t對象同樣,程序員也能夠創建v o l a t i l e對象,甚至還能夠創建const volatile對
象,這個對象不能被程序員改變,但可經過外面的工具改變。下面是一個例子,它表明一個類,

這個類涉及到硬件通訊:

----------------------- Page 141-----------------------

                                                    第7章  常    量    141
 下載

    就像c o n s t同樣,咱們能夠對數據成員、成員函數和對象自己使用 v o l a t i l e ,能夠而且也只
能爲v o l a t i l e對象調用v o l a t i l e成員函數。

    函數i s r ( )不能像中斷服務程序那樣使用的緣由是:在一個成員函數裏,當前對象(  t h i s )
的地址必須被祕密地傳遞,而中斷服務程序I S R通常根本不要參數。爲解決這個問題,能夠使
i s r ( )成爲靜態成員函數,這是下面章節討論的主題。
    v o l a t i l e 的語法與c o n s t是同樣的,因此常常把它們倆放在一塊兒討論。爲表示能夠選擇兩個
中的任何一個,它們倆通稱爲c - v限定詞。

7.6  小結

    關鍵字c o n s t能將對象、函數參數、返回值及成員函數定義爲常量,並能消除預處理器的值
替代而不對預處理器有任何影響。全部這些都爲程序設計提供了很是好的類型檢查形式以及安
全性。使用所謂的const correctness    (在可能的任何地方使用c o n s t )已成爲項目的救星。
    對於忽視c o n s t而繼續使用老的C代碼的程序員, 第1 0、11兩章將改變他們的作法,在那裏
將開始大量使用引用,那時將看到對函數參數使用c o n s t是多麼關鍵。

7.7  練習

    1.  創建一個具備成員函數f l y ( )的名爲b i r d 的類和一個不含f l y ( )的名爲r o c k 的類。創建一個
r o c k對象,取它的地址,把它賦給一個v o i d * 。如今取這個v o i d * ,把它賦給一個b i r d *,經過那
個指針調用函數f l y ( )。C語言容許公開地經過v o i d *賦值是C語言中的一個「缺陷」,爲何呢?
您知道嗎?
    2.  創建一個包含c o n s t成員的類,在構造函數初始化表達式表裏初始化這個 c o n s t成員,建

立一個無標記的枚舉,用它決定一個數組的大小。
    3.  創建一個類,該類具備c o n s t和非c o n s t成員函數。創建這個類的c o n s t和非c o n s t對象,試
着爲不一樣類型的對象調用不一樣類型的成員函數。
    4. 建立一個函數,這個函數帶有一個常量值參數。而後試着在函數體內改變這個參數。
    5.  請自行證實C和C + +編譯器對於c o n s t的處理是不一樣的。建立一個全局的c o n s t並將它用於

一個常量表達式中;而後在C和C + +下編譯它。

----------------------- Page 142-----------------------

                                                                      下載

                      第8章           內聯 函 數

    C + +繼承C的一個重要特性是效率。假如 C + + 的效率顯著地比C低,程序設計者不會使用

它。
    在C中,保護效率的一個方法是使用宏( m a c r o )。宏能夠不用普通函數調用就使之看起來像
函數調用。宏的實現是用預處理器而不是編譯器。預處理器直接用宏代碼代替宏調用,因此就
沒有了參數壓棧、生成彙編語言的 C A L L、返回參數、執行彙編語言的R E T U R N 的時間花費。
全部的工做由預處理器完成,所以,不用花費什麼就具備了程序調用的便利和可讀性。

    C + +中,使用預處理器宏存在兩個問題。第一個問題在 C 中也存在:宏看起來像一個函數
調用,但並不老是這樣。這就隱藏了難以發現的錯誤。第二個問題是 C + +特有的:預處理器不
允許存取私有( p r i v a t e )數據。這意味着預處理器宏在用做成員函數時變得很是無用。
    爲了既保持預處理器宏的效率又增長安全性,並且還能像通常成員函數同樣能夠在類裏訪
問自如,C + +用了內聯函數(inline function) 。本章咱們將研究C + +預處理器宏存在的問題、C + +

中如何用內聯函數解決這些問題以及使用內聯函數的方針。

8.1  預處理器的缺陷

    預處理器宏存在的關鍵問題是咱們可能認爲預處理器的行爲和編譯器的行爲同樣。固然,
有意使宏在外觀上和行爲上與函數調用同樣,所以容易被混淆。當微妙的差別出現時,問題就
出現了。
    考慮下面這個簡單例子:

    #define f (x) (x+1)

    如今假若有一個像下面的f的調用

    f (1)

預處理器展開它,出現下面不但願的狀況:

    (x) (x+1) (1)

    出現這個問題是由於在宏定義中f和括號之間存在空格縫隙。當定義中的這個空格取消後,
實際上調用宏時能夠有空格空隙。像下面的調用:

    f ( 1 )

依然能夠正確地展開爲:

    (1 + 1)

    上面的例子雖然微不足道但問題很是明顯。在宏調用中使用表達式做爲參數時,問題就出
現了。
    存在兩個問題。第一個問題是表達式在宏內展開,因此它們的優先級不一樣於咱們所指望的

優先級。例如:

    #define floor(x,b) x>=b?0:1

    如今假如對參數使用表達式

----------------------- Page 143-----------------------

                                                  第8章  內聯 函數        143
 下載

    if(floor(a&0x0f,0x07)) // ...

宏將展開成:

    if(a&0x0f>=0x07?0:1)

    由於&的優先級比> = 的低,因此宏的展開結果將會使咱們驚訝。一旦發現這個問題,能夠
經過在宏定義內使用括弧來解決。上面的定義可改寫以下:

    #define floor(x,b) ((x)>=(b)?0:1)

    發現問題可能很難,咱們可能一直認爲宏的行爲是正確的。在前面沒有加括號的版本的例
子中,大多數表達式將正確工做,由於> = 的優先級比像+、/ 、- -甚至位移動操做符的優先級都
低。所以,很容易想到它對於全部的表達式都正確,包括那些位邏輯操做符。

    前面的問題能夠經過謹慎地編程來解決:在宏中將全部的內容都用括號括起來。第二個問
題則更加微妙。不像普通函數,每次在宏中使用一個參數,都對這個參數求值。只要宏僅用普
通變量調用,這個求值就開始了。但假如參數求值有反作用,那麼結果可能出乎預料,並確定
不能模仿函數行爲。
    例如,下面這個宏決定它的參數是否在必定範圍:

    #define band(x) (((x)>5 && (x)<10) ? (x) : 0)

    只要使用一個「普通」參數,宏和真的函數工做得很是相像。但只要咱們鬆懈並開始相信
它是一個真的函數時,問題就開始出現了。

    下面是這個程序的輸出,它徹底不是咱們想從真正的函數指望獲得的結果:

----------------------- Page 144-----------------------

  144        C + +編程思想
                                                                      下載

    當a等於4時,測試了條件表達式第一部分,但它不知足條件,而表達式只求值一次,因此
宏調用的反作用是a等於5,這是在相同的狀況下普通函數調用獲得的結果。但當數字在範圍之

內時,兩個表達式都測試,產生兩次自增操做。產生這個結果是因爲再次對參數操做。一旦數
字出了範圍,兩個條件仍然測試,因此也產生兩次自增操做。根據參數不一樣產生的反作用也不
同。
    很清楚,這不是咱們想從看起來像函數調用的宏中所但願的。在這種狀況下,明顯有效
的解決方法是設計真正的函數。固然,若是屢次調用函數將會增長額外的開銷並可能下降效

率。不幸的是,問題可能並不老是如此明顯。咱們可能不知不覺地獲得一個包含混合函數和
宏在一塊兒的庫函數,因此像這樣的問題可能隱藏了一些難以發現的缺陷。例如,在  S T D I O . H
中的putc( )宏可能對它的第二個參數求值兩次。這在標準 C 中做了詳細說明。宏toupper( )不謹
慎地執行也會對第二個參數求值超過兩次。如在使用 toupper(*p++ )  [1]               時就會產生不但願的結

果。

宏和訪問

    固然,對於C須要對預處理器宏謹慎地編碼和使用。即便不是由於宏不是成員函數所須要
的範圍概念這一緣由,咱們也會在C + +中避免使用它所帶來的麻煩。預處理器簡單地執行原文
替代,因此不可能用下面這樣或近似的形式寫:

    class X {

     int i;

    p u b l i c :

    #define val (X::i) //Error

另外,這裏沒有指明咱們正在涉及哪一個對象。在宏裏簡直沒有辦法表示類的範圍。沒有能取代
預處理器宏的方法,程序設計者出於效率考慮,不得不讓一些數據成員成爲 p u b l i c類型,這樣
就會暴露內部的實現並妨礙在這個實現中的改變。

8.2  內聯函數

    在解決C + +中宏存取私有的類成員的問題過程當中,全部和預處理器宏有關的問題也隨着消
失了。這是經過使宏被編譯器控制來實現的。在 C + +中,宏的概念是做爲內聯函數來實現的,
而內聯函數不管在任何意義上都是真正的函數。惟一不一樣之處是內聯函數在適當時像宏同樣展
開,因此函數調用的開銷被取消。所以,應該永遠不使用宏,只使用內聯函數。

  [1] 在Andraw Koenig所著的書《C的陷阱和缺陷》(A d d i s i o n - We s l e y, 1 9 8 9 )中將更詳細地闡述。

----------------------- Page 145-----------------------

                                                  第8章  內聯 函數        145
 下載

    任何在類中定義的函數自動地成爲內聯函數,但也能夠使用 i n l i n e關鍵字放在類外定義的
函數前面使之成爲內聯函數。但爲了使之有效,必須使函數體和聲明結合在一塊兒,不然,編譯

器將它做爲普通函數對待。所以

    inline int PlusOne(int x);

沒有任何效果,僅僅只是聲明函數(這不必定可以在稍後某個時候獲得一個內聯定義)。成功

的方法以下:

    inline int PlusOne(int x) { return ++x ;}

    注意,編譯器將檢查函數參數列表使用是否正確,並返回值(進行必要的轉換)。這些事
情是預處理器沒法完成的。假如對於上面的內聯函數,咱們寫成一個預處理器宏的話,將有不
想要的反作用。
    通常應該把內聯定義放在頭文件裏。當編譯器看到這個定義時,它把函數類型(函數名 +
返回值)和函數體放到符號表裏。當使用函數時,編譯器檢查以確保調用是正確的且返回值被

正確使用,而後將函數調用替換爲函數體,於是消除了開銷。內聯代碼的確佔用空間,但假如
函數較小,這實際上比爲了一個普通函數調用而產生的代碼(參數壓棧和執行 C A L L )佔用的
空間還少。
    在頭文件裏,內聯函數默認爲內部鏈接——即它是 static,  而且只能在它被包含的編譯單元
看到。於是,只要它們不在相同的編譯單元中聲明,在內聯函數和全局函數之間用一樣的名字

也不會在鏈接時產生衝突。

8.2.1 類內部的內聯函數

    爲了定義內聯函數,一般必須在函數定義前面放一個 i n l i n e關鍵字。但這在類內部定義內
聯函數時並非必須的。任何在類內部定義的函數自動地爲內聯函數。以下例:

----------------------- Page 146-----------------------

  146        C + +編程思想
                                                                       下載

    固然,由於類內部的內聯函數節省了在外部定義成員函數的額外步驟,因此咱們必定想在

類聲明內每一處都使用內聯函數。但應記住,內聯的目的是減小函數調用的開銷。假如函數較
大,那麼花費在函數體內的時間相對於進出函數的時間的比例就會較大,因此收穫會較小。而
且內聯一個大函數將會使該函數全部被調用的地方都作代碼複製,結果代碼膨脹而在速度方面
得到的好處卻不多或者沒有。

8.2.2 存取函數

    在類中內聯函數的最重要的用處之一是用於一種叫存取函數的函數。這是一個小函數,它

允許讀或修改對象狀態—即一個或幾個內部變量。類內存取函數使用內聯方式重要的緣由在
下面的例子中能夠看到。

    這裏,在類的設計者控制下,將類裏面狀態變量設計爲私有 ( p r i v a t e ),類的使用者就永遠
不會直接和它們發生聯繫了。對私有( p r i v a t e )數據成員的全部存取只能夠經過成員函數接口進
行。並且,這種存取是至關有效的。例如對於函數 read( ) 。若沒用內聯函數,對read( )調用產
生的代碼將包括對t h i s壓棧和執行彙編語言C A L L。對於大多數機器,產生的代碼將比內聯函數
產生的代碼大一些,執行的時間確定要長一些。

    不用內聯函數,考慮效率的類設計者將忍不住簡單地使i爲公共( p u b l i c )成員,  從而經過讓用
戶直接存取i  而節約開銷。從設計的角度看,這是很很差的。由於 i將成爲公共界面的一部分,
因此意味着類設計者決不能修改它。咱們將和稱爲 i的一個i n t類型變量打交道。這是一個問題,
由於咱們可能在稍後以爲用一個f l o a t變量比用一個int             變量表明狀態信息更有用一些,但由於
int i是公共接口的一部分,因此咱們不能改變它。另外一方面,假如咱們老是使用成員函數讀和

修改一個對象的狀態信息,那麼就能夠滿意地修改對象內部一些描述(應該永遠打消在編碼和
測試以前能使咱們的設計完善的念頭)。
    • 存取器( a c c e s s o r s )和修改器( m u t a t o r s )
    一些人進一步把存取函數的概念分紅存取器(從一個對象讀狀態信息)和修改器(修改狀

----------------------- Page 147-----------------------

                                                  第8章  內聯 函數        147
 下載

態信息)。並且,能夠用重載函數對存取器和修改器提供相同名字的函數,如何調用函數決定
了咱們是讀仍是修改狀態信息。

    構造函數使用構造函數初始表達式表(在第 7章中做了簡介,在1 3中章將詳細介紹)來初
始化Wi d t h和H e i g h t值(對於內部數據類型使用僞編譯器調用形式)。
    固然,存取器和修改器對於一個內部變量沒必要只是簡單的傳遞途徑。有時,它們能夠執行

一些計算。下面的例子使用標準的C庫函數中的時間函數來生成簡單的Ti m e類:

----------------------- Page 148-----------------------

     148   C + +編程思想
                                                                        下載

----------------------- Page 149-----------------------

                                                  第8章  內聯 函數        149
 下載

    標準C庫函數對於時間有多種表示,它們都是類Ti m e的一部分。但所有更新它們是沒有必

要的,因此time_t T被用做基本的表示法,tm local  和A S C I I字符表示法A s c i i都有一個標記來顯
示它們是否已被更新爲當前的時間t i m e _ t 。兩個私有函數updateLocal( )和 updateAscii( )檢查標
記,並有條件地執行更新。
    構造函數調用mark( ) 函數時(用戶也能夠調用它,強迫對象表示當前時間)也就清除了兩
個標記,這時當地時間和A S C I I表示法是無效的。函數ascii( )調用updateAscii( ) , 由於函數

ascii( )使用靜態數據,假如它被調用,則這個靜態數據被重寫,因此updateAscaii( )把標準C庫
函數的結果拷貝到內部緩衝器裏。返回值就是內部緩衝器的地址。
    全部以DaylightSaving( ) 開始的函數都使用函數updateLocal( ) ,這就使得複合的內聯函數
變得至關大。這彷佛不划算,尤爲是考慮到可能不常常調用這些函數。但這不意味着全部的函
數都應該用非內聯函數。假如讓updateLocal( )做爲一個內聯函數,它的代碼將被複制在全部的

非內聯函數裏,也能節省額外的開銷。
    下面是一個小的測試程序:

    在這個例子裏,一個Ti m e對象被建立,而後執行一些時延動做,接着建立第 2個Ti m e對象
來標記結束時間。這些用於顯示開始時間、結束時間和消耗的時間。

----------------------- Page 150-----------------------

  150        C + +編程思想
                                                                      下載

8.3  內聯函數和編譯器

    爲了理解內聯什麼時候有效,應該先理解編譯器遇到一個內聯函數時將作什麼。對於任何函數,
編譯器在它的符號表裏放入函數類型(即包括名字和參數類型的函數原型及函數的返回類型)。
另外,編譯器看到內聯函數和內聯函數的分析沒有錯誤時,函數的代碼也被放入符號表。代碼
是以源程序形式存放仍是以編譯過的彙編指令形式存放取決於編譯器。

    調用一個內聯函數時,編譯器首先確保調用正確,即全部的參數類型必須是正確類型或編
譯器必須可以將類型轉換爲正確類型,而且返回值在目標表達式裏應該是正確類型或可改變爲
正確類型。固然,編譯器對任何類型函數都是這樣作的,這與預處理器顯著不一樣,由於預處理
器不能檢查類型和進行轉換。
    假如全部的函數類型信息符合調用的上下文的話,內聯函數代碼就會直接替換函數調用,

消除了調用的開銷。假如內聯函數也是成員函數,對象的地址 ( t h i s )就會被放入合適的地方,
這固然也是預處理器不能執行的。

8.3.1 侷限性

    這兒有兩種編譯器不能處理內聯的狀況。在這些狀況下,它就像對非內聯函數同樣,經過
定義內聯函數和爲函數創建存貯空間,簡單地將其轉換爲函數的普通形式。假如它必須在多編
譯單元裏作這些(一般將產生一個多定義錯誤),鏈接器就會被告知忽略多重定義。

    假如函數太複雜,編譯器將不能執行內聯。這取決於特定編譯器,但大多數編譯器這時都
會放棄內聯方式,由於這時內聯將可能不爲咱們提供任何效率。通常地,任何種類的循環都被
認爲太複雜而不擴展爲內聯函數。循環在函數裏可能比調用要花費更多的時間。假如函數僅有
一條簡單語句,編譯器可能沒有任何內聯的麻煩,但假若有許多語句,調用函數的開銷將比執
行函數體的開銷少多了。記住,每次調用一個大的內聯函數,整個函數體就被插入在函數調用

的地方,因此沒有任何引人注目的執行上的改進就使代碼膨脹。本書的一些例子可能超過了一
定的合理內聯尺寸。
    假如咱們要顯式或隱含地取函數地址,編譯器也不能執行內聯。由於這時編譯器必須爲函
數代碼分配內存從而爲咱們產生一個函數的地址。但當地址不須要時,編譯器仍可能內聯代
碼。

    咱們必須理解內聯僅是編譯器的一個建議,編譯器不強迫內聯任何代碼。一個好的編譯器
將會內聯小的、簡單的函數,同時明智地忽略那些太複雜的內聯。這將給咱們想要的結果—
具備宏效率的函數調用。

8.3.2 賦值順序

    假如咱們想象編譯器對執行內聯作了些什麼時,咱們可能糊里糊塗地認爲存在着比事實上
更多的限制。特別是,假如一個內聯函數對於一個尚未在類裏聲明的函數進行向前引用,編

譯器就可能不能處理它。

----------------------- Page 151-----------------------

                                                  第8章  內聯 函數       151
 下載

    雖然函數g( )尚未定義,但在函數f( )裏對函數g( )進行了調用。這是可行的,由於語言
定義規定非內聯函數直到類聲明結束才賦值。
    固然,函數g( )也調用函數f( ) ,咱們將獲得一組遞歸調用,這些遞歸對於編譯器進行內聯
是過於複雜了。(應該在函數f( )或g( )裏也執行一些測試來強迫它們之一「中止」,不然遞歸將

是無窮的)。

8.3.3 在構造函數和析構函數裏隱藏行爲

    構造函數和析構函數是兩個使咱們易於認爲內聯比它實際上更有效的函數。構造函數和析
構函數均可能隱藏行爲,由於類能夠包含子對象,子對象的構造函數和析構函數必須被調用。
這些子對象多是成員對象,或可能因爲繼承(繼承尚未介紹)而存在。下面是一個有成員
對象的例子。

----------------------- Page 152-----------------------

  152        C + +編程思想
                                                                      下載

    在類w i t h M e m b e r s裏,內聯的構造函數和析構函數看起來彷佛很直接和簡單,但其實很復
雜。成員對象Q、P和S的構造函數和析構函數被自動調用,這些構造函數和析構函數也是內聯

的,因此它們和普通的成員函數的差異是顯著的。這並非意味着應該使構造函數和析構函數
定義爲非內聯的。通常說來,快速地寫代碼來創建一個程序的初始「輪廓」時,使用內聯函數
常常是便利的。但假如要考慮效率,內聯是值得注意的一個問題。

8.4  減小混亂

    在本書裏,類裏放入內聯定義的簡單性、精練性是很是有用的,由於這樣更容易放在一頁
或一屏中,看起來更方便一些。但Dan Saks 指出,在一個真正的工程裏,這將形成類接口混亂,

所以使類難以使用。他用拉丁文in situ來表示定義在類裏的成員函數(在適當的位置上) ,並主
張全部的定義都放在類外面以保持接口清楚。他認爲這並不妨礙最優化。假如想優化,那麼使
用關鍵字i n l i n e。使用這個方法,前面(8. 2 . 2節)R E C TA N G L . C P P例子修改以下:

----------------------- Page 153-----------------------

                                                  第8章  內聯 函數       153
 下載

    如今假如想比較一下內聯函數與非內聯函數的效果,能夠簡單地移去關鍵字 i n l i n e 。(內聯
函數通 常應該放在頭文件 裏 ,但非 內聯 函數 必須放在它們 本身的編譯單元裏 。)
假如想把函數放入文件,只用簡單的剪切和粘貼操做。 In situ 函數須要更多的操做,且更可能
出錯。這個方法的另一個爭論是咱們可能老是對於函數定義使用一致的格式化類型,有些並

沒有老是在in situ 函數中出現。

8.5  預處理器的特色

    前面我說過,咱們幾乎老是但願使用內聯函數代替預處理器宏。然而當在標準 C預處理器
(經過繼承也是C + +預處理器)裏使用3個特別的特徵時倒是例外:字符串定義、字符串串聯和
標誌粘貼。字符串定義的完成是用#指示,它允許設一個標識符並把它轉化爲字符串,然而字
符串串聯發生在當兩個相鄰的字符串沒有分隔符時,在這種狀況下字符串組合在一塊兒。在寫調

試代碼時,這兩個特徵是很是有效的。

    #define DEBUG(X) cout<<#X " = " << X << endl

    上面的這個定義能夠打印任何變量的值。咱們也能夠獲得一個跟蹤信息,在此信息裏打印
出它們執行的語句。

    #define TRACE(S) cout << #S << endl; S

    # S定義了要輸出的語句。第2個S重申了語句,因此這個語句被執行。固然,這可能會產生
問題,尤爲是在一行f o r循環中。

    for (int i = 0 ; i < 100 ; i++ )

     TRACE(f(i)) ;

    由於在TRACE( )宏裏實際上有兩個語句,因此一行 f o r循環只執行第一個。解決方法是在
宏中用逗號代替分號。

標誌粘貼

    標誌粘貼在寫代碼時是很是有用的。它讓咱們設兩個標識符並把它們粘貼在一塊兒自動產生
一個新的標識符。例如:

    每次調用FIELD( ) 宏,將產生一個保存字符串的標識符和另外一個保存字符串長度的標識符。

----------------------- Page 154-----------------------

  154        C + +編程思想
                                                                       下載

它不只易讀並且消除了編碼出錯,使維護更容易。但注意宏的名字中使用大寫字母。這是對我

們很是有幫助的習慣,由於它告訴讀者這是宏而不是函數。因此假如存在問題,它能夠做爲一

個提示。

8.6  改進的錯誤檢查

    爲本書其他部分改進錯誤檢查是很方便的。用內聯函數能夠簡單地包括一個文件而不用擔

心鏈接什麼。到目前爲止,assert( )宏已用於「錯誤檢查」,但它真正用處是調試並終將被可以
在運行時提供更多有用信息的東西代替。況且異常處理程序(在 1 7章介紹)已提供了更多的處
理這些錯誤的有效的方法。
    這是預處理器仍然有用的另外一個例子,由於 _ F I L E _和_ L I N E _指示僅和預處理器一塊兒起做
用並用在assert( )宏裏。假如assert( )宏在一個錯誤函數裏被調用,它僅打印出錯函數的行號和

文件名字而不是調用錯誤函數。這兒顯示了使用宏聯接(許可能是 assert( ) 方法)函數的方法,
緊接着調用assert( )  (程序調試成功後這由一個#define NDEBUG消除)。
    下面的頭文件將放在書的根目錄中,因此它能夠從全部的章節裏獲得。「A l l e g e」是a s s e r t
的同義詞。

    函數allege_error( )有兩個參數:一個是整型表達式的值,另外一個是這個值爲 f a l s e時需打印

----------------------- Page 155-----------------------

                                                  第8章  內聯 函數       155
 下載

的消息。函數fprintf( )代替i o s t r e a m s是由於在只有少許錯誤的狀況下,它工做得更好。假如這
不是爲調試創建的,e x i t ( 1 )被調用以終止程序。

    allege( )宏使用三重i f - t h e n - e l s e強迫計算表達式e x p r求值。在宏裏調用了allege_error( ),接
着是assert( ) ,因此咱們能在調試時得到assert( ) 的好處——由於有些環境緊密地把調試器和
assert( )結合在一塊兒。
    allegefile( )宏和allegemen( )宏分別是allege( )宏用於檢查文件和內存的專用版本。這個代
碼提供了出錯報告的必要的最少信息,但咱們能夠在這個框架基礎上增長它。

    下面是測試A L L E G E . H簡單例子。

    去掉下面這行的註釋符後,咱們就知道這個程序是如何變爲成品的:

    //#define NDEBUG // turn off asserts

    對於本書其他部分,將一概用allege( )宏代替assert( ) ,只有個別只須在調試時檢查而運行
時不需的狀況才用assert( )。

8.7  小結

    可以隱藏類下面的實現是關鍵的,由於在之後咱們有可能想修改那個實現。咱們可能爲了
效率這樣作,或由於對問題有了更好的理解,或由於有些新類變得可用而想在實現裏使用這些
新類。任何危害實現隱蔽性的東西都會減小語言的靈活性。這樣,內聯函數就顯得很是重要,

由於它實際上消除了預處理器宏和伴隨它們的問題。經過用內聯函數方式,成員函數能夠和預
處理器宏同樣有效。
    固然,內聯函數也許會在類定義裏被屢次使用。由於它更簡單,因此程序設計者都會這樣
作。但這不是大問題,由於之後期待程序規模減小時,能夠將函數移出內聯而不影響它們的功
能。開發指南應該是「首先是使它起做用,而後優化它。」

8.8  練習

    1. 將第7章練習2例子增長一個內聯構造函數和一個稱爲Print( ) 的內聯成員函數,這個函數
用於打印全部數組的值。
    2.  對於第3章的N E S T F R N D . C P P例子,用內聯函數代替全部的成員函數,使它們成爲非

----------------------- Page 156-----------------------

  156        C + +編程思想
                                                                       下載

in situ 內聯函數。同時再把initialize( ) 函數改爲構造函數。
    3. 使用第6章N L . C P P,在它本身的頭文件裏,將n l轉變爲內聯函數。

    4.  建立一個類A ,具備能自我宣佈的缺省構造函數。再寫一個新類B,  將A 的一個對象做爲
B 的成員,併爲類B寫一個內聯構造函數。建立一個B對象的數組並看看發生了什麼事。
    5.  從練習4裏建立大量的對象並使用Ti m e類來計算非內聯構造函數和內聯構造函數之間的
時間差異(假如咱們有剖析器,也試着使用它。)

----------------------- Page 157-----------------------

 下載

                       第9章  命 名 控 制

    建立名字是編程中最基本的活動,當一個項目中包含大量名字時,名字很容易衝突。 C + +

容許咱們對名字的產生和名字的可見性進行控制,包括名字的存儲位置以及名字的鏈接。
    s t a t i c這個關鍵字早在人們知道「重載」這個詞以前就在 C語言中被重載了,在C + +中又增
加了新的含義。s t a t i c最基本的含義是指「位置不變的某個東西」(像「靜電」),這裏則指內存
中的物理位置或文件中的可見性。
    在這一章裏,咱們將看到s t a t i c是怎樣控制存儲和可見的,還將看到一種經過C + +的名字空

間特徵來控制訪問名字的改進方法。咱們還能夠發現怎樣使用C語言中編寫並編譯過的函數。

9.1  來自C語言中的靜態成員

    在C和C + +中,s t a t i c都有兩種基本的含義,而且這兩種含義常常是互相有衝突的:
    1) 在固定的地址上分配,也就是說對象是在一個特殊的靜態數據區上建立的,而不是每次
函數調用時在堆棧上產生的。這也是靜態存儲的概念。
    2)  對一個特定的編譯單位來講是本地的(就像咱們在後面將要看到的,這在 C + +中包括類

的範圍)。這裏s t a t i c控制名字的可見性,因此這個名字在這個單元或類以外是不可見的。這也
描述了鏈接的概念,它決定鏈接器將看到哪些名字。
    本節將着重討論s t a t i c的這兩個含義,這些都是從C中繼承來的。

9.1.1 函數內部的靜態變量

    一般,在函數體內定義一個變量時,編譯器使得每次函數調用時堆棧的指針向下移一個適
當的位置,爲這些內部變量分配內存。若是這個變量有一個初始化表達式,那麼每當程序運行

到此處,初始化就被執行。
    然而,有時想在兩次函數調用之間保留一個變量的值,咱們能夠經過定義一個全局變量來
實現這點,但這樣一來,這個變量就不只僅受這個函數的控制。 C和C + +都容許在函數內部創
建一個s t a t i c對象,這個對象將存儲在程序的靜態數據區中,而不是在堆棧中。這個對象只在
函數第一次調用時初始化一次,之後它將在兩次函數之間保持它的值。好比,下面的函數每次

調用時都返回一個字符串中的下一個字符。

----------------------- Page 158-----------------------

  158        C + +編程思想
                                                                       下載

    static char* s在每次o n e c h a r ( )調用時保留它的值,由於它存放在程序的靜態數據區而不是

存儲在函數的堆棧中。當咱們用一個字符指針做參數調用 o n e c h a r ( )時,參數值被賦給s,而後
返回字符串的第一個字符。之後每次調用 o n e c h a r ( )都不用帶參數,函數將使用缺省參數 0做爲
s t r i n g的值,函數就會繼續用之前初始化的s值取字符,直到它到達字符串的結尾標誌—空字
符爲止。這時,字符指針就不會再增長了,這樣,指針不會越過字符串的末尾。
    可是,若是調用o n e c h a r ( )時沒有參數並且s之前也沒有初始化,那會怎樣呢?咱們也許會

在s定義時提供一個初始值:

    static char* s=0 ;

    但若是說沒有爲一個預約義類型的靜態變量提供一個初始值的話,編譯器也會確保在程序

開始時它被初始化爲零(轉化爲適當的類型),因此在o n e c h a r ( )中,函數第一次調用時s將被賦
值爲零,這樣i f ( ! s )後面的程序就會被執行。
    上例中s的初始化是很簡單的,其實對一個靜態對象的初始化(與其餘對象的初始化同樣)
能夠是任意的常量表達式,它能夠包括常量及在此以前已聲明過的變量和函數。
    1. 函數體內部的靜態對象

    用戶自定義的靜態變量同通常的靜態對象的規則是同樣的,並且它一樣也必須有初始化操做。
可是,零賦值只對預約義類型有效,用戶自定義類型必須用構造函數來初始化。所以,若是咱們
在定義一個靜態對象時沒有指定構造函數參數,這個類就必須有缺省的構造函數。請看下例:

----------------------- Page 159-----------------------

                                                 第9章 命 名控 制        159
 下載

    在函數f ( )內部定義一個靜態的X類型的對象,它能夠用帶參數的構造函數來初始化,也能夠用
缺省構造函數。程序控制第一次轉到對象的定義點時,並且只有第一次時,才須要執行構造函數。
    2. 靜態對象的析構函數
    靜態對象的析構函數(包括靜態存儲的全部對象,不只僅是上例中的局部靜態變量)在程序

從main() 塊中退出時,或者標準的C庫函數e x i t ( )被調用時才被調用。多數狀況下m a i n ( )函數的結尾
也是調用e x i t ( )來結束程序的。這意味着在析構函數內部使用e x i t ( )是很危險的,由於這可能陷入一
個死循環中。但若是用標準的C庫函數a b o r t ( )來退出程序,靜態對象的析構函數並不會被調用。
    咱們能夠用標準C庫函數a t e x i t ( )來指定當程序跳出m a i n ( ) (或調用e x i t ( ))時應執行的操做。
在這種狀況下,在跳出m a i n ( )或調用e x i t ( )以前,用a t e x i t ( )註冊的函數能夠在全部對象的析構函數

以前被調用。
    靜態對象的銷燬是按它們初始化時相反的順序進行的。固然只有那些已經被建立的對象才
會被銷燬。幸運的是,編程系統會記錄對象初始化的順序和那些已被建立的對象。全局對象總
是在m a i n ( )執行以前被建立了,因此最後一條語句只對函數局部的靜態對象起做用。若是一個
包含靜態對象的函數從未被調用過,那麼這個對象的構造函數也就不會執行,這樣天然也不會

執行析構函數。請看下例:

----------------------- Page 160-----------------------

  160        C + +編程思想
                                                                       下載

    在o b j中,字符c的做用就象一個標識符,構造函數和析構函數就能夠顯示出當前正在操做的對

象信息。而A是一個全局的o b j類的對象,因此構造函數老是在m a i n ( )函數以前就被調用。但函數f ( )
的內部的靜態o b j類對象B和函數g ( )內部的靜態對象C的構造函數只在這些函數被調用時才起做用。
    爲了說明哪些構造函數與析構函數被調用,在m a i n ( )中只調用了f ( ),程序的輸出結果爲:

    對象A 的構造函數在進入m a i n ( )函數以前即被調用,而B的構造函數只是由於f ( )的調用而調

用。當從m a i n ( ) 函數中退出時,全部被建立的對象的析構函數按建立時相反的順序被調用。這
意味着若是g ( )被調用,對象B和C的析構函數的調用順序依賴於g ( )和f ( )的調用順序。
    注意跟蹤文件o f s t r e a m 的對象o u t也是一個靜態對象,它的定義(與 e x t e r n聲明意義相反)
應該出如今文件的一開始,在使用 o u t以前,這一點很重要,不然咱們就可能在一個對象初始
化以前使用它。

    在C + +中,全局靜態對象的構造函數是在m a i n ( )以前調用的,因此咱們如今有了一個在進
入m a i n ( )以前執行一段代碼的簡單的、可移植的方法,而且能夠在退出 m a i n ( )以後用析構函數
執行代碼。在C中要作到這一點,咱們不得不熟悉編譯器開發商的彙編語言的開始代碼。

9.1.2 控制鏈接

    通常狀況下,在文件範圍內的全部名字(既不嵌套在類或函數中的名字)對程序中的全部
編譯單元來講都是可見的。這就是所謂的外部鏈接,由於在鏈接時這個名字對鏈接器來講是可
見的,外部的編譯單元、全局變量和普通函數都有外部鏈接。

    有時咱們可能想限制一個名字的可見性。想讓一個變量在文件範圍內是可見的,這樣這個
文件中的全部函數均可以使用它,但不想讓這個文件以外的函數看到或訪問該變量,或不想這
個變量的名字與外部的標識符相沖突。
    在文件範圍內,一個被明確聲明爲s t a t i c的對象或函數的名字對編譯單元(用本書的術語來
說也就是出現聲明的. C P P文件)來講是局部變量;這些名字有內部鏈接。這意味着咱們能夠在

其餘的編譯單元中使用一樣的名字,而不會發生名字衝突。
    內部鏈接的一個好處是這個名字能夠放在一個頭文件中而不用擔憂鏈接時發生衝突。那些
一般放在頭文件裏的名字,像常量、內聯函數( inline function ),在缺省狀況下都是內部鏈接
的(固然常量只有在C + +中缺省狀況下是內部鏈接的,在C中它缺省爲外部鏈接)。注意鏈接只
引用那些在鏈接/裝載期間有地址的成員,所以類聲明和局部變量並無鏈接。

----------------------- Page 161-----------------------

                                                  第9章 命 名控 制       161
 下載

    • 衝突問題
    下面例子說明了s t a t i c的兩個含義怎樣彼此交叉的。全部的全局對象都是隱含爲靜態存儲類,

因此若是咱們定義(在文件範圍)

    int a=0;

則a被存儲在程序的靜態數據區,在進入m a i n ( )函數以前,a即已初始化了。另外,a對全局都是可

見的,包括全部的編譯單元。用可見性術語,s t a t i c           (只在編譯單元內可見)的反義是e x t e r n,它
表示這個名字對全部的編譯單元都是可見的。因此上面的定義和下面的定義是相同的。

    extern int a=0;

但若是這樣定義:

    static int a=0;

咱們只不過改變了a的可見性,如今a成了一個內部鏈接。但存儲類型沒有改變—對象老是駐
留在靜態數據區,而無論是s t a t i c仍是e x t e r n。
    一旦進入局部變量,s t a t i c就不會再改變變量的可見性(這時 e x t e r n是沒有意義的),而只
是改變變量的存儲類型。
    對函數名,s t a t i c和e x t e r n只會改變它的可見性,因此若是說:

    extern void f() ;

它和沒有修飾時的聲明是同樣的:

    void f() ;

若是定義:

    static void f();

它意味着f ( )只在本編譯單元內是可見的,這有時稱做文件靜態。

9.1.3 其餘的存儲類型指定符

    咱們會看到s t a t i c和e x t e r n用得很廣泛。另外還有兩個存儲類型指定符,這兩種用得較少。
一個是a u t o ,人們幾乎不用它,由於它告訴編譯器這是一個局部變量,實際上編譯器老是能夠從

變量定義時的上下文中判斷出這是一個局部變量。因此 a u t o是多餘的。還有一個是r e g i s t e r,它
也是局部變量,但它告訴編譯器這個特殊的變量要常常用到,因此編譯器應該儘量地讓它保
存在寄存器中。它用於優化代碼。各類編譯器對這種類型的變量處理方式也不盡相同,它們有
時會忽略這種存儲類型的指定。通常,若是要用到這個變量的地址,r e g i s t e r指定符一般都會被
忽略。應該避免用r e g i s t e r類型,由於編譯器在優化代碼方面一般比咱們作得更好。

9.2  名字空間

    雖然名字能夠在類中被嵌套,但全局函數、全局變量以及類的名字仍是在同一個名字空間
中。雖然s t a t i c關鍵字能夠使變量和函數內部鏈接(使它們的文件靜態),但在一個大項目中,
若是對全局的名字空間缺少控制就會引發不少問題。爲了解決這些問題,開發商經常使用冗長、
難懂的名字,以使衝突減小,但這樣咱們不得不一個一個地敲這些名字( t y p e d e f經常用來簡化
這些名字)。這不是一個很好的解決方法。

    咱們能夠用C + +的名字空間特徵(咱們的編譯器可能尚未實現這一特徵,請查閱技術文
檔),把一個全局名字空間分紅多個可管理的小空間。名字空間的關鍵字,像 c l a s s , s t r u c t , e n u m
和u n i o n同樣,把它們的成員的名字放到了不一樣的空間中去 ,儘管其餘的關鍵字有其餘的目的,

----------------------- Page 162-----------------------

  162        C + +編程思想
                                                                       下載

但n a m e s p a c e惟一的目的是產生一個新的名字空間。

9.2.1 產生一個名字空間

    名字空間的產生與一個類的產生很是類似:

    這就產生了一個新的名字空間,其中包含了各類聲明。 n a m e s p a c e與c l a s s、s t r u c t、u n i o n
和e n u m有着明顯的區別:

    1) namespace只能在全局範疇定義,但它們之間能夠互相嵌套。
    2) 在n a m e s p a c e定義的結尾,右大括號的後面沒必要要跟一個分號。
    3) 一個n a m e s p a c e能夠在多個頭文件中用一個標識符來定義,就好象重複定義一個類同樣。

    4)  一個n a m e s p a c e 的名字能夠用另外一個名字來做它的別名,這樣咱們就沒必要敲打那些開發
商提供的冗長的名字了。

    5) 咱們不能像類那樣去建立一個名字空間的實例。
    1. 未命名的名字空間
    每一個編譯單元均可包含一個未命名的名字空間—在n a m e s p a c e關鍵字後面沒有標識符。

----------------------- Page 163-----------------------

                                                  第9章 命 名控 制        163
 下載

    在編譯單元內,這個空間中的名字自動而無限制地有效。每一個編譯單元要確保只有一個未
命名的名字空間。若是把一個局部名字放在一個未命名的名字空間中,無需加上 s t a t i c說明就

能夠讓它們做內部鏈接。
    2. 友元
    能夠在一個名字空間的類定義以內插入一個friend  聲明:

這樣函數y o u ( )就成了名字空間m e 的一個成員。

9.2.2 使用名字空間

    能夠用兩種方法在一個名字空間引用同一個名字:一種是用範圍分解運算符,還有一種是
用u s i n g關鍵字。
    1. 範圍分解
    名字空間中的任何命名均可以用範圍分解運算符明確指定,就像引用一個類中的名字一
樣:

----------------------- Page 164-----------------------

  164        C + +編程思想
                                                                       下載

    到目前爲止,名字空間看上去很像一個類。
    2. using指令
    用using  關鍵字能夠讓咱們當即輸入整個名字空間,擺脫輸入一個名字空間中標識符的煩

惱。這種u s i n g和n a m e s p a c e關鍵字的搭配使用叫做using  指令。using 關鍵字在當前範圍內直接
聲明瞭名字空間中的全部的名字,因此能夠很方便地使用這些無限制的名字:

    如今能夠在函數內部聲明m a t h 中的全部名字,但容許這些名字嵌套在函數中。

    若是不用u s i n g指令,這個名字空間的全部名字都須要徹底限定。
    using 指令有一個缺點,那就是看起來不那麼直觀,u s i n g指令引入名字可見性的範圍是在
建立u s i n g的地方。但咱們能夠使來自using  指令的名字暫時無效,就像它們已經被聲明爲這個
範圍的全局名同樣。

----------------------- Page 165-----------------------

                                                  第9章 命 名控 制       165
 下載

若是有第二個名字空間

這個名字空間也用u s i n g指令來引入,就可能產生衝突。這種二義性出如今名字的使用時,而

不是在u s i n g指令使用時。

    這樣,即便永遠不產生二義性,寫u s i n g指令引入帶名字衝突的名字空間也是可能的。
    3. using聲明
    能夠用u s i n g聲明一次性引入名字到當前範圍內。這種方法不像 u s i n g指令那樣把那些名字

當成當前範圍的全局名來看待,而是在當前範圍以內進行一個聲明,這就意味着在這個範圍內
它能夠廢棄來自u s i n g指令的名字。

    u s i n g聲明給出了標識符的完整的名字,但沒有了類型方面的信息。也就是說,若是名字
空間中包含了一組用相同名字重載的函數,u s i n g聲明就聲明瞭這個重載的集合內的全部函數。
    能夠把u s i n g聲明放在任何通常的聲明能夠出現的地方。 u s i n g聲明與普通聲明只有一點不

同:u s i n g聲明能夠引發一個函數用相同的參數類型來重載(這在通常的重載中是不容許的)。

----------------------- Page 166-----------------------

  166        C + +編程思想
                                                                       下載

固然這種不肯定性要到使用時才表現出來,而不是在聲明時。
    u s i n g聲明也能夠出如今一個名字空間內,其做用與在其餘地方時同樣:

    一個using  聲明是一個別名,它容許咱們在不一樣的名字空間聲明一樣的函數。若是咱們不
想因爲引入不一樣名字空間的函數而致使重複定義一個函數時,能夠用 u s i n g聲明,它不會引發
任何不肯定性和重複。

9.3  C++中的靜態成員

    有時須要爲某個類的全部對象分配一個單一的存儲空間。在 C語言中,能夠用全局變量,
但這樣很不安全。全局數據能夠被任何人修改,並且,在一個項目中,它很容易與其餘的名字

相沖突。若是能夠把一個數據當成全局變量那樣去存儲,但又被隱藏在類的內部,而且清楚地
與這個類相聯繫,這種處理方法固然是最理想的了。
    這一點能夠用類的靜態數據成員來實現。類的靜態成員擁有一塊單獨的存儲區,而無論我
們建立了多少個該類的對象。全部這些對象的靜態數據成員都共享這一塊靜態存儲空間,這就
爲這些對象提供了一種互相通訊的方法。但靜態數據屬於類,它的名字只在類的範圍內有效,

而且能夠是p u b l i c (公有的)、p r i v a t e (私有的)或者p r o t e c t e d (保護的)。

9.3.1 定義靜態數據成員的存儲

    由於類的靜態數據成員有着單一的存儲空間而無論產生了多少個對象,因此存儲空間必須
定義在一個單一的地方。固然之前有些編譯器會分配存儲空間,但如今編譯器不會分配存儲空
間。若是一個靜態數據成員被聲明但沒有定義時,鏈接器會報告一個錯誤。
    定義必須出如今類的外部(不容許內聯)並且只能定義一次,所以它一般放在一個類的實

現文件中。這種規定經常讓人感到很麻煩,但實際上它是很合理的。比方說:

以後,在定義文件中,

    int A::i=1;

若是定義了一個普通的全局變量,能夠寫:

----------------------- Page 167-----------------------

                                                  第9章 命 名控 制        167
 下載

    int i=1;

在這裏,類名和範圍分解運算符用於指定了i的範圍。
    有些人對A : : i是私有的這點感到迷惑不解,還有一些事彷佛被公開地處理。這不是破壞了
類結構的保護性嗎?有兩個緣由能夠保證它絕對的安全。第一,這些變量的初始化惟一合法是

在定義時。事實上,若是靜態數據成員是一個帶構造函數的對象時,能夠調用構造函數來代替
「=」操做符。第二,一旦這些數據被定義了,終端用戶就不能再定義它—不然鏈接器會報告
錯誤。並且這個類的建立被迫產生這個定義,不然這些代碼在測試時沒法鏈接。這就保證了定
義只出現一次而且它是由類的構造者來控制的。
    一個靜態成員的初始化表達式是在一個類的範圍內,請看下例:

    這裏,w i t h S t a t i c : : 限定符把w i t h S t a t i c 的範圍擴展到所有定義。
    1. 靜態數組的初始化
    咱們不只能夠產生靜態常量對象,並且能夠產生靜態數組對象,包括常量數組與很是量數
組,下面是初始化一個靜態數組的例子:

----------------------- Page 168-----------------------

  168        C + +編程思想
                                                                       下載

    對全部的靜態數據成員,咱們必須提供一個單一的外部定義。這些定義必須有內部鏈接,
因此能夠放在頭文件中。初始化靜態數組的方法與其餘集合類型的初始化同樣,但不能用自動
計數。除此以外,在類定義結束時,編譯器必須知道足夠的類信息來建立對象,包括全部成員

的精確大小。
    2. 類中的編譯時常量
    在第7章中,咱們介紹了用枚舉型來建立一個編譯時常量(一種被計算出來的常量表達式,
如數組大小)的方法,它做爲一個類的局部常量。儘管這種方法的使用很廣泛,但常被稱做
「enum hack 」,由於它使枚舉喪失了原本的做用。

    爲了用一種更好的方法來完成一樣的事,咱們能夠在一個類中使用一個靜態常量(咱們的
編譯器可能還不支持這一技巧,請查閱隨機文檔)。由於它既是常量(它不會改變)又是靜態
的(整個類中只有惟一的一個定義點),因此一個類中的靜態常量可被用做一個編譯時常量,
以下例:

    若是咱們在類中的一個常量表達式用到靜態常量,那麼這個靜態常量的定義應該出如今這
個類的任何實例或類的成員函數定義以前(也許在頭文件中)。和一個內部數據類型的全局常
量同樣,它並不會爲常量分配存儲空間,又因爲它是內部鏈接的,因此也不會產生衝突。
    這種方法的另外一個好處是任何預約義類型均可以做爲一個靜態常量成員,而用  e n u m時,

只能使用整型值。

9.3.2 嵌套類和局部類

    能夠很容易地把一個靜態數據成員放在一個嵌套內中。這樣的成員的定義顯然是上節中情
況的擴展——咱們只須用另外一種級別的指定。然而在局部類(在函數內部定義的類)中不能有

----------------------- Page 169-----------------------

                                                  第9章 命 名控 制       169
 下載

靜態數據成員。以下例:

    咱們能夠看到一個局部類中有靜態成員的直接問題。爲了定義它,怎樣才能在文件範圍描

述一個數據呢?實際上局部類不多使用。

9.3.3 靜態成員函數

    像靜態數據成員同樣,咱們也能夠建立一個靜態成員函數,它爲類的全體服務而不是爲一
個類的部分對象服務。這樣就不須要定義一個全局函數,減小了全局或局部名字空間的佔用,
把這個函數移到了類的內部。當產生一個靜態成員函數時,也就表達了與一個特定類的聯繫。
    靜態成員函數不能訪問通常的數據成員,它只能訪問靜態數據成員,也只能調用其餘的靜

態成員函數。一般,當前對象的地址( t h i s )是被隱含地傳遞到被調用的函數的。但一個靜態
成員函數沒有t h i s,因此它沒法訪問通常的成員函數。這樣使用靜態成員函數在速度上能夠比
全局函數有少量的增加,它不只沒有傳遞 t h i s所需的額外的花費,並且還有使函數在類內的好
處。
    用s t a t i c關鍵字指定了一個類的全部對象佔有相同的一塊存儲空間,函數能夠並行使用它,

這意味着一個局部變量只有一個拷貝,函數每次調用都使用它。
    下面是一個靜態數據成員和靜態成員函數如何在一塊兒使用的例子:

----------------------- Page 170-----------------------

  170        C + +編程思想
                                                                      下載

    由於靜態成員函數沒有t h i s指針,因此它不能訪問非靜態的數據成員,也不能調用非靜態
的成員函數,這些函數要用到t h i s指針)。
    注意在m a i n ( ) 中一個靜態成員能夠用點或箭頭來選取,把那個函數與一個對象聯繫起來,
但也能夠不與對象相連(由於一個靜態成員是與一個類相聯,而不是與一個特定的對象相連),

用類的名字和範圍分解運算符。
    這是一個有趣的特色:由於靜態成員對象的初始化方法,咱們能夠把上述類的一個靜態數
據成員放到那個類的內部。下面是一個例子,它把構造函數變成私有的,這樣 e g g類只有一個
惟一的對象存在,咱們能夠訪問那個對象,但不能產生任何新的e g g對象。

----------------------- Page 171-----------------------

                                                  第9章 命 名控 制       171
 下載

    E的初始化出如今類的聲明完成後,因此編譯器已有足夠的信息爲對象分配空間並調用構
造函數。

9.4  靜態初始化的依賴因素

    在一個指定的編譯單元中,靜態對象的初始化順序嚴格按照對象在該單元中定義出現的順
序。而清除的順序則與初始化的順序正好相反。

    固然在多個編譯單元之間沒有嚴格的初始化順序,也沒有辦法來指定這種順序。這可能會
引發不小問題。下面的例子若是一個文件包含上述狀況就會當即引發災難(它會暫停操做系統
的運行,停止複雜的進程)。

另外一個文件在它的初始表達式之一中用到了out 對象:

這個程序可能運行,也可能不運行。若是在創建可執行文件時第一個文件先初始化,那麼就不

會有問題,但若是第二個文件先初始化,o o f的構造函數依賴o u t的存在,而此時o u t尚未建立,
因而引發混亂。這只是一個互相依賴的靜態對象初始化的問題,由於當咱們進入 m a i n ( )時,所
有靜態對象的構造函數都已經被調用了。
    在ARM [1]  中能夠看到一個更喪氣的例子,在一個文件中:

  [1] The Annotated C++ Reference Manual,Bjarne Stroustrup和M a rgaret Ellis 著,1 9 9 0年,p p . 2 0 - 2 1 。

----------------------- Page 172-----------------------

  172        C + +編程思想
                                                                       下載

在另外一個文件中

    對全部的靜態對象,鏈接裝載系統在程序員指定的動態初始化發生前保證一個靜態成員初
始化爲零。在前一個例子中,fstream out  對象的存儲空間賦零並無特殊的意思,因此它在構
造函數調用前確實是未定義的。然而,對內部數據類型,初始化爲零是有意義的,因此若是文
件按上面的順序被初始化,y開始被初始化爲零,因此x變成1,然後y被動態初始化爲2。然而,

若是初始化的順序顛倒過來,x被靜態初始化爲零, y被初始化爲1,然後x被初始化爲2。
    程序員必須意識到這些,由於他們可能會在編程時遇到互相依賴的靜態變量的初始化問題,
程序可能在一個平臺上工做正常,把它移到另外一個編譯環境時,忽然莫名其妙地不工做了。

怎麼辦

    有三種方法來處理這一問題:
    1) 不用它,避免初始化時的互相依賴。這是最好的解決方法。

    2)  若是實在要用,就把那些關鍵的靜態對象的定義放在一個文件中,這樣咱們只要讓它們
在文件中順序正確就能夠保證它們正確的初始化。
    3)  若是咱們確信把靜態對象放在幾個編譯單元中是不可避免的(比方在編寫一個庫時,我
們沒法控制那些使用該庫的程序員)這時咱們可用由 Jerry Schwarz在建立i o s t r e a m庫(由於
c i n , c o u t和c e r r的定義是在不一樣的文件中)時提供的一種技術。

    這一技術要求在庫頭文件中加上一個額外的類。這個類負責庫中靜態對象的動態初始化。
下面是一個簡單的例子:

----------------------- Page 173-----------------------

                                                  第9章 命 名控 制        173
 下載

    x、y 的聲明只是代表這些對象的存在,並無爲它們分配存儲空間。然而initializer init                      的

定義爲每一個包含此頭文件的文件分配那些對象的空間,由於名字是 s t a t i c的(這裏控制可見性
而不是指定存儲類型,由於缺省時是在文件範圍內)它只在本編譯單元可見,因此鏈接器不會
報告一個多重定義錯誤。
    下面是一個包含x、y和i n i t _ c o u n t定義的文件:

    (固然,一個文件的i n i t靜態實例也放在這個文件中)假設庫的使用者產生了兩個其餘的文
件:

    和

如今哪一個編譯單元先初始化都沒有關係。當第一次包含 D E P E N D . H 的編譯單元被初始化時,

i n i t _ c o u n t爲零,這時初始化就已經完成了(這是因爲內部類型的全局變量在動態初始化以前都
被設置爲零)。對其他的編譯單元,初始化會跳過去。清除按相反的順序,且~ i n i t i a l i z e r ( )可確

----------------------- Page 174-----------------------

  174        C + +編程思想
                                                                       下載

保它只發生一次。
    這個例子用內部類型做爲全局靜態對象,這種方法也能夠用於類,但其對象必須用

i n i t i a l i z e r動態初始化。一種方法就是建立一個沒有構造函數和析構函數的類,但用不一樣的名字
的成員函數來初始化和清除這個類。固然更經常使用的作法是在 i n i t i a l i z e r ( )函數中,設定指向對象
的指針,並在堆中動態建立它們。這要用到兩個C + +的關鍵字n e w和d e l e t e,第1 2章中介紹。

9.5  轉換鏈接指定

    若是C + +中編寫一個程序須要用到C庫,那該怎麼辦呢?若是這樣聲明一個C函數:

    float f(int a,char b);

    C + +的編譯器就會將這個名字變成像_ f _ i n t _ i n t之類的東西以支持函數重載(和類型安全連
接)。然而,C編譯器編譯的庫通常不作這樣的轉換,因此它的內部名爲_ f 。這樣,鏈接器將無
法解決咱們C + +對f    ()的調用。

    C + +中提供了一個鏈接轉換指定,它是經過重載e x t e r n關鍵字來實現的。e x t e r n後跟一個字
符串來指定咱們想聲明的函數的鏈接類型,後面是函數聲明。

    extern "C" float f(int a,char b);

這就告訴編譯器 f ( )是C鏈接,這樣就不會轉換函數名。標準的鏈接類型指定符有「                               C 」和
「C + +」兩種,但編譯器開發商可選擇用一樣的方法支持其餘語言。
    若是咱們有一組轉換鏈接的聲明,能夠把它們放在花括號內:

或在頭文件中:

    多數C + +編譯器開發商在他們的頭文件中處理轉換鏈接指定,包括 C和C + +,因此咱們不
用擔憂它們。
    雖然標準的C + +只支持「C」和「C + + 」兩種鏈接轉換指定,但用一樣的方法能夠實現對

其餘語言的支持。

9.6  小結

    s t a t i c關鍵字很容易令人糊塗,由於它有時控制存儲分配,而有時控制一個名字的可見性和
鏈接。
    隨着C + +名字空間的引入,咱們有了更好的、更靈活的方法來控制一個大項目中名字的增加。
    在類的內部使用s t a t i c是在全程序中控制名字的另外一種方法。這些名字不會與全局名衝突,

而且可見性和訪問也限制在程序內部,使咱們在維護咱們的代碼時能有更多的控制。

9.7  練習

    1. 建立一個帶整型數組的類。在類內部用未標識的枚舉變量來設置數組的長度。增長一個

----------------------- Page 175-----------------------

                                                  第9章 命 名控 制       175
 下載

const int  變量,並在構造函數初始化表達式表中初始化。增長一個static int  成員變量並用特定
值來初始化。增長一個內聯(i n l i n e )構造函數和一個內聯(i n l i n e )型的p r i n t ( ) 函數來顯示數

組中的所有值,並在這兩函數內調用靜態成員函數。
    2. STAT D E S T. C P P中,在m a i n ( ) 內用不一樣的順序調用f ( )、g ( )來檢驗構造函數與析構函數的
調用順序,咱們的編譯器能正確地編譯它們嗎?
    3.  在S TAT D E S T. C P P中,把o u t的定義變爲一個e x t e r n聲明,並把實際定義放到A    (它的構
造函數o b j傳送信息給o u t )的定義以後,測試咱們的機器是怎樣進行缺省錯誤處理的。當咱們

運行程序時確保沒有其餘重要程序在運行,不然咱們的機器會出現錯誤。
    4.  建立一個類,它的析構函數顯示一條信息,而後調用 e x i t ( )。建立這個類的一個全局靜
態對象,看看會發生什麼?
    5. 修改第7章的V O L AT I L E . C P P ,使c o m m : : i s r ( )做爲一箇中斷服務例程來運行。

----------------------- Page 176-----------------------

                                                                               下載

               第1 0章  引用和拷貝構造函數

    引用是C + +的一個特徵,它就像能自動被編譯器逆向引用的常量型指針同樣。

    雖然P a s c a l語言中也有引用,但C + +中引用的思想來自於A l g o l語言。引用是支持C + +運算
符重載語法的基礎(見 第11章),也爲函數參數傳入和傳出的控制提供了便利。
    本章首先看一下C和C + +的指針的差別,而後介紹引用。但本章的大部份內容將研究使人
迷糊的C + +新的編程問題:拷貝構造函數( c o p y - c o n s t r u c t o r )。它是特殊的構造函數,須要用引
用( & )來實現從現有的相同類型的對象產生新的對象。編譯器用拷貝構造函數經過傳值的方式

來傳遞和返回對象。
    本章最後將闡述有點難以理解的C + +的指向成員的指針( p o i n t e r- t o - m e m b e r )的概念。

10.1   C++中的指針

    C和C + +指針的最重要的區別,在於C + +是一種類型要求更強的語言。就v o i d *而言,這一
點表現得更加突出。C不容許隨便地把一個類型的指針指派給另外一個類型,但容許經過v o i d *來
實現。例如:

    bird* b;

    rock* r;

    void* v;

    v = r;

    b = v;

    C + +不容許這樣作,其編譯器將會給出一個出錯信息。若是真的想這樣作,必須顯式地使
用映射,通知編譯器和讀者(見1 8章C + +改進的映射語法)。

10.2   C++中的引用

    引用( & )像一個自動能被編譯器逆向引用的常量型指針。它一般用於函數的參數表中和函
數的返回值,但也能夠獨立使用。例如:

    int x;

    int & r = x;

    當建立了一個引用時,引用必須被初始化指向一個存在的對象。但也能夠這樣寫:

    int & q = 12;

這裏,編譯器分派了一個存儲單元,它的值被初始化爲 1 2,這樣這個引用就和這個存儲單元聯
繫上了。要點是任何引用必須和存儲單元聯繫。但訪問引用時,就是在訪問那個存儲單元。因
而,若是這樣寫:

    int x=0;

    int & a = x;

    a + + ;

    增長a事實上是增長x 。考慮一個引用的最簡單的方法是把它看成一個奇特的指針。這個指
針的一個優勢是沒必要懷疑它是否被初始化了(編譯器強迫它初始化),也沒必要知道怎樣對它逆

----------------------- Page 177-----------------------

                                         第10章  引用和拷貝構造函數           177
 下載

向引用(這由編譯器作)。
    使用引用時有必定的規則:

    1) 當引用被建立時,它必須被初始化。(指針則能夠在任什麼時候候被初始化。)
    2)  一旦一個引用被初始化爲指向一個對象,它就不能被改變爲對另外一個對象的引用。(指
針則能夠在任什麼時候候指向另外一個對象。)
    3) 不可能有N U L L引用。必須確保引用是和一塊合法的存儲單元關連。

10.2.1 函數中的引用

    最常常看見引用的地方是在函數參數和返回值中。當引用被用做函數參數時,函數內任何

對引用的更改將對函數外的參數改變。固然,能夠經過傳遞一個指針來作相同的事情,但引用
具備更清晰的語法。(若是願意的話,能夠把引用看做一個使語法更加便利的工具。)
    若是從函數中返回一個引用,必須像從函數中返回一個指針同樣對待。當函數返回時,無
論引用關連的是什麼都不該該離開,不然,將不知道指向哪個內存區域。
    這兒有一個例子:

    對函數f( ) 的調用缺少使用引用的方便性和清晰性,但很清楚這是傳遞一個地址。在函數 g
( ) 的調用中,地址經過引用被傳遞,但表面上看不出來。
    1. 常量引用

    僅當在R E F R N C E . C P P例程中的參數是很是量對象時,這個引用參數才能工做。若是是常
量對象,函數g ( )將不接受這個參數,這樣作是一件好事,由於這個函數將改變外部參數。如

----------------------- Page 178-----------------------

  178        C + +編程思想
                                                                       下載

果咱們知道這函數不妨礙對象的不變性的話,讓這個參數是一個常量引用將容許這個函數在任
何狀況下使用。這意味着,對於內部類型,這個函數不會改變參數,而對於用戶定義的類型,

該函數只能調用常量成員函數,並且不該當改變任何公共的數據成員。
    在函數參數中使用常量引用特別重要。這是由於咱們的函數也許會接受臨時的對象,這個
臨時對象是由另外一個函數的返回值創立或由函數使用者顯式地創立的。臨時對象老是不變的,
所以若是不使用常量引用,參數將不會被編譯器接受。看下面一個很是簡單的例子:

    調用f ( 1 )會產生一個編譯錯誤,這是由於編譯器必須首先創建一個引用。即編譯器爲一個
i n t類型分派存儲單元,同時將其初始化爲 1併爲其產生一個地址和引用捆綁在一塊兒。存儲的內
容必須是常量,由於改變它將使之變得沒有意義。對於全部的臨時對象,必須一樣假設它們是
不可存取的。當改變這種數據的時候,編譯器會指出錯誤,這是很是有用的提示,由於這個改
變會致使信息丟失。

    2. 指針引用
    在C語言中,若是想改變指針自己而不是它所指向的內容,函數聲明可能像這樣:

    void f (int**);

傳遞它時,必須取得指針的地址,像下面的例子:

    int I = 47;
    int* ip = &I;
    f (&ip);

    對於C + +中的引用,語法清晰多了。函數參數變成指針的引用,用不着取得指針的地址。
經過運行下面的程序,將會看到指針自己增長了,而不是它指向的內容增長了。

10.2.2 參數傳遞準則

    當給函數傳遞參數時,人們習慣上應該是經過常量引用來傳遞。雖然最初看起來彷佛僅出

----------------------- Page 179-----------------------

                                         第10章  引用和拷貝構造函數           179
 下載

於對效率的考慮(一般在設計和裝配程序時並不考慮效率),但像本章之後部分介紹的,這裏
將會存在不少的危險。拷貝構造函數須要經過值來傳遞對象,但這並不老是可行的。

    這種簡單習慣能夠大大提升效率:傳值方式須要調用構造函數和析構函數,然而若是不想
改變參數,則可經過常量引用傳遞,它僅須要將地址壓棧。
    事實上,只有一種狀況不適合用傳遞地址方式,這就是當傳值是惟一安全的途徑,不然將
會破壞對象時(而不是修改外部對象,這不是調用者一般指望的)。這是下一節的主題。

10.3  拷貝構造函數

    介紹了C + + 中的引用的基本概念後,咱們將講述一個更使人混淆的概念:拷貝構造函數,

它常被稱爲X ( X & )  (「X引用的X 」)。在函數調用時,這個構造函數是控制經過傳值方式傳遞和
返回用戶定義類型的根本所在。

10.3.1 傳值方式傳遞和返回

    爲了理解拷貝構造函數的須要,咱們來看一下 C語言在調用函數時處理經過傳值方式傳遞
和返回變量的方法。

    int f (int x, char c);

    int g = f (a,b);

    編譯器如何知道怎樣傳遞和返回這些變量?其實它天生就知道!由於它必須處理的類型範
圍是如此之小(c h a r,i n t,f l o a t,d o u b l e和它們的變量),這些信息都被內置在編譯器中。
    若是能瞭解編譯器怎樣產生彙編代碼和怎樣肯定調用函數 f ( )產生語句的,咱們就能獲得

下面的等價物:

    push b

    push a

    call f ( )

    add sp,4

    mov g, register a

    這個代碼已被認真整理過,使之具備廣泛意義—b和a 的表達式根據變量是全局變量(在
這種狀況下它們是_ b和_ a )或局部變量(編譯器將在堆棧指針上對其索引)將有差別。 g表達
式也是這樣。對f( )調用的形式取決於咱們的n a m e - m a n g l i n g方案,「寄存器a」取決於C P U寄存
器在咱們的彙編程序中是如何命名的。但無論代碼如何,邏輯是相同的。
    在C和C + + 中,參數是從右向左進棧,而後調用函數,調用代碼負責清理棧中的參數(這

一點說明了add sp,4 的做用)。可是要注意,經過傳值方式傳遞參數時,編譯器簡單地將參數拷
貝壓棧—編譯器知道拷貝有多大,並知道爲壓棧的參數產生正確的拷貝。
    f( ) 的返回值放在寄存器中。編譯器一樣知道返回值的類型,由於這個類型是內置於語言
中的,因而編譯器能夠經過把返回值放在寄存器中返回它。拷貝這個值的比特位等同於拷貝對
象。

    1. 傳遞和返回大對象
    如今來考慮用戶定義的類型。若是建立了一個類,咱們但願傳遞該類的一個對象,編譯器
怎麼知道作什麼?這是編譯器的做者所不知的非內部數據類型,是別人建立的類型。
    爲了研究這個問題,咱們首先從一個簡單的結構開始,這個結構太大以致於不能在寄存器
中返回。

----------------------- Page 180-----------------------

  180        C + +編程思想
                                                                       下載

    在這裏列出彙編代碼輸出有點複雜,由於大多數編譯器使用「 h e l p e r 」函數而不是設置所
有功能性內置。在m a i n ( )函數中,正如咱們猜想的,首先調用函數b i g f u n ( ),整個B 的內容被壓
棧。(咱們可能發現有些編譯器把B 的地址和大小裝入寄存器,而後調用h e l p e r函數把它壓棧。)
    在先前的例子中,調用函數以前要把參數壓棧。然而,在PA S S T R U C . C P P中,將看到附加

的動做:在函數調用以前,B 2 的地址壓棧,雖然它明顯不是一個參數。爲了理解這裏發生的
事,必須瞭解當編譯器調用函數時對編譯器的約束。
    2.  函數調用棧的框架
    當編譯器爲函數調用產生代碼時,它首先把全部的參數壓棧,而後調用函數。在函數內部,
產生代碼,向下移動棧指針爲函數局部變量提供存儲單元。(在這裏「下」是相對的,在壓棧

時,機器棧指針可能增長也可能減少。)在彙編語言C A L L中,C P U把程序代碼中的函數調用指
令的地址壓棧,因此彙編語言R E T U R N能夠使用這個地址返回到調用點。固然,這個地址是非
常神聖的,由於沒有它程序將迷失方向。這兒提供一個在 C A L L後棧框架的樣子,此時在函數
中已爲局部變量分配了存儲單元。

                             函數參數

                             返回地址

                             局部變量

    函數的其餘部分產生的代碼徹底按照這個方法安排內存,所以它能夠謹慎地從函數參數和

局部變量中存取而不觸及返回地址。我稱函數調用過程當中被函數使用的這塊內存爲函數框架
(function frame) 。另外,試圖從棧中獲得返回值是合乎道理的。由於編譯器簡單地把返回值壓
棧,函數能夠返回一個偏移值,它告訴返回值的開始在棧中所處的位置。
    3. 重入
    由於在C和C + + 的函數支持中斷,因此這將出現語言重入的難題。同時,它們也支持函數

遞歸調用。這就意味着在程序執行的任什麼時候候,中斷均可以發生而不打亂程序。固然,編寫中
斷服務程序(I S R )的做者負責存儲和還原他所使用的全部的寄存器,但若是 I S R須要使用深

----------------------- Page 181-----------------------

                                         第10章  引用和拷貝構造函數           181
 下載

入堆棧的內存時,就要當心了。(能夠把I S R當作沒有參數和返回值是v o i d 的普通函數,它存儲
和還原C P U的狀態。有些硬件事件觸發一個I S R函數的調用,而不是在程序中顯式地調用。)

    如今來想象一下,若是調用函數試圖從普通函數中返回堆棧中的值將會發生什麼。由於不
能觸及堆棧返回地址以上任何部分,因此函數必須在返回地址下將值壓棧。但當彙編語言
R E T U R N執行時,堆棧指針必須指向返回地址(或正好位於它下面,這取決於機器。),因此恰
好在R E T U R N語句以前,函數必須將堆棧指針向上移動,以便清除全部局部變量。若是咱們試
圖從堆棧中的返回地址下返回數值,由於中斷可能此時發生,此時是咱們最易被攻擊的時候。

這個時候I S R將向下移動堆棧指針,保存返回地址和局部變量,這樣就會覆蓋掉咱們的返回值。
    爲了解決這個問題,在調用函數以前,調用者應負責在堆棧中爲返回值分配額外的存儲單
元。然而,C不是按照這種方法設計的,C + +也同樣。正如咱們不久將看到的, C + +編譯器使
用更有效的方案。
    咱們的下一個想法多是在全局數據區域返回數值,但這不可行。重入意味着任何函數可

以中斷任何其餘的函數,包括與之相同的函數。所以,若是把返回值放在全局區域,咱們可能
又返回到相同的函數中,這將重寫返回值。對於遞歸也是一樣道理。
    惟一安全的返回場所是寄存器,問題是當寄存器沒有足夠大用於存放返回值時該怎麼作。
答案是把返回值的地址像一個函數參數同樣壓棧,讓函數直接把返回值信息拷貝到目的地。這
樣作不只解決了問題,並且效率更高。這也是在PA S S T R U C . C C P中main( ) 中bigfun( )  調用以前

將B 2 的地址壓棧的緣由。若是看了bigfun( ) 的彙編輸出,能夠看到它存在這個隱藏的參數並在
函數內完成向目的地的拷貝。
    4. 位拷貝(b i t c o p y )與初始化
    迄今爲止,一切都很順利。對於傳遞和返回大的簡單結構有了可以使用的方法。但注意咱們
所用的方法是從一個地方向另外一個地方拷貝比特位,這對於 C着眼於變量的原始方法固然進行

得很好。但在C + +中,對象比一組比特位要豐富得多,由於對象具備含義。這個含義也許不能
由它具備的位拷貝來很好地反映。
    下面來考慮一個簡單的例子:一個類在任什麼時候候知道它存在多少個對象。從第 9章,咱們
瞭解到能夠經過包含一個靜態( s t a t i c )數據成員的方法來作到這點。

----------------------- Page 182-----------------------

  182        C + +編程思想
                                                                       下載

    h o w m a n y類包括一個靜態i n t類型變量和一個用以報告這個變量的靜態成員函數print( ) ,這
個函數有一個可選擇的消息參數。每當一個對象產生時,構造函數增長記數,而對象銷燬時,
析構函數減少記數。
    然而,輸出並非咱們所指望的那樣:

    在h生成之後,對象數是1,這是對的。咱們但願在f( )調用後對象數是2,由於h 2也在範圍
內。然而,對象數是0,這意味着發生了嚴重的錯誤。這從結尾兩個析構函數執行後使得對象
數變爲負數的事實獲得確認,有些事根本就不該該發生。
    讓咱們來看一下函數f( )經過傳值方式傳入參數那一處。原來的對象h存在於函數框架以外,
同時在函數體內又增長了一個對象,這個對象是傳值方式傳入的對象的拷貝。然而,參數的傳

遞是使用C 的原始的位拷貝的概念,但C++ howmany類須要真正的初始化來維護它的完整性。
因此,缺省的位拷貝不能達到預期的效果。
    當局部對象出了調用的函數f( )範圍時,析構函數就被調用,析構函數使o b j e c t _ c o u n t減少。
因此,在函數外面,o b j e c t _ c o u n t等於0 。h 2對象的建立也是用位拷貝產生的,因此,構造函數
在這裏也沒有調用。當對象h和h 2 出了它們的做用範圍時,它們的析構函數又使o b j e c t _ c o u n t值

變爲負值。

10.3.2 拷貝構造函數

    上述問題的出現是由於編譯器對如何從現有的對象產生新的對象做了假定。當經過傳值的

----------------------- Page 183-----------------------

                                         第10章  引用和拷貝構造函數           183
 下載

方式傳遞一個對象時,就創立了一個新對象,函數體內的對象是由函數體外的原來存在的對象
傳遞的。從函數返回對象也是一樣的道理。在表達式中:

    howmany h2 = f(h);

先前未創立的對象h 2是由函數f( ) 的返回值建立的,因此又從一個現有的對象中建立了一個新
對象。

    編譯器假定咱們想使用位拷貝(b i t c o p y )來建立對象。在許多狀況下,這是可行的。但在
h o w m a n y類中就行不通,由於初始化不只僅是簡單的拷貝。若是類中含有指針又將出現問題:
它們指向什麼內容,是否拷貝它們或它們是否與一些新的內存塊相連?
    幸運的是,咱們能夠介入這個過程,並能夠防止編譯器進行位拷貝 ( b i t c o p y )。每當編譯器
須要從現有的對象建立新對象時,咱們能夠經過定義咱們本身的函數作這些事。由於咱們是在

建立新對象,因此,這個函數應該是構造函數,而且傳遞給這個函數的單一參數必須是咱們創
立的對象的源對象。可是這個對象不能傳入構造函數,由於咱們試圖定義處理傳值方式的函數
按句法構造傳遞一個指針是沒有意義的,畢竟咱們正在從現有的對象建立新對象。這裏,引用
就起做用了,能夠使用源對象的引用。這個函數被稱爲拷貝構造函數,它常常被說起爲 X ( X & )
(它是被稱爲X的類的外在表現)。

    若是設計了拷貝構造函數,當從現有的對象建立新對象時,編譯器將不使用位拷貝
( b i t c o p y )。編譯器老是調用咱們的拷貝構造函數。因此,若是咱們沒有設計拷貝函數,編譯器
將作一些判斷,但咱們徹底能夠接管這個過程的控制。
    如今咱們能夠關注HO W M A N Y. C P P中的問題了:

----------------------- Page 184-----------------------

  184        C + +編程思想
                                                                      下載

    這兒有一些新的手法,要很好地理解。首先,字符緩衝器 i d起着對象識別做用,因此能夠
判斷被打印的信息是哪個對象的。在構造函數內,能夠設置一個標識符(一般是對象的名字)。
使用標準C庫函數strncpy( )把標識符拷貝給i d 。strncpy( )只拷貝必定數目的字符,這是爲防止
超出緩衝器的限度。
    其次是拷貝構造函數h o w m a n y 2 ( h o w m a n y 2 & ) 。拷貝構造函數能夠僅從現有的對象創立新

對象,因此,現有對象的名字被拷貝給i d,i d後面跟着單詞「c o p y」,這樣咱們就能瞭解它是從
哪裏拷貝來的。注意,使用標準C庫函數strncat( )拷貝字符給i d也得防止超過緩衝器的限度。
    在拷貝構造函數內部,對象數目會像普通構造函數同樣的增長。這意味着當參數傳遞和返
回時,咱們能獲得準確的對象數目。
    print( ) 函數已經被修改,用於打印消息、對象標識符和對象數目。如今print( ) 函數必須存

取具體對象的i d數據,因此再也不是s t a t i c成員函數。
    在main( ) 函數內部,能夠看到又增長了一次函數f( )             的調用。但此次使用了普通的C語言
調用方式,忽略了函數的返回值。既然如今知道了值是如何返回的(即在函數體內,代碼處理
返回過程並把結果放在目的地,目的地地址做爲一個隱藏的參數傳遞。),咱們可能想知道返回

----------------------- Page 185-----------------------

                                         第10章  引用和拷貝構造函數           185
 下載

值被忽略將會發生什麼事情。程序的輸出將對此做出解釋。
    在顯示輸出以前,這兒提供了一個小程序,這個小程序使用i o s t r e a m s爲文件加入行號:

    整個文件被讀入s t r s t r e a m (咱們能夠從中寫和讀),i f s t r e a m在其範圍內被關閉。而後爲相
同的文件建立一個ofstream, ofstream重寫這個文件。getline( )從s t r s t r e a m中一次取一行,加上行

號後寫回文件。
    行號以右對齊方式按2個字段寬打印,因此輸出仍然按原來的方式排列。能夠改變程序,
在程序中加入可選擇的第2個命令行參數。這個命令行參數能夠讓用戶選擇字段寬,或能夠作
得更聰明些,經過計算文件行數自動決定字段寬度。
    當L I N E N U M . C P P被應用於H O W M A N Y 2 . O U T時,結果以下:

----------------------- Page 186-----------------------

  186        C + +編程思想
                                                                       下載

    正如咱們所但願的,第一件發生的事是爲h調用普通的構造函數,對象數增長爲1。但在進

入函數f( ) 時,拷貝構造函數被編譯器調用完成傳值過程。在f( ) 內建立了一個新對象,它是h的

拷貝(所以被稱爲「h拷貝」),因此對象數變成2,這是拷貝構造函數做用的結果。

    第8行顯示了從f( )返回的開始狀況。但在局部變量「 h拷貝」銷燬之前(在函數結尾這個

局部變量便出了範圍),它必須被拷入返回值,也就是h 2 。先前未建立的對象(h 2 )是從現有

的對象(在函數f( ) 內的局部變量)建立的,因此第9行拷貝構造函數固然又被使用。如今,對

於h 2的標識符,名字變成了「h拷貝的拷貝」。由於它是從拷貝拷過來的,這個拷貝是函數f( )

內部對象。在對象返回以後,函數結束以前,對象數暫時變爲 3,但此後內部對象「h拷貝」

被銷燬。在1 3行完成對f( )調用後,僅有2個對象h和h2                。這時咱們能夠看到h 2最終是「h拷貝

的拷貝」。

    • 臨時對象

    1 5行開始調用f ( h ),此次調用忽略了返回值。在1 6行能夠看到剛好在參數傳入以前,拷貝

構造函數被調用。和前面同樣,2 1行顯示了爲返回值而調用拷貝構造函數。可是,拷貝構造函

數必須有一個做爲它的目的地(t h i s指針)的工做地址。對象返回到哪裏?

    每當編譯器須要正確地計算一個表達式時,編譯器能夠建立一個臨時對象。在這種狀況下,

編譯器建立一個咱們甚至看不見的對象做爲函數 f( )忽略了的返回值的目的地地址。這個臨時

對象的生存期應儘量地短,這樣,空間就不會被這些等待被銷燬且佔珍貴資源的臨時對象搞

亂。在一些狀況下,臨時對象可能當即傳遞給另外的函數。但在如今這種狀況下,在函數調用

----------------------- Page 187-----------------------

                                         第10章  引用和拷貝構造函數            187
 下載

以後,不須要臨時對象,因此一旦函數調用以對內部對象調用析構函數( 2 3和2 4行)的方式結

束,臨時對象就被銷燬(2 5和2 6行)。
    在2 8 - 3 1行,對象h 2被銷燬了,接着對象h被銷燬。對象記數很是正確地回到了0。

10.3.3 缺省拷貝構造函數

    由於拷貝構造函數實現傳值方式的參數傳遞和返回,因此在這種簡單結構狀況下,編譯器
將有效地建立一個缺省拷貝構造函數,這很是重要。在 C 中也是這樣。然而,直到目前所看到
的一切都是缺省的原始行爲:位拷貝(b i t c o p y )。
    當包括更復雜的類型時,若是沒有建立拷貝構造函數, C + +編譯器也將自動地爲咱們建立
拷貝構造函數。這是由於在這裏用位拷貝是沒有意義的,它並不能達到咱們的目的。

    這兒有一個例子顯示編譯器採起的更具智能的方法。設想咱們建立了一個包括幾個現有類
的對象的新類。這個建立類的方法被稱爲組合(c o m p o s i t i o n ),它是從現有類建立新類的方法
之一。如今,假設咱們用這個方法快速建立一個新類來解決某個問題。由於咱們還不知道拷貝
構造函數,因此沒有建立它。下面的例子演示了當編譯器爲咱們的新類建立缺省拷貝構造函數
時編譯器幹了那些事。

----------------------- Page 188-----------------------

  188        C + +編程思想
                                                                       下載

    類w i t h C C有一個拷貝構造函數,這個函數只是簡單地宣佈它被調用。在類 c o m p o s i t e 中,
使用缺省的構造函數建立一個w i t h C C類的對象。若是在類w i t h C C 中根本沒有構造函數,編譯
器將自動地建立一個缺省的構造函數。不過在這種狀況下,這個構造函數什麼也不作。然而,
若是咱們加了一個拷貝構造函數,咱們就告訴了編譯器咱們將本身處理構造函數的建立,編譯
器將再也不爲咱們建立缺省的構造函數。而且除非咱們顯式地建立一個缺省的構造函數,就如同
爲類w i t h C C所作的那樣,不然,編譯器會指示出錯。
    類w o C C沒有拷貝構造函數,但它的構造函數將在內部緩衝器存儲一個信息,這個信息可
以使用print( ) 函數打印出來。這個構造函數在類c o m p o s i t e構造函數的初始化表達式表(初始化
表達式表已在第7章簡單地介紹過了,並將在第 1 3章中全面介紹)中被顯式地調用。這樣作的
緣由在之後將會明白。
    類c o m p o s i t e 既含有w i t h C C類的成員對象又含有 w o C C類的成員對象(注意內嵌的對象
W O C C在構造函數初始化表達式表中被初始化)。類c o m p o s i t e沒有顯式地定義拷貝構造函數。
然而,在main( ) 函數中,按下面的定義使用拷貝構造函數建立了一個對象。

    composite c2 = c ;

    類c o m p o s i t e的拷貝構造函數由編譯器自動建立,程序的輸出顯示了它是如何被建立的。
    爲了對使用組合(和繼承的方法,將在第1 3章介紹)的類建立拷貝構造函數,編譯器遞歸
地爲全部的成員對象和基本類調用拷貝構造函數。若是成員對象也含有別的對象,那麼後者的
拷貝構造函數也將被調用。因此,在這裏,編譯器也爲類 w i t h C C調用拷貝構造函數。程序的
輸出顯示了這個構造函數被調用。由於w o C C沒有拷貝構造函數,編譯器爲它建立一個,它是
缺省的位拷貝(b i t c o p y )的行爲,編譯器在類c o m p o s i t e的拷貝構造函數內部調用這個缺省的
拷貝構造函數,因而在m a i n 中調用的composite::print( )顯示的c 2 . W O C C的內容與c . W O C C 內容
將是相同的。編譯器得到一個拷貝構造函數的過程被稱爲memberwise initialization 。
    最好的方法是建立本身的拷貝構造函數而不讓編譯器建立。這樣就能保證程序在咱們的控
制之下。

10.3.4 拷貝構造函數方法的選擇

    如今,咱們可能已頭暈了。咱們可能想,怎樣才能沒必要了解拷貝構造函數就能寫一個具備

----------------------- Page 189-----------------------

                                         第10章  引用和拷貝構造函數           189
 下載

必定功能的類。可是咱們別忘了:僅當準備用傳值的方式傳遞類對象時,才須要拷貝構造函數。
若是不須要這麼作,就不要拷貝構造函數。
    1. 防止傳值方式傳遞
    咱們也許會說:「若是我本身不寫拷貝構造函數,編譯器將爲我建立。因此,我怎麼能保
證一個對象永遠不會被經過傳值方式傳遞呢?」
    有一個簡單的技術防止經過傳值方式傳遞:聲明一個私有( p r i v a t e )拷貝構造函數。咱們

甚至沒必要去定義它,除非咱們的成員函數或友元( f r i e n d )函數須要執行傳值方式的傳遞。如
果用戶試圖用傳值方式傳遞或返回對象,編譯器將會發出一個出錯信息。這是由於拷貝構造函
數是私有的。由於咱們已顯式地聲明咱們接管了這項工做,因此編譯器再也不建立缺省的拷貝構
造函數。
    這兒提供了一個例子:

    注意使用更普通的形式

    noCC(const noCC&);

    這裏使用了c o n s t。
    2. 改變外部對象的函數
    通常來說,引用語法比指針語法更好,然而對於讀者來講,它使得意思變得模糊。例如,
在i o s t r e a m s庫函數中,一個重載版函數get( )是用一個c h a r &做爲參數,函數經過插入get( ) 的結
果而改變它的參數。然而,當咱們閱讀使用這個函數的代碼時,咱們不會當即明白外面的對象

正被改變:

    char c;

    c i n . g e t ( c ) ;

    事實上函數調用看起來像一個傳值傳遞,暗示着外部對象沒有被改變。
    正由於如此,當傳遞一個可被修改的參數時,從代碼維護的觀點看,使用指針可能安全些。
因此除非咱們打算經過地址修改外部對象(這個地址經過非 c o n s t指針傳遞),要否則都用c o n s t
引用傳遞地址。由於這樣,讀者更容易讀懂咱們的代碼。

----------------------- Page 190-----------------------

  190        C + +編程思想
                                                                       下載

10.4  指向成員的指針(簡稱成員指針)

    指針是指向一些內存地址的變量,既能夠是數據的地址也能夠是函數的地址。因此,能夠
在運行時改變指針指向的內容。除了 C + + 的成員指針( p o i n t e r- t o - m e m b e r )選擇的內容是在類之
外,C + + 的成員指針聽從一樣的原則。困難的是全部的指針須要一個地址,但在類內部沒有地
址;選擇一個類的成員意味着在類中偏移。只有把這個偏移和具體對象的開始地址結合,才能

獲得實際地址。成員指針的語法要求選擇一個對象的同時逆向引用成員指針。
    爲了理解這個語法,先來考慮一個簡單的結構:

    struct simple { int a ; };

    若是有一個這個結構的指針s p和對象s o,能夠經過下面方法選擇成員:

    sp->a ;

    so.a ;

    如今,假設有一個普通的指向i n t e g e r的指針i p 。爲了取得i p指向的內容,用一個*號逆向引
用指針的引用。

    *ip = 4 ;

    最後,考慮若是有一個指向一個類對象成員,甚至假設它表明對象內必定的偏移,將會發
生什麼?爲了取得指針指向的內容,必須用 *號逆向引用。可是,它只是一個對象內的偏移,
因此還必需要指定那個對象。所以, *號要和逆向引用的對象結合。像下面使用 s i m p l e類的例
子:

    sp->*pm = 47 ;

    so.*pm = 47 ;

    因此,對於指向一個對象的指針新的語法變爲- > *,對於一個對象或引用則爲. *。如今,讓
咱們看看定義p m 的語法是什麼?其實它像任何一個指針,必須說出它指向什麼類型。而且,
在定義中也要使用一個‘ *’號。惟一的區別只是必須說出這個成員指針使用什麼類的對象。

固然,這是用類名和全局操做符實現的。所以,可表示以下:

    int simple::*pm ;

    當咱們定義它時(或任何別的時間),也能夠初始化成員指針:

    int simple::*pm = &simple::a ;

    由於引用到一個類而非那個類的對象,因此沒有                         s i m p l e : : a 的確切「地址」。因

而,& s i m p l e : : a僅可做爲成員指針的語法表示。

函數

    這裏提供一個爲成員函數產生成員指針(p o i n t e r- t o - m e m b e r )的例子。指向函數的指針定
義像下面的形式:

    int (*fp)(float) ;

    ( * f p )的圓括號用來迫使編譯器正確判判定義。沒有圓括號,這個表達式就是一個返回 i n t *
值的函數。
    爲了定義和使用一個成員函數的指針,圓括號扮演一樣重要的角色。假設在一個結構內有

一個函數:

    struct simple2 { int f(float); } ;

----------------------- Page 191-----------------------

                                         第10章  引用和拷貝構造函數           191
 下載

經過給普通函數插入類名和全局操做符就能夠定義一個指向成員函數的指針:

    int (simple2::*fp) (float) ;

當建立它時或其餘任什麼時候候,能夠對它初始化:

    int (simple2::*fp) (float) = &simple2::f ;

    和其餘普通函數同樣,&號是可選的;能夠用不帶參數表的函數標識符來表示地址:

    fp = simple2::f ;

    • 一個例子
    在程序運行時,咱們能夠改變指針所指的內容。所以在運行時咱們就能夠經過指針選擇來
改變咱們的行爲,這就爲程序設計提供了重要的靈活性。成員指針也同樣,它容許在運行時選
擇一個成員。特別的,當咱們的類只有公有 ( p u b l i c )成員函數(數據成員一般被認爲是內部實

現的一部分)時,就能夠用指針在運行時選擇成員函數,下面的例子正是這樣:

    固然,指望通常用戶建立如此複雜的表達式不是特別合乎情理。若是用戶必須直接操做成
員指針,那麼t y p e d e f是適合的。爲了安排得當,能夠使用成員指針做爲內部執行機制的一部分。

這兒提供一個前述的在類內使用成員指針的例子。用戶所要作的是傳遞一個數字以選擇一個函
數。[ 1 ]

  [1] 感謝Owen Mortensen提供這個例子。

----------------------- Page 192-----------------------

  192        C + +編程思想
                                                                       下載

    在類接口和main( ) 函數裏,能夠看到,包括函數自己在內的整個實現被隱藏了。代碼甚至
必須請求Count( ) 函數。用這個方法,類的實現者能夠改變大量函數而不影響使用這個類的代
碼。
    在構造函數中,成員指針的初始化彷佛被過度地指定了。是否能夠這樣寫:

    fptr[1] = &g ;

    由於名字g在成員函數中出現,是否能夠自動地認爲在這個類範圍內呢?問題是這不符合
成員函數的語法,它的語法要求每一個人,尤爲編譯器,可以判斷將要進行什麼。類似地,當成
員函數被逆向引用時,它看起來像這樣:

    (this->*fptr[i]) (j);

    它仍被過度地指定了,t h i s彷佛多餘。正如前面所講的,當它被逆向引用時,語法也須要
成員指針老是和一個對象綁定在一塊兒。

10.5  小結

    C + +的指針和C中的指針是很是類似的,這是很是好的。不然,許多C代碼在C + +中將不會
被正確地編譯。僅在出現危險賦值的地方,編譯器會產生出錯信息。假設咱們確實想這樣賦值,
編譯器的出錯能夠用簡單的(和顯式的!)映射(c a s t)清除。

    C + +還從A l g o l和P a s c a l 中引進引用( r e f e r e n c e )概念,引用就像一個能自動被編譯器逆向引
用的常量指針同樣。引用佔一個地址,但咱們能夠把它當作一個對象。引用是操做符重載語

法(下一章的主題)的重點,它也爲普通函數值傳遞和返回對象增長了語法的便利。
    拷貝構造函數採用相同類型的對象引用做爲它的參數,它能夠被用來從現有的類建立新類。
當用傳值方式傳遞或返回一個對象時,編譯器自動調用這個拷貝構造函數。雖然,編譯器將自

----------------------- Page 193-----------------------

                                         第10章  引用和拷貝構造函數           193
 下載

動地建立一個拷貝構造函數,可是,若是認爲須要爲咱們的類建立一個拷貝構造函數,應該自
己定義它以確保正確的操做。若是不想經過傳值方式傳遞和返回對象,應該建立一個私有的

( p r i v a t e )拷貝構造函數。
    成員指針和普通指針同樣具備相同的功能:能夠在運行時選取特定存儲單元(數據或函數)。
成員指針只和類成員一塊兒工做而不和全局數據或函數一塊兒工做。經過使用成員指針,咱們的程
序設計能夠在運行時靈活地改變。

10.6  練習

    1. 寫一個函數,這個函數用一個c h a r &做參數而且修改該參數。在main( ) 函數裏,打印一

個c h a r變量,使用這個變量作參數,調用咱們設計的函數。而後,再次打印此變量以證實它已
被改變。這樣作是如何影響程序的可讀性的?
    2.  寫一個有拷貝構造函數的類,在拷貝構造函數裏用 c o u t 自我聲明。如今,寫一個函數,
這個函數經過傳值方式傳入咱們新類的對象。寫另外一個函數,在這個函數內建立這個新類的局
部對象,經過傳值方式返回這個對象。調用這些函數以證實經過傳值方式傳遞和返回對象時,

拷貝構造函數確實悄悄地被調用了。
    3.  努力發現如何使得咱們的編譯器產生彙編語言 ,  並請爲PA S S T R U C . C P P產生彙編代碼。
跟蹤和揭示咱們的編譯器爲傳遞和返回大結構產生代碼的方法。

----------------------- Page 194-----------------------

                                                                      下載

                     第11章  運算符重載

    運算符重載只是一種「語法修飾」,這意味着它是另外一種調用函數的方法。

    不一樣之處是對於函數的參數不是出如今圓括號內,而是在咱們總認爲是運算符的字符的附
近。
    但在C + + 中,能夠定義一個和類一塊兒工做的新運算符。除了這個名字函數以關鍵字
o p e r a t o r開始,以運算符自己結束之外,這個定義和一個普通函數是同樣的。這是僅有的差異。
它像其餘函數同樣也是一個函數,當編譯器看到它以適當的模式出現時,就調用它。

11.1  警告和確信

    對於運算符重載,人們容易變得過於熱心。首先,它是一個娛樂玩具。注意,它僅僅是一
個語法修飾,是另一種調用函數的方法而已。用這種眼光看,沒有理由重載一個運算符,除
非它會使包含咱們的類的代碼變得更易寫,尤爲是更易讀。(記住,讀代碼的狀況更多)若是
不是這種狀況,就沒必要麻煩去重載運算符。
    對於運算符重載,另一個一般的反映是恐慌:忽然,C運算符再也不有熟悉的意思。「全部

的東西都改變了,個人全部C代碼將作不一樣的事情!」但這不是事實。全部用於僅包含內部數
據類型的表達式的運算符是不可能被改變的。咱們永遠不能重載下面的運算符使執行的行爲不
同。

    1 << 4;

或者重載運算符使得下面的表達式有意義。

    1.414 << 2;

僅僅是包含用戶自定義類型的表達式能夠有重載的運算符。

11.2  語法

    定義一個重載運算符就像定義一個函數,只是該函數的名字是 o p e r a t o r @,這裏@表明運
算符。函數參數表中參數的個數取決於兩個因素:
    1) 運算符是一元的(一個參數)仍是二元的(兩個參數)。

    2)  運算符被定義爲全局函數(對於一元是一個參數,對於二元是兩個參數)仍是成員函數
(對於一元沒有參數,對於二元是一個參數— 對象變爲左側參數)。
    這裏有一個很小的類來顯示運算符重載語法。

----------------------- Page 195-----------------------

                                                 第11章 運算符重載         195
 下載

    這兩個重載的運算符被定義爲內聯成員函數。對於二元運算符,單個參數是出如今運算符
右側的那個。當一元運算符被定義爲成員函數時,沒有參數。成員函數被運算符左側的對象調
用。
    對於非條件運算符(條件運算符一般返回一個布爾值),若是兩個參數是相同的類型,希

望返回和運算相同類型的對象或引用。若是它們不是相同類型,它做什麼樣的解釋就取決於程
序設計者。用這種方法能夠組合複雜的表達式:

    K += I + J ;

    運算符+號產生一個新的整數(臨時的),這個整數被用做運算符‘+ = ’的r v參數。一旦這
個臨時整數再也不須要時就被消除。

11.3  可重載的運算符

    雖然能夠重載幾乎全部C 中可用的運算符,但使用它們是至關受限制的。特別地,不能結
合C中當前沒有意義的運算符(例如 * *求冪),不能改變運算符的優先級,不能改變運算符的

參數個數。這樣限制有意義—全部這些行爲產生的運算符只會形成意思混淆而不是使之清
楚。
    下面兩個小部分給出全部「常常用的」運算符的例子,這些被重載的運算符的形式會常常
用到。

11.3.1 一元運算符

    下面的例子顯示了全部一元運算符重載的語法,它們既以全局函數形式又以成員函數形

式表示。它們將擴充先前顯示的類i n t e g e r和加入新類b y t e 。具體運算符的意思取決於如何使用
它們。

----------------------- Page 196-----------------------

     196   C + +編程思想
                                                                        下載

----------------------- Page 197-----------------------

                                                  第11章 運算符重載               197
下載

----------------------- Page 198-----------------------

     198   C + +編程思想
                                                                        下載

----------------------- Page 199-----------------------

                                                第11章 運算符重載        199
 下載

    根據參數傳遞的方法,將函數分組。如何傳遞和返回參數的方針在後面給出。上面的形式

(和下一小節的形式)是典型的使用形式,因此當重載本身的運算符時能夠以它們做爲範式開始。
    •  自增和自減
    重載的+ +和- -號運算符出現了兩難選擇的局面,這是由於但願根據它們出如今它們做用的
對象前面(前綴)仍是後面(後綴)來調用不一樣的函數。解決是很簡單的,但一些人在開始時
卻發現它們容易使人混淆。例如當編譯器看到 + + a               (先自增)時,它就調用o p e r a t o r + + ( a ) ;但當

編譯器看到a + +時,它就調用o p e r a t o r + + ( a , i n t )。即編譯器經過調用不一樣的函數區別這兩種形式。
在U N A RY. C P P成員函數版中,若是編譯器看到+ + b,它就產生一個對B::perator++( ) 的調用;
若是編譯器看到b + +,它就產生一個對B : : o p e r a t o r + + ( i n t ) 的調用。
    除非對於前綴和後綴版本用不一樣的函數調用,不然用戶永遠看不到它動做的結果。然而,
實質上兩個函數調用有不一樣的署名,因此它們和兩個不一樣函數體相連。編譯器爲 i n t參數(由於

這個值永遠不被使用,因此它永遠不會被賦給一個標識符)傳遞一個啞元常量值用來爲後綴版
產生不一樣的署名。

11.3.2 二元運算符

    下面的清單是用二元運算符重複U N A RY. C P P的例子。全局版本和成員函數版本都在裏面。

----------------------- Page 200-----------------------

     200   C + +編程思想
                                                                        下載

----------------------- Page 201-----------------------

                                                  第11章 運算符重載               201
下載

----------------------- Page 202-----------------------

     202   C + +編程思想
                                                                        下載

----------------------- Page 203-----------------------

                                                  第11章 運算符重載               203
下載

----------------------- Page 204-----------------------

     204   C + +編程思想
                                                                        下載

----------------------- Page 205-----------------------

                                                  第11章 運算符重載               205
下載

----------------------- Page 206-----------------------

     206   C + +編程思想
                                                                        下載

----------------------- Page 207-----------------------

                                                  第11章 運算符重載               207
下載

----------------------- Page 208-----------------------

     208   C + +編程思想
                                                                        下載

----------------------- Page 209-----------------------

                                                 第11章 運算符重載              209
 下載

    能夠看到運算符‘= ’僅容許做爲成員函數。這將在後面解釋。
    做爲總的方針,咱們注意到在運算符重載中全部賦值運算符都有代碼用於覈對自賦值 ( s e l f -

a s s i g n m e n t ) 。在一些狀況下,這是不須要的。例如,能夠用運算符‘ + = ’寫A + = A ,使得A 自
身相加。最重要的核對自賦值的地方是運算符‘= ’,由於複雜的對象可能由於它而發生災難性

----------------------- Page 210-----------------------

  210        C + +編程思想
                                                                      下載

的結果(在一些狀況下這不會有問題,無論怎麼說,在寫運算符‘=’時,應該當心一些)。
    先前的兩個例子中的運算符重載處理單一類型。也可能重載運算符處理混合類型,因此可

以「把蘋果加到橙子裏」。然而,在開始進行運算符重載以前,應該看一下本章後面有關自動
類型轉換部分。常常在正確的地方使用類型轉換能夠減小許多運算符重載。

11.3.3 參數和返回值

    開始看U N A RY. C P P和B I N A RY. C P P例子時會發現參數傳遞和返回方法徹底不一樣,這彷佛有
點使人混淆。雖然能夠用任何想用的方法傳遞和返回參數,但這些例子方法卻不是隨便選擇的。
它們遵照一種很是合乎邏輯的模式,咱們在大部分狀況下都應選擇這種模式:

    1) 對於任何函數參數,若是僅須要從參數中讀而不改變它,缺省地應當按 c o n s t引用來傳
遞它。普通算術運算符(像+和-號等)和布爾運算符不會改變參數,因此以c o n s t引用傳遞是使
用的主要方式。當函數是一個類成員的時候,就轉換爲 c o n s t成員函數。只是對於會改變左側
參數的賦值運算符( o p e r a t o r- a s s i g n m e n t,像+ = )和運算符‘=’,左側參數纔不是常量( c o n s t a n t ),
但由於參數將被改變,因此參數仍然按地址傳遞。

    2) 應該選擇的返回值取決於運算符所指望的類型。(能夠對參數和返回值作任何想作的事)
若是運算符的效果是產生一個新值,將須要產生一個做爲返回值的新對象。例如,
i n t e g e r : : o p e r a t o r +必須生成一個操做數之和的i n t e g e r對象。這個對象做爲一個c o n s t經過傳值方
式返回,因此做爲一個左值結果不會被改變。
    3)  全部賦值運算符改變左值。爲了使得賦值結果用於鏈式表達式(像 A = B = C ),應該可以

返回一個剛剛改變了的左值的引用。但這個引用應該是 c o n s t仍是n o n c o n s t呢?雖然咱們是從左
向右讀表達式A = B = C ,但編譯器是從右向左分析這個表達式,因此並不是必定要返回一個
n o n c o n s t 值來支持鏈式賦值。然而人們有時但願可以對剛剛賦值的對象進行運算,例如
(A = B ). f o o (),這是B賦值給A後調用foo( ) 。所以全部賦值運算符的返回值對於左值應該是
n o n c o n s t引用。

    4)  對於邏輯運算符,人們但願至少獲得一個 i n t返回值,最好是b o o l返回值。(在大多數編
譯器支持C + +內置b o o l類型以前開發的庫函數使用i n t或t y p e d e f等價物)。
    5) 由於有前綴和後綴版本,因此自增和自減運算符出現了兩難局面。兩個版本都改變對象,
因此不能把這個對象看做一個 c o n s t 。所以,前綴版本返回這個對象被改變後的值。這樣,用
前綴版本咱們只需返回* t h i s做爲一個引用。由於後綴版本返回改變以前的值,因此被迫建立一

個表明這個值的單個對象並返回它。所以,若是想保持咱們的本意,對於後綴必須經過傳值方
式返回。(注意,咱們常常會發現自增和自減運算返回一個int  值或b o o l值,例如用來指示是否
有一個循環子( i t e r a t o r )在表的結尾)。如今問題是:這些應該按c o n s t被返回仍是按n o n c o n s t被返
回?若是容許對象被改變,一些人寫了表達式(++A).foo( ),則foo( )做用在A上。但對於表達式
(A++).foo( ) ,foo( )做用在經過後綴運算符+ +號返回的臨時對象上。臨時對象自動定爲c o n s t,

因此被編譯器標記。但爲了一致性,使二者都是 c o n s t更有意義,就像這兒所作的。由於想給
自增和自減運算符賦予各類意思,因此它們須要就事論事考慮。
    1. 按c o n s t經過傳值方式返回
    按c o n s t經過傳值方式返回,開始看起來有些微妙,因此值得多加解釋。咱們來考慮二元運
算符+號。假設在一個表達式像f ( A + B )中使用它,A + B 的結果變爲一個臨時對象,這個對象用

於f( )  調用。由於它是臨時的,自動被定爲c o n s t,因此不管使返回值爲c o n s t仍是不這樣作都沒
有影響。

----------------------- Page 211-----------------------

                                                第11章 運算符重載         211
 下載

    然而,也可能發送一個消息給A + B的返回值而不是僅傳遞給一個函數。例如,能夠寫表達
式(A+B).g( ) ,這裏g( ) 是i n t e g e r的成員函數。經過設返回值爲c o n s t,規定了對於返回值只有

c o n s t成員函數才能夠被調用。用c o n s t是恰當的,這是由於這樣能夠防止在極可能丟失的對象
中存貯有價值的信息。
    2. 返回效率
    當爲經過傳值方式返回而建立一個新對象時,要注意使用的形式。例如用運算符+號:

    return integer (left.i + right.i) ;

    一開始看起來像是一個「對一個構造函數的調用」,但其實並不是如此。這是臨時對象語法,
它是這樣陳述的:「建立一個臨時對象並返回它」。由於這個緣由,咱們可能認爲若是建立一個
命名的本地對象並返回它結果將會是同樣的。其實否則。若是像下面這樣表示,將發生三件事。

首先,t m p對象被建立,與此同時它的構造函數被調用。而後,拷貝構造函數把 t m p拷貝到返
回值外部存儲單元裏。最後,當t m p在做用域的結尾時調用析構函數。

    integer tmp(left.i + right.i) ;

    return tmp ;

    相反,「返回臨時對象」的方法是徹底不一樣的。看這樣狀況時,編譯器明白對建立的對象
沒有其餘需求,只是返回它,因此編譯器直接地把這個對象建立在返回值外面的內存單元。因
爲不是真正建立一個局部對象,因此僅須要單個的普通構造函數調用(不須要拷貝構造函數),
而且不會調用析構函數。所以,這種方法不須要什麼花費,效率是很是高的。

11.3.4 不同凡響的運算符

    有些其餘的運算符對於重載語法有明顯的不一樣。
    下標運算符‘[ ]’必須是成員函數而且它須要單個參數。由於它暗示對象像數組同樣動做,
能夠常常從這個運算符返回一個引用,因此它能夠被很方便地用於等號左側。這個運算符常常
被重載;能夠在本書其餘部分看到有關的例子。
    當逗號出如今逗號運算對象左右時,逗號運算符被調用。然而,逗號運算符在函數參數表

中出現時不被調用,此時,  逗號僅在對象中起分割做用。除了使語言保持一致性外,這個運算
符彷佛沒有許多實際用途。這兒有一個例子用於顯示當逗號出如今對象前面以及後面時,逗號
函數是如何被調用的:

----------------------- Page 212-----------------------

  212        C + +編程思想
                                                                      下載

    全局函數容許逗號放在被討論的對象的前面。這裏的用法顯得至關晦澀和使人懷疑。雖然,
能夠用逗號分割做爲表達式的一部分,但它太敏感以致於在大多數狀況下不能使用。
    運算符( ) 的函數調用必須是成員函數,它是惟一的容許在它裏面有任意個參數的函數。這
使得對象看起來像一個真正的函數名,所以,它最好爲僅有單一運算的類型使用,或至少是特
別優先的一種類型。
    運算符n e w和d e l e t e控制動態內存分配,也可被重載。這是下一章很是重要的主題。
    運算符- > *是其行爲像全部其餘二元運算符的二元運算符。它是爲模仿前一章介紹的內部
數據類型的成員指針行爲的情形而提供的。
    爲了使一個對象的表現是一個指針,就要設計使用靈巧 ( s m a r t )指針:- >。若是想爲類包裝
一個指針以使得這個指針安全,  或是在一個普通的循環子(i t e r a t o r )的用法中,則這樣作特別
有用。循環子是一個對象,這個對象能夠做用於其餘對象的包容器或集合上,每次選擇它們中
的一個,而不用提供對包容器實現的直接訪問。(在類函數裏常常發現包容器和循環子。)
    靈巧指針必須是成員函數。它有一個附加的非典型的內容:它必須返回一個對象(或對象
的引用),這個對象也有一個靈巧指針或指針,可用於選擇這個靈巧指針所指向的內容。這兒
提供了一個例子:

----------------------- Page 213-----------------------

                                                  第11章 運算符重載               213
下載

----------------------- Page 214-----------------------

  214        C + +編程思想
                                                                      下載

    類o b j定義了程序中使用的對象。函數f( )和g( )用靜態數據成員打印使人感興趣的值。使用
o b j _ c o n t a i n e r 的函數add( ) 將指向這些對象的指針儲存在類型 o b j _ c o n t a i n e r 的包容器中。
o b j _ c o n t a i n e r看起來像一個指針數組,但卻發現沒有辦法獲得這些指針。然而,類  s p聲明爲

f r i e n d類,因此它容許進入這個包容器內。類 s p看起來像一個聰明的指針—能夠使用運算符
+ + 向前移動它(也能夠定義一個運算符- - ),它不會超出包容器的範圍,它能夠返回它指向的
內容(經過這個靈巧指針)。注意,循環子( i t e r a t o r )是與包容器配套使用的—不像指針,沒
有「通用目的」的循環子。包容器和循環子將在第 1 5章深刻討論。
    在main( ) 中,一旦包容器O C裝入o b j對象,一個循環子S P就建立了。靈巧指針按下面的表

達式調用:

    SP->f( ) ; //Smart pointer calls

    SP->g( ) ;

    這裏,儘管S P實際上並不含成員函數f( )  和g( ) ,但結構指針機制經過o b j *調用這些函數,
obj* 是經過s p : : o p e r a t o r- >返回的。編譯器進行全部檢查以確信函數調用正確。
    雖然,靈巧指針的執行機制比其餘運算符複雜一些,但目的是同樣的 - - -爲類的用戶提供更
爲方便的語法。

11.3.5 不能重載的運算符

    在可用的運算符集合裏存在一些不能重載的運算符。這樣限制的一般緣由是出於對安全的

考慮:若是這些運算符也能夠被重載的話,將會形成危害或破壞安全機制,使得事情變得困難
或混淆現有的習慣。
    如今,成員選擇運算符‘ .’在類中對任何成員都有必定的意義。但若是容許它重載,就
不能用普通的方法訪問成員,只能用指針和指針運算符- >訪問。
    成員指針逆向引用的運算符‘.* ’由於與運算符‘.’一樣的緣由而不能重載。

    沒有求冪運算符。大多數一般的選擇是從F o t r a n語言引用運算符* *,但這出現了難以分析
的問題。C也沒有求冪運算符,C + +彷佛也不須要,由於這能夠經過函數調用來實現。求冪運
算符增長了使用的方便,但沒有增長新的語言功能,反而給編譯器增長了複雜性。
    不存在用戶定義的運算符,即不能編寫目前運算符集合中沒有的運算符。不能這樣作的部
分緣由是難以決定其優先級,另外一部分緣由是沒有必要增長麻煩。

    不能改變優先級規則。不然人們很難記住它們。

11.4  非成員運算符

    在前面的一些例子裏,運算符多是成員運算符或非成員運算符,這彷佛沒有多大差別。
這就會出現一個問題:「我應該選擇哪種?」總的來講,若是沒有什麼差別,它們應該是成
員運算符。這樣作強調了運算符和類的聯合。當左側操做數是當前類的對象時,運算符會工做
得很好。

----------------------- Page 215-----------------------

                                                  第11章 運算符重載              215
 下載

    但也不徹底是這種狀況—有時咱們左側運算符是別的類對象。這種狀況一般出如今爲
i o s t r e a m s重載運算符<< 和 > >時候。

----------------------- Page 216-----------------------

  216        C + +編程思想
                                                                      下載

    這個類也包含重載運算符[ ],這個運算符在數組裏返回了一個合法值的引用。一個引用被
返回,因此下面的表達式:

    I[4] = -1 ;

看起來不只比使用指針更規範些,並且它也達到了預期的效果。
    被重載的移位運算符經過引用方式傳遞和返回,因此運算將影響外部對象。在函數定義中,
表達式像

    os << ia.i[j] ;

會使現有的重載運算符函數被調用(即那些定義在 I O S T R E A M . H 中的)。在這個狀況下,被調
用的函數是ostream& operator<<(ostream&,int),這是由於i a . i [ j ]是一個i n t值。
    一旦全部的動做在i s t r e a m或o s t r e a m上完成,它將被返回,所以它可被用於更復雜的表達

式。
    這個例子使用的是插入符和提取符的標準形式。若是咱們想爲本身的類建立一個集合,可
以拷貝這個函數署名和返回類型,並聽從它的體的形式。

基本方針

    Murry [1] 爲在成員和非成員之間的選擇提出了以下的方針:

        運算符                                建議使用

    全部的一元運算符                               成員

    = ( ) [] ->                            必須是成員

    += -= /= *= ^=
                                           成員
    &= |= %= >>= <<=

    全部其餘二元運算符                              非成員

11.5  重載賦值符

    賦值符在C + +中經常產生混淆。這是毫無疑問的,由於‘= ’在編程中是最基本的運算符,
是在機器層上拷貝寄存器。另外,當使用‘=’時也能引發拷貝構造函數(上一章內容)調用:

    foo B ;

    foo A = B ;

    A = B ;

    第2行定義了對象A 。一個新對象先前不存在,如今正被建立。由於咱們如今知道了C + +編
譯器關於對象初始化是如何保護的,因此知道在對象被定義的地方構造函數老是必須被調用。

可是哪一個構造函數呢?A是從現有的f o o對象建立的,因此只有一個選擇:拷貝構造函數。因此
雖然這裏只包括一個‘= ’,但拷貝構造函數仍被調用。

  [1] Rob Murray, C++ Strategies & Tactics, Addision-We s l e y, 1993, page 47.

----------------------- Page 217-----------------------

                                                第11章 運算符重載         217
 下載

    第3行狀況不一樣了。在‘=’左側有一個之前初始化了的對象。很清楚,不用爲一個已經存
在的對象調用構造函數。在這種狀況下,爲A調用f o o : : o p e r a t o r =,把f o o : : o p e r a t o r =右側的任何

東西做爲參數。(咱們能夠有多種取不一樣右側參數的o p e r a t o r =函數)
    對於拷貝構造函數沒有這個限制。咱們在任什麼時候候使用一個‘ = ’代替普通形式的構造函
數調用來初始化一個對象時,不管=右側是什麼,編譯器都會爲咱們尋找一個構造函數:

    當處理‘=’時,記住這個差異是很是重要的:若是對象尚未被建立,初始化是須要的,
不然使用賦值運算符‘=’。
    對於初始化,使用‘= ’能夠避免寫代碼。但這要用顯式的構造函數形式。因而最後一行
應寫成:

    fee fum(FI) ;

這個方法能夠避免使讀者混淆。

運算符‘=’的行爲

    在B I N A RY. C P P中,咱們看到運算符‘= ’僅是成員函數,它密切地與‘=’左側的對象相
聯繫。若是容許咱們全局性地定義運算符‘= ’,那麼咱們就會試圖從新定義內置的‘=’:

    int operator=(int,foo) ; // global = not allowed!

這是絕對不容許的,編譯器經過強制運算符‘= ’爲成員函數而避開這個問題。

    當建立一個運算符‘= ’時,必須從右側對象中拷貝全部須要的信息完成爲類的「賦值」,
對於單個對象,這是顯然的:

----------------------- Page 218-----------------------

  218        C + +編程思想
                                                                       下載

    這裏,‘= ’左側的對象拷貝了右側對象中的全部內容,而後返回它的引用,因此咱們還可

以建立更加複雜的表達式。
    這個例子犯了一個普通的錯誤。當準備給兩個相同類型的對象賦值時,應該首先檢查一下
自賦值( s e l f - a s s i g n m e n t ):這個對象是否對自身賦值了?在一些狀況下,例如本例,不管如何執
行這些賦值運算都是無害的,但若是對類的實現做了修改,那麼將會出現差別。若是咱們習慣
於不作檢查,就可能忘記併產生難以發現的錯誤。

    1. 類中指針
    若是對象不是如此簡單時將會發生什麼事情?例如,若是對象裏包含指向別的對象的指針
將如何?簡單地拷貝一個指針意味着以指向相同的存儲單元的對象而結束。這種狀況,就須要
本身作註釋記住這點。
    這裏有兩個解決問題的方法。當咱們作一個賦值運算或一個拷貝構造函數時,最簡單的技

術是拷貝這個指針所涉及的一切,這是很是簡單的。

----------------------- Page 219-----------------------

                                                 第11章 運算符重載         219
 下載

    這裏展現了當咱們的類包含了指針時,老是須要定義的 4個函數:全部必需的普通構造函
數、拷貝構造函數、運算符‘=’(不管定義它仍是不容許它)和析構函數。對運算符‘= ’當

然要檢查自賦值,雖然這兒不須要,但咱們應養成這種習慣。這實際上減小了改變代碼而忘記
檢查自賦值的可能性。
    這裏,構造函數分配存儲單元並對它初始化,運算符‘ = ’拷貝它,析構函數釋放存儲單
元。然而,若是要處理許多存儲單元並對其初始化時,咱們也許想避免這種拷貝。解決這個問
題的一般方法被稱爲引用記數(reference counting) 。能夠使一塊存儲單元具備智能,它知道有

多少對象指向它。拷貝構造函數或賦值運算意味着把另外的指針指向如今的存儲單元並增長引
用記數。消除意味着減少引用記數,若是引用記數爲0意味着銷燬這個對象。
    但若是向這塊存儲單元寫入將會如何呢?由於不止一個對象使用這塊存儲單元,因此當我
們修改本身的存儲單元時,也等於也修改了他人的存儲單元。爲了解決這個問題,常用另
外一個稱爲寫拷貝( c o p y - o n - w r i t e )的技術。在向這塊存儲單元寫以前,應該確信沒有其餘人使

用它。若是引用記數大於 1,在寫以前必須拷貝這塊存儲單元,這樣就不會影響他人了。這兒
提供了一個簡單的引用記數和關於寫拷貝的例子:

----------------------- Page 220-----------------------

     220   C + +編程思想
                                                                        下載

----------------------- Page 221-----------------------

                                                 第11章 運算符重載         221
 下載

    嵌套類m e m b l o c k是被指向的一塊存儲單元。(注意指針b l o c k定義在嵌套類的最後)它包含
了一個引用記數及控制和讀引用記數的函數。同時這裏存在一個拷貝構造函數,因此咱們能夠
從現有的類建立一個新的m e m b l o c k 。
    函數attach( )增長一個m e m b l o c k 引用記數用以指示有另外一個對象使用它。函數 detach( )減

少引用記數。若是引用記數爲0,則說明沒有對象使用它,因此經過表達式delete this  成員函數
銷燬它本身的對象。
    能夠用函數 set( ) 修改存儲單元。但在作修改以前,應該確信不是在別的對象使用的
m e m b l o c k上進行。能夠經過調用connted::unalias( ),connted::unalias( )調用m e m b l o c k : : u n a l i a s (
)來作到這點。若是引用記數爲 1  (意味着沒有別的對象指向這塊存儲單元),後面這個函數將

返回b l o c k指針,但若是引用記數大於1就要複製這個存儲單元。
    這個例子已涉及到下一章的內容。C + +運算符n e w和d e l e t e代替C語言的malloc( )和free( )來
建立和銷燬對象。對於這個例子,除了n e w在分配了存儲單元后調用構造函數,d e l e t e在釋放存
儲單元以前調用析構函數以外,能夠認爲new 和d e l e t e與malloc( ) 和free( )同樣。
    拷貝構造函數給源對象b l o c k賦值b l o c k ,而不是建立它本身的存儲單元。而後由於如今增

加了使用這個存儲單元的對象,因此經過調用memblock::attach( )增長引用記數。
    運算符‘= ’處理‘=’左側已建立的對象,因此它必須經過爲m e m b l o c k調用detach( )而首
先整理這個存儲單元。若是沒有其餘對象使用它,這個老的 m e m b l o c k將被銷燬。而後運算符

----------------------- Page 222-----------------------

  222        C + +編程思想
                                                                      下載

‘= ’重複拷貝構造函數的行爲。注意它首先檢查是否給它自己賦予相同的對象。
    析構函數調用detach( )有條件地銷燬m e m b l o c k 。

    爲了實現寫拷貝,必須控制全部寫存儲單元的動做。這意味着不能向外部傳遞原有指針。
咱們會說:「告訴我您想作什麼,我將爲您作!」例如成員函數write( )容許對這個存儲單元修
改數值。但它首先必須使用unalias( ) 防止修改一個已別名化了的存儲單元(超過一個對象使用
的存儲單元)。
    在main( ) 中測試了幾個必須正確實現引用記數的函數:構造函數、拷貝構造函數、運算符

‘= ’和析構函數。在main( ) 中也經過爲對象C調用write( )測試了寫拷貝,對象C是已別名化了
的A存儲單元。
    2. 跟蹤輸出
    爲了驗證這個方案是正確的,最好的方法是對類增長信息和功能以便產生可被分析的跟蹤
輸出。這兒的R E F C O U N T. C P P增長了跟蹤信息。

----------------------- Page 223-----------------------

                                                  第11章 運算符重載               223
下載

----------------------- Page 224-----------------------

     224   C + +編程思想
                                                                        下載

----------------------- Page 225-----------------------

                                                第11章 運算符重載         225
 下載

    如今m e m b l o c k含有一個s t a t i c數據成員b l o c k c o u n t來記錄建立的存儲單元號碼,爲了區分這
些存儲單元它還爲每一個存儲單元建立了惟一號碼(存放在 b l o c k n u m 中)。在析構函數中聲明哪

一個存儲單元被銷燬,print( )函數顯示塊號和引用記數。
    類c o u n t e d 含有一個緩衝器 i d用來記錄對象信息。 c o u n t e d 構造函數建立了一個新的
m e m b l o c k對象並把結果賦給了b l o c k (這個結果是一個堆上指向m e m b l o c k 的指針)。從參數拷
貝來的標識符加了一個單詞「c o p y」用以顯示它是從哪裏拷貝來的。函數addname( )也讓咱們
在i d (這是實際的標識符,因此咱們能夠看到它是什麼以及從哪裏拷貝來的)中加入有關對象

的附加信息。
    這裏是輸出結果:

    經過研究輸出結果、跟蹤源代碼和對程序測試,咱們會加深對這些技術的理解。
    3.  自動建立運算符‘= ’
    由於將一個對象賦給另外一個相同類型的對象是大多數人可能作的事情,因此若是沒有建立
t y p e : : o p e r a t o r = ( t y p e ),編譯器會自動建立一個。這個運算符行爲模仿自動建立的拷貝構造函數

的行爲:若是類包含對象(或是從別的類繼承的),對於這些對象,運算符‘=’被遞歸調用。
這被稱爲成員賦值(memberwise assignment)。見以下例子:

----------------------- Page 226-----------------------

  226        C + +編程思想
                                                                       下載

    爲f o o 自動生成的運算符‘=’調用b a r : : o p e r a t o r = 。
    通常咱們不會想讓編譯器作這些。對於複雜的類(尤爲是它們包含指針的狀況),咱們應
該顯式地建立一個運算符‘=’。若是真的不想讓人執行賦值運算,能夠把運算符‘=’聲明爲

p r i v a t e 函數。(除非在類內使用它,不然沒必要定義它。)

11.6   自動類型轉換

    在C和C + +中,若是編譯器看到一個表達式或函數調用使用了一個不合適的類型,它常常
會執行一個自動類型轉換。在C + +中,能夠經過定義自動類型轉換函數來爲用戶定義類型達到
相同效果。這些函數有兩種類型:特殊類型的構造函數和重載的運算符。

11.6.1 構造函數轉換

    若是咱們定義一個構造函數,這個構造函數能把另外一類型對象(或引用)做爲它的單個參

數,那麼這個構造函數容許編譯器執行自動類型轉換。以下例:

----------------------- Page 227-----------------------

                                                第11章 運算符重載         227
 下載

    當編譯器看到f( ) 覺得對象o n e參數調用時,編譯器檢查f( ) 的聲明並注意到它須要一個t w o
對象做爲參數。而後,編譯器檢查是否有從對象                      one 到t w o 的方法。它發現了構造函數

t w o : : t w o ( o n e ),t w o : : t w o ( o n e )被悄悄地調用,結果對象t w o被傳遞給f( ) 。
    在這個狀況裏,自動類型轉換避免了定義兩個 f( )重載版本的麻煩。然而,代價是隱藏了
構造函數對t w o 的調用,若是咱們關心f( ) 的調用效率的話,那就不要使用這種方法。
    • 阻止構造函數轉換
    有時經過構造函數自動轉換類型可能出現問題。爲了避開這個麻煩,能夠經過在前面加關
鍵字explicit [1] (只能用於構造函數)來修改構造函數。上例類t w o 的構造函數做了修改,以下:

    經過使類t w o 的構造函數顯式化,編譯器被告知不能使用那個構造函數(那個類中其餘非

顯式化的構造函數仍能夠執行自動類型轉換)執行任何自動轉換。若是用戶想進行轉換必須寫
出代碼。上面代碼f ( t w o ( O n e ) )建立一個從類型O n e到t w o的臨時對象,就像編譯器在前面版本中
作的那樣。

11.6.2 運算符轉換

    第二種自動類型轉換的方法是經過運算符重載。咱們能夠建立一個成員函數,這個函數通
過在關鍵字o p e r a t o r後跟隨想要轉換到的類型的方法,將當前類型轉換爲但願的類型。這種形

式的運算符重載是獨特的,由於沒有指定一個返回類型——返回類型就是咱們正在重載的運算
符的名字。這兒有一個例子:

  [1] 寫做本書時,e x p l i c i t是語言中新的關鍵字。咱們的編譯器可能還不支持它。

----------------------- Page 228-----------------------

  228        C + +編程思想
                                                                       下載

    用構造函數技術,目的類執行轉換。然而使用運算符技術,是源類執行轉換。構造函數技
術的價值是在建立一個新類時爲現有系統增長了新的轉換途徑。然而,建立一個單一參數的構
造函數老是定義一個自動類型轉換(即便它有不止一個參數也是同樣,由於其他的參數將被缺
省處理),這可能並非咱們所想要的。另外,使用構造函數技術沒有辦法實現從用戶定義類

型向內置類型轉換,這隻有運算符重載可能作到。
    • 反身性
    使用全局重載運算符而不用成員運算符的最便利的緣由之一是在全局版本中的自動類型轉
換能夠針對左右任一操做數,而成員版本必須保證左側操做數已處於正確的形式。若是想兩個
操做數都被轉換,全局版本能夠節省不少代碼。這兒有一個小例子。

----------------------- Page 229-----------------------

                                                第11章 運算符重載         229
 下載

    類n u m b e r有一個成員運算符+號和一個友元( f r i e n d )運算符 -  號。由於有一個使用單一i n t參
數的構造函數,i n t 自動轉換爲n u m b e r ,但這要在正確的條件下。在main( ) 裏,能夠看到增長
一個n u m b e r到另外一個n u m b e r進行得很好,這是由於它重載的運算符很是匹配。當編譯器看到
一個n u m b e r後跟一個+號和一個i n t 時,它也能和成員函數n u m b e r : : o p e r a t o r +相匹配而且構造函
數把i n t參數轉換爲n u m b e r 。但當編譯器看到一個i n t、一個+號和一個n u m b e r時,它就不知道如

何去作,由於它所擁有的是n u m b e r : : o p e r a t o r + ,須要左側的操做數是n u m b e r對象。所以,編譯
器發出一個出錯信息。
    對於友元運算符-號,狀況就不一樣了。編譯器須要填滿兩個參數,它不限定 n u m b e r做爲左
側參數。所以,若是看到表達式 1 - a,編譯器就使用構造函數把第一個參數轉換爲 n u m b e r 。有
時咱們也許想經過把它們設成成員函數來限定運算符的使用。例如當用一個矢量與矩陣相乘,

矢量必須在右側。但若是想讓運算符轉換任一個參數,就要使運算符爲友元函數。
    幸運的是編譯器不會把表達式 1 - 1的兩個參數轉換爲n u m b e r對象,而後調用運算符 -  號。
那將意味着現有的C代碼可能忽然執行不一樣的工做了。編譯器首先匹配「最簡單的」可能性,
對於表達式1 - 1將優先使用內部運算符。

11.6.3 一個理想的例子:strings

    這是一個自動類型轉換對於s t r i n g類很是有幫助的例子。若是不用自動類型轉換就想從標

準的C庫函數中使用全部的字符串函數,那麼就得爲每個函數寫一個相應的成員函數,就像
下面的例子:

----------------------- Page 230-----------------------

  230        C + +編程思想
                                                                      下載

    這裏只寫了strcmp( )函數,但必須爲S T R I N G . H可能須要的每個函數寫一個相應的函數。
幸運的是,能夠提供一個容許訪問S T R I N G . H中全部函數的自動類型轉換:

    由於編譯器知道如何從s t r i n g轉換到c h a r *,因此如今任何一個接受c h a r *參數的函數也能夠
接受s t r i n g參數。

11.6.4 自動類型轉換的缺陷

    由於編譯器必須選擇如何執行類型轉換,因此若是沒有正確地設計出轉換,編譯器會產生

----------------------- Page 231-----------------------

                                                第11章 運算符重載              231
 下載

麻煩。類X能夠用operator Y( )  將它自己轉換到類Y ,這是一個簡單且明顯的狀況。若是類Y有
一個單個參數爲X 的構造函數,也表示一樣的類型轉換。如今編譯器有兩個從 X 到Y 的轉換方

法,因此發生轉換時,編譯器會產生一個不明確指示的出錯信息:

    這個問題解決方案不是僅提供一個從一個類型到另外一個類型的自動轉換路徑。
    提供自動轉換到不止一種類型時,會引起更困難的問題。有時,這個問題被稱爲扇出( f a n - o u t ):

----------------------- Page 232-----------------------

  232        C + +編程思想
                                                                       下載

    類C有向A和B的自動轉換。這樣存在一個隱藏的缺陷:使用建立的兩種版本的重載運算符
h( ) 時問題就出現了。(只有一個版本時,main( ) 裏的代碼會正常運行。)

    一般,對於自動類型的解決方案是隻提供一個從一個類型向另外一個類型轉換的自動轉換版
本。固然咱們也能夠有多個向其餘類型的轉換,但它們不該該是自動轉換,而應該建立顯式的
調用函數,例如用名字make_A( )和make_B( ) 表示這些函數。
    • 隱藏的行爲
    自動類型轉換會引入比所但願的更多的潛在行爲。下面看 11 . 5節F E E F I . C P P 的修改後的例

子:

    這裏沒有從f o對象建立fee fiddle 的構造函數。然而,f o有一個到f e e的自動類型轉換。這裏
也沒有從f e e對象建立f e e的拷貝構造函數,但這是一種能由編譯器幫助咱們建立的特殊函數之
一。(缺省的構造函數、拷貝構造函數、運算符‘=’和析構函數可被自動建立)對於下面的聲
明,自動類型轉換運算符被調用並建立一個拷貝函數:

    fee fiddle = FO  ;

    自動類型轉換應該當心使用。它在減小代碼方面是很是出色的,但不值得平白無故地使
用。

11.7  小結

    運算符重載存在的緣由是爲了使編程容易。運算符重載沒有那麼神祕,它只不過是擁有有
趣名字的函數。當它以正確的形式出現時,編譯器調用這個函數。但若是運算符重載對於類的
設計者或類的使用者不能提供特別顯著的益處,則最好不要使用,由於增長運算符重載會使問
題混淆。

----------------------- Page 233-----------------------

                                                 第11章 運算符重載         233
 下載

11.8  練習

    1. 寫一個有重載運算符+ + 的類。試着用前綴和後綴兩種形式調用此運算符,看看編譯器會
給咱們什麼警告。
    2.  寫一個只含有單個private char 成員的類。重載i o s t r e a m運算符< <和> >   (像在I O S O P. C P P
中的同樣)並測試它們,能夠用f s t r e a m s、s t r s t r e a m s和s t d i o s t r e a m s ( c i n和c o u t )測試它們。

    3. 寫一個包含重載的運算符+、-  、*、/和賦值符的n u m b e r類。出於效率考慮,爲這些函數
合理地選擇返回值以便以鏈式寫表達式。寫一個自動類型轉換運算符int( ) 。
    4. 合併在U N A RY. C P P和B I N A RY. C P P中的類。
    5.  對FA N O U T. C P P做以下修改:建立一個顯式函數,用它代替自動轉換運算符來完成類型
轉換。

----------------------- Page 234-----------------------

                                                                      下載

                   第1 2章  動態對象建立

    有時咱們能知道程序中對象的確切數量、類型和生命期。但狀況並不老是這樣。

    空中交通系統必須處理多少架飛機?一個C A D系統須要多少個形狀?在一個網絡中有多少
個節點?
    爲了解決這個普通的編程問題,在運行時能建立和銷燬對象是基本的要求。固然, C 已提
供了動態內存分配函數malloc( )和free( )        (以及malloc( ) 的變種),這些函數在運行時從堆中
(也稱自由內存)分配存儲單元。

    然而,在C + +中這些函數不能很好地運行。構造函數不容許經過向對象傳遞內存地址來初
始化它。若是那麼作了,咱們可能
    1) 忘記了。則對象初始化在C + +中難以保證。
    2) 指望某事發生,但結果是在給對象初始化以前意外地對對象做了某種改變。
    3) 把錯誤規模的對象傳遞給了它。

    固然,即便咱們把每件事都作得很正確,修改咱們的程序的人也容易犯一樣的錯誤。不正
確的初始化是編程出錯的主要緣由,因此在堆上建立對象時,確保構造函數調用是特別重要的。
    C + +是如何保證正確的初始化和清理並容許咱們在堆上動態建立對象的呢?
    答案是使動態對象建立成爲語言的核心。 malloc( )和free( )是庫函數,所以不在編譯器控
制範圍以內。若是咱們有一個能完成動態內存分配及初始化工做的運算符和另外一個能完成清理

及釋放內存工做的運算符,編譯器就能夠保證全部對象的構造函數和析構函數都會被調用。
    在本章中,咱們將明白C + +的n e w和d e l e t e是如何經過在堆上安全建立對象來出色地解決這
個問題的。

12.1  對象建立

    當一個C + +對象被建立時,有兩件事會發生。
    1) 爲對象分配內存。
    2) 調用構造函數來初始化那個內存。
    到目前爲止,咱們應該確保步驟 2 )必定發生。C + +強迫這樣作是由於未初始化的對象是
程序出錯的主要緣由。不用關心對象在哪裏建立和如何建立的—構造函數老是被調用。

    然而,步驟1)能夠以幾種方式或在可選擇的時間內發生:
    1) 靜態存儲區域,存儲空間在程序開始以前就能夠分配。這個存儲空間在程序的整個運行
期間都存在。
    2) 不管什麼時候到達一個特殊的執行點(左花括號)時,存儲單元均可以在棧上被建立。出了
執行點(右花括號),這個存儲單元自動被釋放。這些棧分配運算內置在處理器的指令集中,

很是有效。然而,在寫程序的時候,必須知道須要多少個存儲單元,以使編譯器生成正確的指
令。
    3) 存儲單元也能夠從一塊稱爲堆(也可稱爲自由存儲單元)的地方分配。這稱爲動態內存
分配,在運行時調用程序分配這些內存。這意味着能夠在任什麼時候候分配內存和決定須要多少內

----------------------- Page 235-----------------------

                                               第12章 動態對象建立         235
 下載

存。咱們也負責決定什麼時候釋放內存。這塊內存的生存期由咱們選擇決定—而不受範圍限制。
    這三個區域常常被放在一塊連續的物理存儲單元裏:靜態內存、堆棧和堆(由編譯器的做

者決定它們的順序),但沒有必定的規則。堆棧能夠在某一特定的地方,堆的實現能夠經過調
用由運算系統分配的一塊存儲單元來完成。對於一個程序設計者,這三件事無須咱們來完成,
因此咱們所要思考的是何時申請內存。

12.1.1 C從堆中獲取存儲單元的方法

    爲了在運行時動態分配內存,C在它的標準庫函數中提供了一些函數:從堆中申請內存的
函數malloc( ) 以及它的變種calloc( )和realloc( ) ;釋放內存返回給堆的函數free( ) 。這些函數是

有效的但較原始,須要編程人員理解和當心使用。對使用 C的動態內存分配函數建立一個類的
實例,必須作:

    在下面這行代碼中,咱們能夠看到使用malloc( )爲對象分配內存:

----------------------- Page 236-----------------------

  236        C + +編程思想
                                                                      下載

    obj* Obj = (obj*)malloc(sizeof(obj)) ;

    這裏用戶必須決定對象的長度(這也是程序出錯緣由之一)。由於它是一塊內存而不是一
個對象,因此malloc( )返回一個v o i d * 。C + +不容許將一個void*  賦予任何指針,因此必須映射。
    由於malloc( )可能找不到可分配的內存(在這種狀況下它返回0 ),因此必須檢查返回的指

針以確信內存分配成功。
    但最壞的是:

    Obj->initialize( ) ;

    用戶在使用對象以前必須記住對它初始化。注意構造函數沒有被使用,由於構造函數不能
被顯式地調用—而是當對象建立時由編譯器調用。這裏的問題是如今用戶可能在使用對象時
忘記執行初始化,所以這也是引入程序缺陷的主要來源。
    許多程序設計者發現C 的動態內存分配函數太複雜,使人混淆。因此, C程序設計者經常
在靜態內存區域使用虛擬內存機制分配很大的變量數組以免使用動態內存分配。由於 C + +能

讓通常的程序員安全使用庫函數而不費力,因此應當避免使用C的動態內存方法。

12.1.2 運算符new

    C + +中的解決方案是把建立一個對象所需的全部動做都結合在一個稱爲 n e w 的運算符裏。
當用n e w (n e w 的表達式)建立一個對象時,它就在堆裏爲對象分配內存併爲這塊內存調用構
造函數。所以,若是咱們寫出下面的表達式

    foo *fp = new foo(1,2) ;

在運行時等價於調用m a l l o c ( s i z e o f ( f o o ) ) ,並使用(1,2 )做爲參數表來爲f o o調用構造函數,
返回值做爲t h i s指針的結果地址。在該指針被賦給 f p以前,它是不定的、未初始化的對象—

在這以前咱們甚至不能觸及它。它自動地被賦予正確的f o o類型,因此沒必要進行映射。
    缺省的n e w還檢查以確信在傳遞地址給構造函數以前內存分配是成功的,因此咱們沒必要顯
式地肯定調用是否成功。在本章後面,咱們將會發現,若是沒有可供分配的內存會發生什麼事
情。
    咱們能夠爲類使用任何可用的構造函數而寫一個 n e w表達式。若是構造函數沒有參數,可

以寫沒有構造函數參數表的n e w表達式:

    foo *fp = new foo ;

    咱們已經注意到了,在堆裏建立對象的過程變得簡單了—只是一個簡單的表達式,它帶

有內置的長度計算、類型轉換和安全檢查。這樣在堆裏建立一個對象和在棧裏建立一個對象一
樣容易。

12.1.3 運算符delete

    n e w表達式的反面是d e l e t e表達式。d e l e t e表達式首先調用析構函數,而後釋放內存(常常
是調用free( ) )。正如n e w表達式返回一個指向對象的指針同樣, d e l e t e表達式須要一個對象的
地址。

    delete fp ;

    上面的表達式清除了早先建立的動態分配的對象f o o。

    d e l e t e只用於刪除由n e w建立的對象。若是用malloc( )    (或calloc( )或realloc( ) )建立一個對
象,而後用d e l e t e刪除它,這個行爲是未定義的。由於大多數缺省的 n e w和d e l e t e實現機制都使

----------------------- Page 237-----------------------

                                               第12章 動態對象建立         237
 下載

用了malloc( )和free( ) ,因此咱們極可能會沒有調用析構函數就釋放了內存。
    若是正在刪除的對象指針是0,將不發生任何事情。爲此,建議在刪除指針後當即把指針

賦值爲0 以避免對它刪除兩次。對一個對象刪除兩次必定不是一件好事,這會引發問題。

12.1.4 一個簡單的例子

    這個例子顯示了初始化發生的狀況:

    咱們經過打印t r e e的值以證實構造函數被調用了。這裏是經過重載運算符< <和一個o s t r e a m
一塊兒使用來實現這個運算的。注意,雖然這個函數被聲明爲一個友元( f r i e n d )函數,但它還
是被定義爲一個內聯函數。這僅僅是爲了方便—定義一個友元函數爲內聯函數不會改變友元
狀態並且它還是全局函數而不是一個類的成員函數。同時也要注意返回值是整個輸出表達式的

結果,它自己是一個o s t r e a m & (爲了知足函數返回值類型,它必須是o s t r e a m & )。

12.1.5 內存管理的開銷

    當咱們在堆裏動態建立對象時,對象的大小和它們的生存期被正確地內置在生成的代碼裏,
這是由於編譯器知道確切的數量和範圍。在堆裏建立對象還包括另外的時間和空間的開銷。這
兒提供了一個典型的方案。(咱們能夠用calloc( )或realloc( )代替malloc( ) )
    1) 調用malloc( ) ,這個函數從堆裏申請一塊內存。

    2)  從堆裏搜索一塊足夠知足請求的內存。能夠經過檢查顯示內存使用狀況的某種圖或目錄
來實現搜索。這個過程很快但可能要試探幾回,因此它多是不肯定的—即沒必要期望m a l l o c (
)花費徹底相同的時間。
    3)  在指向這塊內存的指針返回以前,這塊內存大小和地址必須記錄下來,這樣之後調用
malloc( ) 時就不會再使用它了,並且當咱們調用free( ) 時,系統就會知道須要釋放多大的內存。

----------------------- Page 238-----------------------

  238        C + +編程思想
                                                                      下載

    實現這些運算的方法可能變化很大。例如,沒有辦法阻止在處理器裏執行原始的內存分配。
若是好奇的話,你能夠寫一個測試程序來猜想malloc( )實現的方法;也能夠讀一讀庫函數的源

代碼(若是有的話)。

12.2  從新設計前面的例子

    如今已經介紹了n e w和d e l e t e (以及其餘許多主題)。對於本書前面的s t a s h和s t a c k例子,我
們能夠使用到目前爲止討論的全部的技術來重寫。檢查這個新代碼將有助於對這些主題的復
習。

12.2.1 僅從堆中建立string類

    此處,類s t a s h和s t a c k 本身都將不「擁有」它們指向的對象。即當 s t a s h或s t a c k 出了範圍,

它也不會爲它指向的對象調用 d e l e t e 。試圖使它們成爲普通的類是不可能的,緣由是它們是
v o i d指針。若是d e l e t e一個v o i d指針,惟一發生的事是釋放了內存,這是由於既沒有類型信息也
沒有辦法使得編譯器知道要調用哪一個析構函數。當一個指針從s t a s h或s t a c k對象返回時,在使用
它以前必須將它作類型映射。這個問題將在1 3章和1 5章討論。
    由於包容器本身不擁有指針,因此用戶必須對它負責。這意味着在一個包容器上增長一個

指向在棧上建立的對象的指針或增長一個指向在同一個包容器堆上建立的對象的指針時將會發
生嚴重的問題。由於d e l e t e表達式對於不在堆上分配的指針是不安全的。(從包容器取回一個指
針時,如何知道它的對象已經在哪兒分配了內存呢?)爲了在以下一個簡單的 S t r i n g類的版本
中解決這個問題,下面採起了一些步驟以防止在堆之外的地方建立S t r i n g:

----------------------- Page 239-----------------------

                                              第12章 動態對象建立          239
 下載

    爲了限制用戶使用這個類,主構造函數聲明爲p r i v a t e,因此,除了咱們之外,沒有人能夠

使用它。另外,拷貝構造函數也聲明爲 p r i v a t e ,但沒有定義,以防止任何人使用它,運算符
‘= ’也是如此。用戶建立對象的惟一方法是調用一個在堆上建立 S t r i n g                 (因此能夠知道全部的
S t r i n g都是在堆上建立的)並返回它的指針的特殊函數。
    訪問這個函數的方法有兩種。爲了使用簡單,它能夠是全局的f r i e n d函數(稱爲makeString( ) )。
但若是不想「污染」全局名字空間,能夠使之爲                      s t a t i c成員函數(稱爲 make( ) )並經過

String::make( )調用它。後一種形式對於更加明顯地表示它屬於這個類是有好處的。
    在構造函數中,注意表達式:

    s = new char[strlen(S) +1] ;

    方括號意味着一個對象數組被建立(此處是一個 c h a r數組),方括號裏的數字表示將建立
的對象的數量。這就是在程序運行時如何建立一個數組的方法。
    對c h a r *類型的自動轉換意味着在任何須要c h a r *的地方均可以使用一個 S t r i n g對象。另外,
一個i o s t r e a m輸出運算符擴充了i o s t r e a m庫,使得它可以處理St r i n g對象。

12.2.2 stash指針

    在第5章看到的類s t a s h版本現已被修改,它反映了第5章以來所介紹的新技術。另外,新的
p s t a s h擁有指向在堆中原本就存在的對象的指針,但在第5章和它前面章節中,舊的s t a s h是拷貝
對象到s t a s h包容器裏的。有了新介紹的n e w和d e l e t e ,控制指向在堆中建立的對象的指針就變
得安全、容易了。
    下面提供了「pointer stash 」的頭文件:

----------------------- Page 240-----------------------

  240        C + +編程思想
                                                                       下載

    基本的數據成分是很是類似的,但如今  s t o r a g e是一個v o i d指針數組,而且用 n e w代替
malloc( )爲這個數組分配內存。在下面這個表達式中,對象的類型是v o i d *,因此這個表達式表
示分配了一個v o i d指針的數組。

    storage = new void*[quantity = Quantity] ;

    析構函數刪除v o i d指針自己,而不是試圖刪除它們所指向的內容(正如前面指出的,釋放
它們的內存不調用析構函數,這是由於一個v o i d指針沒有類型信息)。
    其餘方面的變化是用運算符[ ]代替了函數fetch( ) ,這在語句構成上顯得更有意義。由於返

回一個v o i d指針,因此用戶必須記住在包容器內存儲的是什麼類型,在取回它們時要映射這些
指針(這是在之後章節將要修改的問題)。
    下面是成員函數的定義:

----------------------- Page 241-----------------------

                                              第12章 動態對象建立          241
 下載

    除了用儲存指針代替整個對象的拷貝外,函數 add( )和之前同樣有效。拷貝整個對象對於
普通對象來講,實際上只須要拷貝構造函數。
    實際上,函數 inflate( ) 代碼比先前的版本更復雜且更低效。這是由於先前使用的函數

realloc( )能夠調整現有內存塊的大小,也能夠自動地把舊的內存塊內容拷貝到更大的內存塊裏。
在任何一件事中咱們都不用爲它擔憂,若是不用移動內存,它將進行得更快。由於                                    n e w和
r e a l l o c不等價,因此在這個例子中必須分配一個更大的內存塊、執行拷貝和刪除舊的內存塊。
在這種狀況下,使用malloc( ) 、realloc( )和free( ) 比n e w和d e l e t e在實現方面可能更有意義。幸運
的是,這些實現是隱藏的,因此用戶能夠不用知道這些變化。只要不對同一塊內存混合調用,

malloc( )家族函數在和new( )及delete( )並行使用時可以保證相互之間的安全。因此這些是徹底
能夠作到的。
    • 一個測試程序
    下面是爲了測試p s t a s h而將s t a s h的測試程序重寫後的程序:

----------------------- Page 242-----------------------

  242        C + +編程思想
                                                                       下載

    和前面同樣,s t a s h被建立並添加了信息。但此次的信息是由n e w表達式產生的指針。注意
下面這一行:

    intStash.add( new int(i) ) ;

    這個表達式new int(i)   使用了僞構造函數形式,因此一個新的i n t對象的內存將在堆上建立
而且這個i n t對象被初始化爲值i。

    注意在打印的時候,由p s t a s h : : o p e r a t o r [ ]返回的值必須被映射成正確的類型,對於這個程
序其餘部分的p s t a s h對象,也將重複這個動做。這是使用 v o i d指針做爲表達式而出現的不但願
的出現效果,將在後面的章節中解決。
    第2步測試打開源程序文件並把它讀到另外一個 p s a t s h裏,把每一行轉換爲一個 S t r i n g對象。
咱們能夠看到,makeString( )和String::make( )都被使用以顯示二者之間的差異。靜態( s t a t i c )成

員函數多是更好的方法,由於它更明顯。
    當取回指針時,咱們能夠看到以下表達式:

    char* p = *(String*)stringStash[i] ;

    由運算符[ ]返回產生的指針必須被映射爲S t r i n g *以使之具備正確的類型。而後,S t r i n g *被
逆向引用,因此表達式可對一個對象求值。此時,當編譯器想要一個                             c h a r * 時,將看到一個
S t r i n g對象,因此它在S t r i n g裏調用自動類型轉換運算符來產生一個c h a r *。
    在這個例子裏,在堆上建立的對象永遠不被銷燬。這並無害處,由於當程序結束時內存
會被釋放,但在實際狀況下咱們並不想這樣,這個問題將在之後的章節裏予以修正。

12.2.3 stack例子

    s t a c k例子用了許多自第4章以來介紹的技術。下面是新的頭文件。

----------------------- Page 243-----------------------

                                               第12章 動態對象建立          243
 下載

    嵌套的struct link如今能夠有本身的構造函數,由於在stack::push( )裏, n e w 的使用能夠安
全地調用那個構造函數。(注意語法很純正,這將減少了出錯的可能。)l i n k : : l i n k構造函數簡單
地初始化了d a t a和n e x t指針,因此在stack::push( )裏,下面這行不只給新的l i n k分配內存,並且

巧妙地爲l i n k初始化:

    head = new link(Data, head) ;

    其他部分的邏輯實際上和第4章是同樣的。下面是剩下的兩個非內聯函數的實現內容。

    惟一的不一樣是在析構函數裏使用d e l e t e代替free( ) 。
    跟s t a s h同樣,使用v o i d指針意味着在堆上建立的對象不能被s t a c k銷燬,因此,若是用戶對

----------------------- Page 244-----------------------

  244        C + +編程思想
                                                                       下載

s t a c k裏的指針不加以管理的話,可能出現不但願的內存漏洞。能夠在下面的測試程序裏看到這
些:

    與s t a s h例子同樣,先打開一個文件,文件的每一行轉換成爲一個 S t r i n g對象並存放在s t a c k
裏,而後打印出來。這個程序沒有刪除 s t a c k裏的指針,s t a c k自己也沒有作,因此那塊內存丟
失了。

12.3  用於數組的new和delete

    在C + +裏,一樣容易在棧或堆上建立一個對象數組。固然,應當爲數組裏的每個對象調
用構造函數。有一個約束:除了在棧上總體初始化(見第4章)外必須有一個缺省的構造函數,
由於不帶參數的構造函數必須被每個對象調用。
    當使用n e w在堆上建立對象數組時,還必須作一些事情。下面有一個對象數組的例子:

    foo* fp = new foo[100] ;

    這樣在堆上爲1 0 0個f o o對象分配了足夠的內存併爲每個對象調用了構造函數。如今,我
們擁有一個f o o *。它和用以下表達式建立單個對象獲得的結果是同樣的:

    foo* fp2 = new foo ;

    由於這是咱們本身寫的代碼,而且咱們知道f p其實是一個數組的起始地址,因此用f p [ 2 ]
選擇數組的元素是有意義的。銷燬這個數組時發生了什麼呢?下面的語句看起來是徹底同樣的:

    delete fp2 ; //O K

    delete fp ; // Not the desired eff e c t

而且效果也會同樣:爲所給地址指向的f o o對象調用析構函數,而後釋放內存。對於 f p 2,這樣
是正確的,但對於f p,另外9 9個析構函數調用沒有進行。正確的存儲單元數量還會被釋放,因

----------------------- Page 245-----------------------

                                               第12章 動態對象建立         245
 下載

爲它被分配在一個大塊的內存裏,整個內存塊的大小被分配程序藏在某處。
    解決辦法是須要給編譯器一個數組起始地址的信息。這能夠用下面的語法來實現:

    delete []fp ;

    空的方括號告訴編譯器產生從數組建立時存放的地方取回數組中對象數量的代碼,併爲數
組的全部對象調用析構函數。這其實是早期語法的改良形式,偶爾仍能夠在老的代碼裏看到

以下的代碼:

    delete [100]fp ;

這個語法強迫程序設計者在數組裏包含對象的數量,程序設計者有可能把對象數量弄錯。而讓
編譯器處理這件事的附加代價是很低的,因此只在一個地方指明對象數量要比在兩個地方指明
好些。

使指針更像數組

    做爲題外話,上面定義的f p能夠被修改指向任何類型,但這對於一個數組的起始地址來說
沒有什麼意義。通常講來,把它定義爲常量更有意義些,由於這樣任何修改指針的企圖都會被

指出出錯。爲了獲得這個效果,咱們能夠試着用下面的表達式:

    int const* q = new int[10] ;

    const int* q = new int[10] ;

但在上面這兩種狀況裏,c o n s t將和int  捆綁在一塊兒,限定指針指向的內容而不是指針自己。必
須用下面的表達式代替:

    int* const q = new int[10] ;

如今在q裏的數組元素能夠被修改,但對q自己的修改(例如q + + )是不合法的,由於它是一個

普通數組標識符。

12.4  用完內存

    當運算符n e w 找不到足夠大的連續內存塊來安排對象時將會發生什麼?一個稱爲                              n e w -
h a n d l e r的函數被調用。或者,檢查指向函數的指針,若是指針非0,那麼它指向的函數被調用。
    對於n e w - h a n d l e r 的缺省動做是拋出一個異常(throw an exception),這個主題在第1 7章介紹。
然而,若是咱們在程序裏用堆分配,至少要用「內存已用完」的信息代替n e w - h a n d l e r,並異常

中斷程序。用這個辦法,在調試程序時會獲得程序出錯的線索。對於最終的程序,咱們總想使
之具備很強的容錯性。
    經過包含N E W. H ,而後以咱們想裝入的函數地址爲參數調用set_new_handler( )函數,這樣
就替換了n e w - h a n d l e r 。

----------------------- Page 246-----------------------

  246        C + +編程思想
                                                                       下載

    new-handler 函數必須不帶參數且具備v o i d返回值。w h i l e循環將持續分配i n t對象(並丟掉

它們的返回地址)直到所有內存被耗盡。在緊接下去對 n e w調用時,將沒有內存可被分配,所
以調用n e w - h a n d l e r 。
    固然,能夠寫更圓滿的n e w - h a n d l e r ,甚至它能夠收回內存(一般叫作垃圾回收)。這不是
編程新手的工做。

12.5  重載new和delete

    當建立一個n e w表達式時有兩件事發生。首先,使用運算符 n e w分配內存,而後調用構造

函數。在d e l e t e表達式裏,調用析構函數,而後使用運算符d e l e t e釋放內存。咱們永遠沒法控制
構造函數和析構函數的調用(不然咱們可能意外地攪亂它們),但能夠改變內存分配函數運算
符n e w和d e l e t e。
    被n e w和d e l e t e使用的內存分配系統是爲通用目的而設計的。但在特殊的情形下,它不能滿
足咱們的須要。改變分配系統的緣由是考慮效率:咱們也許要建立和銷燬一個特定的類的很是

多的對象以致於這個運算變成了速度的瓶頸。C + +容許重載n e w和d e l e t e來實現咱們本身的存儲
分配方案,因此能夠像這樣處理問題。
    另一個問題是堆碎片:分配不一樣大小的內存可能形成在堆上產生不少碎片,以致於很快
用完內存。也就是內存可能還有,但因爲是碎片,找不到足夠大的內存知足咱們的須要。經過
爲特定類建立咱們本身的內存分配器,能夠確保這種狀況不會發生。

    在嵌入和實時系統裏,程序可能必須在有限的資源狀況下運行很長時間。這樣的系統也可
能要求分配內存花費相同的時間且不容許出現堆內存耗盡或出現不少碎片的狀況。由客戶定製
的內存分配器是一種解決辦法,不然程序設計者在這種狀況下要避免使用n e w和d e l e t e,從而失
去了C + +頗有價值的優勢。
    當重載運算符n e w和d e l e t e時,記住只改變原有的內存分配方法是很重要的。編譯器將用

n e w代替缺省的版本去分配內存,而後爲那個內存調用構造函數。因此,雖然編譯器遇到 n e w
時會分配內存並調用構造函數,但當咱們重載n e w時,能夠改變的只是內存分配部分。(d e l e t e
也有類似的限制。)
    當重載運算符n e w 時,也能夠替換它用完內存時的行爲,因此必須在運算符 n e w裏決定作
什麼:返回0、寫一個調用n e w - h a n d l e r 的循環、再試着分配或用一個b a d _ a l l o c異常處理(在第

1 7章中討論)。
    重載n e w和d e l e t e與重載任何其餘運算符同樣。但能夠選擇重載全局內存分配函數,或爲特
定的類使用特定的分配函數。

12.5.1 重載全局new和delete

    當全局版本的n e w和d e l e t e不能知足整個系統時,對其重載是很極端的方法。若是重載全局

----------------------- Page 247-----------------------

                                              第12章 動態對象建立          247
 下載

版本,那麼缺省版本就徹底不能被訪問—甚至在這個重載定義裏也不能調用它們。
    重載的n e w必須有一個s i z e _ t參數。這個參數由編譯器產生並傳遞給咱們,它是要分配內存的對

象的長度。必須返回一個指向等於這個長度(或大於這個長度,若是咱們有這樣作的緣由)的對象
的指針,或若是沒有找到存儲單元(在這種狀況下,構造函數不被調用),返回一個0。然而若是找
不到存儲單元,不能僅僅返回0,還應該調用n e w - h a n d l e r或進行異常處理,通知這裏存在問題。
    運算符n e w 的返回值是一個v o i d *,而不是指向任何特定類型的指針。它所作的是分配內存,
而不是完成一個對象的創建—直到構造函數調用了才完成對象的建立,這是由編譯器所確保

的動做,不在咱們的控制範圍內。
    運算符d e l e t e接受一個指向由運算符n e w分配的內存的v o i d * 。它是一個v o i d * 由於它是在調
用析構函數後獲得的指針。析構函數從存儲單元裏移去對象。運算符d e l e t e的返回類型是v o i d 。
    下面提供了一個如何重載全局n e w和d e l e t e 的簡單的例子:

----------------------- Page 248-----------------------

  248        C + +編程思想
                                                                       下載

    這裏能夠看到重載n e w和d e l e t e 的通常形式。爲了實現內存分配器,使用了標準 C庫函數
malloc( )和free( ) (可能缺省的n e w和d e l e t e也使用這些函數)。它們還打印出了它們正在作什麼

的信息。注意,這裏使用printf( )和puts( )而不是i o s t r e a m s 。當建立了一個i o s t r e a m對象時(像
全局的c i n、c o u t和c e r r ),它們調用n e w去分配內存。用printf( )不會進入死鎖狀態,由於它不調
用n e w來初始化自己。
    在main( )裏,建立內部數據類型的對象以證實在這種狀況下重載的 n e w和d e l e t e也被調用。
而後建立一個類型s的單個對象,接着建立一個數組。對於數組,咱們能夠看到須要額外的內

存用於存放數組對象數量的信息。在全部狀況裏,都是使用全局重載版本的n e w和d e l e t e。

12.5.2 爲一個類重載new和delete

    爲一個類重載n e w和d e l e t e時,沒必要明說是s t a t i c,咱們還是在建立s t a t i c成員函數。它的語
法也和重載任何其餘運算符同樣。當編譯器看到使用 n e w建立類對象時,它選擇成員版本運算
符n e w而不是全局版本的n e w 。但全局版本的n e w和d e l e t e爲全部其餘類型對象使用(除非它們
有本身的n e w和d e l e t e )。

    在下面的例子裏咱們爲類f r a m i s建立了一個很是簡單的內存分配系統。程序開始時在靜態
數據區域內留出一塊存儲單元。這塊內存是用於 f r a m i s類型對象分配的內存空間。爲了決定哪
塊存儲單元已被使用,這裏使用了一個字節( b y t e s )數組,一個字節( b y t e )表明一塊存儲單元。

----------------------- Page 249-----------------------

                                              第12章 動態對象建立          249
 下載

    經過分配一個足夠大的數組來保存psize  個 f r a m i s對象的方法來爲f r a m i s堆建立一塊內存。
這個內存分配圖是p s i z e個字節,因此每一塊內存對應一個字節。使用設置首元素爲 0的集合初

始化技巧,編譯器可以自動地初始化其他的元素,來使內存分配圖裏的全部字節都初始化爲 0 。
    局部運算符n e w和全局運算符n e w具備相同的形式。它所作的是經過對內存分配圖進行搜
索來找到爲0的字節,而後設置該字節爲1,以此聲明這塊存儲單元已經被分配並返回這個存儲
單元的地址。若是它找不到內存,將發送一個消息並返回 0                      (注意n e w - h a n d l e r沒有被調用,也
沒報告異常,這是由於當咱們用完了內存時,行爲在咱們的控制之下)。由於沒有涉及全局運

算符n e w和d e l e t e ,因此在這個例子裏使用i o s t r e a m s是可行的。
    運算符d e l e t e假設f r a m i s 的地址是在這個堆裏建立的。這是一個公平的假設,由於不管什麼時候
在堆上建立單個f r a m i s對象—不是一個數組,都將調用局部運算符n e w 。全局版本的n e w在數
組狀況下使用。因此用戶偶爾會在沒有用空方括號語法來聲明數組被消除的狀況下調用運算符
d e l e t e,這可能會引發問題。由於用戶也可能刪除在棧上建立的指向對象的指針。若是咱們不

想這樣的事情發生,應該加入一行代碼以確保地址是在這個堆內並是在正確的地址範圍內。

----------------------- Page 250-----------------------

  250        C + +編程思想
                                                                       下載

    運算符d e l e t e計算這個指針表明這個堆裏的哪一塊內存,而後將表明這塊內存的分配圖的
標誌設置爲0來聲明這塊內存已經被釋放。

    在main( )裏,動態地分配了不少f r a m i s對象以使內存用完,這樣就能檢查地址用完時的行
爲。而後釋放一個對象,再建立一個對象來顯示那個釋放了的內存被從新使用了。
    由於這個內存分配方案是針對f r a m i s對象的,因此可能比用缺省的n e w和d e l e t e針對通常目
的內存分配方案效率要高一些。

12.5.3 爲數組重載new和delete

    若是爲一個類重載了運算符n e w和d e l e t e,那麼不管什麼時候建立這個類的一個對象都將調用這

些運算符。但若是爲這些對象建立一個數組時,將調用全局運算符 new( )當即爲這個數組分配
足夠的內存。全局運算符delete( )被調用來釋放這塊內存。能夠經過爲那個類重載數組版本的
運算符n e w [ ]和d e l e t e [ ]來控制對象數組的內存分配。這裏提供了一個顯示兩個不一樣版本被調用
的例子:

----------------------- Page 251-----------------------

                                               第12章 動態對象建立         251
 下載

    這裏,全局版本的n e w和d e l e t e被調用,除了加入了跟蹤信息之外,它們和未重載版本 n e w
和d e l e t e的效果是同樣的。固然,咱們能夠在重載的n e w和d e l e t e裏使用想要的內存分配方案。

    能夠看到除了加了一個括號外,數組版本的n e w和d e l e t e與單個對象版本是同樣的。在這兩
種狀況下,要傳遞分配的對象內存大小。傳遞給數組版本的內存大小是整個數組的大小。應該
記住重載運算符n e w惟一須要作的是返回指向一個足夠大的內存的指針。雖然咱們能夠初始化
那塊內存,但一般這是構造函數的工做,構造函數將被編譯器自動調用。
    這裏構造函數和析構函數只是打印出字符,因此咱們能夠看到它們已被調用。下面是這個

跟蹤文件的輸出信息:

    和預計的同樣(這個機器爲一個 i n t使用2個字節),建立一個對象須要2 0個字節。運算符
n e w被調用,而後是構造函數(由*指出)。以一個相反的形式調用d e l e t e使得析構函數被調用,
而後是d e l e t e。

    當建立一個w i d g e t對象數組時,可以使用數組版本n e w 。但注意,申請的長度比指望的大4個
字節。這額外的4個字節是系統用來存放數組信息的,特別是數組中對象的數量。當用下面的
表達式時,方括號就告訴編譯器它是一個對象數組,因此編譯器產生尋找數組中對象的數量的
代碼,而後屢次調用析構函數。

    delete []widget ;

    咱們能夠看到,即便數組運算符n e w和d e l e t e只爲整個數組調用一次,但對於數組中的每一
個對象,缺省的構造函數和析構函數都被調用。

12.5.4 構造函數調用

    foo* f = new foo ;

    上面的表達式調用n e w分配一個f o o長度大小的內存,而後在那個內存上調用f o o構造函數。

----------------------- Page 252-----------------------

  252        C + +編程思想
                                                                      下載

假設全部的安全措施都失敗了而且運算符 n e w返回值是0,將會發生什麼?在上述狀況下,構
造函數沒有調用,因此雖然沒有一個成功建立的對象,但至少咱們也沒有調用構造函數和傳遞

給它一個0指針。下面是一個證實這一點的例子:

    當程序運行時,它僅打印來自運算符n e w 的信息。由於n e w返回0,因此構造函數沒有被調
用,固然它的信息不會打印出來。

12.5.5 對象放置

    重載運算符n e w還有其餘兩個不常見的用途。
    1) 可能想要在內存的指定位置上放置一個對象。這對於面向硬件的內嵌系統特別重要,在
這個系統中,一個對象可能和一個特定的硬件是同義的。
    2) 可能想在調用n e w時,能夠從不一樣的內存分配器中選擇。
    這兩種情形都用相同的機制解決:重載的運算符 n e w能夠帶多於一個的參數。像前面所看

到的,第一個參數老是對象的長度,它是在內部計算出來的並由編譯器傳遞。但其餘參數能夠
由咱們本身定義:一個放置對象的地址、一個對內存分配函數或對象的引用、或其餘方便的任
何設置。
    起先在調用過程當中傳遞額外的參數給運算符 n e w 的方法看起來彷佛有點古怪:在關鍵字
n e w後是參數表(沒有s i z e _ t參數,它由編譯器處理),參數表後面是正在建立的對象的類名字。

例如:

    X* xp = new(a) X ;

----------------------- Page 253-----------------------

                                              第12章 動態對象建立          253
 下載

將傳遞a做爲第二個參數給運算符n e w 。固然,這是在這個運算符n e w 已經聲明的狀況下才有效
的。

    下面的例子顯示瞭如何在一個特定的存儲單元裏放置一個對象。

    注意運算符n e w僅返回傳遞給它的指針。所以,應該由調用者決定對象存放在哪裏,決定
做爲n e w表達式的一部分的構造函數將在哪塊內存上被調用。
    銷燬對象時將會出現兩難選擇的局面。由於僅有一個版本運算符 d e l e t e ,因此沒有辦法說
「對這個對象使用個人特殊內存釋放器」。咱們能夠調用析構函數,但不能用動態內存機制釋放
內存,由於內存不是在堆上分配的。

    答案是用很是特殊的語法:能夠顯式地調用析構函數。例如:

    xp->X::~X( ) ; //explicit destructor call

    這裏會出現一個嚴厲的警告。一些人把這看做是在範圍結束前的某一時刻銷燬對象的一種

方法,而不是調整範圍,或想要這個對象的生命期在運行時肯定時,更正確地使用動態對象創
建的方法。若是用這種方法爲在棧上建立的對象調用析構函數,將會出現嚴重的問題,由於析
構函數在出範圍時又會被調用一次。若是爲在堆上建立的對象用這種方法調用析構函數,析構
函數將被執行,但內存不釋放,這多是咱們不但願的結果。用這種方法顯式地調用析構函數,
其實只有一個緣由,即爲運算符new 支持存放語法。

    雖然這個例子僅顯示一個附加的參數,但若是爲了其餘目的而增長更多的參數,也是可行
的。

12.6  小結

    在棧上建立自動對象既方便又理想,但爲了解決通常程序問題,咱們必須在程序執行的任

----------------------- Page 254-----------------------

  254        C + +編程思想
                                                                       下載

什麼時候候,特別是須要對來自程序外部信息反應時,可以建立和銷燬對象。雖然 C 的動態內存分
配能夠從堆上獲得內存,但它在C + +上不易使用且不可以保證安全。使用n e w和d e l e t e進行動態

對象建立,這已經成爲C + +語言的核心,因此能夠像在棧上建立對象同樣容易地在堆上建立對
象,另外,還能夠有很大的靈活性。若是n e w和d e l e t e不能知足要求,尤爲是它們沒有足夠的效
率時,程序員還能夠改變它們的行爲,也能夠在內存用完時進行修改。(第1 7章討論的異常處
理也在這裏使用了)

12.7  練習

    1. 寫一個有構造函數和析構函數的類。在構造函數和析構函數裏經過 c o u t宣佈本身。經過

這個類本身證實n e w和d e l e t e老是調用構造函數和析構函數。用n e w建立這個類的一個對象,用
d e l e t e銷燬它。在堆上建立和銷燬這些對象的一個數組。
    2.  建立一個p s t a s h對象,並把練習1的對象用n e w建立填入。觀察當這個對象出了範圍和它
的析構函數被調用時發生的狀況。
    3. 寫一個有單個對象版本和數組版本的重載運算符n e w和d e l e t e的類。演示這兩個版本的工

做狀況。
    4.  設計一個對F R A M I S . C P P進行測試的程序來顯示定製的n e w和d e l e t e 比全局的n e w和d e l e t e
大約快多少。

----------------------- Page 255-----------------------

 下載

                     第1 3章  繼承和組合

    C + +最重要的性能之一是代碼重用。可是,爲了具備可進化性,咱們應當可以作比拷貝代

碼更多的工做。
    在C的方法中,這個問題未能獲得很好的解決。而用 C + +,能夠用類的方法解決,經過創
建新類重用代碼,而不是從頭建立它們,這樣,咱們能夠使用其餘人已經建立並調試過的類。
    關鍵是使用類而不是更改已存在的代碼。這一章將介紹兩種完成這件事的方法。第一種方
法是很直接的:簡單地建立一個包含已存在的類對象的新類,這稱爲組合,由於這個新類是由

已存在類的對象組合的。
    第二種方法更巧妙,建立一個新類做爲一個已存在類的類型,採起這個已存在類的形式,
對它增長代碼,但不修改它。這個有趣的活動被稱爲繼承,其中大量的工做由編譯器完成。繼
承是面向對象程序設計的基石,而且還有另外的含義,將在下一章中探討。
    對於組合和繼承(感受上,它們都是由已存在的類型產生新類型的方法),它們在語法上

和行爲上是相似的。這一章中,讀者將學習這些代碼重用機制。

13.1  組合語法

    實際上,咱們一直都在用組合建立類,只不過咱們是在用內部數據類型組合新類。其實使
用用戶定義類型組合新類一樣很容易。
    考慮下面這個在某種意義上頗有價值的類:

在這個類中,數值成員是私有的,因此對於將類型 X                     的一個對象做爲公共對象嵌入到一個新
類內部,是絕對安全的。

----------------------- Page 256-----------------------

  256        C + +編程思想
                                                                      下載

訪問嵌入對象(稱爲子對象)的成員函數只須再一次選擇成員。
    若是嵌入的對象是p r i v a t e 的,可能更具通常性,這樣,它們就變成了內部實現的一部分
(這意味着,若是咱們願意,咱們能夠改變這個實現)。而對於新類的p u b l i c接口函數,包含對
嵌入對象的使用,但沒必要模仿這個嵌入對象的接口。

這裏,p e r m u t e ( ) 函數的執行調用了X 的接口,而X的其餘成員函數也在Y的成員函數中被調用。

13.2  繼承語法

    組合的語法是明顯的,而完成繼承,則有新的不一樣的形式。
    繼承也就是說「這個新的類像那個老的類」。經過給出這個類的名字,在代碼中聲明繼承,
在這個類體的開括號前面,加一冒號和基類名(或加多個類,對於多重繼承)。這樣作,就自
動地獲得了基類中的全部數據成員和成員函數。下面是一個例子:

    在Y 中,咱們能夠看到繼承,它意味着 Y  將包含 X                中的全部數據成員和成員函數。實際

----------------------- Page 257-----------------------

                                                第13章  繼承和組合        257
 下載

上,Y包含了 X  的一個子對象,就像在 Y  中建立X  的一個成員對象,而不從 X 繼承同樣。無

論成員對象仍是基類存儲,都被認爲是子對象。在m a i n ( ),能夠看到這些數據成員已被加入了,

由於s i z e o f ( Y)是s i z e o f ( X )的2倍大。

    咱們將注意到,基類由p u b l i c處理,不然,在繼承中,全部被繼承的東西都是 p r i v a t e ,也

就是說在基類中的全部p u b l i c成員在派生類中都是p r i v a t e 。這固然不是所但願的,但願的結果

是保持基類中的p u b l i c成員在派生類中也是p u b l i c 。這可在繼承期間用關鍵字p u b l i c作到。

    在c h a n g e ( )中,基類p e r m u t e ( ) 函數被調用,派生類對全部的p u b l i c基類函數都有直接的訪問

權。

    在派生類中的s e t ( )函數重定義了基類中的s e t ( )函數。這就是,若是對於類型 Y               的對象調用

函數r e a d ( )和p e r m u t e ( ) ,獲得的是這些函數的基類版本(能夠在m a i n ( ) 中看到表現)。但若是對

於對象 Y  調用 s e t ( ),獲得的是重定義的版本。這意味着,若是咱們不喜歡在繼承中獲得某個

函數的基類版本,能夠改變它(還可以增長全新的函數,例如c h a n g e ( ) )。

    然而,當重定義函數時,咱們可能但願仍然保留基類版本。若是簡單地調用 s e t ( ),獲得的

是這個函數的本地版本(一個遞歸函數調用)。爲了調用基類版本,咱們必須用準確名,即便

----------------------- Page 258-----------------------

  258        C + +編程思想
                                                                      下載

用基類名和範圍分解運算符。

13.3  構造函數的初始化表達式表

    咱們已經看到,在C + +中保證合適的初始化表達式多麼重要,在組合和繼承中,也是同樣。

當建立一個對象時,必須保證編譯器調用全部子對象的構造函數。到目前爲止,例子中的全部
子對象都有缺省的構造函數,編譯器能夠自動調用它們。可是,若是子對象沒有缺省構造函數
或若是咱們想改變某個構造函數的缺省參數,狀況會怎麼樣呢?這是一個問題,由於這個新類
的構造函數不能保證訪問它的子對象的p r i v a t e數據成員,因此不能直接地對它們初始化。
    解決辦法很簡單:對於子對象調用構造函數, C + +爲此提供了專門的語法,即構造函數的

初始化表達式表。構造函數的初始化表達式表的形式模仿繼承活動。對於繼承,咱們在冒號之
後和這個類體的左括號以前放置基類。而在構造函數的初始化表達式表中,咱們能夠將對子對
象構造函數的調用語句放在構造函數參數表和冒號以後,在函數體的開括號以前。對於從  b a r
繼承來的類 f o o,若是bar 有一個取單個i n t參數的構造函數,則表示爲

    foo::foo(int i) : bar(i) { //...

13.3.1 成員對象初始化

    當使用組合時,對於成員對象初始化使用相同的語法是不可行的。對於組合,給出對象的
名字而不是類名。若是在初始化表達式表中有多於一個構造函數調用,應當用逗號隔開:

    foo2:foo2(int I) : bar(i), memb(i+1) { // ...

這是類 foo2  構造函數的開頭,它是從 bar          繼承來的,包含稱爲 memb         的成員對象。注意,當
咱們在這個構造函數的初始化表達式表中能看到基類的類型時,只能看到成員對象的標識符。

13.3.2 在初始化表達式表中的內置類型

    構造函數的初始化表達式表容許咱們顯式地調用成員對象的構造函數。事實上,這裏沒有
其它方法調用那些構造函數。主要思想是,在進入新類的構造函數體以前調用全部的構造函數。
這樣,對子對象的成員函數所作的任何調用都已經轉到了這個被初始化的對象中。沒有對全部
的成員對象和基類對象的構造函數調用,就沒有辦法進入這個構造函數的左括號,即使是編譯
器也必須對缺省構造函數作隱藏調用。這是C + +進一步的強制,以保證沒有調用它的構造函數

就沒有對象(或對象的部分)能進入第一道門。
    全部的成員對象在構造函數的左括號以前被初始化的思想是編程時一個方便的輔助方法。
一旦遇到左括號,咱們能假設全部的子對象已被適當地初始化了,並集中精力在但願該構造函
數完成的特殊任務上。然而,這裏還有一個問題:內置類型的嵌入對象如何?它沒有構造函數
嗎?

    爲了讓語法一致,容許對待內置類型就像對待有單個構造函數的對象同樣,它取單個參數:
這個參數與咱們正在初始化的變量類型相同。這樣,咱們就能夠寫:

----------------------- Page 259-----------------------

                                                 第13章  繼承和組合        259
 下載

    這些「僞構造函數調用」是爲了完成簡單的賦值。它是傳統的技術和好的編碼風格,因此
經常能看到它被使用。
    甚至在類以外建立這種類型的變量時,咱們也可能用僞構造函數語法:

    int i(100);

這使得內置類型的效果更像對象。
    記住,這些並不真的是構造函數,特別是,若是沒有顯式地進行僞構造函數調用,就不會
進行初始化。

13.4  組合和繼承的聯合

    固然,咱們還能夠把二者放在一塊兒使用。下面的例子中這個更復雜類的建立就使用了繼承
和組合兩種方法。

----------------------- Page 260-----------------------

  260        C + +編程思想
                                                                      下載

    C  繼承 B  而且有一個成員對象(這是類A 的對象)。咱們能夠看到,構造函數的初始化表
達式表中調用了基類構造函數和成員對象構造函數。

    函數C::f() 重定義了它所繼承的B : : f ( ),而且還調用基類版本。另外,它還調用了a . f ( )。注意,
只能在繼承期間重定義函數。經過成員對象,只能操做這個對象的公共接口,而不能重定義它。
另外,若是C::f() 尚未被定義,則對類型C 的一個對象調用f() 不會調用a . f ( ),而是調用B : : f ( )。
    •  自動析構函數調用
    雖然經常須要在初始化表達式表中作顯式構造函數調用,但咱們決不須要作顯式的析構函

數調用,由於對於任何類型只有一個析構函數,而且它並不取任何參數。然而,編譯器仍保證
全部的析構函數被調用,這意味着,在整個層次中的全部析構函數,從最底層的析構函數開始
調用,一直到根層。
    構造函數和析構函數不同凡響之處在於每一層函數都被調用,這是值得強調的。然而對於
通常的成員函數,只是這個函數被調用,而不是任意基類版本被調用。若是還想調用通常成員

函數的基類版本,必須顯式地調用。

13.4.1 構造函數和析構函數的次序

    當一個對象有許多子對象時,知道構造函數和析構函數的調用次序是有趣的。下面的例子
明顯代表它如何工做:

----------------------- Page 261-----------------------

                                                第13章  繼承和組合        261
 下載

    首先,建立 ofstream 對象,以發送全部輸出到一個文件中。爲了在書中少敲一些字符也爲

了演示一種宏技術(這個技術將在第 1 8章中被一個更好的技術代替),這裏使用了宏以創建一
些類(這些類將被用於繼承和組合)。每一個構造函數和析構函數向這個跟蹤文件報告它們本身
的行動。注意,這些是構造函數,而不是缺省構造函數,它們每個都有一整型參數。這個參
數自己沒有標識符,它的惟一的任務就是強迫在初始化表達式表中顯式調用這些構造函數。
(消除標識符防止編譯器警告信息)這個程序的輸出是:

    能夠看出,構造在類層次的最根處開始,而在每一層,首先調用基類構造函數,而後調用
成員對象構造函數。調用析構函數則嚴格按照構造函數相反的次序—這是很重要的,由於要

考慮潛在的相關性。另外一有趣的是,對於成員對象,構造函數調用的次序徹底不受在構造函數
的初始化表達式表中次序的影響。該次序是由成員對象在類中聲明的次序所決定的。若是能通
過構造函數的初始化表達式表改變構造函數調用次序,那麼就會對兩個不一樣的構造函數有二種
不一樣的調用順序。而析構函數不可能知道如何爲析構函數相應地反轉調用次序,這就引發了相
關性問題。

13.4.2 名字隱藏

    若是在基類中有一個函數名被重載幾回,在派生類中重定義這個函數名會掩蓋全部基類版

----------------------- Page 262-----------------------

  262        C + +編程思想
                                                                       下載

本,這也就是說,它們在派生類中變得再也不可用。

    由於bart  重定義了 d o h ( ),這些基類版本中沒有一個是對於 bart  對象可調用的。這時,編譯
器試圖變換參數成爲一個 milhouse  對象,並報告出錯,由於它不能找到這樣的變換。正如在

下面章節中會看到的,更廣泛的方法是用與在基類中嚴格相同的符號重定義函數而且返回類型
也與基類中的相同。

13.4.3 非自動繼承的函數

    不是全部的函數都能自動地從基類繼承到派生類中的。構造函數和析構函數是用來處理對
象的建立和析構的,它們只知道對在它們的特殊層次的對象作什麼。因此,在整個層次中的所
有的構造函數和析構函數都必須被調用,也就是說,構造函數和析構函數不能被繼承。

    另外,operator=  也不能被繼承,由於它完成相似於構造函數的活動。這就是說,儘管我
們知道如何由等號右邊的對象初始化左邊的對象的全部成員,但這並不意味着這個初始化在繼
承後仍有意義。在繼承過程當中,若是咱們不親自建立這些函數,編譯器就綜合它們。(經過構
造函數,咱們不能對缺省構造函數和被自動建立的拷貝構造函數建立任何構造函數)這在第 11
章中已經簡要地講過了。被綜合的構造函數使用成員方式的初始化,而被綜合的  operator=  使

用成員方式的賦值。這是由編譯器建立而不是繼承的函數的例子。

----------------------- Page 263-----------------------

                                                第13章  繼承和組合        263
 下載

    全部的構造函數和 operator=  都自我宣佈,因此咱們能知道編譯器什麼時候使用它們。另外,
operator other() 完成自動類型變換,從 root  對象到被嵌入的類 other  的對象。類 derived  直接從
root  繼承,並無建立函數(觀察編譯器如何反應)。函數 f()  取一個 other  對象以測試這個自
動類型變換函數。
    在 main() 中,建立缺省構造函數和拷貝構造函數,調用 root                 版本做爲構造函數調用繼承

的一部分,儘管這看上去像是繼承,但新的構造函數其實是建立的。正如咱們所預料的,自
動建立帶參數的構造函數是不可能的,由於這樣太依賴編譯器的直覺。
    在 derived 中,operator=( )  也被綜合爲新函數,使用成員函數賦值,由於這個函數在新類
中不顯式地寫出。
    關於處理對象建立的重寫函數的全部這些原則,咱們也許會以爲奇怪,爲何自動類型變

換運算也能被繼承。但其實這不足爲奇—若是在 root                    中有足夠的塊創建一個 other  對象,那
麼從 root 派生出的任何東西中,這些塊仍在原地,類型變換固然也就仍然有效。(儘管實際上
咱們可能想重定義它)

13.5  組合與繼承的選擇

    不管組合仍是繼承都能把子對象放在新類型中。二者都使用構造函數的初始化表達式表去
構造這些子對象。如今咱們可能會奇怪,這二者之間到底有什麼不一樣?該如何選擇?

----------------------- Page 264-----------------------

  264        C + +編程思想
                                                                       下載

    組合一般在但願新類內部有已存在類性能時使用,而不但願已存在類做爲它的接口。這就
是說,嵌入一個計劃用於實現新類性能的對象,而新類的用戶看到的是新定義的接口而不是來

自老類的接口。爲此,在新類的內部嵌入已存在類的private  對象。
    有時,容許類用戶直接訪問新類的組合是有意義的,這就讓成員對象是 p u b l i c 。成員函數
隱藏它們本身的實現,因此,當用戶知道咱們正在裝配一組零件而且使得接口對他們來講更容
易理解時,這樣會安全的。Car 對象是一個很好的例子:

----------------------- Page 265-----------------------

                                                第13章  繼承和組合        265
 下載

    由於小汽車的組合是分析這個問題的一部分(不是基本設計的部分),因此讓成員是公共
的有助於客戶程序員理解如何使用這個類,並且能使類的建立者有更小的代碼複雜性。

    稍加思考就會看到,用車輛對象組合小汽車是無心義的—小汽車不能包含車輛,它自己
就是一種車輛。這種i s - a關係用繼承表達,而 has-a  關係用組合表達。

13.5.1 子類型設置

    如今假設想建立包含 ifstream  對象的一個類,它不只打開一個文件,並且還保存文件名。
這時咱們能夠使用組合並把 ifstream 及 strstream 都嵌入這個新類中:

    然而這裏存在一個這樣的問題:咱們也許想經過包含一個從 fname1  到ifstream & 的自動類

型轉換運算,在任何使用 ifstream  的地方都使用 fname1 對象,但在 main  中,

----------------------- Page 266-----------------------

  266        C + +編程思想
                                                                       下載

    c o u t < < f i l e . r d b u f ( ) < < e n d l ;

這一行不能編譯,由於自動類型轉換隻發生在函數調用中,而不在成員選擇期間。因此,此時
這個方法不行。
    第二個方法是對 fname1 增長rdbuf() 定義:

    filebuf * rdbuf() {return File.rdbuf();}

    若是隻有不多的函數從ifsream  類中拿來,這是可行的。在這種狀況下,咱們只是使用了
這個類的一部分,而且組合是適用的。

    可是,若是咱們但願這個類的東西都進來,應該作什麼呢?這稱爲子類型設置,由於正在
由一個已存在的類作一個新類,而且但願這個新類與已存在的類有嚴格相同的接口(但願增長
任何咱們想要加入的其餘成員函數),因此能在已經用過這個已存在類的任何地方使用這個新類,
這就是必須使用繼承的地方。咱們能夠看到,子類型設置很好地解決了先前例子中的問題。

----------------------- Page 267-----------------------

                                                 第13章  繼承和組合        267
 下載

    如今,能與ofstream  對象一塊兒工做的任何成員函數也能與 fname2  對象一塊兒工做。這是因
爲,fname2  是o f s t r e a m的一個類型。不是簡單地包含。這是很是重要的問題,將在本章最後和

在第1 4章中討論。

13.5.2 專門化

    繼承也就是取一個已存在的類,並製做它的一個專門的版本。一般,這意味着取一個通常
目的的類併爲了特殊的須要對它專門化。
    例如,考慮前面章中的 stack  類,與這個類有關的問題之一是必須每次完成計算,從包容
器中引出一個指針。這不只乏味,並且不安全—咱們能讓這個指針指向所但願的任何地方。

    較好的方法是使用繼承來專門化這個通常的 stack  類。這裏有一個例子,它使用來自前一
章的類。

    兩個頭文件S T R I N G S . H (第1 2 . 2 . 1節)和 S TA C K 11.H (第1 2 . 2 . 3章)都從第1 2章引來

----------------------- Page 268-----------------------

  268        C + +編程思想
                                                                      下載

(S TA C K 11.OBJ 文件也必須連進來。)
    s t r i n g l i s t專門化stack ,因此p u s h ( )只接受s t r i n g指針。在此以前,s t a c k會接受v o i d指針,所

以用戶沒有類型檢查以確保插入合適的指針。另外,p e e k ( )和p o p ( )如今返回s t r i n g指針,而不返
回v o i d指針,因此使用這個指針時沒必要映射。
    使人驚奇的是,額外的類型安全性檢查是無需代價的!編譯器給出額外的類型信息,這些
只在編譯時使用,而這些函數是內聯的,並不產生另外的代碼。
    不幸的是,繼承並不能解決全部這些與包容器類有關的問題,析構函數仍然引發麻煩。我

們也許會記得,在第1 2章中,s t a c k : : ~ s t a c k ( )析構函數遍歷整個表,並對全部的指針調用d e l e t e。
問題是,對於v o i d指針調用d e l e t e,它只釋放這塊內存而不調用析構函數(由於v o i d沒有類型信
息)。能夠建立一個s t r i n g l i s t : : ~ s t r i n g l i s t ( )析構函數,它能遍歷這個表並對全部在表中的s t r i n g指
針調用d e l e t e,這樣,問題就解決了。條件是:
    1) stack數據成員被定義爲p r o t e c t e d ,使得這個s t r i n g l i s t析構函數能訪問它們。(p r o t e c t e d在

本章稍後介紹。)
    2) 移去s t a c k基類析構函數,使得這塊內存不會兩次被釋放。
    3)  不執行更多的繼承,不然會再次面臨相同的兩難境地:要麼調用多重析構函數,要麼進
行不正確的析構函數調用(可能包含s t r i n g對象而不是從s t r i n g l i s t派生出的類)。
    這個問題將在下一章重述,但直到第1 5章介紹了模板後才能獲得徹底解決。

13.5.3 私有繼承

    經過在基類表中去掉 public       或者經過顯式地聲明p r i v a t e ,能夠私有地繼承基類(後者可能
是更好的策略,由於能夠讓用戶明白它的含義)。當私有繼承時,建立的新類有基類的全部數
據和功能,但這些功能是隱藏的,因此它只是內部實現部分。該類的用戶訪問不到這些內部功
能,而且一個對象不被看做這個基類的成員(如在第1 3 . 5 . 1節中的F N A M E 2 . C P P 中的)。
    咱們可能奇怪,private  繼承的目的是什麼,由於在這個新類中選擇建立一個p r i v a t e對象似

乎更合適。將p r i v a t e繼承包含在該語言中只是爲了語言的完整性。可是,若是沒有其餘理由,
則應當減小混淆,因此一般建議用p r i v a t e成員而不是p r i v a t e繼承。然而,這裏可能偶然有這種
狀況,便可能想產生像基類接口同樣的接口,而不容許處理該對象像處理基類對象同樣。
p r i v a t e繼承提供了這個功能。
    • 對私有繼承成員公有化

    當私有繼承時,基類的全部p u b l i c成員都變成了p r i v a t e 。若是但願它們中的任何一個是可
視的,只要用派生類的p u b l i c選項聲明它們的名字便可。

----------------------- Page 269-----------------------

                                                 第13章  繼承和組合        269
 下載

    這樣,若是想要隱藏這個類的基類部分的功能,則p r i v a t e繼承是有用的。
    在咱們使用p r i v a t e繼承取代對象成員以前,應當注意到,當與運行類型標識相連時,私有
繼承有特定的複雜性。(第1 8章內容。)

13.6  保護

    關鍵字p r o t e c t e d對於繼承有特殊的意義。在理想世界中,p r i v a t e成員老是嚴格私有的,但
在實際項目中,有時但願某些東西隱藏起來,但仍容許其派生類的成員訪問。因而關鍵字
p r o t e c t e d派上了用場。它的意思是:「就這個類的用戶而言,它是p r i v a t e的,但它可被從這個類
繼承來的任何類使用。」
    數據成員最好是p r i v a t e ,由於咱們應該保留改變內部實現的權利。而後咱們才能經過保護

成員函數控制對該類的繼承者的訪問。

    在附錄C中的S S H A P E例子中,咱們能夠看到須要p r o t e c t e d的很好的例子。

----------------------- Page 270-----------------------

  270        C + +編程思想
                                                                      下載

被保護的繼承

    繼承時,基類缺省爲p r i v a t e ,這意味着全部p u b l i c成員函數對於新類的用戶是 p r i v a t e 的。

一般咱們都會讓繼承p u b l i c ,從而使得基類的接口也是派生類的接口。然而在繼承期間,也可
以使用p r o t e c t e d關鍵字。
    被保護的派生意味着對其餘類來「照此實現」,但對派生類和友元是「i s - a 」。它是不經常使用
的,它的存在只是爲了語言的完整性。

13.7  多重繼承

    既然咱們已能夠從一個類繼承,那麼咱們也就應該能同時從多個類繼承。實際上這是能夠

作到的,可是否它象設計部分同樣有意義還是一個有爭議的話題。不過有一點是能夠確定的:
直到咱們已經很好地學會程序設計並徹底理解這門語言時,咱們才能試着用它。這時,咱們大
概會認識到,無論咱們如何認爲咱們必須用多重繼承,咱們老是能經過單重繼承來完成。
    起初,多重繼承彷佛很簡單,在繼承期間,只需在基類表中增長多個類,用逗號隔開。然
而,多重繼承有不少含糊的可能性,這就是爲何第 1 6章要討論這一主題的緣由。

13.8  漸增式開發

    繼承的優勢之一是它支持漸增式開發,它容許咱們在已存在的代碼中引進新代碼,而不會
給原代碼帶來錯誤,即便產生了錯誤,這個錯誤也只與新代碼有關。也就是說當咱們繼承已存
在的功能類並對其增長數據成員和成員函數(並重定義已存在的成員函數)時,已存在類的代
碼並不會被改變,更不會產生錯誤。
    若是錯誤出現,咱們就會知道它確定是在咱們的新派生代碼中。相對於修改已存在代碼體

的作法來講,這些新代碼很短也很容易讀。
    至關奇怪的是,這些類如何清楚地被隔離。爲了重用這些代碼,甚至不須要這些成員函數
的源代碼,只須要表示類的頭文件和目標文件或帶有已編譯成員函數的庫文件。(對於繼承和
組合都是這樣。)
    認識到程序開發是一個漸增過程,就象人的學習過程同樣,這是很重要的。咱們能作儘可

能多的分析,但當開始一個項目時,咱們仍不可能知道全部的答案。若是開始把項目做爲一個
有機的、可進化的生物來「培養」,而不是徹底一次性的構造它,使之像一個玻璃盒子式的摩
天大樓,那麼咱們就會得到更大的成功和更直接的反饋。
    雖然繼承對於實驗是有用的技術,但在事情穩定以後,咱們須要用新眼光從新審視一下我
們的類層次,把它當作可感知的結構。記住,繼承首先表示一種關係,其意爲:「新類是老類

的一個類型。」咱們的程序不該當關心怎樣擺佈比特位,而應當關心如何建立和處理各種型的
對象,以便用問題的術語表示模型。

13.9  向上映射

    在這一章的前面,咱們已經看到了由ofstream 派生而來的類的對象如何有ofstream 對象全部
的特性和行爲。在13.5.1節中FNAME2.CPP 中,任何ofstream成員函數應當能被fname2對象調用。
    繼承的最重要的方面不是它爲新類提供了成員函數,而在於它是基類與新類之間的關係描

述:「新類是已存在類的一個類型」。
    這個描述不只僅是一種解釋繼承的方法—它直接由編譯器支持。例如,考慮稱爲

----------------------- Page 271-----------------------

                                                第13章  繼承和組合        271
 下載

i n s t r u m e n t的基類(它表示樂器)和派生類w i n d ,由於繼承意味着在基類中的全部函數在派生
類中也是可行的,能夠發送給基類的消息也能夠發送給這個派生類,因此,若是 i n s t r u m e n t類

有p l a y ( )成員函數,那麼wind 也有。這意味着,咱們能夠確切地說,w i n d是instrument            的一個
類型。下面的例子代表編譯器是如何支持這個概念的。

    在這個例子中,有趣的是t u n e ( )函數,它接受i n s t r u m e n t參數。然而,在m a i n ( ) 中,t u n e ( )函
數的調用卻被傳遞了一個w i n d參數 。咱們可能會感到奇怪,C + +對於類型檢查應該是很是嚴格
的,而接受某個類型的函數爲何會這麼容易地接受另外一個類型。直到人們認識到w i n d對象也
是一個i n s t r u m e n t對象,t u n e ( )函數能對instrument  調用,也能對w i n d調用時,纔會恍然大悟。在

t u n e ( ) 中,這些代碼對i n s t r u m e n t和從i n s t r u m e n t派生來的任何類型都有效,這種將w i n d 的對象、
引用或指針轉變成i n s t r u m e n t對象、引用或指針的活動稱爲向上映射。

13.9.1 爲何「向上映射」

    這個術語引入的是有其歷史緣由的,並且它也與類繼承圖的傳統畫

法有關:在頂部是根,向下長(固然咱們能夠用任何咱們認爲方便的方
法畫咱們的圖)。對於W I N D . C C P的繼承如右圖
    從派生類到基類的映射,在繼承圖中是上升的,因此通常稱爲向上映射。向上映射老是安全
的。由於是從更專門的類型到更通常的類型—對於這個類接口可能出現的惟一的狀況是它失去成
員函數,不會增長成員函數。這就是編譯器容許向上映射不須要顯式地說明或作其餘標記的緣由。

    •  向下映射
    固然咱們也能夠實現向上映射的反轉,稱爲向下映射,可是,這涉及到一個兩難問題,這
是第1 7章中討論的主題。

----------------------- Page 272-----------------------

  272        C + +編程思想
                                                                      下載

13.9.2 組合與繼承

    肯定應當用組合仍是用繼承,最清楚的方法之一是詢問是否須要新類向上映射。在本章的
前面,s t a c k類經過繼承被專門化,然而,s t r i n g l i s t對象僅做爲s t r i n g包容器,不需向上映射,所
以更合適的方法多是組合:

    這個文件與I N H S TA C K . C P P ( 1 3 . 5 . 2節中)是同樣的,只不過s t a c k對象被嵌入在s t r i n g l i s t 內,
還有被嵌入對象調用的一些函數也被嵌入在 s t r i n g l i s t 內。這裏沒有時間和空間的開銷,由於其
子類佔用相同量的空間,並且全部另外的類型檢查都發生在編譯時。
    咱們也能夠用p r i v a t e繼承以表示「照此實現」。用以建立s t r i n g l i s t類的方法在這種狀況下不

是重要的—由於各類方法都能解決問題。然而,當可能存在多重繼承時就要注意了,多重繼
承時可能被警告。在這種狀況下,若是能發現一個類組合能夠使用,那就不要用繼承,由於這
樣能夠消除對多重繼承的須要。

----------------------- Page 273-----------------------

                                                第13章  繼承和組合        273
 下載

13.9.3 指針和引用的向上映射

    在W I N D . C P P ( 1 3 . 9 ) 中,向上映射發生在函數調用期間—在函數外的w i n d對象被引用並
且變成一個在這個函數內的i n s t r u m e n t的引用。
    向上映射還能出如今對指針或引用簡單賦值期間:

和函數調用同樣,這兩個例子都不要求顯式地映射。

13.9.4 危機

    固然,任何向上映射都會損失對象的類型信息,若是說

    wind w;
    instrument * ip = &w;

編譯器只能把i p做爲一個i n s t r u m e n t指針處理。這就是說,它不知道i p實際上指向w i n d 的對象。
因此,當調用p l a y ( )成員函數時,若是使用

    i p - > p l a y ( m i d d l e C ) ;

編譯器只知道它正在對於一個i n s t r u m e n t指針調用p l a y ( ) ,並調用i n s t r u m e n t ∷p l a y ( )的基本版本,
而不是它應該作的調用w i n d ∷p l a y ( ) 。這樣將會獲得不正確的結果。
    這是一個重要的問題,將在下一章經過介紹面向對象編程的第三塊基石:多態性(在 C + +
中用v i r t u a l函數實現)來解決。

13.10  小結

    繼承和組合都容許由已存在的類型建立新類型,二者都是在新類型中嵌入已存在的類型的
子對象。然而,當咱們想重用原類型做爲新類型的內部實現的話,咱們最好用組合,若是咱們
不只想重用這個內部實現並且還想重用原來接口的話那就用繼承。若是派生類有基類的接口,
它就能向上映射到這個基類,這一點對多態性很重要,這將在下一章中講到。
    雖然經過組合和繼承進行代碼重用對於快速項目開發有幫助,但一般咱們會但願在容許其
他程序員依據它開發程序以前從新設計類層次。
    咱們的類層次必須有這樣的特性:它的每一個類有專門的用途,不能太大(包含太多的功能不
利於重用),也不能過小(過小如不對它自己增長功能就不能使用)。並且這些類應當容易重用。

13.11  練習

    1.  修改C A R . C P P,使得它也從被稱爲v e h i c l e 的類繼承,在v e h i c l e中放置合適的成員函數
(也就是說,補充一些成員函數)。對v e h i c l e增長一個非缺省的構造函數,在c a r的構造函數內部
必須調用它。
    2.  建立兩個類,A和B,帶有能宣佈本身的缺省構造函數。從A繼承出一個新類,稱爲C,
而且在C中建立B的一個成員對象,而不對C建立構造函數。建立類C 的一個對象,觀察結果。
    3.  使用繼承,專門化在第1 2章(P S TASH.H & PSTA S H . C P P )中的p s t a s h類,使得它接受
和返回S t r i n g指針。修改P S T E S T. C P P並測試它。改變這個類使得p s t a s h是一個成員對象。
    4.  使用p r i v a t e和p r o t e c t e d繼承從基類建立兩個新類。而後嘗試向上映射這個派生類的對象
成爲基類。解釋所發生的事情。

----------------------- Page 274-----------------------

                                                                      下載

                   第1 4章  多態和虛函數

    多態性(在C + + 中用虛函數實現)是面向對象程序設計語言繼數據抽象和繼承以後的第三
個基本特徵。
    它提供了與具體實現相隔離的另外一類接口,即把「 w h a t 」從「h o w 」分離開來。多態性提
高了代碼的組織性和可讀性,同時也可以使得程序具備可生長性,這個生長性不只指在項目的最
初建立期能夠「生長」,並且但願項目具備新的性能時也能「生長」。
    封裝是經過特性和行爲的組合來建立新數據類型的,經過讓細節 p r i v a t e來使得接口與具體
實現相隔離。這類機構對於有過程程序設計背景的人來講是很是有意義的。而虛函數則根據類
型的不一樣來進行不一樣的隔離。上一章,咱們已經看到,繼承如何容許把對象做爲它本身的類型
或它的基類類型處理。這個能力很重要,由於它容許不少類型(從同一個基類派生的)被等價
地看待就象它們是一個類型,容許同一段代碼一樣地工做在全部這些不一樣類型上。虛函數反映
了一個類型與另外一個相似類型之間的區別,只要這兩個類型都是從同一個基類派生的。這種區
別是經過其在基類中調用的函數的表現不一樣來反映的。
    在這一章中,咱們將從最基本的內容開始學習虛函數,爲了簡單起見,本章所用的例子經
過簡化,只保留了程序的虛擬性質。
    • C + +程序員的進步
    C程序員彷佛能夠用三步進入C + +:
    第一步:簡單地把C + +做爲一個「更好的C 」,由於C + +在使用任何函數以前必須聲明它,
而且對於如何使用變量有更苛刻的要求。簡單地用C + +編譯器編譯C程序經常會發現錯誤。
    第二步:進入「面向對象」的C + + 。這意味着,很容易看到將數據結構和在它上面活動的
函數捆綁在一塊兒的代碼組織,看到構造函數和析構函數的價值,也許還會看到一些簡單的繼承,
這是有好處的。許多用過C的程序員很快就知道這是有用的,由於不管什麼時候,建立庫時,這些
都是要作的。然而在C + +中,由編譯器來幫咱們完成這些工做。
    在基於對象層上,咱們可能受騙,由於無須花費太多精力就能獲得不少好處。它也很容易
使咱們感到正在建立數據類型—製造類和對象,向這些對象發送消息,一切漂亮優美。
    可是,不要犯傻,若是咱們停留在這裏,咱們就失去了這個語言的最重要的部分。這個最
重要的部分才真正是向面向對象程序設計的飛躍。要作到這一點,只有靠第三步。
    第三步:使用虛函數。虛函數增強類型概念,而不是隻在結構內和牆後封裝代碼,因此毫
無疑問,對於新C + +程序員,它們是最困難的概念。然而,它們也是理解面向對象程序設計的
轉折點。若是不用虛函數,就等於還不懂得O O P 。
    由於虛函數是與類型概念緊密聯繫的,而類型是面向對象的程序設計的核心,因此在傳統
的過程語言中沒有相似於虛函數的東西。做爲一個過程程序員,沒有以往的參考能夠幫助他思
考虛函數,由於接觸的是這個語言的其餘特徵。過程語言中的特徵能夠在算法層上理解,而虛
函數只能用設計的觀點理解。

14.1  向上映射

    在上一章中,咱們已經看到對象如何做爲它本身的類型或它的基類的對象使用。另外,它

----------------------- Page 275-----------------------

                                               第14章  多態和虛函數         275
 下載

還能經過基類的地址被操做。取一個對象的地址(或指針或引用),並看做基類的地址,這被
稱爲向上映射,由於繼承樹是以基類爲頂點的。
    咱們看到會出現一個問題,這表如今下面的代碼中:

    函數t u n e ( ) (經過引用)接受一個i n s t r u m e n t ,但也不拒絕任何從i n s t r u m e n t派生的類。在
m a i n ( )中,能夠看到,無需映射,就能將w i n d對象傳給t u n e ( ) 。這是可接受的,在instrument        中
的接口必然存在於w i n d 中,由於w i n d是公共的從i n s t r u m e n t繼承而來的。w i n d到i n s t r u m e n t的向
上映射會使w i n d 的接口「變窄」,但它不能變得比i n s t r u m e n t的整個接口還小。

    這對於處理指針的狀況也是正確的,惟一的不一樣是用戶必須顯式地取對象的地址,傳給這
個函數。

14.2   問題

    W I N D 2 . C P P的問題能夠經過運行這個程序看到,輸出是i n s t r u m e n t : : p l a y。顯然,這不是所

----------------------- Page 276-----------------------

  276        C + +編程思想
                                                                      下載

但願的輸出,由於咱們知道這個對象其實是 w i n d而不僅是一個i n s t r u m e n t 。這個調用應當輸
出w i n d : : p l a y 。爲此,由i n s t r u m e n t派生的任何對象應當使它的p l a y版本被使用。

    然而,當對函數用C方法時,W I N D 2 . C P P的表現並不奇怪。爲了理解這個問題,須要知道
捆綁的概念。

函數調用捆綁

    把函數體與函數調用相聯繫稱爲捆綁(b i n d i n g )。當捆綁在程序運行以前(由編譯器和連
接器)完成時,稱爲早捆綁。咱們可能沒有聽到過這個術語,由於在過程語言中是不會有的:
C編譯只有一種函數調用,就是早捆綁。

    上面程序中的問題是早捆綁引發的,由於編譯器在只有 i n s t r u m e n t地址時它不知道正確的
調用函數。
    解決方法被稱爲晚捆綁,這意味着捆綁在運行時發生,基於對象的類型。晚捆綁又稱爲動
態捆綁或運行時捆綁。當一個語言實現晚捆綁時,必須有一種機制在運行時肯定對象的類型和
合適的調用函數。這就是,編譯器還不知道實際的對象類型,但它插入能找到和調用正確函數

體的代碼。晚捆綁機制因語言而異,但能夠想象,一些種類的類型信息必須裝在對象自身中。
稍後將會看到它是如何工做的。

14.3  虛函數

    對於特定的函數,爲了引發晚捆綁,C + +要求在基類中聲明這個函數時使用v i r t u a l關鍵字。
晚捆綁只對v i r t u a l起做用,並且只發生在咱們使用一個基類的地址時,而且這個基類中有
v i r t u a l函數,儘管它們也能夠在更早的基類中定義。

    爲了建立一個v i r t u a l成員函數,能夠簡單地在這個函數聲明的前面加上關鍵字 v i r t u a l 。對
於這個函數的定義不要重複,在任何派生類函數重定義中都不要重複它(雖然這樣作無害)。
若是一個函數在基類中被聲明爲v i r t u a l ,那麼在全部的派生類中它都是v i r t u a l 的。在派生類中
v i r t u a l函數的重定義一般稱爲越位。
    爲了從W I N D 2 . C P P 中獲得所但願的結果,只需簡單地在基類中的p l a y ( )以前增長v i r t u a l關

鍵字:

----------------------- Page 277-----------------------

                                              第14章  多態和虛函數         277
 下載

    這個文件除了增長了v i r t u a l關鍵字以外,一切與W I N D 2 . C P P相同,但結果明顯不同。現
在的輸出是w i n d : : p l a y 。

擴展性

    經過將play( ) 在基類中定義爲v i r t u a l ,不用改變t u n e ( )函數就能夠在系統中隨意增長新函數。
在一個設計好的 O O P程序中,大多數或全部的函數都沿用t u n e ( )模型,只與基類接口通訊。這

樣的程序是可擴展的,由於能夠經過從公共基類繼承新數據類型而增長新功能。操做基類接口
的函數徹底不須要改變就能夠適合於這些新類。
    如今,i n s t r u m e n t例子有更多的虛函數和一些新類,它們都能與老的版本一塊兒正確工做,
不用改變t u n e ( )函數:

----------------------- Page 278-----------------------

     278   C + +編程思想
                                                                        下載

----------------------- Page 279-----------------------

                                               第14章  多態和虛函數         279
 下載

    能夠看到,這個例子已在w i n d之下增長了另外的繼承層,但v i r t u a l機制正確工做,無論這
裏有多少層。a d j u s t ( )函數不對於b r a s s和w o o d w i n d重定義。當出現這種狀況時,自動使用先前
的定義—編譯器保證虛函數老是有定義的,因此,決不會最終出現調用不與函數體捆綁的情
況。(這種狀況將意味着災難。)

    數組A [ ]存放指向基類instrument  的指針,因此在數組初始化過程當中發生向上映射。這個數
組和函數f ( )將在稍後的討論中用到。
    在對t u n e ( ) 的調用中,向上映射在對象的每個不一樣的類型上完成。指望的結果老是能得
到。這能夠被描述爲「發送消息給一對象和讓這個對象考慮用它來作什麼」。v i r t u a l 函數是在
試圖分析項目時使用的透鏡:基類應當出如今哪裏?應當如何擴展這個程序?然而,在程序最

初建立時,即使咱們沒有發現合適的基類接口和虛函數,在稍後甚至更晚,當咱們決定擴展或
維護這個程序時,咱們也經常會發現它們。這不是分析或設計錯誤,它只意味着一開始咱們還
沒有全部的信息。因爲C + +嚴格的模塊化,這並非大問題。由於當咱們對系統的一部分做了
修改時,每每不會象C那樣波及系統的其餘部分。

14.4  C++如何實現晚捆綁

    晚捆綁如何發生?全部的工做都由編譯器在幕後完成。當咱們告訴它去晚捆綁時(用建立

虛函數告訴它),編譯器安裝必要的晚捆綁機制。由於程序員經常從理解 C + +虛函數機制中受
益,因此這一節將詳細闡述編譯器實現這一機制的方法。
    關鍵字v i r t u a l告訴編譯器它不該當完成早捆綁,相反,它應當自動安裝實現晚捆綁所必須
的全部機制。這意味着,若是咱們對b r a s s對象經過基類i n s t r u m e n t地址調用p l a y ( ) ,咱們將獲得

----------------------- Page 280-----------------------

  280        C + +編程思想
                                                                      下載

恰當的函數。
    爲了完成這件事,編譯器對每一個包含虛函數的類建立一個表(稱爲                               V TA B L E )。在

V TA B L E 中,編譯器放置特定類的虛函數地址。在每一個帶有虛函數的類中,編譯器祕密地置一
指針,稱爲v p o i n t e r (縮寫爲V P T R ),指向這個對象的V TA B L E 。經過基類指針作虛函數調用
時(也就是作多態調用時),編譯器靜態地插入取得這個V P T R ,並在V TA B L E表中查找函數地
址的代碼,這樣就能調用正確的函數使晚捆綁發生。
    爲每一個類設置V TA B L E 、初始化V P T R 、爲虛函數調用插入代碼,全部這些都是自動發生

的,因此咱們沒必要擔憂這些。利用虛函數,這個對象的合適的函數就能被調用,哪怕在編譯器
還不知道這個對象的特定類型的狀況下。
    下面幾節將對此作更詳細地闡述。

14.4.1 存放類型信息

    能夠看到,在任何類中,不存在顯式的類型信息。而先前的例子和簡單的邏輯告訴咱們,
必須有一些類型信息放在對象中,不然,類型不能在運行時創建。實際上,類型信息被隱藏了。

爲了看到它,這裏有一個例子,能夠測試使用虛函數的類的長度,並與沒有虛函數的類比較。

----------------------- Page 281-----------------------

                                              第14章  多態和虛函數         281
 下載

    不帶虛函數,對象的長度剛好就是所指望的:單個                      i n t 的長度。而帶有單個虛函數的
o n e _ v i r t u a l,對象的長度是n o _ v i r t u a l 的長度加上一個v o i d指針的長度。它反映出,若是有一個
或多個虛函數,編譯器在這個結構中插入一個指針( V P T R )。在one_virtual  和 t w o _ v i r t u a l s之

間沒有區別。這是由於V P T R指向一個存放地址的表,只須要一個指針,由於全部虛函數地址
都包含在這個表中。
    這個例子至少要求一個數據成員。若是沒有數據成員, C + +編譯器會強制這個對象是非零
長度,由於每一個對象必須有一個互相區別的地址。若是咱們想象在一個零長度對象的數組中索
引,咱們就能理解這一點。一個「啞」成員被插入到對象中,不然這個對象就有零長度。當

v i r t u a l關鍵字插入類型信息時,這個「啞」成員的位置就被佔用。在上面例子中,用註釋符號
將全部類的int a去掉,咱們就會看到這種狀況。

14.4.2 對虛函數做圖                                         對象

    爲了準確地理解使用虛函數時編譯器                 指針數組

作了些什麼,使屏風以後進行的活動看得

見是有幫助的。這裏畫的是在                1 4 . 3節
W I N D 4 . C P P 中的指針數組A [ ] 。
    這個i n s t r u m e n t指針數組沒有特殊類型
信息,它的每個元素指向一個類型爲
instrument 的對象。w i n d 、p e r c u s s i o n 、

s t r i n g和b r a s s都適合這個範圍,由於它們都
是從i n s t r u m e n t派生來的(而且和i n s t r u m e n t     圖 14-1

有相同的接口和響應相同的消息),所以,它們的地址也天然能放進這個數組裏。然而,編譯
器並不知道它們比i n s t r u m e n t對象更多的東西,因此,留給它們本身處理,而一般調用全部函
數的基類版本。但在這裏,全部這些函數都被用 v i r t u a l聲明,因此出現了不一樣的狀況。每當創

建一個包含有虛函數的類或從包含有虛函數的類派生一個類時,編譯器就爲這個類建立一個
V TA B L E,如這個圖的右面所示。在這個表中,編譯器放置了在這個類中或在它的基類中全部
已聲明爲v i r t u a l 的函數的地址。若是在這個派生類中沒有對在基類中聲明爲 v i r t u a l 的函數進行
從新定義,編譯器就使用基類的這個虛函數地址。(在b r a s s的V TA B L E 中,a d j u s t的入口就是這
種狀況。)而後編譯器在這個類中放置V P T R              (可在S I Z E S . C P P中發現)。當使用簡單繼承時,

對於每一個對象只有一個V P T R 。V P T R必須被初始化爲指向相應的V TA B L E 。(這在構造函數中
發生,在稍後會看得更清楚。)
    一旦V P T R被初始化爲指向相應的V TA B L E,對象就「知道」它本身是什麼類型。但只有
當虛函數被調用時這種自我知識才有用。
    經過基類地址調用一個虛函數時(這時編譯器沒有能完成早捆綁的足夠的信息),要特殊

處理。它不是實現典型的函數調用,對特定地址的簡單的彙編語言 C A L L,而是編譯器爲完成
這個函數調用產生不一樣的代碼。下面看到的是經過  i n s t r u m e n t指針對於b r a s s調用a d j u s t ( ) 。

----------------------- Page 282-----------------------

  282        C + +編程思想
                                                                      下載

i n s t r u m e n t引用產生以下結果:
    編譯器從這個i n s t r u m e n t指針開始,這個指針指向這個對象的起始地址。全部的 i n s t r u m e n t

對象或由 i n s t r u m e n t 派生的對象都有它們的
V P T R ,它在對象的相同的位置(經常在對象的
                                           指針
開頭),因此編譯器可以取出這個對象的V P T R 。
V P T R 指向 V TA B L E 的開始地址。全部的
V TA B L E有相同的順序,無論何種類型的對象。                            圖 14-2

p l a y ( )是第一個,w h a t ( )是第二個,a d j u s t ( )是第
三個。因此編譯器知道a d j u s t ( )函數必在V P T R + 2處。這樣,不是「以i n s t r u m e n t : : a d j u s t地址調
用這個函數」(這是早捆綁,是錯誤活動),而是產生代碼,「在V P T R + 2處調用這個函數」。因
爲V P T R 的效果和實際函數地址的肯定發生在運行時,因此這樣就獲得了所但願的晚捆綁。向
這個對象發送消息,這個對象能判定它應當作什麼。

14.4.3 撩開面紗

    若是能看到由虛函數調用而產生的彙編語言代碼,這將是頗有幫助的,這樣能夠看到後捆
綁其實是如何發生的。下面是在函數f                (i n s t r u m e n t & i )中調用

    i . a d j u s t ( 1 ) ;

某個編譯器所產生的輸出:

    C + +函數調用的參數與C函數調用同樣,是從右向左進棧的(這個順序是爲了支持C的變量
參數表),因此參數1首先壓棧。在這個函數的這個地方,寄存器s i                       (intel x86處理器的一部分)
存放i的首地址。由於它是被選中的對象的首地址,它也被壓進棧。記住,這個首地址對應於

t h i s 的值,正由於調用每一個成員函數時t h i s都必須做爲參數壓進棧,因此成員函數知道它工做在
哪一個特殊對象上。這樣,咱們總能看到,在成員函數調用以前壓棧的次數等於參數個數加一
(除了s t a t i c成員函數,它沒有t h i s )。
    如今,必須實現實際的虛函數調用。首先,必須產生 V P T R ,使得能找到V TA B L E 。對於
這個編譯器,V P T R在對象的開頭,因此t h i s的內容對應於V P T R 。下面這一行

    mov bx ,word ptr[si]

取出s i  (即t h i s )所指的字,它就是V P T R 。將這個V P T R放入寄存器b x 中。
    放在b x 中的這個V P T R指向這個V TA B L E 的首地址,但被調用的函數在V TA B L E 中不是第0

個位置,而是第二個位置(由於它是這個表中的第三個函數)。對於這種內存模式,每一個函數
指針是兩個字節長,因此編譯器對V P T R加四,計算相應的函數地址所在的地方,注意,這是
編譯時創建的常值。因此咱們只要保證在第二個位置上的指針剛好指向 a d j u s t ( ) 。幸虧編譯器仔
細處理,並保證在V TA B L E中的全部函數指針都以相同的次序出現。
    一旦在V TA B L E 中相應函數指針的地址被計算出來,就調用這個函數。因此取出這個地址

並立刻在這個句子中調用:

    call word ptr [bx+4]

----------------------- Page 283-----------------------

                                               第14章  多態和虛函數         283
 下載

最後,棧指針移回去,以清除在調用以前壓入棧的參數。在 C和C + +彙編代碼中,咱們將經常
看到調用者清除這些參數,但這依處理器和編譯器的實現而有所變化。

14.4.4 安裝vpointer

    由於V P T R決定了對象的虛函數的行爲,因此咱們會看到V P T R老是指向相應的V TA B L E是
多麼重要。在V P T R適當初始化以前,咱們絕對不能對虛函數調用。固然,能保證初始化的地
點是在構造函數中,可是,在W I N D例子中沒有一個是有構造函數的。
    這樣,缺省構造函數的建立是很關鍵的。在W I N D例子中,編譯器建立了一個缺省構造函
數,它只作初始化 V P T R 的工做。在能用任何 i n s t r u m e n t對象作任何事情以前,對於任何

i n s t r u m e n t對象自動調用這個構造函數。因此,調用虛函數是安全的。
    在構造函數中,自動初始化V P T R的含義在下一節討論。

14.4.5 對象是不一樣的

    認識到向上映射僅處理地址,這是重要的。若是編譯器有一個它知道確切類型的對象,那
麼(在C + +中)對任何函數的調用將再也不用晚捆綁,或至少編譯器沒必要須用晚捆綁。由於編譯
器知道對象的類型,爲了提升效率,當調用這些對象的虛函數時,不少編譯器使用早捆綁。下

面是一個例子:

    在b 1 - > f ( )和b 2 . f ( ) 中,使用地址,就意味着信息不徹底:b 1和b 2可能表示b a s e 的地址也可能
表示其派生對象的地址,因此必須用虛函數。而當調用 b 3 . f ( ) 時不存在含糊,編譯器知道確切
的類型和知道它是一個對象,因此它不多是由b a s e派生的對象,而確切的只是一個b a s e 。這

----------------------- Page 284-----------------------

  284        C + +編程思想
                                                                       下載

樣,能夠用早捆綁。可是,若是不但願編譯器的工做如此複雜,仍能夠用晚捆綁,而且有相同
的結果。

14.5  爲何須要虛函數

    在這個問題上,咱們可能會問:「若是這個技術如此重要,而且能使得任什麼時候候都能調用
‘正確’的函數。那麼爲何它是可選的呢?爲何我還須要知道它呢?」
    問得好。回答關係到C + +的基本哲學:「由於它不是至關高效率的」。從前面的彙編語言輸
出能夠看出,它並非對於絕對地址的一個簡單的 C A L L,而是爲設置虛函數調用須要多於兩
條複雜的彙編指令。這既須要代碼空間,又須要執行時間。一些面向對象的語言已經接受了這

種概念,即晚捆綁對於面向對象程序設計是性質所決定的,因此應當老是出現,它應當是不可
選的,並且用戶不該當必須知道它。這是由創造語言時的設計決定,而這種特殊的方法對於許
多語言是合適的 [ 1 ]   。C + +來自C傳統,效率是重要的。創造C徹底是爲了代替彙編語言以實現

操做系統(從而改寫操做系統—U n i x—使得比它的先驅更輕便)。但願有C + +的主要理由
之一是讓C程序員效率更高 [ 2 ] 。C程序員遇到C + +時提出的第一個問題是「我將獲得什麼樣的規

模和速度效果?」若是回答是「除了函數調用時須要有一點額外的開銷外,一切皆好」,那麼
許多人就會仍使用C,而不會改變到C + + 。另外,內聯函數是不可能的,由於虛函數必須有地
址放在V TA B L E 中。因此虛函數是可選的,並且該語言的缺省是非虛擬的,這是最快的配置。
S t r o u s t r u p聲明他的方針是「若是咱們不用它,咱們就不會爲它花費」。
    所以,v i r t u a l關鍵字能夠改變程序的效率。然而,設計咱們的類時,咱們不該當爲效率問

題而擔憂。若是咱們想使用多態,就在每處使用虛函數。當咱們試圖加速咱們的代碼時,咱們
只需尋找能讓它非虛的函數(在其餘方面一般有更大的好處)。
    有些證據代表,進入C + +的規模和速度改進是在C 的規模和速度的1 0 %以內,而且經常更
接近。可以獲得更小的規模和更高速度的緣由是由於 C + +能夠有比用C更快的方法設計程序,
並且設計的程序更小。
                                                                     公共接口

14.6  抽象基類和純虛函數

    在全部的i n s t r u m e n t 的例子中,基類i n s t r u m e n t
中的函數老是「假」函數。若是調用這些函數,就
會指出已經作錯了什麼事。這是由於,i n s t r u m e n t的
目的是對全部從它派生來的類建立公共接口,如在
下面的圖中看到的:

    虛線表示類(一個類只是一個描述,而不是一                                         不一樣的實現
個物理實體—虛線表明了它的非物理的「性質」)。
從派生類到基類的箭頭表示繼承關係。
    創建公共接口的惟一的理由是使得它能對於每
個不一樣的子類有不一樣的表示。它創建一個基本的格
式,由此能夠知道什麼是對於全部派生類公共的。                                   圖 14-3

注意,另一種表達方法是稱i n s t r u m e n t爲抽象基類(或簡稱爲抽象類),當但願經過公共接口

  [1] 例如,Smalltalk 用這種方法得到了很大的成功。

  [2] 發明C + +的貝爾實驗室就是利用高效率爲公司節約了大筆的費用。

----------------------- Page 285-----------------------

                                              第14章  多態和虛函數         285
 下載

操做一組類時就建立抽象類。
    注意,只需在基類中聲明函數爲v i r t u a l 。與這個基類聲明相匹配的全部派生類函數都將按

照虛機制調用。固然咱們也能夠在派生類聲明中使用v i r t u a l關鍵字 (有些人爲了清楚而這樣作),
但這是多餘的。
    若是咱們有一個真實的抽象類(就像i n s t r u m e n t ),這個類的對象幾乎老是沒有意義的。也
就是說,i n s t r u m e n t的含義只表示接口,不表示特例實現。因此建立一個i n s t r u m e n t對象沒有意
義。咱們也許想防止用戶這樣作。這能經過讓 i n s t r u m e n t 的全部虛函數打印出錯信息而完成,

但這種方法到運行時才能得到出錯信息,而且要求用戶可靠而詳盡地測試。因此最好是在編譯
時就能發現這個問題。
    C + +對此提供了一種機制,稱爲純虛函數。下面是它的聲明語法:

    virtual void x() = 0;

    這樣作,等於告訴編譯器在V TA B L E 中爲函數保留一個間隔,但在這個特定間隔中不放地
址。只要有一個函數在類中被聲明爲純虛函數,則V TA B L E就是不徹底的。包含有純虛函數的
類稱爲純抽象基類。
    若是一個類的V TA B L E是不徹底的,當某人試圖建立這個類的對象時,編譯器作什麼呢?

因爲它不能安全地建立一個純抽象類的對象,因此若是咱們試圖製造一個純抽象類的對象,
編譯器就發出一個出錯信息。這樣,編譯器就保證了抽象類的純潔性,咱們就不用擔憂誤用
它了。
    這是修改後的WIND4.CPP 14.3節,它使用了純虛函數:

----------------------- Page 286-----------------------

     286   C + +編程思想
                                                                        下載

----------------------- Page 287-----------------------

                                               第14章  多態和虛函數         287
 下載

    純虛函數是很是有用的,由於它們使得類有明顯的抽象性,並告訴用戶和編譯器但願如何
使用。

    注意,純虛函數防止對純抽象類的函數以傳值方式調用。這樣,它也是防止對象意外使用
值向上映射的一種方法。這樣就能保證在向上映射期間老是使用指針或引用。
    純虛函數防止產生V TA B L E,但這並不意味着咱們不但願對其餘函數產生函數體。咱們常
常但願調用一個函數的基類版本,即使它是虛擬的。把公共代碼放在儘量靠近咱們的類層次
根的地方,這是很好的想法。這不只節省了代碼空間,並且能容許使改變的傳播變得容易。

純虛定義

    在基類中,對純虛函數提供定義是可能的。咱們仍然告訴編譯器不要容許純抽象基類的對
象,並且純虛函數在派生類中必須定義,以便於建立對象。然而,咱們可能但願一塊代碼對於
一些或全部派生類定義能共同使用,不但願在每一個函數中重複這段代碼,以下所示:

----------------------- Page 288-----------------------

  288        C + +編程思想
                                                                      下載

    在base VTA B L E 中的間隔仍然空着,但在這個派生類中恰好有一個函數,能夠經過名字調
用它。

    這個特徵的另外的好處是,它容許使用一個純虛函數而不打亂已存在的代碼。(這是一個
處理沒有重定義虛函數類的方法。)

14.7  繼承和VTABLE

    能夠想象,當實現繼承和定義一些虛函數時,會發生什麼事情?編譯器對新類建立一個新
V TA B L E表,而且插入新函數的地址,對於沒有重定義的虛函數使用基類函數的地址。不管如何,
在V TA B L E中總有全體函數的地址,因此絕對不會對不在其中的地址調用。(不然損失慘重。)

    但當在派生類中增長新的虛函數時會發生什麼呢?這裏有一個例子:

----------------------- Page 289-----------------------

                                               第14章  多態和虛函數        289
 下載

    類b a s e包含單個虛函數v a l u e ( ) ,而類d e r i v e d增長了第二個稱爲s h i f t ( )的虛函數,並重定義
了v a l u e 的含義。下圖有助於顯示發生的事情,

其中有編譯器爲 b a s e 和d e r i v e d 建立的兩個
V TA B L E。
    注意,編譯器映射 derived VTA B L E 中的
v a l u e地址位置等於在base VTA B L E 中的位置。                   圖 14-4
相似的,若是一個類從d e r i v e d繼承而來,它的

s h i f t版本在它的V TA B L E 中的位置應當等於在d e r i v e d中的位置。這是由於(正如經過彙編語言
例子看到的)編譯器產生的代碼只是簡單地在V TA B L E 中用偏移選擇虛函數。不論對象屬於哪
個特殊的類,它的V TA B L E是以一樣的方法設置的,因此對虛函數的調用將老是用一樣的方法。
    這樣,編譯器只對指向基類對象的指針工做。而這個基類只有v a l u e函數,因此它就是編譯
器容許調用的惟一的函數。那麼,若是隻有指向基類對象的指針,那麼編譯器怎麼可能知道自

己正在對d e r i v e d對象工做呢?這個指針可能指向其餘一些沒有s h i f t函數的類。在V TA B L E 中,可
能有,也可能沒有一些其餘函數的地址,但不管何種狀況,對這個V TA B L E地址作虛函數調用都
不是咱們想要的。因此編譯器防止對只在派生類中存在的函數作虛函數調用,這是幸運的,合
乎邏輯的。
    有一些不多見的狀況:可能咱們知道指針實際上指向哪種特殊子類的對象。這時若是想

調用只存在於這個子類中的函數,則必須映射這個指針。下面的語句能夠糾正由前面程序產生
的錯誤:

    ( ( d e r i v e d * ) B [ 1 ] ) - > s h i f t ( 3 )

在這裏咱們碰巧知道B [ 1 ]指向d e r i v e d對象,但這種狀況不多見。若是咱們的程序肯定咱們必須
知道全部對象的準確的類型,那麼咱們應當從新考慮它,由於咱們可能在進行不正確的虛函數
調用。然而對於有些狀況若是知道保存在通常包容器中的全部對象的準確類型,會使咱們的設
計工做在最佳狀態(或沒有選擇)。這就是運行時類型辨認問題(簡稱RT T I)。
    運行時類型辨認是有關映射基類指針向下到派生類指針的問題。(「向上」和「向下」是相

對典型類圖而言的,典型類圖以基類爲頂點。)向上映射是自動發生的,不需強制,由於它是
絕對安全的。向下映射是不安全的,由於這裏沒有關於實際類型的編譯信息,因此必須準確地
知道這個類其實是什麼類型。若是把它映射成錯誤的類型,就會出現麻煩。
    第1 8章將描述C + +提供運行時類型信息的方法。
    • 對象切片

    當多態地處理對象時,傳地址與傳值有明顯的不一樣。全部在這裏已經看到的例子和將會看
到的例子都是傳地址的,而不是傳值的。這是由於地址都有相同的長度 [ 1 ] ,傳派生類型(它通

常稍大一些)對象的地址和傳基類(它一般小一點)對象的地址是相同的。如前面解釋的,使
用多態的目的是讓對基類對象操做的代碼也能操做派生類對象。
    若是使用對象而不是使用地址或引用進行向上映射,發生的事情會使咱們吃驚:這個對象

被「切片」,直到所剩下來的是適合於目的的子對象。在下面例子中能夠看到經過檢查這個對
象的長度切片剩下來的部分。

  [1] 實際上,並非全部機器上的指針都是一樣大小的。但就本書討論的範圍而言,它們可被認爲是一樣大小的。

----------------------- Page 290-----------------------

  290        C + +編程思想
                                                                      下載

    函數c a l l ( )經過傳值傳遞一個類型爲b a s e 的對象。而後對於這個b a s e對象調用虛函數s u m ( )。
咱們可能但願第一次調用產生 1 0,第二次調用
產生5 7。實際上,兩次都產生1 0。
    在這個程序中,有兩件事情發生了。第一,

c a l l ( )接受的只是一個b a s e對象,因此全部在這
個函數體內的代碼都將只操做與b a s e相關的數。
對c a l l ( ) 的任何調用都將引發一個與b a s e 大小相
                                              切片以前              切片以後
同的對象壓棧並在調用後清除。這意味着,如
                                                       圖 14-5
果一個由b a s e派生來類對象被傳給c a l l,編譯器

接受它,但只拷貝這個對象對應於b a s e 的部分,切除這個對象的派生部分,如圖:
    如今,咱們可能對這個虛函數調用感到奇怪:這裏,這個虛函數既使用了b a s e  (它仍存在),
又使用了d e r i v e d的部分(d e r i v e d再也不存在了,由於它被切片)。
    其實咱們已經從災難中被解救出來,這個對象正安全地以值傳遞。由於這時編譯器認爲它
知道這個對象的確切的類型(這個對象的額外特徵有用的任何信息都已經失去)。另外,用值

傳遞時,它對b a s e對象使用拷貝構造函數,該構造函數初始化 V P T R指向base VTA B L E ,而且
只拷貝這個對象的b a s e部分。這裏沒有顯式的拷貝構造函數,因此編譯器自動地爲咱們合成一
個。因爲上述諸緣由,這個對象在切片期間變成了一個b a s e對象。

    對象切片其實是去掉了對象的一部分,而不是象使用指針或引用那樣簡單地改變地址的

----------------------- Page 291-----------------------

                                               第14章  多態和虛函數        291
 下載

內容。所以,對象向上映射不常作,事實上,一般要提防或防止這種操做。咱們能夠經過在基

類中放置純虛函數來防止對象切片。這時若是進行對象切片就將引發編譯時的出錯信息。

14.8  虛函數和構造函數

    當建立一個包含有虛函數的對象時,必須初始化它的 V P T R 以指向相應的V TA B L E 。這必

須在有關虛函數的任何調用以前完成。正如咱們可能猜到的,由於構造函數有使對象成爲存在
物的工做,因此它也有設置V P T R 的工做。編譯器在構造函數的開頭部分祕密地插入能初始化

V P T R 的代碼。事實上,即便咱們沒有對一個類建立構造函數,編譯器也會爲咱們建立一個帶

有相應V P T R初始化代碼的構造函數(若是有虛函數)。這有幾個含意。
    首先這涉及效率。內聯(i n l i n e )函數的理由是對小函數減小調用代價。若是C + +不提供內

聯(i n l i n e )函數,預處理器就可能被用以建立這些「宏」。然而,預處理器沒有通道或類的概
念,所以不能被用以建立成員函數宏。另外,有了由編譯器插入隱藏代碼的構造函數,預處理

宏根本不能工做。

    當尋找效率漏洞時,咱們必須明白,編譯器正在插入隱藏代碼到咱們的構造函數中。這些
隱藏代碼不只必須初始化V P T R,並且還必須檢查t h i s 的值(萬一operator new返回零)和調用

基類構造函數。放在一塊兒,這些代碼能影響咱們認爲是一個小內聯函數的調用。特別是,構造
函數的規模會抵消減小函數調用節省的費用。若是作大量的內聯構造函數調用,咱們的代碼長

度就會增加,而在速度上沒有任何好處。

    固然,咱們也許並不會當即把這些小構造函數都變成非內聯,由於它們更容易作爲內聯的
來寫。可是,當咱們正在調整咱們的代碼時,記住,務必去掉這些構造函數的內聯性。

14.8.1 構造函數調用次序

    構造函數和虛函數的第二個有趣的方面涉及構造函數的調用順序和在構造函數中虛函數調

用的方法。

    全部基類構造函數老是在繼承類構造函數中被調用。這是有意義的,由於構造函數有一項
專門的工做:確保對象被正確的創建。派生類只訪問它本身的成員,而不訪問基類的成員,只

有基類構造函數能恰當地初始化它本身的成員。所以,確保全部的構造函數被調用是很關鍵的,
不然整個對象不會適當地被構造。這就是爲何編譯器強制構造函數對派生類的每一個部分調用。

若是不在構造函數初始化表達式表中顯式地調用基類構造函數,它就調用缺省構造函數。若是

沒有缺省構造函數,編譯器將報告出錯。(在這個例子中,class x沒有構造函數,因此編譯器
能自動建立一個缺省構造函數。)

    構造函數調用的順序是重要的。當繼承時,咱們必須徹底知道基類和能訪問基類的任何
public  和p r o t e c t e d成員。這也就是說,當咱們在派生類中時,必須能確定基類的全部成員都是

有效的。在一般的成員函數中,構造已經發生,因此這個對象的全部部分的成員都已經創建。

然而,在構造函數內,必須想辦法保證全部咱們的成員都已經創建。保證它的惟一方法是讓基
類構造函數首先被調用。這樣,當咱們在派生類構造函數中時,在基類中咱們能訪問的全部成

員都已經被初始化了。在構造函數中,「必須知道全部成員對象是有效的」也是下面作法的理
由:只要可能,咱們應當在這個構造函數初始化表達式表中初始化全部的成員對象(放在合成

的類中的對象)。只要咱們聽從這個作法,咱們就能保證全部基類成員和當前對象的成員對象

已經被初始化。

----------------------- Page 292-----------------------

  292        C + +編程思想
                                                                      下載

14.8.2 虛函數在構造函數中的行爲

    構造函數調用層次會致使一個有趣的兩難選擇。試想;若是咱們正在構造函數中而且調用

虛函數,那麼會發生什麼現象呢?對於普通的成員函數,虛函數的調用是在運行時決定的,這
是由於編譯時並不能知道這個對象是屬於這個成員函數所在的那個類,仍是屬於由它派生出來
的類。因而,咱們也許會認爲在構造函數中也會發生一樣的事情。
    然而,狀況並不是如此。對於在構造函數中調用一個虛函數的狀況,被調用的只是這個函數
的本地版本。也就是說,虛機制在構造函數中不工做。

    這個行爲有兩個理由。在概念上,構造函數的工做是把對象變成存在物。在任何構造函數
中,對象可能只是部分被造成—咱們只能知道基類已被初始化了,但不知道哪一個類是從這個
基類繼承來的。然而,虛函數是「向前」和「向外」 進行調用。它能調用在派生類中的函數。
若是咱們在構造函數中也這樣作,那麼咱們所調用的函數可能操做尚未被初始化的成員,這
將致使災難的發生。

    第二個理由是機械的。當一個構造函數被調用時,它作的首要的事情之一是初始化它的
V P T R 。所以,它只能知道它是「當前」類的,而徹底忽視這個對象後面是否還有繼承者。當
編譯器爲這個構造函數產生代碼時,它是爲這個類的構造函數產生代碼 - -既不是爲基類,也不
是爲它的派生類(由於類不知道誰繼承它)。因此它使用的V P T R必須是對於這個類的V TA B L E 。
並且,只要它是最後的構造函數調用,那麼在這個對象的生命期內, V P T R將保持被初始化爲

指向這個V TA B L E 。但若是接着還有一個更晚派生的構造函數被調用,這個構造函數又將設置
V P T R指向它的V TA B L E,等等,直到最後的構造函數結束。V P T R 的狀態是由被最後調用的構
造函數肯定的。這就是爲何構造函數調用是從基類到更加派生類順序的另外一個理由。
    可是,當這一系列構造函數調用正發生時,每一個構造函數都已經設置 V P T R指向它本身的
V TA B L E 。若是函數調用使用虛機制,它將只產生經過它本身的V TA B L E 的調用,而不是最後

的V TA B L E (全部構造函數被調用後纔會有最後的V TA B L E )。另外,許多編譯器認識到,如
果在構造函數中進行虛函數調用,應該使用早捆綁,由於它們知道晚捆綁將只對本地函數產生
調用。不管哪一種狀況,在構造函數中調用虛函數都沒有結果。

14.9  析構函數和虛擬析構函數

    構造函數不能是虛的(在附錄B中的技術只相似於虛構造函數)。但析構函數可以且經常必
須是虛的。

    構造函數有其特殊的工做。它首先調用基本構造函數,而後調用在繼承順序中的更晚派生
的構造函數,如此一塊一塊地把對象拼起來。相似的,析構函數也有一個特殊的工做—它必
須拆卸可能屬於某類層次的對象。爲了作這些工做,它必須按照構造函數調用相反的順序,調
用全部的析構函數。這就是,析構函數自最晚派生的類開始,並向上到基類。這是安全且合理
的:當前的析構函數能知道基類成員還是有效的,由於它知道它是從哪個派生而來的,但不

知道從它派生出哪些。
    應當記住,構造函數和析構函數是必須遵照調用層次惟一的地方。在全部其餘函數中,只
是某個函數被調用,而不管它是虛的仍是非虛的。同一個函數的基類版本在一般的函數中被調
用(不管虛否)的惟一的方法是直接地調用這個函數。
    一般,析構函數的活動是很正常的。可是,若是咱們想經過指向某個對象的基類的指針操

縱這個對象(這就是,經過它的通常接口操縱這個對象),會發生什麼現象呢?這在面向對象

----------------------- Page 293-----------------------

                                              第14章  多態和虛函數         293
 下載

的程序設計中確實很重要。當咱們想d e l e t e在棧中已經用n e w建立的類的對象的指針時,就會出
現這個問題。若是這個指針是指向基類的,編譯器只能知道在 d e l e t e期間調用這個析構函數的

基類版本。咱們已經知道,虛函數被建立偏偏是爲了解決一樣的問題。幸虧,析構函數能夠是
虛函數,因而一切問題就迎刃而解了。
    雖然析構函數象構造函數同樣,是「例外」函數,但析構函數能夠是虛的,這是由於這個
對象已經知道它是什麼類型(而在構造期間則否則)。一旦對象已被構造,它的V P T R就已被初
始化了,因此虛函數調用能發生。

    若是咱們建立一個純虛析構函數,咱們就必須提供函數體,由於(不像普通函數)在類層
次中全部析構函數都老是被調用。這樣,這個純虛析構函數的函數體以調用結束。下面是例子:

    儘管在基類中的析構函數的純虛性有強制繼承者重定義這個析構函數的做用,但這個基類
體仍做爲析構函數的一部分被調用。
    做爲準則,任什麼時候候在類中有虛函數,咱們就應當直接增長虛析構函數(即使它什麼事也
不作)。這樣,能保證之後不發生意外。

在析構函數中的虛機制

    在析構期間,有一些咱們可能不但願立刻發生的狀況。若是咱們正在一個普通的成員函數
中,而且調用一個虛函數,則這個函數被使用晚捆綁機制調用。而對於析構函數,這樣不行,

不管是虛的仍是非虛的。在析構函數中,只有成員函數的本地版本被調用,虛機制被忽略。
    爲何是這樣呢?假設虛機制在析構函數中使用,那麼調用下面這樣的虛函數是可能的:
這個函數是在繼承層次中比當前的析構函數「更靠外」(更晚派生的)。可是,有一點咱們要注
意,析構函數從「外層」被調用(從最晚派生析構函數向基本析構函數)。因此,實際上被調

----------------------- Page 294-----------------------

  294        C + +編程思想
                                                                      下載

用的函數就可能操做在已被刪除的對象上。所以,編譯器決定在編譯時只調用這個函數的「本
地」版本。注意,對於構造函數也是如此(這在前面已講到)。但在構造函數的狀況下,這樣

作是由於信息還不可用,在析構函數中,信息(也就是V P T R )雖存在,但不可靠。

14.10  小結

    多態性在C + +中用虛函數實現,它有不一樣的形式。在面向對象的程序設計中,咱們有相同
的表面(在基類中的公共接口)和使用這個表面的不一樣的形式:虛函數的不一樣版本。
    在這一章中,咱們已經看到,理解甚至建立一個多態的例子,不用數據抽象和繼承是不可
能的。多態是不能隔離看待的特性(例如像c o n s t和s w i t c h語句),它必須同抽象與繼承一塊兒工做,

它是類關係的一個重要方面。人們經常被C + + 的其餘非面向對象的特性所混淆,例如重載和缺
省參數,它們有時被做爲面向對象的特性描述。不要犯傻,若是它不是晚捆綁它就不是多態。
    爲了在咱們的程序中有效的使用多態等面嚮對象的技術,咱們不能只知道讓咱們的程序包含單
個類的成員和消息,並且還應當知道類的共性和它們之間的關係。雖然這須要很大的努力,但這是
值得的,由於咱們將更快地開發程序和更好地組織代碼,獲得可擴充的程序和更容易維護的代碼。

    多態完善了這個語言的面向對象特性,但在C + +中,有兩個更重要的特性:模板(第1 5章)
和異常處理(第1 7章)。這些特性使咱們的程序設計能力有很大的提升,就像面向對象的其餘
特性:抽象數據類型、繼承和多態同樣。

14.11  練習

    1.  建立一個很是簡單的「 s h a p e 」層次:基類稱爲 s h a p e,派生類稱爲c i r c l e 、s q u a r e和
t r i a n g l e 。在基類中定義一個虛函數d r a w ( ),再在這些派生類中重定義它。建立指向咱們在堆中

建立的s h a p e對象的指針數組(這樣就造成了指針向上映射)。而且經過基類指針調用d r a w ( ) ,
檢驗這個虛函數的行爲。若是咱們的調試器支持,就用單步執行這個例子。
    2.  修改練習1,使得d r a w ( )是純虛函數。嘗試建立一個類型爲s h a p e的對象。嘗試在構造函
數內調用這個純虛函數,結果如何。給出d r a w ( )的一個定義。
    3.  寫出一個小程序以顯示在普通成員函數中調用虛函數和在構造函數中調用虛函數的不

同。這個程序應當證實兩種調用產生不一樣的結果。
    4.  在E A R LY. C P P中,咱們如何能知道編譯器是用早捆綁仍是晚捆綁進行調用?根據咱們
本身的編譯器來肯定。
    5. (中級)建立一個不帶成員和構造函數而只有一個虛函數的基類class X ,建立一個從X
繼承的類class Y ,它沒有顯式的構造函數。產生彙編代碼並檢驗它,以肯定X 的構造函數是否

被建立和調用,若是是的,這些代碼作什麼?解釋咱們的發現。 X沒有缺省構造函數,可是爲
什麼編譯器不報告出錯?
    6. (中級)修改練習5,讓每一個構造函數調用一個虛函數。產生彙編代碼。肯定在每一個構
造函數內V P T R在何處被賦值。在構造函數內編譯器使用虛函數機制嗎?肯定爲何這些函數
的本地版本仍被調用。

    7. (高級)參數爲傳值方式傳遞的對象的函數調用若是不用早捆綁,則虛調用可能會侵入
不存在的部分。這可能嗎?寫一些代碼強制虛調用,看是否會引發衝突。解釋這個現象,檢驗
當對象以傳值方式傳遞時會發生什麼現象。
    8. (高級)經過咱們的處理器的彙編語言信息或者其餘技術,找出簡單調用所需的時間數
及虛函數調用的時間數,從而得出虛函數調用須要多用多少時間。

----------------------- Page 295-----------------------

 下載

                 第1 5章  模板和包容器類

    包容器類經常使用於建立面向對象程序的構造模塊(building block),它使得程序內部代碼更容易

構造。
    一個包容器類可描述爲容納其餘對象的對象。可把它想像成容許向它存儲對象,而之後可
以從中取出這些對象的高速暫存存儲器或智能存儲塊。
    包容器類很是重要,曾被認爲是早期的面嚮對象語言的基礎。例如,在 S m a l l t a l k中,程序
員把語言設想爲帶有類庫的程序翻譯器,而類庫的重要部分就是包容器類。因此 C + +編譯器的

供應商很天然地會爲用戶提供包容器類庫。
    像許多早期的別的C + +庫同樣,早期的包容器類庫仿效 S m a l l t a l k的基於對象的層次結構,
該結構很是適合S m a l l t a l k,可是該結構在C + + 的使用中卻帶來了一些不便,所以有必要尋求另
外的方法。
    包容器類是解決不一樣類型的代碼重用問題的另外一種方法。繼承和組合爲對象代碼的重用提

供一種方法,而C + +的模板特性爲源代碼的重用提供一種方法。
    雖然C + +模板是通用的編程工具,但當它們被引入該語言時它們彷佛不支持基於對象的包
容器類層次結構。新近版本的包容器類庫則徹底由模板構造,程序員能夠很容易地使用。
    本章首先介紹包容器類和採用模板實現包容器類的方法,接着給出一些包容器類和怎樣使
用它們的例子。

15.1  包容器和循環子

    倘若打算用C語言建立一個堆棧,咱們須要構造一個數據結構和一些相關函數,而在 C + +
中,則把兩者封裝在一個抽象數據類型內。下面的s t a c k類是一個棧類的例子,爲簡化起見,它
僅處理整數:

----------------------- Page 296-----------------------

     296   C + +編程思想
                                                                        下載

----------------------- Page 297-----------------------

                                             第15章 模板和包容器類          297
 下載

    類i s t a c k是最爲常見的自頂向下式的堆棧的例子。爲了簡化,此處棧的尺寸是固定的,但
是也能夠對其修改,經過把存儲器安排在堆中分配內存,來自動地擴展其長度(後面的例子會
介紹)。
    第二個類i s t a c k I t e r是循環子的例子,咱們能夠把其看成僅能和 i s t a c k協同工做的超指針。

注意,i s t a c k I t e r是i s t a c k的友元,它能訪問i s t a c k的全部私有成員。
    像一個指針同樣,i s t a c k I t e r的工做是掃視i s t a c k並能在其中取值。在上述的簡單的例子中,
i s t a c k I t e r能夠向前移動(利用運算符+ + 的前綴和後綴形式)和取值。然而,此處卻並無對定
義循環子方法予以限制。徹底能夠容許循環子在相關包容器中以任何方法移動和對包容的值進
行修改。但是,按照慣例,循環子是由構造函數建立的,它只與一個包容器對象相連,而且在

生命週期中不從新相連。(大多數循環子較小,因此咱們能夠容易地建立其餘循環子。)
    爲了使例子更有趣,這個f i b o n a c c i函數產生傳統的「兔子繁殖數」,這是一個至關有效的
實現,由於它決不會屢次產生這些數。
    在主程序m a i n ( )中,咱們能夠看到棧和它的相關循環子的建立和使用。一旦建立了這些類,
即可以很方便的使用它們。

包容器的必要性

    很明顯,一個整數堆棧不是一個重要的工具。包容器類的真正的需求是在堆上使用 n e w創
建對象和使用d e l e t e析構對象的時候體現的。一個廣泛的程序設計問題是程序員在寫程序時不
知道將建立多少對象。例如在設計航空交通控制系統時不該限制飛機的數目,不但願因爲實

際飛機的數目超過設計值而致使系統終止。在 C A D系統設計中,能夠安排許多造型,可是隻
有用戶能肯定到底須要多少造型。咱們一旦注意到上述問題,即可在程序開發中發現許多這
樣的例子。
    依賴虛存儲器去處理「存儲器管理」的 C程序員經常發現n e w 、d e l e t e和包容器類思想的
混亂。表面上看,建立一個囊括任何可能需求的 h u g e型全局數組是可行的,這沒必要有不少考

慮(並不須要弄清楚m a l l o c ( )和f r e e ( ) ),可是這樣的程序移植性較差,並且暗藏着難以捕捉的
錯誤。
    另外,建立一個h u g e型全局數組對象,構造函數和析構函數的開銷會使系統效率顯著地下
降。C + +中有更好的解決方法:將所須要的對象用n e w建立並將其指針放入包容器中,待實際
使用時將其取出。該方法肯定了只有在絕對須要時才真正建立對象。因此在啓動系統時能夠忽

略初始化條件,在環境相關的事件發生時才真正建立對象。
    在大多數狀況下,咱們應當建立存放感興趣的對象的包容器,應當用 n e w建立對象,而後
把結果指針放在包容器中(在這個過程當中向上映射),具體使用時再將指針從包容器中取出。
該技術有很強的靈活性且易於分類組織。

15.2  模板綜述

    如今出現了一個問題,i s t a c k可存放整數,可是也應該容許存放造型、航班、工廠等等數

據對象,若是這種改變每次都依賴源碼的更新,則不是一個明智的辦法。應該有更好的重用

----------------------- Page 298-----------------------

  298        C + +編程思想
                                                                      下載

方法。
    有三種源代碼重用方法:用於契約的 C方法;影響過C + + 的S m a l l t a l k方法;C + + 的模板方

法。

15.2.1 C方法

    毫無疑問,應該摒棄C方法,這是因爲它表現繁瑣、易發生錯誤、缺少美感。若是須要拷
貝s t a c k的源碼並對其手工修改,還會帶入新的錯誤。這是很是低效的技術。

15.2.2 Smalltalk 方法

    S m a l l t a l k方法是經過繼承來實現代碼重用的,既簡單又直觀。每一個包容器類包含基本通用
類o b j e c t的所屬項目。S m a l l t a l k的基類庫十分重要,它是建立類的基礎。建立一個新類必須從

已有類中繼承。能夠從類庫中選擇功能和需求接近的已有類做爲父類,並在對父類的繼承中加
以修正從而建立一個新類。很明顯這種方法能夠減小咱們的工做而提升效率(所以花大量的時
間去學習S m a l l t a l k類庫是成爲熟練的S m a l l t a l k程序員的必由之路)。
    因此這意味着S m a l l t a l k的全部類都是單個繼承樹的一部份。當建立新類時必須繼承樹的某
一枝。大多數樹是已經存在的(它是S m a l l t a l k的類庫),樹的根稱做o b j e c t——每一個S m a l l t a l k包

容器所包含的相同的類。
    這種方法表現出的整潔明瞭在於 S m a l l t a l k類層次上的任何類都源於o b j e c t 的派生,因此任
何包容器可容納任何類,包括包容器自己。基
於基本通用類的(常稱爲 o b j e c t )的單樹形層
次模式稱爲「基於對象的繼承」。咱們可能聽
                                                                   不從Object派生
說過這個概念,並猜測這是另外一個O O P 的基本
概念,就像「多態性」同樣。但實際上這僅僅
意味着以o b j e c t (或相近的名稱)爲根的樹形               存放指向對
類結構和包含o b j e c t的包容器類。                     象的指針

    因爲S m a l l t a l k類庫的發展史較C + +更長久,

且早期的 C + +編譯器沒有包容器類庫,因此
                                                       圖 15-1
C + +能將S m a l l t a l k類庫的良好思想加以借鑑。
這種借鑑出如今早期的C + +實現中 [1]        ,因爲它表現爲一個有效的代碼實體,許多人開始使用它,

但把它用於包容器類的編程時則發現了一個問題。
    該問題在於,在S m a l l t a l k中,咱們能夠強迫人們從單個層次結構中派生任何東西,但在

C + +中則不行。咱們原本可能擁有完善的基於 o b j e c t的層次結構以及它的包容器類,可是當我
們從其餘不用這種層次結構的供應商那裏購買到一組類時,如造型類、航班類等等(層次結構
增長開銷,而C程序員能夠避免這種狀況),咱們如何把這些類樹集成進基於o b j e c t的層次結構
的包容器中呢?這些問題以下所示:
    因爲C + +支持多個無關聯層次結構,因此 S m a l l t a l k的「基於o b j e c t的層次結構」不能很好

地工做。
    解決方案彷佛是明顯的。若是咱們有許多繼承層次結構,咱們就應當能從多個類繼承:多
重繼承能夠解決上述問題。因此咱們應按下述的方法去實施:

  [1] OOPS庫,Keith Gorlen在N I H時開發的。通常以公開源代碼的形式使用。

----------------------- Page 299-----------------------

                                             第15章 模板和包容器類          299
 下載

    o s h a p e具備s h a p e的特色和行爲,但它也是o b j e c t的派生類,因此可將其置於包容器內。
    可是原先的C + +並不包含多重繼承,當包容
器問題出現時,C + +供應商被迫去增長多重繼承
的特性。另一些程序員一直認爲多重繼承不
是一個好主意,由於它增長了沒必要要的複雜性。
那時一句再三重複的話是「C + +不是S m a l l t a l k」,
這意味着「不要把基於o b j e c t 的層次結構用於包

               [ 1 ]
容器類」。但最終  ,因爲不斷的壓力,仍是把
多重繼承加入到該語言中了。編譯器供應商將
基於o b j e c t的包容器類層次結構加入產品中並進                          圖 15-2
行了調整,它們中的大多數由模板來替代。我
們能夠爲多重繼承是否能夠解決大多數編程問題而進行爭論,可是,在下一章中能夠看到,由
於其複雜性,除某些特殊狀況,最好避免使用它。

15.2.3 模板方法

    儘管具備多重繼承的基於對象的層次結構在概念上是直觀的,可是在實踐上較爲困難。在
S t r o u s t r u p的最初著做 [2] 中闡述了基於對象層次的一種更可取的選擇。包容器類被創造做爲參

數化類型的大型預處理宏,而不是帶自變量的模板,這些自變量能爲咱們所但願的類型替代。
當咱們打算建立一個包容器存放某個特別類型時,應當使用一對宏調用。
    不幸的是,上述方法在當時的S m a l l t a l k文獻中被弄混淆了,加之難以處理,基本上沒有什
麼人將其澄清。
    在此期間,S t r o u s t r u p和貝爾實驗室的C + +
小組對原先的宏方法進行了修正,對其進行了
簡化並將它從預處理範圍移入了編譯器。這種
新的代碼替換裝置被稱爲模板 [3]           ,並且它表現

了徹底不一樣的代碼重用方法:模板對源代碼進
                                                       圖 15-3
行重用,而不是經過繼承和組合重用對象代碼。
包容器再也不存放稱爲o b j e c t 的通用基類,而由一個非特化的參數來代替。當用戶使用模板時,
參數由編譯器來替換,這很是像原來的宏方法,卻更清晰、更容易使用。
    如今,使用包容器類時關於繼承和組合的憂慮能夠消除了,咱們能夠採用包容器的模板版
本而且複製出和咱們的問題相關的特定版本,像這樣:
    編譯器會爲咱們作這些工做,而咱們最終是以所須要的包容器去作咱們的工做,而不是用
那些使人頭疼的繼承層次。在C + +中,模板實現了參數化類型的概念。模板方法的另外一好處是
對繼承不熟悉、不適應的程序新手能正確地使用密封的包容器類。

15.3  模板的語法

    「模板(t e m p l a t e )」這一關鍵字會告訴編譯器下面的類定義將操做一個或更多的非特定類
型。當對象被定義時,這些類型必須被指定以使編譯器可以替代它們。

  [1] 咱們也許決不能知道其所有,由於該語言的控制仍在AT & T中。

  [2] The C++Programming Language,由Bjarne Stroustrup著(初版,A d d i s i o n - We s l e y, 1986 )。

  [3] 模板的靈感最初出如今A D A 。

----------------------- Page 300-----------------------

  300        C + +編程思想
                                                                      下載

    下面是一個說明模板語法的小例子:

    除了這一行

    template<class T>

之外,它看上去像一個一般的類。這裏 T是替換參數,它表示一個類型名稱。在包容器類中,
它將出如今那些本來由某一特定類型出現的地方。
    在a r r a y中,其元素的插入和取出都用相同的函數,即重載的o p e r a t o r [ ]來實現。它返回一個
引用,所以可被用於等號的兩邊。注意,當下標值越界時,標準 C庫的宏a s s e r t ( )將輸出提示信

息(使用a s s e r t ( )而不是a l l e g e ( )在於咱們能夠在調試後完全移去測試代碼)。這裏,拋出一個異
常,並由類的用戶處理它會更適合一些,關於這些將在第1 7章中作進一步介紹。
    在m a i n ( ) 中,咱們能夠看到很是容易地建立包含了不一樣類型對象的數組。當咱們說:

    array<int> ia;

    array<float> fa;

這時,編譯器兩次擴展了數組模板(這被稱爲實例),建立兩個新產生的類,咱們能夠把它們當
做a r r a y _ i n t和a r r a y _ f l o a t (不一樣的編譯器對名稱有不一樣的修飾方法)。這些類就像手工建立的同樣,
除非你定義對象i a和f a,編譯器會爲你建立它們。注意要避免類在編譯和鏈接中被重複定義。

15.3.1 非內聯函數定義

    固然,有時咱們使用非內聯成員函數定義,這時編譯器會在成員函數定義以前察看模板聲

----------------------- Page 301-----------------------

                                             第15章 模板和包容器類          301
 下載

明。下面在前述例子的基礎上加以修正來講明非內聯成員函數的定義:

    注意,在成員函數的定義中,類名稱被限制爲模板參數類型:a r r a y < T >。

    你能夠想象在一些混合型中編譯器實際支持兩個名字和參數類型。
    • 頭文件
    甚至是在定義非內聯函數時,模板的頭文件中也會放置全部的聲明和定義。這彷佛違背了通
常的頭文件規則:「不要在分配存儲空間前放置任何東西」,這條規則是爲了防止在鏈接時的多重定
義錯誤。但模板定義很特殊。由t e m p l a t e <…>處理的任何東西都意味着編譯器在當時不爲它分配存

儲空間,它一直處於等待狀態直到被一個模板實例告知。在編譯器和鏈接器的某一處,有一機制能
去掉指定模板的多重定義。因此爲了容易使用,幾乎老是在頭文件中放置所有的模板聲明和定義。
    有時,也可能爲了知足特殊的須要(例如,強制模板實例僅存在於一簡單的Windows DLL
文件中)而要在一個獨立的C P P文件中放置模板的定義。大多數編譯器有一些機制容許這麼作,
那麼咱們就必須檢查咱們特定的編譯器說明文檔以便使用它。

15.3.2 棧模板(the stack as a template)

    對於I S TA C K . C P P的包容器和循環子(第1 5 . 1節),能夠使用模板,做爲普通包容器類實現:

----------------------- Page 302-----------------------

       302   C + +編程思想
                                                                       下載

    注意在引用模板名稱的地方,必須伴有該模板的參數列表,如 stackt<T>& S。咱們能夠想
象,模板參數表中的參數將被重組,以對於每個模板實例產生惟一的類名稱。
    同時也注意到,模板會對它包含的對象作必定的假設。例如,在p u s h ( ) 函數中,s t a c k t會認
爲T的內部有一種賦值運算。
    這裏有一個修正過的例子用於檢驗模板:

----------------------- Page 303-----------------------

                                             第15章 模板和包容器類                303
 下載

    惟一的不一樣是在實例i s和i t的建立中:咱們指明瞭棧和循環子應該存放在模板參數表內部
對象的類型。

15.3.3 模板中的常量

    模板參數並不侷限於有類定義的類型,能夠使用編譯器內置類型。這些參數值在模板特定
實例時變成編譯期間常量。咱們甚至能夠對這些參數使用缺省值:

----------------------- Page 304-----------------------

  304        C + +編程思想
                                                                      下載

    類m b l o c k是一個可選的數組對象,咱們不能在其邊界之外進行索引。(若是出現這種狀況,
將在第1 7章中介紹比a s s e r t ( )更好的方法。)
    類h o l d e r和m b l o c k 極爲類似,可是在 h o l d e r 中有一個指向m b l o c k 的指針,而不是含有

m b l o c k類型的嵌入式對象。該指針並不在h o l d e r 的構造函數中初始化,其初始化過程被安排在
第一次訪問的時候。 若是咱們正在建立大量的對象,卻又不當即所有訪問它們,能夠用這種
技術,以節省存儲空間。

15.4  stash & stack模板

    貫穿本書且不斷修正更新的s t a s h和s t a c k都是真正的包容器類,因此將其轉化成爲模板是必要
的。可是,首先須要解決一個有關包容器類的重要問題:當包容器釋放一個指向對象的指針時,

----------------------- Page 305-----------------------

                                             第15章 模板和包容器類          305
 下載

會析構該對象嗎?例如,當一個包容器對象失去了指針控制,它會析構全部它所指向的對象嗎?

15.4.1 全部權問題

    全部權問題是廣泛關心的問題。對對象進行徹底控制的包容器一般無需擔憂全部權問題,

由於它清晰、直接、徹底地擁有其包含的對象。可是若包容器內包含指向對象的指針(這種情
況在C + + 中至關廣泛,尤爲在多態狀況下),而這些指針極可能用於程序的其餘地方,那麼刪
除了該指針指向的對象會致使在程序的其餘地方的指針對已銷燬的對象進行引用。爲了不上
述狀況,在設計和使用包容器時,必須考慮全部權問題。
    許多程序都很是簡單,一個包容器所包含的指針所指向的對象都僅僅用於包容器自己。在

這種狀況下,全部權問題簡單而直觀:該包容器擁有這些指針所指向的對象。因爲一般大多數
都是上述狀況,所以把包容器徹底擁有包容器內的指針所指向的對象的狀況定義爲缺省情形。
    處理全部權問題的最好方法是由用戶程序員來選擇。這經常用構造函數的一個參數來完成,
它缺省地指明全部權(對於典型理想化的簡單程序)。另外還有讀取和設置函數用來查看和修
正包容器的全部權。倘若包容器內有刪除對象的函數,包容器全部權的狀態會影響刪除,因此

咱們還能夠找到在刪除函數中控制析構的選項。咱們能夠對在包容器中的每個成員添加全部
權信息,這樣,每一個位置都知道它是否須要被銷燬,這是一個引用記數變量,在這裏是包容器
而不是對象知道所指對象的引用數。

15.4.2 stash模板

    「s t a s h」類是一個理想的模板構造的實例,有關它的修改貫穿於本書(最近的見第1 2章) 。下
面的例子中,帶有全部權操做的循環子也加入其中。

----------------------- Page 306-----------------------

     306   C + +編程思想
                                                                        下載

----------------------- Page 307-----------------------

                                              第15章 模板和包容器類                 307
下載

----------------------- Page 308-----------------------

  308        C + +編程思想
                                                                       下載

    儘管枚舉enum owns經常被嵌入在類中,但這裏仍是將其定義成全局量。這是更方便的使
用方法,倘若打算觀察其效果,咱們能夠試着把它移進去。
    s t o r a g e指針在類中被置爲保護方式,這樣經過繼承獲得的類能夠直接訪問它。這意味着繼
承類必然依賴於t s t a s h的特定實現,可是正如咱們將在S O RTED.CPP            例子(見1 5 . 7 )中看到的,這

樣作是值得的。
    o w n標誌指明瞭包容器是否以缺省方式擁有它所包容的對象。若是是這樣,存在於包容器
中的指針所指向的對象將在析構函數中被相應地銷燬。這是一種簡單的方法,包容器知道它所
包含的類型。能夠在構造函數中用重載函數o w n s ( )讀和修改缺省的全部權。
    應該認識到,若是包容器存放的指針是指向基類的,該類型應該具有一個虛析構函數來保

證正確地清除派生對象,置於包容器中的派生對象的地址已經被向上映射。
    在生存期中t s t a s h I t e r遵循着與單個包容器相結合的循環子模式。另外拷貝構造函數容許我
們建立新的循環子,指向已存在的循環子所指向的位置,這樣能夠很是高效地在包容器中建立
書籤。forward() 和 b a c k w a r d ( )成員函數容許移動循環子幾步,它和包容器邊界有關。增量和減
量的重載運算符能夠移動循環子一個位置。循環子所涉及的元素經常用靈活的指針來對其操做,

經過調用包容器中的r e m o v e ( )函數可完成對當前對象的消除。
    下面的例子是建立和檢測兩個不一樣的t s t a s h對象,一個屬於新類I n t並在它的析構函數和構
造函數中給出報告,而另外一個含有屬於第1 2章的S t r i n g類的對象:

----------------------- Page 309-----------------------

                                             第15章 模板和包容器類                309
下載

   在兩種情形中,都建立了循環子,用來在包容器中先後移動。請注意使用構造函數所產生

----------------------- Page 310-----------------------

       310   C + +編程思想
                                                                       下載

的優美效果:咱們無需關心使用數組的實現細節。咱們告訴包容器和循環子作什麼,而不是怎

麼作,這將使得問題的解更容易造成概念,更容易創建和更容易修改。

15.4.3 stack模板

    在第1 3章中的s t a c k類,它既是一個包容器,也是一個帶有相關循環子的模板。下面是新的
頭文件:

----------------------- Page 311-----------------------

                                              第15章 模板和包容器類                 311
下載

----------------------- Page 312-----------------------

  312        C + +編程思想
                                                                      下載

    咱們能夠注意到,這個類已被修改,能支持全部權處理,由於該類能夠識別出確切的類型
(或至少是基類型,它在運做中使用了虛析構函數)。如同t s t a s h 的情形同樣,缺省方式是包容

器銷燬它的對象,但咱們能夠經過修改析構函數的參數或者經過用 O w n s ( )對成員函數進行讀寫
以改變這種缺省方式。
    循環子是很是簡單的小規模指示器。當建立一個 t s t a c k I t e r a t o r時,它從鏈表的頭開始,在
鏈表中只能向前推動。倘若打算從新從頭部啓動,能夠建立一個新的循環子;倘若要記住鏈表
中的某位置,能夠從指向該位置的已生成的循環子處建立一個新的循環子(使用拷貝構造函

數)。
    爲了對循環子所指的對象調用函數,咱們能夠使用靈巧指針(循環子的一般方法)或使用
被稱爲c u r r e n t ( )的函數,該函數看上去和靈巧指針相同,由於它返回一個指向當前對象的指針,
但它們是不一樣的,由於靈巧指針執行逆向引用的外部層次(見第 11章)。最後,operator int()指
出,是否咱們已處在表的尾部和是否容許在條件語句中使用該循環子。

    完整的實現包含在這個頭文件中,因此這裏沒有單獨的 C P P文件。下面是循環子檢測和練
習的小例子:

    t s t a c k被實例化爲存放S t r i n g對象而且填充了來自某文件的一些行。而後循環子被建立,用

----------------------- Page 313-----------------------

                                             第15章 模板和包容器類           313
 下載

來在被連接的表中移動。第十行用拷貝構造函數由第一個循環子產生第二循環子,之後,這一
行被打印,動態建立的循環子被銷燬。這裏,動態對象的建立被用於控制對象的生命週期。

    這和先前的s t a c k類測試例子十分類似,可是如今所包含的對象能隨 t s t a c k 的銷燬而適當地
被銷燬。

15.5  字符串和整型

    爲了在本章的剩餘部分進一步改進這些例子,有必要引入功能強大的字符串類,它與整數
對象一塊兒來保證初始化。

15.5.1 棧上的字符串

    這裏是一個更加徹底的字符串類,在這本書以前該類已被使用。另外,它使用了模板,添

加了一個特殊的特性:對S S t r i n g實例化時,咱們可以決定它存在於堆上仍是棧上。

----------------------- Page 314-----------------------

  314        C + +編程思想
                                                                       下載

    經過使用typedef Hstring ,咱們能夠獲得一個普通的基於堆的字符串(使用t y p e d e f而不是

使用繼承是由於繼承須要從新建立構造函數和重載符= )。可是,倘若關心的是生成和銷燬許多
字符串時的效率,咱們能夠冒險設定所涉及問題的解的字符最大可能長度。如給出了模板的長
度參數,它能夠自動地在棧上而不是在堆上建立對象,這意味着每一個對象的 n e w和d e l e t e的開銷
將被忽略。咱們能發現運算符=也被提升了運行速度。
    字符串的比較運算符使用了稱之爲 s t r i c m p ( )的函數,雖然它不是標準C函數,但卻能爲大

多數編譯器的庫所承認。它執行字符串比較時會忽略字母大小寫。

15.5.2 整型

    類i n t e g e r的構造函數會賦零值,它包含一個自動將類型轉換成i n t型的運算符,因此能夠容

----------------------- Page 315-----------------------

                                             第15章 模板和包容器類          315
 下載

易地提取數據:

    雖然這個類至關小(它僅僅知足本章的須要),但咱們能夠方便地遵循第11章中的例子而

添加不少咱們所須要的運算。

15.6  向量

    雖然t s t a s h 的表現有一點和向量相似,可是建立一個和向量同樣的類是很方便的,也就是,
它的僅有的行爲是索引。(由於它僅有的接口是o p e r a t o r [ ] .)

15.6.1 「無窮」向量

    下面的向量類僅僅擁有指針。它從不須要調整大小:咱們能夠簡單地對任何位置進行索引,
這些位置可魔術般地變化,而無需提早通知向量類。o p e r a t o r [ ]能返回一個指針的引用,因此可

以出如今= 的左邊(它能夠是一個左值,也能夠是一個右值)。向量類僅僅與指針打交道,因此
它工做的對象沒有類型限制,對於類型的行爲沒有事先假定的必要。

----------------------- Page 316-----------------------

     316   C + +編程思想
                                                                        下載

  下面是它的頭文件:

----------------------- Page 317-----------------------

                                             第15章 模板和包容器類          317
 下載

    爲了易於實現,向量被分紅正和負兩個部分,咱們能夠出於某些緣由改變下面的實現使相
鄰的存儲空間得以利用。
    當增長存儲單元時,將使用私有函數e x p a n d ( )。e x p a n d ( )採用的參數是引用T * * &而不是一個
指針。這是由於在r e a l l o c ( )以後,它必須改變外部參數以指向一個新的物理地址。另外,還須要
參數s i z e和index  ,以便於對新的單元賦零。(這一點是重要的,由於若是向量擁有對象,析構函

數會對全部的指針調用delete 。)size以int&進行傳遞,由於它也必須改變以反映新的存儲長度。
    在o p e r a t o r [ ]中,無論是向量內的正或負的部分,若索取一個在當前使用的存儲空間以外的
存儲位置,那麼更多的存儲空間會被分配。這比內置的數組更加方便,而且建立也無需爲存儲
長度的大小而擔憂。

----------------------- Page 318-----------------------

  318        C + +編程思想
                                                                       下載

    本程序使用了標準C函數s t r t o k ( ),它取字符緩衝區的起始地址(第一個參數)和尋找定界

符(第二個參數)。它用零來代換定界符並返回以標誌爲起始的地址。倘若在隨後的時間以第
一參數爲零的形式來調用它,它將從剩餘的字符串中繼續抽取直至最後。在上面的例子中,是
以空格和製表符爲定界符來抽取字詞的。每一個字詞被返回進 St r i n g 中,而每一個指針被存放進
w o r d s 向量中,它最終能以拆分紅字詞的方式而包含整個文件。

15.6.2 集合

    一個集合的約束條件是它的元素不重複。咱們能夠向一個集合添加元素,也能夠測試一下

某個元素是不是集合中的成員。下面的集合類使用了一個包含其元素的向量:

----------------------- Page 319-----------------------

                                             第15章 模板和包容器類          319
 下載

    a d d ( )在檢測肯定一個新元素不在集合後,將其追加入集合中。 c o n t a i n s ( )告訴咱們對象是
否已存在於集合之中,i n d e x ( )告訴咱們對象在集合之中的位置。能夠使用 o p e r a t o r [ ]來檢索它。
l e n g t h ( )告訴咱們集合中有多少個元素。
    上面的例子中始終記着數組元素的數量。做爲提升的手段,最後被檢索的索引元素會被保
存下來,因此i n d e x ( )直接跟隨在c o n t a i n s ( )以後將不會有兩次對集合的遍歷運算。內聯的私有函

數w i t h i n ( )使實施更爲容易。

----------------------- Page 320-----------------------

  320        C + +編程思想
                                                                       下載

    下面的驗證例子使用了集合類而生成一個字詞索引,它由存於某文件中的一批字詞組成。

    這個程序再次使用了strtok(),但此次的定界符則更多,此外,亦刪除了尾部字符及其編號。

    注意到因爲 a d d ( )所但願接受的對象是字符串型的,而對  c h a r *型的類型轉換是由使用
S S t r i n g < 4 0 >構造函數來自動完成的,該構造函數會取得一個 c h a r *型參數。這樣會產生一個臨
時對象,該對象的地址可傳遞給a d d ( ),倘若a d d ( )在表中未發現該臨時對象,就會複製它將其
加入表中。a d d ( )的參數傳遞使用了對象引用而非指針的方式,這是重要的一點,由於在這裏
指針沒有必要置於其中,倘若使用n e w及運算指針將會最終失去指針。

15.6.3 關聯數組

    一個普通的數組使用一個整型數值做爲某類型序列元素的下標。在通常狀況下,若想使用

----------------------- Page 321-----------------------

                                             第15章 模板和包容器類          321
 下載

任意類型爲數組下標和其餘任意類型爲元素,這就是模板的一個理想情形。關聯數組能夠使用
任何類型做爲元素的下標。

    這裏展現的關聯數組使用了集合類和向量類建立了關於指針的兩個數組:一個爲輸入類型,
另外一個爲輸出類型。倘若採用了一個之前未遇到的下標,它會建立一個該下標的副本(假定輸
入類型具備拷貝構造函數和o p e r a t o r = )做爲新的輸入對象而且生成一個新的使用缺省構造函數
的輸出對象。o p e r a t o r [ ]僅僅返回一個引用給輸出對象,因此咱們能夠使用它來完成運算。也可
以用一整型參數調用in_value() 和o u t _ v a l u e ( )函數去產生輸入和輸出的數組的全部元素:

----------------------- Page 322-----------------------

       322   C + +編程思想
                                                                       下載

    關聯數組的一個經典的程序實例是對一文件進行字數統計,這是一個至關簡單但很實用的
工具。關聯數組的輸出值不能是內部數據類型( b u i l t - i n ),這是關聯數組的限制之一。由於內
部數據類型沒有缺省構造函數,在建立時不能初始化。爲了在字數統計程序中解決該問題,整

數類和S S t r i n g類一同用於關聯數組的檢測:

----------------------- Page 323-----------------------

                                             第15章 模板和包容器類          323
 下載

    該程序中關鍵的一行是:

    strcount[s]++;//count word

    因爲s是c h a r *型參數,而o p e r a t o r [ ]所但願的是S S t r i n g < 8 0 >型,編譯器可生成一個臨時對象
傳遞給使用構造函數S S t r i n g < 8 0 > ( c h a r * )的o p e r a t o r [ ] 。該臨時對象被建立於棧上,因此它能夠
快速生成和銷燬。
    o p e r a t o r [ ]返回一個i n t e g e r &給a s s o c _ a r r a y中相應的對象,這樣咱們能夠賦於對象任何行爲。
這裏,簡單地經過累加運算來講明發現了另外一個單詞。主程序的後面部分用於解決傳統  「貨

單」問題。貨單文件的每一行都包含項目(名稱可能用空格分開)和數量。這些項目可能在表
中不止一次出現,關聯數組可對其進行統計。在這些代碼中 C 的標準宏i s s p a c e ( ) 的使用做爲對
環境空間的額外補償。下面代碼行用於統計的實現:

    shoplist[&buf[i]] +=count;

    和之前同樣, c h a r *的內部檢索會產生一個S S t r i n g對象,由於這是索引運算符預期搜尋的
對象,類型間的轉化由構造函數完成。若是咱們跟蹤該程序就會發現由這個臨時對象引發的構
造函數和析構函數的調用。

15.7  模板和繼承

    沒有東西能妨礙咱們採用在普通類中的方法來使用類模板。例如咱們能很容易地從一個模
板中得到繼承,能夠從已存在的模板中繼承和加以實例化從而建立一個新的模板。儘管 t s t a s h
類在前述中已經可知足咱們的要求,如今當咱們但願它追加自排序功能時,能夠方便地對其進
行代碼重用和數值添加:

----------------------- Page 324-----------------------

     324   C + +編程思想
                                                                        下載

----------------------- Page 325-----------------------

                                             第15章 模板和包容器類           325
 下載

    本例子包含了一個隨機數發生器類,它可產生惟一數和重載o p e r a t o r ( ) 以便使用通常的函數
調用語句。u r a n d的惟一性是由保存隨機數空間的全部可能的數的影(m a p )象而產生的(隨機
數空間的上界由模板參數設置),並標記每個已使用的爲關閉。構造函數的第二個可選參數
的做用是容許咱們在界內隨機數用完的狀況下能夠重用這些數。注意,爲了優化運行速度咱們
將影象定義成固定完整數組,而不論咱們須要多少數。倘若咱們打算優化數組長度,能夠這樣

改動下面的實現:把m a p安排成動態申請存儲方式;把隨機數自己送入 m a p而不是置標誌,這
樣的改變不會影響任何客戶代碼。
    模板s o r t e d爲全部由它實例化而產生的類施加一個約束:它們必須包含一個 >運算符。在
S S t r i n g中,這種施加是明顯的,可是在i n t e g e r中,自動類型轉換運算符i n t ( )提供了一個內置>

----------------------- Page 326-----------------------

  326        C + +編程思想
                                                                      下載

運算符的途徑。當模板提供更多的功能時,一般要對類賦予更多的需求,。有時不得不繼承被
包含的類以增長必要的功能。注意使用重載運算符的價值: i n t e g e r類所提供的功能依賴於它的

底層實現。
    在例中,能夠看到在t s t a s h 中的s t o r a g e 以保護方式而非私有方式定義的好處。這對於讓類
s o r t e d知道很重要,這是真正的依賴性。倘若改變 t s t a s h 的下層實現的一些東西而非一個數組,
如鏈表,冒泡排序中的元素交換就會和原來徹底不一樣,因而從屬類 s o r t e d也須要改變。然而,
一個更好的選擇是(倘若有可能的話),對讓t s t a s h實現採用保護方法的一種更好的替代是提供

足夠的保護接口函數,這樣一來,訪問和交換可在派生類中完成而無需涉及底層實現。這種方
法仍然能夠改變下層實現,但不會傳播這些修改。
    注意,附加類s o r t e d S e t說明怎樣能夠快速地由已存在的類中粘貼所需的功能。 s o r t e d S e t可
從s e t類中獲取接口,並且當添加一個新元素到s e t類時,也同時爲其添加這個元素到s o r t e d對象,
所返回的值全是排序過的。咱們可能會考慮設計一個更爲簡化、有效的類版本,在這裏就再也不

詳述了(標準C + +模板庫包含一個可自排序的集合類)。下面是對S O RT E D . H的測試:

    該例經過建立一個排序數組來驗證SString 和i n t e rg e r類。

15.7.1 設計和效率

    在s o r t e d中,每次調用a d d ( )時,新元素都將被插入,數組也從新排序。這裏使用的冒泡排

序方法效率低下,不提倡使用(但它易於理解和編碼)。因爲冒泡排序方法是私有實現的一部
分,在這裏是至關合適的。在咱們開發程序時,通常的步驟是:

----------------------- Page 327-----------------------

                                             第15章 模板和包容器類          327
 下載

    1) 使類接口正確。
    2) 儘可能迅速、準確地實現原型。
    3) 驗證咱們的設計。
    經常,僅當咱們集成工做系統的初期「草稿」時纔會發現類接口問題,這種狀況很是廣泛。
在系統集成和初次實現期間,咱們還可能發現須要「幫助者」類,如同對包容器和循環子的須要
同樣。有時在系統分析期間很難發現上述問題(在分析中咱們的目標是獲得能快速實現和檢測的
全貌設計)。只有在設計被驗證後,咱們纔有必要花時間對其進行徹底刷新和考慮性能要求。假
如設計失敗或者性能要求不需考慮,則冒泡排序方法就不錯了,沒有必要進一步浪費時間。(當
然,一個理想的解決方案是利用別人的已被證明的排序包容器;首先應留意標準C + +的模板庫)。

15.7.2 防止模板膨脹

    每次模板實例化,其中的代碼都會從新生成(其中的內聯函數除外)。倘若模板內的一些
功能並不依賴定義類型,咱們能夠把它們放入一個通用基類以免沒必要要的代碼從新生成。例
如在第1 3章的I N H S TA K . C P P (見1 3 . 5 . 2 )中的繼承被用於定義s t a c k所能接受和產生的類型。下面
是一個模板化的版本代碼:

----------------------- Page 328-----------------------

  328        C + +編程思想
                                                                       下載

    在之前,內聯函數不產生代碼,而是經過僅一次性地建立一個基類代碼提供其功能。可是
全部權問題則能夠經過加入一個析構函數來解決 (它依賴類型,必須由模板來構造),在這裏的

全部權是缺省的。注意,當基類析構函數被調用時,棧將被清空,因此不會出現重複釋放問
題。

15.8  多態性和包容器

    多態性、動態對象生成和包容器在一個真正的面向對象程序中和諧地被利用,這是很廣泛
的。動態對象生成和包容器所解決的問題在於設計初期咱們可能不知道須要多少對象,須要什
麼類型的對象,這是由於包容器可持有指向基類對象的指針,每逢咱們把派生類指針放入包容

器,會發生向上映射(具備相應的代碼組織和可擴展性的好處)。下面的例子有點像垃圾回收
的工做過程,首先全部的垃圾被放入一個垃圾箱中,而後分類放入不一樣的箱中,它有一個函數
用於遍歷垃圾箱並估算出其中什麼有價值。這裏的垃圾回收模擬實現並不完美,在第 1 8章說明
「運行時類型識別( RTTI)  」時,再對該例進一步介紹。

----------------------- Page 329-----------------------

                                              第15章 模板和包容器類                 329
下載

----------------------- Page 330-----------------------

     330   C + +編程思想
                                                                        下載

----------------------- Page 331-----------------------

                                             第15章 模板和包容器類          331
 下載

    這裏使用了基類中的虛函數結構,這些函數將在派生類中獲得再定義。因爲包容器 t s t a c k
是t r a s h 的實例化,因此它含有t r a s h指針,這些指針是指向基類的。然而, t s t a c k也會含有指向
t r a s h 的派生類對象的指針,正如調用p u s h ( )所看到的那樣。隨着這些指針的加入,它們會失去
原本的特定身份而變成t r a s h的指針。然而因爲多態性,當經過tally  和 s o r t e r循環調用虛函數時,

與要求相適應的行爲仍會發生。(注意,使用循環子的靈巧指針會致使虛函數的調用。)
    t r a s h類還包含一個虛析構函數,向任何類中自動增長內容都應當利用虛函數。當b i n包容器超
出了相應範圍時,包容器的析構函數會爲它所包容的全部對象調用虛析構函數,進行有效的清除。
    因爲包容器類模板通常不多有所見到的「普通」類的向下繼承和向上映射,因此咱們幾乎
不會看到在這些類中存在虛函數,它們的重用是以模板方式而非繼承方式。

15.9  包容器類型

    這裏所採用的一系列包容器類大約和「數據結構」類工具至關(固然包容器因爲具有一些
相關聯的功能於是比數據結構更豐富)。包容器將其功能和數據結構打包集成在一塊兒,它能更
爲天然地表達「數據結構」的概念。
    雖然包容器可能須要特定的行爲(如在棧尾壓入和拋出元素),可是經過使用較通用的循
環子即可得到更大的自由度。下列的大多數類型都很容易支持關聯循環子。

    全部的包容器都容許放入和取出一些東西。它們的不一樣在於其功能用途,而有一些包容器
的相異之處則僅僅在於存放內容的類型不一樣。它們的不一樣在於訪問速度:一些易於線性訪問,
但在序列中間插入元素時,時間開銷卻較高。其餘的一些在中間插入元素時時間開銷較低,但
線性訪問的開銷卻較高。若是要在這兩種情形下作出取捨,應首先着眼於最可通融的方法。如
果在程序運行後,發現速度的通融性較差則須要進一步優化,優化成更加高效的方法。

    下面是存在於C + +標準模板庫S T L       (standard template library )中的包容器子集,S T L將在
附錄A 中討論:
    袋子:項目的(可能有重複)集合。它的元素沒有特定的次序要求, S T L不包含它,這是
由於其功能可由表和向量來實現。
    集合:不容許有重複元素的袋子就是集合。在                      S T L 中,集合是以一種聯合包容器

(associative container)來描述的,聯合包容器能夠根據關鍵字向包容器提供和從包容器中取回
數據元素。在集合中數據元素的存取檢索關鍵字就是元素自己。一個 S T L的多重集合容許將同
一關鍵值的許多副本放入不一樣的對象中。
    向量:可索引的項目序列。因爲它有一致的訪問時間,因此可做爲缺省選擇。
    隊列:從尾部追加元素,從頭部刪除元素的項目序列。它是雙端隊列的子集,有時不實現

它們,但咱們能夠用雙端隊列來代替。
    雙端隊列:具備兩個端部的隊列。序列中項目元素的追加和刪除可從任意一端進行。在大
部分的插入和刪除運算髮生在序列頭部或尾部時,可用其代替列表以提升效率。在頭部或尾部
作插入和刪除運算時,雙端隊列的時間複雜度爲常量級,但在序列中部實施運算時呈線性時間

----------------------- Page 332-----------------------

  332        C + +編程思想
                                                                      下載

複雜度。
    棧:在相同一端實施追加和刪除的項目序列。儘管在本書中被用做一個例子,其實它是雙

端隊列功能的一個子集。
    環形隊列:環形結構的項目序列,元素的追加和刪除位於環形結構的頂端,它是頭部和尾
部關聯的隊列。一般讓其支持系統底層活動,很是有效並且其時間複雜度爲常量級。例如在一
個通信中斷服務例程中,插入一些字符而隨後將其刪除,將不用擔憂存儲的耗盡及必須的時間
分配。可是,如不加以細緻地編程,環形隊列會超出正常的控制範圍。 S T L不包含環形隊列。

    列表:容許以相等的時間在任意點實施插入和刪除的有根的項目序列。它不提供快速隨機
訪問,但能在序列中間進行快速插入和刪除運算。列表通常以單鏈表和雙鏈表的形式來實現。
單鏈表的遍歷是單方向的,而雙鏈表可在任意節點上向前向後移動。因爲單鏈表僅包含一個指
向下一個節點的指針而雙鏈表則包含前趨和後繼兩個指針,因此單鏈表較雙鏈表的存儲開銷低。
可是在插入和刪除工做效能上,雙鏈表則優於單鏈表。

    字典:關鍵字和相應值的映射,在S T L中被稱爲「映像」(它是聯合包容器的另外一種形式)。
關鍵字和相應值的映射對有時被稱爲「聯繫」。字典值的存取和關鍵字是相關聯的。 S T L也提
供多重映射,容許多重映射相同關鍵字到相應值的多個副本上,這和哈希表至關。
    樹:存在一個根節點的節點和弧(節點間的鏈接)的集合。樹不包含環(沒有封閉的路徑)
和交叉路徑。S T L中不提供樹,樹具備的特性功能由S T L的其餘類型來提供。

    二叉樹:一個普通樹從每一個節點上射出的弧的數目是沒有限制的,而二叉樹從每一個節點上
最多隻射出兩條弧,即「左」「右」兩條弧。因爲節點的插入位置隨其值而定,當搜尋所指望
值時則沒必要瀏覽許多節點(而線性表則不一樣),因此二叉樹是最快的信息檢索方法之一。平衡
二叉樹在每次插入節點時,它從新組合以保證樹每一部分的深度都不超過基準值。然而每次插
入時的平衡處理的開銷則較高。

    圖:無根節點的節點和弧的集合。圖能夠包含環和交叉路徑。它一般更具備理論上的意義
並不經常使用於實際實施。它不是S T L的一部分。
    • 不要作重複工做
    在工做中咱們首先應在編譯器或其餘方便之處尋求 S T L中的公共組件,或從第三方供應商
處購得,以減小重複工做量。從頭建立這些組件目的僅僅是做爲練習或沒有辦法的辦法。

15.10   函數模板

    類模板描述了類的無限集合,出現模板的大部分地方都是出現類的地方。 C + + 一樣能夠支
持函數的無限集合的概念,有時它很是有用,其語法部分除用函數來替代類外和類模板沒有什
麼兩樣。
    倘若打算建立一些函數,這些函數除了處理各自的不一樣類型外,函數體看上去都相同,這
就有必要建立一個函數模板來描述這些函數。函數模板的典型例子是一個排序函數  [ 1 ] ,然而它

可適用於各類場合,下面的第一個例子做爲示範。第二個例子則揭示函數模板連同循環子和包
容器中的使用。

15.10.1 存儲分配系統

    例程m a l l o c ( )、c a l l o c ( )和 r e a l l o c ( )均可以較安全地對未開闢的存儲空間進行分配。下面的

  [1] 參看做者的C++Inside & out(Osborne/McGraw-Hill, 1993) 。

----------------------- Page 333-----------------------

                                             第15章 模板和包容器類               333
 下載

函數模板可產生既能分配一部分新的存儲空間又能爲已開闢的區域重設大小(如同 r e a l l o c ( ) )的
函數getmem()  。另外,它僅對新的存儲進行清零處理,而且檢查被其分配的存儲空間。並且,

提交給g e t m e m ( )的參數僅是所指望的某類型元素的數目而非字節數目,因此能夠下降程序出錯
的機率。這是它的頭文件:

----------------------- Page 334-----------------------

  334        C + +編程思想
                                                                       下載

    爲了可以僅在新的存儲區進行清零處理,將有一個用來指示所分配元素數目的計數器被置
於每一存儲區的首部。計數器的類型爲 typedef cntr             ,當處理較大的存儲區時可將其由整型改

爲長整型。(當使用長整型時其餘的一些問題會出現,然而無論怎樣,編譯器都會在警示中提
示這些問題。)
    之因此使用指針引用o l d m e m做爲參數是因爲外部變量(一個指針)必須改成指向新存儲
區,o l d m e m必須指向零(以分配新存儲區)或指向由 g e t m e m ( )建立的存儲區。該函數設想我
們能正確地使用它,但若是咱們打算對其調試,可在計數器附近加一個輔助標識,經過檢查該

標識來幫助發現g e t m e m ( )中的錯誤調用。
    若是所要求的元素數爲零,則該存儲被釋放。有另一個函數模板 f r e e m e m                   (),是這個行
爲的別名。
    咱們將注意到, g e t m e m ( ) 的處理層次很低,存在許多底層調用和字節處理。例如,
o l d m e m指針並不指向存儲區的真實的起始位置,而剛好在起始位置計數器以後。因此在用

f r e e ( )釋放存儲區時,g e t m e m ( )必須將指針向後退由c n t r 佔用的存儲空間的數目。因爲o l d m e m的
類型是T * ,必須首先將其映射爲c n t r *,而後它被向後索引一個位置。最後,爲f r e e ( )產生指定
位置地址的語句表達爲:

    free(&(((cntr*) oldmem )[-1]));

    一樣地,倘若這是一個已經分配的存儲空間,g e t m e m ( )必須後退一個c n t r長度,以獲取真
實的存儲空間的起始地址並取回先前的元素數目。在 r e a l l o c ( ) 內部須要真實的起始地址。若要
開闢的存儲空間是向上增長的,在m e m s e t ( ) 中的起始地址和須要清零的元素數目可由新的元素
數目減去舊的元素數目而求得。最後,產生計數器後面的地址,並將其賦給 o l d m e m,賦值語

句爲:

    o l d m e m = ( T * ) & ( ( ( c n t r * ) m ) [ 1 ] ) ;

    再者,因爲o l d m e m是對一個指針的引用,這致使傳給g e t m e m ( )的外部參數的變化。
    下面的程序用於測試g e t m e m ( )。它分配和填入值,而後再進一步開闢更多的存儲空間:

----------------------- Page 335-----------------------

                                             第15章 模板和包容器類          335
 下載

    在每次調用g e t m e m ( )後,存儲區中的值都被打印出來,能夠看到新的存儲區都被清零了。
    注意由整型指針和浮點型指針而實例化g e t m e m ( )的不一樣版本。因爲上述功能涉及到很是底
層的處理,咱們可能認爲應當使用一個非模板函數並傳遞 void* &做爲o l d m e m 的方式來實現。
這種想法是不能實現的,由於編譯器必須將咱們的類型轉化成 v o i d * 。爲了獲取引用,編譯器
會安排一個臨時域,因爲修改的是臨時指針而非咱們真正想要修正的指針,因此會出現錯誤。

故使用函數模板爲參數產生相應的確切類型是必需的。

15.10.2 爲tstack提供函數

    假設咱們打算擁有一個 t s t a c k並使一函數適用它所包含的全部對象。因爲 t s t a c k能夠包含
任意類型的對象,因此該函數應該能夠在 t s t a c k 的任意類型和其包含的任意類型對象下工做:

----------------------- Page 336-----------------------

     336   C + +編程思想
                                                                        下載

----------------------- Page 337-----------------------

                                             第15章 模板和包容器類          337
 下載

    a p p l i s t ( )函數模板可獲取包容器類的引用及類中成員函數的指針。爲了在棧中移動a p p l i s t ( ),
這裏使用了一個循環子而且把函數應用於每個對象。倘若咱們已經忘了成員指針的語法,可
複習第1 0章的後面部分。
    咱們能夠看到有不僅一個a p p l i s t ( )版本,因此重載函數模板是可行的。雖然它們均可接受

任意類型的返回值(這被忽略了,但對於匹配成員指針來講,類型信息則是所要求的),
a p p l i s t ( ) 的每個版本都有一些不一樣的參數,因爲它是一個模板,因此這些參數的類型是任意
的。(在類g r o m i t中,能夠看到一批不一樣的函數 [ 1 ] )。因爲不存在「超模板」爲咱們生成模板,

因此咱們必須決定究竟須要多少參數。
    雖然a p p l i s t ( ) 的定義至關複雜,其中的一部分不能期望一個初學者去理解它,可是它的使

用則很是清晰而簡單,初學者僅僅須要知曉完成什麼而非怎樣完成,因此初學者能夠容易地使
用它。咱們應儘可能把程序組件分紅不一樣的類別,僅僅關心所要完成的目標而不需關心底層的實
現細節。棘手的細節問題僅僅是設計者的任務。
    固然,這些功能類型會牢固地聯繫着t s t a c k類,因此一般咱們應該隨t s t a c k一道在頭文件中
找到這些函數模板,加以分析利用。

15.10.3 成員函數模板

    把a p p l i s t ( )安排爲成員函數模板也是可行的,這是一個和類模板相對獨立的模板定義 ,並且
它仍然是類的成員。所以可以使用下面更巧妙的語句:

    d o g s . a p p l i s t ( & g r o m i t : : s i t ) ;

    這和在類內部引出普通函數的作法(第2章)相相似 [ 2 ] 。

15.11  控制實例

    顯式實例化一個模板有時是有用的,這會告訴編譯器爲模板的特定版本安排代碼,即便並
不打算生成一個對象。爲了實現它,能夠重用下面的模板關鍵字:

    template class bobbin<thread>;

    template void sort<char>(char *[]);

    這是S O RT E D . C P P例子的一個版本(見1 5 . 7),在使用它以前會顯式實例化一個模板:

  [1] 參見對Nick Park 的英國活潑短片《糟糕的褲子》。

  [2] 檢查咱們的編譯器版本信息,看它是否支持函數模板。

----------------------- Page 338-----------------------

  338        C + +編程思想
                                                                       下載

    在該例子中,顯式實例並不真正地完成什麼事情,它未參與程序的運做。顯式實例僅在外
部控制的特定狀況下才是必須的。
    • 模板特殊化

    s o r t e d向量僅工做於用戶類型的對象上。例如,它不能對c h a r *型數組進行排序。爲了建立一
個特定版本,咱們能夠本身寫一個實例版本,就像編譯器已經經過並把咱們的類型代入了模板參
數同樣。可是要把咱們本身的代碼放入特殊化的函數體中。下面的例子揭示c h a r *型s o r t e d向量:

----------------------- Page 339-----------------------

                                             第15章 模板和包容器類          339
 下載

    在b u b b l e s o r t ( ) 中,咱們能夠看到使用的是 s t r c m p ( )而不是> 。

15.12  小結

    包容器類是O O P的一個基本部分,它是簡化和隱藏實施細節,提升開發效率的另外一種方法。
另外,它經過對C 中的舊式數組和粗糙的數據結構技術的更新替代從而大大地提升了靈活性和

安全性。
    因爲包容器是客戶程序員所須要的,因此包容器的實質是便於使用,這樣,模板就被引入
進來。對源代碼進行重用(相反的是繼承和組合實施對對象代碼的重用)的模板語句可對初學
者來講變得十分日常。實際上,使用模板實施代碼重用比繼承和組合容易得多。
    雖然在本書中咱們已經學習了包容器和循環子,但在實際中,學習編譯器所帶的包容器和
循環子是更迅速的方法,要否則就從第三方供應商處購買一個庫  [ 1 ] 。標準C + +庫是很完備的,

但在包容器和循環子方面並不充分。
    本章簡單地說起了包容器類設計方面的內容,咱們能夠加以總結以有更多的體會。一個復
雜的包容器類庫可能涉及全部的附加內容,包括持久性(在第 1 6章中介紹)和垃圾回收(在第
1 2章中介紹),也包含處理全部權問題的附加方法。

15.13  練習

    1. 修改第1 4章練習一的結果,以便使用tstack          和t s t a c k I t e r a t o r替代 s h a p e指針數組。增長針
對類層次的析構函數以便在t s t a c k超出範圍時觀察s h a p e對象被析構。
    2. 修改第1 4章例子S S H A P E 2 . C P P以使用t s t a c k替代數組。
    3. 修改R E C Y C L E . C P P 以使用t s t a s h替代t s t a c k 。
    4. 改變S E T T E S T. C P P以使用s o r t e d S e t替代s e t .。

    5. 爲tstash 類複製A P P L I S T. C P P的功能。
    6.  將T S TA C K . H拷貝到新的頭文件中,並增長A P P L I S T. C P P 中的函數模板做爲t s t a c k 的成
員函數模板。本練習要求咱們的編譯器支持成員函數模板。
    7. (高級)修改t s t a c k類以促進增長全部權的區分粒度。爲每個連接增長標誌以代表它
是否擁有其指向的對象,並在 a d d ( )函數和析構函數中支持這一標誌信息。增長用於讀取和改

變每一連接全部權的成員函數,並在新的上下文環境中肯定a d d ( )標誌的含義。
    8. (高級)修改t s t a c k類,使每個入口包含引用計數信息(不是它們所包容的對象)而且
增長用於支持引用計數行爲的成員函數。
    9. (高級)改變S O RT E D . C P P中u r a n d 的底層實現以提升其空間效率(S O RT E D . C P P後面段
落所描述的) 而非時間效率。

    10. (高級)將G E T M E M . H中的typedef cntr從整型改爲長整型,而且修改代碼以消除失去
精度的警示信息,這是一個指針算術問題。
    11. (高級)設計一個測試程序,用於比較建立在堆上和建立在棧上的S S t r i n g的執行速度。

    [1] C++標準庫包含一個很是地道的但並不是詳盡無遺的包容器和循環子集。

----------------------- Page 340-----------------------

                                                                       下載

                     第1 6章  多 重 繼 承

    多重繼承M I   (multiple inheritance) 的基本概念聽起來很是簡單。

    能夠經過繼承多個基類來生成一個新類。這種語法正如料想的那樣,只要繼承圖簡單,
M I也不復雜。然而M I會引入許多二義性和奇異的狀況,它貫穿於本章。如今首先給出關於本
主題概述。

16.1  概述

    在C + + 之前,最爲成功的面嚮對象語言是S m a l l t a l k。S m a l l t a l k從開始創建就成爲面向對象
的語言。就面向對象來講,S m a l l t a l k是「純種的」,而C + +則是「混血的」,這是由於C + +是在C
語言上創建的。S m a l l t a l k的一個設計原則是:全部的類應從一個單一層次上進行派生,它們根
植於一個基類(稱之爲O b j e c t,它是基於對象層次的模式),在S m a l l t a l k中不從一個已生成類中

進行繼承就不能生成一個新類,因此在建立新類以前必須學習 S m a l l t a l k類庫,這就是爲何得
花至關長的時間才能成爲S m a l l t a l k熟手的緣由。因此Smalltalk class  類層次老是一棵簡單的單
一樹。
    S m a l l t a l k的類有至關部分是共同的,例如其中O b j e c t的行爲和特性必然是相同的,因此幾
乎不會碰上須要繼承多個基類的狀況。然而只要咱們想想, C + +能夠建立任意個繼承樹,因

而對C + +語言的邏輯完備性來講,它必須支持多個類的結合,由此要求有多重繼承性。
    然而,這並非多重繼承必須存在的使人信服的理由。過去有,如今仍然有關於 M I是C + +
精華的否認意見。AT&T  的cfront2.0  版首先把M I加入語言並對語言作了明顯修改。從那之後,
許多改變咱們編程方法的其餘特性(特別如模板)都被加入進去而且下降了 M I 的重要性。我
們能夠把M I看成語言的「次要」特性。

    一個最爲緊迫的問題是如何駕馭涉及 M I
的包容器。倘若咱們打算建立一個使每一個用戶
都容易使用的包容器,在包容器內使用 v o i d *
是一個方法,如pstash  和 s t a c k。而S m a l l t a l k的
方法是建立一個包含 O b j e c t s 的包容器(記住

O b j e c t是整個S m a l l t a l k層次的基類型),因爲
S m a l l t a l k的全部東西都是由O b j e c t派生的,所
以任何含有O b j e c t s的包容器均可以包容任何東
                                                       圖 16-1
西,該方法是不錯的。
    如今考慮C + +的狀況。倘若供應商A建立了一個基於對象的層次結構,該層次包含了一組

有用的包容器,其中含有一個打算使用的稱之爲 h o l d e r 的包容器。如今咱們又發現了供應商B
的類層次,其中有一些重要的其餘類,如B i t I m a g e類,它能夠含有位圖。建立一個位圖包容器
僅有的方法是對B i t I m a g e和h o l d e r進行繼承以建立一個新類,它可擁有B i t I m a g e和h o l d e r 。
    對於M I來講,這是一個重要的理由,許多類庫都是以這種模式構造的。然而,正如在第
1 5章所看到的,模板的增長已經改變了包容器的生成方法,因此上述狀況並非 M I 的有力論

----------------------- Page 341-----------------------

                                                 第16章  多重繼承        341
 下載

點。
    其餘須要M I 的理由是和設計相關的邏輯上的。和以上狀況不一樣,這裏有對基類的控制,

同時咱們爲了使設計更靈活,更有用而採用 M I 。這種狀況
能夠以最初的輸入輸出流的類庫設計爲例:
    輸入流和輸出流自己都是有用的類,可是經過對它們
的繼承可將二者的特性和行爲加以結合而造成一個新類。
    無論使用M I是出於什麼樣的動機,在處理過程當中都會

出現許多問題,必須先理解它們然後再使用它們。

16.2  子對象重疊                                                 圖 16-2

    當繼承基類時,在派生類中就得到了基類全部數據成員的副本,該副本稱爲子對象。倘若
對類d 1和類d 2進行多重繼承而造成類m i ,類m i會包含d 1的子對象和d 2的子對象,因此m i對象
看上去如:

           圖 16-3                                       圖 16-4

    如今考慮若是d 1和d 2都是從相同基類派生的,該基類稱爲b a s e,那麼會發生什麼呢?
    在上面的圖中,d 1和d 2都包含b a s e的子對象,因此m i包含基的兩個子對象。從繼承圖形狀
上看,有時該繼承層次結構稱爲「菱形」。沒有菱形狀況時,多重繼承至關簡單,可是隻要菱
形一出現,因爲新類中存在重疊的子對象,麻煩就開始了。重疊的子對象增長了存儲空間,這

種額外開銷是否成爲一個問題取決於咱們的設計,但它同時又引入了二義性。

16.3   向上映射的二義性

    在上圖中,倘若把一個指向 m i 的指針映射給一個指向b a s e 的指針時,將會發生什麼呢?
b a s e有兩個子對象,所以會映射成哪個的地址呢?這裏用代碼來揭示上圖的問題:

----------------------- Page 342-----------------------

  342        C + +編程思想
                                                                       下載

    這裏存在兩個問題。首先,因爲d 1和d 2分別對v f ( )定義這會致使一個衝突,因此不能生成
m i類。其次,在對b [ ] 的數組定義中試圖建立一個new mi             並將類型轉化爲b a s e * ,因爲沒有辦法
搞清咱們打算使用d 1子對象的b a s e仍是d 2子對象的b a s e做爲結果地址,因此編譯器將不會受
理。

16.4  虛基類

    爲了解決第一個問題,必須對類m i 中的函數v f ( )進行從新定義以消除二義性。
    對於第二個問題的解決應着眼於語言擴展,這就是對 v i r t u a l賦予新的含義。倘若以v i r t u a l
的方式繼承一個基類,則僅僅會出現一個基類子對象。虛基類由編譯器的指針法術( p o i n t e r
m a g i c )來實現,該方法令人想起普通虛函數的實現。
    因爲在多重繼承期間僅有一個虛基類子對象,因此在地址回溯中不會產生二義性。下面是

一個例子:

----------------------- Page 343-----------------------

                                                 第16章  多重繼承         343
 下載

    如今編譯器能夠接受地址向上映射了,可是依然要對 m i 中的v f ( )消除二義性,不然,編譯
器分辨不出須要使用哪個版本。

16.4.1  「最晚輩派生」類和虛基初始化

    虛基類的使用並不如此簡單。以上的例子中使用的是編譯器生成的缺省構造函數,若是虛
基類存在一個構造函數,狀況就有些不一樣了。爲了便於理解,引入一個新術語:最晚輩派生類
(m o s t - d e r i v e d )。

    最晚輩派生類是當前所在的類,當考慮構造函數時它尤爲重要。在前面的例子中,基構造
函數裏的最晚輩派生類是b a s e ;在d 1構造函數中,d 1是最晚輩派生類;在m i構造函數中,m i是
最晚輩派生類。
    打算使用一個虛基類時,最晚輩派生類的構造函數的職責是對虛基類進行初始化。這意味
着該類無論離虛基類多遠,都有責任對虛基類進行初始化。這裏有一個初始化的例子:

----------------------- Page 344-----------------------

       344   C + +編程思想
                                                                       下載

    d 1和d 2在它們的構造函數中都必須對b a s e初始化,這和咱們想象的同樣。但 m i和x也都如

此,儘管它們和b a s e相隔好幾個層次。這是由於每個都能成爲最晚輩派生類。編譯器是沒法
知道使用d 1初始化b a s e仍是使用d 2的,於是咱們老是被迫在最晚輩派生類中具體指出。注意,
僅僅這個被選中的虛基構造函數被調用。

----------------------- Page 345-----------------------

                                                 第16章  多重繼承              345
 下載

16.4.2 使用缺省構造函數向虛基「警告」

    爲了促進最晚輩派生類初始化一個虛基類,最好經過建立一個虛基類的缺省構造函數,將

虛基視爲黑箱,以下面的例子。這是因爲虛基可能被深深地埋藏在類層次中,它看上去繁瑣而
使人困惑:

----------------------- Page 346-----------------------

       346   C + +編程思想
                                                                       下載

    倘若咱們總能爲虛基類安排缺省構造函數,這可以使別人繼承咱們的類變得很是容易。

 16.5 開銷

    術語「指針法術(pointer magic )」用於描述虛繼承的實現。下面的程序可觀察到虛繼承的
物理開銷。

----------------------- Page 347-----------------------

                                                         第16章  多重繼承           347
 下載

    這些都包含一個單字節,該字節是「內核長度( core size )」。因爲全部類都包含虛函數,
因爲有一個指針,所以對象長度會比內核長度大(起碼編譯器爲了校訂定位會把額外的字節添

入對象中),然而結果卻有點讓人吃驚。(該結果來自特定的編譯器,不一樣版本可能有所不一樣。)

    s i z e o f ( b ) = 2

    s i z e o f ( n o n v _ i n h e r i t a n c e ) = 2

    sizeof (v_inheritance)=6

    s i z e o f ( M I ) = 1 2

    正如所料,b和n o n v _ i n h e r i t a n c e包含有額外指針。但當虛繼承時,顯示出V P T R和兩個額外

指針被加了進去。到了多重繼承執行時,對象顯示出它擁有五個額外指針(然而,指針之一可
能是針對第二個多重繼承子對象的V P T R 。)
    這種奇怪的狀況能夠經過探究咱們的特殊實現和考查成員選擇的彙編語言,以準確地肯定
這些額外字節是爲何而設計的以及用多重繼承成員選擇的開銷有多大 [ 1 ] 。總之,虛擬多重繼

承不過是權益之計,在重視效率的狀況下,應該保守地(或避免)使用它。

16.6    向上映射

    不管是經過建立成員對象仍是經過繼承的方式,當咱們把一個類的子對象嵌入一個新類中
時,編譯器會把每個子對象置於新對象中。固然,每個子對象都有本身的 t h i s指針,在處
理成員對象的時候能夠萬事俱簡。可是隻要引入多重繼承,一個有趣的現象就會出現:因爲對
象在向上映射期間出現多個類,於是對象存在多個t h i s指針。下面就是這種狀況的例子:

   [1] 看Jan Gray C++Under the Hood, a chapter in Black Belt C++(edited by Bruce Eckel, M& T press, 1995) 。

----------------------- Page 348-----------------------

  348        C + +編程思想
                                                                       下載

    例子中因爲每一個類的數組字節數都是用十六進制長度來建立的,因此用十六進制數打出的
輸出地址是容易閱讀的。每一個類都有一個打印 t h i s指針的函數,這些類經過多重繼承和組合而
被裝配成類m i,它打印本身和其餘全部子對象的地址,由主程序調用這些打印功能。能夠清楚
地看到,能在一個相同的對象中得到兩個不一樣的 t h i s指針。下面是m i及向上映射到兩個不一樣類

的地址輸出:

    sizeof(mi)=40 hex

    mi this=0x223e

    base1 this=0x223e

----------------------- Page 349-----------------------

                                                 第16章  多重繼承        349
 下載

    base2 this=0x224e

    member1 this=0x225e

    member2 this=0x226e

    base 1 pointer=0x223e

    base 2 pointer=0x224e

    雖然上述輸出根據編譯器的不一樣而有所不一樣,而且在標準 C + +中亦未作詳細說明,但它仍
具備至關的典型性。派生對象的起始地址和它的基類列表中的第一個類的地址是一致的,第二
個基類的地址隨後,接着根據聲明的次序安排成員對象的地址。

    當向b a s e 1和b a s e 2進行映射時,產生的指針表面上是指向同一個對象,而實際上則必然有
不一樣 t h i s指針,只有這樣,固有的起始地址可以被傳給相應子對象的成員函數。倘若當咱們爲
多重繼承的子對象調用一個成員函數時,隱式向上映射就會產生,而只有上述方法才能使程序
正確工做。

持久性

    因爲打算調用與多重繼承對象的子對象相關的成員函數,持久性一般並非一個問題。然

而,倘若成員函數須要知曉對象的真實起始地址,多重繼承就會出現問題。反而言之,多重繼
承有用的一種狀況是擁有持久性。
    局部對象的生命週期由其定義肯定範圍,全局對象的生命週期就是程序的生命週期,持久
對象則存在於程序請求之間,一般它被認爲存在於磁盤上而非存儲器中。面向對象數據庫的定
義就是「一個持久對象集」。

    爲了實現持久性,必須把一個持久對象從磁盤中移入存儲器以便爲其調用函數,稍後在程
序任務完成前還必須把它存入磁盤。把對象存入磁盤涉及四個方面的內容:
    1) 必須把對象在存儲器中的表示轉化成磁盤上的字節序列。
    2) 因爲存儲器中的指針值在下一次程序啓用時已毫無心義,因此必須把它們轉化成有意義
的東西。

    3) 指針指向的東西也必須被存儲和取回。
    4)  當從磁盤到存儲器上重組一個對象時,必須考慮對象中的虛指針。
    將對象從存儲器中轉換到磁盤上的過程(把對象寫入磁盤)稱爲串行化( s e r i a l i z a t i o n);
而把對象恢復重組到存儲器中的過程稱爲反串行化(d e s e r i a l i z a t i o n )。儘管這些過程十分方便,
但因爲處理的開銷過大而使語言不能直接支持它。類庫經常根據增長特定的成員函數以及在新

類上設置該需求而支持串行化和反串行化。(一般每一個新類都有特定的serialize()  函數)持久性
的實現通常不是自動完成的,一般必須對對象進行顯式讀寫。
    1. 基於M I 的持久性
    如今能夠考慮跨越指針問題,考慮建立一個使用多重繼承把持久性裝入簡單對象的類。通
過繼承這個p e r s i s e n c e類連同咱們的新類,咱們能夠自動地建立能讀寫磁盤的類。這聽起來很

好,可是使用多重繼承會引入下例所示的一個缺陷。

----------------------- Page 350-----------------------

     350   C + +編程思想
                                                                        下載

----------------------- Page 351-----------------------

                                                 第16章  多重繼承        351
 下載

    在上面的簡單版本中,persistent::read()  和p e r s i s t e n t : : w r i t e ( ) 函數可獲取t h i s指針並調用輸入
輸出流的read()  和 write() 函數(注意,任意類型的I O流均可使用)。一個更爲複雜的持久性類
可能爲每一個子對象安排虛w r i t e ( )函數。
    到目前爲止,本書涉及的語言特性尚不能使持久性類知道對象的字節長度,因此在構造函
數中插入了一個長度參數。(在第1 8章中,「運行時類型識別」會揭示怎樣找到僅由一個基指針

所給定對象的確切類型,一旦得到確切類型,就能夠使用s i z e o f運算符而求得正確的長度。)
    類d a t a不含有指針或V P T R ,因此向磁盤寫及從磁盤讀運算是安全的。類w d a t a 1在m a i n ( )中
向F 1 . D AT寫入,稍後把數據從文件中取出,沒有什麼問題。然而當把p e r s i s t e n t置於類w d a t a 2 的
繼承列表中的第二項時,p e r s i s t e n t的t h i s指針指向對象的末端,因此讀寫運算的內容會超出對
象的尾部。這樣從磁盤文件中讀取的內容毫無價值並且會對安排在該對象以後的存儲內容形成

破壞。
    在多重繼承中,這種問題是在類必需從子對象的t h i s指針產生實際對象的t h i s指針時發生的。
固然,咱們知道編輯器是根據繼承列表中的類聲明次序而安排對象,因此應把重要的類放置在
列表的首部(假定只有一個重要類),然而該類可能存在於其餘類的繼承層次中,這可能會使
咱們無心識地把該類置於錯誤的位置上。幸運的是,即便使用了多重繼承,在第 1 8章中介紹的

「運行時類型識別」技術也會產生指向實際對象的正確指針。
    2. 改良的持久性
    下面是一個更具實踐化、常用的持久性方法的例子。該例子在基類中建立了具備讀寫
功能的虛函數,當類不斷派生時,它要求每一個新類的建立者能重載這些虛函數。函數的參數是
可讀出或寫入的流對象 [ 1 ] 。做爲類的建立者,咱們知道怎樣對新的部分進行讀寫,有責任建立

正確的函數調用。該例子沒有前面例子的精巧的質量,它要求部分用戶有更多的知識和參加更
多的編碼工做,但該方法並不會因爲提交指針而出錯。

    [1] 有時流只有一個函數,參數包括打算讀仍是寫的信息。

----------------------- Page 352-----------------------

     352   C + +編程思想
                                                                        下載

----------------------- Page 353-----------------------

                                                  第16章  多重繼承               353
下載

----------------------- Page 354-----------------------

  354        C + +編程思想
                                                                       下載

    基類p e r s i s t e n t 的純虛函數在派生類中必須被重載以執行正確的讀寫運算。倘若咱們已經知

道d a t a是持久的,能夠直接繼承它並在那裏重載虛函數,於是可沒必要使用多重繼承。本例的思
想是在於咱們不擁有d a t a的代碼,這些代碼在別處已經建立了,它多是其餘類層次的一部分
(咱們不能控制它的繼承)。然而,爲了使這個方案能正確地工做,咱們必須對下層實現能訪問,
使得它能被存放,因此咱們使用了p r o t e c t e d 。
    類w d a t a 1和w d a t a 2使用了常見的I O流插入器和取出器以便向流對象存儲和從流對象取回

d a t a的保護性數據。在w r i t e ( ) 中,咱們能夠看到每一個浮點數後都加了一個空格,這對讀入數據
的分解是必要的。類c o n g l o m e r a t e不只繼承了d a t a,並且擁有兩個w d a t a 1和w d a t a 2類型的成員對
象及一個字符串指針。另外,由p e r s i s t e n t派生的全部類也包含一個V P T R ,因此該例子揭示了
使用持久性所遇到的一些問題。
    當建立w r i t e ( )和r e a d ( )函數對時,r e a d ( )函數必須對發生在w r i t e ( )期間的東西進行準確鏡像,

因此可經過r e a d ( )抽取磁盤上由w r i t e ( )安置的比特流。這裏的第一個問題是c h a r *,它指向一個
任意長的字符串。對字符串進行計算並在磁盤上存儲其長度,這可以使 r e a d ( ) 函數能正確地分配
存儲容量。
    當擁有的子對象具備r e a d ( )和w r i t e ( )成員函數時,咱們所須要作的是在新的函數 read()  和
w r i t e ( )中調用基類的成員函數。

    它的後面跟着基類直接存儲的成員函數。
    人們在自動持久性方面已不遺餘力。例如,定義類時建立了修改過的預處理器以支持「持
久性」主題。你能夠想出一個實現持久性更好的方法,但上述方法的優勢是它在 C + +實現下工
做,無需特別的語言擴展,相對較健壯。

16.7  避免MI

    在P E R S I S T 2 . C P P 中對多重繼承的使用有一點人爲的因素,它考慮到在項目中一些類代碼

不受程序員控制的狀況。對以上的例子進行細查,能夠看到,經過使用 d a t a類型的成員對象以
及把虛read()  和w r i t e ( )成員放入d a t a或w d a t a 1和w d a t a 2 中而不是置於一個獨立的類中,這樣M I
是能夠避免使用的。語言會包含一些不經常使用的特性,這種特殊性只有在其餘方法困難或者不可
能處理時才使用。當出現是否使用多重繼承的問題時,咱們能夠先問本身兩個問題:
    1) 咱們有必要同時使用兩個類的公共接口嗎,是否可在一個類中用成員函數包含這些接口

呢?
    2) 咱們須要向上映射到兩個基類上嗎?(固然,在咱們有兩個以上的基類被應用。)

----------------------- Page 355-----------------------

                                                 第16章  多重繼承         355
 下載

    若是咱們對以上兩個問題都能以「不」來回答,那麼就能夠避免使用M I。
    須要注意,當類僅僅須要做爲一個成員參數被向上回溯的狀況。在這種狀況下,該類能夠

被嵌入,同時可由新類的自動類型轉化運算符產生一個針對被嵌入對象的引用。當將新類的對
象做爲參數傳給但願以嵌入對象爲參數的函數時,都將發生類型轉換。然而類型轉換不能用於
一般的成員選擇,這時須要繼承。

16.8  修復接口

    使用多重繼承的最好的理由之一是使用控制以外的代碼。假定已經擁有了一個由頭文件和
已經編譯的成員函數組成的庫,可是沒有成員函數的源代碼。該庫是具備虛函數的類層次,它

含有一些使用庫中基類指針的全局函數,這就是說它多態地使用庫對象。如今,假定咱們圍繞
着該庫建立了一個應用程序而且利用基類的多態方式編寫了本身的代碼。
    在隨後的項目開發或維護期間,咱們發現基類接口和供應商所提供的不兼容:咱們所須要
的是某虛函數而可能提供的倒是非虛的,或者對於解決咱們的問題的基本虛函數在接口中根本
不存在。倘若有源代碼則能夠返回去修改,可是咱們沒有,咱們有大量的依賴最初接口的已生

成的代碼,這時多重繼承則是極好的解決方法。
    下面的例子是所得到的庫的頭文件:

    假定庫很大而且有更多的派生類和更大的接口。注意,它包含函數 A ( )和B ( ),以基類指針
爲參數。下面是庫的實現文件:

----------------------- Page 356-----------------------

       356   C + +編程思想
                                                                       下載

    在咱們的項目中,這些源代碼是不能獲得的,而咱們獲得的是已編譯過的 V E N D O R . O B J
或V E N D O R . L I B文件(或系統中相應的等價物)。
    使用該庫會產生問題。首先析構函數不是虛的,這其實是建立者的一個設計錯誤。另外,

----------------------- Page 357-----------------------

                                                 第16章  多重繼承             357
 下載

f ( )也不是虛的,這多是庫的建立者認爲沒有必要。可是咱們會發現基類接口失去了解決前述
問題的必要能力。倘若咱們已經使用已存在的接口(不包含函數A ( )和B ( ),由於它們不受控制)

編制了大量代碼,並且並不打算改變它。
    爲了補救該問題,咱們能夠建立本身的類接口以及從咱們的接口和已存在的類中進行多重
繼承,以便生成一批新類:

----------------------- Page 358-----------------------

  358        C + +編程思想
                                                                      下載

    在m y b a s e (它不使用M I ) 中 , f ( )和析構函數都改爲虛的,並在接口中增長了新的虛函數 g ( ) 。
如今,每個原來的派生類都必須從新建立,採用                       M I 使其攙入到一個新接口中。函數

paste1::v() 和p a s t e 1 : : f ( )僅須要調用該函數原先基類的版本。可是,若是如今在  m a i n ( ) 中對
m y b a s e進行向上映射:

    mybase * mp=plp;// upcast

這樣,全部函數的調用包括d e l e t e都經過多態的m p來完成,一樣對新接口函數g ( )的調用也經過
m p 。下面是程序的輸出:

    原先的庫函數A ( )和B ( )仍然能夠工做(若新的v ( )調用它的基類版本)。析構函數如今是虛
的並表現了正確的行爲。
    雖然這是一個散亂的例子,但它確實能夠在實際中出現,同時很好地說明了多重繼承在何
處是必要的:咱們必須可以向上映射到兩個基類。

16.9  小結

    除C + +以外,其餘O O P語言都不支持多重繼承,這是因爲針對O O P來講C + +是一個「混血」
版,它不能像S m a l l t a l k那樣把類強制安排成一個單一完整的類層次。 C + +支持許多不一樣形式的
繼承樹,有時須要結合兩個或更多的繼承樹接口造成一個新類。

    倘若在類層次中沒有出現「菱形」形狀, M I是至關簡單的,儘管必須解決在基類中相同
的函數標識。倘若出現「菱形」形狀,咱們必須處理因爲引入虛基類致使的子對象重疊問題。

----------------------- Page 359-----------------------

                                                 第16章  多重繼承        359
 下載

這不只增長了混亂並且使底層變得更爲複雜和缺少效率。多重繼承被Zack Urlocker稱爲「九十
年代的g o t o 」,由於它確實像g o t o。在一般編程開發時,應避免使用多重繼承,只是在某一時候

它才變得很是有用。M I是C + +中「次要的」但更爲高級的特性,它被設計用於處理特定的狀況。
若是咱們發現常用它,應該調查一下使用它的緣由,應基於 O c c a m所提出的簡單完美性原
則(O c c a m ’s Razor ) 「我必須向上映射到全部基類嗎」,若是咱們的回答是否認的,則用嵌
入全部基類實例的方法會更容易,而沒必要使用向上映射。

16.10  練習

    1.  本練習會使咱們一步一步地穿過M I 陷阱。建立一個含有單個i n t參數的構造函數和返回

爲v o i d型的無參數成員函數f ( )的基類X 。從X派生出Y和Z,爲Y和Z各建立一個單個i n t參數的構
造函數。經過多重繼承從Y和Z 中派生出A 。生成一個類A 的對象併爲對象調用f ( )。以明顯無二
義性的方式解決這個問題。
    2.  建立一個指向X 的指針p x ,將類型A 的對象的地址在它被建立前賦予p x 。注意虛基類的
使用問題。如今修改X,使得這時沒必要再在A中爲X調用構造函數。

    3.  移去f ( )的明顯無二義性說明,觀察可否經過p x調用f ( )。對其跟蹤以便觀察哪一個函數被調
用。注意這個問題以在一個類層次中調用正確的函數。

----------------------- Page 360-----------------------

                                                                      下載

                     第1 7章  異 常 處 理

    錯誤修復技術的改進是提升代碼健壯性的最有效方法之一。

    可是,大多數程序設計人員在實際設計中每每忽略出錯處理 ,彷佛是在沒有錯誤的狀態下編
程。毫無疑問,出錯處理的繁瑣及錯誤檢查引發的代碼膨脹是致使上述問題的主要緣由。例
如,雖然printf( ) 函數可返回打印參數的個數,可是實際程序設計中沒有人檢查該值。出錯處
理引發的代碼膨脹將不可避免地增長程序閱讀的困難,這對於程序設計人員來講是十分使人
煩惱的。

    C語言中實現出錯處理的方法是將用戶函數與出錯處理程序緊密地結合起來,可是這將造
成出錯處理使用的不方便和難以接受。
    異常處理是C + +語言的一個主要特徵,它提出了出錯處理更加完美的方法。
    1)  出錯處理程序的編寫再也不繁瑣,也不須將出錯處理程序與「一般」代碼緊密結合。在錯
誤有可能出現處寫一些代碼,並在後面的單獨節中加入出錯處理程序。若是程序中屢次調用一

個函數,在程序中加入一個函數出錯處理程序便可。
    2)  錯誤發生是不會被忽略的。若是被調用函數需發送一條出錯信息給調用函數,它可向調
用函數發送一描述出錯信息的對象。若是調用函數沒有捕捉和處理該錯誤信號,在後續時刻該
調用函數將繼續發送描述該出錯信息的對象,直到該出錯信息被捕捉和處理。
    在這一章中咱們將討論C語言的出錯處理方法,討論爲什麼該方法在C語言中不是很理想的,

而且沒法在C + +中使用;而後學習t r y,t h r o w和c a t c h的用法,它們在C + +中支持異常處理。

17.1  C語言的出錯處理

    本書在第8章之前使用C標準庫的assert( )宏做爲出錯處理的方法。第8章之後assert( )被按
照原先的設計目的使用:在開發過程當中,使用它們,完成後用 #define NDEBUG使之失效,以

便推出產品。爲了在運行時檢查錯誤, assert( )被allege( )函數和第8章中引入的宏所取代。通
常咱們會說:「對於出錯處理咱們必須面對複雜的代碼,可是在這個例子中咱們沒必要由此感到
煩惱」。allege( )函數對一些小型程序很方便,對於複雜的大型程序,所編寫的出錯處理程序也
將更加複雜。
    在經過檢查條件咱們能確切地知道作什麼的狀況下,出錯處理就變得十分明確和容易了,

由於咱們經過上下文獲得了全部必要的信息。固然,咱們只是在這一點上處理錯誤。這些都是
十分普通的錯誤,不是這一章的主題。
    若錯誤問題發生時在必定的上下文環境中得不到足夠的信息,則須要從更大的上下文環境
中提取出錯處理信息,下面給出了C語言處理這類狀況的三種典型方法。
    1) 出錯信息可經過函數的返回值得到。若是函數返回值不能用,則可設置一全局錯誤判斷

標誌(標準C語言中errno( )和perror( ) 函數支持這一方法)。正如前文提到的,因爲對每一個函數
調用都進行錯誤檢查,這十分繁瑣並增長了程序的混亂度。程序設計者可能簡單地忽略這些出
錯信息,由於乏味而迷亂的錯誤檢查必須隨着每一個函數調用而出現。另外,來自偶然出現異常
的函數的返回值可能並不反映什麼問題。

----------------------- Page 361-----------------------

                                                 第17章  異 常處理       361
 下載

    2)  可以使用C標準庫中通常不太熟悉的信號處理系統,利用s i g n a l ( )函數(判斷事件發生的類
型)和r a i s e ( )函數(產生事件)。因爲信號產生庫的使用者必須理解和安裝合適的信號處理系統,

因此應緊密結合各信號產生庫,但對於大型項目,不一樣庫之間的信號可能會產生衝突。
    3)  使用C標準庫中非局部的跳轉函數:setjmp( )  和 longjmp( ) 。setjmp( )  函數可在程序中
存儲一典型的正常狀態,若是進入錯誤狀態, longjmp( )可恢復setjmp( )                 函數的設定狀態,並
且狀態被恢復時的存儲地點與錯誤的發生地點緊密聯繫。
    考慮C + +語言的出錯處理方案時會存在另外一個關鍵性問題:因爲 C語言的信號處理技術和

s e t j m p / l o n g j m p函數不能調用析構函數,因此對象不能被正確地清除。因爲對象不能被清除,
它將被保留下來而且將不能再次被存取,因此存在這種問題時其實是不可能有效正確地從異
常狀況中恢復出來。下面的例子將演示s e t j m p / l o n g j m p的這一特色:

    s e t j m p ( )是一個特別的函數,由於若是咱們直接調用它,它就把當前進程狀態的全部相關
信息存放在j m p _ b u f 中,並返回零。這樣,它的行爲象一般的函數。然而,若是使用同一個
j m p _ b u f 調用l o n g j m p ( ),這就象再次從s e t j m p ( )返回,即正確地彈出s e t j m p ( )的後端。這時,返
回值對於l o n g j m p ( )是第二個參數,因此能發現實際上從l o n g j m p ( )中返回了。能夠想象,有多個

----------------------- Page 362-----------------------

  362        C + +編程思想
                                                                      下載

不一樣的j m p _ b u f ,能夠彈出程序的多個不一樣位置的信息。局部 g o t o          (用標號)和這個非局部跳
轉的不一樣在於咱們能經過s e t j m p / l o n g j m p跳轉到任何地方(一些限制不在這裏討論)。

                                                                           [1]
    在C++中的問題是,longjmp()不適用於對象,特別是,當它跳出範圍時它不調用析構函數  。
析構函數調用是必須的,因此這種方法在C++中不可行。

17.2  拋出異常

    若是程序發生異常狀況,而在當前的上下文環境中獲取不到異常處理的足夠信息,咱們可
以建立一包含出錯信息的對象並將該對象拋出當前上下文環境,將錯誤信息發送到更大的上下
文環境中。這稱爲異常拋出。如:

    throw myerror ("something bad happened");

    m y e r r o r是一個普通類,它以字符變量做爲其參數。當進行異常拋出時咱們可以使用任意類
型變量做爲其參數(包括內部類型變量),但更爲經常使用的辦法是建立一個新類用於異常拋出。

    關鍵字t h r o w 的引入引發了一系列重要的相關事件發生。首先是t h r o w調用構造函數建立一
個原執行程序中並不存在的對象。其次,實際上這個對象正是 t h r o w函數的返回值,即便這個
對象的類型不是函數設計的正常返回類型。對於交替返回機制,若是類推太多有可能會陷入困
境,但仍可看做是異常處理的一種簡單方法,可經過拋出一個異常來退出普通做用域並返回一
個值。

    由於異常拋出同常規函數調用的返回地點徹底不一樣,因此返回值同普通函數調用具備很小
的類似性(異常處理器地點與異常拋出地點可能相差很遠)。另外,只有在異常時刻成功建立
的對象才被清除掉。(常規函數調用則不一樣,它使做用域內的全部對象均被清除。)固然,異常
狀況產生的對象自己在適當的地點也被清除。
    另外,咱們可根據要求拋出許多不一樣類型的對象。通常狀況下,對於每種不一樣的錯誤可設

定拋出不一樣類型的對象。採用這樣的方法是爲了存儲對象中的信息和對象的類型,因此別人可
以在更大的上下文環境中考慮如何處理咱們的異常。

17.3  異常捕獲

    若是一個函數拋出一個異常,它必須假定該異常能被捕獲和處理。正如前文所提到的,
容許對一個問題集中在一處解決,而後處理在別處的差錯,這也正是  C + +語言異常處理的一
個優勢。

17.3.1 try塊

    若是在函數內拋出一個異常(或在函數調用時拋出一個異常),將在異常拋出時退出函數。
若是不想在異常拋出時退出函數,可在函數內建立一個特殊塊用於解決實際程序中的問題(和
潛在產生的差錯)。因爲可經過它測試各類函數的調用,因此被稱爲測試塊。測試塊爲普通做
用域,由關鍵字t r y引導:

    try {

      // code that may generate exceptions

    }

  [1] 當咱們運行這個例子時會驚奇地發現—一些C + +編譯器調用longjmp( ) 函數清除堆棧中的對象。這是兼容性

     差的問題。

----------------------- Page 363-----------------------

                                                 第17章  異 常處理       363
 下載

    若是沒有使用異常處理而是經過差錯檢查來探測錯誤,即便屢次調用同一個函數,也不得
不圍繞每一個調用函數重複進行設置和代碼檢測。而使用異常處理時不需作差錯檢查,可將全部

的工做放入測試塊中。這意味着程序不會因爲差錯檢查的引入而變得混亂,從而使得程序更加
容易編寫,其可讀性也大爲改善。

17.3.2 異常處理器

    異常拋出信號發出後,一旦被異常器處理接收到就被銷燬。異常處理器應具有接受任何一
種類型的異常的能力。異常處理器緊隨t r y塊以後,處理的方法由關鍵字c a t c h引導。

    每個c a t c h語句(在異常處理器中)就至關於一個以特殊類型做爲單一參數的小型函數。

異常處理器中標識符(i d 一、id2  等)就如同函數中的一個參數。若是異常拋出給出的異常類型

足以判斷如何進行異常處理,則異常處理器中的標識符可省略。

    異常處理部分必須直接放在測試塊以後。若是一個異常信號被拋出,異常處理器中第一個

參數與異常拋出對象相匹配的函數將捕獲該異常信號,而後進入相應的 c a t c h語句,執行異常

處理程序。c a t c h語句與s w i t c h語句不一樣,它不須要在每一個c a s e語句後加入b r e a k用以中斷後面程

序的執行。

    注意,在測試塊中不一樣的函數的調用可能會產生相同的異常狀況,可是,這時只須要一個

異常處理器。

    • 終止與恢復

    在異常處理原理中含有兩個基本模式:終止與恢復。假設差錯是致命性的,當異常發生後

將沒法返回原程序的正常運行部分,這時必須調用終止模式( C + +支持)結束異常狀態。不管

程序的哪一個部分只要發生異常拋出,就代表程序運行進入了沒法挽救的困境,應結束運行的非

正常狀態,而不該返回異常拋出之處。

    另外一個爲恢復部分。恢復意味着但願異常處理器可以修改狀態,而後再次對錯誤函數進行

檢測,使之在第二次調用時可以成功運行。若是要求程序具備恢復功能,就但願程序在異常處

理後仍能繼續正常執行程序,這樣,異常處理就更象一個函數調用—C ++程序中在須要進行

恢復的地方如何設置狀態(換言之就是使用函數調用,而非異常拋出來解決問題)。另外也可

將測試塊放入w h i l e循環中,以便始終裝入測試塊直到恢復成功獲得滿意的結果。

    過去,程序員們使用的支持恢復性異常處理的操做系統最終被終止性模式所取代,它取消

了恢復性模式。因此雖然恢復性模式初聽起來是十分吸引人的,但在實際運用中卻並不是十分有

效。其中一個緣由多是異常發生與異常處理相距較遠的緣故。要終止相距較遠的異常處理器,

可是因爲異常可能由不少地點產生,因此對於一個大型系統,從異常處跳轉到異常處理器再跳

轉返回,這在概念上是十分困難的。

----------------------- Page 364-----------------------

  364        C + +編程思想
                                                                      下載

17.3.3 異常規格說明

    能夠不向函數使用者給出全部可能拋出的異常,可是這通常被認爲是很是不友好的,由於

這意味着他沒法知道該如何編寫程序來捕獲全部潛在的異常狀況。固然,若是他有源程序,他
可尋找異常拋出的說明,可是庫一般不以源代碼方式提供。C + +語言提供了異常規格說明語法,
咱們以可利用它清晰地告訴使用者函數拋出的異常的類型,這樣使用者就可方便地進行異常處
理。這就是異常規格說明,它存在於函數說明中,位於參數列表以後。
    異常規格說明再次使用了關鍵字t h r o w ,函數的全部潛在異常類型均隨着關鍵字t h r o w而插

入函數說明中。因此函數說明能夠帶有異常說明以下:

    void f ( ) throw ( toobig, toosmall, divzero) ;

而傳統函數聲明:

    void f ( );

意味着函數可能拋出任何一種異常。

    若是是:

    void f ( ) throw ( );

這意味着函數不會有異常拋出。

    爲了獲得好的程序方案和文件,爲了方便函數調用者,每當寫一個有異常拋出的函數時都
應當加入異常規格說明。
    1. unexpected( )
    若是函數實際拋出的異常類型與咱們的異常規格說明不一致,將會產生什麼樣的結果呢?
這時會調用特殊函數unexpected( ) 。

    2. set_unexpected( )
    unexpected( ) 是使用指向函數的指針而實現的,因此咱們可經過改變指針的指向地址來改
變相對應的運算。這些可經過相似於 set_new_handler( ) 的函數set_unexpected( ) 來實現,
set_unexpected( ) 函數可獲取不帶輸入和輸出參數的函數地址和  v o i d 返回值。它還返回
u n e x p e c t e d指針的先前值,這樣咱們可存儲unexpected( ) 函數的原先指針值,並在後面恢復它。

爲了使用set_unexpected( )函數,咱們必須包含頭文件E X C E P T. H 。下面給出一實例展現本章所
討論的各個特色的簡單使用:

----------------------- Page 365-----------------------

                                                 第17章  異 常處理       365
 下載

    做爲異常拋出類,up 和f i t分別被建立。一般異常類均是小型的,但有時它們包含許多額外
信息,這樣異常處理器可經過查詢它們來得到輔助信息。

    f( ) 函數在它的異常規格說明中聲明函數的異常拋出只能是類up  和 f i t,而且函數體的定義
同函數的異常規格說明是一致的。函數g( )               (v e r s i o n 1 )被函數f( )調用,但並不拋出異常,因
此這也是可行的。當函數g( )          (v e r s i o n 1 )被修改之後得g( ) (v e r s i o n 2 ),g( ) (v e r s i o n 2 )還是
f( ) 的調用函數,但其具備異常拋出功能。函數g( )修改之後f( ) 函數具備了新的異常拋出,但最
初建立的f( )函數對於這些卻未加聲明,這樣就違反了異常規格說明。

    my_unexpected( ) 函數能夠沒有輸入或輸出參數, 它是按照定製的unexpected( ) 函數的正
確格式編寫的。它僅僅打出一條有關異常的信息就退出,因此一旦被調用,咱們就能夠觀察到
這條信息。新函數unexpected( )沒必要有返回值(能夠按照這種方法編寫程序,但這是錯誤的)。
然而它卻可拋出另外一個異常(也可以使它拋出同一個異常),或者調用函數exit( )或abort( )。若是
函數unexpected( )拋出一個異常,異常處理器將在異常拋出時開始搜尋u n e x c e p t e d異常。(這種

特色對於u n e x c e p t e d ()來講是獨特的)
    雖然new_handler( ) 函數的指針可爲空,但unexpected( ) 函數的指針卻不能爲空。它的缺省
值指向terminate( ) (後面將會介紹)函數,可是,只要咱們使用異常拋出和異常規格說明,我
們就應該編寫本身的unexpected( ) 函數,用於記錄或者再次拋出異常及拋出新的異常或終止程
序運行。

    在主程序中,爲了對全部的潛在異常進行檢測,測試塊被放入f o r循環中。注意這裏提到的

----------------------- Page 366-----------------------

  366         C + +編程思想
                                                                            下載

實現方法很象前文介紹的恢復模式,將測試塊放入f o r, while, do 或 if  的循環語句中,並利用每
一個異常來試圖消除差錯問題;而後再一次的調用測試塊對潛在異常進行檢測。

    因爲程序中f( )     的函數聲明引入了u p和f i t兩類異常,所以只有該兩類異常可被拋出。由於
f( ) 的函數聲之後要拋出的整型,因此修改後的g( )                   (v e r s i o n 2 )會使得函數my_unexpected( )
被調用。(咱們可以使用任意的異常類型,包括內部類型。)
    函數set_unexcepted( )被調用後,它的返回值可被忽略,但也能夠被保存爲函數指針,並
在隨後用於恢復unexcepted( ) 的原先指針。

17.3.4 更好的異常規格說明

    咱們可能以爲在前面介紹的已存在的異常規格說明規則並不是十分可靠,而且

    void f ( )  ;

應該意味着函數沒有異常拋出,但按照前面的規則這正好相反,它表示可拋出任意類型的異常。

若是程序員要拋出任意類型的異常,咱們可能會想他應該說明以下

    void f ( ) throw (. . .) ;// not in C++

    由於函數聲明應當更加清晰,因此這是一個改進。但不幸的是,我不能老是經過查看程序
代碼來知道函數是否有異常拋出—例如,函數的異常拋出發生在存儲分配過程當中。較爲糟糕
的是因爲調用了在異常處理以前引入的函數而出現非有意的異常拋出。(函數可能與一個新版
本的異常拋出相鏈接)因此採用不明確的描述,如:

    void f ( ) ;

表示有可能有異常拋出,也可能沒有。這種不明確的描述對於避免阻礙程序執行是十分必要
的。

17.3.5 捕獲全部異常

    前面論述過,若是函數沒有異常規格說明,任何類型的異常都有可能被函數拋出。爲了解
決這個問題,應建立一個能捕獲任意類型的異常的處理器。這能夠經過將省略號加入參數列表

(á la C)中來實現這一方案。

    catch (. . . ) {

       cout << "an exception was thrown" <<endl;

    }

    爲了不漏掉異常拋出,可將能捕獲任意異常的處理器放在一系列處理器以後。
    在參數列表中加入省略號可捕獲全部的異常,但使用省略號就不可能有參數,也不可能知
道所接受到的異常爲什麼種類型。

17.3.6 異常的從新拋出

    有時須要從新拋出剛接收到的異常,尤爲是在咱們沒法獲得有關異常的信息而用省略號捕
獲任意的異常時。這些工做經過加入不帶參數的t h r o w就可完成:

    catch (. . .) {

       cout << "an exception was thrown "<<endl;

       t h r o w ;

    }

----------------------- Page 367-----------------------

                                                 第17章  異 常處理        367
 下載

    若是一個c a t c h句子忽略了一個異常,那麼這個異常將進入更高層的上下文環境。因爲每一個
異常拋出的對象是被保留的,因此更高層上下文環境的處理器可從拋出來自這個對象的全部信

息。

17.3.7 未被捕獲的異常

    若是測試塊後面的異常處理器沒有與某一異常相匹配,這時內層對異常的捕獲失敗,異常
將進入更高層的上下文環境中(高層測試塊通常不最早進行異常接收),這個過程一直進行直
到在某個層次異常處理器與該異常相匹配,這時這個異常才被認爲是被捕獲了,進一步的查詢
也將中止。

    假如任意層的處理器都沒有捕獲到這個異常,那麼這個異常就是「未捕獲的」或「未處理
的」。若是已存在的異常在被捕獲以前又有一個新的異常產生將形成異常不能被獲取,最多見
的這種狀況的產生緣由是異常對象的構造函數自身會致使新的異常。
    1. terminate( )
    若是異常未能被捕獲,特殊函數terminate( )將自動被調用。如同函數unexception( )終止函

數同樣,它實際上也是一個指向函數的指針。在 C標準庫中它的缺省值爲指向函數abort( ) 的指
針,abort( )函數能夠不用調用正常的終止函數而直接從程序中退出(這意味着靜態全局函數的
析構函數不用被調用)。
    若是一個異常未被捕獲,析構函數不會被調用,則異常將不會被清除。含有未捕獲的異常
將被認爲是程序錯誤。咱們可將程序(若是有必要,包括main( ) 的全部代碼)封裝在一個測試

塊中,這個測試塊由各異常處理器按序組成,並能夠捕獲任意異常的缺省處理器 (catch(. . .))結
束。若是咱們不將程序按上述方法封裝,將使咱們的程序十分臃腫。一個未能被捕獲的異常可
當作是一個程序錯誤。
    2. set_terminate( )
    咱們能夠使用標準函數set_ terminate( )來安裝本身的終止函數terminate( ),set_ terminate( )

返回被替代的terminate( ) 函數的指針,這樣就可存貯該指針並在須要時進行恢復。定作的終止
函數terminate( ) 必須不含有輸入參數,其返回值爲  v o i d 。另外所安裝的任何終止處理器
terminate( )必須不返回或拋出異常,可是做爲替換將調用一些程序終止函數。在實際中若是函
數terminate( ) 被調用就意味着問題將沒法被恢復。
    如同函數unexpected( )同樣,函數terminate( )的指針不能爲零。

    這兒給出一實例用以展現set_ terminate( ) 的使用。例中set_ terminate( )函數被調用後返回
函數terminate( ) 的原先的指針,存儲該指針併爲之後的恢復作準備,這樣可經過函數t e r m i n a t e (
)爲判斷未捕獲的異常在程序中何處發生提供幫助:

----------------------- Page 368-----------------------

  368        C + +編程思想
                                                                       下載

    o l d _ t e r m i n a t e 的定義初看上去有些使人費解:該語句不只建立了一個指向函數的指針
o l d _ t e r m i n a t e,並且將其初始化爲函數set_ terminate( ) 的返回值。雖然咱們可能比較熟悉在函
數指針後面加分號的定義方法,但例中所給出是另外一種變量並可在定義時進行初始化。
    類b o t c h不只在函數f( ) 內部會拋出異常,並且在它的析構函數內也會拋出異常。從主程序中
可見,這是調用函數terminate( ) 的一種狀況。雖然異常處理器中使用了c a t c h ( . . . )函數,從表面上

看它彷佛能夠捕獲全部的異常,避免函數terminate( ) 的調用,可是當處理一個異常需清除堆棧
中的對象時,在這一過程當中將調用類b o t c h 的析構函數,由此產生了第二個異常,這將迫使函數
terminate( )被調用。所以析構函數中含有異常拋出或引發異常拋出都將是一個設計錯誤。

17.4  清除

    異常處理的部分難度就在於異常拋出時從正常程序流轉入異常處理器中。若是異常拋出時
對象沒有被正確地清除,這一操做將不會頗有效。 C + +的異常處理器能夠保證當咱們離開一個

做用域時,該做用域中全部結構完整的對象的析構函數都將被調用,以清除這些對象。
    這裏給出一個例子,用以演示當對象的構造函數不完整時其析構函數將不被調用,它也用
來展現若是在被建立對象過程當中發生異常拋出時將出現什麼結果,若是 unexpected( ) 函數再次
拋出意外的異常時將出現什麼結果:

----------------------- Page 369-----------------------

                                                  第17章  異 常處理              369
下載

----------------------- Page 370-----------------------

  370        C + +編程思想
                                                                      下載

    類n o i s y可跟蹤對象的建立,因此可經過它跟蹤程序的運行。類 n o i s y 中含有靜態整數變量i
用以記錄建立對象的個數,整數變量 o b j n u m用以記錄特殊對象的個數,字符緩衝器 n a m e用以
保存字符標識符。該緩衝器首先設置爲零,而後把構造函數的參數拷貝給它。(注意這裏用缺
省的字符串參數代表所建立的爲數組元素,因此該構造函數實際上充當了缺省構造函數。)因
爲C標準庫中函數strncpy( )在它的第三個參數指定的字符數出現或零終結符出現時,將終止字
符的複製,因此被複制字符的數確定小於緩衝器的大小,而且最後一個字符始終爲零,所以打
印語句將決不會超出緩衝器。
    構造函數在兩種狀況下會發生異常拋出。第一種狀況是當第五個對象被建立時(這只是爲
了顯示在對象數組建立中發生異常,而不是真正的異常條件),這種異常將拋出一個整數,並
且函數在異常規格說明中已引入了整數類型。第二種狀況固然也是特地設計的,當參數字符串
的第一個字符爲「z 」時將拋出一字符型異常。因爲異常規格說明中不含有字符型,因此這類
異常將調用unexpected( ) 函數。
    函數new 和 d e l e t e可對類進行重載,其功能可見其函數調用。
    函數unexpected_rethrow( )  打印一條信息,而且再次拋出同一個異常。在主程序main( ) 的
第一行中,它充當unexpected( ) 函數被安裝。在測試塊中將建立一些n o i s y對象,可是在對象數
組的建立中有異常拋出,因此對象n 2將不會被建立。這些在程序輸出結果中能夠見到:

----------------------- Page 371-----------------------

                                                 第17章  異 常處理       371
 下載

    程序成功地建立了四個對象數組單元,但在構造第五個對象時發生異常拋出。因爲第五個
對象的構造函數未完成,所以異常在清除對象時只有 1 ~ 4的析構函數被調用。
    全局函數n e w 的一次調用所產生的對象數組的存儲分配是分離的。注意,即便程序中沒有
明確地調用函數d e l e t e ,但異常處理系統仍不可避免地調用d e l e t e函數來釋放存儲單元。只有在
使用規範的n e w函數形式時纔會出現上述狀況。若是使用第 1 2章介紹的語法,異常處理機構將

不會調用d e l e t e函數來清除對象,由於它只適用於清除不是堆結構的存儲區。
    最終對象n 1將被清除,而對象n 2 因爲沒被建立因此也不存在被清除的問題。
    在測試函數unexpected_rethrow( ) 的程序段中,對象n 3 已被建立,對象n 4 的構造函數已開
始建立對象。可是在它建立完成以前已有異常拋出。該異常爲字符型,不存在於函數的異常規
格說明中,因此函數unexpection( )將被調用(在此例中爲函數unexpected_rethrow( ) )。因爲函

數unexpected_rethrow( ) 可拋出全部類型的異常,因此該函數將再次拋出與已知類型徹底相同
的異常。當對象n 4 的構造函數被調用拋出異常後,異常處理器將進行查找並捕獲該異常(在成
功建立的對象n 3被清除以後)。這樣函數unexpected_rethrow( ) 的做用就是接收任意的未加說明
的異常,並做爲已知異常再次拋出;使用這種方法該函數可爲咱們提供一濾波器,用以跟蹤意
外異常的出現並獲取該異常的類型。

17.5  構造函數

    當編寫的程序出現異常時,咱們總會問:「當異常出現時,這能被合理地清除掉嗎?」這
是十分重要的。對於大多數狀況,程序是至關安全的;可是若是構造函數中出現異常,這將產
生問題:若是異常拋出發生在構造函數建立對象時,對象的析構函數將沒法調用其相應的對象。
這意味着在編寫構造函數的程序時必須十分謹慎。
    構造函數進行存儲資源分配時存在廣泛的困難。若是構造函數在運行時有異常拋出,析構

函數將沒法收回這些存儲資源。這些問題大多數發生在未加保護的指針上。例如:

----------------------- Page 372-----------------------

  372        C + +編程思想
                                                                       下載

    輸出是:

    當進入類u s e R e s o u r c e s 的構造函數後,而且類b o n k 的構造函數已成功地完成了對象數組的
建立,而這時,在og::operator new 中拋出一個異常(例如存儲耗盡所產生的異常)。這樣咱們
就意外地在異常處理器中結束程序,而u s e R e s o u r c e s所對應的析構函數未獲得調用。這是正常
的,由於類u s e R e s o u r c e s 的構造函數的所有構造工做沒能所有完成,這就意味着基於堆存儲的
類b o n k的對象也不能被析構。

對象化

    爲了防止上文提到的狀況,應避免對象經過自己的構造函數和析構函數將「不完備的」資
源分配到對象中。利用這種方法,每一個分配就變成了原子的,像一個對象,而且若是失敗,那

----------------------- Page 373-----------------------

                                                   第17章  異 常處理             373
 下載

麼已分配資源的對象也被正確地清除。採用模板是修改上例的一個好方法:

----------------------- Page 374-----------------------

  374        C + +編程思想
                                                                       下載

    不一樣點是使用模板封裝指針並將它送入對象。這些對象的構造函數的調用先於

u s e R e s o u r c e s構造函數的調用,若是這些構造函數的建立操做完成以後發生了異常拋出,與它

們相對應的析構函數被調用。

    模板p w r a p演示了比前面所見更爲經典的異常使用:若是 o p e r a t o r [ ]的參數出界,那麼就創

建一個嵌入類r a n g e E r r o r用於o p e r a t o r [ ]中。由於o p e r a t o r [ ]返回一個引用而不是返回0。(沒有0引

用。)這是一個真實的異常狀況:不知道在當前上下文中該作什麼,也不能返回一個不可能的

值。在此例中,r a n g e E r r o r很簡單並且設想全部必須的信息都在類名中,但咱們也可加入含有

索引值的成員,若是這樣作有用的話。

    如今輸出是:

    對o g的空間存儲分配又拋出一個異常,但此次b o n k對象數組正確地被清除,因此沒有存儲
損耗。

----------------------- Page 375-----------------------

                                                 第17章  異 常處理        375
 下載

17.6  異常匹配

    當一個異常拋出時,異常處理系統會根據所寫的異常處理器順序找到「最近」的異常處理
器,而不會搜尋更多的異常處理器。
    異常匹配並不要求在異常和處理器之間匹配得十分完美。一個對象或一個派生類對象的引
用將與基類處理器匹配(然而倘若處理器針對的是對象而非引用,異常對象在傳遞給處理器時

會被「切片」,這樣不會受到破壞但會丟失全部的派生類型信息)。倘若拋出一個指針,標準指
針轉化處理會被用於匹配異常,但不會有自動的類型轉化將某個異常類型在匹配過程當中轉化爲
另外一個。下面是一個例子:

    儘管咱們可能認爲第一個處理器會使用構造函數轉化,將一個 e x c e p t 1對象轉化成e x c e p t 2
對象,可是系統在異常處理期間將不會執行這樣的轉換,咱們將在e x c e p t 1處終止。
    下面的例子展現基類處理器怎樣捕獲派生類的異常:

----------------------- Page 376-----------------------

  376         C + +編程思想
                                                                               下載

    這裏的異常處理機制,對於第一個處理器老是匹配一個 t r o u b l e對象或從t r o u b l e派生的什麼
事物,因爲第一個處理器捕獲涉及第二和第三處理器的全部異常,因此第二和第三處理器永遠
不被調用。光捕獲派生類異常把基類的通常異常放在末端捕獲更有意義(或者在隨後的下一個

開發週期中引入的派生類)。
    另外,倘若s m a l l和b i g 的對象比t r o u b l e 的大(這經常是真實的,由於一般爲派生類添加成
員),那麼這些對象會被「切片」以適應處理器。固然,在本例中因爲派生類沒有附加成員,
並且在處理器中也沒有參數標識,因此這一點並不重要。一般在處理器中,應該使用引用參數
而非對象以免裁剪掉信息。

17.7   標準異常

    用於C + +類標準庫的一批異常能夠用於咱們本身的程序中。從標準異常類開始會比咱們盡
量本身定義來得快和容易。倘若標準異常類不能知足須要,咱們能夠繼承它並添加本身的特定
內容。下面的表描述了標準異常:

    e x c e p t i o n   是全部標準C + +庫異常的基類。咱們能夠調用w h a t ( ) 以得到其特性的顯示說明

    l o g i c _ e r r o 是由e x c e p t i o n派生的。它報告程序的邏輯錯誤,這些錯誤在程序執行前能夠被檢測到

    r u n t i m e _ e r r o r 是由e x c e p t i o n派生的。它報告程序運行時錯誤,這些錯誤僅在程序運行時能夠被檢

                       測到

    I / O流異常類i o s : : f a i l u r e也由e x c e p t i o n派生,但它沒有進一步的子類:
    下面兩張表中的類均可以按說明使用,也能夠做爲基類去派生咱們本身的更爲特殊的異常

類型。

             由l o g i c _ e r r o r派生的異常

    d o m a i n _ e r r o r 報告違反了前置條件

    i n v a l i d _ a rg u m e n t 指出函數的一個無效參數

    l e n g t h _ e r r o r 指出有一個產生超過N P O S長度的對象的企圖(N P O S :類型size_t  的最大可表現值)

    o u t _ o f _ r a n g e 報告參數越界

    b a d _ c a s t    在運行時類型識別中有一個無效的d y n a m i c _ c a s t表達式(見第1 8章)

    b a d _ t y p e i d 報告在表達式t y p e i d ( * p ) 中有一個空指針P (運行時類型識別的特性見第1 8章)

            由r u n t i m e _ e r r o r派生的異常

    r a n g e _ e r r o r 報告違反了後置條件

    o v e r f l o w _ e r r o r 報告一個算術溢出

    b a d _ a l l o c  報告一個存儲分配錯誤

----------------------- Page 377-----------------------

                                                 第17章  異 常處理        377
 下載

17.8  含有異常的程序設計

    對大多數程序員尤爲是C程序員,在他們的已有的程序設計語言中不能使用異常,需進一
步矯正。下面是一些含有異常的程序設計原則。

17.8.1 什麼時候避免異常

    異常並不能回答所發生的全部問題。實際上若對異常進行鑽牛角尖式的推敲,將會遇到許
多麻煩。下面的段落指出異常不能被保證的狀況。

    1. 異步事件
    標準C的s i g n a l ( )系統以及其餘相似的系統操縱着異步事件:該事件發生在程序控制的範圍
之外,它的發生是程序所不能預計的。因爲異常和它的處理器都在相同的調用棧上,因此異常
不能用來處理異步事件。也就是異常限制在某範圍內,而異步事件必須有徹底獨立的代碼來處
理,這些代碼不是普通程序流的一部分(典型的如中斷服務和事件循環例程)。

    這並非說異步事件不能和異常發生關係。可是,中斷服務處理器都儘量快地工做,然
後返回。在一些定義明確的程序點上,一個異常能夠以基於中斷的方式拋出。
    2. 普通錯誤狀況
    倘若有足夠的信息去處理一個錯誤,這個錯誤就不是一個異常。咱們應該關心當前的上下
文環境,而不該該把異常拋向更大的上下文環境中。一樣,在 C + + 中也不該當爲機器層的事件

拋出異常,如「除零溢出」。能夠認爲這些「異常」可由其餘的機制去處理,如操做系統或硬
件。這樣,C + +異常能夠至關有效,而且它們的使用和程序級的異常條件相互隔離。
    3. 流控制
    一個異常看上去有點象一個交替返回機制,也有點象一個 s w i t c h語句段,咱們可能被它們
吸引,改變了想使用它們的初衷,這是一個很糟糕的想法,一部分緣由是由於異常處理系統比

普通的程序運行缺少效率。異常是一個罕有的事件,因此普通程序不該爲其支付時間,來自非
錯誤條件的其餘什麼地方的異常也會給使用咱們的類或函數的用戶帶來至關的混亂。
    4. 不強迫使用異常
    一些程序至關簡單,如一些實用程序,可能僅僅須要獲取輸入和執行一些加工。若是在這
類程序中試圖分配存儲然而失敗了,或打開一個文件然而失敗了等等,這樣能夠在這類程序中

使用a s s e r t ( ) 以示出錯信息,使用a b o r t ( )終止程序,容許系統清除混亂。可是若是咱們本身努力
去捕獲全部異常,修復系統資源,則是不明智的。從根本上說,倘若咱們沒必要使用異常,咱們
就不要用。
    5. 新異常,老代碼
    另外一種情形出如今對沒有使用異常的已存在的程序進行修改的時候。咱們可能引入一個使

用異常的庫並且想知道是否有必要在程序中修改全部的代碼。假定已經安放了一個可接受的出
錯處理配置,這裏所要作的最明智的事情是圍繞着使用新類  t r y塊的最大程序塊,追加一個
c a t c h (…)和基本出錯信息。咱們能夠追加有必要的更多特定的處理器,並使修改更爲細緻。但
是,在這種狀況下,被迫增長的代碼必須是最小限度的。
    咱們也能夠把咱們的異常生成代碼隔離在t r y塊中,而且編寫一個把當前異常轉換成已存在

的出錯處理方案的處理器。
    建立一個爲其餘人使用的庫,並且無從知曉用戶在遭遇決定性錯誤的狀況下如何反應,這
時考慮異常才真正重要。

----------------------- Page 378-----------------------

  378        C + +編程思想
                                                                      下載

17.8.2 異常的典型使用

    使用異常便於:

    1) 使問題固定下來和從新調用這個(致使異常的)函數。

    2) 把事情修補好而繼續運行,不去重試函數。
    3) 計算一些選擇結果用於代替函數假定產生的結果。

    4) 在當前上下文環境盡其所能而且再把一樣的異常彈向更高的上下文中。
    5) 在當前上下文環境盡其所能而且把一個不一樣的異常彈向更高的上下文中。

    6) 終止程序。

    7) 包裝使用普通錯誤方案的函數(尤爲是C的庫函數),以便產生異常替代。
    8) 簡化,倘若咱們的異常方案建造得過於複雜,使用時會使人懊惱。

    9) 使咱們的庫和程序更安全。這是短時間投資(爲了調試)和長期投資(爲了應用的健壯性)
問題。

    1. 隨時使用異常規格說明

    異常的規格說明像一個函數原型:它告訴用戶書寫異常處理代碼以及處理什麼異常。它告
訴編譯器異常可能出如今這個函數中。

    固然,咱們不能老是經過檢查代碼而預見什麼異常會發生在特定的函數中。有時這個特定
函數所調用的函數產生了一個出乎意料的異常,有時一個不會拋出異常的老函數被一個會拋出

異常的新函數替換了,這樣咱們將產生對 u n e x p e c t ( ) 的調用。不管什麼時候,只要使用異常規格說

明或者調用含有異常的函數,都應該建立本身的u n e x p e c t e d ( ) 函數,該函數記錄信息並且從新
拋出一樣的異常。

    2. 起始於標準異常
    在建立咱們本身的異常前應檢查標準C + +異常庫。倘若標準異常正合所需,則這樣會使我

們的用戶更易於理解和處理。

    倘若所須要的異常類型不是標準庫的一部分,則儘可能從某個已存在標準 e x p e c t i o n中派生形
成。倘若在e x p e c t i o n的類接口中老是存在w h a t ( )函數的指望定義,這會使用戶受益不淺。

    3. 套裝咱們本身的異常
    若是爲咱們的特定類建立異常,在咱們的類中套裝異常類是一個很好的主意,這爲讀者提

供了一個清晰的消息—這些異常僅爲咱們的類所使用。另外,它可防止命名域混亂。

    4. 使用異常層次
    異常層次爲不一樣類型的重要錯誤的分類提供了一個有價值的方法,這些錯誤可能會與咱們

的類或庫衝突。該層次可爲用戶提供有幫助的信息,幫助他們組織本身的代碼,讓他們能夠選
擇是忽略全部異常的特定類型仍是正確地捕獲基類類型。並且在之後,任何異常可經過對相同

基類的繼承而追加,而不會被迫改寫全部的已生成代碼—基類處理器將捕獲新的異常。

    固然,標準C + +異常是一個異常層次的優秀例子,經過使用可進一步加強和豐富它。
    5. 多重繼承

    咱們會記得,在第1 5章中,多重繼承最必要作的地方就是須要把一個指向對象的指針向上
映射到兩個不一樣的基類,也就是須要兩個基類的多態行爲的地方。這樣,異常層次對於多重繼

承是有用的,由於多重繼承異常類的任一根的基類處理器均可處理異常。

    6. 用「引用」而非「值」去捕獲
    若是拋出一個派生類對象並且該對象被基類的對象處理器經過值捕獲到,對象會被「切片」,

----------------------- Page 379-----------------------

                                                 第17章  異 常處理       379
 下載

這就是說,隨着向基類對象的傳遞,派生類元素會依次被割下,直到傳遞完成。這樣的偶然性

並非所要的,由於對象的行爲像基類而不象它原本就是的派生類對象(實際就是「切片」以
前)。下面是一個例子:

    輸出爲

    當對象經過值被捕獲時,由於它被轉化成一個b a s e對象( 由構造函數完成),並且在全部的情
況下表現出b a s e對象的行爲;然而當對象經過引用被捕獲時,僅僅地址被傳遞而對象不會被切
片,因此它的行爲反映了它處於派生中的真實狀況。
    雖然也能夠拋出和捕獲指針,但這樣作會引入更多的耦合——拋出器和捕獲器必須爲怎樣
分配和清理異常對象而達成一致。這是一個問題,由於異常自己可能會因爲堆的耗盡而產生。

若是拋出異常對象,異常處理系統會關注全部的存儲。

----------------------- Page 380-----------------------

  380        C + +編程思想
                                                                      下載

    7. 在構造函數中拋出異常
    因爲構造函數沒有返回值,所以在先前咱們能夠有兩個選擇以報告在構造期間的錯誤:

    1) 設置一個非局部標誌而且但願用戶檢查它。
    2) 返回一個不徹底被建立的對象而且但願用戶檢查它。
    這是一個嚴重的問題,由於 C程序員必須依賴一個隱含的保證:對象老是成功地被建立,
這在類型如此粗糙的C中是不合理的。可是在C + +程序中,構造失敗後繼續執行是註定的災難,
因而構造函數成爲拋出異常最重要的地方之一。如今有一個安全有效的方法去處理構造函數錯

誤。然而咱們還必須把注意力集中在對象內部的指針上和構造函數異常拋出時的清除方法上。
    8. 不要在析構函數中致使異常
    因爲析構函數會在拋出其餘異常時被調用,因此永遠不要打算在析構函數中拋出一個異常,
或者經過執行在析構函數中的相同動做致使其餘異常的拋出。若是這些發生了,這意味着在已
存在的異常到達引發捕獲以前拋出了一個新的異常,這會致使對t e r m i n a t e ( )的調用。

    這裏的意思是:倘若調用一個析構函數中的任何函數都有可能會拋出異常,這些調用應該
寫在析構函數中的一個t r y塊中,並且析構函數必須本身處理全部自身的異常。這裏的異常都不
應逃離析構函數。
    9. 避免無保護的指針
    請看第1 7 . 5 . 1節中的W R A P P E D . C P P程序,倘若資源分配給無保護的指針,那麼意味着在

構造函數中存在一個缺點。因爲該指針不擁有析構函數,因此當在構造函數中拋出異常時那些
資源將不能被釋放。

17.9  開銷

    爲了使用新特性必然有所開銷。當異常被拋出時有至關的運行時間方面的開銷,這就是從

來不想把異經常使用於普通流控制的一部分的緣由,而無論它多麼使人心動。異常的發生應當是很
少的,因此開銷彙集在異常上而不是在普通的執行代碼上。設計異常處理的重要目標之一是:
在異常處理實現中,當異常不發生時應不影響運行速度。這就是說,只要不拋出異常,代碼的
運行速度如同沒有加載異常處理時同樣。不管與否,異常處理都依賴於使用的特定編譯器。
    異常處理也會引出額外信息,這些信息被編譯器置於棧上。

    除了能做爲特定的「異常範圍」  (它可能偏偏是全局範圍)的對象傳進送出外,異常對
象能夠像其餘對象同樣被正確地在周圍傳遞。當異常處理器工做完成時,異常對象也被相應地
銷燬。

17.10  小結

    錯誤恢復是和咱們編寫的每一個程序相關的基本原則,在 C + +中尤爲重要,建立程序組件爲
其餘人重用是開發的目標之一。爲了建立一個穩固系統,必須使每一個組件具備健壯性。
    C + +中異常處理的目標是簡化大型可靠程序的建立,使用盡量少的代碼,使應用中沒有
不受控制的錯誤而使咱們更加自信。這幾乎不損害性能,而且對其餘代碼的影響很小。

    基本異常不特別難學,咱們應該在程序中儘可能地使用它們。異常是能給咱們提供即時而顯
著的好處的特性之一。

17.11  練習

    1) 建立一個含有可拋出異常的成員函數的類。在該類中,建立一個被嵌套的類用做一個異

----------------------- Page 381-----------------------

                                                 第17章  異 常處理        381
 下載

常對象,它帶有一個c h a r *參數,該參數表示一個描述型字符串。建立一個可拋出該異常的成
員函數。(標明函數的異常規格說明)書寫一個t r y塊使它能調用該函數而且捕獲異常,以打印

描述型字符串的方式處理該異常。
    2) 重寫第1 2章中的s t a s h類以便爲o p e r a t o r [ ]拋出o u t - o f - r a n g e異常。
    3) 寫一個通常的m a i n ( ) ,它可取走全部的異常而且報告錯誤。
    4)  建立一個擁有自身運算符n e w的類。該運算符分配1 0個對象,在對第11個對象分配時假
定「存儲耗盡」並拋出一個異常。增長一個靜態函數用於回收存儲。如今,建立一個伴有 t r y塊

和可以調用存儲恢復例程的c a t c h子句的主程序,將這些都放入一個w h i l e循環中,演示異常恢
復和連續執行的情形。
    5) 建立一個可拋出異常的析構函數,編寫代碼以向本身證實這是一個糟糕的想法。該代碼
可展現若是處理器對一個已存在異常施加影響以前一個新異常又拋出了,那麼 t e r m i n a t e ( )會被
調用。

    6)  向咱們本身證實全部的異常對象(被拋出的)都能被正確地銷燬。
    7)  向咱們本身證實倘若咱們在堆上建立一個異常對象而且拋出一個指向該對象的指針,則
它不會被清理掉。
    8) (高級)。使用一個帶有構造函數和拷貝構造函數的類來追蹤異常的建立和傳遞,這些
構造函數和拷貝構造函數顯示它們自身並且儘量地提供關於對象是怎樣建立的信息(就拷貝

構造函數而言,說明建立什麼對象)。創立一個有趣的狀態,拋出咱們的新類型的對象並分析
結果。

----------------------- Page 382-----------------------

                                                                      下載

                 第1 8章  運行時類型識別

    運行時類型識別(Run-time type identification, RT T I)是在咱們只有一個指向基類的指針

或引用時肯定一個對象的準確類型。
    這能夠被看做C + + 的第二大特徵,在咱們茫然不知所措時,這確實是一個頗有用的工具。
通常狀況下,咱們並不須要知道一個類的確切類型,虛函數機制能夠實現那種類型的正確行爲。
可是有些時候,咱們有指向某個對象的基類指針,肯定該對象的準確類型是頗有用的。這些信
息讓咱們更高效地完成一個特定狀況下的操做,防止基類的接口變得很笨拙。大多數的類庫用

一些虛函數來提供運行時的類型信息。當異常處理功能加入到 C + +時,它要求知道有關這個對
象的準確類型信息。下一步是在語言中訪問這些信息,這很容易實現。
    這一章解釋RT T I是幹什麼的以及怎樣使用它。另外,這一章還解釋了C + +新的映射語法是
什麼,它和RT T I很類似。

18.1  例子—shape

    這裏有一個使用多態性的類層次的例子。基類爲 s h a p e,三個派生類分別爲c i r c l e、s q u a r e

和t r i a n g l e;
    下面是一個典型的類繼承關係圖,基類在上,派生類向下生長。面向對象程序設計的通常
目標就是用代碼體管理指向基類的指針,因此若是想增長一個新類來擴充程序(好比從 s h a p e
中派生出r h o m b o i d ),代碼體部分並不受影響。在本例中, s h a p e接口部分的虛函數是d r a w ( ),
其目的就是讓用戶程序員經過一個s h a p e指針來調用d r a w ( ),d r a w ( )在全部的派生類中都被從新

定義。因爲它是一個虛函數,因此即便是用一個  s h a p e ( )型的指針來調用它,它仍然會被正確
調用。
    所以,建立一個特定的對象( c i r c l e、s q u a r e或t r i a n g l e ) ,取它的地址並把它映射到 s h a p e *
(忘掉對象的實際類型),而後在程序的其餘地方用這個匿名指針。繼承關係圖如上所示,因此
這種從多個派生類到基類的映射叫作向上映射。

18.2  什麼是RTTI

    假如在編程中遇到了特殊的問題,而只要咱們知
道了一個通常指針的準確類型它就會迎刃而解,咱們
該怎麼辦?好比,假設容許咱們的用戶將任一形狀變
成紫色來表示加亮。用這種方法,他們能夠發現屏幕
上的全部三角形都被加亮。咱們可能天然地想到用虛                                   圖 18-1

函數,像 Tu r n C o l o r I f Yo u A r e A ( ) ,它容許一些種類顏色的枚舉型參數和  s h a p e : : c i r c l e 、
s h a p e : : s q u a r e或s h a p e : : t r i a n g l e參數。
    爲了解決這種問題,多數類庫設計者把虛函數放在基類中,使運行時返回特定對象的類型
信息。咱們可能見過一些名字爲 i s A ( )和type Of()        之類的成員函數,這些就是開發商定義的
RT T I函數。使用這些函數,當處理一個對象列表時就能夠說:「若是這個對象是t r i a n g l e類的,

----------------------- Page 383-----------------------

                                             第18章 運行時類型識別          383
 下載

就把它變成紫色。」
    當C + +中引進異常處理時,它的實現要求把一些運行時間類型信息放在虛函數表中。這意

味着只要對語言做一點小小的擴充,程序員就能得到有關一個對象的運行時間類型信息。全部
的開發商都在本身的類庫中加入了RT T I,因此它已包含在C + +語言中。
    RT T I與異常同樣,依賴駐留在虛函數表中的類型信息。若是試圖在一個沒有虛函數的類上
用RT T I,就得不到預期的結果。

RTTI的兩種使用方法

    使用RT T I有兩種不一樣的方法。第一種就像s i z e o f ( ),由於它看上就像一個函數。但實際上它是

由編譯器實現的。t y p e i d ( )帶有一個參數,它能夠是一個對象引用或指針,返回全局t y p e i n f o類的
常量對象的一個引用。能夠用運算符「= = 」和「!=」來互相比較這些對象 。也能夠用n a m e ( )來
得到類型的名稱。注意,若是給t y p e i d ( )傳遞一個s h a p e *型參數,它會認爲類型爲s h a p e *,因此如
果想知道一個指針所指對象的精確類型,咱們必須逆向引用這個指針。好比,s是個s h a p e * ,

    cout << typeid(*s).name()<<endl;

    將顯示出s所指向的對象類型。
    也能夠用b e f o r e ( t y p e i n f o & )查詢一個t y p e i n f o對象是否在另外一個t y p e i n f o對象的前面(以定
義實現的排列順序),它將返回t r u e或f a l s e。若是寫:

    if(typeid(me).before(typeid(you))) //...

那麼表示咱們正在查詢m e在排列順序中是否在y o u以前。

    RT T I的第二個用法叫「安全類型向下映射」。之因此用「向下映射」這個詞也是因爲類繼
承的排列順序。若是映射一個c i r c l e *到s h a p e *叫向上映射的話,那麼將一個s h a p e *映射成一個
c i r c l e *就叫向下映射了。固然一個c i r c l e *也是一個s h a p e *,編譯器容許任意的向上映射,但一
個s h a p e *不必定就是c i r c l e *,因此編譯器在沒有明確的類型映射時並不容許咱們完成一個向下
映射任務。固然能夠用原有的C風格的類型映射或C + +的靜態映射(s t a t i c _ c a s t ,將在本章末介紹)

來強制執行,這等於在說:「我但願它其實是一個c i r c l e *,並且我打算要求它是。」因爲並沒
有明確地知道它其實是c i r c l e,所以這樣作是很危險的。在開發商制定的RT T I中通常的方法
是:建立一個函數來試着將s h a p e *指派爲一個c i r c l e * (在本例中),檢查執行過程當中的數據類型。
若是這個函數返回一個地址,則成功;若是返回n u l l,說明咱們並無一個c i r c l e *對象。
    C + +的RT T I的「安全類型向下映射」就是按照這種「試探映射」函數的格式,但它(很是

合理地)用模板語法來產生這個特殊的動態映射函數(d y n a m i c _ c a s t),因此本例變成:

    動態映射的模板參數是咱們想要該函數建立的數據類型,也就是這個函數的返回值。函數

參數是咱們試圖映射的源數據類型。
    一般只要對一種類型做這種工做(好比將三角型變成紫色),但若是想算出各類s h a p e的數
目,能夠用面下例子的框架:

    固然這是人爲的—咱們可能已經在各個類型中放了一個靜態數據成員並在構造函數中對

----------------------- Page 384-----------------------

       384   C + +編程思想
                                                                        下載

它自增。若是能夠控制類的源代碼並能夠修改它,固然能夠這樣作。下面這個例子用來計算
s h a p e的個數,它用了靜態數據成員和動態映射兩種方法:

----------------------- Page 385-----------------------

                                              第18章 運行時類型識別                 385
下載

----------------------- Page 386-----------------------

  386        C + +編程思想
                                                                       下載

    對於這個例子,兩種方法都是可行的,但靜態數據成員方法只能用於咱們擁有源代碼並已
安裝了靜態數據成員和成員函數時(或者開發商已爲咱們提供了這些),另外RT T I可能在不一樣
的類中用法不一樣。

18.3  語法細節

    本節詳細介紹RT T I的兩種形式是如何運行的以及二者之間的不一樣。

18.3.1 對於內部類型的typeid()

    爲了保持一致性,t y p e i d ( )也能夠運用於內部類型,因此下面的表達式結果爲t r u e :

18.3.2 產生合適的類型名字

    t y p e i d ( )必須在全部的情況下均可以運行,比方說,下面的類中包含了一個嵌套類:

----------------------- Page 387-----------------------

                                             第18章 運行時類型識別           387
 下載

t y p e i n f o : : n a m e ( )成員函數仍是提供了適當的類名,其結果爲o n e : : n e s t e d。

18.3.3 非多態類型

    雖然typeid( )能夠運用於非多態類型(基類中沒有虛函數),但咱們用這種方法得到的信息

是值得懷疑的。假設類層次以下:

若是建立一個派生類對象並向上映射它:

t y p e i d ( )運算符將返回一個結果,但可能不是咱們想要的。由於沒有非多態機制,因此能夠使
用靜態類型信息:

    通常但願RT T I用於多態類。

18.3.4 映射到中間級

    動態映射不只可用來肯定準確的類型,也可用於多層次繼承關係中的中間類型。以下例:

----------------------- Page 388-----------------------

  388        C + +編程思想
                                                                       下載

    因爲多重繼承,問題變得更復雜了。若是咱們建立了一個 m i 2並將向上映射到根類(在這
種狀況下,從兩種可能的根類中選出一種),而後成功地動態映射回派生類m i或m i 2 。
    咱們甚至能夠從一個根類映射到另外一個:

    這能夠成功地映射,由於D 2實際上指向一個m i 2對象,它包含了類型d 1的一個子對象。
    映射到中間級在d y n a m i c _ c a s t與t y p e i d ( )之間產生了一個有趣的差別。t y p e i d ( )老是產生一個

typeinfo  對象的引用來描述一個對象的準確類型,所以它不會給出中間層次的信息。在下面的
表達式中(它的值是t r u e ),t y p e i d ( )並不像d y n a m i c _ c a s t那樣把D 2看做一個指向派生類的指針:

    D 2的類型就是指針的準確類型:

18.3.5 void指針

    運行時類型的識別對一個v o i d型指針不起做用:

v o i d *確實意味着「根本沒有類型信息」。

18.3.6 用模板來使用RTTI

    模板產生許多不一樣類的名字,而有時但願指出有關下面使用的對象是哪一個類的。 RT T I提供

----------------------- Page 389-----------------------

                                             第18章 運行時類型識別          389
 下載

了一個實現此功能的方便方法。下面的例子修改了第1 3章的代碼,它沒有用預處理宏顯示了構
造函數和析構函數調用的順序。

    T Y P E I N F O . H頭文件必須被包含,以便於調用t y p e i d ( )返回的t y p e i n f o對象的任何成員函數。
這個模板用了一個整型常量來區分兩個類,但用類參數也行。在構造函數與析構函數的內部,
RT T I的信息用來產生類的名字並顯示,類X 既用了繼承又用了組合來建立一個類,這裏構造函
數與析構函數調用的順序頗有趣。

    這種技術有助於咱們理解C + +語言是如何工做的。

18.4  引用

    RT T I必須能與引用一塊兒工做。指針與引用存在明顯不一樣,由於引用老是由編譯器逆向引用,
而一個指針的類型或它指向的類型可能要檢測。請看下例:

----------------------- Page 390-----------------------

  390        C + +編程思想
                                                                       下載

    t y p e i d ( )看到的指針類型是基類而不是派生類,而它看到的引用類型則是派生類:

    與此相反,指針指向的類型在t y p e i d ( )看來是派生類而不是基類,而用一個引用的地址時
產生的是基類而不是派生類。

    表達式也能夠用t y p e i d ( )運算符,由於它們也有一個類型:

異常

    對一個引用完成了一個動態映射,其結果還必須被指定到一個引用上。但若是映射失敗則
會產生什麼呢?由於不能有空的引用,因此這裏是拋出一個異常的合適地方。在標準 C + + 中異
常類型爲b a d - c a s t,但在下面的例子中,用一個處理塊來捕獲全部異常:

    失敗的緣由固然是由於D 1實際上並不指向一個X對象,若是這裏沒有拋出一個異常,x r就
會沒有邊界,全部被建立的對象或引用的安全保障均可能被打破。

    若是在調用t y p e i d ( )時試圖去除一個空指針的引用,也會引發一個異常,在標準C + +中,這
個異常叫b a d - t y p e i d :

    這裏能夠在t y p e i d操做以前檢查指針是否爲空來避免異常的產生(不像上面的那個引用的
例子),這是最好的方法。

18.5  多重繼承

    固然,RT T I機制必須適用於任何複雜的多重繼承,包括v i r t u a l基類:

----------------------- Page 391-----------------------

                                             第18章 運行時類型識別          391
 下載

    即便只提供一個v i r t u a l基類指針,t y p e i d ( )也能準確地檢測出實際對象的名字。用動態映射
一樣也會工做得很好,但編譯器將不容許咱們試圖用原來的方法強制映射:

    編譯器知道這不可能正確,因此它要求咱們用動態映射。

18.6  合理使用RTTI

    由於RT T I能夠讓咱們用一個匿名的多態指針來發現類型信息,因此它經常被初學者濫用,
由於它可能在虛函數完成以前就有意義了。
    對於許多來自過程編程背景的人來講,要他們不把程序組織成爲一組 s w i t c h語句是很是困
難的。他們可能會用RT T I完成這些,但這樣會在代碼開發維護階段丟失多態性的很是重要的
價值。C + +的意圖是:儘量地使用虛函數,必要時才使用RT T I。

    固然,要想以咱們所想的那樣使用虛函數,咱們必須控制基類的定義,由於隨着程序的不
斷擴大,有時咱們可能發現基類並無咱們想要的虛函數,若是基類來自類庫或其餘由別人控
制的來源,就能夠用RT T I做爲一種解決辦法:咱們能夠繼承一個新類並加上咱們的成員函數。
在代碼的其餘地方咱們能夠檢測到咱們的新增類型和調用的那個成員函數。這不會破壞多態性
和程序邏輯的可擴展性,由於加一個新類並不要求咱們尋找 s w i t c h語句。固然若是在主程序中

增長新的代碼時用到了這個新類,咱們就必須檢測這個特定類型。
    把一個特徵放在一個基類中可能意味着爲了某個特定類的利益,全部從該類派生出的類都
保留了一些無心義的虛函數的殘留。這使得接口變得不清晰,使那些必須從新定義純虛函數的
人當他們從這個類派生新類時感到很不方便。比方說,假設在第                                1 4章( 1 4 . 6 )節的
W I N D S . C P P程序中,咱們想清除管絃樂隊中全部樂器的無用值。一種方法是在基類 i n s t r u m e n t

----------------------- Page 392-----------------------

  392        C + +編程思想
                                                                       下載

中放一個虛函數C l e a r S p i t v a l v e ( ),但這就會引發混亂,由於它暗示p e r c u s s i o n和e l e c t r o n i c樂器也
有無用值。RT T I提供了一個更合理的方法,由於能夠把函數放在一個合適的特定類中(這裏

是w i n d )。
    最後,RT T I有時能夠解決效率問題。若是代碼用一種好的方法來用多態機制,但結果是這
種通用代碼對某個對象起副作用,使其運行效率低下。咱們能夠用  RT T I將這種類型找出來,
並寫出針對特定狀況的代碼以提升效率。

回顧垃圾再生器例子

    下面是第1 5章垃圾再生例子(trash         recycling )的一個類似的版本,這裏咱們沒有在類層

次中創建類信息,而是採用了RT T I:

----------------------- Page 393-----------------------

                                              第18章 運行時類型識別                 393
下載

----------------------- Page 394-----------------------

  394        C + +編程思想
                                                                       下載

    這個問題的本質是這些垃圾被扔進了一個沒有分類的單一的垃圾箱中,因此特定的類型信
息被丟失了。但以後特定類型信息必須恢復以便對垃圾準確分類,因此 RT T I被用上了。在第
1 5章中,一個RT T I系統插入到類的繼承關係中,但正如咱們在這裏看到的那樣,用C + +預約義
的RT T I更方便。

18.7  RTTI的機制及花費

    典型的RT T I是經過在V TA B L E中放一個額外的指針來實現的。這個指針指向一個描述該特
定類型的t y p e i n f o結構(每一個新類只產生一個t y p e i n f o 的實例),因此t y p e i d ( )表達式的做用實際
上很簡單。V P T R用來取t y p e i n f o 的指針,而後產生一個結果t y p e i n f o結構的一個引用—這是

一個決定性的步驟—咱們已經知道它要花多少時間。
    對於d y n a m i c _ c a s t < 目標* > <源指針> ,多數狀況下是很容易的,先恢復源指針的RT T I信息
再取出目標*的類型RT T I信息,而後調用庫中的一個例程判斷源指針是否與目標 *相同或者是
目標*類型的基類。它可能對返回的指針作了一點小的改動,由於目的指針類可能存在多重繼
承的狀況,而源指針類型並非派生類的第一個基類。在多重繼承時狀況會變得複雜些,由於

----------------------- Page 395-----------------------

                                             第18章 運行時類型識別          395
 下載

一個基類在繼承層次中可能出現一次以上,而且可能有虛基類。
    用於動態映射的庫例程必須檢查一串長長的基類列表,因此動態映射的開銷比 t y p e i d ( )要

大(固然咱們獲得的信息也不一樣,這對於咱們的問題來講可能很關鍵),而且這是非肯定性的,
由於查找一個基類要比查找一個派生類花更多的時間。另外動態映射容許咱們比較任何類型,
不限於在同一個繼承層次中比較兩個類。這使得動態映射調用的庫例程開銷更高了。

18.8  建立咱們本身的RTTI

    若是編譯器還不支持RT T I,能夠在類庫中很容易地創建本身的RT T I。這是頗有意義的事情,
由於在人們發現全部的類庫實際上都有要用到某種形式的RT T I以後纔在C + +引入RT T I 。(在異

常處理被加入到C + +後,人感受「自由」一些了,由於異常處理要求有關類的準確信息)。
    從本質上說,RT T I只要兩個函數就好了,一個用來指明類的準確類型的虛函數,一個取得
基類的指針並將它向下映射成派生類,這個函數必須產生一個指向更加派生類的指針(咱們可
能但願也能處理引用)。有許多方法來實現咱們本身的RT T I,但都要求每一個類有一個惟一的標
識符和一個能產生類型信息的虛函數。下例用了一個叫 d y n a c a s t ( ) 的靜態成員函數,它調用一

個類型信息函數d y n a m i c _ t y p e ( ),這兩個函數都必須在每一個新派生類中從新定義:

----------------------- Page 396-----------------------

     396   C + +編程思想
                                                                        下載

----------------------- Page 397-----------------------

                                             第18章 運行時類型識別          397
 下載

    每一個子類必須建立它本身的t y p e I D,從新定義虛函數d y n a m i c _ t y p e ( )來返回這個t y p e I D,並
定義一個靜態成員調用d y n a c a s t ( ) ,它用一個基類指針做爲參數(或繼承層次中任意層上的一
個指針—在這種狀況下,指針被簡單地向上映射)。
    在從s e c u r i t y派生出的類中,能夠看到每一個類都定義了本身的 t y p e I D,並加到b a s e I D 中。
b a s e I D能夠從派生類中直接訪問,這一點是關鍵所在,由於e n u m必須在編譯時計算出值的大小,

因此採用內聯函數的方法來讀一個私有數據成員的方法不會成功。這是一個須要 p r o t e c t e d成員
的一個典型事例。
    enum baseID爲全部從s e c u r i t y派生出的類創建了一個基本的標識符,這樣若是一個I D值與
已有I D值發生衝突,可能改變一下基值就能夠改變全部的I D值(由於這個例子中並不比較不一樣
的繼承樹,因此不可能發生I D衝突)。在全部的類中,類的I D值都是p r o t e c t e d ,因此它能夠被

派生類訪問,但終端用戶則不能訪問它們。
    這個例子說明了建立RT T I須要處理哪些事情。咱們不只要肯定對象的準確類型,還要能判
斷這個類是否是從咱們要找的類中派生出來的。好比:  m e t a l 是從c o m m o d i t y派生出來的,
c o m m o d i t y有一個叫s p e c i a l ( )的函數,因此若是有一個m e t a l類的對象,就能夠調用s p e c i a l ( )。如
果d y n a m i c _ t y p e ( )只告訴咱們這個對象的準確類型,當咱們問它一個            m e t a l 對象是不是

c o m m o d i t y對象時,它會說「不是」,而這其實是不正確的。因此RT T I還必須能合理地在繼
承層次中將一種類型映射到某一中間類型上和準確類型上。
    d y n a c a s t ( )函數經過調用虛函數d y n a m i c _ t y p e ( )來肯定類型信息。這個函數用一個咱們正試
圖映射到的類的t y p e I D 爲參數。它是一個虛函數,因此函數體在對象的準確類型中。每一個
d y n a m i c _ t y p e ( )函數首先檢查傳入的t y p e I D是否與本身的類型匹配,它檢查是否與基類匹配,

----------------------- Page 398-----------------------

  398        C + +編程思想
                                                                      下載

這隻要調用基類的d y n a m i c _ t y p e ( )函數就好了。就像一個循環函數調用,每一個d y n a m i c _ t y p e ( )都
檢查傳入的參數是否與本身的I D值相等,如不匹配,它調用基類的d y n a m i c _ t y p e ( ),並將它結

果返回。當一直找到繼承樹的根部時,它將返回零,表示沒有匹配的類。
    若是d y n a m i c _ t y p e ( )返回1 ( t r u e ),則指針指向的對象要麼就是咱們要找的類型,要麼是這
個類的派生類,而後d y n a c a s t ( )用s e c u r i t y指針做參數,並把它映射成想要的類型,若是返回值
是f a l s e , ,d y n a c a s t ( ) 返回零,表示映射不成功,用這種方法使它看上去就像           C + + 中的
d y n a m i c _ c a s t運算符同樣。

    C + +的動態映射運算符比上面的例子多一項功能:它能夠比較兩個繼承層次中的類型,這
兩個繼承層次能夠是徹底分開的。這就增長了系統的通用性,使它適用於跨層次體系的類型比
較,固然這也增長了系統的複雜性。
    如今咱們很容易想像出怎樣建立一個使用上面方案並容許更容易轉換成內置 d y n a m i c _ c a s t
運算符的D Y N A M I C - C A S T宏來。

18.9  新的映射語法

    不管何時用類型映射,都是在打破類型系統 [ 1 ],這其實是在告訴編譯器,即便知道

一個對象的確切類型,仍是能夠假定認爲它是另一種類型。這自己就是一種很危險的事情,
也是一個容易發生錯誤的地方。
    不幸的是,每一種類型映射都是不一樣的:它是用括號括起來的目標類型的名字。因此若是
咱們的一段代碼不能正確工做,而咱們知道應該檢查全部的類型映射看它們是不是產生錯誤的

緣由。咱們怎麼保證能夠找出全部的類型映射呢?在一個 C程序中沒法作到這一點。由於 C編
譯器並不老是要求類型映射(它能夠用一個 v o i d指針指向不一樣的類型而沒必要強迫使用映射),
而映射表現不一樣,因此咱們不知道咱們是否是已經找出全部的映射了。
    爲了解決這個問題, C + +用保留字 d y n a m i c _ c a s t (本章第一部分的主題 ) 、c o n s t _ c a s t、
s t a t i c _ c a s t和r e i n t e r p r e t _ c a s t來提供了一個統一的類型映射語法。當須要進行動態映射時,這就

提供了一個解決問題的可能。這意味着那些已有的映射語法已經被重載得太多了,不能再支持
任何其餘的功能了。
    經過使用這些映射來代替原有的(n e w t y p e )語法,咱們能夠在任何程序中很容易地找出
全部的映射。爲了支持已有的代碼,大多數編譯器均可以產生不一樣級別的錯誤或警告,並可由
用戶對錯誤或警告產生選擇打開或關閉。若是把新類型映射的所有錯誤打開的話,就能夠確保

咱們找出項目中全部的類型映射,這使得查找錯誤變得很容易。
    下表指出了四個不一樣形式的映射的含義:

    s t a t i c _ c a s t          爲了「行爲良好」和「行爲較好」而使用的映射,包括一些咱們

                                 可能如今不用的映射(如向上映射和自動類型轉換)

    c o n s t _ c a s t            用於映射常量和變量(c o n s t和v o l a t i l e )

    d y n a m i c _ c a s t        爲了安全類型的向下映射(本章前面已經介紹)

    r e i n t e r p r e t _ c a s t 爲了映射到一個徹底不一樣的意思。這個關鍵詞在咱們須要把類型

                                 映射回原有類型時要用到它。咱們映射到的類型僅僅是爲了故弄

                                 玄虛和其餘目的。這是全部映射中最危險的

    三個新映射將在後面小節中完整地介紹。

  [1] 參看Josée Lajoie「The new cast notation and the bool data type」 ,C + +報告,1 9 9 4年9月,p p . 4 6 - 5 1 .

----------------------- Page 399-----------------------

                                             第18章 運行時類型識別               399
 下載

18.9.1 static_cast

    s t a t i c _ c a s t能夠用於全部良定義轉換。這些包括「安全」轉換與次安全轉換,「安全」轉換
是編譯器容許咱們不用映射就能完成的轉換。次安全轉換也是良定義的。由 s t a t i c _ c a s t覆蓋的
變換的類型包括典型的無映射變換、窄化變換(丟失信息)、用v o i d *的強制變換、隱式類型變
換和類層次的靜態導航。

----------------------- Page 400-----------------------

  400        C + +編程思想
                                                                       下載

    在第(1)段中,咱們看到了在C語言中使用過的各類類型的轉換,用映射的或不用映射的。

從一個i n t到一個l o n g或f l o a t固然不成問題,由於後者能夠存放 i n t包含的所有值,能夠用一個
s t a t i c _ c a s t來使這些轉換變得醒目一些,雖然並非必需要這樣作。
    在第(2 )段中,咱們能夠看到轉換回原有類型的方法。這裏可能丟失部分數據,由於一
個i n t沒有一個l o n g或f l o a t那麼「寬」。所以這些被稱爲「窄化轉換」,編譯器仍能夠完成這些轉
換,但會給咱們一個警告信息。能夠去掉這些警告,並用一個映射指出咱們確實須要映射。

    在C + +中,從v o i d * 向外賦值是不容許的(這與C中不一樣),請看第(3 )段。這樣作是很危
險的,它要求程序員清楚地知道他正在幹什麼。當咱們查找錯誤時, s t a t i c _ c a s t 比老式的標準
映射更容易定位。
    第(4 )段顯示了幾種隱式類型轉換,這些一般是由編譯器自動完成的。它們是自動的和
不要求映射的,用s t a t i c _ c a s t會使它變得醒目,以便於咱們之後須要查找它或明確它們的含義。

    若是在一個類層次中沒有虛函數或者若是咱們有其餘容許咱們安全地向下映射的信息,則
用靜態向下映射比 d y n a m i c _ c a s t 稍微快一些,就像在第(  5 )段中顯示的那樣。另外,
s t a t i c _ c a s t不容許咱們映射到類層次以外,就像傳統的映射同樣,因此它更安全。然而靜態地
導航類層次老是冒險的,所以咱們應該用d y n a m i c _ c a s t,除非特殊狀況。

18.9.2 const_cast

    若是想把一個const      轉換爲非c o n s t ,或把一個v o l a t i l e轉換成一個非v o l a t i l e ,就要用到

c o n s t _ c a s t 。這是能夠用c o n s t _ c a s t的惟一轉換。若是還有其餘的轉換牽涉進來,它必須分開來
指定,不然會有一個編譯錯誤。

----------------------- Page 401-----------------------

                                             第18章 運行時類型識別           401
 下載

    若是用了一個c o n s t對象的地址建立一個指針指向一個 c o n s t,在沒有一個映射時不能把它
賦給一個非c o n s t的指針中。老式風格的映射能夠完成這個,但使用 c o n s t _ c a s t更合適。這一樣
適用於v o l a t i l e 。
    若是想在一個c o n s t成員函數內部改變一個類成員,傳統的方法就是用(X * )t h i s 映射掉常

量性質。如今也能夠使用較好的 c o n s t _ c a s t來映射掉常量性質,但一個更好的方法是使那些特
殊的數據成員成爲m u t a b l e,這樣在類定義時它更清楚,不會在成員函數定義中被隱藏掉,而
且這些成員能夠在c o n s t成員函數中改變。

18.9.3 reinterpret_cast

    這是一種不太安全的類型映射機制,也是最容易引發錯誤的一種。通常狀況下,編譯器都
包含了一組開關,容許咱們強制使用c o n s t _ c a s t和r e i n t e r p r e t _ c a s t ,它們能夠定位那些不安全的

類型映射。
    r e i n t e r p r e t _ c a s t假設一個對象僅僅是一個比特模式,它能夠被看成徹底不一樣的對象對待
(爲了某種模糊的目的)。這是低層處理,在C中已經很很差了。實際上在咱們用它作別的事情
以前,老是要用r e i n t e r p r e t _ c a s t將其映射回原來的類型。

----------------------- Page 402-----------------------

  402        C + +編程思想
                                                                       下載

    類X包含一些數據和一個虛函數,在m a i n ( ) 中,一個X的對象被打印出以顯示出它已被初始
化爲零了,而後它的地址用r e i n t e r p r e t _ c a s t映射爲一個i n t *,假設它是一個i n t *,這個對象被索
引成像一個數組,而且成員1被置爲4 7             (理論上),但在這裏輸出結果 [1] 倒是:

    0 0 0 0 0

    47 0 0 0 0

    很明顯,認爲對象的第一個數據存放在對象的起始地址處這一假定是不安全的。事實上,
這個編譯器把V P T R放在對象的開始處,因此若是用x p [ 0 ]而不是用x p [ 1 ] ,就會使V P T R變得毫無
價值。
    爲了更正這個錯誤,能夠用對象的大小減去數據成員的大小算出 V P T R 的大小,而後對象

的地址被映射爲一個l o n g型(用r e i n t e r p r e t _ c a s t ) 。假定V P T R 是放在對象的開始處的,這樣,
實際數據的開始地址就被計算出來了。結果數字映被射回 i n t * ,如今索引值能夠產生想要的
結果了:

    0 47 0 0 0

  [1] 對於特定的編譯器,結果可能不一樣。

----------------------- Page 403-----------------------

                                             第18章 運行時類型識別           403
 下載

    固然這種方法不值得推薦,並且可移植性差。這是一個 r e i n t e r p r e t _ c a s t指示器能作的事情
之一,但當咱們決定咱們必需要用它時,它是可用的。

18.10   小結

    RT T I是一個很方便的額外特徵,就像蛋糕上加了一層糖衣。雖然通常都是把一個指針向上
映射爲一個基類指針,而後使用基類的接口(經過虛函數),可是偶爾須要知道一個基類指針
指向的對象的確切類型來提升程序的效率,這時若是咱們一籌莫展,就可以使用 RT T I 。由於基
於虛函數的RT T I 已經出如今幾乎全部的類庫中,因此這是一個很是有用的特徵,由於它意味
着:

    1) 咱們並不須要把它建在其餘類庫中。
    2) 咱們不用擔憂它是否將建在其餘庫中。
    3) 在繼承過程當中咱們不須要有額外的編程花費用來管理RT T I配置。
    4) 語法是一致的,咱們不須要爲每一個新庫來從新配置。
    由於RT T I使用很方便,像C + +的多數特徵同樣,因此它可能被濫用,包括那些天真的或有

決心的程序員。最多見的濫用可能來自那些不理解虛函數的程序員,他們用 RT T I去作類型檢
查編碼。C + + 的哲學彷佛是提供強有力的工具並維護類型的完整和防止類型的違規,但咱們如
果有意濫用某一個特徵的話,沒有什麼能夠阻止咱們。有時走點小彎路是獲取經驗的最快途
徑。
    新的類型映射語法在調試階段對咱們有很大的幫助,由於這種類型映射在咱們的類型系統

中開了一個小洞,並容許錯誤流進去。而這種新的語法使咱們更容易定位這些錯誤入口通道。

18.11  練習

    1. 用RT T I幫助程序調試,即打印一個使用了t y p e i d ( ) 的模板的確切名稱。用不一樣的類型將
這個模板實例化,而後看看結果是什麼。
    2. 用RT T I實現本章前面講的Tu r n C o l o r I f Yo u A r e A ( )函數。
    3.  將第1 4章的W I N D 5 . C P P拷貝到一個新的位置,而後修改其中的 i n s t r u m e n t的層次。在

w i n d類中加一個虛函數C l e a r S p i t Va l v e ( )並在全部的w i n d 的派生類中從新定義它。讓t s t a s h 的一
個實例擁有一些i n s t r u m e n t指針,把用n e w建立的各種i n s t r u m e n t對象賦給它們。如今用RT T I巡
視這個包容器,在全部的對象中找類                   w i n d 或它的派生類的對象,爲這些對象調用
C l e a r S p i t Va l v e ( )函數。注意,若是i n s t r u m e n t基類中已經包含了C l e a r S p i t Va l v e ( )函數,它可能會
引發混亂。

----------------------- Page 404-----------------------

                                                                      下載

                      附錄A           其 他 性 能

    在寫這本書時,C + +的標準尚未制定出來。雖然實際上全部這些特徵最終都會被加入到

這個標準中,但有些並無在全部的編譯器中出現。這個附錄中簡單地介紹了一些其餘特徵,
這些應該在編譯器中(或在編譯器的將來版本中)去查找。

A.1  bool、true、false

    實際上每一個人都在用布爾形變量,並且每一個人定義它們的方式都不相同 [ 1 ] ,有些人用枚舉,

另外一些人用t y p e d e f 。 t y p e d e f是一個特殊的問題,由於不能重載它(一個t y p e d e f對於一個i n t還
是一個i n t),也不能用它初始化一個惟一的模板。

    在標準庫中,可能爲b o o l類型建立了一個類,但這也不能很好地工做,由於咱們只能有了
一個自動類型轉換運算符,而沒有解決重載問題。
    對於這樣一個有用的類型,最好的方法是把它建在語言內部。一個 b o o l型有兩種狀態,它
們由內部常量t r u e   (它轉化成一個整數1)和f a l s e (它轉化成一個整數0 )表示,這三個名字都是
關鍵詞,另外對部分語言成分做了修改:

          成  分                     b o o l型的用法

          & & ‖!                   取b o o l型參數,返回b o o l值

          < > <=
                                   產生b o o l型結果
          >= == !=

          if ,for                  條件表達式轉換爲一個b o o l值

          w h i l e , d o

          ? :                      第一個操做數轉換爲b o o l值

    由於已有的許多代碼經常使用一個i n t表示一個標誌,編譯器將一個i n t隱式轉換成一個b o o l型。
    理想狀況下,編譯器將給咱們一個警告,以建議咱們改正這種狀況。
    一種俗話稱「低劣的編程風格」的狀況是用+ +來把一個標誌置爲t r u e 。這是容許的,但不

同意這樣作,這意味着在未來某個時候它會變成不合法的。這個問題與  e n u m 的增運算同樣:
咱們正在作從bool      型轉化成i n t型的隱式類型轉換,增長這個值(可能越出通常的 b o o l值0 ~ 1的
範圍),而後隱式地映射回來。
    在必要時指針也能夠自動轉換成b o o l型的。

A.2  新的包含格式

    隨着C + +的不斷髮展,不一樣的編譯器開發商選擇了不一樣的文件擴展名。另外,不一樣的操做

系統對文件名有不一樣的限制,特別是在文件名的長度上。爲了適應各類不一樣的狀況, C + +標準
採納了一個新的格式,它容許文件名突破那很很差的八個字符的限制,而且取消了擴展名。比
如,包含I O S T R E A M . H就變成了:

    # include <iostream>

    [1] 見「Josée Lajoie,"The new cast notation and the bool data type」C + +報告,1 9 9 4年9月。

----------------------- Page 405-----------------------

                                                  附錄A   其 他 性 能    405
 下載

    解釋器按特定的編譯器和操做系統去實現文件的包含,必要時縮短文件名並加上一個擴展
名。若是咱們想在編譯器開發商支持這一特性以前使用這一風格的包含文件,也能夠把開發商

提供給咱們的頭文件拷貝到不帶擴展名的文件中去。
    全部從標準C 中繼承來的庫在咱們包含它們時仍是用 . h做爲擴展名,這使讀者很容易從我
們使用的C + +庫中識別出C庫。

A.3  標準C++庫

    標準的C + +不只包含了所有的標準C庫(作了一點小的增補和改動,以支持安全類型),而
且還增長了一些它本身的庫。這些庫比標準的 C庫功能更強,從中得到的益處與從 C 向C + +轉

變得到的益處相似。對這些庫的最好的參考文獻就是標準自己(寫這本書時還只能獲得非正式
版),能夠在I n t e r n e t或B B S上找到它們。
    輸入輸出流庫已在本書第6章作了介紹,下面簡要介紹一下C + +中其餘經常使用的庫:
    語言支持:包括繼承到本語言中來的成分,像 < c l i m i t s >和< c f l o a t > 中的實現限制;< n e w >
中的動態內存聲明,例如b a d _ a l l o c (當咱們超出內存範圍時拋出的異常)和s e t _ n e w _ h a n d l e r;

用於RT T I的< t y p e i n f o >頭文件和聲明瞭t e r m i n a t e ( )和unexpected()  函數的< e x c e p t i o n >頭文件。     診斷庫:一些組件,C + +程序可以用以發現和報告錯誤。< s t d e x c e p t >頭文件聲明瞭標準異 常類,< c a s s e r t >與C中A S S E RT. H的做用相同。     通用實用庫:這些組件被標準C + +庫的其餘部分使用,咱們也能夠在咱們的程序中使用它 們。包括運算符! =、> 、< =和> =    &n

相關文章
相關標籤/搜索