日本搞逼视频_黄色一级片免费在线观看_色99久久_性明星video另类hd_欧美77_综合在线视频

國內最全IT社區平臺 聯系我們 | 收藏本站
阿里云優惠2
您當前位置:首頁 > php開源 > 綜合技術 > C++11學習

C++11學習

來源:程序員人生   發布時間:2016-10-17 15:31:01 閱讀次數:3820次

C++11學習

本章目的:

當Android用ART虛擬機替換Dalvik的時候,為了表示和Dalvik完全劃清界限的決心,Google連ART虛擬機的實現代碼都切換到了C++11。C+11的標準規范于2011年2月正式落稿,而此前10余年間,C++正式標準1直是C++98/03[①]。相比C++98/03,C++11有了非常多的變化,乃至1度讓筆者大呼不認識C++了[②]。不過,作為科技行業的從業者,我們要銘記在心的1個鐵規就是要擁抱變化。既然我們不認識C++11,那就把它當作1門全新的語言來學習吧。

寫在開頭的話

從2007年到2010年,在我參加工作的頭3年中,筆者1直使用C++作為唯1的開發語言,寫過10幾萬行的代碼。從2010年轉向Android開發后,我才正式接觸Java。爾后很多年里,我曾屢次比較過兩種語言,有了1些很直觀,很感性的看法。此處和大家分享,讀者無妨1看:

對業務系統[③]的開發而言,Java相比C++而言,開發確切方便太多。比如:

  • Java天生就是跨平臺的。開發者無需斟酌操作系統,硬件平臺的差異。而C++開發則高度依賴于操作系統和硬件平臺。比如Windows的C++程序到Linux平臺上幾近都沒法直接使用。這其中的問題倒也不能全賴在C++語言本身上。只是選擇1門開發語言不單單是選擇語言本身,其背后的生態系統(OS,硬件平臺,公共類庫,開發資源,文檔等)隨之也被選擇。
  • 開發者無需斟酌內存管理。雖然Java也有內存泄漏之說,但最少在開發進程中,開發者不用瑣屑較量于C++編程中必須要時刻斟酌的“內存是不是會泄漏”,“對象被delete后是不是會致使其他使用者操作無效內存地址”等問題。
  • 最后也是最重要的1點,Java有非常豐富的類庫,諸如網絡操作類,容器類,并發類,XML解析類等等等等。正是有了這些豐富的類庫,才使得業務系統開發者能聚焦在如何利用這些現成的工具、類庫來開發自己的業務系統,而不是從頭到腳得重復制造車輪。比如,當年我在Windows弄1套C++封裝的多線程工具類,以后移植到Linux上又得弄1套,而且還要花很多精力保護它們。

個人感受:

我個人對C++是沒有任何偏好的。之所以用C++,很大程度上是由于直接領導的選擇。作為1個工作多年的老員工,在他印象里,那個年代的Java性能很差,比不得C++的靈巧和高效。另外,由于我們做得是高性能視音頻數據網絡傳輸(在局域網/廣域網,幾個GB的視音頻文件類似FTP這樣的上傳下載),C++貌似是當時唯1能同時和“面向對象”,“性能不錯”掛上鉤的語言了。

在研究ART的時候,筆者發現其源碼是用1種和我之前熟習得C++差別很大的C++語言編寫得,這類差別乃至1度讓我感嘆“不太認識C++語言了”。后來,我才了解到這類“全新的”C++就是C++11。當時我就在想,包括我自己在內,和本書的讀者們要不要學習它呢?思來覆去,我覺得還是有這個必要:

  • 從Android 6.0源碼來看,native模塊改用C++11來編寫已成趨勢。所以我們需要盡快了解C++11,為將來的學習和工作做準備。
  • 既然C++之父都說“C++11看起來像1門新的語言[6]”,那末我們完全可以把它當作1門新的語言來學習,而不用斟酌是不是有過C/C++基礎的問題。這給了我們1個很好的學習機會。

既然下定決心,那末就馬上開始學習。正式介紹C++11前,筆者要特別強調以下幾點注意事項:

  • 編程語言學習,以實用為主。所以本章所介紹的C++11內容,1切以看懂ART源碼為最高目標。源碼中沒有觸及的C++11知識,本章盡可能不予介紹。1些細枝末節,或精深精尖的用法,筆者也不擬詳述。如果讀者想深入研究,無妨瀏覽本章參考文獻所列出的6本C++專著。
  • 學習是1個按部就班的進程。對初學者而言,應首先以看懂C++11代碼為主,然后才能嘗試模仿著寫,直到完全自己寫。用C++寫程序,會碰到很多所謂的“坑”,只有親歷并吃過虧以后,才能深入掌握這門語言。所以,如果讀者想真正學好C++,那末1定要多寫代碼,不能停留在看懂代碼的水平上。

注意:

最后,本章不是專門來討論C++語法的,它更大的作用在于幫助讀者更快得了解C++。故筆者會嘗試采取1些通俗的語言來介紹它。因此,本章在關于C++語法描寫的精準性上必定會有所不足。在此,筆者1方面請讀者體諒,另外一方面請讀者及時反饋所發現的問題。

下面,筆者將正式介紹C++11,本章擬講授以下內容:

  •  數據類型
  • C++源碼構成及編譯
  •  Class
  • 操作符重載
  • 函數模板與類模板
  •  lambda表達式
  •  STL介紹
  • 其他1些經常使用知識點

1.1  數據類型

學習1門語言,首先從它定義的數據類型開始。本節先介紹C++基本內置的數據類型。

1.1.1  基本內置數據類型介紹

圖1所示為C++中的基本內置數據類型(注意,圖中沒有包括所有的內置數據類型):


圖1  C++基本數據類型

圖1展現了C++語言中幾種經常使用的基本數據類型。有幾點請讀者注意:

  • 由于C++和硬件平臺關聯較大,規范沒辦法像Java那樣嚴格規定每種數據類型所需的字節數,所以它只定義了每種數據類型最少需要多少字節。比如,規范要求1個int型整數最少占據2個字節(不過,絕大部份情況下1個int整數將占據4個字節)。
  • C++定義了sizeof操作符,通過這個操作符可以得到每種數據類型(或某個變量)占據的字節個數。
  • 對浮點數,規范只要求最小的有效數字個數。對單精度浮點數float而言,要求最少支持6個有效數字。對雙精度浮點數double類型而言,要求最少支持10個有效數字。

注意:

本章中,筆者可能會常常拿Java語言做對照。由于了解語言之間的差異更有助于快速掌握1門新的語言。

和Java不同的是,C++中的數據類型分無符號和有符號兩種,比如:


圖2  無符號數據類型定義

注意,無符號類型的關鍵詞為unsigned

1.1.2  指針、援用和void類型

現在來看C++里另外3種經常使用的數據類型:指針、援用和void,如圖3所示:


