Note

本章节仅介绍部分printf和scanf的用法, 详细内容见"Chapter22-输入和输出"

3.1 printf函数

printf()函数用来显示格式串(format string)的内容, 并且在转换说明(conversion specification)处插入参数值.

特性说明代码
printf不检查参数数量参数缺失的情况: 未定义行为, 可能随机打印一个数据, 可能打印栈中数据
参数过多的情况: 未定义行为, 可能忽略多余参数, 可能崩溃等
printf("%d %d\n", i);
printf("%d\n", i, j);
printf不检查参数类型printf会继续根据参数变量的二进制数据进行解析, 打印没有意义的数据printf("%f\n", i);

其中转换说明包括:

  1. 常量
  2. 变量
  3. 表达式
printf(格式串, 表达式1, 表达式2, ...);

实例如下:

#include <stdio.h>

int main(void) {
    int i, j;
    float x, y;

    i = 10;
    j = 20;
    x = 43.2892f;
    y = 5527.0f;

    printf("i = %d, j = %d, x = %f, y = %f\n", i, j, x, y);

    return 0;
}

输出如下:

╭─lamecrow@Lam3Cr0w ~/TRY/code/c/Retest 
╰─$ cd "/Users/lamecrow/TRY/code/c/Retest/C-Chapter3/" && gcc test.c -o test && "/Users/lamecrow/TRY/code/c/Retest/C-Chapter3/"test
i = 10, j = 20, x = 43.289200, y = 5527.000000

3.1.1 转换说明表

仅介绍常用的转换说明信息

数据类型格式说明符描述变形
整数类型1. 对齐长度: %5d, 长度不够左侧补空格
2. 对齐精度: %5.3d, 长度不够左侧补零
3. 右侧对齐: %-5d, 长度不够右侧补空格
int%d十进制有符号整数
%i十进制有符号整数(与 %d 类似)
用于printf: 与%d没有区别
用于scanf: 可以识别八进制(前缀为0), 十六进制(前缀为0x或者0X)
unsigned int%u十进制无符号整数
short%hd十进制有符号短整数
unsigned short%hu十进制无符号短整数
long%ld十进制有符号长整数
unsigned long%lu十进制无符号长整数
long long%lld十进制有符号长长整数
unsigned long long%llu十进制无符号长长整数
intunsigned int%o八进制整数
%x十六进制无符号整数(小写字母)
%X十六进制无符号整数(大写字母)
%p指针地址
浮点类型1. 对齐长度: 转换说明前添加数字
2. 精度: 小数点 + 数字 = %.2f
floatdouble%f小数点形式的浮点数
%e科学计数法(小写 e) (注意: 不能使用%ef)
%E科学计数法(大写 E)
%g根据数值自动选择 %f%e
%G根据数值自动选择 %f%E
long double%Lf小数点形式的长双精度浮点数
%Le科学计数法的长双精度浮点数
%Lg根据数值自动选择 %Lf%Le
字符类型
char%c单个字符
char[]%s字符串,遇到空字符 \0 结束
特殊类型
void*%p指针地址
size_t%zu无符号整数(通常为对象大小)
ptrdiff_t%td有符号整数(通常为指针差值)
intmax_t%jd最大的有符号整数
uintmax_t%ju最大的无符号整数

测试代码

#include <stdio.h>

int main(void) {
    int i;
    float x;
    
    i = 40;
    x = 839.21f;

    printf("|%d|%5d|%-5d|%5.3d|%-5.3d|\n", i, i, i, i, i);
    printf("|%10.3f|%10.3e|%-10g|\n", x, x, x);

    return 0;
}

输出:

|40|   40|40   |  040|040  |
|   839.210| 8.392e+02|839.21    |

3.1.2 转义序列

转义序列描述Unicode/ASCII 值
%%%
\\反斜杠符号U+005C
\'单引号U+0027
\"双引号U+0022
\?问号U+003F
\a警报(响铃)ASCII 7
\b退格ASCII 8
\f换页ASCII 12
\n换行ASCII 10
\r回车ASCII 13
\t水平制表符ASCII 9
\v垂直制表符ASCII 11
\ooo八进制数表示的字符例如:\101 表示 A
\xhh十六进制数表示的字符例如:\x41 表示 A
\0空字符(NUL)ASCII 0

3.2 scanf函数

类似于printf(), scanf()格式串也包含: 普通字符和转换说明两个部分

特性说明代码
传入变量地址而非变量本身内容scanf向内存写入数据, 需要的是变量地址而非变量本身
紧密压缩的格式串常用的格式串, 但是输入的时候需要加上空格%d%d%f%f
scanf错误处理处理顺序: 从左到右
错误处理:
1. 如果前半部分的输入正确, scanf会保留并写入变量, 后续从第一个非法字符开始将保留至缓冲区
2. 如果遇到错误输入, 立即返回, 不对后续参数处理
3. scanf返回值传回正确接收的参数个数
scanf忽略空白符输入时中间存在空白仅用于分割输入, 多余的空白scanf会忽略
scanf解析过程1. 整数解析:
a. 开头解析+或者- (数字后不能出现)
b. 解析数字
2. 浮点数解析:
a. 开头解析+或者- (数字后不能出现)
b. 解析数字
c. 解析.小数点
d. 解析小数
scanf不读取最后的换行符该换行符将是下一次scanf读取的第一个字符
影响情况:
下一次scanf是%c时会读取空白符
解决方法:
使用getchar();手动清空换行符.
格式串的普通字符1. 空白字符: 空白字符的数量无关紧要, scanf会忽略多余的空白符
2. 其他字符: 遇到非空白符时, 会将其输入进行比较
若匹配: scanf放弃输入字符, 并继续处理格式串
不匹配: scanf保留字符在缓冲区中, 然后异常退出
格式字符串: %d/%d
输入一: • 5/ • 96, 跳过空白, 5匹配, /匹配, 后续空白跳过
输入二: • 5 • / • 96, 跳过空白, 5匹配, 格式字符串强制要求匹配/, 此时匹配的是空白符, 异常退出
scanf格式串末尾不要带\n\n的行为: 等价于一个空白符, 它会使scanf忽略空白符, 同时忽略了\r回车符, 导致scanf会继续等待输入scanf("%d\n", &num);

实例-分数相加

代码

#include <stdio.h>

int main(void) {
    int num1, denom1, num2, denom2, result_num, result_denom;

    printf("Enter first fraction: ");
    scanf("%d/%d", &num1, &denom1);

    printf("Enter second fracion: ");
    scanf("%d/%d", &num2, &denom2);

    result_num = num1 * denom2 + num2 * denom1;
    result_denom = denom1 * denom2;

    printf("The sum is %d/%d\n", result_num, result_denom);

    return 0;
}

输出

Enter first fraction: 5/6
Enter second fracion: 3/4
The sum is 38/24

练习题

答案链接:

  • http://knking.com/books/c2/answers/c3.html
  • https://github.com/williamgherman/c-solutions/tree/master/03

记号说明:

  • 红色字体: 表明错误的答案或者遗漏的知识点

Practice1

  1. 下面的printf 函数调用产生的输出分别是什么?

(a) printf("%6d,%4d", 86, 1040);

(b) printf("%12.5e", 30.253);

(c) printf("%.4f", 83.162);

(d) printf("%-6.2g", .0000009979);

回答:

注意: •表示空格

••••86,1040 (正确)

••••30.25300 (错误)

•3.02530e+01 (答案)

83.1620 (正确)

1e-06• (正确)

Practice2

编写printf 函数调用,以下列格式显示float 型变量x:

  1. 指数表示形式,字段宽度8,左对齐,小数点后保留1位数字。
  2. 指数表示形式,字段宽度10,右对齐,小数点后保留6位数字。
  3. 定点十进制表示形式,字段宽度8,左对齐,小数点后保留3位数字。
  4. 定点十进制表示形式,字段宽度6,右对齐,小数点后无数字。

回答:

%-8.1e
%10.6e
%-8.3f
%6.0f

Practice3

说明下列每对scanf 格式串是否等价?如果不等价,请指出它们的差异。

  1. "%d" 与" %d" 。
  2. "%d-%d-%d" 与"%d -%d -%d" 。
  3. "%f" 与"%f " 。
  4. "%f,%f" 与"%f, %f" 。

回答:

  1. 等价: 开头的空白符会被scanf忽略
  2. 等价: "转换说明"后续的空白符会被scanf忽略
  3. 不等价: 后续多出的空格符会导致\r回车符忽略, 导致程序即使读取了一个浮点数, 仍会卡在scanf
  4. 等价: ,可以正确匹配, 空白符忽略

Practice4 *

假设scanf函数调用的格式如下:

scanf("%d%f%d", &i, &x, &j);

如果用户录入

10.3 5 6

调用执行后,变量i、x和j的值分别是多少?(假设变量i和变量j都是int型,而变量x是float型。)

回答:

  1. i = 10
  2. x = 未定义值
  3. 订正: x = 0.3 (见书中关于scanf解析浮点数的过程)
  4. j = 未定义值
  5. 订正: j = 5 (继续读取后续的输入)

Practice5 *

假设scanf 函数调用的格式如下:

scanf("%f%d%f", &x, &i, &y);

如果用户录入

12.3 45.6 789

调用执行后,变量x、i和y的值分别是多少?(假设变量x和变量y都是float型,而变量i是int型。)

