C++引用和指針
引用
引用reference為對象起了另外一個名字,引用類型引用refers to另外一種類型,通過將聲明符寫成&d的形式來定義引用類型,其中d是生命的變量名
int ival = 1024;
int &refVal = ival; //refVal指向ival(是ival的另一個名字)
int &refVal2; //報錯:引用必須被初始化
一般在初始化變量時,初始值會被拷貝到新建的對象中
然而定義引用時,程序把引用和它的初始值綁定在一起,而不是將初始值拷貝給引用
一旦初始化完成,引用將和它的初始值對象一直綁定在一起
因為無法令引用重新綁定到另外一個對象,因此引用必須被初始化
引用即別名
引用並非對象,他只是為一個已經存在的對象起的另一個名字
定義了一個引用之后,對其進行的所有操作都是在與之綁定的對象上進行的
refVal = 2; //把2賦給refVal指向的對象,也就是ival
int ii = refVal; //與ii = ival執行結果相同
為引用賦值,實際上是把值賦給了與引用綁定的對象
獲取引用的值,實際上是獲取了與引用綁定的對象的值
同理,以引用作為初始值,實際上是以與引用綁定的對象作為初始值:
//refVal3綁定到了與refVal綁定的對象上,也就是綁定到iVal上
int &refVal = refVal;
//利用與refVal綁定的對象的值初始化變量i,i被初始化為iVal的值
int i = refVal;
引用的定義
允許在一條語句中定義多個引用,其中每個引用標識符都必須以符號&開頭:
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型對象
指針
指針是指向另一種類型的復合類型,和引用類似,實現了對其他對象的間接訪問,然而引用和指針有很多不同點:
- 指針本身就是一個對象,允許對指針賦值和拷貝,而且在其生命周期內它可以先后指向幾個不同的對象;引用並非對象,他只是為一個已經存在的對象起的另一個名字,一旦初始化完成,引用將和它的初始值對象一直綁定在一起
- 指針無需在定義時賦初始值,和其他內置類型一樣,在塊作用域內定義的指針如果沒有初始化,也將擁有一個不確定的值;因為無法令引用重新綁定到另外一個對象,因此引用必須被初始化
獲取對象的地址
指針存放某個對象的地址,想要獲取該地址就要用取地址符號(操作符&)
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種狀態之一:
- 指向一個對象
- 指向緊鄰對象所占空間的下一個位置
- 空指針,沒有指向任何對象
- 無效指針,上述情況之外
利用指針訪問對象
如果指針指向了一個對象,允許使用解引用符(操作符*)來訪問該對象
int ival = 42;
int *p = &ival; //p存放着變量ival的地址,或者說p是指向變量ival的指針
cout<< *p //由符號*得到指針p所指向的對象,輸出42
對指針解引用會得出所指的對象,因此如果給解引用的結果賦值,實際上也就是給指針所指的對象賦值
*p = 0;
cout<< *p;
空指針
不指向任何對象,在試圖使用一個指針之前可以先檢查其是否為空,以下是生成空指針的幾個方法:
int *p1 = nullptr; //C++11
int *p2 = 0;
int *p3 = NULL; //include<cstdlib>
現在的C++程序最好使用nullptr,同時盡量避免使用NULL
把int變量直接賦值給指針是錯誤的操作,即使int變量的值恰好等於0
int zero = 0;
pi = zero; //錯誤
賦值和指針
指針和引用都能提供對其他對象的間接訪問,然而在具體實現細節上二者有很大不同,其中最重要的一點就是引用並非都是一個對象,一旦定義了引用,就無法再令其綁定到其他對象,之后每次使用這個引用都是在訪問它最初綁定的對象
指針的和它存放的地址之間就沒有這種現實,和其他變量(只要不是引用)一樣,給指針賦值就是令他存放一個新的地址,從而指向一個新的對象
int i = 42;
int *pi = 0; //pi被初始化,但沒有指向任何對象
int *pi2 = &i; //pi2被初始化,存有i的地址
int *pi3; //如果pi3定義於塊內,pi3的值無法確定
pi3 = pi2; //pi3和pi2指向同一個對象
pi2 = 0; //現在pi2不指向任何對象了
有的時候想搞清楚一條賦值語句到底是改變了指針的值還是指針所之鄉的對象的值不太容易,最好的辦法就是記住賦值永遠改變的是等號左側的對象:
pi = &ival //pi的值被改變,現在pi指向了ival
上面代碼意思為為pi賦一個新的值,也就是改變了那個存放在pi內的地址值,相反的,如果寫出如下語句:
*pi = 0; //ival的值被改變,指針pi並沒有改變
則*pi(也就是指針pi指向的那個對象)發生改變
其他指針操作
只要指針擁有一個合法值,就能將它在用條件表達式中,和采用算術值作為條件遵循的規則類似,如果指針的值是0,條件取false
int ival = 1024;
int *pi = 0; //pi2是一個合法的指針,存放着ival的地址
int *pi2 = &ival; //pi的值是0,因此條件的值是false
if(pi)
//pi值是0,false
if(pi2)
//pi2指向ival,ival的值不等於0,true
對於兩個類型相同的合法指針,可以用 == 或者 != 來比較他們,如果存放的地址相同則相等,反之則不等
地址相同存在着三種可能:
- 都為空
- 指向同一個對象
- 指向了同一個對象的下一個地址
一個指針指向某對象,另一個指針指向另一個對象的下一個地址,兩個指針也有可能相等
void*指針
void* 指針是一種特殊的指針類型,以用於存放任意對象的地址,和其他指針類似,一個void* 指針存放着一個地址,不同的是,我們不了解該地址中是個什么類型的對象
double obj = 3.14, *pd = &obj;
void *pv = &obj; //obj可以是任意類型的對象
pv = pd; //pv可以存放任意類型的指針
以void* 的視角來看內存空間也僅僅是內存空間,沒辦法訪問內存空間中所存的對象
復合類型的聲明
首先我們知道,一條定義語句可能定義出不同類型的變量
//i是一個int型變量,p是一個int型指針,r是一個int型引用
int i = 1024,*pi = &i,&r = i;
定義多個變量
誤導人的寫法:
int* p; //合法
int和 * 放在一起好像是這條語句所有變量的共同類型是int*一樣,其實恰恰相反,基本數據類型是int, * 僅僅修飾了p而已,在該語句中聲明其他變量並不起任何作用:
int* p1,p2; //p1是指向int的指針,p2是int
所以設計指針或者引用的聲明,我們一般有兩種寫法:
int *p2, *p2
int *p1;
int *p2;
指向指針的指針
指針是內存中的對象,像其他對象一樣也有自己的地址,因此允許把指針的地址再存放到另一個指針當中
通過 * 的個數可以區分指針的級別,** 是指向指針的指針, *** 表示指向指針的指針的指針:
int ival = 1024;
int *pi = &ival; //pi指向一個int型的整數
int **ppi = π //ppi指向一個int型的指針
解引用int型指針會得到一個int型的數,同樣,解引用指向指針的指針會得到一個指針,此時為了訪問最原始的對象需要對指針的指針做兩次解引用,以下程序以三種不同的方式輸出了ival的值
cout<<ival<<endl;
cout<<*pi<<endl;
cout<<**ppi<<endl;
指向指針的引用
引用本身不是一個對象,因此不能定義指向引用的指針,但指針是對象,所以存在對指針的引用
int i = 42;
int *p; //p是一個int型指針
int *&r = p; //r是一個對指針p的引用
r = &1; //r引用了一個指針,因此給r賦值&i就是令p指向i
*r = 0; //解引用r得到i,也就是p指向的對象,將i的值改為0;
const的引用
對常量的引用與普通引用不同的是,對常量的引用不能被用作修改它所綁定的對象
const int ci = 1024;
const int &ri = ci; //正確,引用及其對應的對象都是常量
ri = 2048; //錯誤:r1是對常量的引用
int &r2 = ci //錯誤:試圖讓一個非常量引用指向一個常量對象
對常量的引用是對const的引用,嚴格來說並不存在常量引用,因為引用不是一個對象,我們沒有辦法讓引用本身恆定不變,由於C++並不允許隨意改變引用所綁定的對象,所以從這層意義上理解的所有的引用都算是常量
我們之前提到引用的類型必須與其所引用的對象的類型一致,但是這里有其中一個例外:初始化常量時允許用任意表達式作為初始值,只要該表達式結果能轉換成引用類型即可,尤其允許為一個常量引用綁定非常量的對象,字面值,甚至一般表達式:
int i = 42;
const int &r1 = i; //允許const int&綁定到一個普通int對象上
const int &r2 = 42; //正確:r2是一個常量引用
const int &r3 = r1 * 2; //正確:r3是一個常量引用
int &r4 = r1 * 2; //錯誤:r4是一個普通的非常量引用
為什么呢?然后我們看看在這個綁定過程中發生了什么:
double dval = 3.14;
const int &ri = dval;
此處ri引用了一個int型整數,對ri的操作是整數運算,但dval卻是一個雙精度浮點數而非整數,因此為了確保讓ri綁定一個整數,編譯器把上述代碼變成了:
double dval = 3.14;
const int temp = dval;
const int &ri = temp;
在這種情況下,ri綁定了一個臨時量對象,所謂臨時量對象就是當編譯器需要一個空間暫存表達式的求值結果時臨時創建的一個未命名的對象,我們也常把臨時量對象稱為臨時量
對const的引用可能引用一個並非const的對象
int i = 42;
int &r1 = i;
const int &r2 = i; //r2也綁定對象i,但不允許通過r2修改i
r1 = 0; //r1並非常量,i修改為0
r2 = 0; //錯誤:r2是一個常量引用綁定
常量引用綁定非常量是合法行為,然而不允許通過該常量引用修改其綁定的非常量的值
指針和const
類似於常量的引用,指向常量的指針不能用於改變其所指對象的值,同時,想存放常量對象的地址,只能使用指向常量的指針:
const double pi = 3.14;
double *ptr = π //錯誤:ptr是一個普通指針
const double *cptr = π //正確:cptr可以指向一個雙精度常量
*cptr = 42; //錯誤:不能給*cptr賦值
我們之前也提到指針類型必須與其指向對象的類型一致,但這里也有一個例外:
double dval = 3.14; //dval是一個雙精度浮點數
cptr = &dval; //正確,但不能通過cptr改變dval的值
const指針
指針是對象,而引用不是,因此我們允許把指針本身定為常量,也就是常量指針,常量指針必須被初始化,而且一旦初始化完成(存放在指針里的地址)就不能再改變
int errNumb = 0;
int *const curErr = &errUnmb; //curErr將一直指向errNumb
const double pi = 3.14159;
const double *const pip = π //pip是一個 指向常量對象的 常量指針
這樣的書寫形式也隱含着一層意味:不變的是指針本身而非指向的那個值,也就是說指針本身是一個常量
指針本身是一個常量並不意味這不能通過指針修改其指向的對象的值,這取決於對象的類型:
*pip = 2.72 //錯誤:pip指向的對象是常量
*curErr = 1 //正確:curERR指向的是一個非常量整數
頂層const
指針本身是一個對象,但是它又可以指向另外一個對象,因此指針本身是不是常量和指針所指的是不是一個常量就是兩個互相獨立的問題,我們用這樣區分:
- 頂層const表示指針本身是個常量
- 底層const表示指針所指的對象是一個常量
int i = 0;
int *const p1 = &i; //不能改變p1的值,這是一個頂層const
const int ci = 42; //不能改變ci的值,這是一個頂層const
const int *p2 = &ci; //允許改變p2的值,這是一個底層const
const int *const p3 = p2; //靠右的是頂層const,靠左的const是底層const
const int &r = ci; //用於聲明引用的const都是底層const
當執行拷貝操作時,常量是頂層const還是底層const區別就很明顯了
其中頂層const不受什么影響:
i = ci; //正確:ci是一個頂層const,無影響
p2 = p3; //正確:p2和p3指向對象類型相同,p3頂層const部分不影響
由於執行拷貝操作不會改變被拷貝對象的值,因此拷入和拷出的對象是否是常量都沒什么影響
但是底層const就有限制了,拷入和拷出的對象都有必須具有相同的底層const資格,或者兩個對象的數據類型必須能夠轉換,一般來說非常量可以轉換成常量,反之不行
int *p = p3; //錯誤:p3包含頂層const的定義,p沒有
p2 = p3; //正確:p2和p3都是底層const
p2 = &i; //正確:int*能轉換成const int*
int &r = ci; //錯誤:普通的int&不能綁定到int常量上
const int &r2 = i; //正確:const int&可以綁定到一個普通int上
比如p3既是頂層const也是底層const,拷貝p3時可以不去在乎它是一個頂層const,但是必須清楚它指向的對象得是一個常量,因此不能用p3去初始化p