圖3  指針、援用和void

由圖3可知:

  • 指針類型的書寫格式為T *,其中T為某種數據類型。
  • 援用類型的書寫格式為T &,其中T為某種數據類型。
  • void代表空類型,也就是無類型。這類類型只能用于定義指針變量,比如void*。當我們確切不關注內存中存儲的數據究竟是甚么類型的話,就能夠定義1個void*類型的指針來指向這塊內存。
  • C++11開始,空指針由新關鍵字nullptr[④]表示,類似于Java中的null

下面我們側重介紹1下指針和援用。先來看指針:

1.  指針

關于指針,讀者只需要掌握3個基本知識點就能夠了:

  • 指針的類型。
  • 指針的賦值。
  • 指針的解援用。

(1)  指針的類型

指針本質上代表了虛擬內存的地址。簡單點說,指針就是內存地址。比如,在32位系統上,1個進程的虛擬地址空間為4G,虛擬內存地址從0x00xFFFFFFFF,這個段中的任何1個值都是內存地址。

1個程序運行時,其虛擬內存中會有甚么呢?肯定有數據和代碼。假定某個指針指向1塊內存,該內存存儲的是數據,C++中數據都得有數據類型。所以,指向這塊內存的指針也應當有類型。比如:

int* p,變量p是1個指針,它指向的內存存儲了1個(對數組而言,就是1組)int型數據。

short* p,變量p指向的內存存儲了1個(或1組)short型數據。

如果指針對應的內存中存儲的是代碼的話,那末指向這塊代碼入口地址(代碼常常是封裝在函數里的,代碼的入口就是函數的入口)的指針就叫函數指針。函數指針的定義看起來有些古怪,如圖4所示:


圖4  函數指針定義示例

提示:

函數指針的定義語法看起來比較奇特,筆者也是實踐了很屢次才了解它。

(2)  指針的賦值

定義指針變量后,下1個要斟酌的問題就是給它賦甚么值。來看圖5:


圖5  指針變量的賦值

結合圖5可知,指針變量的賦值有幾種情勢:

  • 直接將1個固定的值(比如0x123456)作為地址賦給指針變量。這類做法很危險。除非明確知道這塊內存的作用和所存儲的內容,否則不能使用這類方法。
  • 通過new操作符在堆上分配1塊內存,該內存的地址存儲在對應的指針變量中。
  • 通過取地址符&對獲得某個變量或函數的地址。

注意

函數指針變量的賦值也能夠直接使用目標函數名,也可以使用取地址符&。2者效果1致

(3)  指針的解援用

指針只是代表內存的某個地址,如何獲得該地址對應內存中的內容呢?C++提供了解指針援用符號*來幫助大家,如圖6所示:


圖6  指針解援用

圖6中:

  • 對數據類型的指針,解援用意味著獲得對應地址中內存的內容。
  • 對函數指針,解援用意味著調用這個函數。

討論:

為何C/C++中會有指針呢?由于C和C++語言作為系統編程(System Programming)語言,出于運行效力的斟酌,它提供了指針這樣的機制讓程序員能夠直接操作內存。固然,這類做法的利弊已討論了幾10年,其主要壞處就在于大部份程序員管不好內存,致使常常出現內存泄漏,訪問異常內存地址等各種問題。

2.  援用

相比C,援用是C++獨有的1個概念。我們來看圖7,它展現了指針和援用的區分:


圖7  援用的用法示例(1)


圖7  援用的用法示例(2)

由圖7可知:

  • 援用只是變量的別名。由因而別名,所以C++要求在定義援用型變量時就必須將它和實際變量綁定。
  • 援用型變量綁定實際變量以后,這兩個變量(原變量和它的援用變量)其實就代表同1個東西了。圖7中(1)以魯迅為例,“魯迅”和“周樹人”都是同1個人。

C語言中沒有援用,1樣工作得很好。那末C++引入援用的目的是甚么呢[⑤]?

  • 既然是別名,那末給原變量換1個更動聽的名字多是1個作用。
  • 比較圖7中(2)的changeRefchangeNoRef可知,當函數的形參為援用時,函數內部對該形參的修改就是對實參的修改。再次強調,對援用類型的形參而言,函數調用時,形參就變成了實參的別名。
  • 比較圖7中(2)的changeRefchangePointers可知,指針型變量書寫起來需要使用解地址援用*符號,不太方便。
  • 援用和原變量是1對1的強關系,而指針則可以任意賦值,乃至還可以通過類型轉換變成別的類型的指針。在實際編碼進程中,1對1的強關系能減少1些毛病的產生。

和Java比較

和Java語言比起來,如果Java中函數的形參是基礎類型(如int,long之類的),則這個形參是傳值的,與圖7中的changeNoRef類似。如果這個函數的形參是類類型,則該形參類似于圖7中的changeRef。在函數內部修改形參的數據,實參的數據相應會被修改。

1.1.3  字符和字符串

圖8所示為字符和字符串的示例:


圖8  字符和字符串示例

請讀者注意圖8中的Raw字符串定義的格式,它的標準格式為R"附加界定符(字符串)附加界定符"。附加界定符可以沒有。而筆者設置圖8中的附加界定符為"**123"。

Raw字符串是C++11引入的,它是為了解決正則表達式里那些煩人的轉義字符\而提供的解決方法。來看看C++之父給出的1個例子,有這樣1個正則表達式('(?:[?\\']|\\.)?'|"(?:[?\\"]|\\.)?")|)

  • 在C++中,如果使用轉義字符串來表達,則變成('(?:[?\\\\']|\\\\.)?'|\"(?:[?\\\\\"]|\\\\.)?\")|。使用轉義字符后,全部字符串變得很難看懂了。
  • 2如果使用Raw字符串,改成R"dfp(('(?:[?\\']|\\.)?'|"(?:[?\\"]|\\.)?")|)dfp"便可。此處使用的界定字符為"dfp"。

很明顯,使用Raw字符串使得代碼看起來更清新,出錯的可能性也下降很多。

1.1.4  數組

直接來看關于數組的1個示例,如圖9所示:


圖9  數組示例

由圖9可知:

  • 定義數組的語法格式為T name[數組大小]。數組大小可以在編譯時由初值列表的個數決定,也能夠是1個常量。總之,這類類型的數組,其數組大小必須在編譯時決定。
  •  動態數組由new的方式在運行時創建。動態數組在定義的時候就能夠通過{}來賦初值。程序中,代表動態數組的是1個對應類型的指針變量。所以,動態數組和指針變量有著天然的關系。

和Java比較

Java中,數組的定義方式是T[]name。筆者覺得這類書寫方式比C++的書寫方式要形象1些。

另外,Java中的數組都是動態數組。

了解完數據類型后,我們來看看C++中源碼構成及編譯相干的知識。

1.2  C++源碼構成及編譯

