|
藍森林 http://www.lslnet.com 2006年6月6日 10:18
C之詭譎[zt]
CSDN - 文檔中心 - 其他開發語言
標題 C之詭譎(上) wtong(原作)
關鍵字 ANSI C,指針,數組
C之詭譎(上)
從研究生二年紀開始學習計算機也差不多兩年了,一路走來,有很多的收穫,也有不少的遺憾,現在正好有一段閒暇,就想對走過的路留下一些足跡,回憶。每個人都有自己不同的人生,說到這裡,就是程序人生了,歌德在《浮士德》中說過:「如果不曾在悲哀中咀嚼過麵包,不曾在哭泣中等待過明天,這樣的人就不知道你——天的力量。」所以我想記下一些帶給我悲哀,帶給我哭泣的程序人生。其實學習計算機的基礎課程是非常重要的,離散數學,編譯原理,操作系統,形式語言……,如果你認真走過了這些路,在以後的日子你會發現你的路會越走越寬,以前的努力和汗水會不斷的給你靈感,給你支持,給你前進的武器和勇氣。你會發現以後取得的很多成就,不過是朝花夕拾而已!
對於程序語言我喜歡的是C++,它能帶給你別的語言無法給予你的無上的智力快感,當然也會給你一門語言所能給你的魔鬼般的折磨。其實Java,C#,Python語言也非常的不錯,我也極為喜歡。它們都是非常成功的語言,我從來就不願意做某一種語言的盲目信仰者,每種語言都有它成功的地方,失敗的地方,都有它適合的地方,不如意的地方。所以每一次看到評價語言的文章,我看看,但從來不會發言。
C++的前世是C,而且C所留下的神秘以及精簡在C++中是青出於藍而勝於藍!C所帶給人的困惑以及靈活太多,即使一個有幾年經驗的高段C程序員仍然有可能在C語言的小水溝裡翻船。不過其實C語言真的不難,下面我想指出C語言中最神秘而又詭譎多變的四個地方,它們也繼續在C++語言中變幻莫測。
指針,數組,類型的識別,參數可變的函數。
一.指針。
它的本質是地址的類型。在許多語言中根本就沒有這個概念。但是它卻正是C靈活,高效,在面向過程的時代所向披靡的原因所在。因為C的內存模型基本上對應了現在von Neumann(馮·諾伊曼)計算機的機器模型,很好的達到了對機器的映射。不過有些人似乎永遠也不能理解指針【注1】。
注1:Joel Spolsky就是這樣認為的,他認為對指針的理解是一種aptitude,不是通過訓練就可以達到的。http://www.joelonsoftware.com/printerFriendly/articles/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[i];//不合法,編譯不會通過。
也許有人會奇怪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;
(to be continued!)
吳桐寫於2003.5.26
最近修改2003.6.16 |
C之詭譎[zt]
標題 C之詭譎(下) wtong(原作)
關鍵字 C,類型的識別,參數可變的函數
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 = &//賦值(assignment)操作
(*pf)(5, 8.9);//函數調用操作
也請注意,C語言本身提供了一種簡寫方式如下:
pf = fun;// 賦值(assignment)操作
pf(5, 8.9);// 函數調用操作
不過我本人不是很喜歡這種簡寫,它對初學者帶來了比較多的迷惑。
數組聲明一般是這樣int a[5];對於數組指針(pointer to array)的聲明是這樣:
int (*pa)[5]; 你也必須習慣。可以這樣使用:
pa = &// 賦值(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語言中定義了一個頭文件<stdarg.h>;專門用來對付可變參數列表,它包含了一組宏,和一個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 <stdio.h>;
#include <stdarg.h>;
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 <stdarg.h>;
int printf(char* format, …)
{
va_list ap;
va_start(ap, format);
int n = vprintf(format, ap);
va_end(ap);
return n;
}
我還專門找到了VC7.1的頭文件<stdarg.h>;看了一下,發現各個宏的具體實現還是有區別的,跟很多預處理(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語言本身就不用再花更多的精力了。
吳桐寫於2003.5.26
最近修改2003.6.19 |
C之詭譎[zt]
參考 |
C之詭譎[zt]
看看這個程序夠不夠詭秘,大家運行看看
#include <stdio.h>;
main(t,_,a)
char *a;
{
return!0<t?t<3?main(
-79,-13,a+main(
-87,1-_,main(
-86,0,a+1
)+a
)
):1,t<_?main(
t+1,_,a
):3,main(-94,-27+t,a)&&t==2?_<13?
main(2,_+1,"%s %d %d\n"):9:16:t<0?t<-72?main(_,t,
"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#\
;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \
q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# \
){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' \
iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \
;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# \
}'+}##(!!/")
:t<-50?_==*a?putchar(31[a]):main(-65,_,a+1):main((*a=='/')+t,_,a+1)
:0<t?main(2,2,"%s"):*a=='/'||main(0,main(-61,*a,
"!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"),a+1);} |
C之詭譎[zt]
編譯通不過呀,什麼東東? |
C之詭譎[zt]
我第一次貼的時候結尾漏了個},現在已經補上了。 |
C之詭譎[zt]
早看過了,至於tinywind貼的程序是一個老東東,
也早調過了,就是30000多次的遞歸而已,
是個公式,別的只是簡單的c技巧,這個東東能得獎,
主要還是想法,技術一般。
我寫了一個簡單的(沒算法、沒轉意),看了就明白tinywind貼的程序了。
#include <stdio.h>;
main(_,__,___)
char _,___;
{
#define aaa(____,_____) ____##_____##ntf
_==1?(_=0x49):_==0x49?(_=0x20):_==(0x20)?(_=0x6c):
_==0x6c?(_=0x6f):_==0x6f?(_=0x76):_==0x76?
(_=0x65):_==0x65?(_=0x21):_==0x21?(_=0x43):_==0x43?
(_=0x55):(_=0);
(_ >;0 )?aaa(p,ri)("%c", _==0x21?_-1:_), main(_,__,___):getchar();
} |
C之詭譎[zt]
夠詭秘. |
C之詭譎[zt]
這也叫C!
測試機器人? |
C之詭譎[zt]
哪位高手願意講講這個詭異的程序麼? |
C之詭譎[zt]
怎麼還有個臉,呵呵,轉意。
#include <stdio.h>;
main(_,__,___)
char _,___;
{
#define aaa(____,_____) ____##_____##ntf
_==1?(_=0x49):_==0x49?(_=0x20):_==(0x20)?(_=0x6c):
_==0x6c?(_=0x6f):_==0x6f?(_=0x76):_==0x76?
(_=0x65):_==0x65?(_=0x21):_==0x21?(_=0x43):_==0x43?
(_=0x55): (_=0);
(_ >;0 )?aaa(p,ri)("%c", _==0x21?_-1:_), main(_,__,___):getchar();
}
就用了一個param,簡單吧。 |
C之詭譎[zt]
up |
C之詭譎[zt]
不妨把C理解成為一種彙編語言的宏擴展.
:D |
C之詭譎[zt]
8錯 |
C之詭譎[zt]
其實每種語法都是複雜的,因為語言的設計者會考慮到
所有的細節,但用戶(就是程序員拉)使用的範圍卻是
很窄的。因此要弄清楚語言的每個細節是要花費大量的
時間的。經常用的記得牢,不常用的容易忘,是基於大
量工作積累的基礎上的。
除去語言本身,每種編譯器,連接器也是複雜的,要想
搞清楚,也是要有大量不同平台開發的經驗的。 |
C之詭譎[zt]
同意 編軟件 門檻低
向用好就得多干多學了 |
| |