字串觀念

我想寫過其它語言如C#, Java這樣語言的人都用過String這個dataType但是在C語言這種比較低階一點的語言是沒有所謂「字串」這種東西的,而它在C語言裡面又是非常重要的一環,因此許多初學者在C中使用字串雖然是會用,但是不了解背後真正C字串的意義,因此想分享一些我對C style String的見解。

※ 字串在C中的意義

在C語言之中,只有字元,並沒有所謂的字串,故字串稱為字元陣列,只要是陣列中一排英文字的後面有個'\0'的字元就稱為「字串」

Example:

char str[] = {'a', 'b', 'c', '\0'}; (str為一字串,其字串為abc)

在C中字串可分為兩種型式(非常重要)

Example:

const char* str = "test1"; (指標字串)
char str[] = "test2"; (陣列字串)

※ 字串常數

所謂的字串常數是在你程式中只要有寫到"abc"等等的字串表示如此稱為字串常數。

Example:

#define MAX "max";
const char* str = "str";
printf("hello world!!");

只要用""裝起來的就是字串常數

但是其中有一點是例外的,就是程式中用字串常數初始一個字元陣列的時候。

Example:

char str[] = "hello"; (使用字串常數初始一個字串陣列)
char str[] = {'h', 'e', 'l', 'l', 'o','\0'};

以上兩種表示方式意思是完全相同的,只是寫"hello world"會比較方便,會在使用compiler幫你編譯的時候自動轉換。而在字串常數中有幾個重點。

* 字串常數一旦建立,在程式執行中不可被修改
* 不可以透過指標解參考(dereference)修改某個字元
* 生命週期和程式共存亡
* 指向字串常數的指標,如果被指向另一個字串常數或是字串陣列,或是任何的記憶體位址後,就永遠沒有辦法在指向本來的字串常數,因為已丟失字串常數的位址

※ 字串陣列

只要是使用陣列來存字元,而且後面加了個'\0'的字元的,在C中就認定是字串。

Example:

char str[] = {'a','b','c','\0'};
char str[] = "abc";

字串陣列就如同一般陣列一樣可以修改其內容值,可透過指標解參考(dereference)修改或是透過下標運算子([])來修改其中的某個字元。而在字串陣列中有幾個重點。

* 看過陣列篇的都知道陣列名字是代表陣列的起始位址,而且是不可以被修改的,因此想要對str做遞增、遞減運算都不行,因為它不是一個指標
* 可以透過指標或是下標運算來修改其中某個字元,但是不可以把空字元'\0'丟失,否則字串陣列就成為單單只是存字元的陣列而已,不具有字串功能
* 生命週期和宣告在那裡有關

※ 字串常數和字串陣列的不同之處(非常重要)

Example:

char str[] = "test1";
char* str = "test1";

以上兩行程式,是否可以分辨出「字串常數」和「字串陣列」,如果分不出來,可以到前面重看了,如果分的出來,那麼他們到底有什麼不同呢?

儲存的空間不同

char str[] = "test1";就像陣列一樣,陣列宣告在那裡,就有那個地方的儲存性質,寫在全域,就是一個靜態陣列,會和程式共存亡,但是如果寫在main()裡面的話就是自動變數,所以當程式離開main()的時候,這個字串陣列就會消失,完全是看你在什麼地方宣告它而定,就如同使用一般陣列一樣。

char* str = "test1"; 在前面有提過test1稱為字串常數,它是在compiler的階段會在Global區段就建立好一個存放test1這個資料的空間,所以程式執行時,不論你在任何地方取得這個字串都是有效的,因為它會和程式共存亡,所以有一個很重要的一點就是「只可以讀,不可以修改」字串內容。

型別的不同

char str[] = "test1";解析str表示陣列的起始位址,那str的型別為char*,看過函式傳遞那篇的人應該很清楚。char*表示著,指向的指標,可讀可寫,可移動。

char* str = "test1";解析str表示恉向Global中test1字串的資料區段起始位址,但是由於我們前面說過,它是「只可以讀,不可以修改」,因此我們要修正這個指標為const char*,如此才完全正確。const char*表示著,指向的指標,只可以讀,不可以寫,指標可移動。

※ 字串常數和字串陣列的相同之處(非常重要)

* 兩者都可以透過*(string+i)或是string[i]來取得字串中的某個字元
* 不論是字串常數或是字串陣列,最後一個字元一定是要空字元'\0'

※ 使用sizeof運算子計算大小時的錯誤

初學者一定想過使用sizeof來計算字串大小,因為這樣可以把連'\0'這個空字元都可以算進去,如此就可以知道字串共用了多少個字元了。

Example:

char str[] = "hello world";
printf("%d\n", sizeof(str));

如此可以達成你的目的,可以知道str這個字串包含空字元一共有多少個字元,但是如下範例。

Example:

