c++中指標和引用的區別?

麥田裡的守望者_zhg發表於2020-12-01

1 引用基礎教程

我們知道,引數的傳遞本質上是一次賦值的過程,賦值就是對記憶體進行拷貝。所謂記憶體拷貝,是指將一塊記憶體上的資料複製到另一塊記憶體上。

對於像 char、bool、int、float 等基本型別的資料,它們佔用的記憶體往往只有幾個位元組,對它們進行記憶體拷貝非常快速。而陣列、結構體、物件是一系列資料的集合,資料的數量沒有限制,可能很少,也可能成千上萬,對它們進行頻繁的記憶體拷貝可能會消耗很多時間,拖慢程式的執行效率。

C/C++ 禁止在函式呼叫時直接傳遞陣列的內容,而是強制傳遞陣列指標,這點已在《C語言指標變數作為函式引數》中進行了講解。而對於結構體和物件沒有這種限制,呼叫函式時既可以傳遞指標,也可以直接傳遞內容;為了提高效率,我曾建議傳遞指標,這樣做在大部分情況下並沒有什麼不妥,讀者可以點選《C語言結構體指標》進行回顧。

但是在 C++ 中,我們有了一種比指標更加便捷的傳遞聚合型別資料的方式,那就是引用(Reference)

在 C/C++ 中,我們將 char、int、float 等由語言本身支援的型別稱為基本型別,將陣列、結構體、類(物件)等由基本型別組合而成的型別稱為聚合型別(在講解結構體時也曾使用複雜型別、構造型別這兩種說法)。

引用(Reference)是 C++ 相對於C語言的又一個擴充。引用可以看做是資料的一個別名,通過這個別名和原來的名字都能夠找到這份資料。引用類似於 Windows 中的快捷方式,一個可執行程式可以有多個快捷方式,通過這些快捷方式和可執行程式本身都能夠執行程式;引用還類似於人的綽號(筆名),使用綽號(筆名)和本名都能表示一個人。

引用的定義方式類似於指標,只是用&取代了*,語法格式為:

type &name = data;

type 是被引用的資料的型別,name 是引用的名稱,data 是被引用的資料。引用必須在定義的同時初始化,並且以後也要從一而終,不能再引用其它資料,這有點類似於常量(const 變數)

下面是一個演示引用的例項:

#include <iostream>
using namespace std;
int main() {
    int a = 99;
    int &r = a;
    cout << a << ", " << r << endl;
    cout << &a << ", " << &r << endl;
    return 0;
}

執行結果:
99, 99
0x28ff44, 0x28ff44

本例中,變數 r 就是變數 a 的引用,它們用來指代同一份資料;也可以說變數 r 是變數 a 的另一個名字。從輸出結果可以看出,a 和 r 的地址一樣,都是0x28ff44;或者說地址為0x28ff44的記憶體有兩個名字,a 和 r,想要訪問該記憶體上的資料時,使用哪個名字都行。

注意,引用在定義時需要新增&,在使用時不能新增&,使用時新增&表示取地址。如上面程式碼所示,第5行中的&表示引用,第 7 行中的&表示取地址。除了這兩種用法,&還可以表示位運算中的與運算。

由於引用 r 和原始變數 a 都是指向同一地址,所以通過引用也可以修改原始變數中所儲存的資料,請看下面的例子:

#include <iostream>
using namespace std;
int main() {
    int a = 99;
    int &r = a;
    r = 47;
    cout << a << ", " << r << endl;
    return 0;
}

執行結果:
47, 47

最終程式輸出兩個 47,可見原始變數 a 的值已經被引用變數 r 所修改。

如果讀者不希望通過引用來修改原始的資料,那麼可以在定義時新增 const 限制,形式為:

const type &name = value;

也可以是:

type const &name = value;

這種引用方式為常引用

1.1 C++引用作為函式引數

在定義或宣告函式時,我們可以將函式的形參指定為引用的形式,這樣在呼叫函式時就會將實參和形參繫結在一起,讓它們都指代同一份資料。如此一來,如果在函式體中修改了形參的資料,那麼實參的資料也會被修改,從而擁有“在函式內部影響函式外部資料”的效果。

至於實參和形參是如何繫結的,我們將在下節《C++引用在本質上是什麼,它和指標到底有什麼區別?》中講解,屆時我們會一針見血地闡明引用的本質。

一個能夠展現按引用傳參的優勢的例子就是交換兩個數的值,請看下面的程式碼:

