C++中的指針與引用

2021-02-22 程序猿

來自:吳秦 - 博客園

作者:吳秦

連結:http://www.cnblogs.com/skynet/archive/2010/09/22/1832911.html

寫在前面

指針和引用形式上很好區別,但是他們似乎有相同的功能,都能夠直接引用對象,對其進行直接的操作。但是什麼時候使用指針?什麼時候使用引用呢?這兩者很容易混淆,在此我詳細介紹一下指針和引用,力爭將最真實的一面展現給大家。如果我噴得不夠好,希望嘴下留情、手下留命,還請指點一二;如果感覺還不錯,請大家鼓掌。

1、指針和引用的定義

在深入介紹之前我們首先來看一下指針和引用的定義、指針和引用的區別,然後分別針對指針和引用展開討論,深入細節為何有這些差異。

●指針的權威定義:

In a declaration T D where D has the form

* cv-qualifier-seqopt D1

And the type of the identifier in the declaration T D1 is "derived-declarator-type-list T", then the type of the identifier of D is "derived-declarator-type-list cv-qualifier-seq pointer to T". The cv-qualifiers apply to the pointer and not to the object pointer to.

——摘自《ANSI C++ Standard》

註:可能有些讀者並不明白cv-qualifier-seq

CV-qualifiers(CV限定符)

CV-qualifiers有三種:const-qualifier(const限定符)、volatile-qualifier(volatile限定符)、以及const-volatile-qualifier(const-volatile限定符)。

const類對象的非靜態、非mutable、以及非引用數據成員是const-qualified;

volatile類對象的非靜態、非引用數據成員是volatile-qualified;

const-volatile類對象的非靜態、非引用數據成員是const-volatile-qualified。

當CV-qualifiers用於限定數組類型時,實際上是數組成員被該CV-qualifiers限定,而非該數組類型。

複合類型並不因其成員被CV-qualifier限定而被該CV-qualifier限定,也就是說,即使複合類型的成員有CV-qualifier限定,該複合類型也不是CV-qualified對象。

引用的權威定義:

In a declaration T D where D has the form

& D1

And the type of the identifier in the declaration T D1 is "derived-declarator-type-list T", then the type of the identifier of D is "derived-declarator-type-list cv-qualifier-seq reference to T". Cv-qualified references are ill-formed except when the cv-qualifiers are introduced through the use of a typedef or a template type argument, in which case the cv-qualifiers are ignored.

——摘自《ANSI C++ Standard》

上面這些定義初看有些難懂,如果是這樣的話,那說明你對C++還不夠熟悉,你還有很長的路要走。下面用通俗易懂的話來概述一下:

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

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

2、指針和引用的區別

首先,引用不可以為空,但指針可以為空。前面也說過了引用是對象的別名,引用為空——對象都不存在,怎麼可能有別名!故定義一個引用的時候,必須初始化。因此如果你有一個變量是用於指向另一個對象,但是它可能為空,這時你應該使用指針;如果變量總是指向一個對象,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的指向,它仍然指向的是i,而不是j。理所當然,這時對ref進行++操作不會影響到j。而這些換做是指針的話,情況大不相同,請自行實驗。輸出結果如下:

再次,引用的大小是所指向的變量的大小,因為引用只是一個別名而已;指針是指針本身的大小,4個字節。見下圖所示:

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

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

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

3、特別之處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出現在右邊,指針本身為常量。而引用本身與天俱來就是常量,即不可以改變指向。

4、指針和引用的實現

我們利用下面一段簡單的代碼來深入分析指針和引用:

#include<iostream>

using namespace std;

int main(int argc, char** argv)

{

int i=1;

int& ref=i;

int x=ref;

cout<<"x is "<<x<<endl;

ref=2;

int* p=&i;

cout<<"ref = "<<ref<<", i = "<<i<<endl;

}

上面的代碼用g++ test.c編譯之後,然後反彙編objdump -d a.out,得到main函數的一段彙編代碼如下:

08048714 <main>:

8048714: 55    push %ebp

8048715: 89 e5   mov %esp,%ebp

8048717: 83 e4 f0 and $0xfffffff0,%esp//為main函數的參數argc、argv保留位置

804871a: 56 push %esi

804871b: 53 push %ebx

804871c: 83 ec 28 sub $0x28,%esp

804871f: c7 44 24 1c 01 00 00 movl $0x1,0x1c(%esp) //將0x1存到esp寄存器中,即int i=1

8048726: 00

8048727: 8d 44 24 1c lea 0x1c(%esp),%eax// esp寄存器裡的變量i的地址傳給eax

