揭開C/C++中數組形參的迷霧楔子

揭開C/C++中數組形參的迷霧楔子
去年,周星星大哥曾經在VCKBASE/C++論壇發表過一篇文章《"數組引用"以避免"數組降階"》*1,當時我不能深入理解這種用法的含義;時隔一年,我的知識有幾經錘鍊,終於對此文章漸有所悟,所以把吾所知作想詳細道來,竟也成了一篇文章。希望本文能對新手有所啓迪,同時也希望大家發現本文中的疏漏之處後不吝留言指教。

故事起源於周星星大哥給出的兩個Demo,爲了節省地方,我把兩個Demo合二爲一,也能說明同樣的問題:


其運行結果如下:

In main function : 400
pass by pointer: 4
pass by reference: 400

這段代碼說明了,如果數組形參是數組名形式(或者指針形式,下文討論)時,使用sizeof運算符,將得不到原來數組的長度;如果用傳遞原數組引用的方法,則沒有問題。

這段代碼的確很難理解,因爲這短短的十幾行涉及到了形參與實參的關係、數組名和指針的關係、引用的意義、聲名和表達式的關係這4大類問題,只要有1條理解不透、或者理解不正確,就理解不透上面的這段代碼。本文也就從這4個問題入手,把這4個問題首先解決掉,然後再探討上面的這段代碼。雖然這樣看來很是繁複,但是我認爲從根上入手來理解、學習,是條似遠實近的道路。

一、函數形參和實參的關係

void Foo(int a);
Foo(10);
這裏的a叫做形式參數(parameter),簡稱形參;這裏的10叫做實際參數(argument),簡稱實參。形參和式參之間是什麼關係呢?他們是賦值的關係,也就是說:把實參傳遞給形參的過程,可以看作是把實參賦值給形參的過程。上面的例子中,實參10傳遞給形參a,就相當於a=10;這個賦值的過程。(因爲數據類型多的很,無法舉例子舉全面,所以這裏就不舉例子了;如果覺得不好理解,就在vc中寫個sample調試一下各種數據類型的情況,你就能夠驗證這個結論了。)

二、數組名和指針的關係

這個問題是個歷史性的問題了,在C語言中,數組名是當作指針來處理的。更確切的說,數組名就是指向數組首元素地址的指針,數組索引就是距數組首元素地址的偏移量。理解這一點很重要,很多數組應用的問題就是有此而起的。這也就是爲什麼C語言中的數組是從0開始計數,因爲這樣它的索引就比較好對應到偏移量上。在C語言中,編譯過程中遇到有數組名的表達式,都會把數組名替換成指針來處理;編譯器甚至無法區分a[4]和4[a]的區別!*2 但是下面這一點需要注意:

int a[100];
int *b;
這兩者並不等價,第一句話聲明瞭數組a,並定義了這個數組,它有100個int型元素,sizeof(a)將得到整個數組所佔的內存大小,是400;第二句話只是聲明並定義了一個int型的指針,sizeof(b)將得到這個指針所佔的內存大小,是4。所以說,雖然數組名在表達式中一般會當作指針來處理,但是數組名和指針還是有差距的,最起碼有a==&a[0]但是sizeof(a)!=sizeof(a[0])。

並且在ANSI C標準中,也明文規定:在函數參數的聲明中,數組名北邊一起當作指向該數組第一個元素的指針。所以,下面的幾種書寫形式是等效的:

void Foo1(int arr[100]){}
void Foo2(int arr[]){}
void Foo3(int *arr){}
C++儘可能的全面兼容C語言,所以這一部分的語法相同。

三、引用的意義

“引用“是C++中引進的概念,C語言中沒有。它的目的在於,在某些方面取代指針。如果你認爲引用和指針並無大不同,肯定會爲指針報不平,頗有一種“即生亮何生瑜”的感慨;但是,引用確實有新的特色,也確實在很多地方的表現和指針有所不同,本文就是一例。使用引用,我們要把握這它最最最重要的一點,這也是它和指針最大的區別:引用一經定義,就和被它引用的變量緊緊地結合在一起,再不分開,對引用的任何操作都反映在它引用的變量上;而指針,只是訪問它指向變量的另一種方式,兩者雖有聯繫,但是並不像引用那樣密不可分。:)


