1. 程式人生 > >[轉]C語言之精華總結!

[轉]C語言之精華總結!

... /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)之間!
你可以