804872b: 89 44 24 18 mov %eax,0x18(%esp)//將寄存器eax中的內容(i的地址)傳給寄存器中的變量ref,即int& ref=i

804872f: 8b 44 24 18 mov 0x18(%esp),%eax//將寄存器esp中的ref傳給eax,即i的地址

8048733: 8b 00 mov (%eax),%eax//以寄存器eax中的值作為地址,取出值給eax 8048735: 89 44 24 14 mov %eax,0x14(%esp) //將寄存器eax中的值傳給寄存器esp中的x,即x=ref

8048739: c7 44 24 04 00 89 04 movl $0x8048900,0x4(%esp)

8048740: 08

8048741: c7 04 24 40 a0 04 08 movl $0x804a040,(%esp)

8048748: e8 cb fe ff ff call 8048618 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>

804874d: 8b 54 24 14 mov 0x14(%esp),%edx

8048751: 89 54 24 04 mov %edx,0x4(%esp)

8048755: 89 04 24 mov %eax,(%esp)

8048758: e8 5b fe ff ff call 80485b8 <_ZNSolsEi@plt>

804875d: c7 44 24 04 38 86 04 movl $0x8048638,0x4(%esp)

8048764: 08

8048765: 89 04 24 mov %eax,(%esp)

8048768: e8 bb fe ff ff call 8048628 <_ZNSolsEPFRSoS_E@plt>//從8048739~8048768這些行就是執行"cout<<"x is "<<x<<endl;"

804876d: 8b 44 24 18 mov 0x18(%esp),%eax//將寄存器esp中的ref傳到eax中

8048771: c7 00 02 00 00 00 movl $0x2,(%eax) //將0x2存到eax寄存器中

8048777: 8d 44 24 1c lea 0x1c(%esp),%eax// esp寄存器裡的變量i的地址傳給eax

804877b: 89 44 24 10 mov %eax,0x10(%esp) //將寄存器eax中的內容(即i的地址)傳到寄存器esp中的p

804877f: 8b 5c 24 1c mov 0x1c(%esp),%ebx

8048783: 8b 44 24 18 mov 0x18(%esp),%eax

8048787: 8b 30 mov (%eax),%esi

8048789: c7 44 24 04 06 89 04 movl $0x8048906,0x4(%esp)

8048790: 08

8048791: c7 04 24 40 a0 04 08 movl $0x804a040,(%esp)

8048798: e8 7b fe ff ff call 8048618 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>

804879d: 89 74 24 04 mov %esi,0x4(%esp)

80487a1: 89 04 24 mov %eax,(%esp)

80487a4: e8 0f fe ff ff call 80485b8 <_ZNSolsEi@plt>

80487a9: c7 44 24 04 0d 89 04 movl $0x804890d,0x4(%esp)

80487b0: 08

80487b1: 89 04 24 mov %eax,(%esp)

80487b4: e8 5f fe ff ff call 8048618 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>

80487b9: 89 5c 24 04 mov %ebx,0x4(%esp)

80487bd: 89 04 24 mov %eax,(%esp)

80487c0: e8 f3 fd ff ff call 80485b8 <_ZNSolsEi@plt>

80487c5: c7 44 24 04 38 86 04 movl $0x8048638,0x4(%esp)

80487cc: 08

80487cd: 89 04 24 mov %eax,(%esp)

80487d0: e8 53 fe ff ff call 8048628 <_ZNSolsEPFRSoS_E@plt>//這些行就是執行"cout<<"ref = "<<ref<<", i = "<<i<<endl;"

80487d5: b8 00 00 00 00 mov $0x0,%eax

80487da: 83 c4 28 add $0x28,%esp

80487dd: 5b pop %ebx

80487de: 5e pop %esi

80487df: 89 ec mov %ebp,%esp

80487e1: 5d pop %ebp

80487e2: c3 ret

從彙編代碼可以看出實際上指針和引用在編譯器中的實現是一樣的:

引用int& ref=i;

8048727: 8d 44 24 1c lea 0x1c(%esp),%eax// esp寄存器裡的變量i的地址傳給eax

804872b: 89 44 24 18 mov %eax,0x18(%esp)//將寄存器eax中的內容(i的地址)傳給寄存器中的變量ref,即int& ref=i

指針int* p=&i;

8048777: 8d 44 24 1c lea 0x1c(%esp),%eax// esp寄存器裡的變量i的地址傳給eax

804877b: 89 44 24 10 mov %eax,0x10(%esp) //將寄存器eax中的內容(即i的地址)傳到寄存器esp中的p

