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

致C語言程序員

Arthur Dent:他怎么啦?

Hig Hurtenflurst:他的鞋子不合腳。

——Douglas Adms,《銀河系漫游指南》,“Fit the Eleventh”

這篇文章是為那些正在考慮是否閱讀本書的有經驗的C語言程序員準備的,其他語言的程序員可以跳過。

Bjarne Stroustrup基于C語言開發了C++。雖然C++與C語言并不完全兼容,但寫得好的C語言程序往往也是有效的C++程序。例如,Brian Kernighan和Dennis Ritchie所著的The C Programming Language中的每個例子都是合法的C++程序。

C語言在系統編程界無處不在的一個主要原因是,相對于匯編語言,C語言允許程序員在較高的抽象層次上編寫程序,這往往會產生更清晰、更不容易出錯、更容易維護的代碼。

一般來說,系統程序員不愿意為編程的便利性買單,所以C語言堅持零開銷原則:不為不使用的東西買單。強類型系統是零開銷抽象的一個典型例子。它只在編譯時被用來檢查程序的正確性,編譯結束后,類型就會消失,產生的匯編代碼也不會有類型系統的痕跡。

作為C語言的后裔,C++也非常重視零開銷的抽象和對硬件的直接映射。這一承諾不僅僅限于C++所支持的C語言特性,C++在C語言基礎上建立的一切(包括新的語言特性)都堅持這些原則,任何違背原則的設計決策都應慎重對待。事實上,一些C++特性的開銷甚至比相應的C代碼產生的開銷更少。constexpr關鍵字就是這樣一個例子,它指示編譯器在編譯時對表達式求值(如果可能的話),如代碼清單1中的程序所示。

代碼清單1 演示constexpr的程序

isqrt函數計算參數n的平方根。從1開始,該函數遞增局部變量i,直到i*i大于或等于n。如果i*i==n,則返回i,否則返回i-1。注意,isqrt的調用有一個字面量,所以編譯器理論上可以為你計算結果。結果將只有一個值?。

在GCC 8.3上用-O2編譯代碼清單1(目標平臺為x86-64),即可得到代碼清單2中的匯編代碼。

代碼清單2 編譯代碼清單1后產生的匯編代碼

這里的重點是main?中的第二條指令,編譯器不是在運行時計算1764的平方根,而是計算它并直接輸出指令,將x視為42。當然,你可以用計算器計算它的平方根并手動插入結果,但使用constexpr有很多好處,不僅可以減少手動復制粘貼導致的許多錯誤,而且使代碼更有表現力。

注意 如果不熟悉x86匯編,可以參考Randall Hyde的The Art of Assembly Language(第2版)和Richard Blum的Professional Assembly Language

升級到Super C

現代C++編譯器可以適應你的大部分C語言編程習慣,這使得你很容易接受C++提供的一些戰術上的好處,同時刻意避開語言本身的更深層次的主題。這種C++風格——我們稱之為Super C——是值得討論的,原因有幾個。首先,經驗豐富的C語言程序員可以立即將簡單的、戰術層面的C++概念應用于自己的程序。其次,Super C并不是地道的C++。在C語言程序中簡單地應用引用和auto的實例,可能會使代碼更加健壯、更加可讀,但你需要學習其他概念來充分利用它。最后,在一些嚴苛的環境(例如,嵌入式軟件、某些操作系統內核和異構計算)中,工具鏈并不完全支持C++。在這種情況下,至少可以從一些C++特性中獲益,而這時Super C很可能得到支持。本節討論一些可以立即應用到代碼中的Super C概念。

注意 一些C語言支持的結構體在C++中無法使用,詳見https://ccc.codes。

函數重載

請考慮以下來自標準C語言庫的轉換函數:

char* itoa(int value, char* str, int base);

char* ltoa(long value, char* buffer, int base);

char* ultoa(unsigned long value, char* buffer, int base);

這些函數實現了相同的目標:將整數類型轉換為C語言風格的字符串。在C語言中,每個函數必須有唯一的名稱。但在C++中,參數不同時,函數可以共享名稱,這被稱為函數重載。我們可以使用函數重載來創建自己的轉換函數,如代碼清單3所示。