#include <iostream>
using namespace std;
void swap1(int a, int b);
void swap2(int *p1, int *p2);
void swap3(int &r1, int &r2);
int main() {
    int num1, num2;
    cout << "Input two integers: ";
    cin >> num1 >> num2;
    swap1(num1, num2);
    cout << num1 << " " << num2 << endl;
    cout << "Input two integers: ";
    cin >> num1 >> num2;
    swap2(&num1, &num2);
    cout << num1 << " " << num2 << endl;
    cout << "Input two integers: ";
    cin >> num1 >> num2;
    swap3(num1, num2);
    cout << num1 << " " << num2 << endl;
    return 0;
}
//直接傳遞引數內容
void swap1(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}
//傳遞指標
void swap2(int *p1, int *p2) {
    int temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}
//按引用傳參
void swap3(int &r1, int &r2) {
    int temp = r1;
    r1 = r2;
    r2 = temp;
}

執行結果:
Input two integers: 12 34↙
12 34
Input two integers: 88 99↙
99 88
Input two integers: 100 200↙
200 100

本例演示了三種交換變數的值的方法:

  1. swap1() 直接傳遞引數的內容,不能達到交換兩個數的值的目的。對於 swap1() 來說,a、b 是形參,是作用範圍僅限於函式內部的區域性變數,它們有自己獨立的記憶體,和 num1、num2 指代的資料不一樣。呼叫函式時分別將 num1、num2 的值傳遞給 a、b,此後 num1、num2 和 a、b 再無任何關係,在 swap1() 內部修改 a、b 的值不會影響函式外部的 num1、num2,更不會改變 num1、num2 的值。

  2. swap2() 傳遞的是指標,能夠達到交換兩個數的值的目的。呼叫函式時,分別將 num1、num2 的指標傳遞給 p1、p2,此後 p1、p2 指向 a、b 所代表的資料,在函式內部可以通過指標間接地修改 a、b 的值。我們在《C語言指標變數作為函式引數》中也對比過第 1)、2) 中方式的區別。

  3. swap3() 是按引用傳遞,能夠達到交換兩個數的值的目的。呼叫函式時,分別將 r1、r2 繫結到 num1、num2 所指代的資料,此後 r1 和 num1、r2 和 num2 就都代表同一份資料了,通過 r1 修改資料後會影響 num1,通過 r2 修改資料後也會影響 num2。

從以上程式碼的編寫中可以發現,按引用傳參在使用形式上比指標更加直觀。在以後的 C++ 程式設計中,我鼓勵讀者大量使用引用,它一般可以代替指標(當然指標在C++中也不可或缺),C++ 標準庫也是這樣做的。

1.2 C++引用作為函式返回值

引用除了可以作為函式形參,還可以作為函式返回值,請看下面的例子:

#include <iostream>
using namespace std;
int &plus10(int &r) {
    r += 10;
    return r;
}
int main() {
    int num1 = 10;
    int num2 = plus10(num1);
    cout << num1 << " " << num2 << endl;
    return 0;
}

執行結果:
20 20

在將引用作為函式返回值時應該注意一個小問題,就是不能返回區域性資料(例如區域性變數、區域性物件、區域性陣列等)的引用,因為當函式呼叫完成後區域性資料就會被銷燬,有可能在下次使用時資料就不存在了,C++ 編譯器檢測到該行為時也會給出警告。

更改上面的例子,讓 plus10() 返回一個區域性資料的引用:

#include <iostream>
using namespace std;
int &plus10(int &r) {
    int m = r + 10;
    return m;  //返回區域性資料的引用
}
int main() {
    int num1 = 10;
    int num2 = plus10(num1);
    cout << num2 << endl;
    int &num3 = plus10(num1);
    int &num4 = plus10(num3);
    cout << num3 << " " << num4 << endl;
    return 0;
}

在 Visual Studio 下的執行結果:

20
-858993450 -858993450

GCC 下的執行結果:

20
30 30

在 C-Free 下的執行結果:

20
30 0

而我們期望的執行結果是:

20
20 30

plus10() 返回一個對區域性變數 m 的引用,這是導致執行結果非常怪異的根源,因為函式是在棧上執行的,並且執行結束後會放棄對所有區域性資料的管理權,後面的函式呼叫會覆蓋前面函式的區域性資料。本例中,第二次呼叫 plus10() 會覆蓋第一次呼叫 plus10() 所產生的區域性資料,第三次呼叫 plus10() 會覆蓋第二次呼叫 plus10() 所產生的區域性資料。

