
(图片来源网络,侵删)
- 学习目的:提供答案是为了帮助您检查自己的思路、理解难点,并作为学习的辅助工具。切勿直接抄袭,这会严重阻碍您对C语言核心概念的理解和编程能力的提升。
- 版本问题:K&R C有两个主要版本:经典的第一版(1978)和更新的第二版(1988)。绝大多数现代课程和开发环境都基于第二版,以下答案主要针对第二版。
- 代码风格:K&R以其简洁、优雅的代码风格著称,这里的答案也尽量遵循其风格,但为了清晰性和可读性,在某些地方可能会加入注释或稍作调整。
- 并非唯一解:编程题通常没有唯一的“标准答案”,以下提供的是一种或几种常见且正确的解法,您可能想探索其他实现方式。
第1章:教程引言
这一章的习题相对简单,旨在帮助您熟悉C语言的基本语法和结构。
习题 1-1在 printf 函数的调用中,错误地将第二个参数 c 写成了 x,观察会发生什么。
答案:
#include <stdio.h>
int main()
{
printf("hello, world\n"); // 第一版代码
printf("hello, world\n"); // 第二版代码
printf("hello, world\n"); // 第三版代码
printf("hello, world\n"); // 第四版代码
printf("hello, world\n"); // 第五版代码
printf("hello, world\n"); // 第六版代码
printf("hello, world\n"); // 第七版代码
printf("hello, world\n"); // 第八版代码
printf("hello, world\n"); // 第九版代码
printf("hello, world\n"); // 第十版代码
int x = 10;
printf("x = %d\n", x); // 正确的用法
// printf("x = %d\n", c); // 错误的用法,c 未定义,编译器会报错
return 0;
}
解析:
c是一个已经定义的变量,但类型不匹配(c是char,而%d需要一个int),可能会导致输出错误或警告。c根本没有定义,编译器(如gcc)会报错,提示 "undeclared variable 'c'"(未声明的变量 'c'),这是最常见的错误。- 编译器会检测到这个错误,并阻止程序生成可执行文件。
习题 1-2做实验,看看当 printf 函数的参数多于格式字符串中指定的数量时会发生什么。
答案:

(图片来源网络,侵删)
#include <stdio.h>
int main()
{
printf("Hello, world\n"); // 0个参数
printf("Value: %d\n", 123); // 1个参数
printf("Values: %d and %f\n", 123, 456.789); // 2个参数
printf("Extra arguments: %d, %f, %s\n", 1, 2.0, "three", 4); // 4个参数,格式字符串只要求3个
return 0;
}
解析:
- 行为是未定义的,这意味着程序可能会崩溃,或者输出一些看起来毫无意义的垃圾数据。
- 原因:
printf函数根据格式字符串中的格式说明符(如%d,%f)从栈上按顺序读取参数,如果参数过多,printf会读取到不属于它的数据,导致不可预测的行为,这是一个典型的缓冲区溢出风险,非常危险。 - 永远要确保
printf的参数数量和类型与格式字符串完全匹配。
习题 1-3修改温度转换程序,以打印出逆序的表格(即从300度到0度)。
答案:
#include <stdio.h>
int main()
{
float fahr, celsius;
int lower, upper, step;
lower = 0; // 下限
upper = 300; // 上限
step = 20; // 步长
printf("Fahrenheit to Celsius (Reverse)\n");
printf("--------------------------------\n");
printf("Fahr\tCelsius\n");
printf("--------------------------------\n");
// 从300度开始,每次减20,直到小于0
for (fahr = upper; fahr >= lower; fahr -= step) {
celsius = (5.0 / 9.0) * (fahr - 32.0);
printf("%3.0f\t%6.1f\n", fahr, celsius);
}
return 0;
}
解析:
- 关键修改在于
for循环。 - 初始化:
fahr = upper(从300开始)。 - 条件:
fahr >= lower(只要fahr大于等于0就继续)。 - 步进:
fahr -= step(每次减20)。 - 这样循环就会从高到低依次打印。
习题 1-4编写一个程序打印摄氏温度到华氏温度的转换表。
答案:

(图片来源网络,侵删)
#include <stdio.h>
int main()
{
float fahr, celsius;
int lower, upper, step;
lower = 0; // 摄氏温度下限
upper = 300; // 摄氏温度上限
step = 20; // 步长
printf("Celsius to Fahrenheit\n");
printf("---------------------\n");
printf("Celsius\tFahrenheit\n");
printf("---------------------\n");
// 从0度开始,每次加20,直到300
for (celsius = lower; celsius <= upper; celsius += step) {
fahr = (9.0 / 5.0) * celsius + 32.0;
printf("%3.0f\t%6.1f\n", celsius, fahr);
}
return 0;
}
解析:
- 这与原题(华氏转摄氏)的逻辑完全相同,只是转换公式和打印的标签反了过来。
- 转换公式:
F = (9/5)C + 32。
习题 1-5修改温度转换程序,以打印出华氏温度对应的摄氏温度的浮点值,并保持对齐美观。
答案:
#include <stdio.h>
int main()
{
float fahr, celsius;
int lower, upper, step;
lower = 0;
upper = 300;
step = 20;
printf("Fahrenheit\tCelsius\n");
printf("------------------------\n");
for (fahr = lower; fahr <= upper; fahr += step) {
celsius = (5.0 / 9.0) * (fahr - 32.0);
// %3.0f: 整数部分至少占3位,无小数
// %6.1f: 总宽度至少6位,小数点后1位
printf("%3.0f\t\t%6.1f\n", fahr, celsius);
}
return 0;
}
解析:
- 关键在于
printf的格式化字符串。 %3.0f:打印fahr,它是一个浮点数,但我们只关心整数部分。.0表示不显示小数部分。3表示总共至少占3个字符宽度,这样能保证数字对齐。%6.1f:打印celsius,总共至少占6个字符宽度,小数点后保留1位,这样也能保证所有小数点对齐。\t(制表符) 也可以用于简单的对齐,但当数字位数变化时(如从100到200),\t的对齐效果可能不如指定宽度来得精确。
习题 1-6验证 getchar() != EOF 的值是0还是1。
答案:
#include <stdio.h>
int main()
{
int c;
printf("Enter some characters, then press Ctrl+D (Unix) or Ctrl+Z (Windows) to signal EOF.\n");
c = getchar() != EOF; // 将比较的结果(0或1)赋值给c
printf("The value of (getchar() != EOF) is: %d\n", c);
return 0;
}
解析:
- 是一个关系运算符,它的结果是一个整型:如果表达式为真,结果为
1;如果为假,结果为0。 EOF(End of File) 是一个在stdio.h中定义的宏,通常值为-1。- 当用户输入一个可打印字符(如 'A')并回车时,
getchar()读取该字符(其ASCII码不为-1),getchar() != EOF的结果为1。 - 当用户输入EOF信号时,
getchar()返回EOF(即-1),getchar() != EOF的结果为0。 - 表达式的值是
1(真)或0(假),而不是getchar()返回的那个字符本身。
习题 1-7编写一个打印 EOF 值的程序。
答案:
#include <stdio.h>
int main()
{
printf("The value of EOF is: %d\n", EOF);
return 0;
}
解析:
- 这是一个非常直接的题目。
EOF是一个宏,我们只需要用printf打印它的整数值即可。 - 在大多数系统上,输出会是
-1。
习题 1-8编写一个统计空格、制表符和换行符数量的程序。
答案:
#include <stdio.h>
int main()
{
int c, nb, nt, nl;
nb = 0; // 空格数
nt = 0; // 制表符数
nl = 0; // 换行符数
printf("Enter text (Ctrl+D or Ctrl+Z to end):\n");
while ((c = getchar()) != EOF) {
if (c == ' ') {
++nb;
} else if (c == '\t') {
++nt;
} else if (c == '\n') {
++nl;
}
}
printf("\n--- Summary ---\n");
printf("Blanks: %d\n", nb);
printf("Tabs: %d\n", nt);
printf("Newlines: %d\n", nl);
return 0;
}
解析:
- 初始化三个计数器
nb,nt,nl。 - 使用
while循环持续读取字符,直到遇到EOF。 - 在循环内部,使用
if-else if结构判断字符类型,并递增相应的计数器。 - 循环结束后,打印出最终的统计结果。
习题 1-9编写一个将输入复制到输出的程序,并将连续的多个空格替换为单个空格。
答案:
#include <stdio.h>
int main()
{
int c, prev_was_space;
prev_was_space = 0; // 标记前一个字符是否是空格
while ((c = getchar()) != EOF) {
if (c != ' ') {
// 如果当前字符不是空格,直接输出
putchar(c);
prev_was_space = 0; // 重置标记
} else {
// 如果当前字符是空格
if (!prev_was_space) {
// 如果前一个字符不是空格,才输出这个空格
putchar(c);
}
// 无论前一个字符是不是空格,都标记当前为空格
prev_was_space = 1;
}
}
return 0;
}
解析:
- 关键在于需要一个状态标记
prev_was_space。 - 它记录上一个被处理的字符是否是空格。
- 当遇到一个空格时,检查
prev_was_space。- 如果为
0(上一个不是空格),说明这是第一个空格,可以输出,并将标记设为1。 - 如果为
1(上一个也是空格),说明这是连续的空格,不输出,标记保持为1。
- 如果为
- 当遇到非空格字符时,直接输出,并将标记重置为
0。
习题 1-10编写一个将制表符替换为 \t,将回退符替换为 \b,将反斜杠替换为 \\ 的程序。
答案:
#include <stdio.h>
int main()
{
int c;
while ((c = getchar()) != EOF) {
if (c == '\t') {
putchar('\\');
putchar('t');
} else if (c == '\b') {
putchar('\\');
putchar('b');
} else if (c == '\\') {
putchar('\\');
putchar('\\');
} else {
putchar(c);
}
}
return 0;
}
解析:
- 使用
if-else if结构来识别这三个特殊字符。 - 对于每个特殊字符,我们输出一个反斜杠
\,然后输出它的替代字符(t,b,\)。 - 对于所有其他字符,直接原样输出
putchar(c)。 - 注意:反斜杠
\本身是一个转义字符,所以在字符串中要写成\\才能表示一个字面的反斜杠。
习题 1-11如何测试 word 统计程序?设计一些输入,使其能覆盖各种边界情况。
答案: 这是一个设计题,没有标准代码答案,关键在于设计测试用例。
测试用例设计思路:
-
空输入:直接按
EOF信号。- 预期:单词数、行数、字符数都应为0。
-
只有空白字符的输入:输入多个空格、制表符、换行符。
- 输入示例:
" \t\t\n\n \t \n" - 预期:单词数应为0,行数和字符数应正确统计。
- 输入示例:
-
单个单词:输入一个单词,以
EOF- 输入示例:
"hello"(然后按EOF) - 预期:单词数=1,行数=0或1(取决于实现),字符数=5。
- 输入示例:
-
单个单词后跟换行符:输入一个单词和一个换行符。
- 输入示例:
"world\n"(然后按EOF) - 预期:单词数=1,行数=1,字符数=6。
- 输入示例:
-
多个单词在同一行:输入多个由空格分隔的单词。
- 输入示例:
"this is a test" - 预期:单词数=4,行数=0或1,字符数=14。
- 输入示例:
-
单词分布在多行:输入多个单词,并用换行符分隔。
- 输入示例:
"first\nsecond third\n" - 预期:单词数=3,行数=2或3(取决于末尾换行符是否计为新行),字符数=18。
- 输入示例:
-
连续的非字母数字字符:输入标点符号。
- 输入示例:
- 预期:单词数=0(取决于程序如何定义单词,K&R的定义是字母、数字、下划线)。
-
混合输入:包含单词、空格、制表符、换行符的复杂情况。
- 输入示例:
" one\ttwo\n three four\n\n" - 预期:单词数=4,行数=3,字符数=24。
- 输入示例:
-
行首和行尾的空格:
- 输入示例:
" start end \n" - 预期:单词数=2。
- 输入示例:
-
只有一个换行符的行:
- 输入示例:
"\n" - 预期:单词数=0,行数=1。
- 输入示例:
习题 1-12编写一个程序,以每行一个单词的形式打印其输入。
答案:
#include <stdio.h>
#define IN 1 // 在单词内部
#define OUT 0 // 在单词外部
int main()
{
int c, state;
state = OUT;
while ((c = getchar()) != EOF) {
if (c == ' ' || c == '\n' || c == '\t') {
// 如果遇到空白字符,并且之前在单词内部,则打印换行符
if (state == IN) {
putchar('\n');
}
state = OUT;
} else {
// 如果不是空白字符,并且之前在单词外部,说明是新单词的开始
if (state == OUT) {
state = IN;
}
putchar(c);
}
}
return 0;
}
解析:
- 引入两个状态常量
IN和OUT,使用一个状态变量state来跟踪当前是否在单词内部。 - 当遇到一个空白字符(空格、制表、换行)时:
- 检查
state。state是IN,说明我们刚刚结束了一个单词,此时打印一个换行符。 - 无论是否打印换行,都将
state设置为OUT。
- 检查
- 当遇到一个非空白字符时:
- 检查
state。state是OUT,说明这是一个新单词的开始,此时将state设置为IN。 - 打印这个字符。
- 检查
- 这个逻辑可以正确处理连续的空白字符,并在每个单词结束后打印换行符。
后续章节习题答案
由于篇幅限制,这里无法一次性列出所有章节的详细答案,但您可以参考以下模式,我将按照同样的方式为您提供后续章节的答案和解析。
第2章(类型、运算符和表达式)的习题 2-3:
编写一个函数 htoi(s),它将一个十六进制数字的字符串(包括可选的前缀 0x 或 0X)转换为等价的整型值。
答案与解析:
#include <stdio.h>
#include <ctype.h> // 用于 isxdigit
int htoi(char s[]);
int main()
{
printf("0x1F -> %d\n", htoi("0x1F")); // 31
printf("0X1a -> %d\n", htoi("0X1a")); // 26
printf("FF -> %d\n", htoi("FF")); // 255
printf("123 -> %d\n", htoi("123")); // 291 (1*256 + 2*16 + 3)
printf("0 -> %d\n", htoi("0")); // 0
return 0;
}
int htoi(char s[])
{
int i, n, digit;
i = 0;
// 检查并跳过可选的 0x 或 0X 前缀
if (s[i] == '0' && (s[i+1] == 'x' || s[i+1] == 'X')) {
i += 2;
}
n = 0;
// 循环处理每个十六进制字符
while (isxdigit(s[i])) {
digit = s[i];
if (digit >= '0' && digit <= '9') {
digit = digit - '0';
} else if (digit >= 'a' && digit <= 'f') {
digit = digit - 'a' + 10;
} else if (digit >= 'A' && digit <= 'F') {
digit = digit - 'A' + 10;
}
n = n * 16 + digit;
i++;
}
return n;
}
解析:
- 跳过前缀:首先检查字符串是否以
0x或0X开头,如果是,则跳过这两个字符。 - 核心转换逻辑:使用一个
while循环来处理字符串中的每一个字符,直到遇到非十六进制字符为止。 - 字符转数字:对于每个字符,需要将其转换为对应的整数值(0-15)。
- 如果是
'0'到'9',用字符的ASCII码减去'0'的ASCII码即可得到数字。 - 如果是
'a'到'f',减去'a'的ASCII码,然后加10。 - 如果是
'A'到'F',减去'A'的ASCII码,然后加10。
- 如果是
- 累加计算:每次转换得到一个数字后,更新结果
n。n = n * 16 + digit是将当前结果左移(乘以16)并加上新的数字位,这是进制转换的标准方法。 - 库函数:使用了
ctype.h中的isxdigit()函数来判断一个字符是否是有效的十六进制字符,这是一种更健壮的方法。
