在C语言中使用中文,本地化全攻略

C语言 专栏收录该内容
9 篇文章 0 订阅

系列文章目录


本地化

很多人认为C语言只支持ascii码,这是误解,你用printf(“这是中文”)同样可以输出中文,用fputs(“C语言本地化”)也可以向文件写入中文,那么C语言默认使用什么编码呢?这个问题不是那么简单,因为C标准并未规定使用什么编码,实际编码与操作系统、所在区域、编译器有很大的关系。另外可能你会发现虽然可以输出中文字符串,但并不能用char c=’中’来声明一个中文字符,如果用strlen(“C语言”)来求字符串长度,给出的值可能是5,因为编码是变长的,默认情况下C并不能很好的支持中文。如何让C语言很好的支持中文呢?这需要我们从基本编码概念讲起。

字符编码

计算机刚刚发明时,只支持ascii码,也就是说只支持英文,随着计算机在全球兴起,各国创建了属于自己的编码来显示本国文字,中文首先使用GB2132编码,它收录了6763个汉字,平日里我们工作学习大概只会用到3千个汉字,因此日常使用已经足够,GBK收录了21003个汉字,远超日常汉字使用需求,不管是日用还是商用都能轻松搞定,因此从windows95开始就将GBK作为默认汉字编码,而GB18030收录了27533个汉字,为汉字研究、古籍整理等领域提供了统一的信息平台基础,平时根本使用不到这种编码。这些中文编码本身兼容ascii,并采用变长方式记录,英文使用一个字节,常用汉字使用2个字节,罕见字使用四个字节。后来随着全球文化不断交流,人们迫切需要一种全球统一的编码能够统一世界各地字符,再也不会因为地域不同而出现乱码,这时Unicode字符集就诞生了,也称为统一码,万国码。新出来的操作系统其内核本身就支持Unicode,由万国码的名称就可以想象这是一个多么庞大的字符集,为了兼容所有地区的文字,也考虑到空间和性能,Unicode提供了3种编码方案:
utf-8 变长编码方案,使用1-6个字节来储存
utf-32 定长编码方案,始终使用4个字节来储存
utf-16 介于变长和定长之间的平衡方案,使用2个或4个字节来储存
utf-8由于是变长方案,类似GB2132和GBK量体裁衣,最节省空间,但要通过第一个字节决定采用几个字节储存,编码最复杂,且由于变长要定位文字,就得从第一个字符开始计算,性能最低。utf-32由于是定长方案,字节数固定因此无需解码,性能最高但最浪费空间。utf-16是个怪胎,它将常用的字放在编号0 ~ FFFF之间,不用进行编码转换,对于不常用字的都放在10000~10FFFF编号之后,因此自然的解决变长的问题。注意对于这3种编码,只有utf-8兼容ascii,utf-32和utf-16都不支持单字节,由于utf-8最省流量,兼容性好,后来解码性能也得到了很大改善,同时新出来的硬件也越来越强,性能已不成问题,因此很多纯文本、源代码、网页、配置文件等都采用utf-8编码,从而代替了原来简陋的ascii。再来看看utf-16,对于常见字2个字节已经完全够用,很少会用到4个字节,因此通常也将utf-16划分为定长,一些操作系统和代码编译器直接不支持4字节的utf-16。Unicode还分为大端和小端,大端就是将高位的字节放在低地址表示,后缀为BE;小端就是将高位的字节放在高地址表示,后缀为LE,没有指定后缀,即不知道其是大小端,所以其开始的两个字节表示该字节数组是大端还是小端,FE FF表示大端,FF FE表示小端。Windows内核使用utf-16,linux,mac,ios内核使用的是utf-8,我们就不去争论谁好谁坏了。另外虽然windows内核为utf-16,但为了更好的本地化,控制面板提供了区域选项,如果设置为简体就是GBK编码,在win10中,控制台和记事本默认编码为gbk,其它第三方软件就不好说了,它们默认编码各不相同。

了解编码后下面来说说BOM,一个文本文件,可以是纯文本、网页、源码等,那么打开它的程序如何知道它采用什么编码呢?为了说明一个文件采用的是什么编码,在文件最开始的部分,可以有BOM,比如0xFE 0xFF表示UTF-16BE,0xFF 0xFE 0x00 0x00表示UTF-32LE。UTF-8原本是不需要BOM的,因为其自我同步的特性,但是为了明确说明这是UTF-8而不是让文本编辑器去猜,也可以加上UTF-8的BOM:0xEF 0xBB 0xBF。

C语言字符编码

C语言源代码采用什么格式取决于IDE环境,通常是utf-8或ANSI,什么是ANSI编码呢?相比unicode它是采取另一种思路,严格来说ANSI不是一种编码,而是一种替代方案,力求找到显示内容的最低编码需求,如果内容只有英文字符就使用ascii,如果发现汉字就替换成本地的GBK编码,如果发现既有汉字又有日语又有韩语是否会自动选择unicode这个没有试过。前面讲过C语言使用的编码和操作系统、区域选择、编译器都有关系,但有一个现象,通常源码采用什么格式的编码,运行时就使用这样的编码,因此我们可以通过源代码先看看这个IDE会使用什么编码。在Dev C++中测试的结果是,如果只有英文源码默认采用utf8,现在的编辑器很少使用纯ascii了,如果发现源码里面有汉字,则将编码改为ANSI,由于windows控制台默认也使用ANSI,因此可以显示标准输入输出中的汉字。

如果只需要向控制台输出一段字符串或者向文件中写入一段中文,那么使用标准输入输出函数即可,因为printf()和puts()可以识别字符串使用的编码,但如果要操作单个字符就不行了,因为GBA和GBK都是变长的,一个英文字母使用一个字节,而一个中文使用2-4个字节,用char c=’中’是不行的,因为char只能是一个字节。再来看看字符串,虽然可以放入中文,但却不能访问数组元素,因为数组元素也只能是一个char,数组元素个数和字符串个数是不对应的,这是历史遗留的问题,无法更改,否则新标准就不能兼容之前的代码了。要处理中文字符,只能另辟途径,一是将编码从变长改为定长,二是使用新的字符类型来处理定长编码,对于变长和定长,在计算机行业种还有一个术语称为窄字符和宽字符,从前面知识可以得知,能够显示各个国家的语言并且采用定长的只有utf-16和utf-32了,C语言为宽字符提供新的类型wchar,这个类型由wchar库提供,导入wchar.h后就可以使用宽字符了,宽字符使用utf-16还是utf-32由编译器决定,windows中宽字符使用utf-16,linux则使用utf-32,我们可以通过以下代码进行测试,如下:

#include <stdio.h>
#include <wchar.h>

int main()
{
	wchar_t wc1=L'a';
	wchar_t wc2=L'中';
	printf("%d,%d\n",sizeof(wc1),sizeof(wc2));
	printf("%x,%x\n",wc1,wc2);
	return 0;
}

上面代码使用wchar_t声明一个宽字符,宽字符前面要添加L,这里使用sizeof()检测的结果是无论中文还是英文都是2字节,说明编码使用的是utf-16,后面一个printf()将编码以十六进制输出。至于utf-16和utf-32哪个好我们也不去争论了,巨头们都很任性且互不买账。

如果要显示一个宽字节符,需要换成putwchar()和wprintf()函数,如下:

#include <stdio.h>
#include <wchar.h>
#include <locale.h>

int main()
{
	setlocale(LC_ALL, "");
	wchar_t wc1=L'a';
	wchar_t wc2=L'中';
	putwchar(wc1);
	putwchar(wc2);
	wprintf(L"%c%c",wc1,wc2);
	return 0;
}

这两个方法需要先调用setlocale()函数进行初始化,设置Unicode区域,setlocale()的格式为:
char* setlocale (int category, const char* locale)
category是类型,表示区域编码影响到的类型,类型有时钟、货币、字符排序等,通常设置为常数LC_ALL表示影响所有类型。locale表示区域,windows和linux表示区域的方式各不相同,例如windows表示简体中文用”chs”,linux用”zh_CN”,但有3个区域总是相同的:”C”表示中立地域,不表示任何一个地区,只对小数点进行了设置,是默认值;“”表示本地区域;NULL表示不指定区域仅仅返回区域信息,可以通过puts(setlocale(NULL,""))输出本地区域信息,中文简体为Chinese (Simplified)_China.936。如果不设置区域,默认区域为”C”,这个区域只能显示英文,无法显示任何中文字符,实际测试结果也是如此。由于setlocale()包含在locale.h中,因此要先导入locale库,下面我们再来看看宽字符串的定义和输出:

#include <stdio.h>
#include <wchar.h>
#include <locale.h>

int main()
{
	setlocale(LC_ALL, "");
	wchar_t wstr[]=L"宽字符串";
	wprintf(L"%s,%c\n",wstr, wstr[1]);
	printf("%d",wcslen(wstr));
	return 0;
}

声明宽字符串后应该使用wcslen()函数获取字符串的长度,使用下标和指针运算都能正确定位,这就是采用定长的原因。除此之外,字符串的复制、连接等都有配套的宽字符串操作方法,相关方法可以查询C语言函数库。需要清楚的是,传统的字符串和相应的方法现在归于窄字符范畴,使用L前缀的字符串和带w的函数属于宽字符串范畴,它们的类型和处理方式都不相同,不能混用。