關於函式呼叫的內部實現,我已在《C語言記憶體精講》專題中講到。

2 C++中指標和引用的區別

指標和引用在C++中很常用,但是對於它們之間的區別很多初學者都不是太熟悉,下面來談談他們2者之間的區別和用法。

2.1 指標和引用的定義和性質區別:

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

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

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

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

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

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

(4)指標的值可以為空,但是引用的值不能為NULL,並且引用在定義的時候必須初始化;

(5)指標的值在初始化後可以改變,即指向其它的儲存單元,而引用在進行初始化後就不會再改變了。

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

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

2.2 指標和引用作為函式引數進行傳遞時的區別。

(1)指標作為引數進行傳遞:

#include<iostream>
using namespace std;

void swap(int *a,int *b)
{
  int temp=*a;
  *a=*b;
  *b=temp;
}

int main(void)
{
  int a=1,b=2;
  swap(&a,&b);
  cout<<a<<" "<<b<<endl;
  system("pause");
  return 0;
}

結果為2 1;

用指標傳遞引數,可以實現對實參進行改變的目的,是因為傳遞過來的是實參的地址,因此使用*a實際上是取儲存實參的記憶體單元裡的資料,即是對實參進行改變,因此可以達到目的。

再看一個程式;

#include<iostream>
using namespace std;

void test(int *p)
{
  int a=1;
  p=&a;
  cout<<p<<" "<<*p<<endl;
}

int main(void)
{
    int *p=NULL;
    test(p);
    if(p==NULL)
    cout<<"指標p為NULL"<<endl;
    system("pause");
    return 0;
}

執行結果為:

0x22ff44 1

指標p為NULL

大家可能會感到奇怪,怎麼回事,不是傳遞的是地址麼,怎麼p會是NULL?事實上,在main函式中宣告瞭一個指標p,並賦值為NULL,當呼叫test函式時,事實上傳遞的也是地址,只不過傳遞的是指標地址。也就是說將指標作為引數進行傳遞時,事實上也是值傳遞,只不過傳遞的是地址。當把指標作為引數進行傳遞時,也是將實參的一個拷貝傳遞給形參,即上面程式main函式中的p為何與test函式中使用的p不是同一個變數,儲存2個變數p的單元也不相同(只是2個p指向同一個儲存單元),那麼在test函式中對p進行修改,並不會影響到main函式中的p的值。

如果要想達到也同時修改的目的的話,就得使用引用了。

2.3 將引用作為函式的引數進行傳遞。

在講引用作為函式引數進行傳遞時,實質上傳遞的是實參本身,即傳遞進來的不是實參的一個拷貝,因此對形參的修改其實是對實參的修改,所以在用引用進行引數傳遞時,不僅節約時間,而且可以節約空間。

看下面這個程式:

#include<iostream>
using namespace std;

void test(int &a)
{
  cout<<&a<<" "<<a<<endl;
}

int main(void)
{
    int a=1;
    cout<<&a<<" "<<a<<endl;
    test(a);
    system("pause");
    return 0;
}

輸出結果為: 0x22ff44 1
0x22ff44 1

再看下這個程式:

這足以說明用引用進行引數傳遞時,事實上傳遞的是實參本身,而不是拷貝。

所以在上述要達到同時修改指標的目的的話,就得使用引用了。

#include<iostream>
using namespace std;

void test(int *&p)
{
  int a=1;
  p=&a;
  cout<<p<<" "<<*p<<endl;
}

int main(void)
{
    int *p=NULL;
    test(p);
    if(p!=NULL)
    cout<<"指標p不為NULL"<<endl;
    system("pause");
    return 0;
}

輸出結果為:0x22ff44 1

​ 指標p不為NULL

3 C++引用在本質上是什麼,它和指標到底有什麼區別?

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

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

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

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

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

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

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

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

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

★相同點:

●都是地址的概念;

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

★不同點:

●指標是一個實體,而引用僅是個別名;

●引用只能在定義時被初始化一次,之後不可變;指標可變;引用“從一而終”,指標可以“見異思遷”;

●引用沒有const,指標有const,const的指標不可變;

●引用不能為空,指標可以為空;

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

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

●引用是型別安全的,而指標不是 (引用比指標多了型別檢查

reference

http://www.ryxxff.com/20171.html
http://c.biancheng.net/view/vip_2252.html

相關文章