1. 程式人生 > >【C++ Primer每日一刷之八】之八 C 風格字串

【C++ Primer每日一刷之八】之八 C 風格字串

4.3 C 風格字串

儘管 C++ 支援 C 風格字串,但不應該在 C++ 程式中使用這個型別。C 風格字串常常帶來許多錯誤,是導致大量安全問題的根源。

在前面我們第一次使用了字串字面值,並瞭解字串字面值的型別是字元常量的陣列,現在可以更明確地認識到:字串字面值的型別就是const char 型別的陣列。C++ 從 C 語言繼承下來的一種通用結構是C 風格字串,而字串字面值就是該型別的例項。實際上,C 風格字串既不能確切地歸結為 C 語言的型別,也不能歸結為 C++ 語言的型別,而是以空字元 null 結束的字元陣列:

char ca1[] = {'C', '+', '+'}; // no null,not C-style

string

char ca2[] = {'C', '+', '+', '\0'}; //explicit null

char ca3[] = "C++"; // nullterminator added automatically

const char *cp = "C++"; // nullterminator added automatically

char *cp1 = ca1; // points to first elementof a array, but not C-style string

char *cp2 = ca2; // points to first elementof a null-terminated char array

ca1 和 cp1 都不是 C 風格字串:ca1 是一個不帶結束符 null 的字元陣列,而指標 cp1 指向 ca1,因此,它指向的並不是以 null 結束的陣列。其他的宣告則都是 C 風格字串,陣列的名字即是指向該陣列第一個元素的指標。於是,ca2 和 ca3 分別是指向各自陣列第一個元素的指標。

C 風格字串的使用

C++ 語言通過(const)char*型別的指標來操縱 C 風格字串。一般來說,我們使用指標的算術操作來遍歷 C 風格字串,每次對指標進行測試並遞增 1,直到到達結束符 null 為止:

const char *cp = "some value";

while (*cp)

 {

// do something to *cp

++cp;

}

while 語句的迴圈條件是對const char* 型別的指標 cp 進行解引用,並判斷cp 當前指向的字元是 true 值還是 false 值。真值表明這是除 null 外的任意字元,則繼續迴圈直到 cp 指向結束字元陣列的 null 時,迴圈結束。

while 迴圈體做完必要的處理後,cp加1,向下移動指標指向陣列中的下一個字元。

如果 cp 所指向的字元陣列沒有null 結束符,則此迴圈將會失敗。這時,迴圈會從 cp 指向的位置開始讀數,直到遇到記憶體中某處 null 結束符為止。

C 風格字串的標準庫函式

表列出了 C 語言標準庫提供的一系列處理 C 風格字串的庫函式。要

使用這些標準庫函式,必須包含相應的 C 標頭檔案:

cstring 是 string.h 標頭檔案的 C++ 版本,而 string.h 則是 C 語言提供

的標準庫。

這些標準庫函式不會檢查其字串引數。

表 4.1. 操縱 C 風格字串的標準庫函式

strlen(s) 返回 s 的長度,不包括字串結束符 null

strcmp(s1, s2) 比較兩個字串 s1 和 s2 是否相同。若 s1 與 s2 相等,返

回 0;若 s1 大於 s2,返回正數;若 s1 小於 s2,則返回負

strcat(s1, s2) 將字串s2 連線到 s1 後,並返回 s1

strcpy(s1, s2) 將 s2複製給 s1,並返回 s1

strncat(s1,s2,n) 將s2 的前 n 個字元連線到 s1 後面,並返回 s1

strncpy(s1,s2, n) 將s2 的前 n 個字元複製給 s1,並返回 s1

#include <cstring>

傳遞給這些標準庫函式例程的指標必須具有非零值,並且指向以 null 結束的字元陣列中的第一個元素。其中一些標準庫函式會修改傳遞給它的字串,這些函式將假定它們所修改的字串具有足夠大的空間接收本函式新生成的字元,程式設計師必須確保目標字串必須足夠大。

C++ 語言提供普通的關係操作符實現標準庫型別 string 的物件的比較。這些操作符也可用於比較指向C 風格字串的指標,但效果卻很不相同:實際上,此時比較的是指標上存放的地址值,而並非它們所指向的字串:

if (cp1 < cp2) // compares addresses,not the values pointed to

如果 cp1 和 cp2 指向同一陣列中的元素(或該陣列的溢位位置),上述表

達式等效於比較在 cp1 和 cp2 中存放的地址;如果這兩個指標指向不同的數

組,則該表示式實現的比較沒有定義。

字串的比較和比較結果的解釋都須使用標準庫函式 strcmp 進行:

const char *cp1 = "A stringexample";

const char *cp2 = "A differentstring";

int i = strcmp(cp1, cp2); // i is positive

i = strcmp(cp2, cp1); // i is negative

