C語言詳解字符串和多字節字符


字符串

簡介

C 語言沒有單獨的字符串類型,字符串被當作字符數組,即char類型的數組。比如,字符串“Hello”是當作數組{'H', 'e', 'l', 'l', 'o'}處理的。
編譯器會給數組分配一段連續內存,所有字符儲存在相鄰的內存單元之中。在字符串結尾,C 語言會自動添加一個全是二進制0的字節,寫作\0字符,表示字符串結束。字符\0不同於字符0,前者的 ASCII 碼是0(二進制形式00000000),后者的 ASCII 碼是48(二進制形式00110000)。所以,字符串“Hello”實際儲存的數組是{'H', 'e', 'l', 'l', 'o', '\0'}
所有字符串的最后一個字符,都是\0。這樣做的好處是,C 語言不需要知道字符串的長度,就可以讀取內存里面的字符串,只要發現有一個字符是\0,那么就知道字符串結束了。

char localString[10];

上面示例聲明了一個10個成員的字符數組,可以當作字符串。由於必須留一個位置給\0,所以最多只能容納9個字符的字符串。
字符串寫成數組的形式,是非常麻煩的。C 語言提供了一種簡寫法,雙引號之中的字符,會被自動視為字符數組。

{'H', 'e', 'l', 'l', 'o', '\0'}
// 等價於
"Hello"

上面兩種字符串的寫法是等價的,內部存儲方式都是一樣的。雙引號里面的字符串,不用自己添加結尾字符\0,C 語言會自動添加。
注意,雙引號里面是字符串,單引號里面是字符,兩者不能互換。如果把Hello放在單引號里面,編譯器會報錯。

// 報錯
'Hello'

另一方面,即使雙引號里面只有一個字符(比如"a"),也依然被處理成字符串(存儲為2個字節),而不是字符'a'(存儲為1個字節)。
如果字符串內部包含雙引號,則該雙引號需要使用反斜杠轉義。

"She replied, \"It does.\""

反斜杠還可以表示其他特殊字符,比如換行符(\n)、制表符(\t)等。

"Hello, world!\n"

如果字符串過長,可以在需要折行的地方,使用反斜杠(\)結尾,將一行拆成多行。

"hello \
world"

上面示例中,第一行尾部的反斜杠,將字符串拆成兩行。
上面這種寫法有一個缺點,就是第二行必須頂格書寫,如果想包含縮進,那么縮進也會被計入字符串。為了解決這個問題,C 語言允許合並多個字符串字面量,只要這些字符串之間沒有間隔,或者只有空格,C 語言會將它們自動合並。

char greeting[50] = "Hello, ""how are you ""today!";
// 等同於
char greeting[50] = "Hello, how are you today!";

這種新寫法支持多行字符串的合並。

char greeting[50] = "Hello, "
"how are you "
"today!";

printf()使用占位符%s輸出字符串。

printf("%s\n", "hello world")

字符串變量的聲明

字符串變量可以聲明成一個字符數組,也可以聲明成一個指針,指向字符數組。

// 寫法一
char s[14] = "Hello, world!";
// 寫法二
char* s = "Hello, world!";

上面兩種寫法都聲明了一個字符串變量s。如果采用第一種寫法,由於字符數組的長度可以讓編譯器自動計算,所以聲明時可以省略字符數組的長度。

char s[] = "Hello, world!";

上面示例中,編譯器會將數組s的長度指定為14,正好容納后面的字符串。
字符數組的長度,可以大於字符串的實際長度。

char s[50] = "hello";

上面示例中,字符數組s的長度是50,但是字符串“hello”的實際長度只有6(包含結尾符號\0),所以后面空出來的44個位置,都會被初始化為\0
字符數組的長度,不能小於字符串的實際長度。

char s[5] = "hello";

上面示例中,字符串數組s的長度是5,小於字符串“hello”的實際長度6,這時編譯器會報錯。因為如果只將前5個字符寫入,而省略最后的結尾符號\0,這很可能導致后面的字符串相關代碼出錯。
字符指針和字符數組,這兩種聲明字符串變量的寫法基本是等價的,但是有兩個差異。
第一個差異是,指針指向的字符串,在 C 語言內部被當作常量,不能修改字符串本身。

char* s = "Hello, world!";
s[0] = 'z'; // 錯誤

上面代碼使用指針,聲明了一個字符串變量,然后修改了字符串的第一個字符。這種寫法是錯的,會導致難以預測的后果,執行時很可能會報錯。
如果使用數組聲明字符串變量,就沒有這個問題,可以修改數組的任意成員。

char s[] = "Hello, world!";
s[0] = 'z';

為什么字符串聲明為指針時不能修改,聲明為數組時就可以修改?原因是系統會將字符串的字面量保存在內存的常量區,這個區是不允許用戶修改的。聲明為指針時,指針變量存儲的值是一個指向常量區的內存地址,因此用戶不能通過這個地址去修改常量區。但是,聲明為數組時,編譯器會給數組單獨分配一段內存,字符串字面量會被編譯器解釋成字符數組,逐個字符寫入這段新分配的內存之中,而這段新內存是允許修改的。
為了提醒用戶,字符串聲明為指針后不得修改,可以在聲明時使用const說明符,保證該字符串是只讀的。

const char* s = "Hello, world!";

上面字符串聲明為指針時,使用了const說明符,就保證了該字符串無法修改。一旦修改,編譯器肯定會報錯。
第二個差異是,指針變量可以指向其它字符串。

char* s = "hello";
s = "world";

上面示例中,字符指針可以指向另一個字符串。
但是,字符數組變量不能指向另一個字符串。

char s[] = "hello";
s = "world"; // 報錯

上面示例中,字符數組的數組名,總是指向初始化時的字符串地址,不能修改。
同樣的原因,聲明字符數組后,不能直接用字符串賦值。

char s[10];
s = "abc"; // 錯誤

上面示例中,不能直接把字符串賦值給字符數組變量,會報錯。原因是字符數組的變量名,跟所指向的數組是綁定的,不能指向另一個地址。
為什么數組變量不能賦值為另一個數組?原因是數組變量所在的地址無法改變,或者說,編譯器一旦為數組變量分配地址后,這個地址就綁定這個數組變量了,這種綁定關系是不變的。C 語言也因此規定,數組變量是一個不可修改的左值,即不能用賦值運算符為它重新賦值。
想要重新賦值,必須使用 C 語言原生提供的strcpy()函數,通過字符串拷貝完成賦值。這樣做以后,數組變量的地址還是不變的,即strcpy()只是在原地址寫入新的字符串,而不是讓數組變量指向新的地址。

char s[10];
strcpy(s, "abc");

上面示例中,strcpy()函數把字符串abc拷貝給變量s,這個函數的詳細用法會在后面介紹。

strlen()

strlen()函數返回字符串的字節長度,不包括末尾的空字符\0。該函數的原型如下。

// string.h
size_t strlen(const char* s);

它的參數是字符串變量,返回的是size_t類型的無符號整數,除非是極長的字符串,一般情況下當作int類型處理即可。下面是一個用法實例。

char* str = "hello";
int len = strlen(str); // 5

strlen()的原型在標准庫的string.h文件中定義,使用時需要加載頭文件string.h

#include <stdio.h>
#include <string.h>
int main(void) {
char* s = "Hello, world!";
printf("The string is %zd characters long.\n", strlen(s));
}

注意,字符串長度(strlen())與字符串變量長度(sizeof()),是兩個不同的概念。

char s[50] = "hello";
printf("%d\n", strlen(s));  // 5
printf("%d\n", sizeof(s));  // 50

上面示例中,字符串長度是5,字符串變量長度是50。
如果不使用這個函數,可以通過判斷字符串末尾的\0,自己計算字符串長度。

int my_strlen(char *s) {
int count = 0;
while (s[count] != '\0')
count++;
return count;
}

strcpy()

字符串的復制,不能使用賦值運算符,直接將一個字符串賦值給字符數組變量。

char str1[10];
char str2[10];
str1 = "abc"; // 報錯
str2 = str1;  // 報錯

上面兩種字符串的復制寫法,都是錯的。因為數組的變量名是一個固定的地址,不能修改,使其指向另一個地址。
如果是字符指針,賦值運算符(=)只是將一個指針的地址復制給另一個指針,而不是復制字符串。

char* s1;
char* s2;
s1 = "abc";
s2 = s1;

上面代碼可以運行,結果是兩個指針變量s1s2指向同一字符串,而不是將字符串s1的內容復制給s2
C 語言提供了strcpy()函數,用於將一個字符串的內容復制到另一個字符串,相當於字符串賦值。該函數的原型定義在string.h頭文件里面。

strcpy(char dest[], const char source[])

strcpy()接受兩個參數,第一個參數是目的字符串數組,第二個參數是源字符串數組。復制字符串之前,必須要保證第一個參數的長度不小於第二個參數,否則雖然不會報錯,但會溢出第一個字符串變量的邊界,發生難以預料的結果。第二個參數的const說明符,表示這個函數不會修改第二個字符串。

#include <stdio.h>
#include <string.h>
int main(void) {
char s[] = "Hello, world!";
char t[100];
strcpy(t, s);
t[0] = 'z';
printf("%s\n", s);  // "Hello, world!"
printf("%s\n", t);  // "zello, world!"
}

上面示例將變量s的值,拷貝一份放到變量t,變成兩個不同的字符串,修改一個不會影響到另一個。另外,變量t的長度大於s,復制后多余的位置(結束標志\0后面的位置)都為隨機值。
strcpy()也可以用於字符數組的賦值。

char str[10];
strcpy(str, "abcd");

上面示例將字符數組變量,賦值為字符串“abcd”。
strcpy()的返回值是一個字符串指針(即char*),指向第一個參數。

char* s1 = "beast";
char s2[40] = "Be the best that you can be.";
char* ps;
ps = strcpy(s2 + 7, s1);
puts(s2); // Be the beast
puts(ps); // beast

上面示例中,從s2的第7個位置開始拷貝字符串beast,前面的位置不變。這導致s2后面的內容都被截去了,因為會連beast結尾的空字符一起拷貝。strcpy()返回的是一個指針,指向拷貝開始的位置。
strcpy()返回值的另一個用途,是連續為多個字符數組賦值。

strcpy(str1, strcpy(str2, "abcd"));

上面示例調用兩次strcpy(),完成兩個字符串變量的賦值。
另外,strcpy()的第一個參數最好是一個已經聲明的數組,而不是聲明后沒有進行初始化的字符指針。

char* str;
strcpy(str, "hello world"); // 錯誤

上面的代碼是有問題的。strcpy()將字符串分配給指針變量str,但是str並沒有進行初始化,指向的是一個隨機的位置,因此字符串可能被復制到任意地方。
如果不用strcpy(),自己實現字符串的拷貝,可以用下面的代碼。

char* strcpy(char* dest, const char* source) {
char* ptr = dest;
while (*dest++ = *source++);
return ptr;
}
int main(void) {
char str[25];
strcpy(str, "hello world");
printf("%s\n", str);
return 0;
}

上面代碼中,關鍵的一行是while (*dest++ = *source++),這是一個循環,依次將source的每個字符賦值給dest,然后移向下一個位置,直到遇到\0,循環判斷條件不再為真,從而跳出循環。其中,*dest++這個表達式等同於*(dest++),即先返回dest這個地址,再進行自增運算移向下一個位置,而*dest可以對當前位置賦值。
strcpy()函數有安全風險,因為它並不檢查目標字符串的長度,是否足夠容納源字符串的副本,可能導致寫入溢出。如果不能保證不會發生溢出,建議使用strncpy()函數代替。

strncpy()

strncpy()strcpy()的用法完全一樣,只是多了第3個參數,用來指定復制的最大字符數,防止溢出目標字符串變量的邊界。

char* strncpy(
char* dest,
char* src,
size_t n
);

上面原型中,第三個參數n定義了復制的最大字符數。如果達到最大字符數以后,源字符串仍然沒有復制完,就會停止復制,這時目的字符串結尾將沒有終止符\0,這一點務必注意。如果源字符串的字符數小於n,則strncpy()的行為與strcpy()完全一致。

strncpy(str1, str2, sizeof(str1) - 1);
str1[sizeof(str1) - 1] = '\0';

上面示例中,字符串str2復制給str1,但是復制長度最多為str1的長度減去1,str1剩下的最后一位用於寫入字符串的結尾標志\0。這是因為strncpy()不會自己添加\0,如果復制的字符串片段不包含結尾標志,就需要手動添加。
strncpy()也可以用來拷貝部分字符串。

char s1[40];
char s2[12] = "hello world";
strncpy(s1, s2, 5);
s1[5] = '\0';
printf("%s\n", s1); // hello

上面示例中,指定只拷貝前5個字符。

strcat()

strcat()函數用於連接字符串。它接受兩個字符串作為參數,把第二個字符串的副本添加到第一個字符串的末尾。這個函數會改變第一個字符串,但是第二個字符串不變。
該函數的原型定義在string.h頭文件里面。

char* strcat(char* s1, const char* s2);

strcat()的返回值是一個字符串指針,指向第一個參數。

char s1[12] = "hello";
char s2[6] = "world";
strcat(s1, s2);
puts(s1); // "helloworld"

上面示例中,調用strcat()以后,可以看到字符串s1的值變了。
注意,strcat()的第一個參數的長度,必須足以容納添加第二個參數字符串。否則,拼接后的字符串會溢出第一個字符串的邊界,寫入相鄰的內存單元,這是很危險的,建議使用下面的strncat()代替。

strncat()

strncat()用於連接兩個字符串,用法與strcat()完全一致,只是增加了第三個參數,指定最大添加的字符數。在添加過程中,一旦達到指定的字符數,或者在源字符串中遇到空字符\0,就不再添加了。它的原型定義在string.h頭文件里面。

char* strncat(
const char* dest,
const char* src,
size_t n
);

strncat()返回第一個參數,即目標字符串指針。
為了保證連接后的字符串,不超過目標字符串的長度,strncat()通常會寫成下面這樣。

strncat(
str1,
str2,
sizeof(str1) - strlen(str1) - 1
);

strncat()總是會在拼接結果的結尾,自動添加空字符\0,所以第三個參數的最大值,應該是str1的變量長度減去str1的字符串長度,再減去1。下面是一個用法實例。

char s1[10] = "Monday";
char s2[8] = "Tuesday";
strncat(s1, s2, 3);
puts(s1); // "MondayTue"

上面示例中,s1的變量長度是10,字符長度是6,兩者相減后再減去1,得到3,表明s1最多可以再添加三個字符,所以得到的結果是MondayTue

strcmp()

如果要比較兩個字符串,無法直接比較,只能一個個字符進行比較,C 語言提供了strcmp()函數。
strcmp()函數用於比較兩個字符串的內容。該函數的原型如下,定義在string.h頭文件里面。

int strcmp(const char* s1, const char* s2);

按照字典順序,如果兩個字符串相同,返回值為0;如果s1小於s2strcmp()返回值小於0;如果s1大於s2,返回值大於0。
下面是一個用法示例。

// s1 = Happy New Year
// s2 = Happy New Year
// s3 = Happy Holidays
strcmp(s1, s2) // 0
strcmp(s1, s3) // 大於 0
strcmp(s3, s1) // 小於 0

注意,strcmp()只用來比較字符串,不用來比較字符。因為字符就是小整數,直接用相等運算符(==)就能比較。所以,不要把字符類型(char)的值,放入strcmp()當作參數。

strncmp()

由於strcmp()比較的是整個字符串,C 語言又提供了strncmp()函數,只比較到指定的位置。
該函數增加了第三個參數,指定了比較的字符數。它的原型定義在string.h頭文件里面。

int strncmp(
const char* s1,
const char* s2,
size_t n
);

它的返回值與strcmp()一樣。如果兩個字符串相同,返回值為0;如果s1小於s2strcmp()返回值小於0;如果s1大於s2,返回值大於0。
下面是一個例子。

char s1[12] = "hello world";
char s2[12] = "hello C";
if (strncmp(s1, s2, 5) == 0) {
printf("They all have hello.\n");
}

上面示例只比較兩個字符串的前5個字符。

sprintf(),snprintf()

sprintf()函數跟printf()類似,但是用於將數據寫入字符串,而不是輸出到顯示器。該函數的原型定義在stdio.h頭文件里面。

int sprintf(char* s, const char* format, ...);

sprintf()的第一個參數是字符串指針變量,其余參數和printf()相同,即第二個參數是格式字符串,后面的參數是待寫入的變量列表。

char first[6] = "hello";
char last[6] = "world";
char s[40];
sprintf(s, "%s %s", first, last);
printf("%s\n", s); // hello world

上面示例中,sprintf()將輸出內容組合成“hello world”,然后放入了變量s
sprintf()的返回值是寫入變量的字符數量(不計入尾部的空字符\0)。如果遇到錯誤,返回負值。
sprintf()有嚴重的安全風險,如果寫入的字符串過長,超過了目標字符串的長度,sprintf()依然會將其寫入,導致發生溢出。為了控制寫入的字符串的長度,C 語言又提供了另一個函數snprintf()
snprintf()只比sprintf()多了一個參數n,用來控制寫入變量的字符串不超過n - 1個字符,剩下一個位置寫入空字符\0。下面是它的原型。

int snprintf(char*s, size_t n, const char* format, ...);

snprintf()總是會自動寫入字符串結尾的空字符。如果你嘗試寫入的字符數超過指定的最大字符數,snprintf()會寫入 n - 1 個字符,留出最后一個位置寫入空字符。
下面是一個例子。

snprintf(s, 12, "%s %s", "hello", "world");

上面的例子中,snprintf()的第二個參數是12,表示寫入字符串的最大長度不超過12(包括尾部的空字符)。
snprintf()的返回值是寫入格式字符串的字符數量(不計入尾部的空字符\0)。如果n足夠大,返回值應該小於n,但是有時候格式字符串的長度可能大於n,那么這時返回值會大於n,但實際上真正寫入變量的還是n-1個字符。如果遇到錯誤,返回一個負值。因此,返回值只有在非負並且小於n時,才能確認完整的格式字符串寫入了變量。

字符串數組

如果一個數組的每個成員都是一個字符串,需要通過二維的字符數組實現。每個字符串本身是一個字符數組,多個字符串再組成一個數組。

char weekdays[7][10] = {
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
};

上面示例就是一個字符串數組,一共包含7個字符串,所以第一維的長度是7。其中,最長的字符串的長度是10(含結尾的終止符\0),所以第二維的長度統一設為10。
因為第一維的長度,編譯器可以自動計算,所以可以省略。

char weekdays[][10] = {
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
};

上面示例中,二維數組第一維的長度,可以由編譯器根據后面的賦值,自動計算,所以可以不寫。
數組的第二維,長度統一定為10,有點浪費空間,因為大多數成員的長度都小於10。解決方法就是把數組的第二維,從字符數組改成字符指針。

char* weekdays[] = {
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
};

上面的字符串數組,其實是一個一維數組,成員就是7個字符指針,每個指針指向一個字符串(字符數組)。
遍歷字符串數組的寫法如下。

for (int i = 0; i < 7; i++) {
printf("%s\n", weekdays[i]);
}

多字節字符

本章介紹 C 語言如何處理非英語字符。

Unicode 簡介