回答:

  1. x = 12.3
  2. i = 45
  3. y = 未定义值
  4. 订正: y = 0.6

Practice6

指出如何修改3.2节中的addfrac.c 程序,使用户可以输入在字符/的前后都有空格的分数。

#include <stdio.h>

int main(void) {
    int num1, denom1, num2, denom2, result_num, result_denom;

    printf("Enter first fraction: ");
    scanf("%d /%d", &num1, &denom1);

    printf("Enter second fracion: ");
    scanf("%d /%d", &num2, &denom2);

    result_num = num1 * denom2 + num2 * denom1;
    result_denom = denom1 * denom2;

    printf("The sum is %d/%d\n", result_num, result_denom);

    return 0;
}

代码题

code1-time

编写一个程序,以月/日/年(即mm/dd/yyyy)的格式接受用户录入的日期信息,并以年月日(即yyyymmdd)的格式将其显示出来:

Enter a date (mm/dd/yyyy): 2/17/2011

You entered the date 20110217

代码如下

#include <stdio.h>

int main(void) {
    int y, m, d;

    printf("Enter a date(mm/dd/yy): ");
    scanf("%d/%d/%d", &m, &d, &y);

    printf("You entered the date %d%2.2d%2.2d\n", y, m, d);

    return 0;
}

编译并运行

╭─lamecrow@Lam3Cr0w ~/TRY/code/c/Retest/C-Chapter3/practice6-addfrac 
╰─$ cd "/Users/lamecrow/TRY/code/c/Retest/C-Chapter3/code1-time/" && gcc time.c -o time && "/Users/lamecrow/TRY/code/c/Retest/C-Chapter3/code1-time/"time
Enter a date(mm/dd/yy): 2/17/2011
You entered the date 20110217

code2-product

编写一个程序,对用户录入的产品信息进行格式化。程序会话应类似下面这样: 其中,产品编号和日期项采用左对齐方式,单位价格采用右对齐方式,允许最大取值为9999.99的美元。 提示:各个列使用制表符控制。

Enter item number: 583
Enter unit price: 13.5
Enter purchase date (mm/dd/yyyy): 10/24/2010

Item     Unit     Purchase
         Price    Date
583      $ 13.50  10/24/2010

代码如下

#include <stdio.h>

int main(void) {
    int number, y, m, d;
    float price;

    printf("Enter item number: ");
    scanf("%d", &number);
    printf("Enter unit price: ");
    scanf("%f", &price);
    printf("Enter purchase date (mm/dd/yyyy): ");
    scanf("%d/%d/%d", &m, &d, &y);

    printf("Item\t\t\tUnit\t\t\tPurchase\n");
    printf("    \t\t\tPrice\t\t\tDate\n");
    printf("%-4d\t\t\t$%6.2f\t\t\t%2d/%2d/%4d\n", number, price, m, d, y);
}

编译并运行

╭─lamecrow@Lam3Cr0w ~/TRY/code/c/Retest/C-Chapter3/code2-product 
╰─$ cd "/Users/lamecrow/TRY/code/c/Retest/C-Chapter3/code2-product/" && gcc product.c -o product && "/Users/lamecrow/TRY/code/c/Retest/C-Chapter3/code2
-product/"product
Enter item number: 583
Enter unit price: 13.5
Enter purchase date (mm/dd/yyyy): 10/10/2020
Item                    Unit                    Purchase
                        Price                   Date
583                     $ 13.50                 10/10/2020

code3-book

图书用国际标准书号(ISBN)进行标识。2007年1月1日之后分配的ISBN包含13位数字(旧的ISBN使用10位数字),分为5组,如978-0-393-97950-3。

  1. 第一组(GS1前缀)目前为978或979。
  2. 第二组(组标识)指明语言或者原出版国(如0和1用于讲英语的国家)。
  3. 第三组(出版商编号)表示出版商(393是W. W. Norton出版社的编号)。
  4. 第四组(产品编号)是由出版商分配的用于识别具体哪一本书的(97950)。
  5. ISBN的末尾是一个校验数字,用于验证前面数字的准确性。 编写一个程序来分解用户录入的ISBN信息:

注意:每组中数字的个数是可变的,不能认为每组的长度都与示例一样。用实际的ISBN值(通常放在书的封底和版权页上)测试你编写的程序。

Enter ISBN: 978-0-393-97950-3

GS1 prefix: 978
Group identifier: 0
Publisher code: 393
Item number: 97950
Check digit: 3

代码如下

#include <stdio.h>

