从调用printf()到能看到输出的字符

0 引入

看如下最简单的C程序:

int main(int argc, char** argv)
{
    printf("ABC");
    return 0;
}

本文就是力图描述这个程序的执行过程,具体来说,就是从调用printf(),到“ABC”三个字符显示到显示器上,到底是一个什么样的过程。

1 第一阶段: printf()最终调用write()写入终端

使用strace跟踪执行上面的程序,可以发现,最终导致调用了 write(1, "ABC", 3)。也就是最终的效果就是向终端设备写入三个字节。

现在我们把简单的ABC换成中文试试。

int main(int argc, char** argv)
{
    printf("中文");
    return 0;
}

同样使用strace跟踪执行,发现最终调用的是: write(1, "\344\270\255\346\226\207", 6)。为便于查看,换成16进制表示,就是向终端设备写入 E4 B8 AD E6 96 87 共6个字节。进一步说,其实就是“中文”这两个字的UTF-8编码。之所以是UTF-8编码,是因为这个C源文件本身就是使用UTF-8编码的。下面我们使用GBK对同样内容的源代码进行编译运行,再用strace跟踪,会发现最终调用的是: write(1, "\326\320\316\304", 4),换成16进制,就是向终端写入 D6 D0 CE C4 共4个字节,而这正是“中文”两字的GBK编码。

由此可见,printf(“字符串")输出最终的结果就是把字符串的编码写入终端设备,而如何编码取决于源文件的存储编码方式。实际上,编码是由编译器完成的。printf只是把给他的参数当成一个字节数组而已,其本身不了解也不需要字符编码的概念。

这显然会导致一些问题,比如UTF-8编码的源文件编译生产的执行文件,只能输出字符串的UTF-8编码,一旦运行在非UTF-8的终端上,则无法正确显示(或者需要使用调用iconv工具 进行转码)。显然这种“编译时就确定字符编码”的方式非常死板。那么如何改进呢,为此printf()提供了一个 %ls 指示符,与之相对应的则是wchar_t类型的字符串,也叫做宽字符。每一个wchar_t类型的字符在内存中占用4个字节,其内容是该字符的UNICODE编码(注意不是UTF-8),而且其编码方式时固定的,不会因为源文件的存放编码改变而改变。为了与传统的char进行区别,声明常量时使用 L"字符”的格式。下面是宽字符版本的源文件。

int main(int argc, char** argv)
{
    printf(”%ls", L"中文");
    return 0;
}

通过%ls,printf就会了解到后面的指针指向的是一个宽字符串,而不是多字节字符串(简单的字节数组),这样在最终调用write写入终端前,就会把这个宽字符串进行相应的字符编码,然后输出。通过strace 调试运行,我们发现最终会调用 write(1, "-N", 2),显然这是不正确的。原因就在于调用write前的字符编码有问题。我们先来看看宽字符串“中文”的内存。为便于gdb调试,稍微修改一下源程序:

int main(int argc, char** argv)
{
    wchar_t str[] = L"中文";
    printf("%ls", str);
    return 0;
}

用gdb调试,查看str指向的内存:

(gdb) p str
$2 = L"中文"
(gdb) x /12xb &str
0xbffff684:     0x2d    0x4e    0x00    0x00    0x87    0x65    0x00    0x00
0xbffff68c:     0x00    0x00    0x00    0x00

可见每个字符占用4个字节,内容为UNICODE代码点,这个常量字符串的内存结构当然是编译时就已经确定的。那么为什么最后会导致编码为 "-N"了呢。原因在于printf("%ls")对宽字符串编码时,依据的是程序运行时的locale,而我们在程序中没有明确设置locale,那么就会采用默认的 C locale,也就是会把宽字符转换为对应的ASCII码,这种转换其实无法进行,所以只是简单的“不转换”,这样当遇到第三个字节0x00时,就认为字符串结束了,而前面的两个字节 2D 4E 其实正是 -N 两个字符的ASCII码。

虽然转码不成功,但是至少是在运行时进行了转码。下面,我们稍微改动程序,添加设置locale的功能。

int main(int argc, char** argv)
{
    setlocale(LC_ALL, argv[1]);
    wchar_t str[] = L"中文";
    printf("%ls", str);
    return 0;
}

这样,我们就可以在运行时为程序提供locale的值,继续使用strace跟踪, strace ./a.out zh_CN.UTF-8,我们发现最终会调用 write(1, "\344\270\255\346\226\207", 6),这次把宽字符串按照UTF-8进行了编码,然后调用write写入终端了,由于当前终端也是采用的UTF-8编码,所以能正确显示出“中文”两字。那么如果终端编码为GBK的呢?没问题,只要执行程序时,提供zh_CN.GBK这个参数就可以了。如下:

./a.out zh_CN.GBK

继续跟踪发现最终调用 write(1, "\326\320\316\304", 4),可见,写入的是"中文"的GBK编码。

做个小结:(1)printf("%s")或printf("")都是把字符串当成字节数组直接调用write写出,不涉及字符编码。

                  (2)printf("%ls")把宽字符串根据运行时的locale进行编码后,调用write写出。

                  (3)通过setlocale()来设置字符编码规则。

这里有四个地方涉及到字符编码:(1)源文件(2)字符串的内存表示(3)printf内部的转码(4)终端本身

只要(3)中的转码能够正确进行,并且(3)和(4)采用相同的字符编码,那么应用程序部分就做够了正确显示字符串的准备了,至于最终是否能在显示器上正确显示出字符串,还要实际操作系统和实际设备的支持,也就是第二阶段。

2 第二阶段: 终端调用显示设备显示

系统调用write(1, 字节数组,长度)会最终调用终端设备的_write()函数,该函数再调用底层硬件(如显卡)驱动控制显示器显示。我们先来看看简单字符串ABC的情况,因为无论采用任何类型的终端设备都可以正确显示它们。

终端按照底层设备,可以分成三大类型。一是底层输出设备就是文字模式的VGA显卡显示器;二是底层输出设备是图形模式的VGA显示设备;三是伪终端设备,底层输出设备是其他GUI系统中的窗口。上图分别针对这三种终端粗略探讨了显示过程。对于字符模式VGA,显卡固件里的字符发生器从来都不支持中文;对于图形模式显卡,理论上只要有中文字形位图那么就可以显示,但是实际上终端软件本身并不支持Unicode编码,也就无法显示中文,非官方有一些软件如zhcon一定程度上支持中文,但是远不完善;对于第三种,几乎所有的GUI窗口都支持中文输出,是目前最完美的终端类型。

3 不同平台的差异

本文在Linux平台讨论printf(),但是printf()是C标准库函数,理论上所有平台通用。但是实际上不同平台的printf()还有有不少差异的,特别是在引入wchar_t之后。例如就wchar_t本身来说,gcc编译时大小为4字节,而VC++编译时却是2字节大小;又如wprintf()函数,在VC++中不支持%ls而是采用%s来表示宽字符串。

字符编码问题是计算机世界里的最基础最重要的一个主题,在UNICODE仍未完全统一全世界的软件之前,各种编码转换让字符串处理更加复杂。乱码问题始终困扰着广大的程序员,深入理解编码原理与转换细节是解决乱码问题的不二选择,坚持使用UNICODE是减少各种麻烦的良好习惯。

时间: 2024-11-08 20:16:24

从调用printf()到能看到输出的字符的相关文章

【Shell脚本学习15】shell printf命令:格式化输出语句

printf 命令用于格式化输出, 是echo命令的增强版.它是C语言printf()库函数的一个有限的变形,并且在语法上有些不同. 注意:printf 由 POSIX 标准所定义,移植性要比 echo 好. 如同 echo 命令,printf 命令也可以输出简单的字符串: $printf "Hello, Shell\n" Hello, Shell $ printf 不像 echo 那样会自动换行,必须显式添加换行符(\n). printf 命令的语法: printf format-s

JAVA中调用CMD命令,并输出执行结果

package com.wzw.util; import java.io.BufferedReader; import java.io.InputStreamReader; public class CmdDemo { public static void main(String[] args) { BufferedReader br = null; try { Process p = Runtime.getRuntime().exec("net user"); br = new Bu

shell printf命令:格式化输出语句

printf 命令用于格式化输出, 是echo命令的增强版.它是C语言printf()库函数的一个有限的变形,并且在语法上有些不同. 注意:printf 由 POSIX 标准所定义,移植性要比 echo 好. 如同 echo 命令,printf 命令也可以输出简单的字符串: $printf "Hello, Shell\n" Hello, Shell $ printf 不像 echo 那样会自动换行,必须显式添加换行符(\n). printf 命令的语法: printf format-s

C++输出中文字符(转)

C++输出中文字符 1. cout 场景1: 在源文件中定义 const char* str = "中文" 在 VC++ 编译器上,由于Windows环境用 GBK编码,所以字符串 "中文" 被保存为 GBK内码,编译器也把 str 指向一个包含有 GBK编码的只读内存空间.用 cout 输出 str 时, 由于中文Windows环境用GBK编码,所以把GBK编码的 str 内容输出到控制台,没问题. 场景2: 在Linux 下编辑一个文件 const char*

[笔记]Go语言在Linux环境下输出彩色字符

Go语言要打印彩色字符与Linux终端输出彩色字符类似,以黑色背景高亮绿色字体为例: fmt.Printf("\n %c[1;40;32m%s%c[0m\n\n", 0x1B, "testPrintColor", 0x1B) 其中0x1B是标记,[开始定义颜色,1代表高亮,40代表黑色背景,32代表绿色前景,0代表恢复默认颜色.显示效果为: 下面代码遍历全部显示效果. package main import ( "fmt" ) func main

C++学习45 流成员函数put输出单个字符 cin输入流详解 get()函数读入一个字符

在程序中一般用cout和插入运算符“<<”实现输出,cout流在内存中有相应的缓冲区.有时用户还有特殊的输出要求,例如只输出一个字符.ostream类除了提供上面介绍过的用于格式控制的成员函数外,还提供了专用于输出单个字符的成员函数put.如:    cout.put('a');调用该函数的结果是在屏幕上显示一个字符a.put函数的参数可以是字符或字符的ASCII代码(也可以是一个整型表达式).如    cout.put(65 + 32);也显示字符a,因为97是字符a的ASCII代码. 可以

python print输出unicode字符

命令行提示符下,python print输出unicode字符时出现以下 UnicodeEncodeError: 'gbk' codec can't encode character '\u30fb 不能输出 unicode 字符,程序中断. 解决方法: sys.stdout = io.TextIOWrapper(sys.stdout.buffer, errors = 'replace', line_buffering = True) python print输出unicode字符,布布扣,bu

【转】python在终端输出彩色字符

终端的字符颜色是用转义序列控制的,是文本模式下的系统显示功能,和具体的语言无 关. 转义序列是以 ESC 开头,可以用 \033 完成相同的工作(ESC 的 ASCII 码用十进制表 示就是 27, = 用八进制表示的 33). \033[显示方式;前景色;背景色m 显示方式:0(默认值).1(高亮).22(非粗体).4(下划线).24(非下划线). 5(闪烁).25(非闪烁).7(反显).27(非反显) 前景色:30(黑色).31(红色).32(绿色). 33(黄色).34(蓝色).35(洋

php输出中文字符

中文字符不可以使用imagettftext()函数在图片中直接输出,如果要输出中文字符,需要先使用iconv()函数对中文字符进行编码,语法格式如下:string iconv ( string $in_charset, string $out_charset, string $str )说明:参数$in_charset是中文字符原来的字符集,$out_charset是编码后的字符集,$str是需要转换的中文字符串.函数最后返回编码后的字符串.这时使用imagettftext()函数就可以在图片中