前言:在面向對象的程序設計語言中,我們經常聽見一些名詞,引用,地址,在函數傳遞參數的時候,我們又經常說值傳遞,引用傳遞,最容易讓人搞混淆的就是“引用”和“地址”這兩個概念了,對於C++和C#來說,引用一詞從他們所呈現的表象來看的確很類似,但是本質實際上是不一樣的,
C++:引用就是一個變量的別名;
C#:引用可以用指針去理解,雖然C#沒有指針,我們經常說某個變量所引用的數據,可以理解爲某個變量所指向的數據。
一、先從C語言的交換兩個變量說起
C語言中:函數傳參有傳值和傳址兩種方式
用swap函數舉例:
1.1 傳值方式(創建了臨時變量存放實參的值)
缺點:不能通過函數形參改變外部實參
優點:不能改變外部的實參
void swap(int left,int right)//此代碼不能完成兩數的交換
{
int tmp = left;
left = right;
right = tmp;
}
2.傳址方式(創建了臨時變量存放了實參的地址)
缺點:每次訪問實參都要解引用
優點:可以改變外部實參
void swap(int* left,int* right)
{
int tmp = *left;
*left = *right;
*right = tmp;
}
而C++中就引入了引用的概念,下面詳細介紹一下C++中的引用
上面的代碼就可以寫成這樣
此時代碼中的left和right就是實參的別名,通過交換left和right就能將傳過來的實參進行交換。
void swap(int& left,int& right)
{
int tmp = left;
left = right;
right = tmp;
}
總結:C++通過引用就可以達到C語言的指針作爲參數的效果。
二、引用的簡單概念
引用不是新定義一個變量,而是給已存在變量取了一個別名,編譯器不會爲引用變量開闢內存空間,它和它引用的變量共用同一塊內存空間。
定義引用類型的格式:
類型 & 引用變量名(對象名) = 引用實體;注意這裏的空格是可選的,即
- &符號與前後均可以有一個空格;如下:int & ra=a;
- &符號與類型挨着,如下:int& ra=a;
- &符號與引用名稱挨着,如下:int &ra=a;
到底怎麼理解引用只是一個變量的別名呢?其實在生活中別名很好理解,比如 張三 有一個別名叫做 三兒 。那麼這兩個名稱指的實際上就是同一個人,張三幹嘛,三兒就在幹嘛,兩者是一起的,爲了說明,我們看一個例子:
int main()
{
int a = 100;
int & ar = a;
cout << a << endl;
cout << ar << endl;
cout << &a << endl;
cout << &a << endl;
ar = 200; //改變引用變量
cout << a << endl;
cout << ar << endl;
cout << &a << endl;
cout << &a << endl;
getchar();
return 0;
}
/*
100
100
012FF77C
012FF77C
200
200
012FF77C
012FF77C
*/
2.1 總結引用的特點
- 引用變量的類型必須與它的實體類型一致(因爲取別名要符合引用實體的身份,如果類型不一致則會報錯)
- 引用變量使用必須要進行初始化(不然沒有實體都不知道給誰取別名)
- 一個變量可以有多個引用(就相當於一個變量有好幾個別名,這是可以的)
int a = 10; int& ra = a; int& rra = ra; ra,raa都是a的別名
- 引用一旦引用一個實體,再不能引用其他實體(同一個別名不能引用不同的人,否則就分不清誰是誰了)
- 引用不是指針,他就是一個變量,僅僅是一個別名;
總而言之:
引用本身也是一個變量,但是這個變量又僅僅是另外一個變量一個別名,它不佔用內存空間,它不是指針哦!不要混淆了,僅僅是一個別名,別名,別名,重要的事情說三遍。
三、引用的更多使用
3.1 常引用
變量可以使變量和常量,別名本質上也是變量,也可以是變量或者是常量,所以對應起來有四種情況,分別如下:
(1)變引用——變量
int a = 10; //可讀可寫
int& ra = a; //可讀可寫
(2)常引用——變量
int a = 10; //可讀可寫
int const& ra = a; //僅僅可讀,不可寫
ra=20; //編譯不通過,ra是常量
(3)變引用——常量
int const a = 100; //常量
int & ar = a; //變量,編譯沒辦法通過,因爲本尊都是常量,別名自然不能是變量
(4)常引用——常量
int const a = 100; //常量
int const & ar = a; //常量,自身和別名都是常量,沒有問題
3.2 指針引用
引用既然就是一個變量,那我同樣也可以給指針變量去一個別名啊,參見下面的
int main()
{
int a = 100;
int *p = &a;
int * &rp = p;
cout << a << endl;
cout << *p << endl;
cout << *rp << endl; //這裏爲什麼要將*放在前面,因爲p的類型是 int * 作爲一個整體哦!!
cout << p << endl;
cout << rp << endl;
getchar();
return 0;
}
/*
100
100
100
012FF84C
012FF84C
*/
我們發現這裏的指針變量p和它的引用(別名)rp是完全一樣的。但是由於引用的目的跟指針的目的是類似的,所以一般不需要對指針再起別名了。(參見兩數交換的函數)
總而言之一句話:
引用變量就是別名、別名、別名。
四、引用與函數的結合使用
4.1 引用變量作爲函數參數
把傳值和傳址的優點結合起來了,寫起來會比較方便,同時對形參修改了會體現到我們的外部實參上(因爲形參就是實參的別名),同時傳引用的效率比傳值的效率高,傳引用寫起來也比傳址方便。
void swap(int& left,int& right)
{
int tmp = left;
left = right;
right = tmp;
}
//函數調用,由於引用僅僅是一個別名,對於形參的操作會影響到實參
a=10;,需要特別注意
b=20;
swap(a,b);
由於引用變量作爲函數參數,對形參修改了會體現到我們的外部實參上(因爲形參就是實參的別名),這需要特別注意,但是如果我不希望改變外面的實參呢?
傳了引用之後,在函數內部進行操作就會把實參修改怎麼辦?
此時就採用const引用
void testfun(const int& a)
{
//a = 10;
//此時a就不能改,因爲a是一個常量的引用,不允許修改
}
總結:所以我們在構建函數的時候,還是要根據實際的需求,來決定到底是傳值、傳指針、還是傳引用。不能一概而論。
4.2 引用變量作爲函數的返回值
(1)不要返回局部變量的引用——一個嚴重的問題
先看一個代碼:
using namespace std;
int& test1()
{
int n = 5;
return n;
}
int main()
{
int i = test1();
cout << i << endl;
getchar();
return 0;
}
注意:
不同的編譯器對於返回局部變量的引用有所區別對待:
- 對於gcc和g++, 編譯報警告,運行的時候會出現錯誤
- 對於msvc:編譯報警告,warning C4172: 返回局部變量或臨時變量的地址: n,但是運行的時候不會出現錯誤,而是像正常的運行一樣,比如上面的代碼結果爲5.
爲什麼不要返回局部變量的引用呢?
因爲當該函數調用結束之後,該函數內部創建的局部變量出了作用域會被銷燬,爲這個函數開闢的棧幀也會被系統回收,在調用下一個函數之前會對這一部分棧空間裏的垃圾數據進行清理,因此你也會失去對這個空間的管控能力。函數調用結束之後,所有的局部變量都銷燬了,哪裏來的別名這一說法。
(2)返回全局變量的引用
int c; //定義全局變量
int & add(int a, int b)
{
c = a + b;
return c; //這裏的返回值就是一個int類型的變量,並不是一個引用類型啊,這是不是和int &不兼容?
}
不兼容問題並不會存在,由於引用變量並不會佔用內存,它實際上就是c,所以返回引用變量就是c。
當然我想下面這樣寫,更規範,也是沒問題的
int c; //定義全局變量
int &rc=c; //先給c把別名起好
int & add(int a, int b)
{
c = a + b;
return rc; //rc 就是c
}
怎麼調用呢?
如下就像普通函數調用即可:
int main()
{
int a = 100;
int b = 200;
int result = add(a, b); //就像普通函數調用即可
cout << result << endl;
getchar();
return 0;
}
當然也可以這樣做,這樣看起來更加規範一些:
int main()
{
int a = 100;
int b = 200;
int result;
int &rresult=result //先給返回值起一個別名
rresultadd(a, b);
cout << rresult << endl;
getchar();
return 0;
}