第 4 章 字符串和格式化输入/输出
前导程序
// filename talkback.c
#include
#include
#define DENSITY 62.4 // 人的密度
int main(void)
{
float weight, volume;
int size, letters;
char name[40];
printf("Hi! What's your first name?\n");
scanf("%s", name); // 数组不需要加取地址符,因为数组名本身就是地址。
printf("%s, what's your weight in pounds?\n", name);
scanf("%f", &weight);
size = sizeof name;
letters = strlen(name);
volume = weight / DENSITY;
printf("Well, %s, your volume is %2.2f cubic feet.\n", name, volume);
printf("Also, your first name has %d letters, \n", letters);
printf("and we have %d bytes to store it in.\n", size);
return 0;
}
程序交互如下 :
Hi! What's your first name?
winn
winn, what's your weight in pounds?
139
Well, winn, your volume is 2.23 cubic feet.
Also, your first name has 4 letters,
and we have 40 bytes to store it in.
该程序有如下新特性:
- 使用一个数组(array)来存放字符串,
char name[40]
该数组的 40 表明有40个字节的连续存储空间。 - 使用
%s
转换说明符(conversion specification)来处理字符串的输入和输出。 - 使用了C预处理器定义了代表值 62.4 的符号常量(后面会详细说明)。
- 使用了头文件
string.h
中的strlen()
函数来获取字符串长度。
字符串简介
字符串 (character string)就是一个或多个字符的序列。
"Zing went the strings of my heart ! "
双引号不是字符串的一部分。双引号用于通知编译器其中包含了一个字符串,正如单引号标识着一个字符一样。
char 数组类型和空字符
C将字符串存储在 char 数组中。字符串中字符存放在相邻的存储单元,每个字符占一个单元。
数组的最后一个位置显示空字符
\0
。它代表空字符(null character),用来标记字符串的结束。
由于空字符的存在,数组的单元数会在存储的字符数基础上加一。
字符与字符串的区别在于是否有空字符,
'x'
属于基本类型(char),
"x"
则属于派生类型(char数组)。
字符串的打印和输入需要使用类型说明符 %s
。
除了使用
%c
说明符,一般scanf()
读取输入会在遇到的第一个空白字符空格(blank)、制表符(tab)、或者换行符(newline)处停止读取。所以一般只会读取一个单词而不是整个语句。
strlen()
函数和 sizeof
运算符的区别
使用 strlen()
函数需要包含 #include
(或 #include
)
strlen()
是一个函数,sizeof
是运算符(圆括号对于类型是必须的,但对于具体量是可选的)。
strlen()
计算字符串的字符长度(不含空字符),sizeof
计算占用的内存单元大小(会包含空字符)。
常量和 C 预处理器
用符号常量代替数字常量(增加可读性)还可以达到一改全改方便使用。
#define PI 3.14159
注意:预定义语句,这里不会有一般语句的分号和赋值符号,变量名使用大写利于区分一般变量(也有使用
c_
或k_
前缀来表示常量的做法)。
使用 #define
语句实际上实在编译之前对源文件进行的替换操作(类似于 #include
),称为编译时代入法(compiletime substitution),这样定义的常量称为明显常量(manifest constant)。
const 修饰符
C90新增了创建符号常量的第二种方法,使用 const
关键字把一个变量声明转换成常量声明(变为具有只读属性的变量)。
const int MONTHS = 12;
还有第三种创建符号常量的方法,枚举(enum)会在后面讲到。
系统定义的明显变量
C 头文件 limits.h
和 float.h
分别提供有关整数类型和浮点类型大小限制的详细信息
例如 limits.h
文件包含了与下面类似的行:
#define INT_MAX +32767 // int类型的最大可能值
#define INT_MIN -32768 // int类型的最小可能值
那么可以使用如下代码:
#include
printf("Maximum int value on this system = %d\n", INT_MAX); // 具体输出结果由计算机决定
书中P66 ~ 67 表4.1和4.2 展示了
limits.h
和float.h
这两个头文件中的一些符号常量。
研究和利用 printf() 和 scanf()
printf() 和 scanf() 函数使您能够与程序通信。它们被称为输入/输出函数(I/O函数),是最通用的 I/O 函数。
它们不是 C 的定义的一部分,最初的时候 C 把输入和输出的实现留给了编译器的编写者,这样可以更好地使 I/O 与特定的机器相匹配。
为了兼容性起见,不同的实现中都带有各自的 printf() 和 scanf() ,但是它们之间偶尔有一些差异。
大多数使用的都是 C90 和 C99 描述的这些函数的标准版本。
printf() 函数
使用 printf()
函数打印变量的指令取决于变量的类型,例如打印整数时使用 %d
符号,打印字符时使用 %c
符号,这些符号被称为转换说明(conversion specification)。
下面表4.3,列出了 ANSI C 标准为 printf() 提供的各种转换说明:
转换说明 | 输出 |
---|---|
%c | 一个字符 |
%d 和 %i | 有符号的十进制整数 |
%u | 无符号的十进制整数 |
%f | 浮点数、十进制计数法 |
%e 和 %E | 浮点数 e- (E-)计数法 |
%a 和 %A | 浮点数、十六进制数字和p-(P-)计数法 (C99) |
%g 和 %G | 根据数值不同自动选择 %f 或%e(%E)。%e 格式在指数个数小于-4或大于等于精度时使用 |
%o | 无符号的八进制整数 |
%x 和 %X | 使用十六进制数字 0f(0F)的无符号十六进制整数 |
%p | 指针 |
%s | 字符串 |
%% | 打印一个百分号 |
#include
#define PI 3.141593
int main(void)
{
int number = 3;
float expresso = 13.5;
int cost = 3100;
printf("The %d CEOs drank %f cups of expresso. \n", number,
expresso);
printf("The value of pi is %f. \n", PI);
printf("Farewell! thou art too dear for my possessing, \n"); // 仅仅打印数据无需任何转换说明
printf("%c%d\n", '$',2 * cost); // 使用常量和变量进行打印
return 0;
}
程序的输出如下:
The 3 CEOs drank 13.500000 cups of expresso.
The value of pi is 3.141593.
Farewell! thou art too dear for my possessing,
$6200
printf()
的使用格式
printf(Control-string, item1,item2, ...);
-
item1,item2
等等都是要打印的项目。可以是变量,也可以是常量,甚至可以是在打印之前进行计算的表达式。 - 控制字符串(control-string)是一个描述项目如何打印的字符串。为每个要打印的项目包含一个转换说明符。
打印项目必须和转换说明两者数目保持一致。
printf() 的转换说明修饰符
可以在 % 和定义转换字符之间通过插入修饰符对基本的转换说明加以修改
表 4.4 printf()修饰符
修饰符 | 含义 |
---|---|
标志 | 五种标志(-、+、空格、# 和 0)可以使用零个或者多个标志。示例:"%-10d" |
digit(s) |
字段宽度的最小值,如果无法容纳要打印的数或字符串,系统会自动使用更宽的字段。示例:"%4d" |
.digit(s) |
精度。对于%e、%E 和 %f 有转换,表示小数点的右边打印的数字位数。 对于 %g 和 %G 转换,是有效数字的最大位数。对于 %s 转换,是将要打印的字符的最大数目。对于整数转换,是将要打印的数字的最小位数;如果必要,要使用前导零来达到这个位数。只需使用. 表示其后跟随一个零,所以%.f 和 %.0f 相同。 示例: "%5.2" 打印一个浮点数,它的字段宽度为 5 个字符,小数点后有两个数字。 |
h | 和整数转换说明符一起使用,表示一个 short int 或者 unsigned short int 类型的值。 示例:"%hu" 、"%hd" |
hh | 和整数说明符一起使用,表示 signed char 或 unsigned char 类型的值。示例:"%hhu" 、"%6.4hhd" |
j | 和整数转换说明一起使用,表示 intmax_t 或 uintmax_t 类型的值。这些类型定义在stdint.h 。示例:%jd 、%8jx |
l | 和整数转换说明一起使用,表示 long int 或 unsigned long int 类型的值。示例:"%ld" 、"%8lu" |
ll | 和整数转换说明一起使用,表示 long long int 或 unsigned long long int 类型的值(C99)。示例:"%lld" 、"%8llu" |
L | 和浮点数转换说明一起使用,表示 long double 类型的值。示例:"%Ld" 、"%10.4Le" |
t | 和整数转换说明一起使用,表示 ptrdiff_t 类型的值。ptrdiff_t 是两个指针差值的类型(C99) ...........................示例:"%ld" 、"%8lu" |
z | 和整数转换说明一起使用,表示 size_t 类型的值。size_t 是 sizeof 返回类型(C99)示例:"%zd" 、"%12zd" |
表 4.5 printf() 的标志
修饰符 | 意义 |
---|---|
- (负号) |
打印的项目是左对齐的,会在字段的左侧开始打印。 示例:"%-20s" |
+ (加号) |
打印出来是带正负符号的。示例:"%+6.2f" |
(空格) |
如果显示的值为正则带一个空格,若为负,则带减号表示。 示例:"% 6.2f" |
# (井号) |
转换说明的可选形式,若为 %o 格式,则以 0 开始;若为 %x 或者 %X 格式,则以 0x 或 0X 开始;对于所有的浮点形式,# 保证了即使后面没有任何数字,也打印一个小数点字符。对于 %g 和 %G 格式,# 防止结果后面的 0 被删除。示例:"%#o" 、"%#8.0f" |
0 (零)....... |
对于数值格式,用前导 0 代替空格填充字段宽度,对于整数格式如果出现 - (负)标记或指定精度,则忽略此标记。 |
浮点型参数的转换
有用于打印浮点类型double
和long double
的转换说明符,但没有用于float
的说明符。原因是K&C C
中float
值在被用于表达式中或被用作参数之前,会被自动转换为double
类型。一般情况下,ASCII C
不会自动把float
转换为double
。不过,为了兼容大量会自动转换的程序,printf()
和其他任何不使用显式原型的 C 函数的所有float
参数仍然会自动被转换成double
。因此都无需专门的转换说明符来显示float
类型。
整数格式的示例程序
#include
#define PAGES 931
int main(void)
{
printf("*%d*\n", PAGES);
printf("*%2d*\n", PAGES); // 宽度不足自动扩展
printf("*%10d*\n", PAGES);
printf("*%-10d*\n", PAGES); // 左对齐
return 0;
}
程序的输出结果如下:
*931*
*931*
* 931*
*931 *
浮点格式的示例程序
#include
int main(void)
{
const double RENT = 3852.99;
printf("*%f*\n", RENT);
printf("*%e*\n", RENT);
printf("*%4.2f*\n", RENT);
printf("*%3.1f*\n", RENT);
printf("*%10.3f*\n", RENT);
printf("*%10.3e*\n", RENT);
printf("*%+4.2f*\n", RENT);
printf("*%010.2f*\n", RENT);
return 0;
}
程序运行结果:
*3852.990000*
*3.852990e+003*
*3852.99*
*3853.0*
* 3852.990*
*3.853e+003*
*+3852.99*
*0003852.99*
一般情况下小数点右边会有 6 位有效数字,除非对小数点的位数进行限制。第四个和第六个进行的四舍五入。最后 +标志使得记过带有它的代数符号。0 标志产生前导零并填充整个字段。
字符串的示例程序
#include
#define BLURB "Authentic imitation !"
int main(void)
{
printf("/%2s/\n", BLURB);
printf("/%24s/\n", BLURB);
printf("/%24.5s/\n", BLURB);
printf("/%-24.5s/\n", BLURB); // 左对齐
return 0;
}
程序的输出如下:
/Authentic imitation !/
/ Authentic imitation !/
/ Authe/
/Authe /
格式说明符中的
.5
说明只打印 5 个字符。
格式标志的示例程序
#include
int main(void)
{
printf("%x %X %#x\n", 31, 31, 31);
printf("**%d**% d**% d**\n", 42, 42, -42);
printf("**%5d**%5.3d**%05d**%05.3d**\n", 6, 6, 6, 6);
return 0;
}
程序的输出如下:
1f 1F 0x1f
**42** 42**-42**
** 6** 006**00006** 006**
空格当值为正时会产生,当为负值时会有负号。 小数点后面是最小数字位数,当位数不够时会产生前导零来填充。
转换说明的意义
转换说明实际上就是翻译,比如存储在计算机中的二进制格式的数值 01001100
,%d
转换符就会将其转换为字符 7 和 6 并显示成 76。%x
转换则把它转换为十六进制表示法 4c。%c
转换符会把该值变为字符表示 L 。
类型说明符不匹配的情况
#include
#define PAGES 336
#define WORDS 65618
int main(void)
{
short num = PAGES;
short mnum = -PAGES;
printf("num as short and unsigned short: %hd %hu\n", num, num);
printf("-num as short and unsigned short: %hd %hu\n", mnum, mnum);
printf("num as int and char: %d %c\n", num, num);
printf("WORDS as int,short,and char: %d %hd %c\n", WORDS, WORDS, WORDS);
return 0;
}
程序输出结果如下:
num as short and unsigned short: 336 336
-num as short and unsigned short: -336 65200
num as int and char: 336 P
WORDS as int,short,and char: 65618 82 R
- 第二行输出中变量是负数,然而说明符是无符号的。系统使用一种被称为 2 的补码(two‘s complement)的方法来表示有符号整数,0 ~ 32767 代表它们本身,32768 ~ 65535 表示负数。65535 表示 -1,65534 表示 -2 以此类推 65536-336 = 65200(在有符号数中表示 -336) 即为结果。
- 第三行输出,将
num
变量当作字符串输出,然而 336 超过了 char 类型存放的大小(一个字节),这个时候就会发生截断,相当于用 256 除以一个整数并取余。余数为 80 也就是字符 P 的 ASCII 码值。
- 最后打印一个打印的数值同上也进行的截断,只使用最后两个字节,相当于被 65536 除后的余数,在这里余数是 82,对应字符是 R。
当混淆了整数和浮点类型的时候的结果
#include
int main(void)
{
float n1 = 3.0;
double n2 = 3.0;
long n3 = 2000000000;
long n4 = 1234567890;
printf("%.1e %.1e %.1e %.1e\n", n1, n2, n3, n4);
printf("%ld %ld\n", n3, n4);
printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);
return 0;
}
程序的运行结果如下:
3.0e+000 3.0e+000 9.9e-315 6.1e-315
2000000000 1234567890
0 0 2000000000 1234567890
第一行打印出前两个相符类型的值,而当使用 %e
打印 long 类型时,函数期望一个 double 类型值(8个字节)但是 n3 的类型只有 4 个字节,所以它也查看邻近 4 个字节的值,并把这 8 个字节的内存存放的值解释成 浮点数。结果是无意义的。
第二行打印证明了类型符正确时可以打印出 n3 和 n4 。
第三行打印输出前面两个是浮点类型的,与类型符不符所以打出了错误的结果零。
原因可能是浮点类型占 8 个字节,然后赋值的数比较小,所以截取了高字节是全零的,于是解释出来的也是零。
参数传递
原书中第三行打印出来的结果是:
0 1074266112 0 1074266112
关于这一段书中的解释是:
所以这里不同的原因可能是函数里面使用了不只是简单的顺序读取,而是通过读取首地址的方法去确定读取内存区域的开始 (具体还未考证)。
pintf() 的返回值
printf()
的返回值是它打印的字符的数目。如果有输出错误,那么 printf()
会返回一个负数。这是个附加功能,通常很少被用到,一般用于检测输出错误。
#include
int main(void)
{
int bph2o = 212;
int rv;
rv = printf("%d F is water's boiling point.\n", bph2o);
printf("The printf() function printed %d characters.\n",
rv); // 断行,但不能在字符串中间。
return 0;
}
程序输出为:
212 F is water's boiling point.
The printf() function printed 32 characters.
返回的字符包括空格和不可见的换行字符。
不支持在引号括起来的字符串中间断行。
分割一个字符串的三个选择
#include
int main(void)
{
printf("Here's one way to print a ");
printf("long string.\n");
printf("Here's another way to print a \
long string.\n");
printf("Here's newest way to print a"
"long string.\n"); // ANSI C
return 0;
}
程序的输出如下:
Here's one way to print a long string.
Here's another way to print a long string.
Here's newest way to print along string.
- 第一种方法不是用
\n
结束,可以使用多个printf()
函数输出一个长的字符串。 - 第二种方法使用了
\
(反斜线)和回车键分割一个字符串,缺点是第二行缩进也会变成字符串的一部分。 - 第三种方法是
ANSI C
的新方法。如果两个双引号引起来的字符串之间仅用空格字符分隔,那么 C 会把该组合当一个字符串处理。
使用 scanf()
scanf()
的工作就是把输入的字符串转换成各种形式:整数、浮点数、字符和C 的字符串。它是 printf()
的逆操作。
跟 printf()
一样,scanf()
使用控制字符串和参数列表。控制字符串指出输入将被转换成的格式。
主要区别在于参数列表。printf()
函数使用变量名、常量和表达式;而 scanf()
函数则使用指向函数的指针所以有以下两点需要注意:
- 当使用
scanf()
来读取某种基本类型的值时,需要在变量名前面加上&
(取地址符)。 - 当使用
scanf()
把一个字符串读进一个字符数组时,不需要使用&
(字符串名本身就是地址)。
scanf()
函数使用空格(换行、制表符合空格)来决定这样把输入分为几个字段。它依次把转换说明与字段相匹配,并跳过它们之间的空格。可以分一行或者 5 行输入,只要在每个输入项目之间至少键入一个换行符、空格或制表符。例外的情况就是 %c
它不会跳过空白字符。
scanf()
函数用的转换说明符与 printf()
所用的几乎完全相同。主要的区别是 printf()
把 %f、%e、%E、%g 和 %G 同时用于 float 类型和 double 类型,而 scanf()
只把它们用于 float 类型,而用于 double 类型时使用 l
修饰符 。
表 4.6 列出了 C99 标准中描述的主要转换说明符
转换说明符 | 意义 |
---|---|
%c | 把输入解释成字符 |
%d,%i | 把输入解释成有符号十进制整数 |
%u | 把输入解释成一个无符号十进制整数 |
%e,%f,%g,%a | 把输入解释成一个浮点数(%a 是 C99 标准) |
%E,%F,%G,%A | 把输入解释成一个浮点数(%A 是 C99 标准) |
%o | 把输入解释成有符号八进制数 |
%x,%X | 把输入解释成有符号十六进制数 |
%p | 把输入解释成一个指针(一个地址) |
%s | 把输入解释成一个字符串;输入的内容以第一个非空字符开始到下一个空字符结束 |
也可以在表 4.6 所示的转换说明符中使用修饰符。修饰符出现在百分号和转换字符之间。如果一个说明符内使用多个修饰符,那么它们出现的顺序应该与在表 4.7 中出现的顺序相同。
修饰符 | 意义 |
---|---|
* (星号) |
滞后符号(具体后面说明)示例:"%*d" |
digit(s) (数字) |
最大自动宽度;在达到最大宽度或遇到第一个空白字符时停止对输入的读取 示例:"%10s" |
hh |
把整数读作 signed char 或 unsigned char 示例:"%hhd" ,"%hhu" |
ll |
把整数读作 long long 或 unsigned long long(C99)示例:"%lld" ,"%llu" |
h,l 或 L |
"%hd" ,"%hi" 指示该值会存储在一个 short int 中。"%ho" ,"%hx" ,"%hu" 指示该值会存储在一个 unsigned short int 中。"%ld" ,"%li" 指示该值会存储在一个 long 中。"%lo" ,"%lx" ,"%lu" 指示该值会存储在一个 unsigned long 中。"%le" ,"%lf" ,"%lg" 指示该值以 double 类型存储。将 L(而非 l)与 e、f 和 g 一起使用指示该值以 long double 存储。如果没有这些修饰符,d、i、o 和 x 指示 int 类型,而 e、f 和 g 指示 float 类型。 |
从 scanf()的角度看输入
这一部分太多了 水一水贴个图 但是内容是很重要的。
格式字符串中的常规字符
scanf()
函数允许您把普通字符放在格式字符串中。除了空格字符之外的普通字符一定要与输入的字符串准确匹配。例如:
scanf("%d,%d", &a, &b);
不是原本的 :12 88
而应该输入:12,88
或者 88 , 121
或者
88,
121
scanf()
函数会将其解释成,您将键入一个数字,键入一个逗号,然后再键入一个数字(当然也会跳过空白字符)。
对于
%c
来说,如果在格式字符串中%c
之前有一个空格,那么scanf()
就会跳到第一个非空字符串处。也就是说,命令scanf("%c", &ch)
读取在输入中遇到的第一个字符,命令scanf(" %c", &ch)
读取在输入中遇到的第一个非空白字符。
scanf()
的返回值
-
scanf()
函数返回成功读取的项目的个数 - 如果没有读取任何项目则返回零。
- 检测到 “文件结尾(end of file)” 时,它返回
EOF
(EOF 是文件stdio.h
中定义的特殊值。一般被定义为 -1)
printf()
和 scanf()
的 * (星号)修饰符
对于 printf()
来说,*
用于不想事先指定字段宽度,而是希望由程序来指定该值。您必须使用一个参数来告诉函数字段宽度。例如:printf("%*d", width, number);
其中需要输入 width
来指定宽度,而 number
才是要打印的数字。
也可以是 printf("%*.*f", width, precision, number);
这样的语句来指定浮点数的宽度和精度。
对于 scanf()
来说,*
是当把它放在 % 和说明符字母之间时,用于跳过相应的输入项目。示例:
`scanf("%*d %*d %d", &val)`
有如下输入:
2004 2005 2006
程序会跳过第一第二个输入,把第三个输入读入变量 `val` 里面。
如果程序需要读取一个文件中某个特定的列,那么该功能将非常有用。
printf()
的用法提示
`printf("%9d %9d %9d", val1, val2, val3);`
使用一个较大的宽度来使输出整齐排列
`printf("**%.2f**", distance);`
输出结果可能是: ** 10.22 **
如果改成
`printf("**%10.2f**", distance);`
输出结果可能是: ** 10.22 **
一个关键的概念:
假设有这样一个值 -13.45e12# 0
- 使用
%d
模式,会读取三个字符(-13)并在小数点处停止,将小数点作为下一个输入字符。 - 使用
%f
模式,它将会读取字符-13.45e12
并在 # 号处停止,将它作为下一个输入字符。 - 使用
%s
模式,他会读取字符-13.45e12#
并在空格处停止,将空格作为下一个输入字符。 - 使用
%c
模式,它会存储读取的第一个字符,在这里是一个空格。