作者:未知 文章來源:天極Yesky軟件頻道
Javascript是世界上最受誤解的語言,其實C++何嘗不是。坊間流傳的錯誤的C++學習方法一抓就是一大把。我自己在學習C++的過程中也走了許多彎路,浪費了不少時間。 為什么會存在這么多錯誤認識?原因主要有三個,一是C++語言的細節(jié)太多。二是一些著名的C++書籍總在(不管有意還是無意)暗示語言細節(jié)的重要性和有趣。三是現(xiàn)代C++庫的開發(fā)哲學必須用到一些犄角旮旯的語言細節(jié)(但注意,是庫設計,不是日常編程)。這些共同塑造了C++社群的整體心態(tài)和哲學。 單是第一條還未必能夠成氣候,其它語言的細節(jié)也不少(盡管比起C++起來還是小巫見大巫),就拿Javascript來說,作用域規(guī)則,名字查找,closure,for/in,這些都是細節(jié),而且其中還有違反直覺的。但許多動態(tài)語言的程序員的理念我猜大約是學到哪用到哪罷。但C++就不一樣了,學C++之人有一種類似于被暗示的潛在心態(tài),就是一定要先把語言核心基本上吃透了才能下手寫出漂亮的程序。這首先就錯了。這個意識形成的原因在第二點,C++書籍。市面上的C++書籍不計其數(shù),但有一個共同的缺點,就是講語言細節(jié)的書太多——《C++ gotchas》,《Effective C++》,《More Effective C++》,但無可厚非的是,C++是這樣一門語言:要拿它滿足現(xiàn)代編程理念的需求,尤其是C++庫開發(fā)的需求,還必須得關注語言細節(jié),乃至于在C++中利用語言細節(jié)已經(jīng)成了一門學問。比如C++模板在設計之初根本沒有想到模板元編程這回事,更沒想到C++模板系統(tǒng)是圖靈完備的,這也就導致了《Modern C++ Design》和《C++ Template Metaprogramming》的驚世駭俗。 這些技術的出現(xiàn)為什么驚世駭俗,打個比方,就好比是一塊大家都認為已經(jīng)熟悉無比,再無秘密可言的土地上,突然某天有人挖到原來地下還蘊藏著最豐富的石油。在這之前的C++雖然也有一些細節(jié),但也還算容易掌握,那可是C++程序員們的happy old times,因為C++的一切都一覽無余,everything is figured out。然而《Modern C++ Design》的出世告訴人們,“瞧,還有多少細節(jié)你們沒有掌握啊?!庇谑荂++程序員們久違的激情被重燃起來,奮不顧身的踏入細節(jié)的沼澤中。尤其是,模板編程將C++的細節(jié)進一步挖掘到了極致——我們干嘛關心涉及類對象的隱式轉換的優(yōu)先級高低?看看boost::is_base_of就可以知道有多詭異了。 但最大的問題還在于,對于這些細節(jié)的關注還真有它合適的理由:我們要開發(fā)現(xiàn)代模板庫,要開發(fā)active library,就必須動用模板編程技術,要動用模板編程技術,就必須利用語言的犄角旮旯,enable_if,type_traits,甚至連早就古井無波的C宏也在亂世中重生,看看boost::preprocessor有多詭異就知道了,連C宏的圖靈完備性(預編譯期的)都被挖掘出來了。為什么要做這些?好玩?標榜?都不是,開發(fā)庫的實際需求。但這也正是最大的悲哀了。在boost里面因實際需求而動用語言細節(jié)最終居然能神奇的完成任務的最好教材就是boost::foreach,這個小設施對語言細節(jié)的發(fā)掘達到了驚天地泣鬼神的地步,不信你先試著自己去看看它的源代碼,再看看作者介紹它的文章吧。而boost::typeof也不甘其后——C++語言里面有太多被“發(fā)現(xiàn)”而不是被“發(fā)明”的技術。難道最初無意設置這些語言規(guī)則的家伙們都是Oracles? 因為沒有variadic templates,人們用宏加上缺省模板參數(shù)來實現(xiàn)類似效果。因為沒有concepts,人們用模板加上析構函數(shù)的細節(jié)來完成類似工作。因為沒有typeof,人們用模板元編程和宏加上無盡的細節(jié)來實現(xiàn)目標… C++開發(fā)者們的DIY精神不可謂不強。 然而,如果僅僅是因為要開發(fā)優(yōu)秀的庫,那么涉及這些細節(jié)都還是情有可原的,至少在C++09出現(xiàn)并且編譯器廠商跟上之前,這些都還能說是不得已而為之。但我們廣大的C++程序員呢?大眾是容易被誤導的,我也曾經(jīng)是。以為掌握了更多的語言細節(jié)就更牛,但實際卻是那些語言細節(jié)十有八九是平時編程用都用不到的。C++中眾多的細節(jié)雖然在庫設計者手里面有其用武之地,但普通程序員則根本無需過多關注,尤其是沒有實際動機的關注。一般性的編碼實踐準則,以及基本的編程能力和基本功,乃至基本的程序設計理論以及算法設計。才是真正需要花時間掌握的東西。 學習最佳編碼實踐比學習C++更重要??磧?yōu)秀的代碼也比埋頭用差勁的編碼方式寫垃圾代碼要有效。直接、清晰、明了、KISS地表達意圖比玩編碼花招要重要… 避免去過問任何語言細節(jié),除非必要。這個必要是指在實際編程當中遇到問題,這樣就算需要過問細節(jié),也是最省事的,懶惰者原則嘛。一個掌握了基本的編程理念并有較強學習能力的程序員在用一門陌生的語言編程時就算拿著那本語言的圣經(jīng)從索引翻起也可以編出合格的程序來。十年學會編程不是指對每門語言都得十年,那一輩子才能學幾門語言哪,如果按字母順序學的話一輩子都別指望學到Ruby了;十年學習編程更不是指先把語言特性從粗到細全都吃透才敢下手編程,在實踐中提高才是最重要的。 至于這種摳語言細節(jié)的哲學為何能在社群里面呈野火燎原之勢,就是一個心理學的問題了。想像人們在論壇上討論問題時,一個對語言把握很細致的人肯定能夠得到更多的佩服,而由于論壇上的問題大多是小問題,所以解決實際問題的真正能力并不能得到顯現(xiàn),也就是說,知識型的人能夠得到更多佩服,后者便成為動力和仿效的砝碼。然而真正的編程能力是與語言細節(jié)沒關系的,熟練運用一門語言能夠幫你最佳表達你的意圖,但熟練運用一門語言絕不意味著要把它的邊邊角角全都記住。懂得一些常識,有了編程的基本直覺,遇到一些細節(jié)錯誤的時候再去查書,是最節(jié)省時間的辦法。 C++的書,Bjarne的圣經(jīng)《The C++ Programming Language》是高屋建瓴的。《大規(guī)模C++程序設計》是挺務實的?!禔ccelerated C++》是最佳入門的?!禖++ Templates》是僅作參考的?!禖++ Template Metaprogramming》是精力過剩者可以玩一玩的,普通程序員碰都別碰的。《ISO.IEC C++ Standard 14882》不是拿來讀的。Bjarne最近在做C++的教育,新書是絕對可以期待的。 P.S. 關于如何學習編程,g9的blog上有許多精彩的文章:這里,這里,這里,這里… 實際上,我建議你去把g9老大的blog翻個底朝天 :P 再P.S. 書單?我是遑于給出一個類似《C++初學者必讀》這種書單的。C++的書不計其數(shù),被公認的好書也不勝枚舉。只不過有些書容易給初學者造成一種錯覺,就是“學習C++就應該是這個樣子的”。比如有朋友提到的《高質(zhì)量C/C++編程》,這本書有價值,但不適合初學者,初學者讀這樣的書容易一葉障目不見泰山。實際上,正確的態(tài)度是,細節(jié)是必要的。但細節(jié)是次要的。其實學習編程我覺得應該最先學習如何用偽碼表達思想呢,君不見《Introduction to Algorithm》里面的代碼?《TAOCP》中的代碼?哦,對了它們是自己建立的語言,但這種僅教學目的的語言的目的就是為了避免讓寫程序的人一開始就忘了寫程序是為了完成功能,以為寫程序就是和語言細節(jié)作斗爭了。Bjarne說程序的正確性最重要,boost的編碼標準里面也將正確性列在性能前面。 此外,一旦建立了正確的學習編程的理念,其實什么書(只要不是太垃圾的)都有些用處。都當成參考書,用的時候從目錄或索引翻,基本就對了。 再再P.S. myan老大和g9老大都給出了許多精彩的見解。我不得不再加上一個P.S。具體我就不摘錄了,如果你讀到這里,請務必往下看他們的評論。轉載者別忘了轉載他們的評論:-) 許多朋友都問我同一個問題,到底要不要學習C++。其實這個問題問得很沒有意義?!皩WC++”和“不學C++”這個二分法是沒意義的,為什么?因為這個問題很表面,甚至很浮躁。重要的不是你掌握的語言,而是你掌握的能力,借用myan老大的話,“重要的是這個磨練過程,而不是結果,要的是你粗壯的腿,而不是你身上背的那袋鹽巴?!?。此外學習C++的意義其實真的是醉翁之意不在酒,像C/C++這種系統(tǒng)級語言,在學習的過程中必須要涉及到一些底層知識,如內(nèi)存管理、編譯連接系統(tǒng)、匯編語言、硬件體系結構等等等等知識(注意,這不包括過分犄角旮旯的語言枝節(jié))。這些東西也就是所謂的內(nèi)功了(其實最最重要的內(nèi)功還是長期學習所磨練出來的自學能力)。對此大嘴Joel在《Joel On Software》里面提到的漏洞抽象定律闡述得就非常漂亮。 所以,答案是,讓你成為高手的并不是你掌握什么語言,精通C++未必就能讓你成為高手,不精通C++也未必就能讓你成為低手。我想大家都不會懷疑g9老大如果要抄起C++做一個項目的話會比大多數(shù)自認熟練C++的人要做得漂亮。所以關鍵的不是語言這個表層的東西,而是底下的本質(zhì)矛盾。當然,不是說那就什么語言都不要學了,按照一種曹操的邏輯,“天下語言,唯imperative與declarative耳”。C++是前者里面最復雜的一種,支持最廣泛的編程范式。借用當初數(shù)學系入學大會上一個老師的話,“你數(shù)學都學了,還有什么不能學的呢?”。學語言是一個途徑,如果你把它用來磨練自己,可以。如果你把它用來作為學習系統(tǒng)底層知識的鑰匙,可以。如果你把它用來作為學習如何編寫優(yōu)秀的代碼,如何組織大型的程序,如何進行抽象設計,可以。如果掉書袋,光啃細節(jié),我認為不可以(除非你必須要用到細節(jié),像boost庫的coder們)。 然后再借用一下g9老大的《銀彈和我們的職業(yè)》中的話: 銀彈和我們的職業(yè)發(fā)展有什么相干?很簡單:我們得把時間用于學習解決本質(zhì)困難。新技術給高手帶來方便。菜鳥們卻不用指望被新技術拯救。沿用以前的比喻, 一流的攝影師不會因為相機的更新?lián)Q代而丟掉飯碗,反而可能借助先進技術留下傳世佳作。因為攝影的本質(zhì)困難,還是攝影師的藝術感覺。熱門技術也就等于相機。 不停追新,學習這個框架,那個軟件,好比成天鉆研不同相機的說明書。而熱門技術后的來龍去脈,才好比攝影技術。為什么推出這個框架?它解決了什么其它框架 不能解決的問題?它在哪里適用?它在哪里不適用?它用了什么新的設計?它改進了哪些舊的設計?Why is forever. 和 朋友聊天時提到Steve McConnell的《Professional Software Development》里面引了一個調(diào)查,說軟件開發(fā)技術的半衰期20年。也就是說20年后我們現(xiàn)在知識里一半的東西過時。相當不壞。朋友打趣道:“應 該說20年后IT界一半的技術過時,我們學的過時技術遠遠超過這個比例。具體到某人,很可能5年他就廢了”。話雖悲觀,但可見選擇學習內(nèi)容的重要性。學習 本質(zhì)技藝(技術遲早過時,技藝卻常用長新)還有一好處,就是不用看著自己心愛的技術受到挑戰(zhàn)的時候干嚎。C/C++過時就過時了唄,只要有其它的系統(tǒng)編程 語言。Java倒了就倒了唄,未必我不能用.NET?Ruby曇花一現(xiàn)又如何。如果用得不爽,換到其它動態(tài)語言就是了。J2EE被廢了又怎樣?未必我們就 做不出分布系統(tǒng)了?這里還舉了更多的例子。 一句話,只有人是真正的銀彈。職業(yè)發(fā)展的目標,就是把自己變成銀彈。那時候,你就不再是人,而是人彈。 最后就以我在Bjarne的眾多訪談當中摘錄的一些關于如何學習C++(以及編程)的看法結束吧(沒空逐段翻譯了,只將其中我覺得最重要的幾段譯了一下,當然,其它也很重要,這些段落是在Bjarne的所有采訪稿中摘抄出來的,所以強烈建議都過目一下): I suspect that people think too little about what they want to build, too little about what would make it correct, and too much about efficiency and following fashions of programming style. The key questions are always: what do I want to do? and how do I know that I have done if?. Strategies for testing enters into my concerns from well before I write the firat line of code, and that despite my view that you have to write code very early - rather than wait until a design is complete. 譯:我感覺人們過多關注了所謂“效率”以及跟隨編程風格的潮流,卻嚴重忽視了本不該被忽視的問題,如“我究竟想要構建什么樣的系統(tǒng)”、“怎樣才能使它正確”。最關鍵的問題永遠是:“我究竟想要做什么?”和“如何才能知道我的系統(tǒng)是否已經(jīng)完成了呢?”就拿我來說吧,我會在編寫第一行代碼之前就考慮測試方案,而且這還是在我關于應當早于設計完成之前就進行編碼的觀點的前提之下。 Obviously, C++ is very complex. Obviously, people get lost. However, most peple get lost when they get diverted into becoming language lawyers rather than getting lost when they have a clear idea of what they want to express and simply look at C++ language features to see how to express it. Once you know data absreaction, class hierarchies (object-oriented programming), and parameterization with types (generic programming) in a fairly general way, the C++ language features fall in place. 譯:誠然,C++非常復雜。誠然,人們迷失其中了。然而問題是,大多數(shù)人不是因為首先對自己想要表達什么有了清晰的認識只不過在去C++語言中搜尋合適的語言特性時迷失的,相反,大多數(shù)人是在不覺成為語言律師的路上迷失在細節(jié)的叢林中的。事實是,只需對數(shù)據(jù)抽象、類體系結構(OOP)以及參數(shù)化類型(GP)有一個相當一般層面的了解,C++紛繁的語言特性也就清晰起來了。 注明:以下及其后續(xù)內(nèi)容部分摘自《Standard C++ Bible》,所有程序代碼都在Visual Stdio 6.0中編譯運行,操作系統(tǒng)為WinXP。本文不涉及VC6.0開發(fā)工具的使用,只講解C++語法知識。 C++和C的共同部分就不講解了(如 常量和變量,循環(huán)語句和循環(huán)控制,數(shù)組和指針等,這里面的一些區(qū)別會在本節(jié)和下節(jié)介紹一下),具體可看精華區(qū)->新手上路->C語言入門,本文著重介紹C++的特點,如類、繼承和多重繼承、運算符重載、類模板、C++標準庫、模板庫、等等。 一、C++概述 (一) 發(fā)展歷史 1980年,Bjarne Stroustrup博士開始著手創(chuàng)建一種模擬語言,能夠具有面向對象的程序設計特色。在當時,面向對象編程還是一個比較新的理念,Stroustrup博士并不是從頭開始設計新語言,而是在C語言的基礎上進行創(chuàng)建。這就是C++語言。 1985年,C++開始在外面慢慢流行。經(jīng)過多年的發(fā)展,C++已經(jīng)有了多個版本。為次,ANSI和ISO的聯(lián)合委員會于1989年著手為C++制定標準。1994年2月,該委員會出版了第一份非正式草案,1998年正式推出了C++的國際標準。 (二) C和C++ C++是C的超集,也可以說C是C++的子集,因為C先出現(xiàn)。按常理說,C++編譯器能夠編譯任何C程序,但是C和C++還是有一些小差別。 例如C++增加了C不具有的關鍵字。這些關鍵字能作為函數(shù)和變量的標識符在C程序中使用,盡管C++包含了所有的C,但顯然沒有任何C++編譯器能編譯這樣的C程序。 C程序員可以省略函數(shù)原型,而C++不可以,一個不帶參數(shù)的C函數(shù)原型必須把void寫出來。而C++可以使用空參數(shù)列表。 C++中new和delete是對內(nèi)存分配的運算符,取代了C中的malloc和free。 標準C++中的字符串類取代了C標準C函數(shù)庫 C++中用來做控制態(tài)輸入輸出的iostream類庫替代了標準C中的stdio函數(shù)庫。 C++中的try/catch/throw異常處理機制取代了標準C中的setjmp()和longjmp()函數(shù)。 二、關鍵字和變量 C++相對與C增加了一些關鍵字,如下: typename bool dynamic_cast mutable namespace static_cast using catch explicit new virtual operator false private template volatile const protected this wchar_t const_cast public throw friend true reinterpret_cast try bitor xor_e and_eq compl or_eq not_eq bitand 在C++中還增加了bool型變量和wchar_t型變量: 布爾型變量是有兩種邏輯狀態(tài)的變量,它包含兩個值:真和假。如果在表達式中使用了布爾型變量,那么將根據(jù)變量值的真假而賦予整型值1或0。要把一個整型變量轉換成布爾型變量,如果整型值為0,則其布爾型值為假;反之如果整型值為非0,則其布爾型值為真。布兒型變量在運行時通常用做標志,比如進行邏輯測試以改變程序流程。 #include iostream.h int main() { bool flag; flag=true; if(flag) cout< } C++中還包括wchar_t數(shù)據(jù)類型,wchar_t也是字符類型,但是是那些寬度超過8位的數(shù)據(jù)類型。許多外文字符集所含的數(shù)目超過256個,char字符類型無法完全囊括。wchar_t數(shù)據(jù)類型一般為16位。 標準C++的iostream類庫中包括了可以支持寬字符的類和對象。用wout替代cout即可。 #include iostream.h int main() { wchar_t wc; wc='b'; wout< wout< wout< } 說明一下:某些編譯器無法編譯該程序(不支持該數(shù)據(jù)類型)。 三、強制類型轉換 有時候,根據(jù)表達式的需要,某個數(shù)據(jù)需要被當成另外的數(shù)據(jù)類型來處理,這時,就需要強制編譯器把變量或常數(shù)由聲明時的類型轉換成需要的類型。為此,就要使用強制類型轉換說明,格式如下: int* iptr=(int*) &table; 表達式的前綴(int*)就是傳統(tǒng)C風格的強制類型轉換說明(typecast),又可稱為強制轉換說明(cast)。強制轉換說明告訴編譯器把表達式轉換成指定的類型。有些情況下強制轉換是禁用的,例如不能把一個結構類型轉換成其他任何類型。數(shù)字類型和數(shù)字類型、指針和指針之間可以相互轉換。當然,數(shù)字類型和指針類型也可以相互轉換,但通常認為這樣做是不安全而且也是沒必要的。強制類型轉換可以避免編譯器的警告。 long int el=123; short i=(int) el; float m=34.56; int i=(int) m; 上面兩個都是C風格的強制類型轉換,C++還增加了一種轉換方式,比較一下上面和下面這個書寫方式的不同: long int el=123; short i=int (el); float m=34.56; int i=int (m); 使用強制類型轉換的最大好處就是:禁止編譯器對你故意去做的事發(fā)出警告。但是,利用強制類型轉換說明使得編譯器的類型檢查機制失效,這不是明智的選擇。通常,是不提倡進行強制類型轉換的。除非不可避免,如要調(diào)用malloc()函數(shù)時要用的void型指針轉換成指定類型指針。 四、標準輸入輸出流 在C語言中,輸入輸出是使用語句scanf()和printf()來實現(xiàn)的,而C++中是使用類來實現(xiàn)的。 #include iostream.h main() //C++中main()函數(shù)默認為int型,而C語言中默認為void型。 { int a; cout< cin>>a; /*輸入一個數(shù)值*/ cout< return 0; } cin,cout,endl對象,他們本身并不是C++語言的組成部分。雖然他們已經(jīng)是ANSI標準C++中被定義,但是他們不是語言的內(nèi)在組成部分。在C++中不提供內(nèi)在的輸入輸出運算符,這與其他語言是不同的。輸入和輸出是通過C++類來實現(xiàn)的,cin和cout是這些類的實例,他們是在C++語言的外部實現(xiàn)。 在C++語言中,有了一種新的注釋方法,就是‘//’,在該行//后的所有說明都被編譯器認為是注釋,這種注釋不能換行。C++中仍然保留了傳統(tǒng)C語言的注釋風格/*……*/。 C++也可采用格式化輸出的方法: #include iostream.h int main() { int a; cout< cin>>a; 六、函數(shù)重載 在C++中,允許有相同的函數(shù)名,不過它們的參數(shù)類型不能完全相同,這樣這些函數(shù)就可以相互區(qū)別開來。而這在C語言中是不允許的。 1.參數(shù)個數(shù)不同 #include iostream.h void a(int,int); void a(int); int main() { a(5); a(6,7); return 0; } void a(int i) { cout< } void a(int i,int j) { cout< } 2.參數(shù)格式不同 #include iostream.h void a(int,int); void a(int,float); int main() { a(5,6); a(6,7.0); return 0; } void a(int i,int j) { cout< } void a(int i,float j) { cout< } 七、變量作用域 C++語言中,允許變量定義語句在程序中的任何地方,只要在是使用它之前就可以;而C語言中,必須要在函數(shù)開頭部分。而且C++允許重復定義變量,C語言也是做不到這一點的。看下面的程序: #include iostream.h int a; int main() { cin>>a; for(int i=1;i<=10;i++) //C語言中,不允許在這里定義變量 { static int a=0; //C語言中,同一函數(shù)塊,不允許有同名變量 a+=i; cout<<::a<< < } return 0; } 八、new和delete運算符 在C++語言中,仍然支持malloc()和free()來分配和釋放內(nèi)存,同時增加了new和delete來管理內(nèi)存。 1.為固定大小的數(shù)組分配內(nèi)存 #include iostream.h int main() { int *birthday=new int[3]; birthday[0]=6; birthday[1]=24; birthday[2]=1940; cout< < return 0; } 在刪除數(shù)組時,delete運算符后要有一對方括號。 2.為動態(tài)數(shù)組分配內(nèi)存 #include iostream.h #include stdlib.h int main() { int size; cin>>size; int *array=new int[size]; for(int i=0;i for(i=0;i return 0; } 九、引用型變量 在C++中,引用是一個經(jīng)常使用的概念。引用型變量是其他變量的一個別名,我們可以認為他們只是名字不相同,其他都是相同的。 1.引用是一個別名 C++中的引用是其他變量的別名。聲明一個引用型變量,需要給他一個初始化值,在變量的生存周期內(nèi),該值不會改變。& 運算符定義了一個引用型變量: int a; int& b=a; 先聲明一個名為a的變量,它還有一個別名b。我們可以認為是一個人,有一個真名,一個外號,以后不管是喊他a還是b,都是叫他這個人。同樣,作為變量,以后對這兩個標識符操作都會產(chǎn)生相同的效果。 #include iostream.h int main() { int a=123; int& b=a; cout< a++; cout< b++; cout< return 0; } 2.引用的初始化 和指針不同,引用變量的值不可改變。引用作為真實對象的別名,必須進行初始化,除非滿足下列條件之一: (1) 引用變量被聲明為外部的,它可以在任何地方初始化 (2) 引用變量作為類的成員,在構造函數(shù)里對它進行初始化 (3) 引用變量作為函數(shù)聲明的形參,在函數(shù)調(diào)用時,用調(diào)用者的實參來進行初始化 3.作為函數(shù)形參的引用 引用常常被用作函數(shù)的形參。以引用代替拷貝作為形參的優(yōu)點: 引用避免了傳遞大型數(shù)據(jù)結構帶來的額外開銷 引用無須象指針那樣需要使用*和->等運算符 #include iostream.h void func1(s p); void func2(s& p); struct s { int n; char text[10]; }; int main() { static s str={123,China}; func1(str); func2(str); return 0; } void func1(s p) { cout< void func2(s& p) { cout< 從表面上看,這兩個函數(shù)沒有明顯區(qū)別,不過他們所花的時間卻有很大差異,func2()函數(shù)所用的時間開銷會比func2()函數(shù)少很多。它們還有一個差別,如果程序遞歸func1(),隨著遞歸的深入,會因為棧的耗盡而崩潰,但func2()沒有這樣的擔憂。 4.以引用方式調(diào)用 當函數(shù)把引用作為參數(shù)傳遞給另一個函數(shù)時,被調(diào)用函數(shù)將直接對參數(shù)在調(diào)用者中的拷貝進行操作,而不是產(chǎn)生一個局部的拷貝(傳遞變量本身是這樣的)。這就稱為以引用方式調(diào)用。把參數(shù)的值傳遞到被調(diào)用函數(shù)內(nèi)部的拷貝中則稱為以傳值方式調(diào)用。 #include iostream.h void display(const Date&,const char*); void swapper(Date&,Date&); struct Date { int month,day,year; }; int main() { static Date now={2,23,90}; static Date then={9,10,60}; display(now,Now: ); display(then,Then: ); swapper(now,then); display(now,Now: ); display(then,Then: ); return 0; } void swapper(Date& dt1,Date& dt2) { Date save; save=dt1; dt1=dt2; dt2=save; } void display(const Date& dt,const char *s) { cout< 5.以引用作為返回值 #include iostream.h struct Date { int month,day,year; }; Date birthdays[]= { {12,12,60}; {10,25,85}; {5,20,73}; }; const Date& getdate(int n) { return birthdays[n-1]; } int main() { int dt=1; while(dt!=0) { cout< if(dt>0 && dt<4) { const Date& bd=getdate(dt); cout< } return 0; } 程序都很簡單,就不講解了。 一、類的設計 1.類的聲明 class 類名 { private: //私有 ... public: //公有 ... }; 2.類的成員 一般在C++類中,所有定義的變量和函數(shù)都是類的成員。如果是變量,我們就叫它數(shù)據(jù)成員如果是函數(shù),我們就叫它成員函數(shù)。 3.類成員的可見性 private和public訪問控制符決定了成員的可見性。由一個訪問控制符設定的可訪問狀態(tài)將一直持續(xù)到下一個訪問控制符出現(xiàn),或者類聲明的結束。私有成員僅能被同一個類中的成員函數(shù)訪問,公有成員既可以被同一類中的成員函數(shù)訪問,也可以被其他已經(jīng)實例化的類中函數(shù)訪問。當然,這也有例外的情況,這是以后要討論的友元函數(shù)。 類中默認的數(shù)據(jù)類型是private,結構中的默認類型是public。一般情況下,變量都作為私有成員出現(xiàn),函數(shù)都作為公有成員出現(xiàn)。 類中還有一種訪問控制符protected,叫保護成員,以后再說明。 4.初始化 在聲明一個類的對象時,可以用圓括號()包含一個初始化表。 看下面一個例子: #include iostream.h class Box { private: int height,width,depth; //3個私有數(shù)據(jù)成員 public: Box(int,int,int); ~Box(); int volume(); //成員函數(shù) }; Box::Box(int ht,int wd,int dp) { height=ht; width=wd; depth=dp; } Box::~Box() { //nothing } int Box::volume() { return height*width*depth; } int main() { Box thisbox(3,4,5); //聲明一個類對象并初始化 cout< } 當一個類中沒有private成員和protected成員時,也沒有虛函數(shù),并且不是從其他類中派生出來的,可以用{}來初始化。(以后再講解) 5.內(nèi)聯(lián)函數(shù) 內(nèi)聯(lián)函數(shù)和普通函數(shù)的區(qū)別是:內(nèi)聯(lián)函數(shù)是在編譯過程中展開的。通常內(nèi)聯(lián)函數(shù)必須簡短。定義類的內(nèi)聯(lián)函數(shù)有兩種方法:一種和C語言一樣,在定義函數(shù)時使用關鍵字inline。如: inline int Box::volume() { return height*width*depth; } 還有一種方法就是直接在類聲明的內(nèi)部定義函數(shù)體,而不是僅僅給出一個函數(shù)原型。我們把上面的函數(shù)簡化一下: #include iostream.h class Box { private: int height,width,depth; public: Box(int ht,int wd,int dp) { height=ht; width=wd; depth=dp; } ~Box(); int volume() { return height*width*depth; } }; int main() { Box thisbox(3,4,5); //聲明一個類對象并初始化 cout< } 這樣,兩個函數(shù)都默認為內(nèi)聯(lián)函數(shù)了。 二、構造函數(shù) 什么是構造函數(shù)?通俗的講,在類中,函數(shù)名和類名相同的函數(shù)稱為構造函數(shù)。上面的Box()函數(shù)就是構造函數(shù)。C++允許同名函數(shù),也就允許在一個類中有多個構造函數(shù)。如果一個都沒有,編譯器將為該類產(chǎn)生一個默認的構造函數(shù),這個構造函數(shù)可能會完成一些工作,也可能什么都不做。 絕對不能指定構造函數(shù)的類型,即使是void型都不可以。實際上構造函數(shù)默認為void型。 當一個類的對象進入作用域時,系統(tǒng)會為其數(shù)據(jù)成員分配足夠的內(nèi)存,但是系統(tǒng)不一定將其初始化。和內(nèi)部數(shù)據(jù)類型對象一樣,外部對象的數(shù)據(jù)成員總是初始化為0。局部對象不會被初始化。構造函數(shù)就是被用來進行初始化工作的。當自動類型的類對象離開其作用域時,所站用的內(nèi)存將釋放回系統(tǒng)。 看上面的例子,構造函數(shù)Box()函數(shù)接受三個整型擦黑素,并把他們賦值給立方體對象的數(shù)據(jù)成員。 如果構造函數(shù)沒有參數(shù),那么聲明對象時也不需要括號。 1.使用默認參數(shù)的構造函數(shù) 當在聲明類對象時,如果沒有指定參數(shù),則使用默認參數(shù)來初始化對象。 #include iostream.h class Box { private: int height,width,depth; public: Box(int ht=2,int wd=3,int dp=4) { height=ht; width=wd; depth=dp; } ~Box(); int volume() { return height*width*depth; } }; int main() { Box thisbox(3,4,5); //初始化 Box defaulbox; //使用默認參數(shù) cout< return 0; } 2.默認構造函數(shù) 沒有參數(shù)或者參數(shù)都是默認值的構造函數(shù)稱為默認構造函數(shù)。如果你不提供構造函數(shù),編譯器會自動產(chǎn)生一個公共的默認構造函數(shù),這個構造函數(shù)什么都不做。如果至少提供一個構造函數(shù),則編譯器就不會產(chǎn)生默認構造函數(shù)。 3.重載構造函數(shù) 一個類中可以有多個構造函數(shù)。這些構造函數(shù)必須具有不同的參數(shù)表。在一個類中需要接受不同初始化值時,就需要編寫多個構造函數(shù),但有時候只需要一個不帶初始值的空的Box對象。 #include iostream.h class Box { private: int height,width,depth; public: Box() { //nothing } Box(int ht=2,int wd=3,int dp=4) { height=ht; width=wd; depth=dp; } ~Box(); int volume() { return height*width*depth; } }; int main() { Box thisbox(3,4,5); //初始化 Box otherbox; otherbox=thisbox; cout< } 這兩個構造函數(shù)一個沒有初始化值,一個有。當沒有初始化值時,程序使用默認值,即2,3,4。 但是這樣的程序是不好的。它允許使用初始化過的和沒有初始化過的Box對象,但它沒有考慮當thisbox給otherbox賦值失敗后,volume()該返回什么。較好的方法是,沒有參數(shù)表的構造函數(shù)也把默認值賦值給對象。 class Box { int height,width,depth; public: Box() { height=0;width=0;depth=0; } Box(int ht,int wd,int dp) { height=ht;width=wd;depth=dp; } int volume() { return height*width*depth; } }; 這還不是最好的方法,更好的方法是使用默認參數(shù),根本不需要不帶參數(shù)的構造函數(shù)。 class Box { int height,width,depth; public: Box(int ht=0,int wd=0,int dp=0) { height=ht;width=wd;depth=dp; } int volume() { return height*width*depth; } }; 三、析構函數(shù) 當一個類的對象離開作用域時,析構函數(shù)將被調(diào)用(系統(tǒng)自動調(diào)用)。析構函數(shù)的名字和類名一樣,不過要在前面加上 ~ 。對一個類來說,只能允許一個析構函數(shù),析構函數(shù)不能有參數(shù),并且也沒有返回值。析構函數(shù)的作用是完成一個清理工作,如釋放從堆中分配的內(nèi)存。 我們也可以只給出析構函數(shù)的形式,而不給出起具體函數(shù)體,其效果是一樣的,如上面的例子。但在有些情況下,析構函數(shù)又是必需的。如在類中從堆中分配了內(nèi)存,則必須在析構函數(shù)中釋放 C++的內(nèi)部數(shù)據(jù)類型遵循隱式類型轉換規(guī)則。假設某個表達市中使用了一個短整型變量,而編譯器根據(jù)上下文認為這兒需要是的長整型,則編譯器就會根據(jù)類型轉換規(guī)則自動把它轉換成長整型,這種隱式轉換出現(xiàn)在賦值、參數(shù)傳遞、返回值、初始化和表達式中。我們也可以為類提供相應的轉換規(guī)則。 對一個類建立隱式轉換規(guī)則需要構造一個轉換函數(shù),該函數(shù)作為類的成員,可以把該類的對象和其他數(shù)據(jù)類型的對象進行相互轉換。聲明了轉換函數(shù),就告訴了編譯器,當根據(jù)句法判定需要類型轉換時,就調(diào)用函數(shù)。 有兩種轉換函數(shù)。一種是轉換構造函數(shù);另一種是成員轉換函數(shù)。需要采用哪種轉換函數(shù)取決于轉換的方向。 一、轉換構造函數(shù) 當一個構造函數(shù)僅有一個參數(shù),且該參數(shù)是不同于該類的一個數(shù)據(jù)類型,這樣的構造函數(shù)就叫轉換構造函數(shù)。轉換構造函數(shù)把別的數(shù)據(jù)類型的對象轉換為該類的一個對象。和其他構造函數(shù)一樣,如果聲明類的對象的初始化表同轉換構造函數(shù)的參數(shù)表相匹配,該函數(shù)就會被調(diào)用。當在需要使用該類的地方使用了別的數(shù)據(jù)類型,便宜器就會調(diào)用轉換構造函數(shù)進行轉換。 #include iostream.h #include time.h #include stdio.h class Date { int mo, da, yr; public: Date(time_t); void display(); }; void Date::display() { char year[5]; if(yr<10) sprintf(year,0%d,yr); else sprintf(year,%d,yr); cout< Date::Date(time_t now) { tm* tim=localtime(&now); da=tim->tm_mday; mo=tim->tm_mon+1; yr=tim->tm_year; if(yr>=100) yr-=100; } int main() { time_t now=time(0); Date dt(now); dt.display(); return 0; } 本程序先調(diào)用time()函數(shù)來獲取當前時間,并把它賦給time_t對象;然后程序通過調(diào)用Date類的轉換構造函數(shù)來創(chuàng)建一個Date對象,該對象由time_t對象轉換而來。time_t對象先傳遞給localtime()函數(shù),然后返回一個指向tm結構(time.h文件中聲明)的指針,然后構造函數(shù)把結構中的日月年的數(shù)值拷貝給Date對象的數(shù)據(jù)成員,這就完成了從time_t對象到Date對象的轉換。 二、成員轉換函數(shù) 成員轉換函數(shù)把該類的對象轉換為其他數(shù)據(jù)類型的對象。在成員轉換函數(shù)的聲明中要用到關鍵字operator。這樣聲明一個成員轉換函數(shù): operator aaa(); 在這個例子中,aaa就是要轉換成的數(shù)據(jù)類型的說明符。這里的類型說明符可以是任何合法的C++類型,包括其他的類。如下來定義成員轉換函數(shù); Classname::operator aaa() 類名標識符是聲明了該函數(shù)的類的類型說明符。上面定義的Date類并不能把該類的對象轉換回time_t型變量,但可以把它轉換成一個長整型值,計算從2000年1月1日到現(xiàn)在的天數(shù)。 #include iostream.h class Date { int mo,da,yr; public: Date(int m,int d,int y) {mo=m; da=d; yr=y;} operator int(); //聲明 }; Date::operator int() //定義 { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; int days=yr-2000; days*=365; days+=(yr-2000)/4; for(int i=0;i days+=da; return days; } int main() { Date now(12,24,2003); int since=now; cout< } 三、類的轉換 上面兩個例子都是C++類對象和內(nèi)部數(shù)據(jù)對象之間的相互轉換。也可以定義轉換函數(shù)來實現(xiàn)兩個類對象之間的相互轉換。 #include iostream.h class CustomDate { public: int da, yr; CustomDate(int d=0,int y=0) {da=d; yr=y;} void display() { cout< }; class Date { int mo, da, yr; public: Date(int m=0,int d=0,int y=0) {mo=m; da=d; yr=y;} Date(const CustomDate&); //轉換構造函數(shù) operator CustomDate(); //成員轉換函數(shù) void display() { cout< }; static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; Date::Date(const CustomDate& jd) { yr=jd.yr; da=jd.da; for(mo=0;mo<11;mo++) if(da>dys[mo]) da-=dys[mo]; else break; mo++; } Date::operator CustomDate() { CustomDate cd(0,yr); for(int i=0;i return cd; } int main() { Date dt(12,24,3); CustomDate cd; cd = dt; //調(diào)用成員轉換函數(shù) cd.display(); dt = cd; //調(diào)用轉換構造函數(shù) dt.display(); return 0; } 這個例子中有兩個類CustomDate和Date,CustomDate型日期包含年份和天數(shù)。 這個例子沒有考慮閏年情況。但是在實際構造一個類時,應該考慮到所有問題的可能性。 在Date里中具有兩種轉換函數(shù),這樣,當需要從Date型變?yōu)镃ustomDate型十,可以調(diào)用成員轉換函數(shù);反之可以調(diào)用轉換構造函數(shù)。 不能既在Date類中定義成員轉換函數(shù),又在CustomDate類里定義轉換構造函數(shù)。那樣編譯器在進行轉換時就不知道該調(diào)用哪一個函數(shù),從而出錯。 四、轉換函數(shù)的調(diào)用 C++里調(diào)用轉換函數(shù)有三種形式:第一種是隱式轉換,例如編譯器需要一個Date對象,而程序提供的是CustomDate對象,編譯器會自動調(diào)用合適的轉換函數(shù)。另外兩種都是需要在程序代碼中明確給出的顯式轉換。C++強制類型轉換是一種,還有一種是顯式調(diào)用轉換構造函數(shù)和成員轉換函數(shù)。下面的程序給出了三中轉換形式: #include iostream.h class CustomDate { public: int da, yr; CustomDate(int d=0,int y=0) {da=d; yr=y;} void display() { cout< }; class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate(); }; Date::operator CustomDate() { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0,yr); for(int i=0;i return cd; } int main() { Date dt(11,17,89); CustomDate cd; cd = dt; cd.display(); cd = (CustomDate) dt; cd.display(); cd = CustomDate(dt); cd.display(); return 0; } 五、轉換發(fā)生的情形 上面的幾個例子都是通過不能類型對象之間的相互賦值來調(diào)用轉換函數(shù),還有幾種調(diào)用的可能: 參數(shù)傳遞 初始化 返回值 表達式語句 這些情況下,都有可能調(diào)用轉換函數(shù)。 下面的程序不難理解,就不分析了。 #include iostream.h class CustomDate { public: int da, yr; CustomDate() {} CustomDate(int d,int y) { da=d; yr=y;} void display() { cout< }; class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate(); }; Date::operator CustomDate() { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0,yr); for (int i=0;i return cd; } class Tester { CustomDate cd; public: explicit Tester(CustomDate c) { cd=c; } void display() { cd.display(); } }; void dispdate(CustomDate cd) { cd.display(); } CustomDate rtndate() { Date dt(9,11,1); return dt; } int main() { Date dt(12,24,3); CustomDate cd; cd = dt; cd.display(); dispdate(dt); Tester ts(dt); ts.display(); cd = rtndate(); cd.display(); return 0; } 六、顯式構造函數(shù) 注意上面Tester類的構造函數(shù)前面有一個explicit修飾符。如果不加上這個關鍵字,那么在需要把CustomDate對象轉換成Tester對象時,編譯器會把該函數(shù)當作轉換構造函數(shù)來調(diào)用。但是有時候,并不想把這種只有一個參數(shù)的構造函數(shù)用于轉換目的,而僅僅希望用它來顯式地初始化對象,此時,就需要在構造函數(shù)前加explicit。如果在聲明了Tester對象以后使用了下面的語句將導致一個錯誤: ts=jd; //error 這個錯誤說明,雖然Tester類中有一個以Date型變量為參數(shù)的構造函數(shù),編譯器卻不會把它看作是從Date到Tester的轉換構造函數(shù),因為它的聲明中包含了explicit修飾符。 七、表達式內(nèi)部的轉換 在表達式內(nèi)部,如果發(fā)現(xiàn)某個類型和需要的不一致,就會發(fā)生錯誤。數(shù)字類型的轉換是很簡單,這里就不舉例了。下面的程序是把Date對象轉換成長整型值。 #include iostream.h class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator long(); }; Date::operator long() { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; long days=yr; days*=365; days+=(yr-1900)/4; //從1900年1月1日開始計算 for(int i=0;i return days; } int main() { Date today(12,24,2003); const long ott=123; long sum=ott+today; cout< } 在表達式中,當需要轉換的對象可以轉換成某個數(shù)字類型,或者表達式調(diào)用了作用于某個類的重載運算符時,就會發(fā)生隱式轉換。運算符重載以后再學習。 一、私有數(shù)據(jù)成員的使用 1.取值和賦值成員函數(shù) 面向對象的約定就是保證所有數(shù)據(jù)成員的私有性。一般我們都是通過公有成員函數(shù)來作為公共接口來讀取私有數(shù)據(jù)成員的。某些時候,我們稱這樣的函數(shù)為取值和賦值函數(shù)。 取值函數(shù)的返回值和傳遞給賦值函數(shù)的參數(shù)不必一一匹配所有數(shù)據(jù)成員的類型。 #include iostream.h class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } int getyear() const { return yr; } void setyear(int y) { yr = y; } }; int main() { Date dt(4,1,89); cout< cout< } 上面的例子很簡單,不分析了。要養(yǎng)成這樣的習慣,通過成員函數(shù)來訪問和改變類中的數(shù)據(jù)。這樣有利于軟件的設計和維護。比如,改變Date類內(nèi)部數(shù)據(jù)的形式,但仍然用修改過的getyear()和setyear()來提供訪問接口,那么使用該類就不必修改他們的代碼,僅需要重新編譯程序即可。 2.常量成員函數(shù) 注意上面的程序中getyear()被聲明為常量型,這樣可以保證該成員函數(shù)不會修改調(diào)用他的對象。通過加上const修飾符,可以使訪問對象數(shù)據(jù)的成員函數(shù)僅僅完成不會引起數(shù)據(jù)變動的那些操作。 如果程序聲明某個Date對象為常量的話,那么該對象不得調(diào)用任何非常量型成員函數(shù),不論這些函數(shù)是否真的試圖修改對象的數(shù)據(jù)。只有把那些不會引起數(shù)據(jù)改變的函數(shù)都聲明為常量型,才可以讓常量對象來調(diào)用。 3.改進的成員轉換函數(shù) 下面的程序改進了從Date對象到CustomDate對象的成員轉換函數(shù),用取值和賦值函數(shù)取代了使用公有數(shù)據(jù)成員的做法。(以前的程序代碼在上一帖中) #include iostream.h class CustomDate { int da,yr; public: CustomDate() {} CustomDate(int d,int y) { da=d; yr=y; } void display() const {cout< void setday(int d) { da=d; } }; class Date { int mo,da,yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate() const; }; Date::operator CustomDate() const { static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0,yr); int day=da; for(int i=0;i return cd; } int main() { Date dt(11,17,89); CustomDate cd; cd=dt; cd.display(); return 0; } 注意上面的程序中Date::operator CustomDate()聲明為常量型,因為這個函數(shù)沒有改變調(diào)用它對象的數(shù)據(jù),盡管它修改了一個臨時CustomDate對象并將其作為函數(shù)返回值。 二、友元 前面已經(jīng)說過了,私有數(shù)據(jù)成員不能被類外的其他函數(shù)讀取,但是有時候類會允許一些特殊的函數(shù)直接讀寫其私有數(shù)據(jù)成員。 關鍵字friend可以讓特定的函數(shù)或者別的類的所有成員函數(shù)對私有數(shù)據(jù)成員進行讀寫。這既可以維護數(shù)據(jù)的私有性,有可以保證讓特定的類或函數(shù)能夠直接訪問私有數(shù)據(jù)。 1.友元類 一個類可以聲明另一個類為其友元,這個友元的所有成員函數(shù)都可以讀寫它的私有數(shù)據(jù)。 #include iostream.h class Date; class CustomDate { int da,yr; public: CustomDate(int d=0,int y=0) { da=d; yr=y; } void display() const {cout< }; class Date { int mo,da,yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate(); }; Date::operator CustomDate() { static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0, yr); for (int i=0;i return cd; } int main() { Date dt(11,17,89); CustomDate cd(dt); cd.display(); return 0; } 在上面的程序中,有這樣一句 friend Date; 該語句告訴編譯器,Date類的所有成員函數(shù)有權訪問CustomDate類的私有成員。因為Date類的轉換函數(shù)需要知道CustomDate類的每個數(shù)據(jù)成員,所以真?zhèn)€Date類都被聲明為CustomDate類的友元。 2.隱式構造函數(shù) 上面程序對CustomDate的構造函數(shù)的調(diào)用私有顯示該類需要如下的一個轉換構造函數(shù): CustomDate(Date& dt); 但是唯一的一個構造函數(shù)是:CustomDate(int d=0;int y=0); 這就出現(xiàn)了問題,編譯器要從Date對象構造一個CustomDate對象,但是CustomDate類中并沒有定義這樣的轉換構造函數(shù)。不過Date類中定義了一個成員轉換函數(shù),它可以把Date對象轉換成CustomDate對象。于是編譯器開始搜索CustomDate類,看其是否有一個構造函數(shù),能從一個已存在的CustomDate的對象創(chuàng)建新的CustomDate對象。這種構造函數(shù)叫拷貝構造函數(shù)??截悩嬙旌瘮?shù)也只有一個參數(shù),該參數(shù)是它所屬的類的一個對象,由于CustomDate類中沒有拷貝構造函數(shù),于是編譯器就會產(chǎn)生一個默認的拷貝構造函數(shù),該函數(shù)簡單地把已存在的對象的每個成員拷貝給新對象?,F(xiàn)在我們已經(jīng)知道,編譯器可以把Date對象轉換成CustomDate對象,也可以從已存在的CustomDate對象生成一個新的CustomDate對象。那么上面提出的問題,編譯器就是這樣做的:它首先調(diào)用轉換函數(shù),從Date對象創(chuàng)建一個隱藏的、臨時的、匿名的CustomDate對象,然后用該臨時對象作為參數(shù)調(diào)用默認拷貝構造函數(shù),這就生成了一個新的CustomDate對象。 3.預引用 上面的例子中還有這樣一句 class Date; 這個語句叫做預引用。它告訴編譯器,類Date將在后面定義。編譯器必須知道這個信號,因為CustomDate類中引用了Date類,而Date里也引用了CustomDate類,必須首先聲明其中之一。 使用了預引用后,就可以聲明未定義的類的友元、指針和引用。但是不可以使用那些需要知道預引用的類的定義細節(jié)的語句,如聲明該類的一個實例或者任何對該類成員的引用。 4.顯式友元預引用 也可以不使用預引用,這只要在聲明友元的時候加上關鍵自class就行了。 #include iostream.h class CustomDate { int da,yr; public: CustomDate(int d=0,int y=0) { da=d; yr=y; } void display() const {cout< }; class Date { ... ... }; Date::operator CustomDate() { ... ... } int main() { ... ... } 5.友元函數(shù) 通常,除非真的需要,否則并不需要把整個類都設為另一個類的友元,只需挑出需要訪問當前類私有數(shù)據(jù)成員的成員函數(shù),將它們設置為該類的友元即可。這樣的函數(shù)稱為友元函數(shù)。 下面的程序限制了CustomDate類數(shù)據(jù)成員的訪問,Date類中只有需要這些數(shù)據(jù)的成員函數(shù)才有權讀寫它們。 #include iostream.h class CustomDate; class Date { int mo,da,yr; public: Date(const CustomDate&); void display() const {cout< class CustomDate { int da,yr; public: CustomDate(int d=0,int y=0) { da=d; yr=y; } friend Date::Date(const CustomDate&); }; Date::Date(const CustomDate& cd) { static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; yr=cd.yr; da=cd.da; for(mo=0;mo<11;mo++) if(da>dys[mo]) da-=dys[mo]; else break; mo++; } int main() { Date dt(CustomDate(123, 89)); dt.display(); return 0; } 6.匿名對象 上面main()函數(shù)中Date對象調(diào)用CustomDate類的構造函數(shù)創(chuàng)建了一個匿名CustomDate對象,然后用該對象創(chuàng)建了一個Date對象。這種用法在C++中是經(jīng)常出現(xiàn)的。 7.非類成員的友元函數(shù) 有時候友元函數(shù)未必是某個類的成員。這樣的函數(shù)擁有類對象私有數(shù)據(jù)成員的讀寫權,但它并不是任何類的成員函數(shù)。這個特性在重載運算符時特別有用。 非類成員的友元函數(shù)通常被用來做為類之間的紐帶。一個函數(shù)如果被兩個類同時聲明為友元,它就可以訪問這兩個類的私有成員。下面的程序說明了一個可以訪問兩個類私有數(shù)據(jù)成員的友元函數(shù)是如何將在兩個類之間架起橋梁的。 #include iostream.h class Time; class Date { int mo,da,yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y;} friend void display(const Date&, const Time&); }; class Time { int hr,min,sec; public: Time(int h,int m,int s) { hr=h; min=m; sec=s;} friend void display(const Date&, const Time&); }; void display(const Date& dt, const Time& tm) { cout << dt.mo << '/' << dt.da << '/' << dt.yr; cout << ' '; cout << tm.hr << ':' << tm.min << ':' << tm.sec; } int main() { Date dt(2,16,97); Time tm(10,55,0); display(dt, tm); return 0; } 一、析構函數(shù)
前面的一些例子都沒有說明析構函數(shù),這是因為所用到的類在結束時不需要做特別的清理工作。下面的程序給出了一新的Date類,其中包括一個字符串指針,用來表示月份。 #include iostream.h #include string.h class Date { int mo,da,yr; char *month; public: Date(int m=0, int d=0, int y=0); ~Date(); void display() const; }; Date::Date(int m,int d,int y) { static char *mos[] = { January,February,March,April,May,June, July,August,September,October,November,December }; mo=m; da=d; yr=y; if(m!=0) { month=new char[strlen(mos[m-1])+1]; strcpy(month, mos[m-1]); } else month = 0; } Date::~Date() { delete [] month; } void Date::display() const { if(month!=0) cout< int main() { Date birthday(8,11,1979); birthday.display(); return 0; } 在Date對象的構造函數(shù)中,首先用new運算符為字符串month動態(tài)分配了內(nèi)存,然后從內(nèi)部數(shù)組中把月份的名字拷貝給字符串指針month。 析構函數(shù)在刪除month指針時,可能會出現(xiàn)一些問題。當然從這個程序本身來看,沒什么麻煩;但是從設計一個類的角度來看,當Date類用于賦值時,就會出現(xiàn)問題。假設上面的main()修改為“ int main() { Date birthday(8,11,1979); Date today; today=birthday; birthday.display(); return 0; } 這會生成一個名為today的空的Date型變量,并且把birthday值賦給它。如果不特別通知編譯器,它會簡單的認為類的賦值就是成員對成員的拷貝。在上面的程序中,變量birthday有一個字符型指針month,并且在構造函數(shù)里用new運算符初始化過了。當birthday離開其作用域時,析構函數(shù)會調(diào)用delete運算符來釋放內(nèi)存。但同時,當today離開它的作用域時,析構函數(shù)同樣會對它進行釋放操作,而today里的month指針是birthday里的month指針的一個拷貝。析構函數(shù)對同一指針進行了兩次刪除操作,這會帶來不可預知的后果。 如果假設today是一個外部變量,而birthday是一個自變量。當birthday離開其作用域時,就已經(jīng)把對象today里的month指針刪除了。顯然這也是不正確的。 再假設有兩個初始化的Date變量,把其中一個的值賦值給另一個: Date birthday(8,11,1979); Date today(12,29,2003); today=birthday; 問題就更復雜了,當這兩個變量離開作用域時,birthday中的month的值已經(jīng)通過賦值傳遞給了today。而today中構造函數(shù)用new運算符給month的值卻因為賦值被覆蓋了。這樣,birthday中的month被刪除了兩次,而today中month卻沒有被刪除掉。 二、重載賦值運算符 為了解決上面的問題,我們應該寫一個特殊的賦值運算符函數(shù)來處理這類問題。當需要為同一個類的兩個對象相互賦值時,就可以重載運算符函數(shù)。這個方法可以解決類的賦值和指針的釋放。 下面的程序中,類中的賦值函數(shù)用new運算符從堆中分配了一個不同的指針,該指針獲取賦值對象中相應的值,然后拷貝給接受賦值的對象。 在類中重載賦值運算符的格式如下: void operator = (const Date&) 后面我們回加以改進。目前,重載的運算符函數(shù)的返回類型為void。它是類總的成員函數(shù),在本程序紅,是Date類的成員函數(shù)。它的函數(shù)名始終是operator =,參數(shù)也始終是同一個類的對象的引用。參數(shù)表示的是源對象,即賦值數(shù)據(jù)的提供者。重載函數(shù)的運算符作為目標對象的成員函數(shù)來使用。 #include iostream.h #include string.h class Date { int mo,da,yr; char *month; public: Date(int m=0, int d=0, int y=0); ~Date(); void operator=(const Date&); void display() const; }; Date::Date(int m, int d, int y) { static char *mos[] = { January,February,March,April,May,June, July,August,September,October,November,December }; mo = m; da = d; yr = y; if (m != 0) { month = new char[strlen(mos[m-1])+1]; strcpy(month, mos[m-1]); } else month = 0; } Date::~Date() { delete [] month; } void Date::display() const { if (month!=0) cout< cin >> name; if (strncmp(name, end, 3) == 0) break; ListEntry* list = new ListEntry(name); if (prev != 0) prev->AddEntry(*list); prev = list; } while (prev != 0) { prev->display(); ListEntry* hold = prev; prev = prev->PrevEntry(); delete hold; } return 0; } 程序運行時,會提示輸入一串姓名,當輸入完畢后,鍵入end,然后程序會逆序顯示剛才輸入的所有姓名。 程序中ListEntry類含有一個字符串和一個指向前一個表項的指針。構造函數(shù)從對中獲取內(nèi)存分配給字符串,并把字符串的內(nèi)容拷貝到內(nèi)存,然后置鏈接指針為NULL。析構函數(shù)將釋放字符串所占用的內(nèi)存。 成員函數(shù)PrevEntry()返回指向鏈表前一個表項的指針。另一個成員函數(shù)顯示當前的表項內(nèi)容。 成員函數(shù)AddEntry(),它把this指針拷貝給參數(shù)的preventry指針,即把當前表項的地址賦值給下一個表項的鏈接指針,從而構造了一個鏈表。它并沒有改變調(diào)用它的listEntry對象的內(nèi)容,只是把該對象的地址賦給函數(shù)的參數(shù)所引用的那個ListEntry對象的preventry指針,盡管該函數(shù)不會修改對象的數(shù)據(jù),但它并不是常量型。這是因為,它拷貝對象的地址this指針的內(nèi)容給一個非長常量對象,而編譯器回認為這個非常量對象就有可能通過拷貝得到的地址去修改當前對象的數(shù)據(jù),因此AddEntry()函數(shù)在聲明時不需要用const。 一、類對象數(shù)組 類的對象和C++其他數(shù)據(jù)類型一樣,也可以為其建立數(shù)組,數(shù)組的表示方法和結構一樣。 #include iostream.h class Date { int mo,da,yr; public: Date(int m=0,int d=0, int y=0) { mo=m; da=d; yr=y;} void display() const { cout< int main() { Date dates[2]; Date today(12,31,2003); dates[0]=today; dates[0].display(); dates[1].display(); return 0; } 1.類對象數(shù)組和默認構造函數(shù) 在前面已經(jīng)說過,不帶參數(shù)或者所有參數(shù)都有默認值的構造函數(shù)叫做默認構造函數(shù)。如果類中沒有構造函數(shù),編譯器會自動提供一個什么都不做的公共默認構造函數(shù) 。如果類當中至少有一個構造函數(shù),編譯器就不會提供默認構造函數(shù)。 如果類當中不含默認構造函數(shù),則無法實例化其對象數(shù)組。因為實例花類對象數(shù)組的格式不允許用初始化值來匹配某個構造函數(shù)的參數(shù)表。 上面的程序中,main()函數(shù)聲明了一個長度為2的Date對象數(shù)組,還有一個包含初始化值的單個Date對象。接著把這個初始化的Date對象賦值給數(shù)組中第一個對象,然后顯示兩個數(shù)組元素中包含的日期。從輸出中可以看到,第一個日期是有效日期,而第二個顯示的都是0。 當聲明了某個類的對象數(shù)組時,編譯器會為每個元素都調(diào)用默認構造函數(shù)。 下面的程序去掉了構造函數(shù)的默認參數(shù)值,并且增加了一個默認構造函數(shù)。 #include class Date { int mo, da, yr; public: Date(); Date(int m,int d,int y) { mo=m; da=d; yr=y;} void display() const { cout < Date::Date() { cout < } int main() { Date dates[2]; Date today(12,31,2003); dates[0]=today; dates[0].display(); dates[1].display(); return 0; } 運行程序,輸出為: Date constructor running Date constructor running 12/31/2003 0/0/0 從輸出中可以看出,Date()這個默認構造函數(shù)被調(diào)用了兩次。 2.類對象數(shù)組和析構函數(shù) 當類對象離開作用域時,編譯器會為每個對象數(shù)組元素調(diào)用析構函數(shù)。 #include iostream.h class Date { int mo,da,yr; public: Date(int m=0,int d=0,int y=0) { mo=m; da=d; yr=y;} ~Date() {cout< int main() { Date dates[2]; Date today(12,31,2003); dates[0]=today; dates[0].display(); dates[1].display(); return 0; } 運行程序,輸出為: 12/31/2003 0/0/0 Date destructor running Date destructor running Date destructor running 表明析構函數(shù)被調(diào)用了三次,也就是dates[0],dates[1],today這三個對象離開作用域時調(diào)用的。 二、靜態(tài)成員 可以把類的成員聲明為靜態(tài)的。靜態(tài)成員只能存在唯一的實例。所有的成員函數(shù)都可以訪問這個靜態(tài)成員。即使沒有聲明類的任何實例,靜態(tài)成員也已經(jīng)是存在的。不過類當中聲明靜態(tài)成員時并不能自動定義這個變量,必須在類定義之外來定義該成員。 1.靜態(tài)數(shù)據(jù)成員 靜態(tài)數(shù)據(jù)成員相當于一個全局變量,類的所有實例都可以使用它。成員函數(shù)能訪問并且修改這個值。如果這個靜態(tài)成員是公有的,那么類的作用域之內(nèi)的所有代碼(不論是在類的內(nèi)部還是外部)都可以訪問這個成員。下面的程序通過靜態(tài)數(shù)據(jù)成員來記錄鏈表首項和末項的地址。 #include iostream.h #include string.h class ListEntry { public: static ListEntry* firstentry; private: static ListEntry* lastentry; char* listvalue; ListEntry* nextentry; public: ListEntry(char*); ~ListEntry() { delete [] listvalue;} ListEntry* NextEntry() const { return nextentry; }; void display() const { cout< ListEntry* ListEntry::firstentry; ListEntry* ListEntry::lastentry; ListEntry::ListEntry(char* s) { if(firstentry==0) firstentry=this; if(lastentry!=0) lastentry->nextentry=this; lastentry=this; listvalue=new char[strlen(s)+1]; strcpy(listvalue,s); nextentry=0; } int main() { while (1) { cout<<\nEnter a name ('end' when done): ; char name[25]; cin>>name; if(strncmp(name,end,3)==0) break; new ListEntry(name); } ListEntry* next = ListEntry::firstentry; while (next != 0) { next->display(); ListEntry* hold = next; next=next->NextEntry(); delete hold; } return 0; } 程序首先顯示提示信息,輸入一串姓名,以end作為結束標志。然后按照輸入順序來顯示姓名。構造函數(shù)將表項加入鏈表,用new運算符來聲明一個表項,但并沒有把new運算符返回的地址賦值給某個指針,這是因為構造函數(shù)會把該表項的地址賦值給前一個表項的nextentry指針。 這個程序和前面將的逆序輸出的程序都不是最佳方法,最好的方法是使用類模板,這在后面再介紹。 main()函數(shù)取得ListEntry::firstentry的值,開始遍歷鏈表,因此必需把ListEntry::firstentry設置成公有數(shù)據(jù)成員,這不符合面向對象程序的約定,因為這里數(shù)據(jù)成員是公有的。 2.靜態(tài)成員函數(shù) 成員函數(shù)也可以是靜態(tài)的。如果一個靜態(tài)成員函數(shù)不需要訪問類的任何實例的成員,可以使用類名或者對象名來調(diào)用它。靜態(tài)成員通常用在只需要訪問靜態(tài)數(shù)據(jù)成員的情況下。 靜態(tài)成員函數(shù)沒有this指針,因為它不能訪問非靜態(tài)成員,所以它們不能把this指針指向任何東西。 下面的程序中,ListEntry類中加入了一個靜態(tài)成員函數(shù)FirstEntry(),它從數(shù)據(jù)成員firstentry獲得鏈表第一項的地址,在這兒,firstentry已經(jīng)聲明為私有數(shù)據(jù)成員了。 #include iostream.h #include string.h class ListEntry { static ListEntry* firstentry; static ListEntry* lastentry; char* listvalue; ListEntry* nextentry; public: ListEntry(char*); ~ListEntry() { delete [] listvalue;} static ListEntry* FirstEntry() { return firstentry; } ListEntry* NextEntry() const { return nextentry; }; void display() const { cout< ListEntry* ListEntry::firstentry; ListEntry* ListEntry::lastentry; ListEntry::ListEntry(char* s) { if(firstentry==0) firstentry=this; if(lastentry!=0) lastentry->nextentry=this; lastentry=this; listvalue=new char[strlen(s)+1]; strcpy(listvalue, s); nextentry = 0; } int main() { while (1) { cout<<\nEnter a name ('end' when done):; char name[25]; cin >> name; if(strncmp(name,end,3)==0) break; new ListEntry(name); } ListEntry* next = ListEntry::FirstEntry(); while (next != 0) { next->display(); ListEntry* hold = next; next = next->NextEntry(); delete hold; } return 0; } 函數(shù)ListEntry::FirstEntry()是靜態(tài)的,返回靜態(tài)數(shù)據(jù)成員firstentry的值。 3.公有靜態(tài)成員 如果一個靜態(tài)成員象上面程序一樣是公有的,那么在整個程序中都可以訪問它??梢栽谌魏蔚胤秸{(diào)用公有景泰成員函數(shù),而且不需要有類的實例存在。但公有靜態(tài)成員函數(shù)不完全是全局的,它不僅僅存在于定義類的作用域內(nèi)。在這個作用域里面,只要在函數(shù)名前加上類名和域解析運算符::就可以調(diào)用該函數(shù)。 一、構造函數(shù)和析構函數(shù)
前面的例子已經(jīng)運用了new和delete來為類對象分配和釋放內(nèi)存。當使用new為類對象分配內(nèi)存時,編譯器首先用new運算符分配內(nèi)存,然后調(diào)用類的構造函數(shù);類似的,當使用delete來釋放內(nèi)存時,編譯器會首先調(diào)用淚的析構函數(shù),然后再調(diào)用delete運算符。 #include iostream.h class Date { int mo,da,yr; public: Date() { cout< int main() { Date* dt = new Date; cout< return 0; } 程序定義了一個有構造函數(shù)和析構函數(shù)的Date類,這兩個函數(shù)在執(zhí)行時會顯示一條信息。當new運算符初始化指針dt時,執(zhí)行了構造函數(shù),當delete運算符釋放內(nèi)存時,又執(zhí)行了析構函數(shù)。 程序輸出如下: Date constructor Process the date Date destructor 二、堆和類數(shù)組 前面提到,類對象數(shù)組的每個元素都要調(diào)用構造函數(shù)和析構函數(shù)。下面的例子給出了一個錯誤的釋放類數(shù)組所占用的內(nèi)存的例子。 #include iostream.h class Date { int mo, da, yr; public: Date() { cout< int main() { Date* dt = new Date[5]; cout< return 0; } 指針dt指向一個有五個元素的數(shù)組。按照數(shù)組的定義,編譯器會讓new運算符調(diào)用Date類的構造函數(shù)五次。但是delete被調(diào)用時,并沒有明確告訴編譯器指針指向的Date對象有幾個,所以編譯時,只會調(diào)用析構函數(shù)一次。下面是程序輸出; Date constructor Date constructor Date constructor Date constructor Date constructor Process the date Date destructor 為了解決這個問題,C++允許告訴delete運算符,正在刪除的那個指針時指向數(shù)組的,程序修改如下: #include iostream.h class Date { int mo, da, yr; public: Date() { cout< int main() { Date* dt = new Date[5]; cout< return 0; } 最終輸出為: Date constructor Date constructor Date constructor Date constructor Date constructor Process the date Date destructor Date destructor Date destructor Date destructor Date destructor 三、重載new和delete運算符 前面已經(jīng)介紹了如何用new和delete運算符函數(shù)來動態(tài)第管理內(nèi)存,在那些例子中使用的都是全局的new和delete運算符。我們可以重載全局的new和delete運算符,但這不是好的想法,除非在進行低級的系統(tǒng)上或者嵌入式的編程。 但是,在某個類的內(nèi)部重載new和delete運算符時可以的。這允許一個類有它自己的new和delete運算符。當一個類需要和內(nèi)存打交道時,采用這種方法來處理其中的細節(jié),可以獲得很搞的效率,同時避免了使用全局new和delete運算符帶來的額外開銷。因為全局堆操作時調(diào)用操作系統(tǒng)函數(shù)來分配和釋放內(nèi)存,這樣效率很低。 如果確定某個類在任何時候,其實例都不會超過一個確定的值,那么就可以一次性為類的所有實例分配足夠的內(nèi)存,然后用該類的new和delete運算符來管理這些內(nèi)存。下面的程序說明了如何對new和delete進行重載。 #include iostream.h #include string.h #include stddef.h #include new.h const int maxnames = 5; class Names { char name[25]; static char Names::pool[]; static bool Names::inuse[maxnames]; public: Names(char* s) { strncpy(name,s,sizeof(name)); } void* operator new(size_t) throw(bad_alloc); void operator delete(void*) throw(); void display() const { cout< char Names::pool[maxnames * sizeof(Names)]; bool Names::inuse[maxnames]; void* Names::operator new(size_t) throw(bad_alloc) { for(int p=0; p if(!inuse[p]) { inuse[p] = true; return pool+p*sizeof(Names); } } throw bad_alloc(); } void Names::operator delete(void* p) throw() { if(p!=0) inuse[((char*)p - pool)/sizeof(Names)] = false; } int main() { Names* nm[maxnames]; int i; for(i=0; i cout< cin >> name; nm[i] = new Names(name); } for(i=0; i nm[i]->display(); delete nm[i]; } return 0; } 上面的程序提示輸入5個姓名,然后顯示它們。程序中定義了名為Names的類,它的構造函數(shù)初始化對象的name值。這個類定義了自己的new和delete運算符。這是因為程序能保證不會一次使用超過maxnames個姓名,所以可以通過重載默認的new和delete運算符來提高運行速度。 Names類中的內(nèi)存池是一個字符數(shù)組,可以同時容納程序需要的所有姓名。與之相關的布爾型數(shù)組inuse為每個姓名記錄了一個true和false值,指出內(nèi)存中的對應的項是否正在使用。 重載的new運算符在內(nèi)存池中尋找一個沒有被使用的項,然后返回它的地址。重載的delete運算符則標記那些沒有被使用的項。 在類定義中重載的new和delete運算符函數(shù)始終是靜態(tài)的,并且沒有和對象相關的this指針。這是因為編譯器會在調(diào)用構造函數(shù)之前調(diào)用new函數(shù),在調(diào)用析構函數(shù)后調(diào)用delete函數(shù)。 new函數(shù)是在類的構造函數(shù)之前被調(diào)用的。因為這時內(nèi)存中還不存在類的對象而且構造函數(shù)也沒有提供任何初始化值,所以它不可以訪問類的任何成員。同理,delete運算符是在析構函數(shù)之后被調(diào)用的,所以它也不可以訪問類的成員。 四、異常監(jiān)測和異常處理 1.檢測異常 上面的例子還缺少必要的保護機制。比如,重載的delete運算符函數(shù)并沒有檢查它的參數(shù),確認其是否落在內(nèi)存池內(nèi)部。如果你絕對相信自己編的程序中不會傳遞錯誤的指針值給delete運算符,那么可以省掉合法性檢查以提高效率,特別是在優(yōu)先考慮效率的程序中。否則應該使用預編譯的條件語句。在軟件的測試版本中加入這些檢測,在正式的發(fā)行版本中去掉這些檢查。 2.重載new和delete中的異常處理 上面的兩個重載運算符函數(shù)都是用了異常處理。異常處理是C++的新內(nèi)容之一,目前還沒有講到。在這里不必關心它是如何工作的。上面程序中,當試圖分配超過內(nèi)存池容量的Names緩沖區(qū),重載的new運算符函數(shù)就會拋出異常,終止程序。 五、重載new[]和delete[] 對于上面的程序,假如有下面的語句: Names *nms=new Names[10] ... delete [] nms; 那么,這些語句會調(diào)用全局new和delete運算符,而不是重載過的new和delete。為了重載能為對象數(shù)組分配內(nèi)存的new和delete運算符,必須像下面的程序一樣,對new[]和delete[]也進行重載。 #include iostream.h #include string.h #include stddef.h #include new.h const int maxnames = 5; class Names { char name[25]; static char Names::pool[]; static bool Names::inuse[maxnames]; public: Names(char* s) { strncpy(name,s,sizeof(name)); } void* operator new(size_t) throw(bad_alloc); void operator delete(void*) throw(); void display() const { cout< char Names::pool[maxnames * sizeof(Names)]; bool Names::inuse[maxnames]; void* Names::operator new[](size_t size) throw(bad_alloc) { int elements=size/sizeof(Names); int p=-1; int i=0; while((i if(!inuse[i]) p=i; ++i; } // Not enough room. if ((p==-1) || ((maxnames-p) } void Names::operator delete[](void* b) throw() { if(b!=0) { int p=((char*)b- pool)/sizeof(Names); int elements=inuse[p]; for (int i=0; i } int main() { Names* np = new Names[maxnames]; int i; for(i=0; i cout< cin >> name; *(np + i) = name; } for(i=0; i delete [] np; return 0; } 重載new[]和delete[]要比重載new和delete考慮更多的問題。這是因為new[]運算符時為數(shù)組分配內(nèi)存,所以它必須記住數(shù)組的大小,重載的delete[]運算符才能正確地把緩沖區(qū)釋放回內(nèi)存池。上面的程序采用的方法比較簡單,吧原來存放緩沖區(qū)使用標志的布爾型數(shù)組換成一個整型數(shù)組,該數(shù)組的每個元素記錄new[]運算符分配的緩沖區(qū)個數(shù),而不再是一個簡單的true。當delete[]運算符函數(shù)需要把緩沖區(qū)釋放回內(nèi)存池時,它就會用該數(shù)組來確認釋放的緩沖區(qū)個數(shù)。 一、拷貝構造函數(shù)
拷貝構造函數(shù)在下列情況下被調(diào)用:用已經(jīng)存在的對象去初始化同一個類的另一個對象;在函數(shù)的參數(shù)中,以傳值方式傳遞類對象的拷貝;類對象的值被用做函數(shù)的返回值??截悩嬙旌瘮?shù)和前面說到的轉換構造函數(shù)有些相似。轉換構造函數(shù)是把一個類的對象轉化為另一個類的對象;拷貝構造函數(shù)是用一個已經(jīng)存在的對象的值實例化該類的一個新對象。 不同對象間的初始化和賦值的區(qū)別:賦值操作是在兩個已經(jīng)存在的對象間進行的;而初始化是要創(chuàng)建一個新的對象,并且其初值來源于另一個已存在的對象。編譯器會區(qū)別這兩種情況,賦值的時候調(diào)用重載的賦值運算符,初始化的時候調(diào)用拷貝構造函數(shù)。 如果類中沒有拷貝構造函數(shù),則編譯器會提供一個默認的。這個默認的拷貝構造函數(shù)只是簡單地復制類中的每個成員。 #include iostream.h #include string.h class Date { int mo, da, yr; char* month; public: Date(int m = 0, int d = 0, int y = 0); Date(const Date&); ~Date(); void display() const; }; Date::Date(int m, int d, int y) { static char* mos[] = { January, February, March, April, May, June, July, August, September, October, November, December }; mo = m; da = d; yr = y; if (m != 0) { month = new char[strlen(mos[m-1])+1]; strcpy(month, mos[m-1]); } else month = 0; } Date::Date(const Date& dt) { mo = dt.mo; da = dt.da; yr = dt.yr; if (dt.month != 0) { month = new char [strlen(dt.month)+1]; strcpy(month, dt.month); } else month = 0; } Date::~Date() { delete [] month; } void Date::display() const { if (month != 0) cout << month <<' '<< da << , << yr << std::endl; } int main() { Date birthday(6,24,1940); birthday.display(); Date newday = birthday; newday.display(); Date lastday(birthday); lastday.display(); return 0; } 本例中,用到了兩次拷貝構造函數(shù)。一個是使用普通的C++初始化變量的語句: Date newday = birthday; 另一個是使用構造函數(shù)的調(diào)用約定,即把初始化值作為函數(shù)的參數(shù): Date lastday(birthday); 二、類的引用 在函數(shù)參數(shù)和返回值中,如果一定要使用傳值方式,那么使用類對象的引用,是一個提高效率的方法。 類的數(shù)據(jù)成員也可以是一個引用,但必須注意:第一,一個引用必須初始化。通常一個類對象并不會像結構那樣用大括號來初始化,而是調(diào)用構造函數(shù)。因此在構造函數(shù)里必須初始化類當中的引用成員。第二,引用是一個別名。盡管類里面的引用在使用方式上看起來和類的一般數(shù)據(jù)成員沒有什么區(qū)別,但是作用在其上的操作,實際上是對用來初始化它的那么對象進行的。 #include iostream.h class Date { int da, mo, yr; public: Date(int d,int m,int y) { da = d; mo = m; yr = y; } void Display() const { cout << da << '/' << mo << '/' << yr; } }; class Time { int hr, min, sec; public: Time(int h, int m, int s) { hr = h; min = m; sec = s; } void Display() const { cout << hr << ':' << min << ':' << sec; } }; class DateTime { const Date& dt; const Time& tm; public: DateTime(const Date& d, const Time& t) : dt(d), tm(t) { //empty } void Display() const { dt.Display(); cout << ' '; tm.Display(); } }; int main() { Date today(7,4,2004); Time now(15,20,0); DateTime dtm(today, now); dtm.Display(); return 0; } 我們來看看這個程序中DateTime的構造函數(shù)的格式:冒號操作符引出了一個參數(shù)初始化表。必須使用這種格式來初始化引用數(shù)據(jù)成員,而不可以在函數(shù)體內(nèi)來進行初始化工作。如果構造函數(shù)像上例一樣不是內(nèi)聯(lián)的,那么最好不要在類聲明中構造函數(shù)的原型上使用冒號和初始化值表,而是像下面這樣,把參數(shù)初始化表放在定義構造函數(shù)的地方: Class DateTime { const Date& dt; const Time& tm; public: DateTime(const Date& d,const Time& t); } DateTime::DateTime(const Date& d,const Time& t):dt(d),tm(t) { //empty } 可以使用構造函數(shù)的參數(shù)初始化表來初始化任何數(shù)據(jù)成員。特別是常量數(shù)據(jù)成員,和引用一樣,只能在參數(shù)初始化表里進行初始化,這是因為不可以在構造函數(shù)內(nèi)部為常量數(shù)據(jù)成員賦值。 當一個類含有引用數(shù)據(jù)成員時,一旦引用被實例化和初始化以后,就無法修改它的值,所以該類不可能徹底地重載賦值運算符函數(shù)。 三、構造函數(shù)的參數(shù)初始化表 如果類對象的某些數(shù)據(jù)成員沒有載構造函數(shù)內(nèi)部被初始化,那么必須使用構造函數(shù)的參數(shù)初始化表對他們進行初始化。否則,編譯器不止到該如何初始化這些還等著在構造函數(shù)內(nèi)部賦值的成員。我們習慣用參數(shù)初始化表來初始化所有數(shù)據(jù)成員。 class Date { int mo,da,yr; public: Date(int m=0,int d=0,int y=0); }; class Employee { int empno; Date datehired; public: Employee(int en,Date& dh); }; 可以用下面兩種方法編寫Employee類的構造函數(shù): Employee::Employee(int en,Date& dt) { empno=en; datehired=dh; } 或者; Employee::Employee(int en,Date& dt):empno(en),datehired(dh) { //empty } 雖然這兩種方法效果是一樣的,但是根據(jù)Date對象默認構造函數(shù)的復雜性的不同,這兩種形式的效率差別是很大的。 四、對const修飾符的簡單說明 如果一個對象被聲明為常量,那么該對象就不可以調(diào)用類當中任何非常量型的成員函數(shù)(除了被編譯器隱式調(diào)用的構造函數(shù)和析構函數(shù))。看下面的代碼; #include iostream.h class Date { int month,day,year; public: Date(int m,d,y):month(m),day(d),year(y) {} void display() { cout< } int main() { const Date dt(4,7,2004); dt.display(); //error return 0; } 這個程序盡管編譯時沒有問題,但運行時卻出錯了。這是因為常量對象不能調(diào)用非常量函數(shù)。編譯器只看函數(shù)的聲明,而不在乎函數(shù)的具體實現(xiàn)。實際上函數(shù)的實現(xiàn)可以在程序中的任何地方,也可以是在另一個源代碼文件中,這就超過了編譯器的當前可見范圍。 //date.h class Date { int month,day,year; public: Date(int m,d,y); void display(); }; //date.cpp #include iostream.h #include date.h Date::Date(int m,d,y):month(m),day(d),year(y) {} void Date::display() { cout< //program.cpp #include iostream.h #include date.cpp int main() { const Date dt(4,7,2004); dt.display(); return 0; } 解決出錯的問題有兩個方法:第一是聲明display()函數(shù)為常量型的 //in date.h void display() const //int date.cpp void Date::display() const { cout< 另一個解決方式就是省略掉Date對象聲明里的const修飾符。 Date dt(4,7,2004); 還有另一個容易出錯的地方: void abc(const Date& dt) { dt.display(); //error 提示display沒有const修飾符 } 函數(shù)abc()聲明了一個Date對象的常量引用,這說明該函數(shù)不會修改傳遞進來的參數(shù)的值。如果Date::display()函數(shù)不是常量型的,那么在函數(shù)abc()里就不能調(diào)用它,因為編譯器會認為Date::display()函數(shù)有可能會修改常量的值。 不論類對象是否是常量型的,它必須修改某個數(shù)據(jù)成員的值時,ANSI委員會設立了mutable關鍵字。 五、可變的數(shù)據(jù)成員 假設需要統(tǒng)計某個對象出現(xiàn)的次數(shù),不管它是否是常量。那么類當中就應該有一個用來計數(shù)的整型數(shù)據(jù)成員。只要用mutable修飾符來聲明該數(shù)據(jù)成員,一個常量型的成員函數(shù)就可以修改它的值。 #include iostream.h class AValue { int val; mutable int rptct; public: AValue(int v) : val(v), rptct(0) { } ~AValue() { cout< void report() const; }; void AValue::report() const { rptct++; cout << val << endl; } int main() { const AValue aval(123); aval.report(); aval.report(); aval.report(); return 0; } C++允許為類的對象構造運算符來實現(xiàn)單目或者雙目運算,這個特性就叫運算符重載??梢酝ㄟ^添加成員函數(shù)來實現(xiàn)運算符重載。 重載是由P.J.Plauger發(fā)現(xiàn)的。 一。重載運算符的時機 1。需要在定義的對象間相互賦值時,重載賦值運算符 2。需要在數(shù)字類型增加算術屬性時,重載算術運算符 3。需要為定義的對象進行邏輯比較時,重載關系運算符 4。對于container,重載下標運算符[] 5。需要從I/O流中讀寫對象時,重載<<和>>運算符。 6。重載成員指針運算符 -> 以實現(xiàn)smart指針 7。在少數(shù)情況下重載new,delete運算符 8。不重載其他運算符 實際上任何用重載運算符完成的工作都可以使用成員函數(shù)來實現(xiàn)。 重載的運算符可以和原來的運算符不一定有必然聯(lián)系,例如我重載'+'運算馬夫,可以不做加法運算,而是把字符串連接起來。當然你要是用'+'運算符來做減法運算,也是可以的,不過這不是明智之舉。 二。重載運算符的規(guī)則 1。重載的運算符不能違反語言的語法規(guī)則 2。如果一個運算符可以放在兩個操作數(shù)之間,就可以重載它來滿足類操作的需要,哪怕這種用法原本為編譯器不能接受。 3。不能創(chuàng)造C++語言中沒有的運算符 4。下列運算符不能重載 . 類成員運算符 .* 成員指針運算符 :: 域解析運算符 : 條件表達式運算符 5。重載時不能改變運算符的優(yōu)先級 三。運算符重載 運算符重載是通過對運算符函數(shù)的重載來實現(xiàn)的。對于每一個運算符@,在C++中都對應一個運算符函數(shù)operator@,其中@為C++各種運算符。 運算符函數(shù)的一般原型為: type operator@ (arglist) 其中type為運算結果的類型,arglist為操作數(shù)列表。 在(五)我們已經(jīng)介紹了重載賦值運算符,這里就不重新說明了。
一。作為類成員函數(shù)的重載 為了能進行類對象和一個整型值的加法運算,需要寫一個類的成員函數(shù)來重載雙目加法(+)運算符。該函數(shù)在類中的聲明如下: Date operator + (int) const; 函數(shù)的聲明指出,返回值是一個Date類對象,函數(shù)名是運算符+,只有一個整型參數(shù),而且函數(shù)是常量型的。當編譯器發(fā)現(xiàn)某個函數(shù)以加上前綴operator的真實運算符作為函數(shù)名,就會把該函數(shù)當作重載運算符函數(shù)來處理。如果在表達式中,該運算符的左邊是一個類對象,右邊是一個參數(shù)類型的一個對象,那么重載運算符函數(shù)就會被調(diào)用。調(diào)用形式如下: Date dt(6,9,2005); dt=dt+100; 也可以顯式的調(diào)用重載運算符函數(shù): dt.operator + (100); 下面代碼重載了雙目加法運算符來計算一個整數(shù)和一個Date類對象之和,并且返回Date類對象。 #include iostream.h class Date { int mo,da,yr; static int dys[]; public: Date(int m=0,int d=0,int y=0) { mo=m; da=d; yr=y;} void display() const { cout< }; int Date::dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; Date Date::operator+(int) const { Date dt=*this; n+=dt.da; while(n>=dys[dt.mo-1]) { n-=dys[dt.mo-1]; if(++dt.da==13) { dt.mo=1; dt,yr++; } } dt.da=n; return dt; } int main() { Date olddate(1,1,2005); Date newdate; newdate=olddate+100; newdate.display(); return 0; } 二。非類成員的運算符重載 在重載運算符的原則中說到,要保持運算符的可交換性。而上面的程序只允許Date類對象在運算符的左邊而整型值在右邊,不支持下面的語句: Date newdate=100+olddate; 所以,僅僅靠一個類的成員重載運算符是無法實現(xiàn)上面功能的。對重載雙目運算符的類成員函數(shù)來說,總是認定調(diào)用函數(shù)的對象位于運算符左邊。不過,我們可以再寫一個非類成員的重載運算符函數(shù),可以規(guī)定Date類的對象在運算符右邊,而別的類型在運算符左邊。例如,我們可以這樣在類的外部定義一個函數(shù): Date operator + (int n,Date& dt) 下面代碼在原先的基礎上增加了一個非類成員函數(shù)來實現(xiàn)雙目加法運算符的重載。 #include iostream.h class Date { int mo,da,yr; static int dys[]; public: Date(int m=0,int d=0,int y=0) { mo=m; da=d; yr=y;} void display() const { cout< }; int Date::dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; Date Date::operator+(int) const { Date dt=*this; n+=dt.da; while(n>=dys[dt.mo-1]) { n-=dys[dt.mo-1]; if(++dt.da==13) { dt.mo=1; dt,yr++; } } dt.da=n; return dt; } Date operator + (int n,Date& dt) { return dt+n; } int main() { Date olddate(1,1,2005); Date newdate; newdate=olddate+100; newdate.display(); return 0; } 上面的例子中非類成員重載運算符函數(shù)調(diào)用了類中的重載+運算符來實現(xiàn)加法運算。如果類當中沒有提供這樣的函數(shù),那么非類成員的重載運算符函數(shù)將被迫訪問類的私有數(shù)據(jù)來實現(xiàn)加法運算。這樣的話,需要把這個函數(shù)聲明為類的友元,如下: class Date { friend Date operator + (int n,Date&); }; 上例中重載運算符函數(shù)聲明了全部兩個參數(shù),這是因為它不是類的成員,因此它不能作為類的成員函數(shù)被調(diào)用,就缺少了一個隱含的參數(shù)。 第一個重載加法運算符函數(shù)也可以用類的友元函數(shù)來實現(xiàn)。作為一種約定,這通常把所有為類重載的運算符都設定為該類的友元。 例子中只給出了重載加法的代碼,我們同樣可以來重載減法,乘除法等等。 三。重載關系運算符 如果想要對兩個日期進行比較,比如出現(xiàn)下面這樣的代碼: if(olddate #include iostream.h class Date { int mo,da,yr; public: Date(int m=0,int d=0,int y=0) { mo=m; da=d; yr=y;} void display() const { cout< int operator < (Date& dt) const; }; int Date::operator== (Date& dt) const { return (this->mo==dt.mo && this->da==dt.da && this->yr==dt.yr); } int Date::operator < (Date& dt) const { if(this->yr == dt.yr) { if(this->mo == dt.mo) return this->da < dt.da; return this->mo < dt.mo; } return this->yr < dt.yr; } int main() { Date date1(2,14,2005); Date date2(6,9,2005); Date date3(2,14,2005); if(date1 date1.display(); cout< } cout< { date1.display(); cout< } return 0; } 可以類似的重載其他關系運算符,如!= int operator != (Date& dt) { return !(*this==dt);} 四。其他賦值運算符 #include iostream.h class Date { int mo,da,yr; static int dys[]; public: Date(int m=0,int d=0,int y=0) { mo=m; da=d; yr=y;} void display() const { cout< Date operator +=(int) { *this=*this+n; return *this;} }; int Date::dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; Date Date::operator+(int) const { Date dt=*this; n+=dt.da; while(n>=dys[dt.mo-1]) { n-=dys[dt.mo-1]; if(++dt.da==13) { dt.mo=1; dt,yr++; } } dt.da=n; return dt; } int main() { Date olddate(1,1,2005); olddate+=100; olddate.display(); return 0; } |
|