C语言学习(十二)C语言中的字符(宽字符与窄字符)、从字符谈谈C语言的编码、转义字符
字符串是多个字符的集合,他们由
" " 包围,如
"http://www.baidu.com" 。字符串中的字符在内存中按照次序、紧挨着排列,整个字符串占用一块连续的内存。
当然,字符串也可以只包含一个字符,例如"A" ,不过一般我们使用专门的字符类型来处理这种只包含一个字符的情况。
常用到的字符类型是char,他的长度为1,只能容纳ASCII码表中的字符,也就是英文字符。
如果想处理汉语、日语、汉语等,就需要使用其他的字符类型,char是做不到的,我们一般使用wchar_t。下面我们详细说说。
英文字符
字符的表示
char字符类型由单引号' ' 包围,而字符串才用双引号" " 包围。
例:
char a = '1';
char b = '$';
char c = 'X';
char d = ' ';
char x = '中';
char y = 'A';
char z = "t";
说明:在字符集中,全角和半角字符对应的编号(或者说编码值)不同,是两个完全不一样的字符。ASCII码只定义了半角字符,没有定义全角字符。
字符的输出
输出char类型的字符有两种办法,分别是:
- 使用专门的字符输出函数putchar
- 使用格式化输出函数printf,char对应的格式控制符是%c。
例:
#include <stdio.h>
int main() {
char a = '1';
char b = '$';
char c = 'X';
char d = ' ';
putchar(a); putchar(d);
putchar(b); putchar(d);
putchar(c); putchar('\n');
printf("%c %c %c\n", a, b, c);
return 0;
}
运行结果: 1 $ X 1 $ X
putchar函数每次只能输出一个字符,输出多个字符需要调用多次。
字符与整数
我们知道,在计算机中存储字符时并不是真的要存储字符实体,而是存储字符在字符集中的编号(编码值)。对于char类型,他实际上存储的就是字符的ASCII码。
无论在哪个字符集中,字符编号都是一个整数。从这个角度看,字符类型和整数类型本质上没什么区别。
我们也可以给字符类型赋值一个整数,或者以整数形式输出字符类型。反过来也可以给整数类型赋值一个字符,或者以字符形式输出整数。
例:
#include <stdio.h>
int main()
{
char a = 'E';
char b = 70;
int c = 71;
int d = 'H';
printf("a: %c, %d\n", a, a);
printf("b: %c, %d\n", b, b);
printf("c: %c, %d\n", c, c);
printf("d: %c, %d\n", d, d);
return 0;
}
输出结果: a: E, 69 b: F, 70 c: G, 71 d: H, 72
在 ASCII 码表中,字符 ‘E’、‘F’、‘G’、‘H’ 对应的编号分别是 69、70、71、72。
a、b、c、d实际上存储的都是整数:
- 当给a、d赋值一个字符时,字符会先转换成ASCII码再存储
- 当给b、c赋值一个整数时,不需要任何转换,直接存储就可以
- 当以%c输出a、b、c、d时,会根据ASCII码表将整数转换成对应的内存
- 当以%d输出a、b、c、d时,不需要任何转换,直接输出就可以
可以说,ASCII码表将英文字符和整数关联了起来。
初识字符串
之前我们提到了字符串的概念,也了解过如何输出字符串,但是没有说如何用变量存储一个字符串。其实在c语言中,没有专门的字符串类型,只能使用数组或者指针来间接的存储字符串。
数组和指针的概念,后面我们会具体说,这里先记下这两种字符串的表示方式:
char str1[] = "http://www.baidu.com";
char *str2 = "百度一下,你就知道";
[]和*是固定写法,我们这里先记住。他们可以通过专用的puts函数和通用的printf函数输出。
演示:
#include <stdio.h>
int main()
{
char web_url[] = "http://www.baidu.com";
char *web_name = "百度一下,你就知道";
puts(web_url);
puts(web_name);
printf("%s\n%s\n", web_url, web_name);
return 0;
}
中文字符
c语言不仅能处理英文,他是一门全球化的编程语言,他支持世界上任何一个国家的语言文化,包括中文、日语、韩语等。
中文字符的存储
正确存储中文需要解决两个问题。
1)足够长的数据类型
char只能处理ASCII编码中的英文字符,是因为char字符太短,只有一个字节,容纳不了中文几万个汉字,要想处理中文字符,必须得使用更长的数据类型。
一个字符在存储前会转换成他在字符集中的编号,这样的编号是一个整数,所以我们可以用整型来存储一个字符,比如unsigned short、unsigned int、unsigned long等。
2)选择包含中文的字符集
c语言规定,对于使用ASCII编码外的单个字符,如汉语、日语、韩语等,也就是专门的字符类型,需要使用宽字符的编码方式。常见的宽字符编码有UTF-16和UTF-32,他们都是基于Unicode字符集的,能支持全球的语言文化。
在真正实现时,微软编译器(内嵌于Visual Studio或Visual C++中)采用UTF-16编码,使用2个字节存储一个字符,用unsigned short类型就可以容纳。GCC、LLVM/Clang(内嵌于Xcode)采用UTF-32编码,使用4个字节存储,用unsigned int就可以容纳。
对于编号较小的字符,UTF-16采用2个字节存储;对于编号较大的字符,UTF-16使用4个字节存储。但是,全球常见的字符也就几万个,使用两个字节足够,只有极其罕见,或者很古老的字符才会用到4个字节。 微软编译器使用两个字节来存储UTF-16编码的字符,虽然不能包含所有的Unicode字符,但也足够容纳全球常见的字符了。基本满足了软件开发需求。使用2个字节存储的另一个好处是可以节省内存,而使用4个字节会浪费50%以上的内存。
不同编译器可以使用不同的整数类型。如果我们的代码使用unsigned int来存储宽字符,那么在微软编译器下就是一种浪费;如果我们的代码用unisigned short来存储宽字符,那么在GCC、LLVM/Clang下就不够。
为了解决这个问题,C语言推出了一种新的类型,叫做wchar_t。w是wide的首字母,t是type的首字符,wchar_t的意思就是宽字符类型。wchar_t的长度由编译器决定:
- 在微软编译器下,他的长度是2,等价于unsigned short
- 在GCC、LLVM/CLang下,他的长度是4,等价于unsigned int
wchar_t 其实是用typedef关键字定义的一个别名,typedef的用法我们以后再说。现在我们只需要知道 wchar_t 在不同编译器下长度不一样。
前文我们说到,单独的字符由单引号' ' 包围,如'A' 、'B' 等。但是这样的字符只能使用ASCII编码,要想使用宽字符编码的方式,就要加上L 前缀,如L'A' 、L'B' 、L'中' 等。
注意,加上 L 前缀后,所有字符都将成为宽字符,占用2个字节或4个字节的内存,包括ASCII中的英文字符。
例:
wchar_t a = L'A';
wchar_t b = L'9';
wchar_t c = L'中';
wchar_t d = L'国';
wchar_t e = L'。';
wchar_t f = L'ヅ';
wchar_t g = L'?';
wchar_t h = L'?';
之后,我们将不加 L 前缀的字符成为窄字符,将加上 L 前缀的字符成为宽字符。窄字符使用ASCII编码,宽字符使用UTF-16或UTF-32编码。
宽字符的输出
putchar、printf智能输出不加 L 前缀的窄字符,要想输出宽字符,我们必须使用 <wchar.h> 头文件中的宽字符输出函数,他们分别是 putwchar 和 wprintf :
- putwchar 函数专门用来输出一个宽字符,他和putchar的用法类似
- wprintf是通用的、格式化的宽字符输出函数,他出了可以输出单个字符,还可以输出字符串。宽字符对应的格式控制符为
%lc 。
另外,在输出宽字符之前还要使用 setlocale 函数进行本地化设置,告诉程序如何才能正确的处理各个国家文化。
如果希望设置为中文简体环境,在Windows下写作:
setlocale(LC_ALL, “zh-CN”);
在Linux和Mac OS下写作:
setlocale(LC_ALL, “zh_CN”);
setlocale 函数位于 <locale.h> 头文件中,使用前我们必须引入他。
完整演示宽字符的输出:(Linux、Mac OS)
#include <wchar.h>
#include <locale.h>
int main(){
wchar_t a = L'A';
wchar_t b = L'9';
wchar_t c = L'中';
wchar_t d = L'国';
wchar_t e = L'。';
wchar_t f = L'ヅ';
wchar_t g = L'?';
wchar_t h = L'?';
setlocale(LC_ALL, "zh_CN");
putwchar(a); putwchar(b); putwchar(c); putwchar(d);
putwchar(e); putwchar(f); putwchar(g); putwchar(h);
putwchar(L'\n');
wprintf(
L"Wide chars: %lc %lc %lc %lc %lc %lc %lc %lc\n",
a, b, c, d, e, f, g, h
);
return 0;
}
运行结果: A9中国。ヅ??
Wide chars: A 9 中 国 。 ヅ ? ?
宽字符串
给字符串加上 L 前缀就变成了宽字符串,他包含的每个字符都是宽字符,一律采用UTF-16或UTF-32编码。输出宽字符串可以使用 <wchar.h> 头文件中的 wprintf函数,对应的格式控制符是%ls 。
如何使用宽字符串演示:
#include <wchar.h>
#include <locale.h>
int main(){
wchar_t web_url = L"http://www.baidu.com"
wchar_t *web_name = L"百度一下,你就知道";
setlocale(LC_ALL, "zh_CN");
wprintf(L"web_url: %ls \nweb_name: %ls\n", web_url, web_name);
return 0;
}
运行结果: web_url: http://www.baidu.com web_name: 百度一下,你就知道
实际上,不加 L 前缀的窄字符也可以处理中文,那么他和加了 L 前缀的宽字符串又有什么区别呢?我们接着往后看。
C语言到底使用什么编码?char类型的字符串使用什么编码?
c语言是70年代的产物,那个时候只有ASCII,各个国家的字符编码都还未走向成熟,所以c语言不可能从底层支持GB2312、GBK、Big5等国家编码,也不可能支持Unicode字符集。
有点c语言编程基础的可能认为c语言使用ASCII编码,字符在存储时会被转成ASCII码值,这是不对的。在c语言中,只有char类型的窄字符才使用ASCII编码,char类型的窄字符串、wchar_t 类型的宽字符和宽字符串都不能使用ASCII编码!
wchar_t类型的宽字符和宽字符串使用UTF-16和UTF-32编码。现在还剩下一个char类型的窄字符串,我们来详细说说。
对于char类型的窄字符串,c语言并没有规定使用哪种特定的编码,只要选用的编码能够适应当前的环境就可以。所以,char类型窄字符串的编码与操作系统和编译器有关。
但是,可以肯定的说,现在计算机中,char类型窄字符串已经不再使用ASCII编码了,因为ASCII编码只能显示字母、数字等英文字符,对汉语、日语、韩语等其他地区的字符无能为力。
我们要从两个方面来聊聊char类型窄字符串的编码。
源文件使用什么编码
源文件用来保存我们编写的代码,他最终会被存储到本地硬盘,或者远程服务器,这个时候就要尽量压缩文件体积,以节省硬盘空间或者网络流量,而代码中大部分字符都是ASCII编码中的字符,用一个字节足以容纳,所以 UTF-8 编码是一个不错的选择。
UTF-8兼容ASCII,代码中大部分字符串可以用一个字节保存;另外UTF-8基于Unicode,能支持全世界的字符。
常见的IDE或者编辑器,如Xcode、Sublime Text、Vim等,在创建源文件时一般默认使用UTF-8编码。不过 Visual Studio默认使用本地编码来创建源文件。
所谓本地编码,就是像GBK、Big5、Shift-JIS等这样的国家编码(地区编码);针对不同国家发行的操作系统,默认的本地编码一般不同。简体中文的Windows默认的本地编码是GBK。
对于编译器来说,他往往支持多种编码格式的源文件。微软编译器、GCC、LLVM/Clang(内嵌于Xcode)都支持UTF-8和本地编码文件,不过微软编译器还支持UTF-16编码的源文件。如果考虑到通用性,就只能使用UTF-8和本地编码了。
char类型窄字符串使用什么编码
前面我们说到,用puts或者printf可以输出字符串,代码如下:
#include <stdio.h>
int main()
{
puts("百度一下");
printf("http://www.baidu.com");
return 0;
}
"百度一下" 和"http://www.baidu.com" 就是需要被处理的窄字符串,程序运行过后,他们会被载入到内存中。这里还用到了中文,肯定不能使用ASCII编码。 1)微软编译器使用本地编码来保存这些字符。不同地区的Windows版本默认的本地编码不一样,所以,同样的窄字符串在不同Windows版本下使用不一样。对于简体中文版的Windows,使用的是GBK编码。
2)GCC、LLVM/Clang编译器使用和源文件相同的编码来保存这些字符:如果源文件使用的是UTF-8编码,那么这些字符也使用UTF-8编码;如果源文件使用GBK编码,那么这些字符也使用GBK编码。
对于代码中需要被处理的窄字符串,不同编译器差别还是很大的。不过可以肯定的是,这些字符始终都使用窄字符(多字节字符)编码。
正是由于这些字符使用UTF-8、GBK等编码,而不是使用ASCII编码,所以他们才能包含中文。
总结
对于char类型的窄字符,始终使用ASCII编码。
对于wchar_t类型的宽字符和宽字符串,使用UTF-16或者UTF-32编码,他们都是基于Unicode字符集的。
对于char类型的窄字符串,微软编译器使用本地编码,GCC、LLVM/Clang使用和源文件编码相同的编码。
另外,处理窄字符和处理宽字符用的函数也不一样:
- stdio.h 头文件中的putchar、puts、printf函数只能用来处理窄字符
- wchar.h 头文件中的putwchar、wprintf函数只能用来处理宽字符
关于编码字符集和运行字符集
站在专业的角度来说,源文件使用的字符集被称为编码字符集,也就是写代码的时候使用的字符集;程序中的字符或者字符串使用的字符集被称为运行字符集,也就是程序运行后使用的字符集。
源文件需要保存到硬盘,或者在网络上传输,使用的编码要尽量节省存储空间,同时要方便跨国交流,所以一般使用UTF-8,这就是选择编码字符集的标准。
程序中的字符或者字符串,在程序运行后必须被载入到内存,才能进行后续的处理,对于这些字符来说,要尽量选用能够提高处理速度的编码,如UTF-16和UTF-32编码就能够快速定位(查找)字符。
编码字符集是站在存储和传输的角度,运行字符集是站在处理或者操作的角度,所以他们并不一定相同。
转义字符
字符集(Character Set)为每个字符分配了唯一的编号,我们不妨将它称为编码值。在c语言中,一个字符出了可以用他的实体(也就是真正字符)表示,还可以用编码值表示。这种用编码值来间接地表示字符的方式称为转义字符(Escape Character)
转义字符以\ 或者\x 开头,以\ 开头表示后跟八进制形式的编码值,以\x 开头表示后跟十六进制形式的编码值。对于转义字符来说,只能使用八进制或者十六进制。
字符 1、2、3、a、b、c 对应的 ASCII 码的八进制形式分别是 61、62、63、141、142、143,十六进制形式分别是 31、32、33、61、62、63。下面的例子演示了转义字符的用法:
char a = '\61';
char b = '\141';
char c = '\x31';
char d = '\x61';
char *str1 = "\x31\x32\x33\x61\x62\x63";
char *str2 = "\61\62\63\141\142\143";
char *str3 = "The string is: \61\62\63\x61\x62\x63"
转义字符既可以用于单个字符,也可以用于字符串,并且一个字符串中可以同时使用八进制形式和十六进制形式。
转义字符的初衷是用于ASCII编码,所以他的取值范围有限:
- 八进制形式的转义字符最后后跟三个数字,也即
\ddd ,最大取值是\177 - 十六进制形式的转义字符最多后跟两个数字,即
\xdd ,最大取值是\x7f 。
超出范围的转义字符行为是未定义的,有的编译器会将编码值直接输出,有的会报错。
对于 ASCII 编码,0~31(十进制)范围内的字符为控制字符,它们都是看不见的,不能在显示器上显示,甚至无法从键盘输入,只能用转义字符的形式来表示。不过,直接使用 ASCII 码记忆不方便,也不容易理解,所以,针对常用的控制字符,C语言又定义了简写方式,完整的列表如下:
\n 和\t 是最常用的两个转义字符:
\n 用来换行,让文本从下一行的开头输出,前面我们已经多次使用\t 用来占位,一般相当于四个空格,或者tab键的功能
单引号、双引号、反斜杠是特殊字符,不能直接表示:
- 单引号是字符类型的开头和结尾,要使用
\' 表示,也即'\'' - 双引号是字符串的开头和结尾,要使用
\" 表示,也即"abc\"123" - 反斜杠是转义字符的开头,要使用
\\ 表示,也即'\\' ,或者"abc\\123" 。
转义字符例子:
#include <stdio.h>
int main(){
puts("C\tC++\tJava\n\"C\" first appeared!");
return 0;
}
运行结果: C C++ Java “C” first appeared!
|