今天继续我们的 hello, world! 之旅:#include<stdio.h>#include<stdlib.h>intmain(void){ printf("hello, world!\n"); return EXIT_SUCCESS;}
对于上面这个简单程序,我们已经讲过很多(请参见文末的相关阅读),但关于 printf 函数,我们还没有说过什么,今天就来谈谈 printf。printf 函数是在 stdio.h 头文件中声明(declaration)的:intprintf(constchar* format, ...); // C99 以前intprintf(constchar* restrict format, ...); // C99 以后
C99(请参见相关阅读)以前,printf 函数是用第1行的形式声明的,而从 C99 开始,printf 函数则用第2行的形式声明。第2行的声明多了一个 restrict,意思是限制,表示对 format 字符串有所限制,目的是优化。这个我们以后再说。去掉 restrict 以后,两个声明是一样的。所以,我们就按照第1行的样子进行讨论。首先,printf 是个函数,目的是将字符串(如"hello, world!\n")进行格式化(注意函数声明中的 format)以后写到标准输出(stdout)上,目前可以简单地认为标准输出就是指屏幕(严格地说是控制台屏幕)。其次,printf 函数的返回类型,与我们已经熟悉的 main 函数一样,也是整型(int),这个整型数的意义是:如果 printf 执行成功,则是写出去的字符数,如果 printf 执行失败,则是一个负数。一般来讲,printf 很少失败,但也可能由于设备问题或字符串的编码问题而导致 printf 执行失败。第三,printf 函数的参数很奇怪,包括两部分:- const char* format:这是一个很复杂的声明,也可以分成两部分来看:
- char* format:这表明 format 是一个指向字符串的指针,目前可以大致上理解为 format 是一个字符串,不必纠结于指针的细节。
- const char* format:我们已经知道 char* format 表示 format 是一个字符串,前面再加上 const,表示 format 是一个常量字符串,如下面所表示的:printf("hello, world!\n");此时,format 就等于 "hello, world!\n"。用引号括起的字符串在 C 中就是一个常量字符串。只不过这个常量字符串中不含有格式符(见下面的说明)。
- ...:这个省略号代表一个参数列表,只不过这个参数列表是不确定的,参数个数可能是 0 个或多个。这种参数个数不确定的函数称为可变参数函数(variadic function)。
最典型的可变参数函数就是 printf 函数。printf 是一个非常复杂的函数,它的复杂性不仅体现在参数的不确定上,还体现在格式控制上。上面我们说,format 是一个常量字符串,这个常量字符串可以含有格式符(format specifier),用来对 format 后面的参数进行格式化。格式符通常由一个百分号(%)后面跟一个字母表示,下面的表格列举了一些常用的格式符:有了字符串和格式符的知识,我们可以将上面的最简 C 程序改写为下面的形式:#include<stdio.h>#include<stdlib.h>intmain(void){ const char* str = "hello, world!"; printf("%s\n", str); return EXIT_SUCCESS;}
上面代码的第6行,变量 str 被声明为一个常量字符串(我们这里先不严格地将 char* 称为字符串)类型,并给它赋值为 hello, world!。用双引号将 hello, world! 括起,表明这是一个常量字符串。赋值号(=)左边是一个常量字符串类型的变量,右边是一个常量字符串,左右两边类型相符,所以这个赋值是合法的。另外,因为这个赋值发生在变量声明的同时,这个赋值也称为变量 str 的初始化(initialization)。第7行的 printf 函数有了一些变化,此时的 printf 可以用一般格式表示如下:此时的 printf 带有两个参数,一个格式参数 format(即"%s\n"),一个字符串类型的参数 string。因为格式参数中的格式符是 %s,要求后面的参数必须是字符串类型。如果不是字符串类型,或者说,格式符和相应的参数类型不一致,则 C 语言标准对其行为无定义,这也就是说,不确定会发生什么事情。下面我们来看看实际会发生什么。首先,修改上面的代码:#include<stdio.h>#include<stdlib.h>intmain(void){ const char* str = "hello, world!"; printf("%s\n", 10); return EXIT_SUCCESS;}
注意,上面的代码第7行,本来应该是 printf("%s\n", str);,现在 str 改成了 10,也就是说,字符串变量变成了一个整数。下面是编译和执行结果:首先,MSVC 的编译就给出了警告,告诉你 printf 的格式字符串 %s 要求一个类型为 char* 的参数,而给出的参数类型却是 int(整型)。虽然给出了警告,但程序还是完成了编译。然而,执行 helloworld.exe 却什么也没显示,用 echo 命令查看返回值,虽然我们的程序中还是用 return EXIT_SUCCESS 返回成功代码,但实际的返回值却是 -1073741819,这个10进制数的16进制是 C0000005,这是一个访问违例(Access Violation)的错误代码,表示程序试图访问不该访问的内存地址。前面我们说过,printf 执行失败时返回小于 0 的值,可不可以通过 printf 的返回值来判断上面的代码执行是否成功呢?答案是:不可以。因为上面代码的问题不是通过返回值来表示的,而是通过抛出异常来表示的。关于异常,我们以后再讲。与 Pascal 将 I/O 功能作为语言定义的一部分不同,C 的 I/O 功能是通过库函数实现的,这些实现 I/O 功能的库函数都声明在 stdio.h 头文件中。今天主要讨论了 printf 这个最为经常使用的库函数,随着内容的展开,以后我们会介绍更多的实现 I/O 功能的库函数。到今天为止,关于 hello, world! 这个最简 C 程序,我们已经谈了四次。程序虽小,但“五脏俱全”,能够谈论的内容还有很多,但需要其他特别是数据类型、变量与表达式、控制程序流程的语句等相关内容,需要慢慢展开。我们的 hello, world! 之旅,到今天就结束了。以后将是更为广阔的天地。