代碼清單3 調用重載函數

幾個函數的第一個參數的數據類型不同,所以C++編譯器可以從傳入toa的參數中獲得足夠的信息來調用正確的函數。每次調用的函數都是唯一的。這里,我們創建了變量a?、b?和c?,它們是不同類型的int對象,分別與三個toa函數對應。這比定義單獨命名的函數更方便,因為我們只需要記住一個名字,編譯器會自己確定要調用哪個函數。

引用

指針是C語言(以及大多數系統編程擴展)的一個重要特性。它能夠讓我們通過傳遞數據地址而不是實際數據來有效地處理大量的數據。指針對C++而言也同樣重要,但C++有了額外的安全特性,可以防止空指針解引用和無意的指針再賦值。

引用是指針處理的一個重大改進。它與指針相似,但有一些關鍵的區別。在語法上,引用與指針有兩個重要的區別。首先,我們通過&而不是*來聲明引用,如代碼清單4所示。

代碼清單4 說明如何聲明接受指針和引用的函數的代碼

其次,我們使用點運算符(.)而不是箭頭運算符(->)與成員交互,如代碼清單5所示。

代碼清單5 說明使用點運算符和箭頭運算符的程序

究其實現原理,引用其實等同于指針,因為它們都是零開銷的抽象概念。編譯器會生成差不多的代碼。為了說明這一點,請考慮在GCC 8.3上以x86-64(-O2)為目標編譯make_sentient函數的結果。代碼清單6給出了通過編譯代碼清單5生成的匯編代碼。

代碼清單6 編譯代碼清單5生成的匯編代碼

然而,在編譯時,引用比原始指針更安全,因為一般來說它不能為空。

對于指針,為了安全起見,需要添加nullptr檢查。例如,我們可以給make_sentient添加一個檢查,如代碼清單7所示。

代碼清單7 對代碼清單5中的make_sentient進行重構,使其執行一個nullptr檢查

在接受引用時,這樣的檢查是不必要的,但是,這并不意味著引用總是有效的。請考慮下面這個函數:

not_dinkum函數返回一個引用,它保證非空。但是,它指向的是垃圾內存(可能是在not_dinkum的返回棧幀中)。我們絕不能這樣做,這樣做將很痛苦,其結果被稱為未定義的運行時行為:它可能會崩潰,也可能會返回一個錯誤,還可能會做一些完全意想不到的事情。

引用的另一個安全特性是,不能被重定位。換句話說,一旦引用被初始化,它就不能指向另一個內存地址,如代碼清單8所示。

代碼清單8 說明引用不能被重定位的程序

我們將a_ref聲明為對int a的引用?。沒有辦法重新設置a_ref,使其指向另一個int。我們可以嘗試用operator=?來重新設置a,但這實際上是將a的值設置為b的值,而不是將a_ref設置為對b的引用。運行這個代碼段后,ab都等于100,并且a_ref仍然指向a。代碼清單9給出了使用指針的等效程序。

代碼清單9 使用指針的代碼清單8的等效程序

這里,我們用a*而不是a&來聲明指針?,把b的值分配給a_ptr指向的內存?。對于引用,等號的左邊不需要有任何裝飾。但是這里如果省略了*a_ptr中的*,編譯器會認為我們試圖把int對象賦值給指針類型。

引用只是帶有額外安全預防措施和語法糖的指針。當我們把引用放在等號的左邊時,實際上就是把被引用的值設置為等號右邊的值。

auto初始化

C語言經常要求我們多次重復類型信息。在C++中,我們可以利用auto關鍵字來表達變量的類型信息,只需一次。編譯器將知道變量的類型,因為它知道用于初始化變量的值的類型。請考慮下面這些C++變量的初始化:

這里,xy都是int類型的。考慮到42是一個整數字面量,你可能會驚訝地發現編譯器可以推斷出y的類型。通過auto,編譯器可以推斷出等號右側的類型,并將變量的類型設置為相同的類型。因為整數字面量是int類型的,所以編譯器推斷y的類型也是int。在這樣一個簡單的例子中,這似乎沒有什么好處,但是請考慮一下用函數的返回值來初始化變量的情況,如代碼清單10所示。