源碼構成是指如何組織、管理和編譯源碼文件。作為對照,我們先來看Java是怎樣處理的:

  • Java中,代碼只能書寫在以.java為后綴的源文件中。
  • Java中,每個Java源文件必須包括1個和文件同名的class。比如A.java必須定義公然的class A(或是interface A)。
  • 絕大部份情況下,class A隸屬于1個package。所以class A的全路徑名為xx.yy.zz.A。其中,xx.yy.zz是包名。
  • 同1個package下的class B如果要使用class A的話,可以直接使用類A。如果class B位于別的package下的話,那末必須使用A的全路徑名xx.yy.zz.A。固然,為了減少書寫A所屬包名的工作量,class B會通過import xx.yy.zz.A引入全路徑名。然后,B也能直接使用類A了。

綜其所述,源碼構成主要討論兩個問題:

  • 代碼寫在甚么地方?Java中是放入.java為后綴的文件中。
  • 如何解決不同源碼文件中的代碼之間相互援用的問題?Java中,同package下,源文件A的代碼可以直接使用源文件B的內容。不同package下,則必須通過全路徑名訪問另外1個Package下的源文件A的內容(通過import可以減少書寫包名的工作量)。

現在來看C++的做法:

  • 在C++中,承載代碼的文件有頭文件和源文件的區分。頭文件的后綴名1般為.h。也能夠.hpp.hxx結尾。源文件以.cpp.cxx.cc結尾。只要開發者之間約定好,采取甚么情勢的后綴都可以。筆者個人喜歡使用.h.cpp做后綴名,而art源碼則以.h.cc為后綴名。
  • 1般而言,頭文件里聲明需要公然的變量,函數或類。源文件則定義(或說實現)這些變量,函數或類。那些需要使用這些公然內容的代碼可以通過#include方式將其包括進來。注意,由于C++中頭文件和源文件都可以承載代碼,所以頭文件和源文件都可使用#include指令。比如,源文件a.cpp可以#include"b.h",從而使用b.h里聲明的函數,變量或類。頭文件c.h也能夠#include "b.h"

下面我們分別通過頭文件和源文件的幾個示例來強化對它們的認識。

1.2.1  頭文件示例

圖10所示為1個非常簡單頭文件示例:


圖10  Type.h示例

下面來分析圖10中的Type.h:

  • 首先,C++中,頭文件的寫法有1定規則需要遵守。比如圖10中的
  • #ifndef _TYPE_H_:ifndef是if not define之意。_TYPE_H_是宏的名稱。
  • #define _TYPE_H_:表示定義1個名為_TYPE_H_的宏、
  • #endif:和前面的#ifndef對應。

這3個宏合起來的意思是,如果沒有定義_TYPE_H_,則定義它。宏的名字可以任意取,但1般是和頭文件的文件名相干,并且該宏不要和其他宏重名。為何要定義1個這樣的宏呢?其目的是為了避免頭文件的重復包括。

探討:如何避免頭文件重復包括

編譯器處理#include命令的方式就是將被包括的頭文件的內容全部讀取進來。1般而言,這類包括關系非常復雜。比如,a.h可以直接包括b.h和c.h,而b.h也能夠直接包括c.h。如此,a.h相當于直接包括c.h1次,并間接包括c.h(通過b.包括c.h的方式)1次。假定c.h采取和圖101樣的做法,則編譯器在第1次包括c.h(由于a.h直接#include"c.h")的時候將定義_C_H_宏,當編譯器第2次嘗試包括c.h的時候(由于在處理#include "b.h"的時候,會將b.h所include的文件順次包括進來)會發現這個宏已定義了。由于頭文件中所有有價值的內容都是寫在#ifndef#endif之間的,也就是只有在沒有定義_C_H_宏的時候,這個頭文件的內容才會真正被包括進去。通過這類方式,c.h雖然被include兩次,但是只有第1次包括會加載其內容,后續include等于沒有真正加載其內容。

固然,現在的編譯器比較高級,也許可以處理這類重復包括頭文件的問題,但是建議讀者自己寫頭文件的時候還是要定義這樣的宏。

除宏定義以外,圖10中還定義了1個命名空間,名字為my_type。并且在命名空間里還聲明了1個test函數:

  • C++中的命名空間和Java中的package類似,但是要求上要簡單很多。命名空間是1個范圍(Scope),可以出現在任意頭文件,源文件里。凡是放在某個命名空間里的函數,類,變量等就屬于這個命名空間。
  • Type.h只是聲明(declare)了test函數,但沒有這個函數的實現。聲明僅是告知編譯器,我們有1個名叫test的函數。但是這個函數在甚么地方呢?這時候就需要有1個源文件來定義test函數,也就是實現test函數。

下面我們來看1個源文件示例:

1.2.2  源文件示例

源文件示例1如圖11所示:


圖11 Test.cpp示例

圖11是1個名為Test.cpp的示例,在這個示例中:

  • 包括Type.h和TypeClass.h。
  • 調用兩個函數,其中1個函數是Type.h里聲明的test。由于test位于my_type命名空間里,所以需要通過my_type::test方式來調用它。

接著來看圖12:


圖12 Type.cpp

圖12所示為Type.cpp:

  • 從文件名上看,Type.cpp和Type.h可能會有些關系。確切如此。正如前文所說,頭文件1般作聲明用,而真實的實現常常放在源文件中。出于文件管理方便性的斟酌,頭文件和對應的源文件有著相同的文件名。
  • Type.cpp還包括了iostreamiomanip兩個頭文件。需要特別注意的是,這兩個include使用的是尖括號<>,而不是""。根據約定俗成的習慣,尖括號中的頭文件常常是操作系統和C++標準庫提供的頭文件。包括這些頭文件時不用攜帶.h的后綴。比如,#include <iostream>這條語句無需寫成#include <iostream.h>。這是由于C++標準庫的實現是由不同廠商來完成的。具體實現的時候可能頭文件沒有后綴名,或后綴名不是.h。所以,C++規范將這個問題交給編譯器來處理,它會根據情況找到正確的文件。
  • C++標準庫里的內容都定義在1個獨立的命名空間里,這個命名空間叫std。如果需要使用某個命名空間里的東西,比如圖12中的代表標準輸出對象的cout,可以通過std::cout來訪問它,或像圖121樣,通過using std::cout的方式來避免每次都書寫"std::"。固然,也能夠1次性將某個命名空間里的所有內容全部包括進來,方法就是usingnamespace std。這類做法和java的import非常類似。
  • my_type命名空間里包括testchangeRef兩個函數。其中,test函數實現了Type.h中聲明的那個test函數。而由于changeRef完全是在Type.cpp中定義的,所以只有Type.cpp內部才知道這個函數,而外界(其他源文件,頭文件)不知道這個世界上還有1個changeRef函數。在此請讀者注意,1般而言,include指令用于包括頭文件,極少用于包括源文件。
  • Type.cpp還定義了1個changeNoRef函數,此函數是在my_type命名空間以外定義的,所以它不屬于my_type命名空間。

