C++引用10分鐘入門教程

C++引用10分鐘入門教程

 
我們知道,參數的傳遞本質上是一次賦值的過程,賦值就是對內存進行拷貝。所謂內存拷貝,是指將一塊內存上的數據複製到另一塊內存上。

對於像 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 變量)。

下面是一個演示引用的實例:
  1. #include <iostream>
  2. using namespace std;
  3. int main() {
  4. int a = 99;
  5. int &r = a;
  6. cout << a << ", " << r << endl;
  7. cout << &a << ", " << &r << endl;
  8. return 0;
  9. }
運行結果:
99, 99
0x28ff44, 0x28ff44

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

注意,引用在定義時需要添加&,在使用時不能添加&,使用時添加&表示取地址。如上面代碼所示,第 6 行中的&表示引用,第 8 行中的&表示取地址。除了這兩種用法,&還可以表示位運算中的與運算。

由於引用 r 和原始變量 a 都是指向同一地址,所以通過引用也可以修改原始變量中所存儲的數據,請看下面的例子:
  1. #include <iostream>
  2. using namespace std;
  3. int main() {
  4. int a = 99;
  5. int &r = a;
  6. r = 47;
  7. cout << a << ", " << r << endl;
  8. return 0;
  9. }
運行結果:
47, 47

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

如果讀者不希望通過引用來修改原始的數據,那麼可以在定義時添加 const 限制,形式爲:

const type &name = value;

也可以是:

type const &name = value;

這種引用方式爲常引用

C++引用作爲函數參數

在定義或聲明函數時,我們可以將函數的形參指定爲引用的形式,這樣在調用函數時就會將實參和形參綁定在一起,讓它們都指代同一份數據。如此一來,如果在函數體中修改了形參的數據,那麼實參的數據也會被修改,從而擁有“在函數內部影響函數外部數據”的效果。

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

一個能夠展現按引用傳參的優勢的例子就是交換兩個數的值,請看下面的代碼:
  1. #include <iostream>
  2. using namespace std;
  3. void swap1(int a, int b);
  4. void swap2(int *p1, int *p2);
  5. void swap3(int &r1, int &r2);
  6. int main() {
  7. int num1, num2;
  8. cout << "Input two integers: ";
  9. cin >> num1 >> num2;
  10. swap1(num1, num2);
  11. cout << num1 << " " << num2 << endl;
  12. cout << "Input two integers: ";
  13. cin >> num1 >> num2;
  14. swap2(&num1, &num2);
  15. cout << num1 << " " << num2 << endl;
  16. cout << "Input two integers: ";
  17. cin >> num1 >> num2;
  18. swap3(num1, num2);
  19. cout << num1 << " " << num2 << endl;
  20. return 0;
  21. }
  22. //直接傳遞參數內容
  23. void swap1(int a, int b) {
  24. int temp = a;
  25. a = b;
  26. b = temp;
  27. }
  28. //傳遞指針
  29. void swap2(int *p1, int *p2) {
  30. int temp = *p1;
  31. *p1 = *p2;
  32. *p2 = temp;
  33. }
  34. //按引用傳參
  35. void swap3(int &r1, int &r2) {
  36. int temp = r1;
  37. r1 = r2;
  38. r2 = temp;
  39. }
運行結果:
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) 中方式的區別。

2) swap3() 是按引用傳遞,能夠達到交換兩個數的值的目的。調用函數時,分別將 r1、r2 綁定到 num1、num2 所指代的數據,此後 r1 和 num1、r2 和 num2 就都代表同一份數據了,通過 r1 修改數據後會影響 num1,通過 r2 修改數據後也會影響 num2。

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

C++引用作爲函數返回值

引用除了可以作爲函數形參,還可以作爲函數返回值,請看下面的例子:
  1. #include <iostream>
  2. using namespace std;
  3. int &plus10(int &r) {
  4. r += 10;
  5. return r;
  6. }
  7. int main() {
  8. int num1 = 10;
  9. int num2 = plus10(num1);
  10. cout << num1 << " " << num2 << endl;
  11. return 0;
  12. }
運行結果:
20 20

在將引用作爲函數返回值時應該注意一個小問題,就是不能返回局部數據(例如局部變量、局部對象、局部數組等)的引用,因爲當函數調用完成後局部數據就會被銷燬,有可能在下次使用時數據就不存在了,C++ 編譯器檢測到該行爲時也會給出警告。

更改上面的例子,讓 plus10() 返回一個局部數據的引用:
  1. #include <iostream>
  2. using namespace std;
  3. int &plus10(int &r) {
  4. int m = r + 10;
  5. return m; //返回局部數據的引用
  6. }
  7. int main() {
  8. int num1 = 10;
  9. int num2 = plus10(num1);
  10. cout << num2 << endl;
  11. int &num3 = plus10(num1);
  12. int &num4 = plus10(num3);
  13. cout << num3 << " " << num4 << endl;
  14. return 0;
  15. }
在 Visual Studio 下的運行結果:

20
-858993450 -858993450

在 GCC 下的運行結果:

20
30 30

在 C-Free 下的運行結果:

20
30 0

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

20
20 30

plus10() 返回一個對局部變量 m 的引用,這是導致運行結果非常怪異的根源,因爲函數是在棧上運行的,並且運行結束後會放棄對所有局部數據的管理權,後面的函數調用會覆蓋前面函數的局部數據。本例中,第二次調用 plus10() 會覆蓋第一次調用 plus10() 所產生的局部數據,第三次調用 plus10() 會覆蓋第二次調用 plus10() 所產生的局部數據。

關於函數調用的內部實現,我已在《C語言內存精講》專題中講到。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章