一.指針。
它的本質是地址的類型。在許多語言中根本就沒有這個概念。但是它卻正是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;//不合法,編譯不會通過。
也許有人會奇怪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;/
int offset;
} 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大學心理學教授麥克.侯威專門研究神童和天才,他的結論很有意思:「一般人以為天才是自然而生、流暢而不受阻的閃亮才華,其實,天才也必須耗費至少十年光陰來學習他們的特殊技能,絕無例外。
要成為專家,需要擁有頑固的個性和堅持的能力……每一行的專業人士,都投注大量心血,培養自己的專業才能。」