到此,我們通過幾個示例向讀者展現了C++中頭文件和源文件的構成和1些經常使用的代碼寫法。現在看看如何編譯它們。

1.2.3  編譯

C/C++程序1般是通過編寫Makefile來編譯的。Makefile其實就是1個命令的組合,它會根據情況履行不同的命令,包括編譯,鏈接等。Makefile不是C++學習的必備知識點,筆者不擬討論太多,讀者通過圖13做簡單了解便可:


圖13 Makefile示例

圖13中,真實的編譯工作還是由編譯器來完成的。圖13中展現了編譯器的工作步驟和對應的參數。此處筆者僅強調3點:

  • Makefile是1個文件的文件名,該文件由make命令解析并處理。所以,我們可認為Makefile是專門供make命令使用的腳本文件。其內容的書寫規則遵照make命令的要求。
  • C++中,編譯單元是源文件(即.cpp文件)。如圖中所示的內容,編譯命令的輸入都是xxx.cpp源文件,極少有單獨編譯.h頭文件的。
  • 筆者習慣先編譯單個源文件以得到對應的obj文件,然后再鏈接這些obj文件得到終究的目標文件。鏈接的步驟也是由編譯器來完成,只不過其輸入文件從源文件變成了obj文件。

make命令如何履行呢?很簡單:

  • 進入到包括Makfile文件的目錄下,履行make。如果沒有指明Makefile文件名的話,它會以當前目錄下的Makefile文件為輸入。make將解析Makefile文件里定義的任務和它們的依賴關系,然后對任務進行處理。如果沒有指明任務名的話,則履行Makefile中定義的第1個任務。
  • 可以通過make任務名來履行Makefile中的指定任務。比如,圖13中最后兩行定義了clean任務。通過make clean可履行它。clean任務的目標就是刪除臨時文件(比如obj文件)和上1次編譯得到的目標文件。

提示

Makefile和make是1個獨立的知識點,關于它們的故事可以寫出1整本書了。不過,就實際工作而言,開發者常常會把Makefile寫好,或可借助1些工具以自動生成Makefile。所以,如果讀者不了解Makefile的話也不用擔心,只要會履行make命令就能夠了。

1.3  Class介紹

本節介紹C++中面向對象的核心知識點——類(Class)。筆者對類有3點認識:

  • Class是C++構造面向對象世界的核心單元。面向對象在編碼中的直觀體現就是程序員可以用Class封裝成員變量和成員函數。之前用C寫程序的時候,是面向進程的思惟方法,斟酌的是函數和函數之間的調用和跳轉關系。C++出現后,我們看待問題和解決問題的思路產生了很大的變化,更多斟酌是設計適合的類并處理對象和對象之間的關系。固然,面向對象其實不是說程序就沒有進程了。程序總還是有順序,有流程的。但是在這個流程里,開發者更多關注的是對象和對象之間的交互,而不是孤伶伶的函數。
  • 另外,Class還支持抽象,繼承和多態。這些概念完全就是圍繞面向對象來設計和斟酌的,它關注的是類和類之間的關系。
  • 最后,從類型的角度來看,和C++基礎內置數據類型1樣,類也是1種數據類型,只不過它是1種可由開發者自定義的數據類型罷了。

探討:

筆者之前幾近沒有從類型的角度來看待過類。直到接觸模板編程后,才發現類型和類型推導在模板中的重要作用。關于這個問題,我們留待后續介紹模板編程時再繼續討論。

下面我們來看看C++中的Class該怎樣實現。先來看圖14所示的TypeClass.h,它聲明了1個名為Base的類。請讀者重點關注它的語法:


圖14  Base類的聲明

來看圖14的內容:

  • 首先,筆者用class關鍵字聲明了1個名為Base的類。Base類位于type_class命名空間里。
  • C++類有和Java1樣的訪問權限控制,關鍵詞也是publicprivateprotected3種。不過其使用方法和Java略有區分。Java中,每一個成員(包括函數和變量)都需要單獨聲明訪問權限,而C++則是分組控制的。例如,位于"public:"以后的成員都有相同的public訪問權限。如果沒有指明訪問權限,則默許使用private訪問權限。
  • 在類成員的構成上,C++除有構造函數賦值函數析構函數等3大類特殊成員函數外,還可以定義其他成員函數和成員變量。成員變量如圖14中的size變量可以像Java那樣在聲明時就賦初值,但筆者感覺C++的習慣做法還是只聲明成員變量,然后到構造函數中去賦初值。
  • C++中,函數聲明時可以指明參數的默許值,比如deleteC函數,它有3個參數,后面兩個參數均有默許值(參數b的默許值是100,參數test的默許值是true)。

接下來,我們先介紹C++的3大類特殊函數。

注意,

這3類特殊函數其實不是都需要定義。筆者此處羅列它們僅為學習用。

1.3.1  構造,賦值和析構函數

C++類的3種特殊成員函數分別是構造、賦值和析構,其中:

  • 構造函數:當創建類的實例對象時,這個對象的構造函數將被調用。1般在構造函數中做該對象的初始化工作。Java中的類也有構造函數,和C++中的構造函數類似。
  • 賦值函數:賦值函數其實就是指"="號操作符,用于將變量A賦值給同類型(不斟酌類型轉換等情況)的變量B。比如,可以將整型變量(假定變量名為aInt)的值賦給另外一個整型變量bInt。在此基礎上,我們也能夠將類A的某個實例(假定變量名為aA)賦值給類A的另外1個實例bA。請讀者注意,1.3節1開始就強調過,類只不過是1種自定義的數據類型罷了。如果整型變量(或其他基礎內置數據類型)可以賦值的話,類也應當支持賦值操作。
  • 析構函數:當對象的生命走向終結時,它的析構函數將被調用。1般而言,該函數內部會釋放這個對象占據的各種資源。Java中,和析構函數類似的是finalize方法。不過,由于Java實現了內存自動回收機制,所以Java程序員幾近不需要斟酌finalize的事情。

下面,我們分別來討論這3種特殊函數。

1.  構造函數

來看類Base的構造函數,如圖15所示:


圖15  構造函數示例

圖15中的代碼實現于TypeClass.cpp中:

  • 在類聲明以外實現類的成員函數時,需要通過"類名::函數名"的方式告知編譯器這是1個類的成員函數,比如圖15中的Base::Base(int a)
  • 默許構造函數:默許構造函數是指不帶參數或所有參數全部有默許值的構造函數。注意,C++的函數是支持參數帶默許值的,比如圖14中Base類的deleteC函數,
  • 普通構造函數:帶參數的構造函數。
  • 拷貝構造函數:用法如圖15中的所示。詳情可見下文介紹。