宽字符的实现原理

当我们用大写的L标记一个宽字符串时,这个字符串编码会以utf-16或utf-32储存,然而输出字符串时控制台或文本不一定是utf-16和utf-32,因此在输出时会转化为窄字符使用的编码,转化的依据是setlocale()中对宽字符编码的区域的设定,例如setlocale()将区域设定为本地,本地编码为简体中文,那么在windows中运行时会将zh-cn的unicode转为gbk,在linux中运行会转化为utf-8,实际上无论将控制台设置为什么编码,宽字符都能正常显示。这种编码的转换被写入到stout中,因此调用setlocale()后除了影响宽字符还影响窄字符,GCC就是一个例子,GCC对宽字符的支持很不友好,如果代码为ANSI或GBK,则以L开头的宽字符或宽字符串不能通过编译,因为linux下的GCC在默认情况下总是假设源代码的编码是utf-8,只有将源码格式设置为utf-8才能通过编译,下图是将使用了minGW的CDT源码格式改为utf-8:
在这里插入图片描述

如果将源码改为utf-8,则编译后输出的编码也为utf-8,还需要将控制台编码设置为utf-8才能显示窄字符,宽字符依然需要调用setlocale()设置区域,然而更改区域又会影响窄字符的显示,要同时显示窄字符和宽字符只能每次显示之前先设置区域,如下:

#include <stdio.h>
#include<Windows.h>
#include<locale.h>

int main()
{
	system("chcp 936");//将控制台编码设置为gbk
	puts("乱码");
	system("pause");
	system("chcp 65001");//将控制台编码设置为utf8
	printf("%s\n", "显示窄字符utf8");
	setlocale(LC_ALL, "");
	printf("%ls\n",L"显示宽字符utf16");
	
	setlocale(LC_ALL, "C");
	puts("还原区域显示窄字符");
	return 0;
}

在windows控制台中执行chcp可以设置控制台字符编码,这里使用system(“chcp 936”)来调用这个命令将控制台编码设置为gbk,由于gcc输出编码为utf-8,因此在gbk下呈现乱码,接着调用system(“chcp 65001”)将控制台编码设置为utf8发现能够正常显示。调用setlocale(LC_ALL, “”)后会同时影响宽字符和窄字符,因此输出窄字符又呈现乱码,在后面调用setlocale(LC_ALL, “C”)还原默认的区域后窄字符能够正确显示。这段代码还不能在CDT的控制台中运行,只能在windows的控制台中测试,因为eclipse控制台采用的编码和windows控制台又不同,使用minGW的CDT无论编译还是输出对本地化的支持都不是很友好。相反在高版本的VS(Visual Studio)中很少碰到显示乱码问题,调用setlocale()设置宽字符区域不会影响窄字符的显示,将控制台设置为utf-8或ANSI都能正确显示,因为对于窄字符无论如何设置VS都会尝试将编码转化为为控制台使用的编码输出。不过现在我们明白宽字符的运作原理了,那就是编译时将文本转为unicode-16或unicode-32的宽字符,运行时将unicode-16或unicode-32转化为ANSI或utf-8的窄字符。宽字符是对传统字符的扩展,在处理上采用完全不同的方案,因此在使用上窄字符和宽字符相互独立,C代码中可以同时存在两套编码,对应的处理函数也不同。

窄字符和宽字符处理函数

由于C语言标准并未为宽字符制定统一的标准,这导致宽字符非常依赖编译器和库,一些编译器对宽字符支持很不友善,一些甚至根本不支持宽字符,但宽字符仍然是趋势所在,这是国际化所需的,在很多高级语言中已经不区分宽字符和窄字符,只关心字符所用的编码。如果你使用C语言只是利用它优越的性能写一些算法,那么你可以不用关心本地化问题;如果你的程序涉及到多字节文本处理、文本读写就不可能回避本地化问题,此时统一使用宽字符是一个很好的解决方案,混用宽字符和窄字符只会让你的代码更难维护,下面我将宽字符函数按功能分类并且对照窄字符函数列出,以供参考。注意左边带w的为宽字符处理函数,右边为窄字符函数,None表示没有对应的函数。

字符类型

iswalnum() isalnum() 测试字符是否为数字或字母
iswalpha() isalpha() 测试字符是否是字母
iswcntrl() iscntrl() 测试字符是否是控制符
iswdigit() isdigit() 测试字符是否为数字
iswgraph() isgraph() 测试字符是否是可见字符
iswlower() islower() 测试字符是否是小写字符
iswprint() isprint() 测试字符是否是可打印字符
iswpunct() ispunct() 测试字符是否是标点符号
iswspace() isspace() 测试字符是否是空白符号
iswupper() isupper() 测试字符是否是大写字符
iswxdigit() isxdigit() 测试字符是否是十六进制的数字

大小写转换

towlower() tolower() 把字符转换为小写
towupper() toupper() 把字符转换为大写

数值转换

None atoi() 把字符串转换为整数
wcstol()/wcstoll() atol()/strtol()/strtoll() 把字符串转换为长整数
wcstoul()/wcstoull() strtoul()/strtoull() 把字符串转换为无符号长整形
wcstof() atof() 把字符串转换为单精度浮点
wcstod() strtod() 把字符串转换为双精度浮点

None itoa() 把整数转换为字符串
None ltoa()/lltoa() 把长整数转换为字符串
None ultoa()/ulltoa() 把无符号长整形转换为字符串
None gcvt()/ecvt()/fcvt() 将浮点转换为字符串

字符串操作

wcslen() strlen() 获得字符串的字符个数
wcscat() strcat() 把一个字符串接到另一个字符串的尾部
wcsncat() strncat() 把一个字符串接到另一个字符串的尾部且指定连接长度.
wcscpy() strcpy() 拷贝字符串
wcsncpy() strncpy() 拷贝字符串同时指定拷贝的字符个数
wcschr() strchr() 查找子字符的第一个位置
wcsrchr() strrchr() 从尾部开始查找子字符出现的第一个位置
wcspbrk() strpbrk() 从一字符字符串中查找另一字符串中任何一个字符第一次出现的位置
wcswcs() / wcsstr() strstr () 在一字符串中查找另一字符串第一次出现的位置
wcsspn() strspn() 返回包含第二个字符串的初始数目
wcscspn() strcspn() 返回不包含第二个字符串的的初始数目
wcscmp() strcmp() 比较两个字符串
wcsncmp() strncmp() 比较两个字符串还要指定比较的数目
wcscoll() strcoll() 根据LC_COLLATE比较字符串
wcstok() strtok() 根据标示符把宽字符串分解成一系列字符串

日期和时间转换

strftime() 根据指定的字符串格式和locale设置格式化日期和时间
strptime() 根据指定格式把字符串转换为时间值, 是strftime()的反过程
wcsftime() 根据指定的字符串格式和locale设置格式化日期和时间, 并返回宽字符串

输入和输出

wprintf() printf() 格式化输出到标准输出
wscanf() scanf() 从标准输入的格式化读入
fwprintf() fprintf() 格式化输出到文件
fwscanf() fscanf() 格式化文件读入
swprintf() sprintf() 格式化成字符串
swscanf() swscanf() 以字符串作格式化读入
vwprintf() vprintf () 使用stdarg参量表格式化输出到标准输出
vfwprintf() vfprintf() 使用stdarg参量表格式化输出到文件
vswprintf() vsprintf() 使用stdarg参量表格式写入到字符串
getwc()/getwchar() getc()/getchar() 从标准输入中读取字符
putwc()/putwchar() putc()/putchar() 将字符输出到标准输出
None gets() 获取字符串
None puts() 输出字符串
ungetwc() ungetc() 把字符放回到输入流中
fgetwc() fgetc() 从文件流中读入一个字符
fputwc() fputc() 把字符转输出到文件流
fgetws() fgets() 从文件流中读入一个字符串
fputws() fputs() 把字符串输出到文件流

内存操作

wmemcpy() memcpy() 内存快复制
wmemmove() memmove() 用于内存块重叠时的复制
wmemchr() memchr() 在内存块中查找字符
wmemcmp() memcmp() 比较内存块
wmemset() memset() 将某一块内存中的内容全部设置为指定的值

安全函数

wprintf_s() printf_s() 安全输出到标准输出流
wscanf_s() scanf_s() 从标准输入流安全输入
sprintf_s() swprintf_s() 安全输出到字符串
sscanf_s() swscanf_s() 安全输入到字符串
fwprintf_s () fprintf_s() 安全输出到文件流
fwscanf_s() fscanf_s() 从文件流安全输入
wcscpy_s() strcpy_s() 安全复制字符串
_wfopen_s() fopen_s() 安全打开文件

前面讲过宽字符和编译器有很大关系,因此这些列出的函数可能和你的编译器有出入,有的并不被支持,有的可能还未列出,但一些常用的宽字符函数,例如wprintf(), getwchar()是一样的,可以注意到宽字符没有对应的gets(),puts()方法,只能使用wprintf()和wscanf(),我想可能是因为不好处理中文空格这样的问题吧。printf()也提供了支持宽字符的格式”%lc”和”%ls”,例如:

