作者:乾坤一笑
來(lái)源:http://blog.vckbase.com/smileonce/archive/2005/06/26/8330.html
摘要:介紹了C語(yǔ)言中的char *和字符串,比較深入
前幾天和清風(fēng)雨交談strncpy()函數的時(shí)候,他認為“如果一個(gè)函數有一個(gè)char * str的參數,那么str一定是一個(gè)字符串”,而我對此不以為然。難得到了周末,抽得出功夫,談?wù)勎覍har *含義的認識,與大家共同討論一下。
C語(yǔ)言是開(kāi)發(fā)操作系統的首選語(yǔ)言,在很多方面都能從C代碼中看到機器的固有性質(zhì)。比如,PASCAL語(yǔ)言的數組索引是從1開(kāi)始計數,而C語(yǔ)言就是從0開(kāi)始計數。從0開(kāi)始計數不太符合一般人的正常思維模式,但是為什么C語(yǔ)言要采用這種設計方式呢?因為C語(yǔ)言最初主要是為操作系統開(kāi)發(fā)人員和編譯器設計人員設計的——對于經(jīng)常需要考察內存地址的開(kāi)發(fā)人員,偏移量的概念在他們腦子里面根深蒂固。要把100個(gè)int型的整數放在從地址0x0000開(kāi)始的一段內存中,如果系統是按Byte編址的,那么第100個(gè)元素(其數組索引為99)的要放入的地址必然是:0x0000 + sizeof(int) * 99, 而不是乘以100,所以索引以0開(kāi)始是很有好處的。(C語(yǔ)言的底層特性請參考P.V.D.L的《Expert C Programming——Deep C Secrets》,中文版《C專(zhuān)家編程》)
同樣而言,C語(yǔ)言對所謂字符串的處理也和其他語(yǔ)言不同。(參見(jiàn)拙著(zhù)《我用錯了strcat() 》文后的評論) 它同樣體現了便于系統設計的特點(diǎn)。例如unix系統總是把設備都映射為文件,對I/O流、光驅、硬盤(pán)、modem的訪(fǎng)問(wèn)最終都轉換為了對文件的處理。而文件,也可以看作是一個(gè)長(cháng)長(cháng)的字符數組(文件以EOF結尾)。C語(yǔ)言沒(méi)有專(zhuān)門(mén)定義字符串數據類(lèi)型(如其他語(yǔ)言中的string),它用以‘\0‘結尾的字符數組來(lái)表示一個(gè)邏輯意義上的字符串。C語(yǔ)言用一些對于字符數組的處理函數(特別是對以‘\0‘結尾的字符數組的處理函數)來(lái)處理所謂的以‘\0‘結尾的字符串,并把他們放在string.*、stdio.*等一些標準庫文件中。string這個(gè)字眼也就對人造成了某些誤解,好像是C語(yǔ)言中定義了字符串這種類(lèi)型一樣。其實(shí)C語(yǔ)言只定義了char、int、float、void、poiner這幾種基本類(lèi)型,這正是C語(yǔ)言簡(jiǎn)潔之處。至于所謂的字符串,只是對字符數組的一種特殊應用而已。
由于沒(méi)有了“字符串類(lèi)型”,“傳char *參數就是傳一個(gè)字符串進(jìn)來(lái)”之說(shuō)自然也不攻自破。那么char *真正的含義是什么?我們不妨用大家最熟悉的int來(lái)對比一下。
#include <stdio.h>int main(int argc, char *argv[]){int b[3] = { 17, 18, 19 };int a = 5;int d = 2103157716;int * i; // pay attention to this variable(注意:這個(gè)指針 i)i = b;printf("%d \n", *i); // i points to a array, *i gets the first number of the array,
// i 是指向一個(gè)數組的 *i 是這個(gè)數組的第一個(gè)元素// *i does not get the all elements of the array. (注: *i 不是指向所有的元素的)i = b + 1; // okey, i can get whichever element of the array.
// 好了, i 得到了那個(gè)數組的第二個(gè)元素了(想想是不是呢) printf("%d \n", *i);printf("Int size: %d \n", sizeof(int));printf("Address0: %#x \n", i); // if i++, the value of i add by sizeof(int) i++;printf("Address1: %#x \n", i);i = &a; // i also can point to a int variable. as i is a pointer, we shoule printf("%d \n", *i); // use & to get the address of a.printf("%#x \n", d); // I use a big number to prove that int * is only a pointer, i = &d; // which is not tightness to int type. printf("%#x \n", *((unsigned char*)i));return 0;}代碼中的int * i就是我們關(guān)注的焦點(diǎn)。它是一個(gè)指向int指針。也就是說(shuō):i指向一個(gè)內存地址,從這個(gè)地址開(kāi)始存儲了一個(gè)數據。int * i中的int標明應該使用int類(lèi)型(長(cháng)度為sizeof(int)個(gè)字節)來(lái)從這個(gè)地址取數據,也就是說(shuō)要一次取sizeof(int)個(gè)byte的數據來(lái)拼成最后的結果。最后一個(gè)例子也證明了這一點(diǎn):如果我們強制用unsigned char的大小的數據類(lèi)型來(lái)對這個(gè)地址操作,就只能取出數據的一部分。反過(guò)來(lái)說(shuō),如果用較大數據類(lèi)型來(lái)取實(shí)際存儲較小數據類(lèi)型的數據,就有可能越界操作內存,取回一些雜亂的內容或導致系統崩潰。int b[]這個(gè)數組,標明有一組數,放在以&b開(kāi)始地址的內存空間內,每個(gè)元素占用了sizeof(int)個(gè)byte的內存單元;如果有類(lèi)似于i=&b;i++;的操作,i的值就每次遞增sizeof(int)而不是1,這樣確保了i每次都能恰好取回一個(gè)正確的int。
同理,char * c也是如此。如果我們定義了一個(gè)char *的變量c,那么c也只不過(guò)是一個(gè)指向內存中某個(gè)地址的指針而已。之所以標明它是char *的類(lèi)型,就是說(shuō)要以sizeof(char)為單位去內存中取數。所以,我們應該稱(chēng)呼char * c為指向char類(lèi)型的指針——而不是說(shuō)c就是字符串。為什么傳一個(gè)char *指針給printf(),strlen()之類(lèi)的函數,它就能把它當作一個(gè)字符串來(lái)處理呢?沒(méi)錯,我們不是定義了‘\0‘來(lái)表示一個(gè)"字符串"的終結么?我們只需從起始地址不斷累加,遍歷字符數組的每一個(gè)元素,直到找到一個(gè)‘\0‘為止,就算是處理一個(gè)字符串了——從起始地址到‘\0‘為止的字符數組元素構成一個(gè)“字符串”,這就是C語(yǔ)言設計字符串的原理。
所以,當一個(gè)函數要求傳入一個(gè)char *的參數,并不一定這個(gè)參數就一定是字符串(以‘\0‘結尾的字符數組),char *只是一個(gè)字符指針而已,它僅僅提供了一個(gè)內存地址和每次遍歷元素的偏移量而已。究竟函數對傳入的參數有什么要求,還要視函數的具體實(shí)現而定。(我想ANSI C應該對參數有所要求和規定,可惜偶沒(méi)有ANSI C文件,無(wú)法參考。 )C語(yǔ)言一般約定是用char * str來(lái)表示以‘\0‘結尾的字符數組,但是由于某些實(shí)現上的效率的考慮,往往沒(méi)有嚴格遵守這個(gè)約定。C語(yǔ)言的設計理念中沒(méi)有強調使用者一定要使用遵守這個(gè)約定,不遵守這個(gè)約定也不違背C語(yǔ)言的基本語(yǔ)法規則。這或許可以看作是C語(yǔ)言和創(chuàng )造和使用它的黑客群體崇尚自由的一種特色、一種精神文化吧。
小練習:
1)參照上面int *的例子來(lái)編寫(xiě)一個(gè)類(lèi)似代碼驗證一下char *是否也有類(lèi)似特性。
2)C語(yǔ)言這么設計字符串會(huì )在那些方面的處理上有為難之處。
3)考察C語(yǔ)言標準庫函數中,有那些函數傳入char*的參數是一定要求以‘\0‘結尾的,那些函數對char *參數不做這個(gè)檢查。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=670277
聯(lián)系客服