下面來介紹圖15中幾個值得注意的知識點:

(1)  構造函數初始值列表

構造函數主要的功能是完成類實例的初始化,也就是對象的成員變量的初始化。C++中,成員變量的初始化推薦使用初始值列表(constructor initialize list)的方法(使用方法如圖15所示),其語法格式為:

構造函數(...):

    成員變量A(A的初值),成員變量B(B的初值){

...//也能夠使用花括號,比如成員變量A{A的初值},成員變量B{B的初值}

}

固然,成員變量的初值設置也能夠通過賦值方式來完成:

構造函數(...){

  成員變量A=A的初值;

  成員變量B=B的初值;

  ....

}

C++中,構造函數中使用初值列表和成員變量賦初值是有區分的,此處不擬詳細討論2者的差異。但推薦使用初值列表的方式,緣由大致有2:

  • 使用初值列表可能運行效力上會有提升。
  • 有些場合必須使用初值列表,比如子類構造函數中初始化基類的成員變量時。后文中將看到這樣的例子。

提示:

構造函數中請使用初值列表的方式來完成變量初始化。

(2)  拷貝構造函數

拷貝構造,即從1個已有的對象拷貝其內容,然后構造出1個新的對象。拷貝構造函數的寫法必須是:

構造函數(const 類& other)

注意,const是C++中的常量修飾符,與Java的final類似。

拷貝進程中有1個問題需要程序員特別注意,即成員變量的拷貝方式是值拷貝還是內容拷貝。以Base類的拷貝構造為例,假定新創建的對象名為B,它用已有的對象A進行拷貝構造:

  • memberA和memberB是值拷貝。所以,A對象的memberA和memberB將賦給B的memberA和memberB。爾后,A、B對象的memberA和memberB值分別相同。
  • 而對pMemberC來講,情況就不1樣了。B.pMemberC和A.pMemberC將指向同1塊內存。如果A對這塊內存進行了操作,B知道嗎?更有甚者,如果A刪除這塊內存,而B還繼續操作它的話,豈不是會崩潰?所以,對這類情況,拷貝構造函數中使用了所謂的深拷貝(deepcopy),也就是將A.pMemberC的內容拷貝到B對象中(B先創建1個大小相同的數組,然后通過memcpy進行內存的內容拷貝),而不是簡單的進行賦值(這類方式叫淺拷貝,shallow copy)。

值拷貝、內容拷貝和淺拷貝、深拷貝

由上述內容可知,淺拷貝對應于值拷貝,而深拷貝對應于內容拷貝。對非指針變量類型而言,值拷貝和內容拷貝沒有區分,但對指針型變量而言,值拷貝和內容拷貝差別就很大了。

圖16解釋了深拷貝和淺拷貝的區分:


圖16  淺拷貝和深拷貝的區分

圖16中,淺拷貝用紅色箭頭表示,深拷貝用紫色箭頭表示:

  • 淺拷貝最明顯的問題就是A和B的pMemberC將指向同1塊內存。絕大多數情況下,淺拷貝的結果絕不是程序員想要的。
  • 采取深拷貝的話,A和B將具有相同的內容,但彼此之間不再有任何糾葛。
  • 對非指針型變量而言,深拷貝和淺拷貝沒有甚么區分,其實就是值的拷貝

最后,筆者還要特別說明拷貝構造函數被觸發的場合。來看代碼:

Base A; //構造A對象

Base B(A);// 直接用A對象來構造B對象,這類情況是“直接初始化”

Base C = A;// 定義C的時候即賦值,這是真正意義上的拷貝構造。2者的區分見下文介紹。

除上述兩種情況外,還有1些場合也會致使拷貝構造函數被調用,比如:

  • 當函數的參數為非援用的類類型時,調用這個函數并傳遞實參時,實參的拷貝構造函數被調用。
  • 函數的返回類型為1個非援用的對象時,該對象的拷貝構造函數被調用。

直接初始化和拷貝初始化的細微區分

Base B(A)只是致使拷貝構造函數被調用,但其實不是嚴格意義上的拷貝構造,由于:

  1. Base確切定義了1個形參為constB&的構造函數。而B(A)的語法恰好滿足這個函數,所以這個構造函數被調用是天經地義的。這樣的構造是很直接的,沒有任何疑義的,所以叫直接初始化。
  2. 而對Base C = A的理解卻是將A的內容拷貝到正在創建的C對象中,這里包括了拷貝和構造兩個概念,即拷貝A的內容來構造C。所以叫拷貝構造。慚愧得說,筆者也很難描寫上述內容在語法上的精確含義。不過,從使用角度來看,讀者只需記住這兩種情況均會致使拷貝構造函數被調用便可。

2.  拷貝賦值函數

拷貝賦值函數是賦值函數的1種,我們先來思考下賦值函數解決甚么問題。請讀者思考下面這段代碼:

int a = 0;

int b = a;//將a賦值給b

所有讀者應當對上述代碼都不會有任何疑問。是的,對基本內置數據類型而言,賦值操作仿佛是天經地義的公道,但對類類型呢?比以下面的代碼:

Base A;//構造1個對象A

Base B; //構造1個對象B

B = A; //A可以賦值給B嗎?

從類型的角度來看,沒有理由不允許類這類自定義數據類型的進行賦值操作。但是從面向對象角度來看,把1個對象賦值給另外1個對象會得到甚么?現實生活中仿佛也難以到類似的場景來比擬它。

不管怎樣,C++是支持1個對象賦值給另外一個對象的。現在把注意力回歸到拷貝賦值上來,來看圖17所示的代碼:


圖17  拷貝賦值函數示例

賦值函數本身沒有甚么難度,不過就是在準備接受另外1個對象的內容前,先把自己清算干凈。另外,賦值函數的關鍵知識點是利用了C++中的操作符重載(Java不支持操作符重載)。關于操作符重載的知識請讀者瀏覽本文后續章節。

3.  移動構造和移動賦值函數

前面兩節介紹了拷貝構造和拷貝賦值函數,還了解了深拷貝和淺拷貝的區分。但關于構造和賦值的故事并沒有完。由于C++11中,除拷貝構造和拷貝賦值以外,還有移動構造和移動賦值。

注意

這幾個名詞中:構造和賦值并沒有變,變化的是構造和賦值的方法。前2節介紹的是拷貝之法,本節來看移動之法。

(1)  移動之法的解釋

圖18展現了移動的含義:


圖18  Move的示意

對照圖16和圖18,讀者會發現移動的含義其實非常簡單,就是把A對象的內容移動到B對象中去:

  • 對memberA和memberB而言,由于它們是非指針類型的變量,移動和拷貝沒有不同。
  • 但對pMemberC而言,差別就很大了。如果使用拷貝之法,A和B對象將各自有1塊內存。如果使用移動之法,A對象將不再具有這塊內存,反而是B對象具有A對象之前具有的那塊內存。

