字符串
简介
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
,则返回目标宽字符串会包含的字符数量。
知乎: @张赐荣
赐荣博客: www.prc.cx
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!