const char* str = "hello world";
printf("%d\n", sizeof(str));

算出來的結果為什麼等於4呢?而且不管怎麼算就是4,其實因為你是算了它的指標大小,而不是真的算到共用了多少的空間,指標在win32系統上都是 4btye所以不管是什麼指標使用sizeof()運算出來都是4,因此想要知道字串常數共有多個字元的話,請使用strlen(str)+1加1是為了空字元,同樣的也適用於字元陣列。

※ 字串陣列溢出

Example:

char str[3] = "abc";
char buffer_str[5];
scanf("%s", buffer_str); (輸入hello world)

由此範立一看就知道是錯的,為什麼呢?因為我們的字串超過字串陣列,可以容納的值,你和記憶體要的空間不夠放你的資料會造成「改到其它的資料」,C 是個很低階的語言,在程式中要了3個char的空間,但是你卻放了abc還有空字元,所以那個空字元會覆寫記憶體中其它變數的區段,造成程式不穩定或是當掉,而buffer的情況也是一樣buffer不夠使用者的輸入,會當成記憶體被新輸入的資料覆寫,所以我們的scanf()需要看buffer的大小來定訂可輸入的資料大小,要改寫成。

scanf("%4s", buffer_str);

為是什麼是4不是5呢?原來輸入的字串需要含有'\0'這個空字元的空間,所以字串的邊界條件是很重要的,如此別人資料輸入在多也沒有用,因為只會讀入4個字元,和自動加上去的空字元,剛剛好符合我們訂的大小。

當一個C語言的程式設計師,要非常的細心,因為指標之所以不穩定,就是因為邊界計算錯誤,會存取或是覆寫到別的記憶體空間,但是如果可以了解觀念後善用的話,C語言是個非常快速和低階的語言,因為可以直接存取記憶體,因此在C中所有的邊界判斷都要自己來,這是辛苦的地方。

※ 字串陣列和字串常數的使用時機

字串陣列

* 當成用scanf讀入字串的buffer來使用,通常buffer我們會設大一點,必避資料輸入超過會造成程式當掉

字串常數

* 當成輸出文字來使用,比如說要出印一個選單,就使用字串常數

※ 字串的輸出

Example:

const char* str1 = "test"; (字串常數)
char str2[] = "test"; (字串陣列)
printf("%s\n", str1);
printf("%s\n", str2);

這樣就可以輸出字串了,但是有沒有想過一個問題就是,明明str1是個指標,而且str2也是一個常數指標,那為什麼本來輸出應該是一個地址才對阿,為什麼是輸出一個字串呢?原因有兩點,一點是「%s」另一點是「'\0'」,在printf()中%s它就會把這個地址視為要印出字串,所以他就會不斷的使用*str++把資料印出來,直遇到\0為止,因此我們觀念可以連貫起來,為什麼字串最重要的就是要有'\0'這個空字元的關係了,所以空字元是整個字串的命脈。

※ 字串的輸入

Example:

char buffer1[];
char* buffer2;
scanf("%s%s", buffer1, buffer2);

有人一定會這樣寫,為的是希望讓程式輸入資料的時候,看資料大小來設定陣大小,或是建立字串常數,這想個想法雖然是很好,但是程式有分成編譯階段和執行階段,現在的情況是在執行階段,但是空間配置是在編譯階段,而我們在編譯階段並沒有和系統要多大的空間來暫存,而在執行期的時候,又要把資料放到「不存在」的資料空間,這樣程式一跑起來就可能會當掉,因為根本沒有配置空間,如果想要在執行期配置空間的話,需要用動態記憶體配置malloc來實作。

因此最重要的部份就是要讓一個buffer1可以存資料的話,一定要預先配好足夠的大小,而buffer2只是個指標,是個完全的錯誤,在此就不討論了,因此範例應該改成。

Example:

char buffer1[80];
scanf("%79s", buffer1);

配置了80個字元的空間,還需要使用%79s才算完全正確,為了怕使用者輸入超過80個字元造成記憶體可能會被覆寫,所以要限定使用者輸入後可取入的大小。

※ 字串的陣列

這裡談到的是字串「的」陣列,和字串陣列(用字元陣列接成的字串)意思不同,請別搞混,所謂的字串的陣列,是一個陣列中裝了很多常數字串。

常數字串的陣列

Example:

const char* str_array[] = {
"today",
"yesterday",
"tomorrow"
};

str_array陣列之中放著3個字串常數

字串陣列的陣列

Example:

char str_array[][20] = {
"today",
"yesterday",
"tomorrow"
};

str_array陣列之中放著3個字串陣列

對於字串的陣列部份,就是字串+陣列,所以如果了解陣列的話,那兩者配合起來沒有什麼問題,兩三分鐘就可以完全了解了,但是如果看不太懂以上的寫法的話,那陣列的部份可能就要仔細的去看看觀念了。

0 意見: