引用也是c++的初學者比較容易迷惑的概念,它幾乎擁有指針所有的功能,但是語法更加簡單,本章我們就來學習引用,并且分清它與指針的區別.
那么什么是引用呢?
簡單概述:引用就是別名
例:
int main(){
int num;
//注意:別名mum前面的符號&不是取址運算符,而是引用運算符,雖然它們符號相同,但是功能卻不一樣.
int &mum=num;
// mum是num的別名,這兩個變量是一個變量,只不過名字不同而已,這就好像李四有個外號叫李大嘴,大家稱呼李四指的是李四這個人,稱呼李大嘴也是指的李四
//這個人,李四和李大嘴都是一個人,只是名字出現了不同.
}
引用的地址
網站建設哪家好,找成都創新互聯!專注于網頁設計、網站建設、微信開發、小程序定制開發、集團企業網站建設等服務項目。為回饋新老客戶創新互聯還提供了高碑店免費建站歡迎大家使用!int a;
int &ra=a;
按照棧堆內存的方式就可以很容易理解 a ra在棧內存 而他們的實例是在堆內存,他們引用是相同的。
引用就是別名常量
當給一個變量定義了一個別名,那么該別名就永遠屬于這個變量,當有別的變量對它進行賦值時,雖然它的內容會改變,但是它的內存地址值不會發生變化,同時這個別名所對應的變量的
值也會發生改變.
由于這種特性,所以我們可以將引用看成是一個別名常量,它的內存空間無法改變,能改變的只是它所引用的值。
引用對象
我們也可以定義一個對象的別名,例:
Human Mike;//定義一個Human對象Mike
Human &rMike=Mike;//定義一個Mike的別名 rMike;
注意:定義一個引用的時候,一定要對該引用進行初始化.
以下例子是錯誤的:
int a;
int &ra;
ra=a;
這樣是錯誤的.引用就象常量,只能對其初始化,不能賦值。
空引用
我們知道指針進行刪除操作后,需要將它們賦值設為空,引用卻不需要這么做,這是因為引用是原來對象的別名,加入該對象存放在棧中,那么在對象超出作用域時別名會和對象一起消失.
加入該對象存放在堆中,由于堆中內存空間必須使用指針來訪問,因此用不著別名,即使在定義一個該指針的別名,那么將指針刪除并賦空之后,該指針的別名中的地址也相應賦空了。
按值傳遞
什么是按值傳遞呢?
例如:
void swap(int a,int b){
int c;
cout<<"swap函數中,交換前,a:"<<a<<"b:"<<b<<endl;
c=a;
a=b;
b=c;
cout<<"swap函數中,交換后,a:"<<a<<"b:"<<b<<endl;
}
int main(){
int a=3,b=4;
cout<<"在主程序中,交換前,a:"<<a<<"b:"<<b<<endl;
swap(a,b);
cout<<"在主程序中,交換后,a:"<<a<<"b:"<<b<<endl;
return 0;
}
通過上面事例可以發現,主程序中(main函數)的a b 兩個值在交換的前后并沒有發生改變,而swap函數中的ab在交換前后發生了改變
這到底是為什么呢?加入說swap函數沒有交換主程序中的a和b,那么它交換的到底是誰的值呢?
這個問題看起來復雜,其實很簡單,swap函數交換的是main函數中a和b的“副本”的值,也就是說在main函數中定義的a和b的備份的值,swap函數交換的是main函數中的a和b的副本,
而不是a和b本身。
那么為什么swap函數不直接交換a和b本身,卻去交換它們的副本的值呢?
這是因為當我們直接將a和b傳遞給swap函數時,這樣的傳遞方式是 --按值傳遞---.
假如將a和b按值傳遞給swap函數,那么編譯器會自動在棧中創建a和b的拷貝,然后將a和b的拷貝傳遞給swap函數.在swap函數中對a和b的拷貝進行交換.因此我們看到的輸出語句,a和
b確實進行了交換,只不過交換的是a和b的副本。
由于交換的是a和b的副本,并不是a和b本身,所以在swap函數結束后,輸出的值顯示main函數中的a和b并沒有改變.
按址傳遞
什么是按址傳遞?
從字面上理解就是按地址的方式傳遞.
例:
void swap(int *a,int *b){//這里參數要改為指針
int c;
cout<<"swap函數中,交換前,a:"<<*a<<"b:"<<*b<<endl;
c=*a;
*a=*b;
*b=c;
cout<<"swap函數中,交換后,a:"<<*a<<"b:"<<*b<<endl;
}
int main(){
int a=3,b=4;
cout<<"在主程序中,交換前,a:"<<a<<"b:"<<b<<endl;
swap(&a,&b);//這里加上取地址符。
cout<<"在主程序中,交換后,a:"<<a<<"b:"<<b<<endl;
return 0;
}
根據輸出就可以看到,主函數中的a和b也改變了.
按別名傳遞
把指針作為函數的接收參數雖然能夠正常使用,但是它卻不易閱讀,而且很難使用.
所以我們可以把接收指針參數改為接收兩個別名,由于別名改變的是內存地址中的值,所以也可以完成改變main函數中a b值的需求
void swap(int &a,int &b){//這里參數要改為指針
int c;
cout<<"swap函數中,交換前,a:"<<a<<"b:"<<b<<endl;
c=a;
a=b;
b=c;
cout<<"swap函數中,交換后,a:"<<a<<"b:"<<b<<endl;
}
int main(){
int a=3,b=4;
cout<<"在主程序中,交換前,a:"<<a<<"b:"<<b<<endl;
swap(a,b);//這里加上取地址符。
cout<<"在主程序中,交換后,a:"<<a<<"b:"<<b<<endl;
return 0;
}
利用指針返回多值
我們知道函數只能返回一個值,那么假如有的時候我們需要函數返回多個值時該怎么辦?
指針或者引用可以幫助我們解決這個問題,我們使用別名或者指針的方式傳遞給函數一個以上的變量,在函數體中將需要返回的值賦給這些變量,由于使用引用或者指針傳遞變量允許函數
改變原來的變量.因此這些在函數體中被修改的變量均可以看做是已經被該函數返回的值。
下例:用指針來演示一下返回三個值的操作。
int func(int a,int *b,int *c);//聲明
int main(){
int a=1,b=2,c=3;
cout<<"主程序,調用func函數前..
";
cout<<"a:"<<a<<endl<<"b:"<<b<<endl<<"c:"<<c<<endl;
func(a,&b,&c);
cout<<"主程序,調用func函數前..
";
cout<<"a:"<<a<<endl<<"b:"<<b<<endl<<"c:"<<c<<endl;
return 0;
}
int func(int a,int *b,int *c){
cout<<"func函數中,計算前...
";
cout<<"a:"<<a<<endl<<"b:"<<*b<<endl<<"c:"<<*c<<endl;
a=a+1;
*b=(*b)*(*b);
*c=(*c)*(*c);
cout<<"func函數中,計算后...
";
cout<<"a:"<<a<<endl<<"b:"<<*b<<endl<<"c:"<<*c<<endl;
return a;
}
寫完以后我發現好坑爹啊,這里的返回3個值的概念,居然是改變3個值?好坑...
這樣的方法的好處是我們可以實現匯報執行程序時的非法操作信息,我們也可以把a作為返回的判斷值,把*b和*c作為運算的返回值。
用接下來的代碼進行演示:
int func(int a,int *b,int *c);//聲明
int main(){
int a,b,c;
int check;
cout<<"請輸入要進行運算的數字,";
cout<<"您輸入的數字將作為圓的半徑和正方形的邊長,";
cin>>a;
check=func(a,&b,&c);
if(check){
cout<<"輸入的數字超過計算范圍!
";
}else{
cout<<"圓的面積為:"<<b<<endl;
cout<<"正方形的面積為:"<<c<<endl;
}
return 0;
}
int func(int a,int *b,int *c){
if(a>20000){
a=1;
}else{
*b=a*a*3.14;
*c=a*a;
}
}
用引用來返回多值
其實就是改變多值了,可看可不看,用引用重寫上面的代碼,
int func(int a,int &b,int &c);//聲明
int main(){
int a,b,c;
int check;
cout<<"請輸入要進行運算的數字,";
cout<<"您輸入的數字將作為圓的半徑和正方形的邊長,";
cin>>a;
check=func(a,b,c);
if(check){
cout<<"輸入的數字超過計算范圍!
";
}else{
cout<<"圓的面積為:"<<b<<endl;
cout<<"正方形的面積為:"<<c<<endl;
}
return 0;
}
int func(int a,int &b,int &c){
if(a>20000){
a=1;
}else{
b=a*a*3.14;
c=a*a;
}
}
按值傳遞對象
從前面幾節我們了解了按址傳遞和按值傳遞的區別,按址傳遞可以修改原始變量的值,按值傳遞由于是傳遞的原始變量的副本,因此它不會修改原始變量的值.
假如僅僅是傳遞變量的話,采用指針或者引用這種按址傳遞的優勢不是很明顯,但是假如是傳遞較大的對象的話,這種優勢是相當的明顯的.
這是因為,按值傳遞在向函數傳遞一個對象時,會象傳遞變量那樣建立一個該對象的拷貝,而從函數返回一個對象時,也要建立這個被返回的對象的一個拷貝.
這樣加入該對象的數據非常多時,這種拷貝帶來的內存開銷是相當可觀的.比如說該對象擁有0.000多個double型成員變量,每個double型變量占據8個字節,1000個就占據8000
字節,每次通過值傳遞的方式給函數傳遞該對象,都要在棧中復制該對象,占用8000個字節的棧內空間,而返回該對象,又要在棧中復制一次,這樣就又要占用8000個字節的內存空間.我
們知道棧的內存只有2M大小,8000個字節占用8K,那么僅僅傳遞該對象就占用了棧內16k字節的空間。并且別的對象想要訪問該對象的8000個數據成員的時候,也要同樣采取復制的方式
,那么系統的開銷將無法估算了。
然后,按值傳遞所付出的開銷遠不止如此,由于在傳遞過程中需要復制對象,因此會默認調用復制構造函數,該函數的作用就是創建某個對象的臨時副本.關于復制構造函數,將會在深
入函數中做進一步講解,這里你只需要知道,這要在棧中創建臨時拷貝都會自動調用復制構造函數即可。
而當函數返回時,傳遞該對象時創建的該對象的副本會被刪除,這時候又會自動調用該對象的析構函數來釋放內存。假設返回的仍然是該對象,并且仍舊采用按值傳遞的方式,那么就
又會調用復制構造函數建立一個該對象的臨時副本,當改值被成功返回給調用程序后,然后再調用該對象的析構函數刪除臨時拷貝并釋放內存。
我們看到復制構造函數和析構函數一連被執行了兩次,這無疑會增加系統的開銷,我們用一個實例來演示一下按值傳遞一個對象的復制與刪除過程。
class A{
public :
A(){cout<<"執行構造函數創建一個對象
";}
A(A&){cout<<"執行復制構造函數創建該對象的副本
";}
~A(){cout<<"執行析構函數刪除該對象
";}
};
A func(A one){
return one;//由于返回也是按值傳遞 所以又調用了類A的復制構造函數
}
int main(){
A a;//創建一個對象會自動調用構造函數
func(a);//將對象a按值傳遞給func函數中,然后對調用類A的復制構造函數
return 0;
}
通過輸出可以發現,將一個對象按值傳遞給一個函數,會調用兩次復制構造函數和兩次析構函數,這樣系統的開銷很很大的
按址傳遞對象
為了解決上面開銷大的問題,我們可以如下寫法:
A func(A *one){
return *one;
}
int main(){
A a;
func(&a);
return 0;
}
這樣就可以減少一次復制構造函數的執行,也就對應的減少一次調用析構函數來刪除復制構造函數創建對象.
但是復制構造函數依然執行了一次,那么我們如何避免這次復制構造函數的執行呢?
上面函數func中 返回return *one 由于返回的是對象,而不是地址,所以這種返回方式是按值返回.就會造成調用復制構造函數。
如果我們不需要調用復制構造函數那么就可以如下寫
A* func(A *one){
return one;
}
這里需要注意的是返回值是類A的內存地址引用 寫法為 A* 而不能是A 否則就會報類型不匹配異常。
使用const指針傳遞對象
按址傳遞對象雖然可以避免調用復制構造函數和析構函數,但是由于它得到了該對象的內存地址,可以隨時修改對象的數據.所以它實際上是破壞了按值傳遞的保護機制。比如說按值傳遞
就象把盧浮宮的那副達芬奇的畫制作了一個副本,送交法國總理希拉克的官邸,這樣希拉克對該畫的任何操作也不會影響到原畫。不過假如希拉克親自跑到盧浮宮去觀賞原畫,那么他就完
全可以對原畫進行修改或操作。不過我們仍然對此有解決辦法,那就是用const指針來接受對象,這樣就可以防止任何試圖對該對象所進行的操作行為,并且保證返回一個不被修改的對象
例:
const A* const func(const A *const one){}
//這樣參數one前加上const 那么參數one就是不可修改的
//在A前加上const這樣參數指針one 指向的對象也就是不可修改的
//在func前面加上const 代表返回的one指針不可修改
//在類名A前面加上const 代表返回的指針one 也是不可以被修改的
這樣就保證了傳遞進來的數據不被修改,同時又保證了返回的數據也不會被修改。
我們將函數的返回值和接收參數都定義為const,就可以保證函數內不可修改原始值,同時避免利用返回值對原始值進行修改.所以加上這個const,實際上是為了實現按值傳遞的開銷,
因為不用再調用復制構造函數。但是加const很麻煩,下面有種簡便的方法.按別名傳遞對象。
按別名傳遞對象
由于引用不能重新分配去引用另一個對象,它始終是常量,所以我們不用將它設置為常量.所以就不需要加const修飾符
A *cunc(A &one){
return one;
}
int main(){
A a;
a.set(11);
A &b=func(a);
count<<b.get()<<endl;
return 0;
}
那么到底是引用還是指針呢?
既然引用實現了指針的功能,而且使用起來更加方便,為什么還要指針呢?
這是因為指針可以為空,但是引用不能為空,指針可以被賦值,但是引用只可以被初始化,不可以被賦為另一個對象的別名.如果你想使一個變量記錄不同對象的地址,那么就必須使用指針
例:
int main(){
int a=6;
int *p=&a;
int b=9;
p=&b;
return 0;
}
指針p可以讓其指向a 也可以指向b 但是別名卻不可以,另外,在堆中創建一塊內存區域,必須要用指針來指向它,否則該區域就會變成無法訪問的內存空間。當然我們也可以使用引用
來引用指向內存空間的指針。
用例子來演示上面這句話的意思
例:
int main(){
int *p=new int;
int &r=*p; //這樣,這個r就變成了用指針p讀取到的值的別名。
r=4;
cout<<*p<<endl;
return 0;
}
但是我們要明白一點,我們不可以直接用引用來指向堆中新建的空間,因為引用只是個別名,它不可以作為指針來使用。
因為在機器運行不正常的情況下,也就是機器虛擬內存大小,無法創建新空間的情況下,那么new int 會自動返回一個空指針。我們知道引用不能為空,因此這種情況下使用
int *&r=new int;就會導致一個無用的別名。而使用(*)讀取一個無用的別名則會引起系統崩潰。
解決的辦法是不要將引用初始化為新建內存區域的別名,而要將r初始化為指向該區域的指針的別名。前提是首先要判斷該指針不為空。
例:
int *p=new int; //第一行定義了一個指向int的指針p,該指針指向新建的一塊內存.
if(p!=null){ //第二行測試p,加入不為空,表示空間創建成功。
int &r=*p; //將r初始化為p指向的內存空間中數據的別名
r=3; //設置空間值為3
cout<<r<endl; //輸出該空間的值
}
指針與引用的區別:
指針可以為空,引用不能為空.
指針可以被賦值,引用不能被賦值。
指針可以指向堆中空間,引用不可以指向堆中空間。
了解了 指針與引用的區別以后,我們就可以有選擇的來使用指針或者引用了。
引用和指針可以一起用
上一節我們已經學習了一個指針和引用混合使用的例子,如:int *&r=new int;
int *func(int &one,int *two,int x);
上面這行語句聲明了一個func函數,該函數有三個參數,第一個是int型變量的別名one,第二個是指向int型變量的指針two,第三個是整型參數x。該函數返回一個指向int型變量
的指針。
另外我們也要注意指針的一些特殊寫法,如:
int *r,ra; 這里的r是指針 ra是變量。一定不要將兩者都看作是指針
引用容易犯的錯誤
與指針一樣,引用使用不當也會出現致命性的錯誤。我們知道引用是對象的別名,那么假如這個對象不存在了,使用這個對象的別名會產生什么樣的后果呢?
class A{
public:
A(int i){x=i;}
int get(){return x;}
private:
int x;
};
A&func(){
A a(23);
return a;
}
int main(){
A &r=func();//用別名接收別名
cout<<r.get()<<endl;
return 0;
}
由于對象a是個局部對象,因此當函數func結束后,局部對象a也就被刪除了。由于對象a消失了,所以func()函數返回的其實是一個并不存在的對象的別名。因此打印的結果并不是23
,返回一個隨機數,如果去掉func方法上的& 那么就會返回a
引用按值返回的堆中對象
常見錯誤2.
A func(){
cout<<"跳轉到func函數中!
";
A *p=new A(99);
cout<<"隊中對象的內存地址"<<p<<endl;
return *p;
}
int main(){
A &r=func();
cout<<"隊中對象的副本的地址"<<&r<<endl;
cout<<r.get()<<endl;
return 0;
}
由于p所指向的堆中空間必須使用delete運算符才能被刪除,因此該空間成了不可訪問的區域,結果導致了內存泄露。
又或者說:p指針被刪除了,它所指向的空間還存在。該空間的地址只有p保存著,而p找不到了,所以我們無法找到該空間。由于無法找到該空間,所以無法對其進行釋放,結果造成了
內存泄露。
引用按別名返回的堆中對象
要避免上面例子出現的內存泄露,我們就不能用按值的方式來返回一個堆中對象,而必須按地址或者別名的方式返回一個別名或者內存地址,這樣就不會調用復制構造函數創建一個該對象的
副本,而是直接將該對象的別名或者地址返回。由于返回的對象的別名或者地址初始化給了main函數中的一個引用或者指針,因此即使被調用函數中的局部指針超過作用域被系統釋放,也
可由main函數中的引用或者指針找到該堆中空間,不會令該空間成為不可訪問的區域,從而避免了內存泄露。
所以將上例中的func()函數改為
A& func(){
cout<<"跳轉到func函數中!
";
A *p=new A(99);
cout<<"堆中對象的地址:"<<p<<endl;
return *p;
}
int main(){
A &r=func();
cout<<"堆中對象的副本的地址:"<<&r<<endl;
cout<<r.get()<<endl;
A *p=&r;
delete p;
return 0;
}
通過輸出語句可以看出兩次的內存地址是相同的,由于它們的地址相同,所以即使func函數中的局部指針p被系統自動銷毀了,我們也能通過main函數中的別名r來訪問到堆中對象。
因為r與堆中對象的地址是相同,r是堆中對象的別名,所以對r的操作就是對堆中對象的操作。由于無法對引用使用delete運算符,因此我們只能定義一個指針來存儲應用的地址,然后
刪除該指針指向的內存空間。
這一切操作都非常順利,程序也輸出了正確的結果。但是這個程序卻隱藏著一個非常嚴重的問題。
由于p指向的堆中對象被刪除了,因此堆中對象的別名r成了個空別名。它就成為了一個不存在的對象的別名,因此假如再使用這個空別名去訪問不存在的對象的成員函數get()時 就會輸出
一個隨機數。這樣都導致了一個不易察覺的錯誤。
在哪里創建,就在哪里釋放
為了解決上例中的不易察覺的錯誤,我們就需要做到在哪里創建,就在哪里釋放。
只要在堆中創建一塊內存空間,就會返回一個指向該空間的指針,我們一定不要弄丟該指針,加入該指針丟失,那么該堆中空間就會成為一塊不可訪問的區域,也就是我們常說的內存泄露
同時假如我們將存儲在堆中的對象初始化給一個引用,那么當該對象被刪除時,這個引用也就成了空引用,假如我們無意中使用了這個空引用的話,就會令程序出錯。
上面的兩個例子中的程序所犯的錯誤,在這里就來解決這些錯誤,我們可以在func()函數中創建堆中對象,然后在main函數中釋放該對象,但是在上例的程序告訴我們,這樣也不安全,雖然
我們也可以用指針來代替引用,但是加入我們無法確定指向堆中對象的指針是哪一個,是func()函數的p指針,還是main函數中接受堆中對象的r指針,那么我們就有可能將該指針刪除兩次,
或者忘記刪除指針。這樣為了避免指針混淆,我們就必須:在哪里創建,就在哪里釋放。
因此我們在main函數中創建一個堆中對象,然后按引用的方式傳遞到func()函數中,在func()函數中對該對象操作完畢后返回該對象,然后在main函數中釋放該對象.這樣就實現了在
main函數中創建,在main函數中釋放。
寫法如下:
#include<iostream>
using namespace std;
class A{
public:
A(int i){cout<<"執行構造函數創建一個對象
";x=1;}
A(A&A){x=a.x;cout<<"執行復制構造函數創建一個對象
";}
~A(){cout<<"執行析構函數!
";}
int get(){return x;}
void set(int i){x=1;}
private:
int x;
};
A& func(A&a){
cout<<"跳轉到func函數中!
";
a.set(66);
return a;
}
int main(){
A *p=new A(99);
func(*p);
cout<<p->get()<<endl;
delete p;
return 0;
}
新聞名稱:(五)引用-創新互聯
文章URL:http://m.newbst.com/article44/dioghe.html
成都網站建設公司_創新互聯,為您提供做網站、外貿網站建設、ChatGPT、面包屑導航、小程序開發、標簽優化
聲明:本網站發布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經允許不得轉載,或轉載時需注明來源: 創新互聯