C++傳值、傳引用
C++的函數參數傳遞方式,可以是傳值方式,也可以是傳引用方式。傳值的本質是:形參是實參的一份復制。傳引用的本質是:形參和實參是同一個東西。
傳值和傳引用,對大多數常見類型都是適用的(就我所知)。指針、數組,它們都是數據類型的一種,沒啥特殊的,因此指針作為函數參數傳遞時,也區分為傳值和傳引用兩種方式。
e.g.
void fun_1(int a); //int類型,傳值(復制產生新變量)
void fun_2(int& a); //int類型,傳引用(形參和實參是同一個東西)
void fun_3(int* arr); //指針類型,傳值(復制產生新變量)
void func_4(int*& arr); //指針類型,傳引用(形參和實參是同一個東西)
如果希望通過將參數傳遞到函數中,進而改變變量的值(比如變量是T a,T表示類型),則可以有這2種方式選擇:
- 傳a的引用:
void myfun(T& a) - 傳a的地址的值:
void myfun(T* a)
傳值方式
這是最簡單的方式。形參意思是被調用函數的參數/變量,實參意思是主調函數中放到括號中的參數/變量。傳值方式下,形參是實參的拷貝:重新建立了變量,變量取值和實參一樣。
寫一段測試代碼,並配合gdb查看:
test.cc
#include <iostream>
using namespace std;
void swap(int a, int b){
int temp;
temp = a;
a = b;
b = temp;
cout << a << " " << b << endl;
}
int main(){
int x = 1;
int y = 2;
swap(x, y);
cout << x << " " << y << endl;
return 0;
}
➜ hello-cpp git:(master) ✗ g++ -g test.cc
➜ hello-cpp git:(master) ✗ gdb a.out
(gdb) b main
Breakpoint 1 at 0x4008fa: file test.cc, line 13.
(gdb) r
Starting program: /home/chris/work/hello-cpp/a.out
Breakpoint 1, main () at test.cc:13
13 int x = 1;
(gdb) s
14 int y = 2;
(gdb) p &x
$1 = (int *) 0x7fffffffdc58
(gdb) p &y
$2 = (int *) 0x7fffffffdc5c
(gdb) s
15 swap(x, y);
(gdb) s
swap (a=1, b=2) at test.cc:6
6 temp = a;
(gdb) p &a
$3 = (int *) 0x7fffffffdc2c
(gdb) p &b
$4 = (int *) 0x7fffffffdc28
(gdb)
可以看到,實參x和y的值為1和2,形參a和b的值都是1和2;而x與a的地址、y與b的地址,並不相同,表明形參a和b是新建里的變量,也即實參是從形參復制了一份。這就是所謂的傳值。
傳指針?其實還是傳值!
test2.cc
#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;
}
return 0;
}
這次依然用gdb調試(不用gdb也可以,直接看運行結果):
➜ hello-cpp git:(master) ✗ g++ -g test2.cc
➜ hello-cpp git:(master) ✗ gdb a.out
(gdb) b main
Breakpoint 1 at 0x4009e0: file test2.cc, line 11.
(gdb) r
Starting program: /home/chris/work/hello-cpp/a.out
Breakpoint 1, main () at test2.cc:11
11 int *p = NULL;
(gdb) s
12 test(p);
(gdb) p p
$1 = (int *) 0x0
(gdb) p &p
$2 = (int **) 0x7fffffffdc58
(gdb) s
test (p=0x0) at test2.cc:4
4 void test(int *p){
(gdb) s
5 int a = 1;
(gdb) p p
$3 = (int *) 0x0
(gdb) p &p
$4 = (int **) 0x7fffffffdc18
(gdb)
可以看到,main()函數內和test()函數內,變量p的值都是0,也就是都是空指針;但是它們的地址是不同的。也就是說,形參p只是從形參p那里復制了一份值(空指針的取值),形參是新創建的變量。
直接運行程序的結果也表明了這一點:
➜ hello-cpp git:(master) ✗ ./a.out
0x7fff2a329e24 1
指針p為NULL
傳引用
傳值是C和C++都能用的方式。傳引用則是C++比C所不同的地方。傳引用,傳遞的是實參本身,而不是實參的一個拷貝,形參的修改就是實參的修改。相比於傳值,傳引用的好處是省去了復制,節約了空間和時間。假如不希望修改變量的值,那么請選擇傳值而不是傳引用。
test3.cc
#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);
return 0;
}
再次開gdb調試(依然是多此一舉的gdb...直接運行a.out看結果就可以):
➜ hello-cpp git:(master) ✗ g++ -g test3.cc
➜ hello-cpp git:(master) ✗ gdb a.out
(gdb) b main
Breakpoint 1 at 0x4009af: file test3.cc, line 8.
(gdb) r
Starting program: /home/chris/work/hello-cpp/a.out
Breakpoint 1, main () at test3.cc:8
8 int main(void){
(gdb) s
9 int a = 1;
(gdb) s
10 cout << &a << " " << a << endl;
(gdb) s
0x7fffffffdc44 1
11 test(a);
(gdb) s
test (a=@0x7fffffffdc44: 1) at test3.cc:5
5 cout << &a << " " << a << endl;
(gdb) s
0x7fffffffdc44 1
6 }
(gdb)
直接運行./a.out的結果:
➜ hello-cpp git:(master) ✗ ./a.out
0x7ffec97399e4 1
0x7ffec97399e4 1
顯然,形參a和實參a完全一樣:值相同,地址也相同。說明形參不是實參的拷貝,而是就是實參本身。
簡單實踐-實現swap()函數
swap()函數用來交換兩個數字。根據前面一節的分析和測試,可以知道,既可以用傳值的方式(也即傳指針)來實現,也可以用傳引用的方式來實現。
代碼如下:
myswap.cc
#include <iostream>
using namespace std;
void myswap_pass_by_reference(int& a, int &b){
int t = a;
a = b;
b = t;
}
void myswap_pass_by_pointer_value(int* a, int* b){
int t = *a;
*a = *b;
*b = t;
}
int main(){
int a=1, b=2;
cout << "originally" << endl;
cout << "a=" << a << ", b=" << b << endl;
myswap_pass_by_reference(a, b);
cout << "after myswap_pass_by_reference" << endl;
cout << "a=" << a << ", b=" << b << endl;
myswap_pass_by_pointer_value(&a, &b);
cout << "after myswap_pass_by_pointer_value" << endl;
cout << "a=" << a << ", b=" << b << endl;
return 0;
}
程序執行結果:
originally
a=1, b=2
after myswap_pass_by_reference
a=2, b=1
after myswap_pass_by_pointer_value
a=1, b=2
真的理解了嗎?
其實出問題最多的還是指針相關的東西。指針作為值傳遞是怎樣用的?指針作為引用傳遞又是怎樣用的?
首先要明確,“引用”類型變量的聲明方式:變量類型 & 變量名。
“指針”類型的聲明方式:基類型* 變量名。
所以,“指針的引用類型”應當這樣聲明:基類型*& 變量名。
這樣看下來,不要把指針類型看得那么神奇,而是把它看成一種數據類型,那么事情就簡單了:指針類型,也是有傳值、傳引用兩種函數傳參方式的。
指針的傳值
void myfun(int* a, int n)
指針的傳引用
void myfun(int*& arr, int n)
update
考慮這樣一個問題:寫一個函數,遍歷輸出一個一維數組的各個元素。
第一種方法,數組退化為指針,傳值。同時還需要另一個參數來指定數組長度:
void traverse_1d_array(int* arr, int n){
...
}
缺點是需要指定n的大小。以及,傳值會產生復制,如果大量執行這個函數會影響性能。
另一種方式,傳入參數是數組的引用。想到的寫法,需要事先知道數組長度:
void traverse_1d_array(int (&arr)[10]){
...
}
缺點是需要在函數聲明的時候就確定好數組的長度。這很受限。
還有一種方法。使用模板函數,來接受任意長度的數組:
template <size_t size>
void fun(int (&arr)[size]){
...
}
這種使用模板聲明數組長度的方式很方便,當調用函數時,編譯器從數組實參計算出數組長度。也就是說,不用手工指定數組長度,讓編譯器自己去判斷。這很方便啊。用這種方式,隨手寫一個2維數組的遍歷輸出函數:
template<size_t m, size_t n>
void traverse_array_2d(int (&arr)[m][n]){
for(int i=0; i<m; i++){
for(int j=0; j<n; j++){
cout << arr[i][j] << ",";
}
cout << endl;
}
}
總結一下
普通類型,以int a為例:
void myfun(int a) //傳值,產生復制
void myfun(int& a) //傳引用,不產生復制
void myfun(int* a) //傳地址,產生復制,本質上是一種傳值,這個值是地址
指針類型,以int* a為例:
void myfun(int* a) //傳值,產生復制
void myfun(int*& a) //傳引用,不產生復制
void myfun(int** a) //傳地址,產生復制,本質上是一種傳值,這個值是指針的地址
數組類型,以int a[10]為例:
void myfun(int a[], int n) //傳值,產生復制
void myfun(int* a, int n) //傳值,產生復制,傳遞的數組首地址
void myfun(int (&arr)[10]) //傳引用,不產生復制。需要硬編碼數組長度
template<size_t size> void myfun(int (&arr)[size]) //傳引用,不產生復制。不需要硬編碼數組長度
reference
http://www.cnblogs.com/dolphin0520/archive/2011/04/03/2004869.html
http://www.cnblogs.com/yjkai/archive/2011/04/17/2018647.html