代碼清單10 用函數的返回值來初始化變量的玩具程序

auto關鍵字更容易閱讀,并且比明確聲明變量的類型更容易進行代碼調整。如果在聲明函數時自由地使用auto,那么以后要改變make_mike的返回類型,只需要做少量的工作。對于更復雜的類型,例如那些與標準庫的模板代碼有關的類型,auto的作用就更大了。auto關鍵字使編譯器為你做所有的類型推導工作。

注意 還可以給auto添加constvolatile&*限定符。

命名空間與結構體、聯合體和枚舉的隱式類型定義

C++把類型標記當作隱式的typedef名稱。在C語言中,當你想使用結構體(struct)、聯合體(union)或枚舉(enum)時,必須使用typedef關鍵字為創建的類型指定一個名稱,例如:

在C++中,你會對這樣的代碼嗤之以鼻,因為typedef關鍵字可以是隱式的,C++允許你像下面這樣聲明Jabberwock的類型:

這樣更方便,也省去了一些打字工作。如果你還想定義一個Jabberwock函數,會發生什么呢?不應該這樣做,因為對數據類型和函數重復使用同一個名字很可能會引起混淆。但是,如果你真的想這樣做,那么可以聲明一個命名空間(namespace),為標識符創建不同的作用域。這有助于保持用戶類型和函數的整潔,如代碼清單11所示。

代碼清單11 使用命名空間來消除名稱相同的函數和類型的歧義

在這個例子中,Jabberwock結構體和Jabberwock函數可以“和諧”地出現在一個地方了。通過將每個元素放在各自的命名空間中——結構體JabberwockCreature命名空間中?,函數JabberwockFunc命名空間中?——你可以分辨出具體指哪個Jabberwock。可以用幾種方法消除歧義,最簡單的方法是用命名空間來限定名稱,例如:

也可以使用using指令導入命名空間中的所有名字,這樣就不再需要使用完整名稱的元素名了。代碼清單12使用了Creature命名空間。

代碼清單12 通過using namespace來指定使用Creature命名空間中的一個類型

使用using namespace?,你可以省略命名空間限定詞?。但是對于函數Jabberwock(),仍然需要使用限定詞(Func::Jabberwock),因為它不是Creature命名空間的一部分。

使用namespace是C++的慣例,是一種零開銷的抽象。就像類型的其他標識符一樣,namespace會在生成匯編代碼時被編譯器清除掉。在大型項目中,它對分離不同庫中的代碼有極大的幫助。

C和C++對象文件的混用

如果小心些,C和C++代碼可以和平共存。有時,有必要讓C編譯器鏈接C++編譯器生成的對象文件(反之亦然)。這是可以做到的,但需要做一些額外工作。

有兩個問題與鏈接文件有關。首先,C和C++代碼中的調用約定可能是不匹配的,例如,調用函數時堆棧和寄存器的設置協議可能不同。調用約定不匹配是語言層面的不匹配,通常與編寫函數的方式無關。其次,C++編譯器發出的符號與C編譯器發出的不同。有時,鏈接器必須通過名稱來識別對象。C++編譯器通過裝飾目標對象,將叫作裝飾名稱的字符串與該對象相關聯。由于函數重載、調用約定和命名空間的使用,編譯器必須通過裝飾對函數的額外信息進行編碼,而不僅僅是其名稱。這樣做是為了確保鏈接器能夠唯一地識別該函數。不幸的是,在C++中沒有關于這種裝飾的標準(這就是在編譯單元之間進行鏈接時應該使用相同的工具鏈和設置的原因)。C語言的鏈接器對C++的名稱裝飾一無所知,如果在C++中對C代碼進行鏈接時不停止裝飾,就會產生問題(反之亦然)。

這個問題的解決辦法很簡單,只要使用extern"C"語句將要用C語言風格的鏈接方式編譯的代碼包裝起來即可,如代碼清單13所示。

代碼清單13 采用C語言風格的鏈接方式

