1. 程式人生 > >指標與引用總結

指標與引用總結

指標

若現在指標不知道指向,可以使用NULL,例如int *b = NULL; char *a = NULL;,但是使用的時候若指標指向的是陣列或者字串等,需要提前宣告大小。若是int *,則不需要,直接將一個int型別的指標賦值給它即可。

c++中的*&對於初學者來說,確實有點讓人搞懵。因為在變數的定義和呼叫時,*&都會表現出不一樣的含義。

* 總結

定義一個指標的三種寫法都對:1. int * p; 2. int* p; 3. int *p; 習慣不同而已
定義一個函式指標的三種寫法都對:1. int *p(); 2. int * p(); 3. int* p();

用於定義

*在定義時是宣告該變數是一個指標,例如

int *p; //那p就是一個int型的指標。

例1:

int a = 0; 
int *p = a; //那p的值就是a的地址。

上面的int a = 0; int *p = a;實際上是int a = 0; int *p = &a;這兩者是等價的!!!

原因是int *p = &a;時,c語言本身提供了可以略去&的簡寫,但是本人不是很喜歡這樣的寫法,因為這樣會誤導初學者!

所以大家還是寫全比較規範一點,寫完int *p = &a,這樣比較好。

c++就沒有這樣的簡寫機制,大家可以測試一下。

用於呼叫時

*在呼叫時是指標指向的那個變數,是取值運算子。

例如:

int a = 0; 
int *p = &a; 
printf("*p = %d\n", *p);

&總結

用於定義時

&在定義時是定義一個引用

例2:

int a = 0; 
int &b = a;

那麼b就是a的引用,即b=0;如果再給a賦值a=10,則b也會變為10;如果給b賦值b=20,則a也會變為20;

關於具體引用的介紹我們將會在下面詳細的進行介紹

用於呼叫時

&在呼叫時是一個取地址運算子。
例如:

int a = 0; 
printf("&a = %p\n", &a);

會打印出a的地址,這個地址因為變數a在各個計算機的地址的不一樣,所以列印的也不一樣。

&在呼叫時還有一種與運算,如:int a = 0; a&=0; //按位與操作,這個就不細說了

關於int *a; int &a; int & *a; int * &a; (int*) &a

上述的四條語句,前面兩個很好理解,而後面兩個,大部分C++初學者都會比較困惑,今天我也是查閱了一些資料以後才恍然大悟。下面具體來說明一下:

int i;

int *a = &i;//這裡a是一個指標,它指向變數i

int &b = i;//這裡b是一個引用,它是變數i的引用,引用是什麼?它的本質是什麼?下面會具體講述

int * &c = a;//這裡c是一個引用,它是指標a的引用

int & *d;//這裡d是一個指標,它指向引用,但引用不是實體,所以這是錯誤的

(int*) &a為取a的地址,然後進行強制型別轉換,轉為int型別的指標。

int * &aint & *a

我在寫這兩句語句時,在int*(&)間空了一格,而後面的&(*)緊跟a。原因是:分析此類語句時,最簡單的辦法就是從右往左讀,離變數名最近的符號對其型別有最直接的影響,即先看a前緊跟的是什麼,它決定了a的型別。例如,對於int & *a,此處是*,表示其首先是個指標,指標的型別是一個int型引用。而int後的一個空格是為了防止int *a, b;//a是指標,而b不是

假設有一個 int 型別的變數 apa是指向它的指標,那麼*&a&*pa分別是什麼意思呢?

*&a可以理解為*(&a)&a表示取變數 a 的地址(等價於 pa),*(&a)表示取這個地址上的資料(等價於 *pa),繞來繞去,又回到了原點,*&a仍然等價於 a。

&*pa可以理解為&(*pa)*pa表示取得 pa 指向的資料(等價於 a),&(*pa)表示資料的地址(等價於 &a),所以&*pa等價於 pa

int **a;

它的功能和int *&a一樣,兩者生成的二進位制程式碼一樣。這再一次說明了引用就是指標。

引用

什麼是引用

引用,顧名思義是某一個變數或物件的別名,對引用的操作與對其所繫結的變數或物件的操作完全等價

語法:型別 &引用名=目標變數名;