移動的含義好像不是很難。不過,讓我們更進1步思考1個問題:移動以后,A、B對象的命運會產生怎樣的改變?

  • 很簡單,B自然是得到A的全部內容。
  • A則掏空自己,成為無用之物。注意,A對象還存在,但是你最好不要碰它,由于它的內容早已移交給了B。

移動以后,A竟然無用了。甚么場合會需要如此“殘暴”的做法?還是讓我們用示例來論述C++11推出移動之法的目的吧:


圖19  有Move和沒有Move的區分

圖19中,左上角是示例代碼:

  • test函數:將getTemporyBase函數的返回值賦給1個名為a的Base實例。
  • getTemporyBase函數:構造1個Base對象tmp并返回它。

圖19展現了沒有定義移動構造函數和定義了移動構造函數時該程序運行后打印的日志。同時圖中還解釋了履行的進程。結合前文所述內容,我們發現tmp確切是1種轉移出去(不論是采取移動還是拷貝)后就不需要再使用的對象了。對這類情況,移動構造所帶來的好處是不言而喻的。

注意:

對圖中的測試函數,現在的編譯器已能做到高度優化,以致于圖中列出的移動或拷貝調用都不需要了。為了到達圖中的效果,編譯時必須加上-fno-elide-constructors標志以制止這類優化。讀者無妨1試。

下面,我們來看看代碼中是如何體現移動的。

(2)  移動之法的代碼實現和左右值介紹

圖20所示為Base的移動構造和移動賦值函數:


圖20  移動構造和移動賦值示例

圖20中,請讀者特別注意Base類移動構造和移動賦值函數的參數的類型,它是Base&&。沒錯,是兩個&&符號:

  • 如果是Base&&(兩個&&符號),則表示是Base的右值援用類型。
  • 如果是Base&(1個&符號),則表示是Base的援用類型。和右值援用相比,這類援用也叫左值援用。

甚么是左值,甚么是右值?筆者不擬討論它們詳細的語法和語義。不過,根據參考文獻[5]所述,讀者掌握以下識便可:

  • 左值是著名字的,并且可以取地址。
  • 右值是無名的,不能取地址。比如圖19中getTemporyBase返回的那個臨時對象就是無名的,它就是右值。

我們通過幾行代碼來加深對左右值的認識:

int a,b,c; //a,b,c都是左值

c = a+b; //c是左值,但是(a+b)卻是右值,由于&(a+b)取地址不合法

getTemporyBase();//返回的是1個無名的臨時對象,所以是右值

Base && x = getTemoryBase();//通過定義1個右值援用類型x,getTemporyBase函數返回

//的這個臨時無名對象從此有了x這個名字。不過,x還是右值嗎?答案為

Base y = x;//此處不會調用移動構造函數,而是拷貝構造函數。由于x是著名的,所以它不再是右值。

如果讀者想了解更多關于左右值的區分,請瀏覽本章所列的參考書籍。此處筆者再強調1下移動構造和賦值函數在甚么場合下使用的問題,請讀者注意掌控兩個關鍵點:

  • 第1,如果肯定被轉移的對象(比如圖19中的tmp對象)不再使用,就能夠使用移動構造/賦值函數來提升運行效力。
  • 第2,我們要保證移動構造/賦值函數被調用,而不是拷貝構造/賦值函數被調用。例如,上述代碼中Base y = x這段代碼實際上觸發了拷貝構造函數,這不是我們想要的。為此,我們需要強迫使用移動構造函數,方法為Base y = std::move(x)move是std標準庫提供的函數,用于將參數類型強迫轉換為對應的右值類型。通過move函數,我們表達了強迫使用移動函數的想法。

如果沒有定義移動函數怎樣辦?

如果類沒有定義移動構造或移動賦值函數,編譯器會調用對應的拷貝構造或拷貝賦值函數。所以,使用std::move不會帶來甚么副作用,它只是表達了要使用移動之法的欲望。

4.  析構函數

最后,來看類中最后1類特殊函數,即析構函數。當類的實例到達生命終點時,析構函數將被調用,其主要目的是為了清算該實例占據的資源。圖21所示為Base類的析構函數示例:


圖21  析構函數示例

Java中與析構函數類似的是finalize函數。但絕大多數情況下,Java程序員不用關心它。而C++中,我們需要知道析構函數甚么時候會被調用:

2 棧上創建的類實例,在退出作用域(比如函數返回,或離開花括號包圍起來的某個作用域)之前,該實例會被析構。

2 動態創建的實例(通過new操作符),當delete該對象時,其析構函數會被調用。

 

1.  總結

1.3.1節介紹了C++中1個普通類的大致組成元素和其中1些特殊的成員函數,比如:

  • 構造函數,分為默許構造,普通構造,拷貝構造和移動構造。
  • 賦值函數,分為拷貝賦值和移動賦值。請讀者先從原理上理解拷貝和移動的區分和它們的目的。
  • 析構函數。

1.3.2  類的派生和繼承

C++中與類的派生、繼承相干的知識比較復雜,相對瑣碎。本節中,筆者擬將精力放在1些相對基礎的內容上。先來看1個派生和繼承的例子,如圖22所示:


圖22  派生和繼承示例

圖22中:

  • 右側居中方框定義了1個Base類,它和圖14中的內容1樣。
  • 右下方框定義了1個VirtualBase類,它包括構造函數,虛析構函數,虛函數test1,純虛函數test2和1個普通函數test3。
  • 左側方框定義了1個Derived類,它同時從Base和VirtualBase類派生,屬于多重繼承。
  • 圖中給出了10個需要讀者注意的函數和它們的簡單介紹。

和Java比較

Java中雖然沒有類的多重繼承,但1個類可以實現多個接口(Interface),這其實也算是多重繼承了。相比Java的這類設計,筆者覺得C++中類的多重繼承太過靈活,使用時需要特別謹慎,否則菱形繼承的問題很難避免。

現在,先來看1下C++中派生類的寫法。如圖22所示,Derived類繼承關系的語法以下:

class  Derived:private Base,publicVirtualBase{

}

其中:

  • classDerived以后的冒號是派生列表,也就是基類列表,基類之間用逗號隔開。
  • 派生有publicprivateprotected3種方式。其意義和Java中的類派生方式差不多,大抵都是用于控制派生類有何種權限來訪問繼承得到的基類成員變量和成員函數。注意,如果沒有指定派生方式的話,默許為private方式。

了解C++中如何編寫派生類后,下1步要關注面向對象中兩個重要特性——多態和抽象是如何在C++中體現的。

注意:

筆者此地方說的抽象是狹義的,和語言相干的,比如Java中的抽象類。

1.  虛函數、純虛函數和虛析構函數