這個頭文件可以在C和C++代碼之間共享。它之所以起作用是因為__cplusplus是一個特殊的標識符,C++編譯器定義了它(但C編譯器沒有)。因此,在預處理完成后,C編譯器將看到代碼清單14中的代碼。

代碼清單14 預處理程序在C環境下處理代碼清單13后的代碼

這只是一個簡單的C頭文件。在預處理過程中,#ifdef __cplusplus語句之間的代碼被刪除了,所以extern"C"包裝器并不可見。對于C++編譯器來說,__cplusplus被定義在header.h中,所以它可以看到代碼清單15所示的內容。

代碼清單15 預處理程序在C++環境下處理代碼清單13后的代碼

extract_arkenstoneMistyMountains現在都用extern"C"來包裝,所以編譯器知道要使用C鏈接。現在C源代碼可以調用已編譯的C++代碼,C++源代碼也可以調用已編譯的C代碼。

C++主題

本節將帶你簡要瀏覽一些使C++成為首要的系統編程語言的核心主題。不要太在意細節,下面各小節的重點是吊起你的胃口。

簡潔地表達想法和重用代碼

精心設計的C++代碼很優雅,很緊湊。請通過以下簡單操作考慮一下從ANSI-C到現代C++的演變,該操作遍歷有n個元素的數組v,如代碼清單16所示。

代碼清單16 說明對數組進行迭代的幾種方法的程序

這段代碼顯示了在ANSI-C、C99和C++中聲明循環的不同方法。ANSI-C?和C99?的例子中,索引變量i可以輔助你完成任務,也就是輔助你訪問v中的每個元素。C++版本?使用了基于范圍(range-based)的for循環,它在v中的數值范圍內循環,同時隱藏了實現迭代的細節。就像C++中的許多零開銷抽象一樣,這個結構使你能夠專注于意義而不是語法。基于范圍的for循環適用于許多類型,甚至可以適用于用戶自定義類型。

用戶自定義類型允許你在代碼中直接表達想法。假設你想設計一個函數navigate_to,告訴假想的機器人導航到給定xy坐標的某個位置。請考慮以下函數原型:

xy是什么?它們的單位是什么?用戶必須閱讀文件(或者源文件)來找出答案。請考慮以下改進后的原型:

這個函數要清楚得多。對于navigate_to接受的內容沒有任何歧義。只要有有效的Position,你就知道如何調用navigate_to。單位轉換等問題則是構建Position類的人的責任。