#include <stdio.h>
#include<Windows.h>
#include<locale.h>

int main()
{
	setlocale(LC_ALL, "");
	printf("%s", "窄字符\n");
	printf("%ls",L"宽字符\n");
	return 0;
}

长度问题

洞悉了相关处理函数后,我们第一个要解决的问题就是长度问题,长度问题涉及字符长度和字符串长度、窄字符和宽字符长度、中文和英文长度。由于窄字符采用变长,因此有的书籍也将窄字符称为多字节字符,对于多字节字符来说英文和中文所需的字节长度是不同的,宽字符由于平台不同,字符长度也不同,我们通过下面代码进行测试:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<locale.h>

int main()
{
	setlocale(LC_ALL, "");
	printf("%d\n", MB_CUR_MAX);//窄字符的最大字符字节长度

	char c[] = "中";
	printf("%d,%d\n", mblen(c, sizeof(c)), sizeof(c)); //获得窄字符字节长度
	char e[] = "z";
	printf("%d,%d\n", mblen(e, sizeof(e)), sizeof(e)); //获得窄字符字节长度

	wchar_t w = L'z';
	printf("%d\n", sizeof(w));//获取宽字符英文字节长度
	w = L'中';
	printf("%d\n", sizeof(w));//获取宽字符中文字节长度

	char mbs[] = "窄字符abc";
	printf("%u,%u\n", strlen(mbs), sizeof(mbs));//获取窄字符串长度和字节长度

	wchar_t wcs[] = L"宽字符abc";
	printf("%u,%u\n", wcslen(wcs), sizeof(wcs));//获得宽字符串长度和字节长度

	char* p = "abcde";
	wchar_t* p1 = L"abcde";
	printf("%d,%d\n",strlen(p),wcslen(p1));//获得常量字符串长度
	printf("%d,%d\n", sizeof(p), sizeof(p1));//不能对常量字符串指针使用sizeof()求字节长度

	char p2[] = "";
	wchar_t p3[] = L"";
	printf("%d,%d\n", sizeof(p2), sizeof(p3)); //测试空字符串字节长度
	printf("%d, %d\n", sizeof(EOF), sizeof(WEOF));//测试结尾符号字节长度
	return(0);
}

在代码printf("%d\n", MB_CUR_MAX);中,MB_CUR_MAX是stdlib提供的宏,它表示窄字符中最大字符长度,这个宏和操作系统默认语言编码有关,对于GBK来说是2个字符。接下来两行代码用实际字符测试窄字符的长度,由于中文不能用一个char表示,因此窄字符都用字符串表示,字符串后面会多一个“\0”,因此stdlib库提供了更加方便的mblen()函数来检测字符长度,它有两个参数:一个参数是字符串地址,另一个参数是字符串字节长度,测试一下看看是否和MB_CUR_MAX显示的结果相同。从结果可以看到,对于变长的窄字符,英文使用1个字节储存,中文使用2个字节储存。接下来我们测试宽字符字节长度,由于宽字符采用定长,因此中英文的字符字节长度是一样的。最后我们测试字符串长度,可以看到窄字符由于采用变长,其字符串的长度和字节长度都不能作为字符个数的依据,因此包含中英文的窄字符串是无法截取其中内容的,这就是为什么窄字符的函数只适合处理单字节ascii码的原因。宽字符由于采用定长,其字符串长度和字节数都和字符个数相关,可以通过数组索引截取内容。下面我们来测试下字符串常量,结果是字符个数可以正常获取了,但用sizeof()获得的是指针变量的字节大小,因为p不是字符串的集合名称。最后我们测试一下空字符串字节大小,宽字符结束符号也是\0,但它占用两个字节,用L’\0’表示,因此空字符串也占据2个字节。表示文件尾的窄字符常量为EOF,占用2个字节,宽字符常量为WEOF,占用4个字节。通过一系列详细的测试,相信你对窄字符和宽字符如何储存有了更加细致的认识。

截取宽字符串

在字符串处理中,最常见的操作是截取字符串和连接字符串,宽字符由于采用定长,截取字符串的思路和截取数组相同,最笨的方法莫过于使用循环将字符一个个抠出来组成新的字符串,如同下面代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<locale.h>

int main()
{
	setlocale(LC_ALL, "");
	wchar_t str1[] = L"牛奶与coffee";
	int startIndex = 3, len = 6;

	wchar_t str2[20] = L"";
	int i = 0;
	for (i = 0; i < len; i++)  str2[i] = str1[i+startIndex];
	wprintf(L"%s\n",str2);
	return(0);
}

这段代码一旦被老程序员看到,你的水平他就心知肚明了,因为这样写违背了C语言简洁高效的原则,简洁是指代码简明,高效指性能优越。现成的方法摆在面前不用非要绕着弯子重新造车,只能说明你基本函数库都没有学好,简洁高效的代码应该是这样:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<locale.h>

int main()
{
	setlocale(LC_ALL, "");
	wchar_t str1[] = L"牛奶与coffee";
	wchar_t str2[20] = L"";
	wcsncpy(str2, str1 + 3, 6);
	wprintf(L"%s\n",str2);
	return(0);
}

如果不需要生成新的字符串,那么可以直接写成:
wprintf(L"%s\n",str1+3);
当然这只适合截取到尾部的情况,我这里要表达的意思是:请善用指针!

连接宽字符串

将两个字符串连接起来也是最常用的操作之一,通过上面截取宽字符串的例子,你可能已经放弃了循环操作字符这种低效的方式,而是找到连接宽字符的函数wcscat()来解决问题,写出如下代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<locale.h>

int main()
{
	setlocale(LC_ALL, "");
	wchar_t str1[] = L"牛奶";
	wchar_t str2[] = L"coffee";
	wchar_t str3[20] = L"";
	wcscpy(str3, str1);
	wcscat(str3, str2);
	wprintf(L"%s\n",str3);
	return(0);
}

不错,这比用循环操作字符的确精简了不少,但如果将3个、4个字符串连接起来,代码看起来会像这样:
wcscpy(str, str1);
wcscat(str, str2);
wcscpy(str, str3);
….
代码又会变得累赘,我们可以写一个函数,这个函数可以接受任意个字符串并将它们连接起来,如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<locale.h>
#include<stdarg.h>
#include<wchar.h>

void wstrcat(const wchar_t* des, ...)
{
	va_list aptr = NULL;
	va_start(aptr, des);
	wchar_t* arg = NULL;
	do
	{
		arg = va_arg(aptr, wchar_t*);
		wcscat(des, arg);
	} while (wcscmp(arg, L""));
	va_end(aptr);
}

int main()
{
	setlocale(LC_ALL, "");
	wchar_t str1[] = L"牛奶";
	wchar_t str2[] = L"coffee";
	wchar_t str3[20] = L"";
	wstrcat(str3, str1, str2, L"");
	wprintf(L"%s\n", str3);
	return(0);
}

接受可变参数的函数wstrcat()的结束条件为最后一个参数是空字符串,因此每次连接字符串时都必须以空字符串L””结尾,否则就会产生错误。有了这个函数连接字符串就方便多了,你的代码不会再被轻视,然而我们能不能再发挥一下呢?如果能够接受多个参数且类型任意,我们的函数会自动将这些参数转化为字符串就更好了,其实C函数库已经为我们提供了这个函数,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<locale.h>

int main()
{
	setlocale(LC_ALL, "");
	wchar_t str1[] = L"牛奶";
	wchar_t str2[] = L"coffee";
	wchar_t str3[20] = L"";
	swprintf(str3, 20, L"%s%s%d%c\n", str1, str2,100,L'$');
	wprintf(L"%s",str3);
	return(0);
} 

swprintf()可以将格式化字符串写入到一个宽字符串中,格式为:
int swprintf(wchar_t *buffer, size_t count, const wchar_t *format, …)
buffer是目标字符串,count是要写入的字符个数,format是格式化字符。相比之下,使用我们自己写的wstrcat()必须指定结束条件,使用swprint()需要写入每个类型对应的格式符,没有一种方法是完美的,这是C语言自身的特性决定的,因为C语言不能像虚拟机中的高级语言那样可以获取变量类型,也没有虚拟机协助收集变参。在C中只能是你告诉编译器变量类型而不是让编译器告诉你,底层语言只认识二进制,如何解释完全由开发者决定。虽然GUN在GCC扩展中提供了typeof关键字,但typeof并非C标准,很多编译器都不支持,况且这个typeof也不是给你判断数据类型的,它只用来辅助定义数据类型。但高级语言就不同了,因为有了类模板,任何数据类型都可以拥有独一无二的类型,至于高级语言如何描述变量类型和收集变参不在我们的讨论范围之内。

宽字符和数值之间的转换

数值和字符串转换也是常有的事情,这取决于你使用字符流还是字节流传递信息,所有基本数据类型都可以转换为字符串,反过来也一样,将数值转换为字符串很简单,因为有swprint()函数,宽字符甚至没有提供itoa(),ltoa()的对应方法,而将字符串转换为数值就包含一个解析的过程,下面我们先解析一个整数:

#include <stdio.h>
#include <errno.h>
#include <wchar.h>

int main(void)
{
	wchar_t* str = L"100000";
	int a = 0;
	a=wcstol(str, NULL, 10);
	printf("%d\n", a);
}

由于没有提供解析short和int的函数,只能调用wcstol()或wcstoll()进行解析,实际上也没有必要提供,因为能解析long和long long就能解析short和int,wcstol()有三个参数,第1个参数是要转换的字符串;第2个参数为解析结束后的指针位置,wcstol()解析完后会设置该参数,设置为NULL会忽略该参数;第3个参数是数字基数,有效值集合是{0,2,3,…,36}。base-2整数的有效数字集合是{0,1},base-3是{0,1,2},依此类推。对于大于等于10的基数,有效数字开始包含字母字符,从Aa基数为11的整数开始到Zz。显然十进制需将base设置为10,16进制需设置为16。wcstol()完整格式如下:
long wcstol(const wchar_t * str,wchar_t ** str_end,int base)
注意字符串中的数值不要超过long或long long的范围,否则会导致意外的结果。当字符串包含多个空格分隔的数字时,可以利用str_end参数将所有数值解析出来,代码如下:

#include <stdio.h>
#include <errno.h>
#include <wchar.h>

int main(void)
{
	const wchar_t* p = L"10 2000000 30 -40";
	
	wchar_t* end;
	for (long i = wcstol(p, &end, 10); p != end; i = wcstol(p, &end, 10))
	{
printf("%ld\n", i);
		p = end;
	}
}

这段代码使用一个for循环控制解析指针,当发现end指针发生变化时,输出解析结果并将当前指针设置为end,注意要向wcstol()传递&end,因为这个参数通过地址修改end的值,它是指针的指针。

将宽字符串解析为wcstof()和wcstod()的方法和wcstol()类似,这里不再赘述,记住不能解析大于浮点取值范围的数值。

切割宽字符串

C函数库提供了一个具有吸引力的功能,那就是切割字符串,这个函数宽字符和窄字符都有,它们是wcstok()和strtok(),它能使用指定的分隔符将字符串分割成一个一个的小字符串。下面我们看看这个函数是如何运作的:

#include <wchar.h>
#include <stdio.h>

int main(void)
{
	wchar_t input[] = L"A bird came down the walk";
	printf("开始切割字符串 '%ls'\n", input);
	wchar_t* buffer=NULL;
	wchar_t* token = wcstok(input, L" ", &buffer);
	while (token) 
	{
		printf("%ls\n", token);
		token = wcstok(NULL, L" ", &buffer);
	}

	printf("切割后的字符串: ");
	for (size_t n = 0; n < sizeof(input) / sizeof (*input); n++)
		input[n] ? printf("%lc", input[n]) : printf("\\0");
}

wcstok()的格式为:
wchar_t * wcstok(wchar_t * str,const wchar_t * delim,wchar_t ** ptr)
它有3个参数,str为要切割的字符串,delim是分隔符,ptr存储解析器的内部状态。第一次调用wcstok()时,需要传入切割的字符串,当wcstok()在字符串中找到匹配的分隔符时,会将分隔符替换为字符串结束标记\0,并返回第一个结果字符串的首地址,ptr储存分割结束后下一个字符地址。第二次调用将str设置为NULL,表示继续切割,wcstok()根据ptr的位置继续查找下一个分隔符并进行继续切割,直到再也找不到分隔符返回NULL,切割结束。可以看到这个函数需要依赖循环工作,buffer只是一个记录读写地址的指针变量,而不是代表一个字符串,将它初始化为NULL即可,其实每次切割时可以指定不同的分隔符,但很少需要这样做。使用\0分割内容比用空格、制表符、回车的优势在于每个字段中可以包含中文空格、英文空格、\r、\n等内容,结合wscanf()和正则表达式可以将子字符串顺利提取。

搜索宽字符串

搜索字符串也是很常见的操作,我们可以搜索字符也可以搜索字符串,可以正向搜索也可以反向搜索,搜索字符的函数有:
wchar_t * wcschr(const wchar_t * str,wchar_t ch)
wchar_t * wcsrchr(const wchar_t * str,wchar_t ch)
wcschr()为正向搜索,wcsrchr()为反向搜索,参数str为要搜索的字符串,ch为要搜索的字符,它们都返回搜索到的第一个字符的位置,下面代码使用正向搜索和反向搜索在字符串中查找同一字符,并输出所有字符的位置:

#include <wchar.h>
#include <stdio.h>
#include<locale.h>
#include<stdlib.h>

int main(void)
{
	setlocale(LC_ALL, "");
	wchar_t wcs[] = L"在宽字符中搜索字符串wcs,并输出wcs的位置";
	wchar_t* result = NULL;

	puts("正向搜索...");
	result = wcschr(wcs, L'w');
	while (result != NULL)
	{
		printf("%d\n", result - wcs);
		result = wcschr(result+1,L'w');
	}

	puts("反向搜索...");
	result = NULL;
	wchar_t* wcs1 = malloc(sizeof(wcs));
	wcscpy(wcs1, wcs);
	result = wcsrchr(wcs1, L'w');
	while (result != NULL && result != wcs1)
	{
		printf("%d\n", result - wcs1);
		*result = L'\0';
		result = wcsrchr(wcs1, L'w');
	}
	free(wcs1);
}

为了正向搜索出所有的位置,我们使用了循环,每次搜索都从下一个字符位置开始,当到达字符串\0时会返回NULL停止搜索。反向搜索则不同,每次循环都必须从首字符位置开始,如果不去掉已搜索完毕的部分则无法继续反向搜索,因此我们在堆上复制了字符串wcs,每次搜索完毕后用\0截断字符串,全部搜索完毕后释放临时字符串wcs1。

搜索宽字符串只有正向函数:
wchar_t * wcsstr(const wchar_t * dest,const wchar_t * src)
窄字符和宽字符都没有提供反向搜索字符串的函数,原因未知,但我们可以自己实现,代码如下:

#include <wchar.h>
#include <stdio.h>
#include<locale.h>
#include<stdlib.h>

int main(void)
{
	setlocale(LC_ALL, "");
	wchar_t wcs[] = L"在宽字符中搜索字符串wcs,并输出wcs的位置";
	wchar_t obj[] = L"wcs";
	wchar_t* result = NULL;
	int	i = 0;
	int	len = wcslen(wcs);

	puts("正向搜索...");
	for (i = 0; i < len; i++)
	{
		result = wcsstr(wcs+i, obj);
		if (result)
		{
			i = result - wcs;
			printf("%d\n", i);
		}
	}

	puts("反向搜索...");
	
	wchar_t *wcs1 = malloc(sizeof(wcs));
	wcscpy(wcs1, wcs);
	for (i = len - 1; i >= 0; i--)
	{
		result = wcsstr(wcs1 + i, obj);
		if (result)
		{
			i = result - wcs1;
			printf("%d\n", i);
			*result = L"\0";
		}
	}
	free(wcs1);
}

包含与不包含

有时候我们不需要精确查找一个字符串中是否包含另一个字符串,而是想知道这两个字符串是否有交集,这时可以使用测试交集函数:
wchar_t * wcspbrk(const wchar_t * dest,const wchar_t * str)
size_t wcscspn(const wchar_t * dest,const wchar_t * src)
wchar_t * wcsspn (const wchar_t * dest,const wchar_t * src)
wcspbrk()返回dest中第一个交集字符地址,wcscspn()返回搜索dest中第一个交集字符所经历的位置,wcsspn()返回搜索dest中第一个非交集字符所经历的位置,这看上去有点绕,我们用代码来测试结果。

#include <stdio.h>
#include <string.h>
#include<wchar.h>
#include<locale.h>

int main()
{
	setlocale(LC_ALL, "");
	
	wchar_t str1[] = L"好莱坞abc";
	wchar_t str2[] = L"haolaiwu好莱坞电影"; 
	wchar_t* ret;

	ret = wcspbrk(str1, str2);
	if (ret) wprintf(L"在str1中第一个交集字符是: %c\n", *ret);
	else wprintf(L"未找到字符\n");

	wchar_t str3[] = L"zzz好莱坞";
	int n = 0;
	n = wcscspn(str3, str2);
	if(n!=wcslen(str1)) wprintf(L"在str3中第一个交集字符位置为%d\n", n);
	else wprintf(L"未找到字符\n");
	wchar_t str4[] = L"好莱坞abczzz";
	n = wcsspn(str4, str2);
	if (n != wcslen(str1)) wprintf(L"在str4中第一个非交集位置为%d\n",n);
	else wprintf(L"未找到字符\n");
	return(0);
}

当使用wcspbrk()搜索交集字符时,如果搜到返回交集字符的地址,搜不到返回NULL,因此这里用*ret求得搜索到的字符。当用wcscspn()和wcsspn()搜索交集和非交集字符时,如果搜遍字符串还未找到,n等于字符串的长度,否则n表示搜到的字符位置,因此这里通过比较n和字符串长度来判断成功与否。

比较与排序