Java語言里,多態是借助派生類重寫(override)基類的函數來表達,而抽象則是借助抽象類(包括抽象方法)或接口來實現。而在C++中,虛函數純虛函數就是用于描寫多態和抽象的利器:

  • 虛函數:基類定義虛函數,派生類可以重寫(override)它。當我們具有1個派生類對象,但卻是通過基類援用類型基類指針類型的變量來調用該對象的虛函數時,被調用的虛函數是派生類重寫過的虛函數(如果該虛函數被派生類重寫了的話)。
  • 純虛函數:具有純虛函數的類不能實例化。從這1點看,它和Java的抽象類和接口非常類似。

C++中,虛函數和純虛函數需要明確標示出來,以VirtualBase為例,相干語法以下:

virtual voidtest1(bool test); //虛函數由virtual標示

virtual voidtest2(int x, int y) = 0;//純虛函數由"virtual"和"=0"同時標示

派生類如何override這些虛函數呢?來看Derived類的寫法:

/*

基類里定義的虛函數在派生類中也是虛函數,所以,下面語句中的virtual關鍵詞不是必須要寫的,

override關鍵詞是C++11新引入的標識,和Java中的@Override類似。

override也不是必須要寫的關鍵詞。但加上它后,編譯器將做1些有用的檢查,所以建議開發者

在派生類中重寫基類虛函數時都加上這個關鍵詞

*/

virtual void test1(bool test)  override;//可以加virtual關鍵詞,也能夠不加

void test2(int x, int y)  override;//如上,建議加上override標識

注意,virtual和override標示只在類中聲明函數時需要。如果在類外實現該函數,則其實不需要這些關鍵詞,比如:

TypeClass.h

class Derived ....{

  .......

  voidtest2(int x, int y) override;//可以不加virtual關鍵字

}    

TypeClass.cpp

void Derived::test2(int x, int y){//類外定義這個函數,不能加virtual等關鍵詞

    cout<<"in Derived::test2"<<endl;

}

提示:

注意,art代碼中,派生類override基類虛函數時,大都會添加virtual關鍵詞,有時候也會加上override關鍵詞。根據參考文獻[1]的建議,派生類重寫虛函數時候最好添加override標識,這樣編譯器能做1些額外檢查而能提早發現1些毛病。

除上述兩類虛函數外,C++中還有虛析構函數。虛析構函數其實就是虛函數,不過它略微有1點特殊,需要開發者注意:

  • 虛函數被override的時候,基類和派生類聲明的虛函數在函數名,參數等信息上需保持1致。但對析構函數而言,由于析構函數的函數名必須是"~類名",所以派生類和基類的析構函數名肯定是不同的。
  • 但是,我們又希望多態對析構函數(注意,析構函數也是函數,和普通函數沒甚么區分)也是可行的。比如,當通過基類指針來刪除派生類對象時,是派生類對象的析構函數被調用。所以,當基類中如果有虛函數時候,1定要記得將其析構函數變成虛析構函數。

禁止虛函數被override

C++中,也能夠禁止某個虛函數被override,方法和Java類似,就是在函數聲明后添加final關鍵詞,比如

virtual void test1(boolean test) final;//如此,test1將不能被派生類override了

最后,我們通過1段示例代碼來加深對虛函數的認識,如圖23所示:


圖23  虛函數測試示例

圖23是筆者編寫的1個很簡單的例子,左側是代碼,右側是運行結果。簡而言之:

  • 如果想實現多態,就在基類中為需要多態的函數增加virtual關鍵詞。
  • 如果基類中有虛函數,也請同時為基類的析構函數添加virtual關鍵詞。只有這樣,指向派生類對象的基類指針變量被delete時,派生類的析構函數才能被調用。

提示:

1 請讀者嘗試修改測試代碼,然后視察打印結果。

2 讀者可將圖23中代碼的最后1行改寫成pvb->~VirtualBase(),即直接調用基類的析構函數,但由于它是虛析構函數,所以運行時,~Derived()將先被調用。

 

2.  構造和析構函數的調用次序

類的構造函數在類實例被創建時調用,而析構函數在該實例被燒毀時調用。如果該類有派生關系的話,其基類的構造函數和析構函數也將被順次調用到,那末,這個順次的順序是甚么?

  • 對構造函數而言,基類的構造函數先于派生類構造函數被調用。如果派生類有多個基類,則基類依照它們在派生列表里的順序調用各自的構造函數。比如Derived派生列表中基類的順序是:先Base,然后是VirtualBase。所以Base的構造函數先于VirtualBase調用,最后才是Derived的構造函數。
  • 析構函數則是相反的進程,即派生類析構函數先被調用,然后再調用基類的析構函數。如果是多重繼承的話,基類依照它們在派生列表里出現的相反次序調用各自的析構函數。比如Derived類實例析構時,Derived析構函數先調用,然后VirtualBase析構,最后才是Base的析構。

補充內容:

如果派生類含有類類型的成員變量時,調用次序將變成:

構造函數:基類構造->派生類中類類型成員變量構造->派生類構造

析構函數:派生類析構->派生類中類類型成員變量析構->基類析構

多重派生的話,基類依照派生列表的順序/反序構造或析構

3.  編譯器合成的函數

Java中,如果程序員沒有為類編寫構造函數函數,則編譯器會為類隱式創建1個不帶任何參數的構造函數。這類編譯器隱式創建1些函數的行動在C++中也存在,只不過C++中的類有構造函數,賦值函數,析構函數,所以情況會復雜1些,圖24描寫了編譯器合成特殊函數的規則:


圖24  編譯器合成特殊函數的規則

圖24的規矩可簡單總結為:

  •  如果程序員定義了任何1種類型的構造函數(拷貝構造、移動構造,默許構造,普通構造),則編譯器將不再隱式創建默許構造函數
  • 如果程序沒有定義拷貝(拷貝賦值或拷貝構造)函數或析構函數,則編譯器將隱式合成對應的函數。
  • 如果程序沒有定義移動(移動賦值或移動構造)函數,并且,程序沒有定義析構函數或拷貝函數(拷貝構造和拷貝賦值),則編譯器將合成對應的移動函數。

從上面的描寫可知,C++中編譯器合成特殊函數的規則是比較復雜的。即便如此,圖24中展現的規則還僅是冰山1角。以移動函數的合成而言,即便圖中的條件滿足,編譯器也未必能合成移動函數,比如類中有沒有法移動的成員變量時。

關于編譯器合成規則,筆者個人感覺開發者應當以實際需求為動身點,如果確切需要移動函數,則在類聲明中定義就行。

(1)  =default和=delete

有些時候我們需要1種方法來控制編譯器這類自動合成的行動,控制的目的無外乎兩個:

  • 讓編譯器必須合成某些函數。
  • 制止編譯器合成某些函數。

借助=default=delete標識,這兩個目的很容易到達,來看1段代碼:

//定義了1個普通的構造函數,但同時也想讓編譯器合成默許的構造函數,則可使用=default標識

