C語言字符串處理的驚天大坑問題解決
引言
毋庸置疑,在使用 C 字符串時必須小心,否則你就會因為各種的未定義行為而感到頭疼。
最近,我一直在學(xué)習(xí) C 語言,也因此領(lǐng)教了低級編程所涉及的復(fù)雜性。作為一名數(shù)據(jù)科學(xué)家或者是 Python 程序員,我一直在與字符串打交道。有人說,C 語言中的字符串處理非常糟糕。我很好奇,所以想一探究竟。
C 語言字符串
C 語言的字符串是以空終止符 \0 結(jié)尾的字符數(shù)組。在 C 語言操作字符串時,空終止符會告訴函數(shù)已到達字符串的末尾。在 C 中,我們可以通過兩種不同的方式聲明一個字符串。
第一種也是最困難的方法是定義字符數(shù)組。
#include <stdio.h> int main() { char myString[] = {'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd','!','\n','\0'}; printf("%s", myString); return 0; }
這種方式易出錯,且需要手動插入空終止符。如果單詞很長,鍵入的時間也會很長。
第二種方式是用雙引號括起來的字符串。
#include <stdio.h> int main() { char myString[] = "Hello, World!\n"; printf("%s", myString); return 0; }
在這種情況下,C 知道字符串的長度,就可以自動插入空終止符。
字符串操作
正確創(chuàng)建字符串之后,你就可以執(zhí)行許多操作了。常用的字符串操作函數(shù)包括 strcpy、strlen 和 strcmp。
●strcpy:將存儲在一個變量中的字符串復(fù)制到另一個變量中。
●strlen:獲取字符串的長度(不包括空終止符)。
●strcmp:用于比較兩個字符串并根據(jù)比較結(jié)果返回整數(shù)?;拘问綖?strcmp(str1,str2),若 str1=str2,則返回零;若 str1<str2,則返回負(fù)數(shù);若 str1>str2,則返回正數(shù)。
然而壞消息是,每個字符串函數(shù)的使用都有細微的差別。首先,我們來看一個 strcpy 的示例。
int main() { char source[] = "Hello, world!"; char destination[20]; strcpy(destination, source); // Copy the source string to the destination string printf("Source: %s\n", source); printf("Destination: %s\n", destination); return 0; }
輸出結(jié)果如下:
Source: Hello, world!
Destination: Hello, world!
如你所料,strcpy 的作用就是復(fù)制一個字符串并將其內(nèi)容放入另一個字符串中。但你可能會問:“為什么我不能直接將源變量分配給目標(biāo)變量?”
int main() { char source[] = "Hello, world!"; char* destination = source; strcpy(destination, source); // Copy the source string to the destination string printf("Source: %s\n", source); printf("Destination: %s\n", destination); return 0; }
事實上,這樣也未嘗不可。只不過現(xiàn)在 destination 變成了 char*,而且是作為指向源字符數(shù)組的指針存在。
下一個字符串操作是 strlen,它的作用是獲取字符串的大小,但不包括空終止符。
#include <stdio.h> #include <string.h> int main() { char str[] = "Hello, world!"; // The string to find the length of int length = strlen(str); // Find the length of the string printf("The length of the string '%s' is %d.\n", str, length); return 0; }
輸出結(jié)果如下:
The length of the string 'Hello, world!' is 13.
這個函數(shù)很簡單,就是統(tǒng)計字符數(shù)量,直到遇到空終止符。
我們的最后一個函數(shù)是 strcmp,它的作用是比較兩個字符串,看看它們是否相等。如果相等,則返回 0;若 str1<str2,則返回負(fù)數(shù);若 str1>str2,則返回正數(shù)。
#include <stdio.h> #include <string.h> int main() { char str1[] = "Hello, world!"; char str2[] = "hello, world!"; int result = strcmp(str1, str2); // Compare the two strings if (result == 0) { printf("The strings are equal.\n"); } else { printf("The %s is not equal to %s\n", str1, str2); } return 0; }
輸出結(jié)果如下:
The strings are not equal
我們了解了如何復(fù)制字符串、獲取字符串的長度,以及如何比較字符串,下面我們來看一些難點。
上述這些函數(shù)沒有一個是安全的操作,而且很容易產(chǎn)生未定義的行為。根源在于使用 \0 作為空終止符。對于上述 C 函數(shù)以及其他函數(shù),C 希望找到一個 \0,然后告訴函數(shù)停止讀取字符串所在的內(nèi)存區(qū)域。但是如果沒有空終止符呢?在字符串應(yīng)該結(jié)束后,C 會繼續(xù)讀取內(nèi)存中的內(nèi)容。如果我們的程序函數(shù)需要驗證用戶提供的密碼,那么不法分子可能會利用字符串的緩沖區(qū)溢出,跳過檢查密碼的內(nèi)存區(qū)域,直接調(diào)用獲取密碼的函數(shù)。這樣就可以避開授權(quán)。
那么,我們應(yīng)當(dāng)如何處理呢?
保證 C 代碼的安全性
四處搜尋,你可能會發(fā)現(xiàn)一個名為 strncpy 的函數(shù)。查看定義,你會發(fā)現(xiàn)這個函數(shù)可以將源字符串復(fù)制到目標(biāo)字符串中,并允許指定復(fù)制的字節(jié)數(shù)。你可能會說:“這個函數(shù)看起來很完美!”我可以確保目標(biāo)字符串只接收它可以處理的字節(jié)數(shù)。下面的代碼展示了這個函數(shù)的用法及其輸出。
#include <stdio.h> #include <string.h> #define dest_size 12 int main(){ char source[] = "Hello, World!"; char dest[dest_size]; // Copy at most 12 characters from source to dest strncpy(dest, source, dest_size); printf("Source string: %s\n", source); printf("Destination string: %s\n", dest); return 0; } Source string: Hello, World! Destination string: Hello, World
初看之下還不錯,但還是有問題。如果源字符串的長度減去空終止符的長度后正好等于目標(biāo)字符串的長度,結(jié)果會怎樣?
答案是目標(biāo)字符串會被源字符串的所有字符填滿,沒有空間留給空終止符。一個沒有非 null 終止的字符串勢必會引發(fā)各種令你頭疼的問題。你可能會說,但至少它可以處理源字符串小于目標(biāo)字符串的情況。是嗎?沒錯,它確實可以處理這種情況,但 strcpy 也可以。如果源字符串的長度小于目標(biāo)字符串,那么目標(biāo)字符串中所有未使用的額外空間仍將保留,而且會被填充。因此,假設(shè)目標(biāo)字符串的長度為 20 個字符,但源字符串只有 13 個字符,那么實際上你得到的是一個像下面這樣的目標(biāo)字符串。
char destination[20] = {'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0', '\0', '\0', '\0', '\0', '\0', '\0'};
這個字符串沒有正確的空終止,而且還有一大堆填充字符。情況不太妙。如果你碰巧在 Windows 上使用 strncpy 函數(shù),那么 Microsoft Visual C++ 甚至都編譯不過去。你必須手動設(shè)置一個標(biāo)志,允許使用已棄用的功能,當(dāng)然我們不應(yīng)該使用已棄用的功能。
編譯器建議改用 strncpy_s。我們來看看,strncpy_s 接受這些參數(shù):
●char *restrict dest:目標(biāo)字符串。
●rsize_t destsz:目標(biāo)字符串的大小。
●const char *restrict src:要復(fù)制的源字符串。
●rsize_t count:從源字符串復(fù)制的最大字節(jié)數(shù)。
如果目標(biāo)字符串的長度大于源字符串,那么復(fù)制可以順利進行。但如果目標(biāo)字符串的長度小于源字符串,則只復(fù)制目標(biāo) -1 的大小。strncpy_s 進行的額外檢查是確保將源字符串復(fù)制到目標(biāo)字符串中,并且生成的字符串始終以 null 結(jié)尾。這很好,但是我們又遇到了兩個問題。
●strncpy_s 不會處理額外的填充字符。
●strncpy_s 不可移植到 macOS 或 Linux。
看到這里,你是不是拳頭都硬了,想問一問已經(jīng)過去 34 年了,為什么 C 標(biāo)準(zhǔn)委員會還是沒能沒有提供可移植且更安全的字符串操作?
那么,我們應(yīng)該如何安全地應(yīng)對這種情況呢?我想到了幾種方法。
1.如果已知字符串的長度,就像我們?nèi)藶樵O(shè)計的示例一樣,那么只需將目標(biāo)字符串初始化為 sizeof() 源字符串。
2.你可以直接使用指向源字符串的指針,并完全放棄復(fù)制。只要源字符串有正確的終止符,你就不會遇到緩沖區(qū)大小不匹配的情況。
3.你可以放棄可移植性,在 Windows 上使用 _s 版本的字符串函數(shù),或在 macOS 上使用“ l ”版本。
4.你可以選用其他語言。
至此,你可能已經(jīng)注意到我花了很多時間談?wù)?strcpy,而 strcmp 和 strlen 只是一筆帶過。實際上,這兩個函數(shù)也會遇到由于 C 的字符串終止方式引發(fā)的相同問題。由于字符串的長度在遇到空終止符之前是未知的,所以你會遇到各種未定義的行為和攻擊向量。這與 C++ 形成了鮮明的對比,C++ 將字符串視為對象,并將字符串的長度和字符數(shù)保存到了一起。這就是人們傾向于用 C++ 編寫 C 的原因之一。
為了在純 C 中正確處理這些問題,你需要認(rèn)真檢查字符串的操作。這些操作很容易出錯,而且隨著程序規(guī)模增加,難度也會上升。這就是我們認(rèn)為 C 不安全的原因之一。
非拉丁語言的處理
Unicode 是計算機文本編碼的重要環(huán)節(jié)。如今文本使用最廣泛的編碼是 UTF-8。C 語言直到版本 C99 才獲得了 Unicode 支持,而且即使你在 C 語言中正確處理 Unicode,也會遇到其他方面的問題。假設(shè)我們需要輸出一些日文字符:
#include <stdio.h> #include <string.h> int main() { printf("有り難う\n"); return 0; }
輸出就會出問題:
這是因為我們沒有按照 Unicode 解釋字符。下面我們來重寫上述代碼:
#include <stdio.h> #include <wchar.h> #include <locale.h> int main() { setlocale(LC_ALL, ""); // Set the locale to the user's default locale wchar_t thankyou[] = L"有り難う"; wprintf(L"Thank You in Japanese is: %ls\n", thankyou); return 0; }
我添加了一個字符串:“Thank You in Japanese is”,仔細觀察下面的屏幕截圖,你就能明白其中的原因。但是輸出結(jié)果依然沒有顯示日文。
檢查 PowerShell 控制臺的編碼,我們發(fā)現(xiàn)它是 ASCII 格式的。我們來試試看修改編碼方式:$OutputEncoding = [System.Text.Encoding]::UTF8。這樣就變成了 UTF-8。但依然不起作用??赡苁且驗樽煮w不支持日文。我快速上網(wǎng)搜索了以下,然后發(fā)現(xiàn) MS Gothic 字體支持日文,所以我修改了字體。
怎么反斜杠(“ \ ”)變成了“ ¥ ”?但如果這樣可以顯示日文的話,我也可以接受。我將一個測試文件夾命名為“有り難う”,以確保 PowerShell 能夠正確顯示文件名。下面,我們來看一看這個文件夾,我們看到文件名可以正常顯示。但即使這樣修改代碼,輸出結(jié)果依然無法顯示漢字字符!我嘗試將語言環(huán)境設(shè)置為 ja_JP.UTF8,但仍然無法輸出日文。繼續(xù)上網(wǎng)搜索,我看到一篇文章討論如何在 Windows Server 20222 上 PowerShell 控制臺中顯示中文、日文以及韓文的文章,其中指出:
默認(rèn)情況下,Windows PowerShell .lnk 快捷方式會被硬編碼為使用“ Consolas ”字體。“ Consolas ”字體不包含中文、日文以及韓文字符的字形,因此無法正確呈現(xiàn)這些字符。將字體更改為“ MS Gothic ”可以解決這個問題,因為“ MS Gothic ”字體擁有漢字字符。
命令提示符(cmd.exe)沒有這個問題,因為 cmd .lnk 快捷方式?jīng)]有指定字體。控制臺會根據(jù)系統(tǒng)語言在運行時選擇正確的字體。
解決方法
該問題很快就能在 Windows 11 和 Windows Server 2022 中得到修復(fù),但不會向后移植到較低版本。
如果想解決這個問題,請使用以下兩種解決方法之一。
雖然文中提到的問題與我遇到的問題略有差別,但似乎默認(rèn)情況下 PowerShell 并不能很好地處理日文字符。我嘗試結(jié)合使用命令提示符與 MS Gothic,但也沒能解決問題。上網(wǎng)搜索的所有結(jié)果表明我的代碼可以在 C 中運行。于是,我將代碼恢復(fù)到了第一版:
#include <stdio.h> #include <wchar.h> #include <locale.h> int main() { setlocale(LC_ALL, ""); // Set the locale to the user's default locale wchar_t thankyou[] = L"有り難う"; wprintf(L"Thank You in Japanese is: %ls\n", thankyou); return 0; }
然后在樹莓派上運行,結(jié)果發(fā)現(xiàn)可以正常工作!
我在 Macbook Pro 上試了一下,也沒有任何問題。我在 Macbook Pro 上啟動 PowerShell,一切依然正常,所以這不是 C 中的一個 bug,但看起來確實是 Windows 在終端中處理非拉丁字符的方式有問題。
下面,我們來看看如何在 C 中正確輸出日文字符,這是我們的最后一個例子。如上所述,我們可以通過 strlen 來獲取字符串的長度。接下來,我們來修改 C 代碼,獲取日文字符串的 strlen,如下所示:
#include <stdio.h> #include <string.h> int main() { printf("The length of the string is %d characters\n", strlen("有り難う")); return 0; }
輸出結(jié)果如下:
The length of the string is 12 characters
改成前面最初版本的輸出,就可以看到這 12 個字符。
你可能已經(jīng)注意到了這個字符串包含 12 個字符,原因是我們將字符串解釋為 ascii。由于每個漢字被編碼成了 4 個字節(jié),因此每個字節(jié)都被解釋為一個單獨的字母,而無法集中到一起形成一個漢字。如果我們給字符串加上前綴“ L ”,將字符串的類型從 char 改為 w_char,然后將函數(shù) strlen 改為 wcslen,代碼如下:
#include <stdio.h> #include <wchar.h> #include <locale.h> int main() { printf("The length of the string is %d characters\n", wcslen(L"有り難う")); return 0; }
輸出結(jié)果如下:
The length of the string is 4 characters
這樣問題就得到了解決!
在本文中,我們探討的 C 語言字符串相關(guān)知識只不過是一些皮毛,我們甚至沒有提及 C11 中引入的 Unicode 文字,例如“ u8 ”、“ u ”和“ U ”。毋庸置疑,在使用 C 字符串時必須小心,否則你就會因為各種的未定義行為而感到頭疼。另一方面,你的代碼還會受到不法分子的攻擊。如果你只有使用垃圾收集編程語言的經(jīng)驗,那么要仔細想一想是否有必要大費周折學(xué)習(xí) C 語言。Python 這類語言提供了很多數(shù)據(jù)科學(xué)領(lǐng)域使用的庫,其中大部分建立在 C 和 C++ 之上。當(dāng)然這些庫也必須有人去編寫,如果你有這方面的知識,幾乎所有語言都有一個 C 外部函數(shù)接口,可以用來提高代碼的運行速度,所以其他語言也能受惠。所以,我們都應(yīng)該學(xué)習(xí)一下 C 語言,但也許不應(yīng)該從字符串開始學(xué)習(xí)。
以上就是C語言字符串處理的驚天大坑?的詳細內(nèi)容,更多關(guān)于C語言字符串處理的資料請關(guān)注腳本之家其它相關(guān)文章!

項目之C++如何實現(xiàn)數(shù)據(jù)庫連接池

C語言實現(xiàn)學(xué)生信息管理系統(tǒng)開發(fā)

C++ throw關(guān)鍵字實現(xiàn)拋出異常和異常規(guī)范