複合類型(compound type)是指基於其餘類型定義的類型,C++語言有幾種複合類型,這裏只介紹兩種:引用和指針程序員
一:引用函數
引用(reference)爲對象起了另一個名字,引用類型引用另一種類型,經過將聲明符寫成&d的形式來定義引用類型,其中d是聲明的變量名。spa
int ival = 1024指針
int &refval = ival;//refval指向ival(是ival的另一個名字)調試
int &refval2; //報錯,引用必須初始化對象
通常在初始化變量的時候,初始值會被拷貝到新建的對象中,然而定義引用時,程序把引用和它的初始值綁定在一塊兒,而不是將初始值拷貝給引用,一旦初始化完成,引用將和它的初始值對象一直綁在一塊兒,由於沒法令引用從新綁定到另一個對象,所以引用必須初始化。blog
引用即別名生命週期
引用並不是對象,相反的,它只是爲一個已經存在的對象起的另外的名字。ip
定義了一個引用以後,對其進行的全部操做都是在與之綁定的對象上進行的;內存
refval = 2; //把2賦值給refval指向的對象,此處便是賦值給了ival;
int ii = refval; //與ii = ival執行的結果是同樣的。
爲引用賦值,其實是把值賦給了與引用綁定的對象,獲取引用的值,其實是獲取了與引用綁定的對象的值,同理,以引用做爲初始值,其實是以與引用綁定的對象做爲初始值;
//正確:refval3綁定到了那個與refval綁定的對象上,這裏就是綁定到ival上
int &refval3 = refval;
//利用與refval綁定的對象的值初始化變量
int i = refval;//正確,i 被初始化爲ival的值
由於引用自己不是一個對象,因此不能定義引用的引用;
引用的定義
容許在一條語句中定義多個引用,其中每一個引用標識符都必須以符號&開頭。
int i = 1024,i2 = 2048; //i和i2都是int;
int &r = i,r2 = i2; //r是一個引用,與i綁定在一塊兒,r2是int;
int i3 = 1024,&ri = i3;//i3 是int,ri是一個引用,與i3綁定在一塊兒。
int &r3 = i3,&r4 = i2; //r3和r4都是引用。
引用類型都要和與之綁定的對象嚴格匹配,並且,引用只能綁定到對象上,而不能與字面值或某個表達式的計算結果綁定在一塊兒。
int &refval4 = 10; //錯誤,引用類型的初始值必須是一個對象。
double dval = 3.14;
int &refval5 = dval; //錯誤,此處引用類型的初始值必須是int型對象。
二:指針
指針(pointer)是指向(point to)另一種類型的複合類型,與引用相似,指針也實現了對其它對象的間接訪問,然而指針與引用相比又有不少不一樣點,其一,指針自己就是一個對象,容許對指針賦值和拷貝,並且在指針的生命週期內它能夠前後指向幾個不一樣的對象。其二,指針無須在定義時賦值,和其它內置類型同樣,在塊做用域內定義的指針若是沒有被初始化,也將擁有一個不肯定的值。
WARNING:指針一般難以理解,即便是有經驗的程序員也經常由於調試指針引起的錯誤而備受折磨。
定義指針類型的方法將聲明符寫成*d的形式,其中d是變量名,若是在一條語句中定義了幾個指針變量,每一個變量前面都必須有符號*;
int *ip1, *ip2; //ip1,ip2都是指向int型對象的指針。
double dp,*dp2; //dp2是指向double型對象的指針,dp是double型對象。
獲取對象的地址
指針存放某個對象的地址,要想獲取該地址,須要使用取地址符(操做符&);
int ival = 42;
int *p = &ival; //p存放了ival的地址,或者說p是指向了變量ival的指針。
第二條語句把p定義了一個指向int的指針,隨後初始化p另其指向名爲ival的int對象,由於引用不是對象,沒有實際地址,因此不能定義指向引用的指針。
指針的類型都要和它所指向的對象嚴格匹配。
double dval;
double *pd = &dval; //正確,初始值是double型對象的地址
double *pd2 = pd; //初始值是指向double對象的指針
int *pi = pd; //錯誤,指針pi的類型和pd的類型不匹配。
pi = &dval; //錯誤,試圖把double型對象的地址賦給int型指針。
由於在聲明語句中指針的類型實際上被用於指定它所指向對象的類型,因此二者必須匹配,若是指針指向了一個其餘類型的對象,對該對象的操做將發生錯誤。
指針值
指針的值(即地址)應屬於下列4種狀態之一:
1:指向一個對象;
2:指向緊鄰對象所佔空間的下一個位置;
3:空指針,意味着指針沒有指向任何對象;
4:無效指針,也就是上述狀況以外的位置;
試圖拷貝或者以其餘形式訪問無效指針的值都將引起錯誤,編譯器並不負責檢查此類錯誤,這一點和試圖使用未初始化的變量同樣,訪問無效指針的後果沒法預計,所以程序員必須清楚任意給定的指針是否有效。
儘管第2種和第三種形式的指針是有效的,但其使用一樣受到限制,顯然這些指針沒有指向任何具體的對象,因此試圖訪問此類指針對象的行爲不被容許,若是這樣作了,後果也是沒法預計的。
利用指針訪問對象
若是指針指向了一個對象,則容許使用解引用符(操做符*)來訪問對象;
int ival = 42;
int *p = &ival; //p存放着變量ival的地址,或者說p是指向變量ival的指針;
cout << *p; //由符號*獲得指針p所指的對象,輸出42;
對指針解引用會獲得所指的對象,所以若是給解引用的結果賦值,實際上也就是給指針所指對象賦值;
*p = 0; //由符號*獲得指針p所指的對象,便可經由p爲變量ival賦值
cout << *p; // 輸出0
如上述程序所示,爲*p賦值其實是爲p所指的對象賦值
空指針
空指針(null pointer)不指向任何對象,在試圖使用一個指針以前代碼能夠首先檢查它是否爲空,一下列出幾個生成空指針的方法。
int *p = nullptr; //等價於int *p = 0;
int *p2 = 0; //直接將p2初始化爲字面常量0;
//須要首先#include<cstdlib>
int *p3 = NULL; // 等價於int *p3 = 0;
獲得空指針最直接的辦法就是用字面值nullptr來初始化指針,這也是C++11新標準剛剛引入的一種方法,nullptr是一種特殊類型的字面值,它能夠被轉換成任意其餘指針類型,另外一種方法就是如對p2的定義同樣,也能夠經過將指針初始化爲字面值0來生成空指針。
過去的程序還會用到一個名爲NULL的預處理變量來給指針賦值,這個變量在頭文件cstdlib中定義,它的值就是0
當用到一個預處理變量時,預處理器就會自動將它替換爲實際值,所以用NULL來初始化指針和用0來初始化指針是同樣的,在新標準下,如今的C++最好使用nullptr,同是應儘可能避免使用NULL。
把int變量直接賦值給指針是錯誤的操做,即便int變量的值剛好是0也不行
int zero = 0;
int *pi;
pi = zero; //錯誤,不能把int變量直接賦值給指針
建議
使用未經初始化的指針是引起運行時錯誤的一大緣由。
和其它變量同樣,訪問未經初始化的指針所引起的後果是沒法預計的,一般這一行爲將形成程序崩潰,並且一旦崩潰,要想定位到出錯的位置將是特別棘手的問題。
在大多數編譯器環境下,若是使用了未經初始化的指針,則該指針多佔內存空間的當前內容將被看作一個地址值。訪問該指針,至關於訪問一個本不存在的位置上的本不存在的對象,槽糕的是,若是指針所佔內存空間中剛好有內容,而這些內容又被看成了某個地址,咱們就很難分清它究竟是合法仍是非法的了。
所以咱們建議初始化全部的指針,而且在可能的狀況下,儘可能定義了對象以後再定義指向它的指針。若是實在不清楚應該指向何處,就把它初始化爲nullptr或者0,這樣程序就能檢測並知道它沒有指向任何具體的對象了。
賦值和指針
指針和引用都能提供其餘對象的間接訪問,然而在具體實現細節上兩者有很大不一樣,其中最重要的一點就是引用自己並不是是一個對象。一旦定義了引用,就沒法令其再綁定到另外的對象,以後每次使用這個引用都是訪問它最初綁定的那個對象。
指針和它存放的地址就沒有這種限制了,和其餘任何變量(只要不是引用)同樣,給指針賦值就是令它存放一個新地址,從而指向一個新的對象。
int i = 42;
int *pi = 0; //pi被初始化,但沒有指向任何對象;
int *pi2 = &i; //pi2被初始化,存有i的地址
int *pi3; //若是pi3定義於塊內,則pi3的值是沒法肯定的。
pi3 = pi2; //pi3和pi2指向了同一個對象i;
pi2 = 0; //如今pi2不指向任何對象了;
有時候想搞清楚一條賦值語句究竟是改變了指針的值仍是改變了指針所指對象的值不太容易,最好的辦法就是記住永遠改變的是等號左側的對象。
pi = &ival; //pi的值被改變了,如今pi指向了ival;
意思就是爲pi賦了一個新的值,也就是改變了那個存放在pi內的地址值,相反,若是寫出如下語句:
*pi = 0; //ival的值被改變,指針pi並無改變;
則*pi(也就是指針pi指向的那個對象)發生改變
void*指針
void*是一種特殊的指針類型,可用於存聽任意對象的地址,一個void*指針存放着一個地址,這一點和其餘指針相似,不一樣的是,咱們對該地址中究竟是個什麼類型的對象並不瞭解。
double obj = 3.14, *pd = &obj;
void *pv = &obj; //正確,void*能存聽任意類型對象的地址 obj能夠是任意類型的對象
pv = pd; //pv能夠存聽任意類型的指針。
利用void*指針能作的事情比較有限,拿它和別的指針比較,做爲函數的輸入和輸出,或者賦值給另一個void*指針。不能直接操做void*指針所指的對象,由於咱們不知道這個對象究竟是什麼類型,也就沒法肯定能在這個對象上作哪些操做。
歸納來講,以void*的視角來看內存空間也就僅僅是內存空間,沒辦法訪問內存空間中所存的對象。
定義多個變量
常常有一種觀點會誤覺得,在定義語句中,類型修飾符(*或&)做用於本次定義的所有變量。形成這種錯誤見解的緣由有不少,其中之一就是咱們能夠把空格寫在類型修飾符和變量名中間。
int* p;//合法可是容易產生誤導
咱們說這種寫法可能產生誤導是由於int*放在一塊兒好像是這條語句中全部變量共同的類型同樣。其實偏偏相反,基本數據類型是int而非int*,*僅僅是修飾了p而已,對該聲明語句中的其餘變量,它並不產生任何做用。
int* p1, p2; //p1是指向int的指針,p2是int
涉及指針或引用的聲明,通常有兩種寫法,第一種把把修飾符和變量標識符寫在一塊兒。
int *p1, *p2;//p1,p2都是指向int的指針
這種形式着重強調變量具備的複合類型, 第二種把修飾符和類型名寫在一塊兒,而且每條語句只定義一個變量;
int* p1;//p1是指向int的指針
int* p2; //p2是指向int的指針
這種形式着重強調本次聲明定義了一種複合類型。
我的推薦第一種寫法。
指向指針的指針
通常來講,聲明符中修飾符的個數沒有限制。當有多個修飾符寫在一塊兒的時候,按照其邏輯關係詳加解釋便可,以指針爲例,指針是內存中的對象,像其餘對象同樣也有本身的地址,所以容許把指針的地址再存放到另外一個指針當中。
經過*的個數能夠區分指針的級別,也就是說,**表示指向指針的指針,***表示指向指針的指針的指針,以此類推:
int ival = 1024;
int *pi = &ival; //pi是指向一個int型的數
int **ppi = &pi; //ppi是指向一個int型的指針
此處pi是指向int型的指針,而ppi是指向int型指針的指針,下圖描述了他們之間的關係:
解引用int型指針會獲得一個int型的數,一樣,解引用指向指針的指針會獲得一個指針,此時爲了訪問最原始的那個對象,須要對指針的指針作兩次解引用;
cout << "The Value of ival\n"
<<"direct value: " << ival << "\n"
<<"indirect value: "<< *pi << "\n"
<<"doubly indirect value: " << **pi <<endl;
該程序使用三種不一樣的方式輸出了變量ival的值,第一種直接輸出,第二種經過int型指針pi輸出,第三種兩次解引用ppi,取得ival的值;
指向指針的引用
引用自己不是一個對象,所以不能定義指向引用的指針,可是指針是一個對象,因此存在對指針的引用;
int i = 42;
int *p; //p 是一個int型指針
int *&r = p; //r是一個對指針p的引用
r = &i;//r引用了一個指針,所以給r賦值&i就是另p指向了i;
*r = 0; //解引用r獲得i,也就是p指向的對象,將i的值改成了0;
要理解r的類型究竟是什麼,最簡單的辦法就是從右向左閱讀r的定義,離變量名最近的符號(此例中是&r的符號&)對變量的類型有最直接的影響,所以r是一個引用。聲明符的其他部分用以肯定r引用的類型是什麼,此例中的符號*說明r引用的是一個指針,最後
聲明的基本數據類型部分指出r引用的是一個int型指針。