指針,數組,類型的識別,參數可變的函數。
一.指針。
它的本質是地址的類型。在許多語言中根本就沒有這個概念。但是它卻正是C靈活,高效,在面向過程的時代所向披靡的原因所在。因為C的內存模型基本上對應了現在von Neumann(馮。諾伊曼)計算機的機器模型,很好的達到了對機器的映射。不過有些人似乎永遠也不能理解指針「注1」。
注1:Joel Spolsky就是這樣認為的,他認為對指針的理解是一種aptitude,不是通過訓練就可以達到的http://www.joelonsoftware.com/pr …… /fog0000000073.html
指針可以指向值、數組、函數,當然它也可以作為值使用。
看下面的幾個例子:
int* p;//p是一個指針,指向一個整數
int** p;//p是一個指針,它指向第二個指針,然後指向一個整數
int (*pa)[3];//pa是一個指針,指向一個擁有3個整數的數組
int (*pf)();//pf是一個指向函數的指針,這個函數返回一個整數
後面第四節我會詳細講解標識符(identifier)類型的識別。
1.指針本身的類型是什麼?
先看下面的例子:int a;//a的類型是什麼?
對,把a去掉就可以了。因此上面的4個聲明語句中的指針本身的類型為:
int*
int**
int (*)[3]
int (*)()
它們都是復合類型,也就是類型與類型結合而成的類型。意義分別如下:
point to int(指向一個整數的指針)
pointer to pointer to int(指向一個指向整數的指針的指針)
pointer to array of 3 ints(指向一個擁有三個整數的數組的指針)
pointer to function of parameter is void and return value is int (指向一個函數的指針,這個函數參數為空,返回值為整數)
2.指針所指物的類型是什麼?
很簡單,指針本身的類型去掉 “*”號就可以了,分別如下:
int
int*
int ()[3]
int ()()
3和4有點怪,不是嗎?請擦亮你的眼睛,在那個用來把“*”號包住的“()”是多余的,所以:
int ()[3]就是int [3](一個擁有三個整數的數組)
int ()()就是int ()(一個函數,參數為空,返回值為整數)「注2」
注2:一個小小的提醒,第二個“()”是一個運算符,名字叫函數調用運算符(function call operator)。
3.指針的算術運算。
請再次記住:指針不是一個簡單的類型,它是一個和指針所指物的類型復合的類型。因此,它的算術運算與之(指針所指物的類型)密切相關。
int a[8];
int* p = a;
int* q = p + 3;
p++;
指針的加減並不是指針本身的二進制表示加減,要記住,指針是一個元素的地址,它每加一次,就指向下一個元素。所以:
int* q = p + 3;//q指向從p開始的第三個整數。
p++;//p指向下一個整數。
double* pd;
……//某些計算之後
double* pother = pd – 2;//pother指向從pd倒數第二個double數。
4.指針本身的大小。
在一個現代典型的32位機器上「注3」,機器的內存模型大概是這樣的,想象一下,內存空間就像一個連續的房間群。每一個房間的大小是一個字節(一般是二進制8位)。有些東西大小是一個字節(比如char),一個房間就把它給安置了;但有些東西大小是幾個字節(比如double就是8個字節,int就是4個字節,我說的是典型的32位),所以它就需要幾個房間才能安置。
注3:什麼叫32位?就是機器CPU一次處理的數據寬度是32位,機器的寄存器容量是32位,機器的數據,內存地址總線是32位。當然還有一些細節,但大致就是這樣。16位,64位,128位可以以此類推。
這些房間都應該有編號(也就是地址),32位的機器內存地址空間當然也是32位,所以房間的每一個編號都用32位的二進制數來編碼「注4」。請記住指針也可以作為值使用,作為值的時候,它也必須被安置在房間中(存儲在內存中),那麼指向一個值的指針需要一個地址大小來存儲,即32位,4個字節,4個房間來存儲。
注4:在我們平常用到的32位機器上,絕少有將32位真實內存地址空間全用完的(232 = 4G),即使是服務器也不例外。現代的操作系統一般會實現32位的虛擬地址空間,這樣可以方便運用程序的編制。關於虛擬地址(線性地址)和真實地址的區別以及實現,可以參考《Linux源代碼情景分析》的第二章存儲管理,在互聯網上關於這個主題的文章汗牛充棟,你也可以google一下。
但請注意,在C++中指向對象成員的指針(pointer to member data or member function)的大小不一定是4個字節。為此我專門編制了一些程序,發現在我的兩個編譯器(VC7.1.3088和Dev-C++4.9.7.0)上,指向對象成員的指針的大小沒有定值,但都是4的倍數。不同的編譯器還有不同的值。對於一般的普通類(class),指向對象成員的指針大小一般為4,但在引入多重虛擬繼承以及虛擬函數的時候,指向對象成員的指針會增大,不論是指向成員數據,還是成員函數。「注5」。
注5:在Andrei Alexandrescu的《Modern C++ Design》的5.13節Page124中提到,成員函數指針實際上是帶標記的(tagged)unions,它們可以對付多重虛擬繼承以及虛擬函數,書上說成員函數指針大小是16,但我的實踐告訴我這個結果不對,而且具體編譯器實現也不同。一直很想看看GCC的源代碼,但由於旁骛太多,而且心不靜,本身難度也比較高(這個倒是不害怕^_^),只有留待以後了。
還有一點,對一個類的static member來說,指向它的指針只是普通的函數指針,不是pointer to class member,所以它的大小是4.
5.指針運算符&和*
它們是一對相反的操作,&取得一個東西的地址(也就是指針),*得到一個地址裡放的東西。這個東西可以是值(對象)、函數、數組、類成員(class member)。
其實很簡單,房間裡面居住著一個人,&操作只能針對人,取得房間號碼;
*操作只能針對房間,取得房間裡的人。
參照指針本身的類型以及指針所指物的類型很好理解。
小結:其實你只要真正理解了1,2,就相當於掌握了指針的牛鼻子。後面的就不難了,指針的各種變化和C語言中其它普通類型的變化都差不多(比如各種轉型)。
二.數組。
在C語言中,對於數組你只需要理解三件事。
1.C語言中有且只有一維數組。
所謂的n維數組只是一個稱呼,一種方便的記法,都是使用一維數組來仿真的。
C語言中數組的元素可以是任何類型的東西,特別的是數組作為元素也可以。所以int a[3][4][5]就應該這樣理解:a是一個擁有3個元素的數組,其中每個元素是一個擁有4個元素的數組,進一步其中每個元素是擁有5個整數元素的數組。
是不是很簡單!數組a的內存模型你應該很容易就想出來了,不是嗎?:)
2.數組的元素個數,必須作為整數常量在編譯階段就求出來。
int i;
int a;//不合法,編譯不會通過。
也許有人會奇怪char str[] = “test”;沒有指定元素個數為什麼也能通過,因為編譯器可以根據後面的初始化字符串在編譯階段求出來,
不信你試試這個:int a[];
編譯器無法推斷,所以會判錯說“array size missing in a”之類的信息。不過在最新的C99標准中實現了變長數組「注6」
注6:如果你是一個好奇心很強烈的人,就像我一樣,那麼可以查看C99標准6.7.5.2.
3.對於數組,可以獲得數組第一個(即下標為0)元素的地址(也就是指針),從數組名獲得。
比如int a[5]; int* p = a;這裡p就得到了數組元素a[0]的地址。
其余對於數組的各種操作,其實都是對於指針的相應操作。比如a[3]其實就是*(a+3)的簡單寫法,由於*(a+3)==*(3+a),所以在某些程序的代碼中你會看到類似3[a]的這種奇怪表達式,現在你知道了,它就是a[3]的別名。還有一種奇怪的表達式類似a[-1],現在你也明白了,它就是*(a-1)「注7」。
注7:你肯定是一個很負責任的人,而且也知道自己到底在干什麼。你難道不是嗎?:)所以你一定也知道,做一件事是要付出成本的,當然也應該獲得多於成本的回報。
我很喜歡經濟學,經濟學的一個基礎就是做什麼事情都是要花成本的,即使你什麼事情也不做。時間成本,金錢成本,機會成本,健康成本……可以這樣說,經濟學的根本目的就是用最小的成本獲得最大的回報。
所以我們在自己的程序中最好避免這種邪惡的寫法,不要讓自己一時的智力過剩帶來以後自己和他人長時間的痛苦。用韋小寶的一句話來說:“賠本的生意老子是不干的!”
但是對邪惡的了解是非常必要的,這樣當我們真正遇到邪惡的時候,可以免受它對心靈的困擾!
對於指向同一個數組不同元素的指針,它們可以做減法,比如int* p = q+i;p-q的結果就是這兩個指針之間的元素個數。i可以是負數。但是請記住:對指向不同的數組元素的指針,這樣的做法是無用而且邪惡的!
對於所謂的n維數組,比如int a[2][3];你可以得到數組第一個元素的地址a和它的大小。*(a+0)(也即a[0]或者*a)就是第一個元素,它又是一個數組int[3],繼續取得它的第一個元素,*(*(a+0)+0)(也即a[0][0]或者*(*a)),也即第一個整數(第一行第一列的第一個整數)。如果采用這種表達式,就非常的笨拙,所以a[0][0]記法上的簡便就非常的有用了!簡單明了!
對於數組,你只能取用在數組有效范圍內的元素和元素地址,不過最後一個元素的下一個元素的地址是個例外。它可以被用來方便數組的各種計算,特別是比較運算。但顯然,它所指向的內容是不能拿來使用和改變的!
關於數組本身大概就這麼多,下面簡要說一下數組和指針的關系。它們的關系非常暧昧,有時候可以交替使用。
比如 int main(int args, char* argv[])中,其實參數列表中的char* argv[]就是char** argv的另一種寫法。因為在C語言中,一個數組是不能作為函數引數(argument)「注8」直接傳遞的。因為那樣非常的損失效率,而這點違背了C語言設計時的基本理念——作為一門高效的系統設計語言。
注8:這裡我沒有使用函數實參這個大陸術語,而是運用了台灣術語,它們都是argument這個英文術語的翻譯,但在很多地方中文的實參用的並不恰當,非常的勉強,而引數表示被引用的數,很形象,也很好理解。很快你就可以像我一樣適應引數而不是實參。
dereferance,也就是*運算符操作。我也用的是提領,而不是解引用。
我認為你一定智勇雙全:既有寬容的智慧,也有面對新事物的勇氣!你不願意承認嗎?:)
所以在函數參數列表(parameter list)中的數組形式的參數聲明,只是為了方便程序員的閱讀!比如上面的char* argv[]就可以很容易的想到是對一個char*字符串數組進行操作,其實質是傳遞的char*字符串數組的首元素的地址(指針)。其它的元素當然可以由這個指針的加法間接提領(dereferance)「參考注8」得到!從而也就間接得到了整個數組。
但是數組和指針還是有區別的,比如在一個文件中有下面的定義:
char myname[] = “wuaihua”;
而在另一個文件中有下列聲明:
extern char* myname;
它們互相是並不認識的,盡管你的本義是這樣希望的。
它們對內存空間的使用方式不同「注9」。
對於char myname[] = “wuaihua”如下
myname
w
u
a
i
h
u
a
\0
對於char* myname;如下表
myname
\|/
w
u
a
i
h
u
a
\0
注9:可以參考Andrew Konig的《C陷阱與缺陷》4.5節。
改變的方法就是使它們一致就可以了。
char myname[] = “wuaihua”;
extern char myname[];
或者
char* myname = “wuaihua”;//C++中最好換成const char* myname = “wuaihua”。
extern char* myname;
C之詭谲(下)
三.類型的識別。
基本類型的識別非常簡單:
int a;//a的類型是a
char* p;//p的類型是char*
……
那麼請你看看下面幾個:
int* (*a[5])(int, char*); //#1
void (*b[10]) (void (*)()); //#2
doube(*)() (*pa)[9]; //#3
如果你是第一次看到這種類型聲明的時候,我想肯定跟我的感覺一樣,就如晴天霹雳,五雷轟頂,頭昏目眩,一頭張牙舞爪的猙獰怪獸撲面而來。
不要緊(Take it easy)!我們慢慢來收拾這幾個面目可憎的紙老虎!
1.C語言中函數聲明和數組聲明。
函數聲明一般是這樣int fun(int,double);對應函數指針(pointer to function)的聲明是這樣:
int (*pf)(int,double),你必須習慣。可以這樣使用:
pf = &fun;//賦值(assignment)操作
(*pf)(5, 8.9);//函數調用操作
也請注意,C語言本身提供了一種簡寫方式如下:
pf = fun;// 賦值(assignment)操作
pf(5, 8.9);// 函數調用操作
不過我本人不是很喜歡這種簡寫,它對初學者帶來了比較多的迷惑。
數組聲明一般是這樣int a[5];對於數組指針(pointer to array)的聲明是這樣:
int (*pa)[5]; 你也必須習慣。可以這樣使用:
pa = &a;// 賦值(assignment)操作
int i = (*pa)[2]//將a[2]賦值給i;
2.有了上面的基礎,我們就可以對付開頭的三只紙老虎了!:)
這個時候你需要復習一下各種運算符的優先順序和結合順序了,順便找本書看看就夠了。
#1:int* (*a[5])(int, char*);
首先看到標識符名a,“[]”優先級大於“*”,a與“[5]”先結合。所以a是一個數組,這個數組有5個元素,每一個元素都是一個指針,指針指向“(int, char*)”,對,指向一個函數,函數參數是“int, char*”,返回值是“int*”。完畢,我們干掉了第一個紙老虎。:)
#2:void (*b[10]) (void (*)());
b是一個數組,這個數組有10個元素,每一個元素都是一個指針,指針指向一個函數,函數參數是“void (*)()”「注10」,返回值是“void”。完畢!
注10:這個參數又是一個指針,指向一個函數,函數參數為空,返回值是“void”。
#3. doube(*)() (*pa)[9];
pa是一個指針,指針指向一個數組,這個數組有9個元素,每一個元素都是“doube(*)()”「也即一個指針,指向一個函數,函數參數為空,返回值是“double”」。
現在是不是覺得要認識它們是易如反掌,工欲善其事,必先利其器!我們對這種表達方式熟悉之後,就可以用“typedef”來簡化這種類型聲明。
#1:int* (*a[5])(int, char*);
typedef int* (*PF)(int, char*);//PF是一個類型別名「注11」。
PF a[5];//跟int* (*a[5])(int, char*);的效果一樣!
注11:很多初學者只知道typedef char* pchar;但是對於typedef的其它用法不太了解。Stephen Blaha對typedef用法做過一個總結:“建立一個類型別名的方法很簡單,在傳統的變量聲明表達式裡用類型名替代變量名,然後把關鍵字typedef加在該語句的開頭”。可以參看《程序員》雜志2001.3期《C++高手技巧20招》。
#2:void (*b[10]) (void (*)());
typedef void (*pfv)();
typedef void (*pf_taking_pfv)(pfv);
pf_taking_pfv b[10]; //跟void (*b[10]) (void (*)());的效果一樣!
#3. doube(*)() (*pa)[9];
typedef double(*PF)();
typedef PF (*PA)[9];
PA pa; //跟doube(*)() (*pa)[9];的效果一樣!
3.const和volatile在類型聲明中的位置
在這裡我只說const,volatile是一樣的「注12」!
注12:顧名思義,volatile修飾的量就是很容易變化,不穩定的量,它可能被其它線程,操作系統,硬件等等在未知的時間改變,所以它被存儲在內存中,每次取用它的時候都只能在內存中去讀取,它不能被編譯器優化放在內部寄存器中。
類型聲明中const用來修飾一個常量,我們一般這樣使用:const在前面
const int;//int是const
const char*;//char是const
char* const;//*(指針)是const
const char* const;//char和*都是const
對初學者,const char*;和 char* const;是容易混淆的。這需要時間的歷練讓你習慣它。
上面的聲明有一個對等的寫法:const在後面
int const;//int是const
char const*;//char是const
char* const;//*(指針)是const
char const* const;//char和*都是const
第一次你可能不會習慣,但新事物如果是好的,我們為什麼要拒絕它呢?:)const在後面有兩個好處:
A. const所修飾的類型是正好在它前面的那一個。如果這個好處還不能讓你動心的話,那請看下一個!
B.我們很多時候會用到typedef的類型別名定義。比如typedef char* pchar,如果用const來修飾的話,當const在前面的時候,就是const pchar,你會以為它就是const char* ,但是你錯了,它的真實含義是char* const.是不是讓你大吃一驚!但如果你采用const在後面的寫法,意義就怎麼也不會變,不信你試試!
不過,在真實項目中的命名一致性更重要。你應該在兩種情況下都能適應,並能自如的轉換,公司習慣,商業利潤不論在什麼時候都應該優先考慮!不過在開始一個新項目的時候,你可以考慮優先使用const在後面的習慣用法。
四.參數可變的函數
C語言中有一種很奇怪的參數“…”,它主要用在引數(argument)個數不定的函數中,最常見的就是printf函數。
printf(“Enjoy yourself everyday!\n”);
printf(“The value is %d!\n”, value);
……
你想過它是怎麼實現的嗎?
1. printf為什麼叫printf?
不管是看什麼,我總是一個喜歡刨根問底的人,對事物的源有一種特殊的癖好,一段典故,一個成語,一句行話,我最喜歡的就是找到它的來歷,和當時的意境,一個外文翻譯過來的術語,最低要求我會盡力去找到它原本的外文術語。特別是一個字的命名來歷,我一向是非常在意的,中國有句古話:“名不正,則言不順。”printf中的f就是format的意思,即按格式打印「注13」。
注13:其實還有很多函數,很多變量,很多命名在各種語言中都是非常講究的,你如果細心觀察追溯,一定有很多樂趣和滿足,比如哈希表為什麼叫hashtable而不叫hashlist?在C++的SGI STL實現中有一個專門用於遞增的函數iota(不是itoa),為什麼叫這個奇怪的名字,你想過嗎?
看文章我不喜歡意猶未盡,己所不欲,勿施於人,所以我把這兩個答案告訴你:
(1)table與list做為表講的區別:
table:
-------|--------------------|-------
item1 | kadkglasgaldfgl | jkdsfh
-------|--------------------|-------
item2 | kjdszhahlka | xcvz
-------|--------------------|-------
list:
****
***
*******
*****
That's the difference!
如果你還是不明白,可以去看一下hash是如何實現的!
(2)The name iota is taken from the programming language APL.
而APL語言主要是做數學計算的,在數學中有很多公式會借用希臘字母,
希臘字母表中有這樣一個字母,大寫為Ι,小寫為ι,
它的英文拼寫正好是iota,這個字母在θ(theta)和κ(kappa)之間!
你可以http://www.wikipedia.org/wiki/APL_programming_language
下面有一段是這樣的:
APL is renowned for using a set of non-ASCII symbols that are an extension of traditional arithmetic and algebraic notation. These cryptic symbols, some have joked, make it possible to construct an entire air traffic control system in two lines of code. Because of its condensed nature and non-standard characters, APL has sometimes been termed a "write-only language", and reading an APL program can feel like decoding an alien tongue. Because of the unusual character-set, many programmers used special APL keyboards in the production of APL code. Nowadays there are various ways to write APL code using only ASCII characters.
在C++中有函數重載(overload)可以用來區別不同函數參數的調用,但它還是不能表示任意數量的函數參數。
在標准C語言中定義了一個頭文件專門用來對付可變參數列表,它包含了一組宏,和一個va_list的typedef聲明。一個典型實現如下「注14」:
typedef char* va_list;
#define va_start(list) list = (char*)&va_alist
#define va_end(list)
#define va_arg(list, mode)
((mode*) (list += sizeof(mode)))[-1]
注14:你可以查看C99標准7.15節獲得詳細而權威的說明。也可以參考Andrew Konig的《C陷阱與缺陷》的附錄A.
ANSI C還提供了vprintf函數,它和對應的printf函數行為方式上完全相同,只不過用va_list替換了格式字符串後的參數序列。至於它是如何實現的,你在認真讀完《The C Programming Language》後,我相信你一定可以do it yourself!
使用這些工具,我們就可以實現自己的可變參數函數,比如實現一個系統化的錯誤處理函數error.它和printf函數的使用差不多。只不過將stream重新定向到stderr.在這裡我借鑒了《C陷阱與缺陷》的附錄A的例子。
實現如下:
#include
#include
void error(char* format, …)
{
va_list ap;
va_start(ap, format);
fprintf(stderr, “error: “);
vfprintf(stderr, format, ap);
va_end(ap);
fprintf(stderr, “\n”);
exit(1);
}
你還可以自己實現printf:
#include
int printf(char* format, …)
{
va_list ap;
va_start(ap, format);
int n = vprintf(format, ap);
va_end(ap);
return n;
}
我還專門找到了VC7.1的頭文件看了一下,發現各個宏的具體實現還是有區別的,跟很多預處理(preprocessor)相關。其中va_list就不一定是char*的別名。
typedef struct {
char *a0; /* pointer to first homed integer argument */
int offset; /* byte offset of next parameter */
} va_list;
其它的定義類似。
經常在Windows進行系統編程的人一定知道函數調用有好幾種不同的形式,比如__stdcall,__pascal,__cdecl.在Windows下_stdcall,__pascal是一樣的,所以我只說一下__stdcall和__cdecl的區別。
(1)__stdcall表示被調用端自身負責函數引數的壓棧和出棧。函數參數個數一定的函數都是這種調用形式。
例如:int fun(char c, double d),我們在main函數中使用它,這個函數就只管本身函數體的運行,參數怎麼來的,怎麼去的,它一概不管。自然有main負責。不過,不同的編譯器的實現可能將參數從右向左壓棧,也可能從左向右壓棧,這個順序我們是不能加於利用的「注15」。
注15:你可以在Herb Sutter的《More Exceptional C++》中的條款20:An Unmanaged Pointer Problem, Part 1:Parameter Evaluation找到相關的細節論述。
(2)__cdecl表示調用端負責被調用端引數的壓棧和出棧。參數可變的函數采用的是這種調用形式。
為什麼這種函數要采用不同於前面的調用形式呢?那是因為__stdcall調用形式對它沒有作用,被調用端根本就無法知道調用端的引數個數,它怎麼可能正確工作?所以這種調用方式是必須的,不過由於參數參數可變的函數本身不多,所以用的地方比較少。
對於這兩種方式,你可以編制一些簡單的程序,然後反匯編,在匯編代碼下面你就可以看到實際的區別,很好理解的!
重載函數有很多匹配(match)規則調用。參數為“…”的函數是匹配最低的,這一點在Andrei Alexandrescu的驚才絕艷之作《Modern C++ Design》中就有用到,參看Page34-35,2.7“編譯期間偵測可轉換性和繼承性”。
後記:
C語言的細節肯定不會只有這麼多,但是這幾個出現的比較頻繁,而且在C語言中也是很重要的幾個語言特征。如果把這幾個細節徹底弄清楚了,C語言本身的神秘就不會太多了。
C語言本身就像一把異常鋒利的剪刀,你可以用它做出非常精致優雅的藝術品,也可以剪出一些亂七八糟的廢紙片。能夠將一件武器用到出神入化那是需要時間的,需要多長時間?不多,請你拿出一萬個小時來,英國Exter大學心理學教授麥克。侯威專門研究神童和天才,他的結論很有意思:“一般人以為天才是自然而生、流暢而不受阻的閃亮才華,其實,天才也必須耗費至少十年光陰來學習他們的特殊技能,絕無例外。要成為專家,需要擁有頑固的個性和堅持的能力……每一行的專業人士,都投注大量心血,培養自己的專業才能。”「注16」
注16:台灣女作家、電視節目主持人吳淡如《拿出一萬個小時來》。《讀者》2003.1期。“不用太努力,只要持續下去。想擁有一輩子的專長或興趣,就像一個人跑馬拉松賽一樣,最重要的是跑完,而不是前頭跑得有多快。”
推薦兩本書:
K&R的《The C Programming language》,Second Edition.
Andrew Konig的《C陷阱與缺陷》。本文從中引用了好幾個例子,一本高段程序員的經驗之談。
但是對純粹的初學者不太合適,如果你有一點程序設計的基礎知識,花一個月的時間好好看看這兩本書,C語言本身就不用再花更多的精力了*/
/*
剛看完書,說什麼也得寫點東西總結一下,不自量力挑我認為最困難的部分吧
數組
給我印象最深刻的就是兩個字:“左值”,當然還得加上兩個字:“不是”。數組名是一個指針,指針是左值,但是數組名在C中不是一個左值,所以在C中不能出現在賦值運算的左邊。
要牢記這一點,所以不可以出現把一個數組名當整個數組賦值,要賦值的話,沒什麼好辦法,循環吧,或者用一些庫裡的函數,但是在用函數的時候,要牢記數組的長度,要提防越界,像我們不經常使用的fscanf這樣的函數,有緩沖區,就要顧及他的溢出問題,可以制定一個字段寬度,表示要讀入的最大字符數。
要確定數組的元素個數可以用到sizeof如:sizeof(a)/sizeof(a[0]),用數組中的第一個元素的大小去除整個數組的大小。
還有一點需要注意的是:值傳遞。當用一個簡單變量調用函數時,函數會受到被調用的參數的一個拷貝,這就是參數傳遞,而當一個數組參數的函數被調用時,形參和實參的關系就發生了變化,變成了值傳遞。(不會在blog裡插入圖片-_-b)用語言來描述其原因的話,就是因為指針,當數組作為參數傳遞給函數時,只有數組的基址——數組首元素——數組名傳遞給函數,作為函數局部棧的地址,這個名字存放了實際數組的地址,所以即便是想象的“值賦值”(當然不能,原因如上),內容也是一樣的。如果選擇在函數范圍內定義的局部數組中的一個元素,那麼把偏移量+基地址一樣會在實際數組中操作,最終結果是函數聲明中定義的形參與函數調用時使用的數組實參是相同的一塊內存,而不是一個拷貝。