&出現在等式左邊的定義時。或者當&出現在函式的引數值中的時候是引用。或者在函式定義時,如float &fn2(float r)

特別注意:

  • &不是求地址運算子,而是起標誌作用

  • 引用的型別必須和其所繫結的變數的型別相同

#include<iostream>
using namespace std;
int main(){
    double a=10.3;
    int &b=a; //錯誤,引用的型別必須和其所繫結的變數的型別相同
    cout<<b<<endl;
}

報錯

error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'|
  • 宣告引用的同時必須對其初始化,否則系統會報錯
#include<iostream>
using namespace std;
int main(){
    int &a; //錯誤!宣告引用的同時必須對其初始化
    return 0;
}
  • 引用相當於變數或物件的別名,因此不能再將已有的引用名作為其他變數或物件的名字或別名

  • 引用不是定義一個新的變數或物件,因此記憶體不會為引用開闢新的空間儲存這個引用

#include<iostream>
using namespace std;
int main(){
    int value=10;
    int &new_value=value;
    cout<<"value的值為:"<<value<<endl;
    cout<<"new_value的值為:"<<new_value<<endl;
    cout<<"value在記憶體中的地址為:"<<&value<<endl;
    cout<<"new_value在記憶體中的地址為:"<<&new_value<<endl;
    return 0;
}

即可簡單的理解為上例中的new_valuevalue

對陣列的引用

語法:型別 (&引用名)[陣列中元素數量]=陣列名;

例如

#include<iostream>
using namespace std;
int main(){
    int a[3]={1,2,3};
    int (&b)[3]=a;//對陣列的引用
    cout<<&a[0]<<" "<<&b[0]<<endl;
    cout<<&a[1]<<" "<<&b[1]<<endl;
    cout<<&a[2]<<" "<<&b[2]<<endl;
    return 0;
}

對指標的引用

語法:型別 *&引用名=指標名;//可以理解為:(型別*) &引用名=指標名,即將指標的型別當成型別*

例如:

#include<iostream>
using namespace std;
int main(){
    int a=10;
    int *ptr=&a;
    int *&new_ptr=ptr;
    //int *&new_ptr1 = &a; //error: invalid initialization of non-const reference of type 'int*&' from an rvalue of type 'int*'|
    cout<<ptr<<" "<<new_ptr<<endl;
    cout<<&ptr<<" "<<&new_ptr<<endl;
    return 0;
}

上例可理解為new_ptr為變數ptr的引用,因此兩者的地址均相同。

上例中的int *&new_ptr1 = &a;會報錯,可以理解為引用的型別只能與繫結的型別一致,因此只能給new_ptr1賦值指標變數。

引用的應用

引用作為函式的引數