雖然指針和引用最終在編譯中的實現是一樣的,但是引用的形式大大方便了使用也更安全。有人說:"引用只是一個別名,不會佔內存空間?"通過這個事實我們可以揭穿這個謊言!實際上引用也是佔內存空間的。

5、指針傳遞和引用傳遞

為了更好的理解指針和引用,我們下面來介紹一下指針傳遞和引用傳遞。當指針和引用作為函數的函數是如何傳值的呢?(下面這一段引用了C++中引用傳遞與指針傳遞區別(進一步整理))

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

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

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

參考文獻

[1]The C++ Programming Language(Special Edition),Bjarne Stroustrup

[2] ANSI C++ Standard

[3] 彙編語言

來自:吳秦 - 博客園

作者:吳秦

連結:http://www.cnblogs.com/skynet/archive/2010/09/22/1832911.html

網際網路創業的時代,創業更具人文氣息。聚最經典的IT創業技巧,最頂尖的商界精英交流平臺。

●本文編號876,以後想閱讀這篇文章直接輸入876即可。

●本文分類「C++」,搜索分類名可以獲得相關文章。

●輸入m可以獲取到全部文章目錄

●輸入r可以獲取到熱門文章推薦

●輸入f可以獲取到全部分類名稱

相關焦點

  • C++的轉換手段並與explicit關鍵詞配合使用
    本文轉載自【微信公眾號:羽林君,ID:Conscience_Remains】經微信公眾號授權轉載,如需轉載與原文作者聯繫前言C中我們會進行各種類型的強制轉化,而在C中我們經常可以看到這種轉換memset(OTA_FLAG_ADDRESS
  • C/C++中常用的編程關鍵字
    常量指針被轉化成非常量指針,並且仍然指向原來的對象;常量引用被轉換成非常量引用,並且仍然指向原來的對象;常量對象被轉換成非常量對象。11. continuecontinue(繼續)關鍵字用於循環結構。dynamic_cast 提供了兩種轉換方式,把基類指針轉換成派生類指針,或者把指向基類的左值轉換成派生類的引用。17. elseelse 緊跟在 if 後面,用於對 if 不成立的情況的選擇。
  • 入門C語言中的數組,字符串常量與指針
    MAX_SIZE + 1];C數組開始於元素0,所以像陣列定義 int a[3];將創建三個int元件,可尋址如a[0],a[1],和a[2]請注意,即使定義很難說a[3],也沒有名為a[3]與其他變量一樣,全局和靜態數組元素默認情況下初始化為0,並且自動數組元素填充有垃圾值可以使用大括號中的一個或多個值來初始化數組
  • C++中的static
    C++的static有兩種用法:面向過程程序設計中的static和面向對象程序設計中的static。前者應用於普通變量和函數,不涉及類;後者主要說明static在類中的作用。
  • void 型指針的高階用法,你掌握了嗎?
    在C/C++中,在任意時刻都可以使用其它類型指針來代替void指針,或者用void指針來代替其他類型指針。由這些特性就可以衍生出很多比較有用的技巧。指針的本質,是其值為一個地址,那麼延伸一下:當使用關鍵字void聲明指針變量時,它將成為通用指針變量。任何數據類型(char,int,float等)的任何變量的地址都可以賦值給void指針變量。對指針變量的解引用,使用間接運算符*達到目的。
  • 《逆襲進大廠》第二彈之C++進階篇59問59答(超硬核乾貨)
    但是在引用或指針傳遞函數調用中,因為傳進去的是一個引用或指針,這樣函數內部可以改變引用或指針所指向的變量,這時const 才是實實在在地保護了實參所指向的變量。因為在編譯階段編譯器對調用函數的選擇是根據實參進行的,所以,只有引用傳遞和指針傳遞可以用是否加const來重載。一個擁有頂層const的形參無法和另一個沒有頂層const的形參區分開來。
  • C++ Primer Plus (第6版) 中英文版 高清PDF電子書
    string類作為成員嗎4.4.4 其他結構屬性4.4.5 結構數組4.4.6 結構中的位欄位4.5 共用體4.6 枚舉4.6.1 設置枚舉量的值4.6.2 枚舉的取值範圍4.7 指針和自由存儲空間4.7.1 聲明和初始化指針4.7.2 指針的危險4.7.3 指針和數字4.7.4 使用new來分配內存
  • (正經版)面試官:切片作為函數參數是傳值還是傳引用?
    ❞切片傳參的幻覺 - 傳引用golang中函數的參數為切片時是傳引用還是傳值?結合圖來看相信會更清晰一點:傳引用(引用傳遞)是指在調用函數時將實際參數的地址傳遞到函數中,在函數中對參數所進行的修改,將影響實際參數。
  • 如何編寫 C++ 遊戲引擎
    偶爾要抵制一下 DRY 原則舉個例子,我的引擎包含了幾個「智能指針」模板類,與 std :: shared_ptr 類似。每一個指針作為一個原始指針的包裝,有助於防止內存洩漏。<> 是用於具有單個所有者的動態分配的對象。Reference<> 使用引用計數來允許一個對象擁有多個所有者。
  • C/C++編程筆記:C 語言編程面試常見問題,大廠面試必備
    什麼時候應該在C程序中使用指針? 1.獲取變量的地址 2.為了在C中實現引用傳遞:指針允許不同的函數共享和修改其局部變量。 3.通過大型結構,以便避免完整複製結構。 4.實現「連結」數據結構,例如連結列表和二叉樹。
  • 寫算法,用 C++ 還是用 Java ,差別大嗎?
    C++ 用字面量符號 l 或 L 表示 long,用 UL 表示 unsigned long。字符串很多 C++ 程式設計師喜歡的用 char* 或 char 類型的數組存儲字符串,這其實是 C 語言用戶帶過來的習慣,我給出的 C++ 算法實現對字符串一般都用 std::string,對應 Java 的 String。
  • C語言的指針,簡介
    指針,是一個表示變量或函數的地址的無符號整數。指針的字節數,與CPU的位數有關,32位機是4位元組,64位機是8位元組。與高級語言的long類型的大小是一致的。所以在java的JNI庫裡,如果需要把native層的C結構體的指針保存到java層,一般是定義一個long類型的變量。指針指向的變量,可以是普通變量、數組變量、結構體變量,以及數組元素、結構體的成員變量,還可以是指針變量。指針還可以指向函數,叫做函數指針。
  • 導航數學提升 建哥指針數學指針秘寶教你理清解題路線
    同樣的問題也在數學的學習過程中,與各式各樣的知識、公式和題型相比,城市交通的複雜完全知識小巫見大巫,很多同學們為找到答案所付出的辛苦遠比一段車程、一次繞遠大得多,而且路線錯了還有更多到達目的地的辦法,而數學答案可能硬著頭皮也沒辦法找到。
  • JO式系統圖標&滑鼠指針製作
    Jo式滑鼠指針1第一款是迪亞波羅的指針Jo式滑鼠指針2第三款空條承太郎指針Jo式滑鼠指針3第五款是老師岸邊露伴的指針Jo式滑鼠指針4最後作者製作的是我喬魯諾歐巴馬喬巴拿有一個夢想指針.ani 結尾的是動態的,.cur結尾是靜態
  • 「C語言更新」C語言中如何來定義一個指針,並且對其進行初始化
    文/Edward上一節中,我們利用了一個小知識來介紹了一下指針,在上面這個例子中,小明和小麗手中的這個杯子就好比我們C語言中的變量,它確實是實實在在存放一些有具體意義的數據。函數的參數傳遞指針從一開始被設計出來的時候,其目的不是用來存儲某一個具體的常量或變量,它的目的只有一個,就是存放相應變量的地址,即這個變量的內存地址。我們定義完成一個變量之後,就可以將這個指針指向這個變量了。所謂指向某個變量,說直白點就是將這個變量的地址存入指針中。
  • 你根本不懂什麼是空指針
    空指針,號稱天下最強刺客。他原本不叫這個名字,空指針原本複姓異常,空指針只不過是他的武器,但他殺戮過多,漸漸地人們只記住了空指針這三個字。我打聽了很久,原來空指針是異常組織的三代嫡傳,異常組織是這個世界上最恐怖的殺手組織,空指針就是異常現在最出色的刺客。
  • 手錶的夜光指針材質是什麼?有輻射嗎?
    這款上海牌全自動機械夜光金鑽表6008,這款手錶採用了經典三大針設計,指針類型屬於棒型針,棒型指針棒形指針是用這種指針的形狀來命名的。英語中的指示針(index watch hand)也應該算到是棒形針裡。這種指針很直接,也很簡潔。
  • 指針變奏曲:氣勢磅礡的腕錶,有你喜歡的一款嗎?
    太妃指針(Dauphine Hands) 名稱的由來,與法國皇室有段淵源。1349 年法王菲利普六世為了要併入當時屬於勃艮第王國一部分的多菲內(Dauphiné),答應負責管理的統治者阿爾邦伯爵的條件,也就是要將法國王儲命名為Dauphin;加上阿爾邦伯爵家徽中有一隻海豚(Dauphin也是法文海豚的意思),因此太妃指針也被稱為海豚針,仔細看還真的有點神似。