初級程序員面試不靠譜指南(三)


二、指針的好基友的&

1.&的意義。說&是指針的好基友其實不恰當,因為&這個符號在C/C++不止有一種含義,但是因為其經常會和指針一起出現在被問的問題列表上,所以,在大部分情況下,它們是好基友,那么&符號一共有哪些涵義呢?這一般都是初級篩選的題目,這種題目的意義在於快速的篩選掉那些根本什么也不會的人。答案很簡單,主要有三個地方會用到這個符號,第一個取變量的地址,比如在int *pointer=&i;時,這是這個符號是出現在等號的右邊(也就是右值),第二個表示引用,這個概念會是本節的重點,出現在int &ref=i;這種類型的等式之中,在等號的左邊,第三種是一種運算符,標示兩個位相與(1&1=1,1&0=0,0&1=0,0&0=0)。

2.什么是“引用”。引用這個詞可以理解為精確無誤的轉述或者表達別人曾經說過的話或者寫過的文字,說白了完全就是別人的東西,那么怎么理解引用這個東西呢?按照很多經典書籍上所敘述的,引用就可以看做是變量的一個別名(alias),可以理解成換了另外一個符號表示這個變量。在編譯器編譯你的程序的時候,會將你在程序里定義的所有符號放在一個叫做符號表的物體之中,然后按照一定的規律給符號表中的內容分配內存。比如你定義了一個int i=3;那么這個i就會被放在符號表中,然后會給這個符號表示的內容分配一個內存單元,其中的內容是3,再按照某種深刻的方式將這個分配的內存和這個符號聯系起來,這就完成了一個變量的定義(編譯原理只記得個大概了,有錯誤的話請指正)。對比指針和引用的使用方式,多少有點類似,那么先從下面的一個小例子初窺一下指針和引用在這個方面有什么不同:

 int i=0;
 int *pointer_i=&i;
 int &ref_i=i;
 
 cout<<"address of i:"<<&i<<endl;
 cout<<"address of ref_i:"<<&ref_i<<endl;
 cout<<"address of pointer_i:"<<&pointer_i<<endl;

      這里面出現了太多&符號,參考1里面的內容,主要有兩個含義,一個是取變量地址,比如cout里面那些,pointer的那句,一個是引用,分清楚這些應該並不是什么難事,運行這段程序,可以看到輸出如下:

     

      可以看到ref_i和i的地址是一樣的,pointer的地址不一樣,說明了編譯器給pointer分配了一個內存單元,而ref_i沒有,這里就是它們的第一個不同點,編譯器會給指針分配一個內存單元,而對於引用不會,所以說ref_i所表示都不能稱之為一個變量或什么的,它僅僅是i的另一個符號表示而已。它和i一樣,一舉一動都是操作的同一個內容,完成的同一件事情。那么設計這樣一個概念的作用是什么呢?在上面這段程序中使用兩個符號表示一個變量的意義明顯不是要一個做另一個的備胎,其作用主要體現在涉及到函數的時候(包括成員函數),下面一個例子真的是爛大街了。

void swap(int& i, int& j)
{
  int tmp = i;
  i = j;
  j = tmp;
}

      如果你看過100頁的關於C++的書,你會像背誦一樣說出上面代碼的意義,它可以完成兩個數的交換,如果不使用&,就不可能得到正確的結果。為了稍微深刻一點的展示這個概念,可以采用如下的代碼:

void swap1(int& i, int& j)
{
  cout<<"1:address1:"<<&i<<endl;
  cout<<"1:address2:"<<&j<<endl;
  int tmp = i;
  i = j;
  j = tmp;
}

void swap2(int i, int j)
{
  cout<<"2:address1:"<<&i<<endl;
  cout<<"2:address2:"<<&j<<endl;
  int tmp = i;
  i = j;
  j = tmp;
}

void main()
{
 int a=7,b=10;
  
 cout<<"address of a:"<<&a<<endl;
 cout<<"address of b:"<<&b<<endl;
 
 swap1(a,b);
 swap2(a,b);
} 
swap code

     目的是為了查看在函數調用的時候各個變量的具體情況,其執行結果如下圖所示:

    

     可以看到在swap1中,傳入兩個參數的地址就是main函數中兩個變量的地址,而swap2的兩個參數地址是新的,和原始變量的地址沒有任何關系(而且還很遠,有興趣的話這里也可以繼續研究下去,但是我想一起放在函數的時候再寫),也就是說無論怎么操作,它並沒有改變原來的變量的值,它改變的是在另外某一個地方,我們這個傳入參數的值所賦予的兩個新的變量的值(這就是按值傳遞),和我們想改變的兩個內存單元里面保存的值沒有關系,為了能夠簡潔點的表示出這個概念,一般都會用一個詞“副本”。而swap1,我們是將地址傳給了調用函數(按地址傳遞),直接改變的我們想要改變的內存單元的值,也就是可以調用者想要達到的目的。有一點需要強調的是,在C語言中,不能使用類似swap1(int &i,int &j)這樣的形式,因為C語言中沒有按址傳遞的概念,它通過傳遞指針實現按址傳遞,但是其本質還是傳遞的值,只不過傳遞的是一個地址的值,這樣說好像有些邏輯不通,不過這個問題不大,因為有的書上也認為C語言有按址傳遞,我想表達的主要意思是,在C語言中,不能使用swap1(int &i,int &j)。希望下面的例子能多少表達一點我想說的意思。