下面是運行的結果,以供參考:

10 20
20 20
20 20

0012FED4 0012FED4

0012FED4 0012FEBC
0012FEBC 0012FEBC

四、聲明和表達式的關係

這裏想說明的是,分析一個聲明可以把它看作一個表達式,按照表達式中的運算符優先級順序來聲明。比如int (&arr)[100],你首先要找到聲明器arr,那麼&arr說明arr是一個引用。什麼引用呢?在看括號外面,[]說明了這一個數組,100說明這個數組有100個元素,前面的int說明了這個數組的每個元素都是int型的。所以,這個聲明的意思就是:arr就是指向具有100個int型元素的數組的引用。如果你覺得這種理解很晦澀,那你就不妨用typedef來簡化聲明中的複雜的運算符優先級關係,比如下面的形式就很好理解,其效果是和最初的那個例子是一樣的:


===大結局===

吐沫星亂飛了半天,大家感覺還好吧,快結束了,大家再忍耐一下。看看下面這段程序:

#include
using namespace std;
void main()
{
 int a[100];
 int * pa = a;
 int (&a_ref)[100] = a;
 cout << sizeof(a) << endl;
 cout << sizeof(pa) << endl;
 cout << sizeof(a_ref) << endl;
 system("pause");
}

怎麼樣,是不是對輸出結果感到很自然呢?如果是,那就好辦了。我總結一下就下課哈!^_^ 數組名在表達式中,往往被當作是指向首元素a[0]地址的指針,但是在sizeof(a)中,返回的結果是數組a佔用內存的大小;pa是指向a的指針,他也指向a[0],但是sizeof(pa)中,返回結果是pa這個指針所佔內存空間的大小,之所以這樣,因爲pa這個指針和數組a的結合不夠緊密,屬於訪問數組a的第二被選方案;a_ref這個引用,就是對數組a的引用,就像“惡鬼附體”一樣,一旦附體附上了,你怎麼也甩不掉它,對它的任何操作,全部都反映在a上。在看本文最初的那個例子,比這個例子所增加的操作就是函數實參到形參的傳遞,我們在上面說過了,從實參到形參的傳遞可以看作是把實參賦值給形參。所以本文最初的那個例子,其實際的操作過程就和本文最後的這個例子是一樣的。所以,並非函數把數組給“降階”了,而是它原原本本就該這樣,千萬不必奇怪。 :p


意猶未盡,在PS一段:在C語言中,沒有引用,是怎麼解決這種問題呢。下面是常用的幾種作法:

1.傳遞數組的時候,在增加一個參數,用來記錄數組的元素個數或者長度。main(int argc, char ** args)就是這種做法;這種方法還可以防止溢出,安全性比較高。
2.在數組的最後一個有效元素後面作一個標誌,指明數組已經結束了。C語言中用char數組表示字符串,傳給相關的字符串函數,用的就是這種做法。這種方法保證了C的所謂字符串是無限長度的,因爲用一個變量表示數組的長度的話,終歸會受到這個變量類型的限制,比方說這個變量是unsigned byte型的,那麼字符串長度就不能超過256,否則這個變量就溢出了。
3.對於多維數組,通常的方法是在最後一個有效維後面做一行標誌,比如a[3][3]={{1,0,2},{2,2,5},{-1,-1,-1}}。如果我的程序用不到-1,我可以拿-1來填充最後一行,作爲標誌。這樣在函數內部檢測到某一維的元素都是-1,就說明到底了。

方法是靈活多變的,關鍵看人怎麼用了。C老爹Dennis Ritchie曾經說過:C詭異離奇,缺陷重重,卻獲得了巨大的成功。

注1:本文將不再引用“降階”這個術語,原因是我認爲這個“降階”的概念有種把類似2維數組壓扁到1維的意思,其實本文討論的並不是這個問題,本文討論的是數組形參傳遞過程中數組長度損失的問題(這麼說也不準確,還是看文中的討論吧)。

注2:C語言的編譯器遇到數組元素arr[i],就會替換成*(arr+i)的形式。

發佈了34 篇原創文章 · 獲贊 3 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章