i = strcmp(cp1, cp1); // i is zero

標準庫函式 strcmp 有 3 種可能的返回值:若兩個字串相等,則返回 0

值;若第一個字串大於第二個字串,則返回正數,否則返回負數。

永遠不要忘記字串結束符 null

在使用處理 C 風格字串的標準庫函式時,牢記字串必須以結束符 null

結束:

char ca[] = {'C', '+', '+'}; // notnull-terminated

cout << strlen(ca) << endl; //disaster: ca isn't

null-terminated

在這個例題中,ca 是一個沒有 null 結束符的字元陣列,則計算的結果不

可預料。標準庫函式 strlen 總是假定其引數字串以 null 字元結束,當呼叫

該標準庫函式時,系統將會從實參 ca 指向的記憶體空間開始一直搜尋結束符,直

到恰好遇到 null 為止。strlen 返回這一段記憶體空間中總共有多少個字元,無

論如何這個數值不可能是正確的。

呼叫者必須確保目標字串具有足夠的大小

傳遞給標準庫函式 strcat 和strcpy 的第一個實引數組必須具有足夠大

的空間存放新生成的字串。以下程式碼雖然演示了一種通常的用法,但是卻有潛

在的嚴重錯誤:

// Dangerous: What happens if wemiscalculate the size of largeStr?

char largeStr[16 + 18 + 2]; // will holdcp1 a space and cp2

strcpy(largeStr, cp1); // copies cp1 intolargeStr

strcat(largeStr, " "); // adds aspace at end of largeStr

strcat(largeStr, cp2); // concatenates cp2to largeStr

// prints A string example A differentstring

cout << largeStr << endl;

問題在於我們經常會算錯 largeStr 需要的大小。同樣地,如果 cp1 或 cp2所指向的字串大小發生了變化,largeStr 所需要的大小則會計算錯誤。不幸的是,類似於上述程式碼的程式應用非常廣泛,這類程式往往容易出錯,並導致嚴重的安全漏洞。

使用strn 函式處理 C 風格字串

如果必須使用 C 風格字串,則使用標準庫函式 strncat 和 strncpy 比

strcat和 strcpy 函式更安全

char largeStr[16 + 18 + 2]; // to hold cp1a space and cp2

strncpy(largeStr, cp1, 17); // size to copyincludes the null

strncat(largeStr, " ", 2); //pedantic, but a good habit

strncat(largeStr, cp2, 19); // adds at most18 characters, plus a null

使用標準庫函式 strncat 和strncpy 的訣竅在於可以適當地控制複製字元的個數。特別是在複製和串連字串時,一定要時刻記住算上結束符null。在定義字串時要切記預留存放 null 字元的空間,因為每次呼叫標準庫函式後都必須以此結束字串 largeStr。讓我們詳細分析一下這些標準庫函式的呼叫:

• 呼叫 strncpy 時,要求複製17 個字元:字串 cp1 中所有字元,加上結束符 null。留下儲存結束符 null 的空間是必要的,這樣 largeStr 才可以正確地結束。呼叫 strncpy 後,字串 largeStr 的長度 strlen 值是 16。

記住:標準庫函式 strlen 用於計算 C 風格字串中的字元個數,不包括 null 結束符。

• 呼叫 strncat 時,要求複製2 個字元:一個空格和結束該字串字面值的 null。呼叫結束後,字串 largeStr 的長度是 17,原來用於結束largeStr 的 null 被新新增的空格覆蓋了,然後在空格後面寫入新的結束符 null。

• 第二次呼叫 strncat 串接cp2 時,要求複製 cp2 中所有字元,包括字串結束符null。呼叫結束後,字串 largeStr 的長度是 35:cp1 的16 個字元和 cp2 的 18 個字元,再加上分隔這兩個字串的一個空格。

整個過程中,儲存 largeStr 的陣列大小始終保持為 36(包括結束符)。只要可以正確計算出 size 實參的值,使用 strn 版本要比沒有 size 引數的簡化版本更安全。但是,如果要向目標陣列複製或串接比其 size 更多的字元,陣列溢位的現象仍然會發生。如果要複製或串接的字串比實際要複製或串接的size 大,我們會不經意地把新生成的字串截短了。截短字串比陣列溢位要安全,但這仍是錯誤的。

儘可能使用標準庫型別string

如果使用 C++ 標準庫型別string,則不存在上述問題:

string largeStr = cp1; // initialize largeStr as a copy of cp1

largeStr += " "; // add space atend of largeStr

largeStr += cp2; // concatenate cp2 ontoend of largeStr

此時,標準庫負責處理所有的記憶體管理問題,我們不必再擔心每一次修改字串時涉及到的大小問題。對大部分的應用而言,使用標準庫型別 string,除了增強安全性外,效率也提高了,因此應該儘量避免使用 C 風格字串。