void test(int *u)
{
    printf("address of parameter itself:%p\r\n",&u);
    printf("address of parameter pointed:%p\r\n",&(*u));
}

void main()
{
    int i=100;
    printf("address of i:%p\r\n",&i);
        test(&i);     
} 
C語言中的按址傳遞

     執行結果如下,請仔細分別一下,2C和7C。

    

3.const和引用。const和引用結合也會有很多可能會咨詢的問題,先從最簡單的開始,下面的代碼能不能通過編譯?

int &b=7;

    你可以先試試,答案可能是不能通過編譯,因為7是一個常量,自然需要一個常量的引用,所以正確的寫法應該是const int& b=7,那么這個引用有沒有被分配內存呢?如果你試着輸出會發現其被分配了一個內存,你可能要說不是引用不會被分配內存嗎?但是這里被分配內存的是這個常量'7',而不是引用b,所以這一點和前面所論述的並不矛盾。

    下面是一個經典問題了,在這里我也是完整的“引用”

//(1)
     int i = 42;
     const int &r = 42;
     int temp = r + i;
     int& r2 = temp;

//(2)
     int i = 42;
     const int &r = 42;
     int& r2 = r + i;
const和引用

    上面兩段代碼哪一個是正確的?答案是第一個,r+i 會產生一個int類型臨時變量所以在使用的時候要用const 引用,所以第二種用法,如果寫成const int& r2 = r + i;就是對的。

    第三個問題就是在傳遞的時候為什么經常會遇到f(const A& a),A是一個struct或者class的名字,前面說過了按值傳遞是將傳進來的值一個個復制到某一個地方的變量之中,如果這個類很龐大,自然在傳遞的過程中就會復制很多的內容,這樣太浪費時間,所以如果采用的是引用的方式,僅僅是傳入對象的一個別名,避免了復制所造成效率的浪費。那為什么要加一個const呢?從2中也可以看到,如果不采用const的話,傳入的變量有會被改變,所以使用const可以保證不會被誤操作而發生改變。但是內置數據類型(比如int)能用傳值的盡量不要用傳址,因為內置的數據類型沒有構造析構的過程,這兩種效率都差不多,而且你不按常理出牌,使用const和引用結合的方式,對於可讀性會造成影響,別人會好奇什么這里需要使用這樣一個東西。

4.返回值和引用。將引用作為返回值和3類似,主要目的是為了提高效率,這樣做不會產生臨時變量,不會調用構造函數,這是其優點,但是有一個特別值得注意的地方就是,不要返回臨時變量的引用,我們以下面的例子來說明:

int& ReturnReference()
{
    int i=100;
    cout<<"function1---address i:"<<&i<<endl;
    return i;
}

void test()
{
    int x=0,y=0,z=0;
    cout<<"function2---address x:"<<&x<<endl;
    cout<<"function2---address y:"<<&y<<endl;
    cout<<"function2---address z:"<<&z<<endl;
}

void main()
{
 int &a=ReturnReference(); 
 cout<<"main---address a:"<<&a<<endl;
 test();
 cout<<"a is:"<<a<<endl; 
} 
引用返回值

   先來看看輸出結果好了:

  

   可以看到,輸出的a的值並不正確了,查看一下各個函數中的地址信息,a所得到的值正是第一個函數中返回的引用的值(地址相同),但是第二個函數調用以后,可以看到x也用了第一個函數中i的地址,此時a所表示的變量也在這個地址之中,所以,a變量的值被覆蓋了,至於為什么不是0,這個問題在這里展開就太多了,你可以試試在你注釋掉所有的cout和test語句中,就能得到正確的結果。其原因是函數里面的變量在函數結束之后(局部變量)就消失(析構)了,它原來的地址下一次仍然會被使用,從輸出中也可以看到這一點,所以不要返回一個局部變量的引用。但是如果你將第一個函數的返回值保存到一個變量a中,那么無論調用test與否都是正確的,這是因為你已經又開辟了一個內存單元保存了返回的引用值,后面的修改也不會針對這個內存單元了。

5.指針和引用的區別。指針和引用的區別也是經常會被問到的一個問題,稍微總結一下好了:

   引用就是該變量自身,而指針只是指向該變量而已。

   當一個引用被創建的時候,它能再作為其他的引用,但是指針可以重新調整其指向的對象(但是這一點在有些編譯器里面貌似已經是允許的了)。

   引用不能設置為NULL,指針可以。

   引用不能不初始化,指針在不考慮出問題的情況下至少是允許不初始化的。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM