批评
#include <stdio.h>
#include <conio.h>
所以你在 Windows 上工作。
#include <ctype.h>
void main(void)
main() 的返回类型应该是int,尽管你会发现很多相反的例子。
{
int val_a;
int val_b;
int result;
printf("Write a number A=");
scanf("%d", &val_a);
您应该测试来自scanf() 的返回值,看看它是否能够读取数字。
if (isdigit(val_a))
此测试“有效”,但它会检查输入的数字是否为 48..57 范围内的值(这是 ASCII、CP1252、Unicode、ISO 8859-1 等数字 '0' 到'9')。这可能不是你的想法。
{
printf("\nWrite a number B=");
scanf("%d", &val_b);
用户输入了换行符,因此printf() 格式字符串前面的换行符不是必需的,尽管它也无害。关于检查scanf() 的相同评论也适用于此处。
}
else
printf("\nI said number.\n");
最好做错误检查。但是,即使您检测到错误,您也会继续前进。你真的需要停下来。
result = val_a + val_b;
printf("%d + %d = %d", val_a, val_b, result);
总的来说,最好用换行符结束行。它将数据刷新到文件或屏幕。
getch();
printf("\033[2J"); \\ to clear the screen
printf() 格式字符串不可移植。鉴于您包含<conio.h>,您可能应该使用clrscr()(它可能会将该转义序列或等效序列发送到终端)。我不认为有必要在程序结束时清除屏幕,但我主要在 Unix 系统上工作,而不是在 Windows 上工作。
}
鉴于 MSVC 仍然是 C89 编译器,您应该在程序末尾包含 return 0; 以返回成功状态。
重写
添加所有更改,您最终会得到:
#include <stdio.h>
#include <conio.h>
int main(void)
{
int val_a;
int val_b;
int result;
printf("Write a number A=");
if (scanf("%d", &val_a) != 1)
{
printf("I said number.\n");
getch();
clrscr();
return(1);
}
printf("\nWrite a number B=");
if (scanf("%d", &val_b) != 1)
{
printf("I said number.\n");
getch();
clrscr();
return(1);
}
result = val_a + val_b;
printf("%d + %d = %d\n", val_a, val_b, result);
getch();
clrscr();
return(0);
}
当你学会了编写函数时,你可以把它压缩成:
#include <stdio.h>
#include <conio.h>
static int get_a_number(const char *prompt)
{
int value;
printf(prompt);
if (scanf("%d", &value) != 1)
{
printf("I said number.\n");
getch();
clrscr();
exit(1);
}
return value;
}
int main(void)
{
int val_a = get_a_number("Write a number A=");
int val_b = get_a_number("Write a number B=");
int result = val_a + val_b;
printf("%d + %d = %d\n", val_a, val_b, result);
getch();
clrscr();
return(0);
}
get_a_number() 函数不是通用函数,但它在这种情况下很有用,因为它封装了一段通用代码。
- 子例程调用允许我们总结参数列表中的不规则性。
- 子程序本身总结了代码的规律。
这是 Kernighan & Plauger 的一句话,“编程风格的要素”。
使用atexit()
上面的代码已更新为在所有退出路径上包含getch() 和clrscr()(并删除了#include <ctype.h>,因为其中没有任何函数被使用)。重复写出两个函数调用很麻烦。避免该问题的另一种方法是编写一个在程序退出时调用的函数。您使用atexit() 函数注册它,该函数在<stdlib.h> 中声明:
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
static void wait_at_exit(void)
{
getch();
clrscr();
}
static int get_a_number(const char *prompt)
{
int value;
printf(prompt);
if (scanf("%d", &value) != 1)
{
printf("I said number.\n");
exit(1);
}
return value;
}
int main(void)
{
atexit(wait_at_exit);
int val_a = get_a_number("Write a number A=");
int val_b = get_a_number("Write a number B=");
int result = val_a + val_b;
printf("%d + %d = %d\n", val_a, val_b, result);
return(0);
}
main() 中的代码假定您可以任意混合语句(例如 atexit())和声明(例如 int val_a)。标准 C 的当前 (2011) 和旧 (1999) 版本都允许这样做,但标准 C 的原始 (1989) 版本不允许这样做。我相信 MSVC 遵守原始标准。如果是这样,您可能需要将 main() 写为:
int main(void)
{
atexit(wait_at_exit);
{
int val_a = get_a_number("Write a number A=");
int val_b = get_a_number("Write a number B=");
int result = val_a + val_b;
printf("%d + %d = %d\n", val_a, val_b, result);
}
return(0);
}
内部块(内部大括号)内的代码是否应该缩进一个额外的级别是一个很好的讨论点。您始终可以在用大括号括起来的代码块的开头声明变量,这样我就可以编写对atexit() 的调用,然后定义和使用这些变量。
如果您阅读有关敏捷编程的文章,您会发现其中的一个口头禅是:
上面的重写以避免重复的getscr() 调用可以被认为是该口头禅的应用。与get_a_number() 函数类似;它适用于 DRY 口头禅。
再试一次
如果你想让程序在用户输入错误时返回并再次提示,那么你需要修改get_a_number()函数。而且,由于函数变得相当复杂,您肯定需要使用函数来完成这项工作。您还遇到了scanf() 的问题;它不允许您每行强制执行一个数字。如果您在第一次提示时键入1 2,对get_a_number() 的第一次调用将读取一个和空白,停止解析(并将空白放回以供重用),并返回值 1。第二次调用to get_a_number() 将输出提示但返回值 2 而用户无需输入更多内容。如果这不是您想要的行为,那么您必须安排阅读整个第一行并扫描数字,然后丢弃其余部分。还有另一个令人担忧的原因。如果用户键入a 而不是1,则scanf() 将永远不会再读取任何内容;它将进入一个循环,发现a 作为数字无效,返回0 转换完成,您的代码可能会进入无限循环,提示输入并读取a。这类问题就是为什么很多人(尤其是我)避免使用scanf()。
避免上述大多数问题的最简单方法是使用fgets() 和sscanf():
enum { MAX_LINELENGTH = 4096 };
enum { MAX_ATTEMPTS = 10 };
static int get_a_number(const char *prompt)
{
int value;
char line[MAX_LINELENGTH];
int count = 0;
while (fputs(prompt, stdout) != EOF &&
fgets(line, sizeof(line), stdin) != 0)
{
if (sscanf(line, "%d", &value) == 1)
return value;
if (count++ > MAX_ATTEMPTS)
{
printf("I give in; I don't understand what you're typing\n");
exit(1);
}
printf("I said please enter a number.\n");
}
printf("Oops: I got EOF or an error; goodbye!\n");
exit(1);
}
这涵盖了很多问题。通常,回显错误数据是个好主意。你可以这样做:
printf("I didn't understand what you meant when you typed:\n%s\n"
"I said please enter a number.\n", line);
这使用'相邻字符串连接',并向用户显示程序接收到的内容,这通常可以帮助用户了解错误所在。
我使用fputs(prompt, stdout)而不是printf(prompt)来输出提示。两者都可以使用(printf(prompt) != 0 将是测试),但fputs() 有一点优势,因为如果有人设法破坏您的程序并在提示中输入%,printf() 将尝试处理它没有传递一个参数,而fputs() 根本不解释它的参数字符串。在这里,差异可以忽略不计;可以合理地争辩说我把事情复杂化了。但是程序的安全性也是你最终要学习的东西,诸如“不要使用printf(variable)”之类的技术可以帮助你的程序更有弹性。 (其实复杂就是解释,代码很简单。)
另一个详细的讨论点是'在fputs() 之后和fgets() 之前是否应该有一个fflush()?可以将额外的“条件”fflush(stdout) == 0 && 添加到循环控制中,但实际上通常没有必要。只有当标准输入来自终端以外的东西并且标准输出来自文件时,它才会有所作为。所以,我忽略了它。
循环中的计数器可防止出现严重错误——程序退出而不是永远挂起,这让无法正确输入数据的用户感到沮丧。您是否曾经有过一个程序弹出一个对话框,您只能在该对话框中点击“确定”按钮(即使它不是确定),然后一遍又一遍地再次返回同一个框?我希望不是为了你。我有;当它发生时让我很沮丧。
您可以看到为什么在get_a_number() 中编写代码一次比两次更有利;这将是很多重复。