今天重溫了一下C++ Primer,對上面三個概念有了更清晰的認識,自我認為已經有了比較全面的理解了,所以趕緊記錄下來,也請大家批評指正。
1.引用
引用,簡單來說就是為對象起了一個別名,可以用別名來等同於操作對象,通過將聲明符寫成&d的形式來定義引用類型,其中d是聲明的變量名,即引用變量的別名:
int i =1; int &r = i; //r指向i(r是i的別名,可以通過操作r來改變i的值)、 r=2; cout<<r<<" "<<i<<" "<<endl; //r和i的值都為2 int j=r; //j被初始化為i的值,即為2
通俗的講,為引用賦值,實際上就是將值賦給了與引用綁定的對象。獲取引用的值,實際上就是獲取了與引用綁定的對象的值。但是引用本身不是一個對象,所以不能定義引用的引用。
另外引用必須要被初始化,剛才也說了,在定義引用的時候,程序是吧引用和它的初始值綁定在一起,而不是將初始值拷貝給引用。一旦初始化完成,引用將和它的初始值對象一直綁定在一起,因為無法令引用重新綁定到另外一個對象,因此引用必須初始化。
int &r; //錯誤,引用必須被初始化 int &r=10; //錯誤,引用類型的初始值必須是一個對象 int i=10,&r=i; //正確,r是一個引用,與i綁定在了一起,i是一個int int j=0; int& r=j,p=j; //正確,r是一個引用,與j綁定,p是一個int
在這里強調一個可能有一些人會忽視的問題,為什么上面的最后一段代碼不是應該r和p都是引用嗎?
這種想法是錯誤的,上面我故意把引用符號&寫到了int后面,而沒有寫到變量的旁邊,兩種寫法的結果都是一樣的,r是引用,p是int;
這里我們要搞清楚一條生命語句是由一個基本數據類型和緊隨其后的一個聲明符列表組成。對應到上面那條代碼就是基本數據類型是int,聲明符是&,聲明符只對其后的第一個變量有效,所以不管我們在int后面加多少個聲明符,只對第一個變量有效,與后面的變量的無關,例如:
int *&p,q; //p是一個引用,綁定一個指針變量,q是一個int int* p,i,&r=i; //p是指針,i是int,r是引用
引用的還有一個比較重要的點就是引用的類型與之要綁定的對象嚴格匹配。(有兩種例外的情況,在const部分會介紹一種)
int i=0,&r1=i; double d=3.14,&r2=d; double &r3=i; //錯誤,引用類型為double,對象類型為int r1=r2; //double型向int型轉換,會丟失精度 cout<<i<<" "<<r1<<" "<<endl;//i和r1的值都為3
2.指針
指針與引用類似,也實現了對其他對象的間接訪問。但是,與引用對比,又有很多不同點,其一,指針本身就是一個對象,允許對指針賦值和拷貝,而且還可以先后指向不同的對象(引用一旦綁定就不能修改了);其二,指針無需再定義的時候賦初值。
指針存放的是某個對象的地址,要想獲得該地址,需要使用取地址符&,並且一般情況下(有兩種例外情況,再次暫時不談),指針的類型都要與它所指向的對象嚴格匹配:
int a=1; int *p=&a; //p存放變量a的地址,或者說p是指向變量a的指針 double b=3.14; double *q; q=&b; //q是指向p的指針 int *r=&b //錯誤,指針類型與對象類型不匹配
指針的值(即地址)有四種狀態:
- 指向一個對象
- 指向緊鄰對象所占空間的下一個位置(int *p=&i,p++)
- 空指針,意味着指針沒有指向任何對象
- 無效指針,也就是上述情況之外的其他值
訪問無效指針將引發錯誤,但是編譯的時候並不會報錯,運行的時候才會報錯,訪問無效指針的后果無法預計,因此程序必須清楚給定的指針是否有效。
盡管上述第2和第3中形式的指針是有效的,顯然這些指針沒有指向任何對象,所以試圖訪問此類指針對象的行為也是不允許的,如果這樣做了,后果也無法預計。
如果指針指向了一個對象,就可以用解引用符(操作符*)來訪問對象,下面看一個引用和指針結合的例子:
int i = 42; int &r = i; //r為i的引用 int *p; p = &i; //p是指向i的指針 cout<<*p<<endl; //輸出42 *p = 32; //相當於給i賦值 cout<<i<<endl; //輸出32 int &r2 = *p; //相當於int &r2 = i; cout<<r2<<endl; //輸出32 r2=22; //改變i的值 cout<<i<<" "<<r2<<" "<<*p<<endl; //三者相等,都為22 *p=12; cout<<i<<" "<<r2<<" "<<*p<<endl; //三者相等,都為12 i=2; cout<<i<<" "<<r2<<" "<<*p<<endl; //三者相等,都為2 int j = 52; *p=j; //相當於將j的值賦給i,i=j; cout<<i<<" "<<r2<<" "<<*p<<endl; //三者相等,都為52 p=&j; //將p指向j, j=62; cout<<i<<" "<<r2<<" "<<*p<<endl; //i=r2=52,*p=62 r=j; 相當於i=j, cout<<i<<" "<<r2<<" "<<*p<<endl; //都為62
可能有些同學對&和*的含義理解的很模糊,為什么一會表示這樣,一會又是另外一個意思。確實,像&和*這樣的符號,既可以用作表達式里的運算符,也能作為聲明的一部分出現,符號的上下文決定了符號的意義:
int i=2; int &r=i; //&緊隨類型名出現,因此是聲明的一部分,r是一個引用 int *p; //*緊隨類型嗎出現,因此是聲明的一部分,p是一個指針 p=&i; //&出現在表達式中,是一個取地址符 *p=i; //*出現在表達式中,是一個解引用符 int &r2=*p;//&是聲明的一部分,是引用,*是一個解引用符
空指針不指向任何對象,定義空指針的方法:
int *p = nullptr;//推薦方法 int *p2=0; int *p3=NULL;//NULL的值就是0,等價int *p3=0
得到空指針最好的方法是第一種,它是C++11新標准新引入的方法,nullptr是一種特殊類型的值,它可以被轉換成任意其它類型的指針。NULL是一個預處理變量,預處理器會自動將它替換為0。新標准下,最好使用nullptr,同時盡量避免使用NULL。
另外不能直接把int變量直接賦給指針,就算int變量為0也不行
int z=0; int *p=z;//錯誤,不能把int變量直接賦給指針
前面已經說過,指針和引用都能提供對其它對象的間接訪問,然后在實現細節上二者差別很大,引用本身不是一個對象,一旦綁定一個對象就不能更改。而指針則不同,它只要對指針賦值存放一個新的地址,就指向了一個新的對象:
int i=2; int *pi=0; //pi是空指針 int *pi2=&i; //pi2指向i int *pi3; //pi3的值無法確定 pi3=pi2; //pi3和pi2都指向同一個對象i pi2=0; //現在pi2不指向任何對象,但pi3還是指向i
指向指針的指針
一般來說聲明符中修飾符的個數並沒有限制,譬如可以有用**來表示指向指針的指針,***表示指向指針的指針的指針,依次類推:
int i=1024; int *pi=&i; //pi指向int型的數 int **ppi=π //ppi指向一個int型的指針
輸出i,*pi和**ppi的值都是一樣,都為1024.
指向指針的引用
如前所述,引用不是一個對象,所以就不會存在指向引用的指針,但指針是對象,所以存在指向指針的引用:
int i=42; int *p; //p是一個未被初始化的指針 int *&r=p; //r是一個指向指針p的引用 r=&i; //相當於p=&i; cout<<i<<" "<<*p<<" "<<*r<<endl;//三者的值相等
對於上面r的類型到底是什么,教給大家一個最簡單的方法,離變量名最近的符號(此處是&)決定變量的類型,所以此處r肯定是一個引用,聲明符的部分確定r引用的類型是什么,此處是*,就說明引用的是一個指針,最后,聲明的數據類型是int,那么,也就是說,r引用的是一個int型的指針。以后這樣復雜的定義都是這樣理解。
3.const限定符
const是用作定義常量,一旦某個對象定義為一個常量,那么就不能改變這個對象的值。
const int buff = 512; buff=1024//錯誤,試圖向const對象賦值 const int i=get_size(); //正確:運行時初始化 const int j = 42; //正確:編譯時初始化 const int k; //錯誤:k是一個未經初始化的變量 int h=12; const int m=h; //正確:h的值拷貝給了m int n=m; //正確:m的值拷貝了n
可以把引用綁定到一個const對象上,稱為對常量的引用。與普通引用不同的是,對常量的引用不能被用作修改它所綁定的對象,另外允許一個常量引用綁定一個非常量的對象、子面值,甚至是一個表達式:
const int ci=1024; const int &r1=ci; //正確:引用及其對應的對象都是常量 r1=42; //錯誤:r1是對常量的引用,不能修改 int &r2 = ci; //錯誤,非常量引用不能指向常量對象 int i=42; const int &r1=i; //正確:允許常量引用綁定到普通int對象上 const int &r2=42; //正確:r2為常量引用 const int &r3=r1*2; //正確:r3為常量引用 i=2; cout<<r1<<" "<<i<<" "<<r3<<endl; //r1=i=2,r3=84; double d = 3.14; const int &r4 = d; //正確:r4為一個綁定到int對象的常量引用 cout<<d<<" "<<r4<<endl; //d=3.14,r4=3 d=5.15; cout<<d<<" "<<r4<<endl; //d=5.15,r4=3
大家一定很奇怪,為什么在將引用的時候說兩邊的類型要一致,並且要指向一個對象,但是在這里又存在這樣的一種情況,就算指向不同類型和一個具體的數值都可以。要想理解者是為什么,就要弄清楚當一個常量引用綁定到另外一種類型上時到底發生了什么:
在最后的那一段代碼中,其實編譯器為了確保r4綁定一個整型對象,編譯器引入了一個臨時變量,將代碼變成了如下形式:
double d=3.14; const int temp=d; const int &r4=temp;
寫成這樣以后,大家就好理解了,那么我們回到之前所說的,如果r4不是const,那么為什么就引用的類型和對象的類型必須一致,你可以這樣想,如果r4不是常量,那么就允許對r4賦值,也就會改變r4所引用對象的值,但是注意,r4綁定的是一個臨時變量而不是對象d,所以r4改變不了d的值,如此看來r4引用d,但是不能用來改變d的值,那么就沒有什么意義了,C++也就將這種行為歸為非法了。
與引用一樣,也可以令指針指向常量或非常量,類似於常量引用,指向常量的指針不能用於改變其所指對象的值,並且,指向常量的指針也沒有規定其所指的對象必須是一個常量。所謂指向常量的指針僅僅要求不能通過指針改變對象的值,而沒有規定那個對象的值不能通過其他途徑改變。
const double pi=3.14; //pi是一個常量,它的值不能改變 double *ptr=π //錯誤:ptr不能指向常量 const double *cptr = π //正確 *cptr = 2.1; //錯誤,不能給*cptr賦值 double d = 3.14; cptr = &d; //正確 *cptr=4.1; //錯誤:不能直接修改*cptr的值 d=4.1; //正確,*cptr=4.1
指針本身是一個對象,所以可以把*放在const之前來說明指針本身是一個常量,這樣寫的意思是,不變的是指針本身的值而不是指向的那個值:
int errNumb = 0; int c=1; int *const curErr = &errNumb; //curErr將一直指向errNumb,不能修改 curErr=&c; //錯誤,curErr不能修改 *curErr = c; //正確:curErr不能修改,但是*curErr可以修改 cout<<*curErr<<" "<<errNumb<<endl; //都等於1 const double pi = 3.14; double p=4.5; const double *const pip = π //pip是一個指向常量對象的常量指針 pip=&p; //錯誤 *pip=p; //錯誤 const int ci=42; int bi = 32; const int *p2=&ci; //正確:p2指向ci const int *const p3=p2; //正確:p3和p2同時指向ci cout<<*p2<<" "<<*p3<<endl; //二者相等 p2=&bi; //p2指向bi,p3指向ci cout<<*p2<<" "<<*p3<<endl; //*p2=32,*p3=42 p2=p3; //正確 int &r=ci; //錯誤:普通int不能綁定到int常量上
在這里,教給大家一個方法,判斷向上面這種賦值語句,你只需要看假設能夠賦值成功,那么改變左邊的值,右邊的對象會不會出現錯誤,例如:p2=p3,不能改變*p2,但是可以改變p2,但是改變p2的值,對p3的值沒有造成影響,所以可以p2=p3,為什么int &r=ci就錯了,假設可以賦值成功,那么就可以改變r的值來改變ci的值了,但是ci是const型,不能改變,所以上式就有問題。像這樣的賦值語句都可以這樣考慮。