这一篇在干嘛?

上一章认识了数据类型,这一章解决”程序怎么和人好好说话”:把一串字符装进 char 数组,用 strlen() 量长度,用 define 和 const 造符号常量,再把 printf() 和 scanf() 的转换说明、修饰符、标志位一次讲透。学完这篇,你打印任何格式、读取任何输入都有章可循。

4.1 示例程序

程序清单 4.1 talkback.c

// talkback.c -- nosy, informative program
#include <stdio.h>
#include <string.h>    // 提供 strlen() 原型
#define DENSITY 62.4   // 人体密度:磅/立方英尺
int main()
{
    float weight, volume;
    int size, letters;
    char name[40];     // 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?
Sharla
Well, Sharla, your volume is 2.23 cubic feet.
Also, your first name has 6 letters,
and we have 40 bytes to store it in.

四个新东西:用数组存字符串、用 %s 读写字符串、用 define 定义符号常量、用 strlen() 量长度。注意细节:scanf("%s", name) 没有加 &,而 scanf("%f", &weight) 加了——后文解释。

4.2 字符串简介

char 数组与空字符

C 没有为字符串设计专门的变量类型,字符串存在 char 数组里:字符一个挨一个放在相邻内存单元中,末尾以空字符(null character)\0 收尾。\0 不是数字 0,而是编码值为 0 的不可打印字符,它是字符串的”句号”——C 全靠它判断字符串在哪结束,因此数组必须比字符串多留至少 1 个格子

/图:字符串存放在 char 数组中,末尾以空字符 \0 结束

数组(array)是有序的同类型元素序列,声明 char name[40]; 创建 40 个格子。好消息:放字符、加 \0 这些细节大多由编译器和 scanf() 代劳。

/图:声明普通变量与声明数组的对比

用 %s 配合 scanf()/printf() 就能读写单词字符串,scanf() 读入后自动补 \0。但注意:scanf() 用 %s 只读一个”单词”——遇到第一个空白(空格、制表符、换行)就停,输入 Hilary Bubbles 只会读到 Hilary。读整句话要用别的函数(gets() 等,后面章节讲)。

字符串与字符的区别

"x"'x' 是两回事:‘x’ 属于基本类型 char;“x” 是派生类型(char 数组),实际包含 ‘x’ 和 ‘\0’ 两个字符。

/图:字符常量 'x' 与字符串常量 "x" 的存储差异

strlen() 函数

sizeof(第 3 章)以字节报告大小,strlen() 以字符报告字符串长度(不含 \0)。两者一对比,空字符就现形了。

程序清单 4.2 praise2.c

/* praise2.c */
#include <stdio.h>
#include <string.h>    /* 提供 strlen() 原型 */
#define PRAISE "What a super marvelous name!"
int main(void)
{
    char name[40];
    printf("What's your name?\n");
    scanf("%s", name);
    printf("Hello, %s. %s\n", name, PRAISE);
    printf("Your name of %d letters occupies %d memory cells.\n",
           strlen(name), sizeof name);
    printf("The phrase of praise has %d letters ", strlen(PRAISE));
    printf("and occupies %d memory cells.\n", sizeof PRAISE);
    return 0;
}
Your name of 6 letters occupies 40 memory cells.
The phrase of praise has 28 letters and occupies 29 memory cells.

数组 name 有 40 个格子,sizeof 如实报告 40;strlen() 数到第 7 格的 \0 就停,报 6。PRAISE 是编译器按字符数自己分配的,28 字符 + 1 个 \0 = 29 字节。strlen() 属于 string.h 家族——C 把库函数按家族分家,每个家族配一个头文件:printf()/scanf() 属于标准 I/O(stdio.h),字符串函数属于 string.h。另注意 sizeof 的括号:对类型必须加(sizeof(char)),对具体量可加可不加(sizeof name)。

/图:strlen 数到空字符就停,sizeof 则统计全部字节

strlen 与 sizeof 分工明确

记一句口诀:strlen 问”字符串有几个字符”,sizeof 问”占多少字节”。处理字符串用 strlen,规划内存用 sizeof,混用是初学者常见错误来源。

4.3 常量与 C 预处理器

circumference = 3.14159 * diameter; 固然可以,但更好的做法是用符号常量(symbolic constant)circumference = pi * diameter;。理由有二:名字比数字更有信息量(taxrate * housevalue 显然比 0.015 * housevalue 好读);换值方便,只改定义处一处,全程序自动生效。

define 指令

C 预处理器(preprocessor)在编译前做文本替换。文件顶部写 #define TAXRATE 0.015,编译时所有 TAXRATE 都被替换成 0.015,这叫编译时替换(compile-time substitution),这类常量也叫明示常量(manifest constant)。格式要点:一般形式 #define NAME value——没有 = 号、没有分号,因为它不是 C 语句而是替换指令;惯例上常量名全大写;命名规则与变量相同(首字符不能是数字)。#define 也能定义字符和字符串常量,如 #define BEEP '\a'#define OOPS "Now you have done it!"

define 最常见的错误:多写 = 或分号

#define TOES = 20 会让 TOES 被替换成 ”= 20”(连等号一起),digits = fingers + TOES; 就变成非法的 digits = fingers + = 20;。同理 #define TOES 20; 会把分号也替换进去。记住:名字后面的所有内容都会被原样替换。

const 修饰符

C90 提供了第二种造符号常量的方法:const int MONTHS = 12;。MONTHS 变成**只读(read-only)**值:能打印、能参与计算,但改不了。const 比 define 更灵活(有类型、有作用域,第 12 章细讲),现代代码里更推荐;此外还有第三种方式 enum(第 14 章)。

limits.h 与 float.h:现成的明示常量

头文件 limits.h 和 float.h 里满是 define 好的类型极限常量,拿来即用:

常量(limits.h)含义常量(float.h)含义
INT_MAX / INT_MINint 最大/最小值FLT_DIGfloat 有效数字位数
SHRT_MAX / LONG_MAXshort / long 最大值DBL_DIGdouble 有效数字位数
UINT_MAX / ULONG_MAX无符号最大值FLT_MAX / FLT_MINfloat 最大值 / 最小正全精度值
CHAR_BITchar 的位数FLT_EPSILON1.00 与比它大的最小 float 之差
LLONG_MAX / LLONG_MINlong long 极值DBL_MAXdouble 最大值

程序清单 4.3 defines.c

// defines.c -- uses defined constants from limits.h and float.h
#include <stdio.h>
#include <limits.h>    // 整数极限
#include <float.h>     // 浮点极限
int main(void)
{
    printf("Some number limits for this system:\n");
    printf("Biggest int: %d\n", INT_MAX);
    printf("Smallest long long: %lld\n", LLONG_MIN);
    printf("One byte = %d bits on this system.\n", CHAR_BIT);
    printf("Largest double: %e\n", DBL_MAX);
    printf("Smallest normal float: %e\n", FLT_MIN);
    printf("float precision = %d digits\n", FLT_DIG);
    printf("float epsilon = %e\n", FLT_EPSILON);
    return 0;
}
Biggest int: 2147483647
Smallest long long: -9223372036854775808
One byte = 8 bits on this system.
Largest double: 1.797693e+308
float precision = 6 digits
float epsilon = 1.192093e-07

/图:你写的符号常量与编译器实际编译的值

别再硬编码类型极限

想知道”这台机器的 int 最大是多少”,不用背、不用猜,printf("%d", INT_MAX) 一行搞定,代码在别的系统上照样正确。这是符号常量最实在的用法之一。

4.4 深入 printf() 和 scanf()

printf() 和 scanf() 合称 I/O 函数,一个负责输出、一个负责输入,工作方式相同:控制字符串(control string)+ 参数列表。它们把数据在”机器内部表示”和”人类可读文本”之间互相转换——printf() 把数变成字符,scanf() 把字符变成数。

printf() 转换说明符总表

说明符输出
%a / %A浮点数,十六进制数字与 p 记数法(C99)
%c单个字符
%d / %i有符号十进制整数
%e / %E浮点数,e 记数法
%f浮点数,十进制记数法
%g / %G按值自动选 %f 或 %e(指数小于 −4 或大于等于精度时用 e 风格)
%o无符号八进制整数
%p指针
%s字符串
%u无符号十进制整数
%x / %X无符号十六进制整数(0f / 0F)
%%打印一个百分号

使用格式:printf(控制字符串, item1, item2, ...);。要打印的项可以是变量、常量,甚至先求值的表达式(如 printf("%c%d\n", '$', 2 * cost); 输出 $6200)。控制字符串由两类内容组成:照原样打印的普通字符转换说明。想打印 % 本身就写 of Sally’s gribbles were edible.\n”, pc);输出Only 12% of …`。

/图:控制字符串的解剖:普通字符与转换说明

printf() 修饰符与标志

在 % 和转换字符之间可以插入修饰符,顺序为:标志 → 数字(字段宽度)→ .数字(精度)→ 大小修饰符。

修饰符含义示例
flag五个标志:-、+、空格、#、0(见下表)”%-10d”
digit(s)最小字段宽度,装不下自动加宽”%4d”
.digit(s)精度:%f/%e 为小数位数;%g 为最大有效数字;%s 为最大字符数;整数为最少位数(不足补 0)“%5.2f”
h / hh表示 short / char”%hu”、“%hhd”
l / ll表示 long / long long(C99)“%ld”、“%lld”
L与浮点说明符连用,表示 long double”%Lf”
z / t / jsize_t / ptrdiff_t / intmax_t(C99)“%zd”、“%td”

五个标志:

标志含义示例
-左对齐”%-20s”
+正数显示 +,负数显示 −”%+6.2f”
空格正数前导空格,负数显示 −;+ 优先于空格”% 6.2f”
#八进制加前导 0,十六进制加 0x/0X;浮点保证打印小数点”%#o”
0用前导 0 而非空格填充字段;有 - 标志或指定精度时被忽略”%010d”

程序清单 4.4 width.c(字段宽度)

/* width.c -- field widths */
#include <stdio.h>
#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       *

%d 默认字段宽度恰好装下数字;%2d 要求宽 2 但数字 3 位,字段自动加宽;%10d 右对齐补空格;%-10d 左对齐。星号 * 用来标出字段边界。

程序清单 4.5 floats.c(浮点组合)

// floats.c -- some floating-point combinations
#include <stdio.h>
int main(void)
{
    const double RENT = 3852.99;   // const 风格常量
    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+03*
3852.99*
3853.0*
  3852.990*
3.853e+03*
+3852.99*
0003852.99*

%f 默认小数点后 6 位;%4.2f 表示宽度至少 4、小数 2 位(超出自动加宽);%+4.2f 强制显示正号;%010.2f 中第一个 0 是标志,后面 10 才是字段宽度。整数也有对应玩法:%5.3d 表示宽度 5、至少 3 位数字(不足补 0),%05d 用 0 填满整个字段;同时给 0 标志和精度时精度获胜。字符串同理:%.5s 表示最多打印 5 个字符——精度是截断手段,%-24.5s 则左对齐。

转换说明到底”转换”了什么

它把内存中的二进制值翻译成字符序列:同一个值 01001100(十进制 76),%d 显示 76,%x 显示 4c,%c 显示 L。所以”转换”其实是翻译说明——原值本身不变。

说明符与类型不匹配的后果

程序清单 4.6 intconv.c(整数家族内部混搭)

/* intconv.c -- some mismatched integer conversions */
#include <stdio.h>
#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

三个现象都源于”位模式不变、解读方式变了”:① −336 按 %hu 打成 65200——二补数(two’s complement)表示法里同一位模式有两种解读;② 336 按 %c 打成 P——%c 只看低 1 字节,相当于 336 % 256 = 80,正好是 ‘P’ 的 ASCII 码;③ 65618 按 %hd 打成 82——只看低 2 字节,65618 % 65536 = 82。

程序清单 4.7 floatcnv.c(整数与浮点混搭更离谱)

/* floatcnv.c -- mismatched floating-point conversions */
float n1 = 3.0;  double n2 = 3.0;
long n3 = 2000000000;  long n4 = 1234567890;
printf("%.1e %.1e %.1e %.1e\n", n1, n2, n3, n4);   /* float 已自动转 double */
printf("%ld %ld\n", n3, n4);                        /* 说明符正确,输出正常 */
printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);        /* 前面错位,全盘皆输 */
3.0e+00 3.0e+00 3.1e+46 1.7e+266
2000000000 1234567890
0 1074266112 0 1074266112

%e 期待 8 字节的 double,却只收到 4 字节的 long,于是把相邻内存也读进来、按浮点位模式硬解——得 3.1e+46 这种天文数字。第三行更冤:n3、n4 本身没错,但前面 n1、n2 占的字节数与 %ld 期待的对不上,导致后面全读错位。一句话:用错说明符,printf() 读错字节,输出全是垃圾。

/图:参数按变量类型压栈、printf 按说明符读取,错位即乱码

printf() 的返回值与长字符串

printf() 返回打印的字符个数(含空格和换行),出错返回负值:rv = printf("%d F is water's boiling point.\n", bph2o); 之后 rv 为 32——一行干两件事:打印并把返回值存进 rv;写文件时检查返回值很有用。

字符串常量不能在引号中间直接换行,可用三招拆长串:① 用多条 printf() 拼接;② 行尾反斜杠 \ 续行(下一行必须顶格,缩进会进入字符串);③ 字符串连接(ANSI C)——两个引号字符串之间只隔空白时视为一个字符串,如 printf("Here's the newest way to print a " "long string.\n");。空格要自己写在引号里:“young” “lovers” 拼出来是 “younglovers”。

scanf() 的用法

scanf() 是 printf() 的逆过程:把键盘输入的文本逐字符转换成数值等机器形式。两条黄金规则:读基本类型变量的值,变量名前加 &scanf("%d", &age););读字符串进 char 数组,不加 &scanf("%s", pet);)——数组名本身就是地址。

程序清单 4.8 input.c

// input.c -- when to use &
#include <stdio.h>
int main(void)
{
    int age;        // 变量
    float assets;   // 变量
    char pet[30];   // 字符串
    printf("Enter your age, assets, and favorite pet.\n");
    scanf("%d %f", &age, &assets);   // 这里用 &
    scanf("%s", pet);                // char 数组不用 &
    printf("%d $%.2f %s\n", age, assets, pet);
    return 0;
}
Enter your age, assets, and favorite pet.
38
92360.88 llama
38 $92360.88 llama

scanf() 用空白(换行、制表符、空格)把输入切成字段,依次匹配转换说明——输入可以随意分行,只要有空白分隔。唯一例外是 %c:它读下一个字符,哪怕是空白也照读

scanf() 转换说明符与修饰符总表

说明符含义
%c把输入解释为字符
%d / %i有符号十进制整数
%e、%f、%g、%a(大写同)浮点数(%a 为 C99)
%o / %x、%X有符号八进制 / 十六进制整数
%s字符串:从第一个非空白字符起,到下一个空白为止
%u无符号十进制整数
%p指针(地址)
修饰符含义
*抑制赋值:读入但不保存(见后文 * 修饰符)
digit(s)最大字段宽度,到宽或遇空白即停(如 “%10s”)
hh / h读成 char / short(%hd、%hi、%ho、%hx、%hu)
l读成 long(%ld 等);与 e/f/g 连用读成 double(%le、%lf、%lg)
L / ll与 e/f/g 连用读成 long double / 读成 long long(C99)

注意 scanf() 与 printf() 的一大差异:printf() 的 %f 对 float 和 double 通用(float 传参自动转 double),但 scanf() 的 %f 只对应 float,double 必须用 %lf

scanf() 怎么看输入

以 %d 为例,scanf() 逐字符处理:跳过空白 → 期待数字或正负号 → 连续读数字 → 遇到非数字就停,把那个字符退回输入队列,下次读取从它开始。若第一个非空白字符就是 ‘A’ 这种非法字符,scanf() 停在那里不给变量赋值,下次还从 ‘A’ 开始——如果你的程序全是 %d,它永远过不去这个 ‘A’。%s 跳过空白后读非空白字符直到再遇空白,即只读一个单词,末尾自动补 ‘\0’;%c 则下一个字符无条件照收。

格式串里的普通字符

scanf() 格式串里的普通字符必须与输入精确匹配scanf("%d,%d", &n, &m); 要求输入形如 88,121(逗号必须紧跟 88)。格式串里的空格表示”跳过接下来所有空白”,所以 scanf("%d, %d", &n, &m); 对 88,121、88, 121、逗号后换行全盘通吃。由于除 %c 外说明符都会自动跳过前导空白,scanf("%d%d", ...)scanf("%d %d", ...) 等价;但 %c 前加空格有实质区别scanf("%c", &ch) 读第一个字符(可能是空格/换行),scanf(" %c", &ch) 跳过空白读第一个非空白字符。

scanf() 返回成功读入的项目数:一个都没读到(如要数字却输入了文字)返回 0,检测到文件末尾返回 EOF(stdio.h 定义的常量,通常为 −1)。学会 if 和 while 之后(第 6、7 章),就能用它做输入验证。

printf() 与 scanf() 中的 * 修饰符

同一个小星星,两家用途完全不同。printf() 里的 *:字段宽度(和精度)运行时再定——printf("The number is :%*d:\n", width, number); 用变量 width 的值作字段宽度;%*.*f 则依次取宽度、精度、数值三个参数。程序因此可以边看数据边决定格式。

scanf() 里的 *:跳过对应输入项,不赋值。 scanf("%*d %*d %d", &n); 的意思是:跳过两个整数,把第三个存进 n——输入 2004 2005 2006 时输出 The last integer was 2006。处理”只要某几列数据”的规整文件时特别好使。

实战建议:打印多列数据时用固定字段宽度对齐(如 printf("%9d %9d %9d\n", val1, val2, val3);),说明符之间留空格可防止一个数溢出字段时黏到下一个数上;数字嵌在句子里时字段宽度给小一点(如 %.2f),避免句子中间出现多余的空洞。

4.5 关键概念与小结

  • C 用 char 表示单个字符,用字符串表示字符序列;无论字符串常量还是存进数组的字符串,末尾都有一个看不见的 ‘\0’。字符串 = 以 ‘\0’ 结尾的字符序列,存于 char 数组:char name[30];,长度要留出 \0 的位置;strlen()(string.h)量字符数,sizeof 量字节数;scanf(“%s”) 只能读单词;
  • 数值常量应当符号化:#define 或 const 二选一,程序更可读、更好维护;limits.h 与 float.h 提供全套类型极限常量;
  • printf()/scanf() 都靠”格式串中的说明符与后面的参数一一对应”来工作,数目对不上、类型配错了都会产出怪异结果;scanf() 还要记得给变量加 &;字段宽度、小数位数、对齐、填充全可调;
  • 空白字符在 scanf() 眼里是字段分隔符。同一行输入 -13.45e12# 0:%d 读走 −13(停在句点),%f 读走 −13.45e12(停在 #),%s 读走 −13.45e12#(停在空格),%c 只读第一个字符(空格)。

三大高频翻车点回顾

#define 写了 = 或分号,替换出非法代码;② scanf() 给基本类型变量忘了 &,或给数组名多写了 &——程序当场崩溃或读到垃圾地址;③ 说明符与类型不匹配,printf() 读错字节输出乱码,且编译器通常不报错。

学完这章你能做什么

打印对齐的报表列(字段宽度 + 标志)、格式化金额(%.2f)、按列抽取数据(scanf 的 *)、查询系统极限(limits.h/float.h)——printf/scanf 是 C 程序的门面与入口,本章两张总表值得打印出来贴在显示器旁边。