1. 程式人生 > >詳解C++指標和引用

詳解C++指標和引用

C++是在C語言的基礎上發展來的。C++除了有C語言的指標外,還增加一個新的概念——引用,初學者容易把引用和指標混淆一起,面試或者筆試經常被考到。

要弄清楚這兩個概念,先從變數說起。

 

一:變數

什麼是變數呢?變數(variable)的定義在電腦科學中到底是如何定義的?然後variable到底是在記憶體中如何儲存值的呢?那麼跟著上面的問題,我們來一一的解答。

首先最重要的,變數的定義,當你申明一個變數的時候,計算機會將指定的一塊記憶體空間和變數名進行繫結;這個定義很簡單,但其實很抽象,例如:int x = 5; 這是一句最簡單的變數賦值語句了, 我們常說“x等於5”,其實這種說法是錯誤的,x僅僅是變數的一個名字而已,它本身不等於任何值的。這條語句的正確翻譯應該是:“將5賦值於名字叫做x的記憶體空間”,其本質是將值5賦值到一塊記憶體空間,而這個記憶體空間名叫做x。切記:x只是簡單的一個別名而已,x不等於任何值。其圖示如下:

 變數在記憶體中的操作其實是需要經過2個步驟的:

1)找出與變數名相對應的記憶體地址。

2)根據找到的地址,取出該地址對應的記憶體空間裡面的值進行操作。

 

二:指標

首先介紹到底什麼是指標?指標變數和任何變數一樣,也有變數名,和這個變數名對應的記憶體空間,只是指標的特殊之處在於:指標變數相對應的記憶體空間儲存的值恰好是某個記憶體地址。這也是指標變數區別去其他變數的特徵之一。例如某個指標的定義如下

int x = 5;
int *ptr = &x;

ptr即是一個指正變數名。通過指標獲取這個指標指向的記憶體中的值稱為dereference,間接引用。

其相對於記憶體空間的表示如下:

 

使用指標的優點和必要性:

    指標能夠有效的表示資料結構;

    能動態分配記憶體,實現記憶體的自由管理;

    能較方便的使用字串;

    便捷高效地使用陣列

    指標直接與資料的儲存地址有關,比如:值傳遞不如地址傳遞高效,因為值傳遞先從實參的地址中取出值,再賦值給形參代入函式計算;而指標則把形參的地址直接指向實參地址,使用時直接取出資料,效率提高,特別在頻繁賦值等情況下(注意:形參的改變會影響實參的值!)

三:引用

引用是C++引入的新語言特性,是C++常用的一個重要內容之一。

引用(reference)在C++中也是經常被用到,尤其是在作為函式引數的時候,需要在函式內部修改更新函式外部的值的時候,可以說是引用場景非常豐富。正確、靈活地使用引用,可以使程式簡潔、高效。

我在工作中發現,許多人使用它僅僅是想當然,只是知道怎麼應用而已,而不去具體分析這個reference。

在某些微妙的場合,很容易出錯,究其原由,大多因為沒有搞清本源。

下面我就來簡單的分析一下這個reference。首先我們必須明確的一點就是:reference是一種特殊的pointer。從這可以看出reference在記憶體中的儲存結構應該跟上面的指標是一樣的,也是儲存的一塊記憶體的地址。例如reference的定義如下:

int x = 5;
int &y = x;

引用就是某一變數(目標)的一個別名,對引用的操作與對變數直接操作完全一樣。

  引用的宣告方法:型別識別符號 &引用名=目標變數名;

       上面的程式碼,定義了引用y,它是變數x的引用,別名,這樣子,目標變數有兩個名稱,即該目標原名稱和引用名,且不能再把該引用名作為其他變數名的別名。

四、引用和指標有什麼區別?

(1)指標:指標是一個變數,只不過這個變數儲存的是一個地址,指向記憶體的一個儲存單元;而引用跟原來的變數實質上是同一個東西,只不過是原變數的一個別名而已。如:

int a=1;int *p=&a;
int a=1;int &b=a;

上面定義了一個整形變數和一個指標變數p,該指標變數指向a的儲存單元,即p的值是a儲存單元的地址。

    而下面2句定義了一個整形變數a和這個整形a的引用b,事實上a和b是同一個東西,在記憶體佔有同一個儲存單元。

(2)  引用不可以為空,當被建立的時候,必須初始化,初始化後就不會再改變了;而指標可以是空值,可以在任何時候被初始化,指標的值在初始化後可以改變,即指向其它的儲存單元。

(3)可以有const指標,但是沒有const引用;

(4)指標可以有多級,但是引用只能是一級(int **p;合法 而 int &&a是不合法的)

(5)”sizeof引用”得到的是所指向的變數(物件)的大小,而”sizeof指標”得到的是指標本身的大小;

(6)指標和引用的自增(++)運算意義不一樣;

(7)如果返回動態記憶體分配的物件或者記憶體,必須使用指標,引用可能引起記憶體洩漏;

(8)從記憶體分配上看,程式為指標變數分配記憶體區域,而不為引用分配記憶體區域,因為引用宣告時必須初始化,從而指向一個已經存在的物件。引用不能指向空值。

        注:標準沒有規定引用要不要佔用記憶體,也沒有規定引用具體要怎麼實現,具體隨編譯器 http://bbs.csdn.net/topics/320095541

(9)從編譯上看,程式在編譯時分別將指標和引用新增到符號表上,符號表上記錄的是變數名及變數所對應地址。指標變數在符號表上對應的地址值為指標變數的地址值,而引用在符號表上對應的地址值為引用物件的地址值。符號表生成後就不會再改,因此指標可以改變指向的物件(指標變數中的值可以改),而引用物件不能改。這是使用指標不安全而使用引用安全的主要原因。從某種意義上來說引用可以被認為是不能改變的指標。

(10)不存在指向空值的引用這個事實,意味著使用引用的程式碼效率比使用指標的要高。因為在使用引用之前不需要測試它的合法性。相反,指標則應該總是被測試,防止其為空。

 

下面用通俗易懂的話來概述一下:

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

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

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

 

五、指標傳遞和引用傳遞

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

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

        而在引用傳遞過程中,被調函式的形式引數雖然也作為區域性變數在棧中開闢了記憶體空間,但是這時存放的是由主調函式放進來的實參變數的地址。被調函式對形參的任何操作都被處理成間接定址,即通過棧中存放的地址訪問主調函式中的實參變數。正因為如此,被調函式對形參做的任何操作都影響了主調函式中的實參變數。

        引用傳遞和指標傳遞是不同的,雖然它們都是在被調函式棧空間上的一個區域性變數,但是任何對於引用引數的處理都會通過一個間接定址的方式操作到主調函式中的相關變數。而對於指標傳遞的引數,如果改變被調函式中的指標地址,它將影響不到主調函式的相關變數。如果想通過指標引數傳遞來改變主調函式中的相關變數,那就得使用指向指標的指標,或者指標引用。

 

六、返回引用和返回指標

C++返回引用型別

    A& a(){ return *this;} 就生成了一個固定地址的指標,並把指標帶給你。

但A a() { return *this;}會生成一個臨時物件變數,並把這個臨時變數給你,這樣就多了一步操作。

當返回一個變數時,會產生拷貝。當返回一個引用時,不會發生拷貝,你可以將引用看作是一個變數的別名,就是其他的名字,引用和被引用的變數其實是一個東西,只是有了兩個名字而已。

問題的關鍵是,當你想要返回一個引用而不是一個拷貝時,你要確保這個引用的有效性,比如:

        int & fun() { int a; a=10; return a; }

這樣是不行的,因為a會在fun退出時被銷燬,這時返回的a的引用是無效的。

這種情況下,如果fun的返回型別不是int & 而是int就沒有問題了。

 

返回指標的話,誰呼叫該函式,誰負責接觸返回的指標。

 

全域性變數,區域性靜態變數,區域性動態分配變數 都可以作為函式返回值。 

區域性自動變數不行

函式內部等區域性變數,儲存在棧中的變數是不能作為返回值的,雖然可以讀取正確的值,但是這是一塊未分配的記憶體,當別的程序用到時就會出錯,這個指標相當於野指標。返回值可以是區域性動態分配的記憶體空間,這一部分分配在堆上,在主動釋放之前別的程序是無法使用的記憶體區域。

不管是指標還是引用都是如此。

 

 

七、特別之處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;

  1. double b=1;
  2. constdouble& a=b;
  3. b=2;//正確
  4. a=3;//出錯error: assignment of read-only reference `a'

 

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