#include<iostream>
using namespace std;
void swap(int &a,int &b){//引用作為函式的引數
    int temp=a;
    a=b;
    b=temp; 
}
int main(){
    int value1=10,value2=20;
    cout<<"----------------------交換前----------------------------"<<endl;
    cout<<"value1的值為:"<<value1<<endl; 
    cout<<"value2的值為:"<<value2<<endl;
    swap(value1,value2); 
    cout<<"----------------------交換後----------------------------"<<endl;
    cout<<"value1的值為:"<<value1<<endl;
    cout<<"value2的值為:"<<value2<<endl;

上述例子可以簡單的理解為但value1傳入到swap函式中的時候,為int &a = value1,對應上面介紹的引用定義。

特別注意:

  • 當用引用作為函式的引數時,其效果和用指標作為函式引數的效果相當。當呼叫函式時,函式中的形參就會被當成實參變數或物件的一個別名來使用,也就是說此時函式中對形參的各種操作實際上是對實參本身進行操作,而非簡單的將實參變數或物件的值拷貝給形參

  • 通常函式呼叫時,系統採用值傳遞的方式將實參變數的值傳遞給函式的形參變數。此時,系統會在記憶體中開闢空間用來儲存形參變數,並將實參變數的值拷貝給形參變數,也就是說形參變數只是實參變數的副本而已;並且如果函式傳遞的是類的物件,系統還會呼叫類中的拷貝建構函式來構造形參物件。而使用引用作為函式的形參時,由於此時形參只是要傳遞給函式的實參變數或物件的別名而非副本,故系統不會耗費時間來在記憶體中開闢空間來儲存形參。因此如果引數傳遞的資料較大時,建議使用引用作為函式的形參,這樣會提高函式的時間效率,並節省記憶體空間

  • 使用指標作為函式的形參雖然達到的效果和使用引用一樣,但當呼叫函式時仍需要為形參指標變數在記憶體中分配空間,而引用則不需要這樣,故在C++中推薦使用引用而非指標作為函式的引數

  • 如果在程式設計過程中既希望通過讓引用作為函式的引數來提高函式的程式設計效率,又希望保護傳遞的引數使其在函式中不被改變,則此時應當使用對常量的引用作為函式的引數。

  • 陣列的引用作為函式的引數:C++的陣列型別是帶有長度資訊的,引用傳遞時如果指明的是陣列則必須指定陣列的長度

#include<iostream>
using namespace std;
void func(int(&a)[5])
{
    //陣列引用作為函式的引數,必須指明陣列的長度 
    //函式體 
}
int main()
{
    int number[5]={0,1,2,3,4};
    func(number); 
    return 0; 
}

常引用

語法:const 型別 &引用名=目標變數名;

常引用不允許通過該引用對其所繫結的變數或物件進行修改

#include<iostream>
using namespace std;
int main(){
    int a=10;
    const int &new_a=a;
    new_a=11;//錯誤!不允許通過常引用對其所繫結的變數或物件進行修改 
    return 0;
}

特別注意

#include<iostream>
#include<string> 
using namespace std;
string func1(){
    string temp="This is func1";
    return temp;
}
void func2(string &str){
    cout<<str<<endl;
}
int main(){
    func2(func1());
    func2("Tomwenxing");
    return 0;
}

執行上面的程式編譯器會報錯

這是由於func1()和“Tomwenxing”都會在系統中產生一個臨時物件(string物件)來儲存它們,而在C++中所有的臨時物件都是const型別的,而上述func1返回值與"Tomwenxing"均沒有與之對應的變數名,而上面的程式試圖將const物件賦值給非const物件,這必然會使程式報錯。如果在函式func2的引數前新增const,則程式便可正常運行了

#include<iostream>
#include<string>
using namespace std;
string func1(){
    string temp="This is func1";
    return temp;
}
void func2(const string &str){
    cout<<str<<endl;
    //str = "hello";  // 報錯,因為const不可更改
}
int main(){
    func2(func1());
    func2("Tomwenxing");
    return 0;
}

或者可以改為下面形式,將上面的常量給賦值成為變數:

#include<iostream>
#include<string>
using namespace std;
string func1(){
    string temp="This is func1";
    return temp;
}
void func2(string &str){
    cout<<str<<endl;
}
int main(){
    string one = func1();
    func2(one);
    string two = "Tomwenxing";
    func2(two);
    return 0;
}

引用作為函式的返回值

語法:型別 &函式名(形參列表){ 函式體 }
注意事項1
  • 引用作為函式的返回值時,&必須在定義函式時在函式名前
注意事項2
  • 用引用作函式的返回值的最大的好處是在記憶體中不產生返回值的副本
//程式碼來源:RUNOOB
#include<iostream>
using namespace std;
float temp;
float fn1(float r){
    temp=r*r*3.14;
    return temp;
} 
float &fn2(float r){ //&說明返回的是temp的引用,換句話說就是返回temp本身
    temp=r*r*3.14;
    return temp;
}
int main(){
    float a=fn1(5.0); //case 1:返回值
    //float &b=fn1(5.0); //case 2:用函式的返回值作為引用的初始化值 [Error] invalid initialization of non-const reference of type 'float&' from an rvalue of type 'float'
                           //(有些編譯器可以成功編譯該語句,但會給出一個warning) 
    float c=fn2(5.0);//case 3:返回引用
    float &d=fn2(5.0);//case 4:用函式返回的引用作為新引用的初始化值
    cout<<a<<endl;//78.5
    //cout<<b<<endl;//78.5
    cout<<c<<endl;//78.5
    cout<<d<<endl;//78.5
    return 0;
}

上例中4個case的說明解釋:

case 1

用返回值方式呼叫函式(如下圖,圖片來源:伯樂線上):

返回全域性變數temp的值時,C++會在記憶體中建立臨時變數並將temp的值拷貝給該臨時變數。當返回到主函式main後,賦值語句a=fn1(5.0)會把臨時變數的值再拷貝給變數a

case 2

用函式的返回值初始化引用的方式呼叫函式(如下圖,圖片來源:伯樂線上)

這種情況下,函式fn1()是以值方式返回到,返回時,首先拷貝temp的值給臨時變數。返回到主函式後,用臨時變數來初始化引用變數b,使得b成為該臨時變數到的別名。由於臨時變數的作用域短暫(在C++標準中,臨時變數或物件的生命週期在一個完整的語句表示式結束後便宣告結束,也就是在語句float &b=fn1(5.0);之後) ,所以b面臨無效的危險,很有可能以後的值是個無法確定的值。

如果真的希望用函式的返回值來初始化一個引用,應當先建立一個變數,將函式的返回值賦給這個變數,然後再用該變數來初始化引用:

int x=fn1(5.0);
int &b=x;
case 3

用返回引用的方式呼叫函式(如下圖,圖片來源:伯樂線上)

這種情況下,函式fn2()的返回值不產生副本,而是直接將變數temp返回給主函式,即主函式的賦值語句中的左值是直接從變數temp中拷貝而來(也就是說c只是變數temp的一個拷貝而非別名) ,這樣就避免了臨時變數的產生。尤其當變數temp是一個使用者自定義的類的物件時,這樣還避免了呼叫類中的拷貝建構函式在記憶體中建立臨時物件的過程,提高了程式的時間和空間的使用效率。

case 4

用函式返回的引用作為新引用的初始化值的方式來呼叫函式(如下圖,圖片來源:伯樂線上)

這種情況下,函式fn2()的返回值不產生副本,而是直接將變數temp返回給主函式。在主函式中,一個引用宣告d用該返回值初始化,也就是說此時d成為變數temp的別名。由於temp是全域性變數,所以在d的有效期內temp始終保持有效,故這種做法是安全的。

注意事項3

**不能返回區域性變數的引用。**如上面的例子,如果temp是區域性變數,那麼它會在函式返回後被銷燬,此時對temp的引用就會成為“無所指”的引用,程式會進入未知狀態。

注意事項4

不能返回函式內部通過new分配的記憶體的引用。雖然不存在區域性變數的被動銷燬問題,但如果被返回的函式的引用只是作為一個臨時變量出現,而沒有將其賦值給一個實際的變數,那麼就可能造成這個引用所指向的空間(有new分配)無法釋放的情況(由於沒有具體的變數名,故無法用delete手動釋放該記憶體),從而造成記憶體洩漏。因此應當避免這種情況的發生

注意事項5

當返回類成員的引用時,最好是const引用。這樣可以避免在無意的情況下破壞該類的成員。

注意事項6

可以用函式返回的引用作為賦值表示式中的左值

#include<iostream>
using namespace std;
int value[10];
int error=-1;
int &func(int n){
    if(n>=0&&n<=9)
        return value[n];//返回的引用所繫結的變數一定是全域性變數,不能是函式中定義的區域性變數 
    else
        return error;
}

int main(){
    func(0)=10;
    func(4)=12;
    cout<<value[0]<<endl;
    cout<<value[4]<<endl;
    return 0; 
}

用引用實現多型

在C++中,引用是除了指標外另一個可以產生多型效果的手段。也就是說一個基類的引用可以用來繫結其派生類的例項

class Father;//基類(父類)
class Son:public Father{.....}//Son是Father的派生類
Son son;//son是類Son的一個例項
Father &ptr=son;//用派生類的物件初始化基類物件的使用

特別注意:
ptr只能用來訪問派生類物件中從基類繼承下來的成員如果基類(類Father)中定義的有虛擬函式,那麼就可以通過在派生類(類Son)中重寫這個虛擬函式來實現類的多型。

引用總結

  • 在引用的使用中,單純給某個變數去別名是毫無意義的,引用的目的主要用於在函式引數的傳遞中,解決大塊資料或物件的傳遞效率和空間不如意的問題

  • 用引用傳遞函式的引數,能保證引數在傳遞的過程中不產生副本,從而提高傳遞效率,同時通過const的使用,還可以保證引數在傳遞過程中的安全性

  • 引用本身是目標變數或物件的別名,對引用的操作本質上就是對目標變數或物件的操作。因此能使用引用時儘量使用引用而非指標

指標和引用的對比

指標

對於一個型別TT*就是指向T的指標型別,也即一個T*型別的變數能夠儲存一個T物件的地址,而型別T是可以加一些限定詞的,如const、volatile等等。見下圖,所示指標的含義:

引用

引用是一個物件的別名,主要用於函式引數和返回值型別,符號X&表示X型別的引用。見下圖,所示引用的含義:

區別

首先,引用不可以為空,但指標可以為空。前面也說過了引用是物件的別名,引用為空——物件都不存在,怎麼可能有別名!故定義一個引用的時候,必須初始化。因此如果你有一個變數是用於指向另一個物件,但是它可能為空,這時你應該使用指標;如果變數總是指向一個物件,i.e.,你的設計不允許變數為空,這時你應該使用引用。如下圖中,如果定義一個引用變數,不初始化的話連編譯都通不過(編譯時錯誤):

而宣告指標是可以不指向任何物件,也正是因為這個原因,使用指標之前必須做判空操作,而引用就不必

其次,引用不可以改變指向,對一個物件"至死不渝";但是指標可以改變指向,而指向其它物件。說明:雖然引用不可以改變指向,但是可以改變初始化物件的內容。例如就++操作而言,對引用的操作直接反應到所指向的物件,而不是改變指向;而對指標的操作,會使指標指向下一個物件,而不是改變所指物件的內容。見下面的程式碼:

#include<iostream>
 
using namespace std;
 
int main(int argc,char** argv)
{
 
    int i=10;
    int& ref=i;
    ref++;
    cout<<"i="<<i<<endl;
    cout<<"ref="<<ref<<endl;
 
    int j=20;
    ref=j;
    ref++;
    cout<<"i="<<i<<endl;
    cout<<"ref="<<ref<<endl;
    cout<<"j="<<j<<endl;
    return 0;
}

ref++操作是直接反應到所指變數之上,對引用變數ref重新賦值"ref=j"(此處要注意ref是可以重新在賦值的,但指向並不會發生變化),並不會改變ref的指向,它仍然指向的是i,而不是j。理所當然,這時對ref進行++操作不會影響到j。而這些換做是指標的話,情況大不相同,請自行實驗。輸出結果如下:

再次,引用的大小是所指向的變數的大小,因為引用只是一個別名而已;指標是指標本身的大小,4個位元組。見下圖所示:

從上面也可以看出:引用比指標使用起來形式上更漂亮,使用引用指向的內容時可以之間用引用變數名,而不像指標一樣要使用;定義引用的時候也不用像指標一樣使用&取址。*

最後,引用比指標更安全。由於不存在空引用,並且引用一旦被初始化為指向一個物件,它就不能被改變為另一個物件的引用,因此引用很安全。對於指標來說,它可以隨時指向別的物件,並且可以不被初始化,或為NULL,所以不安全。const 指標雖然不能改變指向,但仍然存在空指標,並且有可能產生野指標(即多個指標指向一塊記憶體,free掉一個指標之後,別的指標就成了野指標)。

總而言之,言而總之——它們的這些差別都可以歸結為"指標指向一塊記憶體,它的內容是所指記憶體的地址;而引用則是某塊記憶體的別名,引用不改變指向。"

指標傳遞和引用傳遞比較

在C語言中,如果要實現在函式內部改變外部變數的值的話,就應該傳遞這個變數的指標。如果要通過指標訪問變數,必須使用指標運算子*。這樣在原始碼中就會顯得比較彆扭:

void function(int *pval)
{
    *pval=100;
    //pval=100;先不考慮此處型別轉換的錯誤,該程式碼只能改變堆疊中臨時指標變數的地址,而不能改變指標指向物件的值
}
int main()
{
   int x=200;
   function(&x);
   //或者如下呼叫
   int *refx=&x;
   function(refx);
   return 0;
}

為了能透明地使用指標來訪問變數,C++中引入了“引用”的概念

void function(int &refval)
{
 refval=100;
}
int main()
{
 int x=200; 
 function(x);
 //當然,如下呼叫也可以。但這樣做就失去引入"引用"的原本意義了
 int &refx=x;
 function(refx);
 return 0;
}

這樣一來,只要改一下函式宣告,就可以在原始碼的級別上實現指標訪問和一般訪問的一致性。可以把“引用”想象成一個不需要"*"操作符就可以訪問變數的指標

總結

從概念上講。指標從本質上講就是存放變數地址的一個變數,在邏輯上是獨立的,它可以被改變,包括其所指向的地址的改變和其指向的地址中所存放的資料的改變。

而引用是一個別名,它在邏輯上不是獨立的,它的存在具有依附性,所以引用必須在一開始就被初始化,而且其引用的物件在其整個生命週期中是不能被改變的(自始至終只能依附於同一個變數)。

在C++中,指標和引用經常用於函式的引數傳遞,然而,指標傳遞引數和引用傳遞引數是有本質上的不同的:

指標傳遞引數本質上是值傳遞的方式,它所傳遞的是一個地址值。值傳遞過程中,被調函式的形式引數作為被調函式的區域性變數處理,即在棧中開闢了記憶體空間以存放由主調函式放進來的實參的值,從而成為了實參的一個副本。值傳遞的特點是被調函式對形式引數的任何操作都是作為區域性變數進行,不會影響主調函式的實參變數的值。

而在引用傳遞過程中,被調函式的形式引數雖然也作為區域性變數在棧中開闢了記憶體空間,但是這時存放的是由主調函式放進來的實參變數的地址(指標傳遞引數時,指標中存放的也是實參的地址,但是在被調函式內部指標存放的內容可以被改變,即可能改變指向的實參,所以並不安全,而引用則不同,它引用的物件的地址一旦賦予,則不能改變)。被調函式對形參的任何操作都被處理成間接定址,即通過棧中存放的地址訪問主調函式中的實參變數。正因為如此,被調函式對形參做的任何操作都影響了主調函式中的實參變數。

引用傳遞和指標傳遞是不同的,雖然它們都是在被調函式棧空間上的一個區域性變數,但是任何對於引用引數的處理都會通過一個間接定址的方式操作到主調函式中的相關變數。而對於指標傳遞的引數,如果改變被調函式中的指標地址,它將影響不到主調函式的相關變數。如果想通過指標引數傳遞來改變主調函式中的相關變數,那就得使用指向指標的指標,或者指標引用。即指標傳遞只是傳了一個地址copy, 在函式內部改變形參所指向的地址,不能改變原實參指向的地址,僅可以通過修改形參地址的內容,來達到修改實參內容的目的(原C語言中的通過指標來互換值小函式例子),所以如果想通過被調函式來修改原實參的地址或給重新分配一個物件都是不能完成的,只能使用雙指標或指標引用(下面會進行詳解)

如以下指標傳遞的例子:

#include<iostream>
using namespace std;
void function(int *pval)
{
    int a = 200;
    pval = &a;
    *pval = 300;
    cout<<"內部函式值:"<<*pval<<endl;
    //pval=100;先不考慮此處型別轉換的錯誤,該程式碼只能改變堆疊中臨時指標變數的地址,而不能改變指標指向物件的值
}
int main()
{
   int x = 100;
   function(&x);
   cout<<"外部函式值:"<<x<<endl;
   return 0;
}

執行結果:

如以下引用傳遞的例子:

#include<iostream>
using namespace std;
void function(int &pval)
{
    int a = 200;
    pval = a;
    cout<<"內部函式值:"<<pval<<endl;
    //pval=100;先不考慮此處型別轉換的錯誤,該程式碼只能改變堆疊中臨時指標變數的地址,而不能改變指標指向物件的值
}
int main()
{
   int x = 100;
   function(x);
   cout<<"外部函式值:"<<x<<endl;
   return 0;
}

為了進一步加深大家對指標和引用的區別,下面我從編譯的角度來闡述它們之間的區別:

程式在編譯時分別將指標和引用新增到符號表上,符號表上記錄的是變數名及變數所對應地址。指標變數在符號表上對應的地址值為指標變數的地址值,而引用在符號表上對應的地址值為引用物件的地址值。符號表生成後就不會再改,因此指標可以改變其指向的物件(指標變數中的值可以改),而引用物件則不能修改。

最後,總結一下指標和引用的相同點和不同點:

相同點:

  • 都是地址的概念;
    指標指向一塊記憶體,它的內容是所指記憶體的地址;而引用則是某塊記憶體的別名。

不同點:

  • 指標是一個實體,而引用僅是個別名;
  • 引用只能在定義時被初始化一次,之後不可變;指標可變;引用“從一而終”,指標可以“見異思遷”;
  • 引用沒有const,指標有constconst的指標不可變;(具體指沒有int& const a這種形式,而const int& a是有的,前者指引用本身即別名不可以改變,這是當然的,所以不需要這種形式,後者指引用所指的值不可以改變)
  • 引用不能為空,指標可以為空;
  • sizeof 引用得到的是所指向的變數(物件)的大小,而sizeof 指標得到的是指標本身的大小;
  • 指標和引用的自增(++)運算意義不一樣;
  • 引用是型別安全的,而指標不是 (引用比指標多了型別檢查)

指標的指標和指標的引用對比

在下列函式宣告中,為什麼要同時使用*&符號?以及什麼場合使用這種宣告方式?

void func1( MYCLASS *&pBuildingElement );

先來看int **ppint *&rp區別。前者是一個指向指標的指標;後者是一個指標的引用。如果這樣看不明白的話,變換一下就清楚了:

typedef int * LPINT;
 LPINT *pp;
 LPINT &rp;

而指標的指標和指標的引用作為傳遞引數時,如下面的兩個函式在被呼叫時,編譯器編譯的二進位制程式碼都將傳遞一個雙重指標,只不過兩者的呼叫方法不同:

void function1(int **p)
{
 **p=100;
 *p=NULL;
}
void function2(int *&ref)
{
 *ref=100;
 ref=NULL;
}

可見,“引用”僅僅是為了給過載操作符提供了方便之門,其本質和指標是沒有區別的。所以只要你碰到*&,就應該想到**。也就是說這個函式修改或可能修改呼叫者的指標,而呼叫者象普通變數一樣傳遞這個指標,不使用地址操作符&

關於指標的引用的詳解

下面用三個函式onePointerFunc,poiPointerFunc, refPointerFunc舉例詳解,三個函式均想要在函式呼叫完畢後可以指向新的物件。

傳單指標:

voidonePointerFunc(MYCLASS *pMyClass) 
{ 
DoSomething(pMyClass); 
pMyClass = // 其它物件的指標 
}  

呼叫:MYCLASS* p = new MYCLASS; onePointerFunc(p); 呼叫onePointerFuncp沒有指向新的物件:

第二條語句在函式過程中只修改了pMyClass的值。並沒有修改呼叫者的變數p的值。如果p指向某個位於地址0x008a00的物件,當func1返回時,它仍然指向這個特定的物件。

傳雙指標:

voidpoiPointerFunc(MYCLASS** pMyClass); 
{ 
*pMyClass = new MYCLASS; 
}   

呼叫:MYCLASS* p =new MYCLASS;poiPointerFunc(&p);呼叫poiPointerFunc之後,p指向新的物件。

BTW,在COM程式設計中,到處都會碰到這樣的用法–例如在查詢物件介面的QueryInterface函式中:

 interface ISomeInterface { 
    HRESULT QueryInterface(IID &iid, void** ppvObj); 
    …… 
    }; 
    LPSOMEINTERFACE p=NULL; 
    pOb->QueryInterface(IID_SOMEINTERFACE, &p);

此處,pSOMEINTERFACE型別的指標,所以&p便是指標的指標,在QueryInterface返回的時候,如果呼叫成功,則變數p包含一個指向新的介面的指標。

傳指標的引用:

voidrefPointerFunc(MYCLASS *&pMyClass); 
    { 
    pMyClass = new MYCLASS; 
    …… 
    }   

其實,它和前面所講得指標的指標例子是一碼事,只是語法有所不同。傳遞的時候不用傳p的地址&p,而是直接傳p本身:  
呼叫:MYCLASS* p = new MYCLASS; refPointerFunc(p);呼叫refPointerFunc之後,p指向新的物件。

MFC在其集合類中用到的*&作為返回修飾符的例子–CObList,它是一個CObjects指標列表。

class CObList : public CObject { 
    …… 
    // 獲取/修改指定位置的元素 
    CObject*& GetAt(POSITION position); 
    CObject* GetAt(POSITION position) const; 
    };   

這裡有兩個GetAt函式,功能都是獲取給定位置的元素。區別何在呢? 區別在於一個讓你修改列表中的物件,另一個則不行。所以如果你寫成下面這樣:CObject* pObj = mylist.GetAt(pos);pObj是列表中某個物件的指標,

如果接著改變pObj的值: pObj = pSomeOtherObj; 這並改變不了在位置pos處的物件地址,而僅僅是改變了變數pObj.

但是,如果寫成下面這樣: CObject*& rpObj = mylist.GetAt(pos);。現在,rpObj是引用一個列表中的物件的指標,所以當改變rpObj時,也會改變列表中位置pos處的物件地址–換句話說,替代了這個物件。這就是為什麼CObList會有兩個GetAt函式的緣故。一個可以修改指標的值,另一個則不能。注意我在此說的是指標,不是物件本身。這兩個函式都可以修改物件,但只有*&版本可以替代物件

const

在這裡我為什麼要提到const關鍵字呢?因為const對指標和引用的限定是有差別的,下面聽我一一到來。

常量指標VS常量引用

常量指標

指向常量的指標,在指標定義語句的型別前加const,表示指向的物件是常量。

定義指向常量的指標只限制指標的間接訪問操作,而不能規定指標指向的值本身的操作規定性。

常量指標定義"const int* pointer=&a"告訴編譯器,*pointer是常量,不能將*pointer作為左值進行操作。

常量引用

指向常量的引用,在引用定義語句的型別前加const,表示指向的物件是常量。也跟指標一樣不能利用引用對指向的變數進行重新賦值操作。

指標常量VS引用常量

在指標定義語句的指標名前加const,表示指標本身是常量。在定義指標常量時必須初始化!而這是引用天生具來的屬性,不用再引用指標定義語句的引用名前加const

指標常量定義"int* const pointer=&b"告訴編譯器,pointer是常量,不能作為左值進行操作,但是允許修改間接訪問值,即*pointer可以修改

常量指標常量VS常量引用常量

常量指標常量

指向常量的指標常量,可以定義一個指向常量的指標常量,它必須在定義時初始化。常量指標常量定義"const int* const pointer=&c"告訴編譯器,pointer*pointer都是常量,他們都不能作為左值進行操作。

而就不存在所謂的"常量引用常量",因為跟上面講的一樣引用變數就是引用常量。C++不區分變數的const引用和const變數的引用。程式決不能給引用本身重新賦值,使他指向另一個變數,因此引用總是const的。如果對引用應用關鍵字const,起作用就是使其目標稱為const變數。即沒有:Const double const& a=1;只有const double& a=1

總結

有一個規則可以很好的區分const是修飾指標,還是修飾指標指向的資料——畫一條垂直穿過指標宣告的星號(*),如果const出現線上的左邊,指標指向的資料為常量;如果const出現在右邊,指標本身為常量。而引用本身與天俱來就是常量,即不可以改變指向。

或者看離const最近的是什麼,例如char * const [指向字元的靜態指標],離const最近的是*,則是指標常量,不能更改指標;而const char * [指向靜態字元的指標],,離const最近的是char,則是代表字元不能改變,但是指標可以變,也就是說該指標可以指標其他的const char。

如下例子:

#include<iostream>
#include<string>
using namespace std;

int main(){
    char a[] = "hello";
    char aa[] = "haha";

    //const 修飾的是 *,則指標指向不可改變,但是其指向的內容可以更改
    char * const c = a;
    //c = aa; //error: assignment of read-only variable 'c'
    a[1] = 'a';

    // const 修飾的是char。表示指標指向可更改,但是指向的內容不能改變
    const char * d = a;
    d = aa;
    //d[1] = 'a'; //error: assignment of read-only variable 'c'
    cout<<c<<endl;
    cout<<d<<endl;
    return 0;
}

參考

C++:引用的簡單理解
C++中引用,指標,指標的引用,指標的指標
c++的*與&簡單總結


One more thing

更多關於人工智慧、Python、C++、計算機等知識,歡迎訪問我的個人部落格進行交流, 點這裡~~