在C99/C11中,你也可以使用常量(const)指針來實現這種表現力,但C++也使返回類型變得緊湊而富有表現力。假設你想為機器人寫一個名為get_position(獲取位置的推論函數。在C語言中,有兩種方法,如代碼清單17所示。

代碼清單17 用于返回用戶自定義類型的C風格API

在第一種方法中,調用者負責清理返回值?,這可能產生一個動態內存分配(盡管從代碼中看不清楚)。在第二種方法中,調用者負責分配一個Position并把它傳入get_position?。第二種方法更符合C語言的習慣,但語言礙手礙腳:你本來只想得到一個位置對象,卻不得不擔心是調用者還是被調用者負責分配和刪除內存。C++可以讓你通過直接從函數中返回用戶自定義類型來簡潔地完成這一切,如代碼清單18所示。

代碼清單18 在C++中按值返回用戶自定義類型

因為get_position返回一個值?,編譯器可以忽略這個復制操作,所以就好像你直接構造了一個自動的Position變量?,沒有運行時開銷。從功能上講,這個情況與代碼清單17中的C風格指針傳遞非常相似。

C++標準庫

C++標準庫(stdlib)是人們從C語言遷移到C++的一個主要原因。它包含了高性能的泛型代碼,保證可以從符合標準的盒子里直接使用。stdlib的三個主要組成部分是容器、迭代器和算法。

容器是數據結構,負責保存對象的序列。容器是正確的、安全的,而且(通常)至少和你手寫的代碼一樣有效,這意味著自己編寫容器將花費巨大的精力,而且不會比stdlib的容器更好。容器大體分為兩類:順序容器和關聯容器。順序容器在概念上類似于數組,提供對元素序列的訪問權限。關聯容器包含鍵值對,所以容器中的元素可以通過鍵來查詢。

算法是通用的函數,用于常見的編程任務,如計數、搜索、排序和轉換。與容器一樣,算法的質量非常高,而且適用范圍很廣。用戶應該很少需要自己實現算法,而且使用stdlib算法可以極大地提高程序員的工作效率、代碼安全性和可讀性。

迭代器可以將容器與算法連接起來。對于許多stdlib算法的應用,你想操作的數據駐留在容器中。容器公開迭代器,以提供平滑、通用的接口,而算法消費迭代器,使程序員(包括stdlib的實現者)不必為每種容器類型實現一個自定義算法。

代碼清單19顯示了如何用幾行代碼對容器的值進行排序。

代碼清單19 使用stdlib對容器的值進行排序

雖然會在后臺進行大量的計算,但代碼是緊湊而富有表現力的。首先,初始化一個std::vector?。向量是stdlib中大小動態變化的數組。初始化大括號(即{0, 1,...})用來設置x的初始值。我們可以使用中括號([])和索引號,像訪問數組的元素一樣訪問向量的元素。我們用這個方法將第一個元素設置為21?。因為向量數組的大小是動態的,所以可以用push_back方法向它們追加數值?。std::sort看似神奇的調用展示了stdlib算法的強大?。方法x.begin()x.end()返回迭代器,std::sort用該迭代器對x進行原地排序。通過迭代器,排序算法與向量解耦了。

有了迭代器,我們就可以用類似的方式使用stdlib中的其他容器了。例如,我們可以使用list(stdlib的雙向鏈表)而不是向量,因為list也通過.begin().end()公開了迭代器,我們可以用同樣的方式在列表迭代器上調用sort

此外,代碼清單19使用了iostream。iostream是一種執行緩沖輸入和輸出的機制。我們使用左移操作符(<<)將x.size()的值(x中的元素數)、一些字符串字面量和斐波那契元素number發給std::cout(包裝標準輸出)??。std::endl對象是一個I/O操縱符,它寫下\n并刷新緩沖區,確保在執行下一條指令之前將整個數據流寫到標準輸出。

現在,想象一下用C語言編寫同等程序所要經歷的所有障礙,你就會明白為什么stdlib是一個如此有價值的工具。

lambda

lambda,在某些圈子里也被稱為無名函數或匿名函數,是另一個強大的語言特性,它可以提高代碼的局部性。在某些情況下,我們應該將指針傳遞給函數,使用函數指針作為新創建的線程的目標函數,或者對序列的每個元素進行一些轉換。一般來說,定義一個一次性使用的自由函數通常很不方便。這就是lambda發揮作用的地方。lambda是一個新的、自定義的函數,與調用的其他參數一起內聯定義。考慮下面這個單行程序,它計算x中偶數的數量:

這行代碼使用stdlib的count_if算法來計算x中偶數的數量。std::count_if的前兩個參數與std::sort相似,它們是定義算法將操作的范圍的迭代器。第三個參數是lambda。這個符號可能看起來有點陌生,但基本原理非常簡單:

capture包含任何需要從定義lambda的范圍中獲得的對象,以便執行body中的計算。arguments定義lambda希望被調用的參數的名稱和類型。body包含在調用時完成的任何計算。它可能返回值,也可能不返回值。編譯器將根據隱含的類型來推導函數原型。

在上面的std::count_if調用中,lambda不需要捕獲任何變量。它所需要的所有信息就是單一參數number。因為編譯器知道x中包含的元素的類型,所以我們用auto聲明number的類型,這樣編譯器就可以自己推導出來。lambda被調用時,x中的每個元素都作為number參數傳入。在body中,只有當數字能被2整除時,該lambda返回true,所以只有為偶數時才會計數。

C語言中不存在lambda,我們也不可能真正模擬它。每次需要函數對象時,我們都需要聲明一個單獨的函數,而且不可能以同樣的方式將對象捕獲到一個函數中。

使用模板的泛型編程

泛型編程是指寫一次代碼就能適用于不同的類型,而不是通過復制和粘貼每個類型來多次重復相同的代碼。在C++中,我們使用模板來產生泛型代碼。模板是一種特殊的參數,告訴編譯器它代表各種可能的類型。

我們已經使用過模板了:stdlib的所有容器都是模板。在大多數情況下,這些容器中的對象的類型并不重要。例如,確定容器中的元素數量或返回其第一個元素的邏輯并不取決于元素的類型。

假設我們想寫一個函數,將三個相同類型的數字相加。我們希望函數接受任何可加類型。在C++中,這是一個簡單的泛型編程問題,可以直接用模板解決,如代碼清單20所示。

代碼清單20 使用模板來創建一個泛型add函數

當聲明add?時,我們不需要知道T,只需要知道所有的參數和返回值都是T類型的,并且T類型數據是可加的。當編譯器遇到add調用時,它會推斷出T并生成一個定制的函數。這就是一種重要的代碼復用!

類的不變量和資源管理

也許C++給系統編程帶來的最大創新是對象生命周期。這個概念源于C語言,在C語言中,對象具有不同的存儲期,這取決于它們在代碼中的聲明方式。C++在這個內存管理模型的基礎上,創造了構造函數和析構函數。這些特殊函數是屬于用戶自定義類型的方法。用戶自定義類型是C++應用程序的基本構建塊,我們可以把它想象成有函數的struct對象。

對象的構造函數在其存儲期開始后被調用,而析構函數則在其存儲期結束前被調用。構造函數和析構函數都是沒有返回類型的函數,其名稱與包圍它的類相同。要聲明一個析構函數,請在類名的開頭加上~,如代碼清單21所示。

代碼清單21 一個包含構造函數和析構函數的Hal類

Hal的第一個方法是構造函數?。它設置了Hal對象并建立了它的類不變量。不變量是類的特征,一旦被構造出來就不會改變。在編譯器和運行時的幫助下,程序員決定類的不變量是什么,并確保代碼可以保證這些不變量。在本例中,構造函數將version設定為9000,這是一個不變量。析構函數是第二個方法?。每當Hal要被刪除時,它就向控制臺打印Stop, Dave.(讓HalDaisy Bell是留給讀者的一個練習)。

編譯器會確保靜態、本地和線程局部存儲期的對象自動調用構造函數和析構函數。對于具有動態存儲期的對象,可以使用關鍵字newdelete來分別代替mallocfree,代碼清單22說明了這一點。

代碼清單22 一個創建和銷毀Hal對象的程序

如果構造函數無法達到一個好的狀態(無論什么原因),那么它通常會拋出異常。作為C語言程序員,你可能在使用一些操作系統API編程時處理過異常(例如,Windows結構化異常處理)。當拋出異常時,堆棧會展開,直到找到異常處理程序,這時程序就會恢復。謹慎地使用異常可以使代碼更干凈,因為只需要在有意義的地方檢查錯誤條件。如代碼清單23所示,C++對異常有語言級的支持。

代碼清單23 一個try-catch塊

我們可以把可能拋出異常的代碼放在緊跟try的代碼塊中?。如果拋出了異常,堆棧將展開(“慷慨”地銷毀任何超出作用域的對象)并運行catch表達式之后的代碼?。如果沒有拋出異常,catch代碼塊永遠不會執行。

構造函數、析構函數和異常與另一個C++核心主題密切相關,該核心主題便是把對象的生命周期與它所擁有的資源聯系起來。

這就是資源獲取即初始化(RAII)的概念(有時也稱為構造函數獲取、析構函數釋放)。請考慮代碼清單24中的C++類。

代碼清單24 一個File類

File的構造函數需要兩個參數?。第一個參數是文件的路徑,第二個參數是一個布爾值,對應于文件模式(file_mode)應該是寫(true)還是讀(false)。這個參數的值通過三元運算符?:設置file_mode?。三元運算符評估一個布爾表達式,并根據布爾值返回其中的一個,例如:

如果布爾表達式xtrue,表達式的值為val_if_true。如果xfalse,則值為val_if_false

在代碼清單24的File構造函數代碼段中,構造函數試圖以讀/寫訪問方式打開位于path路徑的文件?。如果有問題,本次調用將把file_pointer設置為nullptr,這是一個特殊的C++值,類似于0。當發生這種情況時,將拋出一個system_error?。system_error是封裝了系統錯誤細節的對象。如果file_pointer不是nullptr,它就可以有效地使用。這就是這個類的不變量。

現在請考慮代碼清單25中的程序,它采用了File類。

代碼清單25 一個使用File類的程序

大括號??定義了一個作用域。因為第一個文件file駐留在這個作用域內,作用域定義了file的生命周期。一旦構造函數返回?,我們就知道file.file_pointer是有效的,這要歸功于類的不變量。根據File的構造函數的設計,我們知道file.file_pointer必須在File對象的生命周期內有效。我們用fwrite寫了一條信息。沒有必要明確地調用fclose,因為file會過期,而且析構函數會自動清理file.file_pointer?。我們再次打開File,但這次以讀訪問方式打開?。同樣,只要構造函數返回,我們就知道last_message.txt被成功打開并繼續讀入read_message。打印信息之后,調用file的析構函數,file.file_pointer又被清理掉了。

有時,我們需要動態內存分配的靈活性,但仍然想依靠C++的對象生命周期來確保不會泄漏內存或意外地“釋放后使用”。這正是智能指針的作用,它通過所有權模型來管理動態對象的生命周期。一旦沒有智能指針擁有動態對象,該對象就會被銷毀。

unique_ptr就是一種這樣的智能指針,它模擬了獨占的所有權。代碼清單26說明了它的基本用法。

代碼清單26 一個采用unique_ptr的程序

我們動態地分配了一個Foundation,而產生的Foundation*指針被傳給second_foundation的構造函數,使用大括號初始化語法?。second_foundation的類型是unique_ptr,它只是一個包裹著動態Foundation的RAII對象。當second_foundation被析構時?,動態Foundation就會適當地銷毀。

智能指針與普通的原始指針不同,因為原始指針只是一個簡單的內存地址。我們必須手動協調所有涉及該地址的內存管理。但是,智能指針可以自行處理所有這些混亂的細節。用智能指針包裝動態對象,我們可以很放心,一旦不再需要這個對象,內存就會被適當地清理掉。編譯器知道不再需要該對象了,因為當它超出作用域時,智能指針的析構函數會被調用。

移動語義

有時,我們想轉移對象的所有權,這種情況很常見,例如使用unique_ptr時。我們不能復制unique_ptr,因為一旦某個副本被銷毀,剩下的unique_ptr將持有對已刪除對象的引用。與其復制對象,不如使用C++的移動(move)語義將所有權從一個指針轉移到另一個指針,如代碼清單27所示。

代碼清單27 一個移動unique_ptr的程序

像以前一樣,我們創建unique_ptr<Foundation>?。在使用它一段時間后,我們決定將所有權轉移到一個Mutant對象。move函數告訴編譯器我們想轉移所有權。在構造the_mule后?,Foundation的生命周期通過其成員變量與the_mule的生命周期聯系在一起。

放松并享受C++學習之旅

C++是最主要的系統編程語言。你的許多C語言知識都可以直接用到C++中,但你也將學到許多新概念。隨著對C++的一些深層主題的掌握,你會發現現代C++相比C語言有許多實質性的優勢。你將能夠在代碼中簡潔地表達想法,利用令人印象深刻的stdlib在更高的抽象水平編寫代碼,采用模板來提高運行時的性能、強化代碼復用,并依靠C++對象生命周期來管理資源。

我希望你學習完C++后將獲得巨大的回報。讀完本書后,我想你會同意這種看法的。

主站蜘蛛池模板: 鸡泽县| 马边| 宁夏| 新民市| 思南县| 湘西| 九龙县| 綦江县| 治多县| 乐陵市| 买车| 襄汾县| 峨眉山市| 伊春市| 定陶县| 稻城县| 奎屯市| 达拉特旗| 绿春县| 旬阳县| 团风县| 麦盖提县| 舒城县| 邢台县| 和顺县| 乌拉特中旗| 马鞍山市| 松江区| 马边| 荔浦县| 太康县| 舟山市| 崇信县| 乐昌市| 水城县| 古浪县| 黎平县| 上杭县| 太仆寺旗| 张北县| 田东县|