无论是窄字符还是宽字符在计算机中都以二进制储存,默认情况下比较字符实际是比较它们的编码,我们通过下面代码进行测试:

#include <stdio.h>
#include <string.h>
#include<wchar.h>
#include<locale.h>

int main()
{
	setlocale(LC_ALL, "");

	printf("%d\n", 'a' > 'b');
	printf("%d\n", L'a' > L'b');
	printf("%d\n", '亚' > '非');
	printf("%d\n", L'亚' > L'非');
	printf("%d\n", 'a' == L'a');
	return(0);
}

可以看到对于英文字符,无论是窄字符还是宽字符,都和ascii结果一样,但对于中文就未必了,gbk大致上按照拼英顺序但也不是绝对,unicode就比较混乱了,只有查看编码表或将编码打印出来才能预测结果。

字符串和字符一样按照首字符的编码进行比较,不同的是首字符相同时再比较第二个字符,直到得出结果,因此字符串需要调用wcscmp()函数进行比较。wcscmp()也按unicode编码进行比较,如果想按照区域习惯进行比较,可以使用wcscoll()函数。wcscoll()比wcscmp()强一些,它尽可能按拼英顺序排列而不是按unicode编码,将上面代码中的wcscmp()换成wcscoll()。下面使用wcscoll()将一个字符串数组排序:

#include <stdio.h>
#include <string.h>
#include<wchar.h>
#include<locale.h>

void wcharsort(wchar_t** arr, int len)
{
	if (len < 2) return;
	wchar_t **p, **q;
	wchar_t* k;
	for (p = arr + len - 1; p > arr; p--)
	{
		for (q = arr; q < p; q++)
		{
			if (wcscoll(*q, *(q + 1)) > 0)
			{
				k = *q;
				*q = *(q + 1);
				*(q + 1) = k;
			}
		}
		wchar_t** p1;
	}
}

int main()
{
	setlocale(LC_ALL, "");
	wchar_t** strarr[] = { L"亚洲",L"非洲",L"欧洲",L"South America",L"North America" };
	int len = 5;
	wcharsort(strarr, len);

	wchar_t** p;
	for (p = strarr; p < strarr + len; p++) wprintf(L"%s ",*p);
	puts("-----");
	for (p = strarr; p < strarr + len; p++) wprintf(L"%x ",**p);
	return(0);
}

wcharsort()是一个使用冒泡排序的函数,顺序为从小到大,由于数组长度无法检测,因此需要传入。通过结果可以看到,英文排在中文之前,中文按拼音排序,可以输出具体的unicode编码查看大小。

插入与删除

对于使用数组储存的字符串来说,插入和删除字符是一件具有挑战的事情,因为这会改变数组元素的个数,可能导致溢出或批量移动字符,但挑战是一件快乐的事情,特别是将挑战的的工作完成的出色。既然数组长度固定不可修改,我们就视情况写入到新的数组中,既然批量移动字符性能低下我们就不移动字符。另外最好将插入与删除功能集中到一个函数,这个函数可以对字符串进行任意修改,不仅使用起来非常方便,还能兼顾性能,告知加工的结果,怀着这样的愿望我们写出下面的代码:

#include <stdio.h>
#include <string.h>
#include<wchar.h>
#include<locale.h>
#include<stdlib.h>

wchar_t* splice(wchar_t* dest, wchar_t* src, int index, size_t len, wchar_t *add)
{
	//字符串检测
	if (dest == NULL || src == NULL) return NULL;
	//索引检测
	if (index < 0) index = wcslen(src) + index;
	size_t srclen = wcslen(src);
	if (index > srclen) return NULL;
	//长度检测
	if (index + len > srclen) len = srclen - index;
	//大小检测
	const int WCHAR_WIDTH = sizeof(L'a');
	size_t headsize = (index + 1)*WCHAR_WIDTH;
	size_t	tailsize = (srclen - index - len + 1) * WCHAR_WIDTH;

	//保存头尾
	wchar_t* head = NULL;
	wchar_t* tail = NULL;
	if (index > 0)
	{
		head = malloc(headsize);
		memset(head, 0, headsize);
		wcsncpy(head, src, index);
	}
	if (index + len < srclen)
	{
		tail = malloc(tailsize);
		memset(tail, 0, tailsize);
		wcscpy(tail, src + index + len);
	}

	//拼接
	dest[0] = L'\0';//head==NULL && add==NULL && tail==NULL
 	if (head) wcscpy(dest, head);
	if (add) if(head == NULL) wcscpy(dest, add);else wcscat(dest, add);
	if (tail) if (head == NULL && add == NULL) wcscpy(dest,tail);else wcscat(dest, tail);
	free(head);
	free(tail);

	return dest;
}

int main()
{
	setlocale(LC_ALL, "");
	puts("删除测试");
	wchar_t str[] = L"abcdefghijklmn";
	splice(str, str, 1, 3, NULL);
	wprintf(L"%s\n", str);
	puts("插入测试");
	splice(str, str, 1, 0, L"cba");
	wprintf(L"%s\n", str);
	puts("同时插入删除");
	wchar_t str1[20] = L"";
	splice(str1, str, 3, 4, L"中文字符");
	wprintf(L"%s\n", str1);
	puts("测试全部删除");
	splice(str, str, 0, wcslen(str), NULL);
	wprintf(L"%d\n", wcslen(str));
	puts("测试向空字符串添加内容");
	splice(str, str, 0, 0, L"新的内容");
	wprintf(L"%s\n", str);
	puts("测试在末尾添加内容");
	splice(str, str, wcslen(str), 0, L"abc");
	wprintf(L"%s\n", str);
	puts("测试全部替换内容");
	splice(str, str, 0, wcslen(str), L"替换的内容");
	wprintf(L"%s\n", str);
	puts("测试常量");
	splice(str1, L"abcdefg", 3, 0, L"中文字符");
	wprintf(L"%s\n", str1);
	puts("测试负索引");
	splice(str1, L"abc", -2, 0, L"中文字符");
	wprintf(L"%s\n", str1);
	puts("索引超限测试");
	if(splice(str, L"abc", 5, 0, L"中文字符")==NULL) puts("索引超限");
	
	wchar_t str2[200] = L"";
	wprintf(L"%s\n",splice(str2,splice(str2,L"abc",3,0,L"efg"),6,0,L"hijk"));
	return(0);
}

splice的英文意思为拼接,就像加工管道一样,先将管道从中间截断,切除破损的部分,然后接上新的部分,根据这样的设想,为splice()设置了3个参数,src是要加工的字符串,dest是将加工后的字符串放入的地址,可以是一个新的字符串也可以是原字符串,条件是必须保证dest中的空间足够容纳加工的结果。index是要插入或删除的索引,index可以为负数,-1表示最后一个字符,-2表示倒数第二个字符,以此类推。len是要删除的字符个数,如果len超出最大删除个数会自动计算边界。add是要添加的字符串,NULL表示不添加任何内容。在splice()中首先对参数合理性进行了检测和校验,如果index超限停止执行返回-1。接着我们在堆上申请空间,将截断的字符串头尾保存在堆上,然后进行拼接工作,由于将会更改字符串的内容,因此拼接之前将字符串清空。由于使用vs 2019测试时发现如果不用memset()初始化malloc()申请的内存,wcsncpy()执行时会产生问题,GCC则不会,因此这里加上了memset()初始化语句。如果成功完成拼接工作,splice()返回dest的地址,不成功返回NULL。最后我们在main()中对splice()进行了详细的测试,测试过程中按照具体情况选择保存到新的字符串还是原字符串,保证dest空间一定足够容纳处理的结果,否则会导致溢出。这里有一个问题,为什么splice()内部不能对dest的大小进行测试呢?因为sizeof()只能对集合名进行大小测试,例如数组名,而对指向集合的指针求出的是指针变量的大小,例如形参、指向字符串常量的指针等。splice()不能修改常量字符串,常量字符串可以作为src,但不能作为dest传入。测试结果令人满意,不仅结果正确,性能也颇为出色,因为在splice()中没有使用任何循环,wcscpy()在底层通过复制内存块来拷贝数据,比用循环拷贝字符效率高得多。如果还嫌每次在堆上创建释放空间拖慢了速度,可以修改splice(),手动传入保存头尾的缓存地址来避免动态开辟空间,就像wcstok()一样。有了这个splice()这个方法后,修改字符串就变得容易多了,最后一段代码我们直接用一句话将3个字符串连接起来,这样又多了一种连接多个字符串的方式。

宽字符和窄字符互相转换

如果既有的内容不可更改,例如函返回值是窄字符串,而我们统一使用宽字符串,就需要手动转化,stdlib库提供了4个转换函数,如下:
size_t mbstowcs( wchar_t* dst, const char* src, size_t len) 把窄字符串转换为宽字符串,按字符个数进行转换
size_t wcstombs(char *dst, const wchar_t *src, size_t n) 把宽字符串转换为窄字符串,按字节数进行转换
int mbtowc(whcar_t *pwc, const char *str, size_t n) 把窄字符转换为宽字符,按字节数进行转换
int wctomb(char *str, wchar_t wchar) 把宽字符转换为窄字符
前面两个函数转化字符串,后面两个函数转化字符,我们先来看看字符串的转换,对于mbstowcs()和wcstombs()来说,dst为目标字符串地址,src为源字符串地址,len为转化的字符个数,n为转换的字节数,下例将一个窄字符串转为宽字符串:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<locale.h>

int main()
{
	setlocale(LC_ALL, "");
	char mbs[] = "窄字符";
	wchar_t wcs[4] = L"";
	mbstowcs(wcs, mbs, 3);
	wprintf(L"%s", wcs);
	return(0);
}

窄字符和宽字符相互转换时最需要关注的是给予新的字符数组空间是否能够容纳转换后的字符,空间小了会导致溢出,空间大了又会导致浪费,因此前面讨论的字符长度问题是保证转换代码正确基石。对于能够目测字符个数的字符串,例如:“ABC英语”,相互转换时不会有任何问题,所需空间口算都可以得出,但对于复杂的多字节字符串,例如:“女神Any有一个男朋友Tom,他们在一年前结婚并生下了Tony,今年又生下了Jim,后面的故事很复杂……”,这么长一句话就很难目测了,但我们仍然可以通过前面所学的知识找出规律写出很漂亮的代码。将宽字符串转换为窄字符串很好办,因为宽字符串空间肯定足够容纳窄字符串,转换代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<locale.h>

int main()
{
	setlocale(LC_ALL, "");
	wchar_t wcs[] = L"女神Any有一个男朋友Tom,他们在一年前结婚并生下了Tony,今年又生下了Jim,后面的故事很复杂……";
	int len = sizeof(wcs);
	char* mbs=malloc(len);
	printf("设置的空间为%d\n", len);
	int ret=wcstombs(mbs, wcs, len);
	printf("转换的字节数为%d\n", ret);
	len=ret+1;
	mbs=realloc(mbs, len);
	printf("重设的空间大小为%d\n", len);
	printf("%s\n", mbs);
	return(0);
}

这段代码在堆上创建窄字符串,先将宽字符空间大小用于窄字符,然后根据实际转换的字符数确定窄字符需要的空间,最后重设窄字符的空间,这样一来没有丝毫的空间浪费。ret是wcstombs()函数转换并写入mbs中的实际字符个数,不包含结尾的\0,因此重设的空间大小为ret+1。

将窄字符转换为宽字符时,由于字符采用变长,字符个数和所需的空间都不确定,但我们知道宽字符需要2个或4个字节保存,实际字符宽度可以通过代码求出,将窄字符的空间扩大2倍或4倍肯定够用。按照这个思路也可以顺利写出下面的代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<locale.h>

int main()
{
	setlocale(LC_ALL, "");

	wchar_t wc = L'中';
	const int WC_CUR_MAX = sizeof(wc);
	char mbs[] = "女神Any有一个男朋友Tom,他们在一年前结婚并生下了Tony,今年又生下了Jim,后面的故事很复杂……";
	int len = sizeof(mbs) * WC_CUR_MAX;
	wchar_t* wcs = malloc(len);
	printf("设置的空间为%d\n", len);
	int ret = mbstowcs(wcs, mbs, strlen(mbs));
	printf("转换的字节数为%d\n", ret);
	len = (ret + 1) * WC_CUR_MAX;
	wcs = realloc(wcs, len);
	printf("重设的空间大小为%d\n", len);
	wprintf(L"%s\n", wcs);
	return(0);
}

我们先测试一个宽字符所占的字节数,然后将结果放入常量WC_CUR_MAX中,WC_CUR_MAX就是后面空间要扩展的倍数。注意mbstowcs()中第3个参数是写入的宽字符最大字符个数,返回值是实际转化的宽字符个数,而wcstombs()中第3个参数是写入的窄字符最大字节数,返回值是实际转化的窄字符字节数,这也是因为窄字符是变长,无法确认字符个数,只能用字节长度来处理。

学会字符串转换后,字符转换就相对简单了,因为窄字符和宽字符长度都是已知的,所以代码很好写出,例如:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<locale.h>

int main()
{
	setlocale(LC_ALL, "");
	//将窄字符转化为宽字符
	char cstr[] = "中";
	wchar_t w;
	int	ret = mbtowc(&w, cstr, sizeof(cstr));
	printf("转换了%d个字符\n", ret);
	putwchar(w);

	//将宽字符转化为窄字符
	wchar_t w1 = L'美';
	char cstr1[3]="";
	ret = wctomb(cstr1, w1);
	printf("转换了%d个字节\n", ret);
	puts(cstr1);
	return(0);
}

由于中文字符仍然需要用窄字符串表示,因此必须传入窄字符串,宽字符就未必了,在wctomb()参数中参数wchar是按值传递的,无论是bstowc()还是wctobs(),返回值都是处理的字节数。掌握宽字符和窄字符互相转换的方法后代码变得自由,如果某个功能宽字符没有而窄字符有,可以先将宽字符转为窄字符,将处理后的结果还原为宽字符。例如:

#include <wchar.h>
#include <stdio.h>
#include<locale.h>
#include<stdlib.h>

int main(void)
{
	setlocale(LC_ALL, "");
	double pi = 3.141592654;
	char s[20] = "";
	wchar_t ws[20] = L"";
	gcvt(pi, 10, s);
	mbstowcs(ws, s, 20);
	wprintf(L"%s\n",ws);
}

gcvt(),ecvt(),fcvt()都是宽字符没有的浮点转字符串函数,可以先将浮点值转为窄字符串,然后在转换为宽字符串。

兼顾宽字符和窄字符

C语言在发展过程中有很多兼容性问题,字符编码问题是其中一个,混合窄字符和宽字符的代码复杂性会大大增加,而且不便于维护,特别是移植困难,为了简化代码提升兼容性,C函数库和IDE环境也做出了一些努力,例如printf()增加了对宽字符的支持。微软的vs在tchar库中提供了一种新的字符类型TCHAR来统一char和wchar_t,TCHAR根据宏UNICODE来决定采用char还是wchar_t,如果UNICODE未被定义采用char,相反则采用wchar_t,winnt库中的TEXT()方法则根据UNICODE宏将字符串解释为宽字符串和窄字符串。在virsual studio中将项目字符集设置为Unicode会自动定义宏UNICODE,设置为多字节编码会自动定义宏_MBCS,它们分别代表项目使用宽字符和窄字符,如图:
在这里插入图片描述

这个选项只会影响宏的定义,不会设置字符编码,使用Unicode时仍然需要setlocale()设置区域。TCHAR和TEXT()适合同样用宏定义的函数,例如debugapi库中的OutputDebugString()函数,这个函数用来在vs的输出窗口显示调试信,从而代替将调试信息显示到控制台。OutputDebugString也是一个宏,实际上指向OutputDebugStringW和OutputDebugStringA两个函数,分别处理宽字符和窄字符,并且也是通过UNICODE宏来区分的,因此OutputDebugString既可以输出宽字符串也可以输出窄字符串,下面使用OutputDebugString()方法输出一个调试信息:

#include <stdio.h>
#include<Windows.h>

int main()
{
	OutputDebugString(TEXT("这是一条调试信息"));
	return 0;
}

由于Windows.h中已经对tchar.h,winnt.h,debugapi.h进行了导入,还导入了wchar.h等宽字符需要的头文件,因此只需要导入Windows.h。无论将项目字符集设置为Unicode还是多字节这段代码都可以运行,由于OutputDebugString()输出目的地为IDE的输出信息窗口,它甚至不需要setlocale()来设置区域,这也是它比printf()方便的地方。然而TCHAR,TEXT(),OutputDebugString(),还有兼顾宽字符和窄字符的一系列输入输出函数都不是C标准库提供的内容,它们只能用于windows,对于其它平台就不适用了。

安全函数

由于C不对数组进行边界检测,输入输出时就可能发生越界,如果这种机制被黑客用来进行溢出攻击就相当危险,考虑到安全性,微软的VS将可能发生溢出的标准输入输出函数换成缀为s的安全函数,默认情况下如果在VS中使用了会溢出的标准库函数就不能通过编译,这个开关可以在项目设置中,如图:
在这里插入图片描述

将SDL检查设置为是意味着强制使用安全函数,使用标准输入输出函数的代码将变得不兼容。安全函数原理很简单,在输入输出内容时添加一个长度参数,这个参数可以确保输入输出时不产生溢出,例如在scanf_s()中输入字符和字符串时必须指定个数:

#include <stdio.h>
int main(void)
{
	int i, b;
	scanf_s("%d %d", &i, &b); //输入整数没有区别
	getchar();
	char c, s[80];
	scanf_s("%c%s", &c, 1, s, 80);  //字符c输入1个,c后面跟1,s字符数组80个,所以s后面加80。
}
getchar()由于没有溢出的风险,因此这个函数没有被替换。宽字符也有对应的安全函数,例如:
#include <wchar.h>
#include <stdio.h>
#include<locale.h>
#include<stdlib.h>

int main(void)
{
	setlocale(LC_ALL, "");
	wchar_t str1[] = L"安全拷贝函数";
	wchar_t str2[20] = L"";
	wcscpy_s(str2,20,str1);
	wprintf_s(L"%s\n", str2);
}

wcscpy()中的第二个参数需要明确指出str2的大小,如果str1的字符个数超过了指定的大小就会导致wcscpy()报错,报错比代码被攻破要强得多,至少在运行时可以捕获错误。

操作内存块

内存操作是C语言的特色,虽然通过指针可以对某个地址的内存进行操作,但用指针操作一个内存块效率极为低下,因为需要依赖循环,为了提升性能,C语言提供了操作内存块的函数,这些函数在底层由汇编实现,并且分为宽字符和窄字符操作函数,我们一起来看看。

设置内存块

使用某个值对内存块进行初始化是最常见的操作,例如对malloc()申请的内存进行初始化或者对已有的内存块进行擦除,这会用到memset()和wmemset()两个函数,一个填充窄字符,一个填充宽字符,两个函数的格式如下:
void *memset(void *s, int ch, size_t n)
wchar_t * wmemset(wchar_t * dest,wchar_t ch,size_t count)
memset()中的参数ch类型为int,实际上窄字符内存操作函数都被设计为字节操作,ch可以是一个char,也可以是0~255的数值,数据超过一个字节只会取末尾8位。虽然这两个函数可以只填充内存块中的一部分,但我们很少需要这么做,大部分情况下都用于初始化内存块或数据集合,使用memset()初始化malloc()声明字符串在前面例子中已经见过,用memset()初始化比str[0]=’\0’或者说wmemset()比str[0]=L’\0’更加彻底,某些编译器可能因为不彻底的字符串初始化产生极为隐藏的bug,但大多数情况不需要这么做。wmemset()只能初始化宽字符串,memset()却常常用于初始化数组和结构体,请看下面代码:

#include <stdio.h>
#include <string.h>
#include<wchar.h>
#include<locale.h>
#include<stdlib.h>

struct MyStruct
{
	char name[20];
	int age;
};

int main()
{
	setlocale(LC_ALL, "");
	
	wchar_t wstr[5]=L"";
	wmemset(wstr, L'中',4);
	wprintf(L"%s\n",wstr);

	int a[10];
	memset(a, -1, sizeof(a));
	for (int i=0; i < 10; i++) printf("%d ",a[i]);

	struct MyStruct tom = {"Tom",18};
	printf("%s,%d\n",tom.name,tom.age);
	memset(&tom, 0, sizeof(tom));
	printf("%s,%d\n", tom.name, tom.age);

	return 0;
}

在上面代码中,我们使用wmemset()将wstr中的所有字符设置为”中”字,用memset()将数组a中的所有元素设置为-1,且将结构体tom中的内容清空。由于memset()中的参数类型为void*,因此可以接受任何类型的变量,但memset()按字节进行初始化,而数组中的元素是int,如果设置为0或-1以外的数会破坏int的结构,其值会和期望的不同,例如填充字节设置为2,对于2个字节的int会变为00000010 00000010,值为514,因此memset()只能用0清空数组而不能将所有元素设置为某个值。对于结构体来说,各个成员类型往往不同,更无法设置为0以外的数值。

复制内存块

当需要将内存块中的数据复制到另一个地方时,可以调用memcpy()和wmemcpy(),原型为:
void *memcpy(void *destin, void *source, unsigned n)
wchar_t * wmemcpy(wchar_t * dest,const wchar_t * src,size_t count)
wmemcpy()按宽字符复制,只适用于宽字符串,memcpy()按字节复制,适用于各种数据类型,当我们想把一个数组中的内容复制到另一个数组中时,不要再使用循环复制元素这种低效的方式,直接复制内存块吧;当我们复制宽字符串时也可以选择wmemcpy(),请看下面代码:

#include <stdio.h>
#include <string.h>
#include<wchar.h>
#include<locale.h>
#include<stdlib.h>


int main()
{
	setlocale(LC_ALL,"");
	int a[10] = {0,1,2,3,4,5,6,7,8,9};
	int	b[10];

	memcpy(b, a, sizeof(a));
	for (int i = 0; i < 10; i++) printf("%d ",b[i]);

	wchar_t wstr1[] = L"中文字符";
	wchar_t wstr2[20]=L"";
	wstr1[0] = L'\0';
	wmemcpy(wstr2, wstr1, 4);
	wprintf(L"%c%c%c\n", wstr2[1], wstr2[2], wstr2[3]);
}

这段代码中我们先用memcpy()复制了一个整数数组,复制长度为sizeof(a)而不是元素的个数,wmemcpy()复制的长度为宽字节字符个数,窄字符内存操作函数总是操作字节,宽字符内存操作函数总是操作字符个数。wmemcpy()与wcscpy()和wcsncpy()不同的是即使遇到结尾字符\0也不会停止复制,内存操作函数总是会忽略字符串结尾的\0。下面我们来看看另一个问题,将数据从一个数组复制到另一个数组很容易,因为复制区域没有重叠,但在同一个数组中复制就不是那么简单了,复制区域可能发生重叠,而重叠又有两种情况,如下:
在这里插入图片描述

假设复制时是从第一个元素开始,对于第一种情况不会有问题,src中的元素会从左到右顺利复制到对应的位置,但对于第二种情况,src中最后两个元素拷贝的是已经被覆盖的元素,这显然会导致结果错误,为此C提供了memmove()和wmemmove()两个函数,用于第二种情况时保证结果的正确性,但是现代编译器认为memcpy()和wmemcpy()改进后本身就可以保证正确性,虽然会多一些判断,但优化后的memcpy()和wmemcpy()性能差距可以忽略不计。而move名字更容易产生歧义,因为其本质也是复制,还要在特定情况下进行函数的选择,为此现代编译器已经不区分它们,通过下面代码可以验证它们是否有区别:

#include <stdio.h>
#include <string.h>
#include<wchar.h>
#include<locale.h>
#include<stdlib.h>


int main()
{
	setlocale(LC_ALL, "");
	wchar_t s[] = L"1234567890";
	wchar_t p1[11] = L"";
	wmemcpy(p1, s, 10);

	wchar_t* p2 = p1 + 2;
	wmemcpy(p2, p1, 5);
	wprintf(L"%s\n", p1);

	wchar_t p3[11] = L"";
	wmemcpy(p3, s, 10);

	wchar_t* p4 = p3 + 2;
	wmemmove(p4, p3, 5);
	wprintf(L"%s\n", p3);
}

这段代码先复制字符串s中的内容到p1,p3,然后依据第二种重叠情况将p1,p3复制到p2,p4,p2使用wmemcpy(),p4使用wmemmove(),如果输出结果相同,那么今后可以放心的使用wmemcpy(),如果不相同为了保险可以统一使用wmemmove()。

搜索内存块

可以直接对内存中的数据进行搜索,相比字符串搜索来说,内存搜索不受字符串结尾的影响,memchr()按字节进行搜索,数据类型任意,wmemchr()只能搜索宽字符串,它们的原型为:
void *memchr(const void *str, int c, size_t n)
wchar_t * wmemchr(const wchar_t * ptr,wchar_t ch,size_t count)
只要告诉搜索起点和终点,就可以找出这段内存中的第一个匹配数据的位置,下面通过wmemchr()找出字符串“奥林匹克运动会”中的“匹”字:

#include <stdio.h>
#include<wchar.h>
#include<locale.h>
#include<stdlib.h>


int main()
{
	setlocale(LC_ALL, "");
	wchar_t s[] = L"奥林匹克运动会";
	wchar_t* p = wmemchr(s, L'匹', wcslen(s));
	if(p) putwchar(*p);
}

相比wcsstr(),wmemchr()可以更灵活的定义搜索范围,例如只搜索前4个字符,即使遇到字符串结尾也不会停止搜索,搜索长度也可以超过字符个数。memchr()更加灵活,它甚至可以搜索结构体内部的数据,例如:

#include <stdio.h>
#include <string.h>
#include<stdlib.h>

struct MyStruct
{
	char a;
	char b;
	char c;
};

int main()
{
	struct MyStruct s = { 'a','b','c' };
	char* p = memchr(&s, 'b', sizeof(s));
	putchar(*p);
}

比较内存块

可以按照宽字符或字节比较两个内存块,这两个函数是:
int memcmp(const void *str1, const void *str2, size_t n)
int wmemcmp(const wchar_t * lhs,const wchar_t * rhs,size_t count)
wmemcmp()显然只能比较宽字符串,相比wcscmp()和wcsncmp()功能并没有增强,但会忽略字符串结尾,memcmp()可以比较任意类型的内存块,例如:

#include <stdio.h>
#include <string.h>
#include<wchar.h>
#include<locale.h>
#include<stdlib.h>

int main()
{
	int a[5] = {0,1,2,3,4};
	int b[5] = {0,1,2,3,5};

	int flag = memcmp(a, b, sizeof(a));
	printf("%d\n",flag);

	flag = wmemcmp(L"abc",L"123",3);
	printf("%d\n", flag);
	return 0;
}

memcmp()从第一个元素开始比较整形数组a和b,当发现元素4不同时返回结果-1,wmemcmp()比较两个宽字符串常量,由于第一个字符a的ascii码大于字符1,因而返回1。

读写文本文件

从以前学过的知识我们知道,文件读写与标准输入输出思路相同,只不过输入输出的对象不同,对于标准输入输出来说,通常输出到控制台的字符编码与代码文本编码相同,获取内容时会将键盘输入的内容转化为与代码文本相同的编码,而宽字符的本质是保存时将编码转为unicode,输出时转为窄字符编码。按照这个思路,当读取窄字符文件时,保存的文本编码应该是代码文本使用的编码,当读取文本文件时,编译器会认为文本文件采用的编码与代码使用的编码相同,这就会导致乱码。我们先用下面代码保存一句中文:

#include <stdio.h>

int main()
{
	FILE* fp;
	char str[13];
	//写入一个文件
	if ((fp = fopen("file2.txt", "w")) == NULL) puts("文件打开失败");
	else
	{
		fputs("你好!世界!", fp);
		fclose(fp);
	}

	return 0;
}

如果运行环境为windows,找到这个文件,用记事本打开,在另存为对话框中可以看到文本编码,这里是ANSI,如图:
在这里插入图片描述

将编码改为utf-8后保存,然后在保证路径正确的情况下读取该文本,如下:

#include <stdio.h>

int main()
{
	FILE* fp;
	char str[13];

	//读取刚才写入的文件
	if ((fp = fopen("file2.txt", "r")) == NULL) puts("文件打开失败");
	else
	{
		fgets(str, 13, fp);
		puts(str);
		fclose(fp);
	}
}

会发现输出乱码:
在这里插入图片描述

这告诉我们,文本文件的读写与当前代码使用的编码有很大关系,如果无需处理文本,可以直接选择窄字符,否则只能改用宽字符。那么宽字符的文件读写是怎样的呢?下面改用fputws()输出到文本:

#include <stdio.h>
#include<wchar.h>
#include<locale.h>
#include<stdlib.h>


int main()
{
	setlocale(LC_ALL,"");
	FILE* fp;
	wchar_t str[20];
	//写入一个文件
	if ((fp = fopen("file2.txt", "w")) == NULL) puts("文件打开失败");
	else
	{
		fputws(L"你好!世界!", fp);
		fclose(fp);
	}

	return 0;
}

对于宽字符来说,无论输出到哪里,都要设置unicode区域,否则会导致运行时编码转换失败,输出到文件与输出到控制台一样,本质上是将宽字符转化为窄字符保存,因此打开文件可以发现,文本编码依然是ANSI。下面我们修改读取文件的代码,将fgets()改为fgetws():

#include <stdio.h>
#include<wchar.h>
#include<locale.h>
#include<stdlib.h>


int main()
{
	setlocale(LC_ALL,"");
	FILE* fp;
	wchar_t str[20];

	//读取刚才写入的文件
	if ((fp = fopen("file2.txt", "r")) == NULL) puts("文件打开失败");
	else
	{
		fgetws(str, 20, fp);
		wprintf(L"%s\n",str);
		fclose(fp);
	}

	return 0;
}

可以看到文件内容被顺利读取,在windows中fgetws()会将ANSI文本转化为unicode-16保存到str中,在Linux中会将utf-8转为utf-16保存。在windows中尝试将文本改为utf-8读入仍然显示为乱码,换做另外两个读写字符的函数fgetwc()和fputwc()也一样,宽字符和窄字符都受到当前代码文本编码的影响。现在问题来了,如果读取的文件的编码和代码编码不同,怎么才能正确输入?C语言提供了一个简单的解决方案,请看下面代码:

#include <stdio.h>
#include<wchar.h>
#include<locale.h>
#include<stdlib.h>


int main()
{
	setlocale(LC_ALL, "");
	FILE* fp;
	wchar_t str[20];

	//读取刚才写入的文件
	if ((fp = fopen("file2.txt", "r,ccs=utf-8")) == NULL) puts("文件打开失败");
	else
	{
		fgetws(str, 20, fp);
		wprintf(L"%s\n", str);
		fclose(fp);
	}

	return 0;
}

在fopen()中,可以通过ccs参数指定读取的编码为unicode,允许的值为UNICODE, utf-8,以及UTF-16LE。同样通过设置ccs可以将文本保存为unicode编码,如下:

#include <stdio.h>
#include<wchar.h>
#include<locale.h>
#include<stdlib.h>


int main()
{
	setlocale(LC_ALL, "");
	FILE* fp;
	wchar_t str[20];
	//写入一个文件
	if ((fp = fopen("file2.txt", "w,ccs=utf-8")) == NULL) puts("文件打开失败");
	else
	{
		fputws(L"你好!世界!", fp);
		fclose(fp);
	}

	return 0;
}

如果使用了ccs,fopen()首先尝试使用ccs读取打开的文件,如果成功,此函数将读取 BOM 以确定文件的编码;如果失败,此函数将使用代码使用的编码作为默认编码载入。这种简单的处理方式告诉我们,如果载入的文本文件编码不能被编译器所识别,必须使用记事本这样的工具将它转化为Unicode,然后配合setlocale()读写,这样可以读取日文、韩文等多国文字,但这并未解决根本问题,我们不能总是依赖记事本这样的工具来帮我们转换,如果能提供一个函数帮助我们转码就好了,然而这不是一件轻松的事情,因为转码是一件复杂的工作。就拿中文来说,unicode和gbk之间并不存在映射的公式,只能对照码表查找,这就意味着转码函数还要纳入码表,而包含各种语言码表的函数库体积可想而知,这是一个庞大的项目,显然不会纳入C语言的标准库,要想读取任意编码文件或者将当前编码转为任意编码只能依赖第三方库。

转码工具

对于转码来说有不少第三方库可以选择,做的比较好的是GNU提供的libiconv库,它支持大部分欧洲语系和犹太语系,多字节语言支持中文、日文、韩文、泰语、越南语等等,由于使用的比较多,很多linux直接包含了它,在linux中可以直接调用,iconv进行转码,例如:
iconv -f utf8 -t gbk test -o test.gbk
安装了libiconv库后就可以在C中导入iconv.h,然后用iconv()函数进行转码,下例使用libiconv库将utf-8转为gb2312:

#include <iconv.h>
#include <stdio.h>
#include <string.h>

int code_convert(char* inbuf, int inlen, char* outbuf, int outlen)
{
	iconv_t cd;
	int rc;
	char** pin = &inbuf;
	char** pout = &outbuf;

	cd = iconv_open("UTF-8", "GB2312");
	if (cd == 0) return -1;
	memset(outbuf, 0, outlen);
	if (iconv(cd, pin, &inlen, pout, &outlen) == -1) return -1;
	iconv_close(cd);
	return 0;
}

int main() {
	wchar_t aa = L"中文";
	char src[100] = "\xcc\xb7\xbf\xad";
	char dst[100];
	int srclen = 100;
	int dstlen = 100;
	int ret = code_convert(src, srclen, dst, dstlen);
	printf("%d\%s\%s\n", ret, src, dst);
	return 0;
}

关于libiconv的安装和使用方法可以查看官方网站,地址为:http://www.gnu.org/software/libiconv。

总结

字符串处理是一个程序员基本功底的体现,字符串处理不仅包含复杂的逻辑,还对性能、边界、内存溢出有着较高的要求。C属于底层语言,它使用原始的数组来储存字符串,因此边界、溢出等问题显得特别突出。C中的字符还分为窄字符和宽字符,这又加上了历史遗留问题,所以用C语言处理好字符串很不容易。由于C没有面向对象的封装能力,并且不能支持完整的正则表达式,更不支持XML,因此在处理高级问题时显得力不从心,即便是不太复杂的字符串处理,相比javascript这样的高级语言来说编写效率也是非常低下的。我们看重的是它的性能,当进行高负载字符串处理时,底层语言的性能和内存可控性就体现出来了,例如用C编写文字搜索引擎,或者对大量文本进行转码等工作。

  • 3
    点赞
  • 2
    评论
  • 12
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

1. 概述.............................................................................................................................................1 1.1. 自然语言&计算机语言................................................................................................1 1.2. 计算机语言 & C/C++语言..........................................................................................2 1.3. 简单的C/C++程序及其运行方法(环境的使用)................................................2 1.3.1. C/C++程序开发运行环境....................................................................................2 1.3.2. 格式输出函数printf()和格式输入函数scanf()....................................................3 1.4. 习题..............................................................................................................................5 2. 基本的C语言................................................................................................................................6 2.1. C语言的名词——类型、量值(常量和变量)....................................................6 2.1.1. 整型和整型量值...................................................................................................6 2.1.2. 浮点型和浮点量(常量和变量).......................................................................8 2.1.3. 字符型和字符量(常量和变量).......................................................................9 2.1.4. 字符串常量.........................................................................................................10 2.2. C语言的动词—运算符,短语-表达式 和和特殊动词性关键字....................11 2.2.1. 赋值运算符和赋值表达式.................................................................................11 2.2.2. 算术运算符和算术表达式.................................................................................12 2.2.3. 逻辑运算符和逻辑表达式.................................................................................13 2.2.4. 关系运算符和关系表达式.................................................................................14 2.2.5. 其它运算符和表达式.........................................................................................15 2.2.
©️2021 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值