Base(int x); //定義1個普通構造函數后,編譯器將停止自動合成默許的構造函數

//=default后,強迫編譯器合成默許的構造函數。注意,開發者不用實現該函數

Base() = default;//通知編譯器來合成這個默許的構造函數

//如果不想讓編譯器合成某些函數,則使用= delete標識

Base&operator=(const Base& other) = delete;//禁止編譯合成拷貝賦值函數

注意,這類控制行動只針對構造、賦值和析構等3類特殊的函數。

(2)  “繼承”基類的構造函數

1般而言,派生類可能希望有著和基類類似的構造方法。比如,圖25所示的Base類有3種普通構造方法。現在我們希望Derived也能支持通過這3種方式來創建Derived類實例。怎樣辦?圖25展現了兩種方法:


圖25  派生類“繼承”基類構造函數

  • 第1種方法就是在Derived派生類中手動編寫3個構造函數,這3個構造函數和Base類里的1樣。
  • 另外1種方法就是通過使用using關鍵詞“繼承”基類的那3個構造函數。繼承以后,編譯器會自動合成對應的構造函數。

注意,這類“繼承”實際上是1種編譯器自動合成的規則,它僅支持合成普通的構造函數。而默許構造函數,移動構造函數,拷貝構造函數等遵守正常的規則來合成。

探討

前述內容中,我們向讀者展現了C++中編譯器合成1些特殊函數的做法和規則。實際上,編譯器合成的規則比本節所述內容要復雜很多,建議感興趣的讀者瀏覽參考文獻來展開進1步的學習。

另外,實際使用進程中,開發者不能完全依賴于編譯器的自動合成,有些細節問題必須由開發者自己先回答。比如,拷貝構造時,我們需要深拷貝還是淺拷貝?需不需要支持移動操作?在取得這些問題答案的基礎上,讀者再結合編譯器合成的規則,然后才選擇由編譯器來合成這些函數還是由開發者自己來編寫它們。

1.3.3  友元和類的前向聲明

前面我們提到過,C++中的類訪問其實例的成員變量或成員函數的權限控制上有著和Java類似的關鍵詞,如publicprivateprotected。嚴格遵照“信息該公然的要公然,不該公然的1定不公然”這1封裝的最高原則無疑是1件好事,但現實生活中的情況是如此變化萬端,有時候我們也需要破個例。比如,熟人之間是不是可以公然1些信息以避開如果按“公事公辦”走流程所帶來的太高溝通本錢的問題?

C++中,借助友元,我們可以做到小范圍的公然信息以減少溝通本錢。從編程角度來看,友元的作用不過是:提供1種方式,使得類外某些函數或某些類能夠訪問1個類的私有成員變量或成員函數。對被訪問的類而言,這些類外函數或類,就是被訪問的類的朋友

來看友元的示例,如圖26所示:


圖26  類的友元示意

圖26展現了如作甚某個類指定它的“朋友們”,C++中,類的友元可以是:

  • 1個類外的函數或1個類中的某些成員函數。如果友元是函數,則必須指定該函數的完全信息,包括返回值,參數,屬于哪一個類等。
  • 1個類。

基類的友元會變成從該基類派生得來的派生類的友元嗎?

C++中,友元關系不能繼承,也就是說:

1 基類的友元可以訪問基類非公然成員,也能訪問派生類中屬于基類的非公然成員。

2 但是不能訪問派生類自己定義的非公然成員。

友元比較簡單,此處就不擬多說。現在我們介紹下圖26中提到的類的前向聲明,先來回顧下代碼:

class Obj;//類的前向聲明

void accessObj(Obj& obj);

C++中,數據類型應當先聲明,然后再使用。但這會帶來1個“先有雞還是先有蛋”的問題:

  • accessObj函數的參數中用到了Obj。但是類Obj的聲明卻放在圖26的最后。
  • 如果把Obj的聲明放在accessObj函數的前面,這又沒法把accessObj指定為Obj的友元。由于友元必須要指定完全的函數。

怎樣破解這個問題?這就用到了類的前向聲明,以圖26為例,Obj前向聲明的目的就是告知類型系統,Obj是1個class,不要把它當作別的甚么東西。1般而言,類的前向聲明的用法以下:

  • 假定頭文件b.h中需要引入a.h頭文件中定義的類A。但是我們不想在b.h里包括a.h。由于a.h可能太復雜了。如果b.h里包括a.h,那末所有包括b.h的地方都間接包括了a.h。此時,通過引入A的前向聲明,b.h中可使用類A。
  • 注意,類的前向聲明1種聲明,真正使用的時候還得包括類A所在的頭文件a.h。比如,b.cpp(b.h相對應的源文件)是真正使用該前向聲明類的地方,那末只要在b.cpp里包括a.h便可。

這就是類的前向聲明的用法,即在頭文件里進行類的前向聲明,在源文件里去包括該類的頭文件。

類的前向聲明的局限

前向聲明好處很多,但同時也有限制。以Obj為例,在看到Obj完全定義之前,不能聲明Obj類型的變量(包括類的成員變量),但是可以定義Obj援用類型或Obj指針類型的變量。比如,你沒法在圖26中class Obj類代碼之前定義ObjaObj這樣的變量。只能定義Obj& refObjObj* pObj。之所以有這個限制,是由于定義Obj類型變量的時候,

生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈
程序員人生
------分隔線----------------------------
分享到:
------分隔線----------------------------
關閉
程序員人生
主站蜘蛛池模板: av在线毛片 | 国产美女被遭强高潮免费网站 | 亚洲黄色片子 | 日韩一级视频 | 国产成人在线一区二区 | 日韩av免费在线观看 | 久久精品九九 | 红桃视频成人免费网站 | 国产又黄又爽又色在线视频播放 | 精品国产乱码久久久久久图片 | 久久久美女 | 欧美在线播放一区 | 亚洲精品一区久久久久久 | 第一福利在线 | 国产福利在线播放 | 国产精品久久久久久久第一福利 | 精品一区二区三区免费 | 福利片一区二区 | 在线观看av资源 | 不卡在线一区 | 五月婷婷综合色拍 | 久久久久久免费毛片精品 | 亚洲视频在线观看网站 | 亚洲人成网站b2k3cm | 一区二区自拍 | 国产精品无码久久久久 | 91久久国产综合久久91精品网站 | 成片在线| 婷婷精品视频 | 欧美黑人xxxxx | 久久国产精品免费一区二区三区 | www.日韩欧美 | 精品国产乱码久久久久久蜜柚 | 欧美激情综合五月色丁香小说 | 俄罗斯精品一区二区三区 | 国产一区二区三区四区www. | 久久综合成人精品亚洲另类欧美 | 国产在线观看一区二区 | 欧日韩在线观看 | 国产精品日韩欧美一区二区 | 国产精品久久久久久久久免费相片 |