C 語言誕生時,只考慮了英語字符,使用7位的 ASCII 碼表示所有字符。ASCII 碼的范圍是0到127,也就是100多個字符,所以char類型只占用一個字節。
但是,如果處理非英語字符,一個字節就不夠了,單單是中文,就至少有幾萬個字符,字符集就勢必使用多個字節表示。
最初,不同國家有自己的字符編碼方式,這樣不便於多種字符的混用。因此,后來就逐漸統一到 Unicode 編碼,將所有字符放入一個字符集。
Unicode 為每個字符提供一個號碼,稱為碼點(code point),其中0到127的部分,跟 ASCII 碼是重合的。通常使用“U+十六進制碼點”表示一個字符,比如U+0041表示字母A
Unicode 編碼目前一共包含了100多萬個字符,碼點范圍是 U+0000 到 U+10FFFF。完整表達整個 Unicode 字符集,至少需要三個字節。但是,並不是所有文檔都需要那么多字符,比如對於 ASCII 碼就夠用的英語文檔,如果每個字符使用三個字節表示,就會比單字節表示的文件體積大出三倍。
為了適應不同的使用需求,Unicode 標准委員會提供了三種不同的表示方法,表示 Unicode 碼點。

  • UTF-8:使用1個到4個字節,表示一個碼點。不同的字符占用的字節數不一樣。
  • UTF-16:對於U+0000 到 U+FFFF 的字符(稱為基本平面),使用2個字節表示一個碼點。其他字符使用4個字節。
  • UTF-32:統一使用4個字節,表示一個碼點。
    其中,UTF-8 的使用最為廣泛,因為對於 ASCII 字符(U+0000 到 U+007F),它只使用一個字節表示,這就跟 ASCII 的編碼方式完全一樣。
    C 語言提供了兩個宏,表示當前系統支持的編碼字節長度。這兩個宏都定義在頭文件limits.h
  • MB_LEN_MAX:任意支持地區的最大字節長度,定義在limits.h
  • MB_CUR_MAX:當前語言的最大字節長度,總是小於或等於MB_LEN_MAX,定義在stdlib.h

字符的表示方法

字符表示法的本質,是將每個字符映射為一個整數,然后從編碼表獲得該整數對應的字符。
C 語言提供了不同的寫法,用來表示字符的整數號碼。

  • \123:以八進制值表示一個字符,斜杠后面需要三個數字。
  • \x4D:以十六進制表示一個字符,\x后面是十六進制整數。
  • \u2620:以 Unicode 碼點表示一個字符(不適用於 ASCII 字符),碼點以十六進制表示,\u后面需要4個字符。
  • \U0001243F:以 Unicode 碼點表示一個字符(不適用於 ASCII 字符),碼點以十六進制表示,\U后面需要8個字符。
printf("ABC\n");
printf("\101\102\103\n");
printf("\x41\x42\x43\n");

上面三行都會輸出“ABC”。

printf("\u2022 Bullet 1\n");
printf("\U00002022 Bullet 1\n");

上面兩行都會輸出“• Bullet 1”。

多字節字符的表示

C 語言預設只有基本字符,才能使用字面量表示,其它字符都應該使用碼點表示,並且當前系統還必須支持該碼點的編碼方法。
所謂基本字符,指的是所有可打印的 ASCII 字符,但是有三個字符除外:@$`
因此,遇到非英語字符,應該將其寫成 Unicode 碼點形式。

char* s = "\u6625\u5929";
printf("%s\n", s); // 春天

上面代碼會輸出中文“春天”。
如果當前系統是 UTF-8 編碼,可以直接用字面量表示多字節字符。

char* s = "春天";
printf("%s\n", s);

注意,\u + 碼點\U + 碼點的寫法,不能用來表示 ASCII 碼字符(碼點小於0xA0的字符),只有三個字符除外:0x24$),0x40@)和0x60`)。

char* s = "\u0024\u0040\u0060";
printf("%s\n", s);  // @$`