int main(void) {
    int prefix, ident, code, num, check;

    printf("Enter ISBN: ");
    scanf("%d-%d-%d-%d-%d", &prefix, &ident, &code, &num, &check);

    printf("GS1 prefix: %d\n", prefix);
    printf("Group identifier: %d\n", ident);
    printf("Publisher code: %d\n", code);
    printf("Item number: %d\n", num);
    printf("Check digit: %d\n", check);

    return 0;
}

编译并运行

╭─lamecrow@Lam3Cr0w ~/TRY/code/c/Retest/C-Chapter3/code3-book 
╰─$ cd "/Users/lamecrow/TRY/code/c/Retest/C-Chapter3/code3-book/" && gcc book.c -o book && "/Users/lamecrow/TRY/code/c/Retest/C-Chapter3/code3-book/"bo
ok
Enter ISBN: 978-0-393-97950-3
GS1 prefix: 978
Group identifier: 0
Publisher code: 393
Item number: 97950
Check digit: 3

code4-phone

编写一个程序,提示用户以(xxx) xxx-xxxx的格式输入电话号码,并以xxx.xxx.xxxx的格式显示该号码:

Enter phone number [(xxx) xxx-xxxx]: (404) 817-6900

You entered 404.817.6900

代码如下

#include <stdio.h>

int main(void) {
    int area, pre, num;

    printf("Enter phone number [(xxx) xxx-xxxx]: ");
    scanf("(%d) %d-%d", &area, &pre, &num);

    printf("You entered %d.%d.%d\n", area, pre, num);

    return 0;
}

编译并运行

Enter phone number [(xxx) xxx-xxxx]: (404) 817-6900

You entered 404.817.6900

code5-matrix

编写一个程序,要求用户(按任意次序)输入从1到16的所有整数,然后用4×4矩阵的形式将它们显示出来,再计算出每行、每列和每条对角线上的和:

如果行、列和对角线上的和都一样(如本例所示),则称这些数组成一个幻方 (magic square)。这里给出的幻方出现于艺术家和数学家Albrecht Dürer在1514年的一幅画中。(注意,矩阵的最后一行中间的两个数给出了该画的创作年代。)

Enter the numbers from 1 to 16 in any order:
16 3 2 13 5 10 11 8 9 6 7 12 4 15 14 1

16  3  2 13
 5 10 11  8
 9  6  7 12
 4 15 14  1

Row sums: 34 34 34 34
Column sums: 34 34 34 34
Diagonal sums: 34 34

代码如下

#include <stdio.h>

int main(void) {
    int matrix[16];
    
    printf("Enter the numbers from 1 to 16 in any order:\n");
    for (int i = 0; i < 16; i++) {
        scanf("%d", &matrix[i]);
    }

    printf("\n");
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d\t", matrix[i * 4 + j]);
        }
        printf("\n");
    }
    printf("\n");

    int row[4];
    int column[4];
    int diagonal[2];

    for (int i = 0; i < 4; i++) {
        row[i] = matrix[i * 4 + 0] + matrix[i * 4 + 1] + matrix[i * 4 + 2] + matrix[i * 4 + 3];
        column[i] = matrix[i] + matrix[i + 4] + matrix[i + 8] + matrix[i + 12];
    }

    diagonal[0] = matrix[0] + matrix[5] + matrix[10] + matrix[15];
    diagonal[1] = matrix[3] + matrix[6] + matrix[9] + matrix[12];

    printf("Row sums: %d %d %d %d\n", row[0], row[1], row[2], row[3]);
    printf("Column sums: %d %d %d %d\n", column[0], column[1], column[2], column[3]);
    printf("Diagonal sums: %d %d\n", diagonal[0], diagonal[1]);

    return 0;
}

编译并运行

Enter the numbers from 1 to 16 in any order:
16 3 2 13 5 10 11 8 9 6 7 12 4 15 14 1

16      3       2       13
5       10      11      8
9       6       7       12
4       15      14      1

Row sums: 34 34 34 34
Column sums: 34 34 34 34
Diagonal sums: 34 34

code6-addfrac2

修改3.2节的addfrac.c 程序,使用户可以同时输入两个分数,中间用加号隔开:

Enter two fractions separated by a plus sign: 5/6+3/4

The sum is 38/24

代码如下

#include <stdio.h>

int main(void) {
    int m1, s1, m2, s2, rm, rs;

    printf("Enter two fractions separated by a plus sign: ");
    scanf("%d/%d+%d/%d", &s1, &m1, &s2, &m2);

    rs = s1 * m2 + s2 * m1;
    rm = m1 * m2;

    printf("\nThe sum is %d/%d\n", rs, rm);

    return 0;
}

编译并运行

Enter two fractions separated by a plus sign: 5/6+3/4

The sum is 38/24

TODO

  • 为什么float和double的格式化输出相同
    • 因为在printf中float类型的参数会被自动提升为double (称为默认参数提升), 这是C标准规定的