字符串
簡介
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;
上面代碼可以運行,結果是兩個指針變量s1
和s2
指向同一字符串,而不是將字符串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
小於s2
,strcmp()
返回值小於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
小於s2
,strcmp()
返回值小於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
上面示例中,多字節字符串mbs
被mbstowcs()
轉為寬字符串,成功轉換了4個字符,所以該函數的返回值為4。
如果mbstowcs()
的第一個參數為NULL
,則返回目標寬字符串會包含的字符數量。