上面代碼會輸出三個 Unicode 字符“@$`”,但是其它 ASCII 字符都不能用這種表示法表示。
為了保證程序執行時,字符能夠正確解讀,最好將程序環境切換到本地化環境。

setlocale(LC_ALL, "");

上面代碼中,使用setlocale()切換執行環境到系統的本地化語言。setlocale()的原型定義在頭文件locale.h,詳見標准庫部分的《locale.h》章節。
像下面這樣,指定編碼語言也可以。

setlocale(LC_ALL, "zh_CN.UTF-8");

上面代碼將程序執行環境,切換到中文環境的 UTF-8 編碼。
C 語言允許使用u8前綴,對多字節字符串指定編碼方式為 UTF-8。

char* s = u8"春天";
printf("%s\n", s);

一旦字符串里面包含多字節字符,就意味着字符串的字節數與字符數不再一一對應了。比如,字符串的長度為10字節,就不再是包含10個字符,而可能只包含7個字符、5個字符等等。

setlocale(LC_ALL, "");
char* s = "春天";
printf("%d\n", strlen(s)); // 6

上面示例中,字符串s只包含兩個字符,但是strlen()返回的結果卻是6,表示這兩個字符一共占據了6個字節。
C 語言的字符串函數只針對單字節字符有效,對於多字節字符都會失效,比如strtok()strchr()strspn()toupper()tolower()isalpha()等不會得到正確結果。

寬字符

上一小節的多字節字符串,每個字符的字節寬度是可變的。這種編碼方式雖然使用起來方便,但是很不利於字符串處理,因此必須逐一檢查每個字符占用的字節數。所以除了這種方式,C 語言還提供了確定寬度的多字節字符存儲方式,稱為寬字符(wide character)。
所謂“寬字符”,就是每個字符占用的字節數是固定的,要么是2個字節,要么是4個字節。這樣的話,就很容易快速處理。
寬字符有一個單獨的數據類型 wchar_t,每個寬字符都是這個類型。它屬於整數類型的別名,可能是有符號的,也可能是無符號的,由當前實現決定。該類型的長度為16位(2個字節)或32位(4個字節),足以容納當前系統的所有字符。它定義在頭文件wchar.h里面。
寬字符的字面量必須加上前綴“L”,否則 C 語言會把字面量當作窄字符類型處理。

setlocale(LC_ALL, "");
wchar_t c = L'牛';
printf("%lc\n", c);
wchar_t* s = L"春天";
printf("%ls\n", s);

上面示例中,前綴“L”在單引號前面,表示寬字符,對應printf()的占位符為%lc;在雙引號前面,表示寬字符串,對應printf()的占位符為%ls
寬字符串的結尾也有一個空字符,不過是寬空字符,占用多個字節。
處理寬字符,需要使用寬字符專用的函數,絕大部分都定義在頭文件wchar.h

多字節字符處理函數

mblen()

mblen()函數返回一個多字節字符占用的字符數。它的原型定義在頭文件stdlib.h

int mblen(const char* mbstr, size_t n);

它接受兩個參數,第一個參數是多字節字符串指針,一般會檢查該字符串的第一個字符;第二個參數是需要檢查的字節數,這個數字不能大於當前系統單個字符占用的最大字節,一般使用MB_CUR_MAX
它的返回值是該字符占用的字節數。如果當前字符是空的寬字符,則返回0;如果當前字符不是有效的多字節字符,則返回-1

setlocale(LC_ALL, "");
char* mbs1 = "春天";
printf("%d\n", mblen(mbs1, MB_CUR_MAX)); // 3
char* mbs2 = "abc";
printf("%d\n", mblen(mbs2, MB_CUR_MAX)); // 1

上面示例中,字符串“春天”的第一個字符“春”,占用3個字節;字符串“abc”的第一個字符“a”,占用1個字節。

wctomb()

wctomb()函數(wide character to multibyte)用於將寬字符轉為多字節字符。它的原型定義在頭文件stdlib.h

int wctomb(char* s, wchar_t wc);

wctomb()接受兩個參數,第一個參數是作為目標的多字節字符數組,第二個參數是需要轉換的一個寬字符。它的返回值是多字節字符存儲占用的字節數量,如果無法轉換,則返回-1

setlocale(LC_ALL, "");
wchar_t wc = L'牛';
char mbStr[10] = "";
int nBytes = 0;
nBytes = wctomb(mbStr, wc);
printf("%s\n", mbStr);  // 牛
printf("%d\n", nBytes);  // 3

上面示例中,wctomb()將寬字符“牛”轉為多字節字符,wctomb()的返回值表示轉換后的多字節字符占用3個字節。

mbtowc()

mbtowc()用於將多字節字符轉為寬字符。它的原型定義在頭文件stdlib.h

int mbtowc(
wchar_t* wchar,
const char* mbchar,
size_t count
);

它接受3個參數,第一個參數是作為目標的寬字符指針,第二個參數是待轉換的多字節字符指針,第三個參數是多字節字符的字節數。
它的返回值是多字節字符的字節數,如果轉換失敗,則返回-1

setlocale(LC_ALL, "");
char* mbchar = "牛";
wchar_t wc;
wchar_t* pwc = &wc;
int nBytes = 0;
nBytes = mbtowc(pwc, mbchar, 3);
printf("%d\n", nBytes); // 3
printf("%lc\n", *pwc);  // 牛

上面示例中,mbtowc()將多字節字符“牛”轉為寬字符wc,返回值是mbchar占用的字節數(占用3個字節)。

wcstombs()

wcstombs()用來將寬字符串轉換為多字節字符串。它的原型定義在頭文件stdlib.h

size_t wcstombs(
char* mbstr,
const wchar_t* wcstr,
size_t count
);

它接受三個參數,第一個參數mbstr是目標的多字節字符串指針,第二個參數wcstr是待轉換的寬字符串指針,第三個參數count是用來存儲多字節字符串的最大字節數。
如果轉換成功,它的返回值是成功轉換后的多字節字符串的字節數,不包括尾部的字符串終止符;如果轉換失敗,則返回-1
下面是一個例子。

setlocale(LC_ALL, "");
char mbs[20];
wchar_t* wcs = L"春天";
int nBytes = 0;
nBytes = wcstombs(mbs, wcs, 20);
printf("%s\n", mbs); // 春天
printf("%d\n", nBytes); // 6

上面示例中,wcstombs()將寬字符串wcs轉為多字節字符串mbs,返回值6表示寫入mbs的字符串占用6個字節,不包括尾部的字符串終止符。
如果wcstombs()的第一個參數是 NULL,則返回轉換成功所需要的目標字符串的字節數。

mbstowcs()

mbstowcs()用來將多字節字符串轉換為寬字符串。它的原型定義在頭文件stdlib.h

size_t mbstowcs(
wchar_t* wcstr,
const char* mbstr,
size_t count
);

它接受三個參數,第一個參數wcstr是目標寬字符串,第二個參數mbstr是待轉換的多字節字符串,第三個參數是待轉換的多字節字符串的最大字符數。
轉換成功時,它的返回值是成功轉換的多字節字符的數量;轉換失敗時,返回-1。如果返回值與第三個參數相同,那么轉換后的寬字符串不是以 NULL 結尾的。
下面是一個例子。

setlocale(LC_ALL, "");
char* mbs = "天氣不錯";
wchar_t wcs[20];
int nBytes = 0;
nBytes = mbstowcs(wcs, mbs, 20);
printf("%ls\n", wcs); // 天氣不錯
printf("%d\n", nBytes); // 4

上面示例中,多字節字符串mbsmbstowcs()轉為寬字符串,成功轉換了4個字符,所以該函數的返回值為4。
如果mbstowcs()的第一個參數為NULL,則返回目標寬字符串會包含的字符數量。

地址: https://github.com/wangdoc/clang-tutorial


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM