本章目的:
當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++而言,開發確切方便太多。比如:
個人感受:
我個人對C++是沒有任何偏好的。之所以用C++,很大程度上是由于直接領導的選擇。作為1個工作多年的老員工,在他印象里,那個年代的Java性能很差,比不得C++的靈巧和高效。另外,由于我們做得是高性能視音頻數據網絡傳輸(在局域網/廣域網,幾個GB的視音頻文件類似FTP這樣的上傳下載),C++貌似是當時唯1能同時和“面向對象”,“性能不錯”掛上鉤的語言了。
在研究ART的時候,筆者發現其源碼是用1種和我之前熟習得C++差別很大的C++語言編寫得,這類差別乃至1度讓我感嘆“不太認識C++語言了”。后來,我才了解到這類“全新的”C++就是C++11。當時我就在想,包括我自己在內,和本書的讀者們要不要學習它呢?思來覆去,我覺得還是有這個必要:
既然下定決心,那末就馬上開始學習。正式介紹C++11前,筆者要特別強調以下幾點注意事項:
注意:
最后,本章不是專門來討論C++語法的,它更大的作用在于幫助讀者更快得了解C++。故筆者會嘗試采取1些通俗的語言來介紹它。因此,本章在關于C++語法描寫的精準性上必定會有所不足。在此,筆者1方面請讀者體諒,另外一方面請讀者及時反饋所發現的問題。
下面,筆者將正式介紹C++11,本章擬講授以下內容:
學習1門語言,首先從它定義的數據類型開始。本節先介紹C++基本內置的數據類型。
圖1所示為C++中的基本內置數據類型(注意,圖中沒有包括所有的內置數據類型):
圖1 C++基本數據類型
圖1展現了C++語言中幾種經常使用的基本數據類型。有幾點請讀者注意:
注意:
本章中,筆者可能會常常拿Java語言做對照。由于了解語言之間的差異更有助于快速掌握1門新的語言。
和Java不同的是,C++中的數據類型分無符號和有符號兩種,比如:
圖2 無符號數據類型定義
注意,無符號類型的關鍵詞為unsigned。
現在來看C++里另外3種經常使用的數據類型:指針、援用和void,如圖3所示:
圖3 指針、援用和void
由圖3可知:
下面我們側重介紹1下指針和援用。先來看指針:
關于指針,讀者只需要掌握3個基本知識點就能夠了:
指針本質上代表了虛擬內存的地址。簡單點說,指針就是內存地址。比如,在32位系統上,1個進程的虛擬地址空間為4G,虛擬內存地址從0x0到0xFFFFFFFF,這個段中的任何1個值都是內存地址。
1個程序運行時,其虛擬內存中會有甚么呢?肯定有數據和代碼。假定某個指針指向1塊內存,該內存存儲的是數據,C++中數據都得有數據類型。所以,指向這塊內存的指針也應當有類型。比如:
2 int* p,變量p是1個指針,它指向的內存存儲了1個(對數組而言,就是1組)int型數據。
2 short* p,變量p指向的內存存儲了1個(或1組)short型數據。
如果指針對應的內存中存儲的是代碼的話,那末指向這塊代碼入口地址(代碼常常是封裝在函數里的,代碼的入口就是函數的入口)的指針就叫函數指針。函數指針的定義看起來有些古怪,如圖4所示:
圖4 函數指針定義示例
提示:
函數指針的定義語法看起來比較奇特,筆者也是實踐了很屢次才了解它。
定義指針變量后,下1個要斟酌的問題就是給它賦甚么值。來看圖5:
圖5 指針變量的賦值
結合圖5可知,指針變量的賦值有幾種情勢:
注意
函數指針變量的賦值也能夠直接使用目標函數名,也可以使用取地址符&。2者效果1致
指針只是代表內存的某個地址,如何獲得該地址對應內存中的內容呢?C++提供了解指針援用符號*來幫助大家,如圖6所示:
圖6 指針解援用
圖6中:
討論:
為何C/C++中會有指針呢?由于C和C++語言作為系統編程(System Programming)語言,出于運行效力的斟酌,它提供了指針這樣的機制讓程序員能夠直接操作內存。固然,這類做法的利弊已討論了幾10年,其主要壞處就在于大部份程序員管不好內存,致使常常出現內存泄漏,訪問異常內存地址等各種問題。
相比C,援用是C++獨有的1個概念。我們來看圖7,它展現了指針和援用的區分:
圖7 援用的用法示例(1)
圖7 援用的用法示例(2)
由圖7可知:
C語言中沒有援用,1樣工作得很好。那末C++引入援用的目的是甚么呢[⑤]?
和Java比較
和Java語言比起來,如果Java中函數的形參是基礎類型(如int,long之類的),則這個形參是傳值的,與圖7中的changeNoRef類似。如果這個函數的形參是類類型,則該形參類似于圖7中的changeRef。在函數內部修改形參的數據,實參的數據相應會被修改。
圖8所示為字符和字符串的示例:
圖8 字符和字符串示例
請讀者注意圖8中的Raw字符串定義的格式,它的標準格式為R"附加界定符(字符串)附加界定符"。附加界定符可以沒有。而筆者設置圖8中的附加界定符為"**123"。
Raw字符串是C++11引入的,它是為了解決正則表達式里那些煩人的轉義字符\而提供的解決方法。來看看C++之父給出的1個例子,有這樣1個正則表達式('(?:[?\\']|\\.)?'|"(?:[?\\"]|\\.)?")|)
很明顯,使用Raw字符串使得代碼看起來更清新,出錯的可能性也下降很多。
直接來看關于數組的1個示例,如圖9所示:
圖9 數組示例
由圖9可知:
和Java比較
Java中,數組的定義方式是T[]name。筆者覺得這類書寫方式比C++的書寫方式要形象1些。
另外,Java中的數組都是動態數組。
了解完數據類型后,我們來看看C++中源碼構成及編譯相干的知識。
源碼構成是指如何組織、管理和編譯源碼文件。作為對照,我們先來看Java是怎樣處理的:
綜其所述,源碼構成主要討論兩個問題:
現在來看C++的做法:
下面我們分別通過頭文件和源文件的幾個示例來強化對它們的認識。
圖10所示為1個非常簡單頭文件示例:
圖10 Type.h示例
下面來分析圖10中的Type.h:
這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函數:
下面我們來看1個源文件示例:
源文件示例1如圖11所示:
圖11 Test.cpp示例
圖11是1個名為Test.cpp的示例,在這個示例中:
接著來看圖12:
圖12 Type.cpp
圖12所示為Type.cpp:
到此,我們通過幾個示例向讀者展現了C++中頭文件和源文件的構成和1些經常使用的代碼寫法。現在看看如何編譯它們。
C/C++程序1般是通過編寫Makefile來編譯的。Makefile其實就是1個命令的組合,它會根據情況履行不同的命令,包括編譯,鏈接等。Makefile不是C++學習的必備知識點,筆者不擬討論太多,讀者通過圖13做簡單了解便可:
圖13 Makefile示例
圖13中,真實的編譯工作還是由編譯器來完成的。圖13中展現了編譯器的工作步驟和對應的參數。此處筆者僅強調3點:
make命令如何履行呢?很簡單:
提示
Makefile和make是1個獨立的知識點,關于它們的故事可以寫出1整本書了。不過,就實際工作而言,開發者常常會把Makefile寫好,或可借助1些工具以自動生成Makefile。所以,如果讀者不了解Makefile的話也不用擔心,只要會履行make命令就能夠了。
本節介紹C++中面向對象的核心知識點——類(Class)。筆者對類有3點認識:
探討:
筆者之前幾近沒有從類型的角度來看待過類。直到接觸模板編程后,才發現類型和類型推導在模板中的重要作用。關于這個問題,我們留待后續介紹模板編程時再繼續討論。
下面我們來看看C++中的Class該怎樣實現。先來看圖14所示的TypeClass.h,它聲明了1個名為Base的類。請讀者重點關注它的語法:
圖14 Base類的聲明
來看圖14的內容:
接下來,我們先介紹C++的3大類特殊函數。
注意,
這3類特殊函數其實不是都需要定義。筆者此處羅列它們僅為學習用。
C++類的3種特殊成員函數分別是構造、賦值和析構,其中:
下面,我們分別來討論這3種特殊函數。
來看類Base的構造函數,如圖15所示:
圖15 構造函數示例
圖15中的代碼實現于TypeClass.cpp中:
下面來介紹圖15中幾個值得注意的知識點:
構造函數主要的功能是完成類實例的初始化,也就是對象的成員變量的初始化。C++中,成員變量的初始化推薦使用初始值列表(constructor initialize list)的方法(使用方法如圖15所示),其語法格式為:
構造函數(...):
成員變量A(A的初值),成員變量B(B的初值){
...//也能夠使用花括號,比如成員變量A{A的初值},成員變量B{B的初值}
}
固然,成員變量的初值設置也能夠通過賦值方式來完成:
構造函數(...){
成員變量A=A的初值;
成員變量B=B的初值;
....
}
C++中,構造函數中使用初值列表和成員變量賦初值是有區分的,此處不擬詳細討論2者的差異。但推薦使用初值列表的方式,緣由大致有2:
提示:
構造函數中請使用初值列表的方式來完成變量初始化。
拷貝構造,即從1個已有的對象拷貝其內容,然后構造出1個新的對象。拷貝構造函數的寫法必須是:
構造函數(const 類& other)
注意,const是C++中的常量修飾符,與Java的final類似。
拷貝進程中有1個問題需要程序員特別注意,即成員變量的拷貝方式是值拷貝還是內容拷貝。以Base類的拷貝構造為例,假定新創建的對象名為B,它用已有的對象A進行拷貝構造:
值拷貝、內容拷貝和淺拷貝、深拷貝
由上述內容可知,淺拷貝對應于值拷貝,而深拷貝對應于內容拷貝。對非指針變量類型而言,值拷貝和內容拷貝沒有區分,但對指針型變量而言,值拷貝和內容拷貝差別就很大了。
圖16解釋了深拷貝和淺拷貝的區分:
圖16 淺拷貝和深拷貝的區分
圖16中,淺拷貝用紅色箭頭表示,深拷貝用紫色箭頭表示:
最后,筆者還要特別說明拷貝構造函數被觸發的場合。來看代碼:
Base A; //構造A對象
Base B(A);// ①直接用A對象來構造B對象,這類情況是“直接初始化”
Base C = A;// ②定義C的時候即賦值,這是真正意義上的拷貝構造。2者的區分見下文介紹。
除上述兩種情況外,還有1些場合也會致使拷貝構造函數被調用,比如:
直接初始化和拷貝初始化的細微區分
Base B(A)只是致使拷貝構造函數被調用,但其實不是嚴格意義上的拷貝構造,由于:
拷貝賦值函數是賦值函數的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不支持操作符重載)。關于操作符重載的知識請讀者瀏覽本文后續章節。
前面兩節介紹了拷貝構造和拷貝賦值函數,還了解了深拷貝和淺拷貝的區分。但關于構造和賦值的故事并沒有完。由于C++11中,除拷貝構造和拷貝賦值以外,還有移動構造和移動賦值。
注意
這幾個名詞中:構造和賦值并沒有變,變化的是構造和賦值的方法。前2節介紹的是拷貝之法,本節來看移動之法。
圖18展現了移動的含義:
圖18 Move的示意
對照圖16和圖18,讀者會發現移動的含義其實非常簡單,就是把A對象的內容移動到B對象中去:
移動的含義好像不是很難。不過,讓我們更進1步思考1個問題:移動以后,A、B對象的命運會產生怎樣的改變?
移動以后,A竟然無用了。甚么場合會需要如此“殘暴”的做法?還是讓我們用示例來論述C++11推出移動之法的目的吧:
圖19 有Move和沒有Move的區分
圖19中,左上角是示例代碼:
圖19展現了沒有定義移動構造函數和定義了移動構造函數時該程序運行后打印的日志。同時圖中還解釋了履行的進程。結合前文所述內容,我們發現tmp確切是1種轉移出去(不論是采取移動還是拷貝)后就不需要再使用的對象了。對這類情況,移動構造所帶來的好處是不言而喻的。
注意:
對圖中的測試函數,現在的編譯器已能做到高度優化,以致于圖中列出的移動或拷貝調用都不需要了。為了到達圖中的效果,編譯時必須加上-fno-elide-constructors標志以制止這類優化。讀者無妨1試。
下面,我們來看看代碼中是如何體現移動的。
圖20所示為Base的移動構造和移動賦值函數:
圖20 移動構造和移動賦值示例
圖20中,請讀者特別注意Base類移動構造和移動賦值函數的參數的類型,它是Base&&。沒錯,是兩個&&符號:
甚么是左值,甚么是右值?筆者不擬討論它們詳細的語法和語義。不過,根據參考文獻[5]所述,讀者掌握以下識便可:
我們通過幾行代碼來加深對左右值的認識:
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下移動構造和賦值函數在甚么場合下使用的問題,請讀者注意掌控兩個關鍵點:
如果沒有定義移動函數怎樣辦?
如果類沒有定義移動構造或移動賦值函數,編譯器會調用對應的拷貝構造或拷貝賦值函數。所以,使用std::move不會帶來甚么副作用,它只是表達了要使用移動之法的欲望。
最后,來看類中最后1類特殊函數,即析構函數。當類的實例到達生命終點時,析構函數將被調用,其主要目的是為了清算該實例占據的資源。圖21所示為Base類的析構函數示例:
圖21 析構函數示例
Java中與析構函數類似的是finalize函數。但絕大多數情況下,Java程序員不用關心它。而C++中,我們需要知道析構函數甚么時候會被調用:
2 棧上創建的類實例,在退出作用域(比如函數返回,或離開花括號包圍起來的某個作用域)之前,該實例會被析構。
2 動態創建的實例(通過new操作符),當delete該對象時,其析構函數會被調用。
1.3.1節介紹了C++中1個普通類的大致組成元素和其中1些特殊的成員函數,比如:
C++中與類的派生、繼承相干的知識比較復雜,相對瑣碎。本節中,筆者擬將精力放在1些相對基礎的內容上。先來看1個派生和繼承的例子,如圖22所示:
圖22 派生和繼承示例
圖22中:
和Java比較
Java中雖然沒有類的多重繼承,但1個類可以實現多個接口(Interface),這其實也算是多重繼承了。相比Java的這類設計,筆者覺得C++中類的多重繼承太過靈活,使用時需要特別謹慎,否則菱形繼承的問題很難避免。
現在,先來看1下C++中派生類的寫法。如圖22所示,Derived類繼承關系的語法以下:
class Derived:private Base,publicVirtualBase{
}
其中:
了解C++中如何編寫派生類后,下1步要關注面向對象中兩個重要特性——多態和抽象是如何在C++中體現的。
注意:
筆者此地方說的抽象是狹義的,和語言相干的,比如Java中的抽象類。
Java語言里,多態是借助派生類重寫(override)基類的函數來表達,而抽象則是借助抽象類(包括抽象方法)或接口來實現。而在C++中,虛函數和純虛函數就是用于描寫多態和抽象的利器:
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
C++中,也能夠禁止某個虛函數被override,方法和Java類似,就是在函數聲明后添加final關鍵詞,比如
virtual void test1(boolean test) final;//如此,test1將不能被派生類override了
最后,我們通過1段示例代碼來加深對虛函數的認識,如圖23所示:
圖23 虛函數測試示例
圖23是筆者編寫的1個很簡單的例子,左側是代碼,右側是運行結果。簡而言之:
提示:
1 請讀者嘗試修改測試代碼,然后視察打印結果。
2 讀者可將圖23中代碼的最后1行改寫成pvb->~VirtualBase(),即直接調用基類的析構函數,但由于它是虛析構函數,所以運行時,~Derived()將先被調用。
類的構造函數在類實例被創建時調用,而析構函數在該實例被燒毀時調用。如果該類有派生關系的話,其基類的構造函數和析構函數也將被順次調用到,那末,這個順次的順序是甚么?
補充內容:
如果派生類含有類類型的成員變量時,調用次序將變成:
構造函數:基類構造->派生類中類類型成員變量構造->派生類構造
析構函數:派生類析構->派生類中類類型成員變量析構->基類析構
多重派生的話,基類依照派生列表的順序/反序構造或析構
Java中,如果程序員沒有為類編寫構造函數函數,則編譯器會為類隱式創建1個不帶任何參數的構造函數。這類編譯器隱式創建1些函數的行動在C++中也存在,只不過C++中的類有構造函數,賦值函數,析構函數,所以情況會復雜1些,圖24描寫了編譯器合成特殊函數的規則:
圖24 編譯器合成特殊函數的規則
圖24的規矩可簡單總結為:
從上面的描寫可知,C++中編譯器合成特殊函數的規則是比較復雜的。即便如此,圖24中展現的規則還僅是冰山1角。以移動函數的合成而言,即便圖中的條件滿足,編譯器也未必能合成移動函數,比如類中有沒有法移動的成員變量時。
關于編譯器合成規則,筆者個人感覺開發者應當以實際需求為動身點,如果確切需要移動函數,則在類聲明中定義就行。
有些時候我們需要1種方法來控制編譯器這類自動合成的行動,控制的目的無外乎兩個:
借助=default和=delete標識,這兩個目的很容易到達,來看1段代碼:
//定義了1個普通的構造函數,但同時也想讓編譯器合成默許的構造函數,則可使用=default標識
Base(int x); //定義1個普通構造函數后,編譯器將停止自動合成默許的構造函數
//=default后,強迫編譯器合成默許的構造函數。注意,開發者不用實現該函數
Base() = default;//通知編譯器來合成這個默許的構造函數
//如果不想讓編譯器合成某些函數,則使用= delete標識
Base&operator=(const Base& other) = delete;//禁止編譯合成拷貝賦值函數
注意,這類控制行動只針對構造、賦值和析構等3類特殊的函數。
1般而言,派生類可能希望有著和基類類似的構造方法。比如,圖25所示的Base類有3種普通構造方法。現在我們希望Derived也能支持通過這3種方式來創建Derived類實例。怎樣辦?圖25展現了兩種方法:
圖25 派生類“繼承”基類構造函數
注意,這類“繼承”實際上是1種編譯器自動合成的規則,它僅支持合成普通的構造函數。而默許構造函數,移動構造函數,拷貝構造函數等遵守正常的規則來合成。
探討
前述內容中,我們向讀者展現了C++中編譯器合成1些特殊函數的做法和規則。實際上,編譯器合成的規則比本節所述內容要復雜很多,建議感興趣的讀者瀏覽參考文獻來展開進1步的學習。
另外,實際使用進程中,開發者不能完全依賴于編譯器的自動合成,有些細節問題必須由開發者自己先回答。比如,拷貝構造時,我們需要深拷貝還是淺拷貝?需不需要支持移動操作?在取得這些問題答案的基礎上,讀者再結合編譯器合成的規則,然后才選擇由編譯器來合成這些函數還是由開發者自己來編寫它們。
前面我們提到過,C++中的類訪問其實例的成員變量或成員函數的權限控制上有著和Java類似的關鍵詞,如public、private和protected。嚴格遵照“信息該公然的要公然,不該公然的1定不公然”這1封裝的最高原則無疑是1件好事,但現實生活中的情況是如此變化萬端,有時候我們也需要破個例。比如,熟人之間是不是可以公然1些信息以避開如果按“公事公辦”走流程所帶來的太高溝通本錢的問題?
C++中,借助友元,我們可以做到小范圍的公然信息以減少溝通本錢。從編程角度來看,友元的作用不過是:提供1種方式,使得類外某些函數或某些類能夠訪問1個類的私有成員變量或成員函數。對被訪問的類而言,這些類外函數或類,就是被訪問的類的朋友。
來看友元的示例,如圖26所示:
圖26 類的友元示意
圖26展現了如作甚某個類指定它的“朋友們”,C++中,類的友元可以是:
基類的友元會變成從該基類派生得來的派生類的友元嗎?
C++中,友元關系不能繼承,也就是說:
1 基類的友元可以訪問基類非公然成員,也能訪問派生類中屬于基類的非公然成員。
2 但是不能訪問派生類自己定義的非公然成員。
友元比較簡單,此處就不擬多說。現在我們介紹下圖26中提到的類的前向聲明,先來回顧下代碼:
class Obj;//類的前向聲明
void accessObj(Obj& obj);
C++中,數據類型應當先聲明,然后再使用。但這會帶來1個“先有雞還是先有蛋”的問題:
怎樣破解這個問題?這就用到了類的前向聲明,以圖26為例,Obj前向聲明的目的就是告知類型系統,Obj是1個class,不要把它當作別的甚么東西。1般而言,類的前向聲明的用法以下:
這就是類的前向聲明的用法,即在頭文件里進行類的前向聲明,在源文件里去包括該類的頭文件。
類的前向聲明的局限
前向聲明好處很多,但同時也有限制。以Obj為例,在看到Obj完全定義之前,不能聲明Obj類型的變量(包括類的成員變量),但是可以定義Obj援用類型或Obj指針類型的變量。比如,你沒法在圖26中class Obj類代碼之前定義ObjaObj這樣的變量。只能定義Obj& refObj或Obj* pObj。之所以有這個限制,是由于定義Obj類型變量的時候,
下一篇 JAVA EE-JSP