NOOB C 编程问题:我在将从文件读取的值插入代码中的其他变量时遇到问题

NOOB C Programming Question: I'm having trouble with inserting values that are being read from a file into other variables throughout my code

因此,为了简化我的问题,我创建了这个更加模块化的程序,它首先在我的 'main()' 和其他两种方法中的 switch case 中请求用户输入。我下面有这段代码,在我的 readDigits() 方法中,我想知道如何从 value 获取特定值到 'firstDigit 和 secondDigit。假设 value == 5,我希望“5”进入 firstDigit

#include <stdio.h>
#include <stdlib.h>

int firstDigit, secondDigit, input;

void getNumbers() {
   printf("Enter these digits: \n");
   scanf("%d", &firstDigit);
}

void getMoreNumbers() {
   printf("Enter some more digits: \n");
   scanf("%d", &secondDigit);
}

int readDigits(int value) {
   FILE *fp
   fp = fopen("digits.txt", "r");
   if(fp == NULL) {
     printf("Failed to open file");
     return -1;
   }
   while(fscanf(fp, "%d", &value)==1){
      printf("%d ", value);


**#I was thinking of doing these 'if' checks whenever value has a number that 
#I would want 'firstDigit' ,'secondDigit' , or 'input' to be. But then I 
#figured that this would be too tedious and not efficient If I were to read 
#a file with a lot of data in it.**

      if(value== 1){
         firstDigit = value;
      }
  }   
  fclose(fp);
}  

int main() {
   while(input < 3){
   printf("Please select which method you'd like to access:\n1) getNumbers()\n2getMoreNumbers()");
  // I also want input's value to be read from digits.txt
  scanf("%d", &input);  
  switch(input){
     case 1:
        getNumbers();
        break;
     case 2:
        getMoreNumbers();
        break;
     default:
        printf("Program complete");
     }  
  }
  return 0;
}

我知道这几乎不是最难的事情,但是,我是 C 语言的新手,我真的因为我觉得这是一个简单的问题而感到压力很大。

我的 digits.txt 文件的值为:

1
10
299
1
200
15
3
150
13
2
150

感谢您发布输入文件。您的问题小于 well-received 的原因是很难确定您想要达到的目标。虽然很明显您需要帮助,但从哪里开始有点神秘。

为什么?对于初学者,您的 getNumbers()[1]getMoreNumbers() 函数做完全相同的事情,只是使用不同的 global多变的。没有理由在任何函数中使用 global 变量,因为它们可以很容易地作为参数传递给任何需要它们的函数。更一般地说,除非绝对必要,否则您希望避免使用全局变量(全局变量有合法用途,但是 none 您在最初学习 C 时可能会遇到 -- 避免使用它们)

接下来,如果您从这个答案中没有学到任何其他东西,请学习始终通过至少检查所用函数的 return 来验证用户输入。否则,您将盲目使用输入变量,而没有验证它是否确实包含有效数据,从而引发 未定义行为

退后一步并尝试了解您想要做什么,看来您的目标是双重的,(1) 根据菜单选择从 stdin 中读取一个整数,或者 (2) 从file "digits.txt" 如果选择了替代菜单。但是,您的 readDigits() 函数永远不会被调用。此外,如果您想从 "digits.txt" 以外的文件中读取怎么办?为什么不简单地提示用户输入文件名,然后打开该文件(处理任何错误),从文件中读取所有整数,然后在完成后关闭文件?

那么我们如何实现这两个目标,是从 stdin 中读取一个整数,还是从用户给出的文件名中读取所有整数?

首先,您必须完全验证用户提供的所有输入,然后根据该输入处理可能出现的任何错误。 scanf(及其函数族)对于新的 C 程序员来说充满了陷阱。例如,用户在您的任何 scanf ("%d", ...) 函数调用中不小心键入 alpha-character 而不是 "digit" 会发生什么?试试吧。

当使用scanf时,如果输入与conversion-specifier不匹配,则会发生匹配失败 .当发生 匹配失败 时,停止从输入缓冲区读取,不再提取任何字符,所有字符都保留在失败时存在的状态 unread 在输入缓冲区中(在你的情况下为 stdin),只是等待在你下次尝试阅读时再次咬你。此外,只有 numeric 转换说明符和 "%s" 消耗前导 whitespace

表示用于输入的任何其他转换说明符(或 line-oriented 输入函数)无法读取任何内容(当遇到用户按 [= 生成的 '\n' 时停止163=]Enter 并留在 stdin),否则它将读取 '\n' 作为用户输入。两者都不是你想要的。

您如何确保从用户那里获取的每个输入都是用户输入的内容?您可以通过在每次输入后清空输入缓冲区来自行清理,以确保 stdin 中没有来自先前输入尝试的遗留字符或其他字符。 (这也是为什么鼓励新的 C 程序员避免 scanf 用于用户输入,而是使用 line-oriented 输入函数,例如 fgets()(具有足够的缓冲区大小)或 POSIX getline 将根据需要读取和分配以读取任何行。两个 line-oriented 函数 读取并包含 它们填充的缓冲区中的 '\n' 消除它保留在输入缓冲区中未读的机会。

如果您选择继续使用 scanf,那么至少在每次有字符留在 [=21] 中时,至少使用一个简单的函数手动清空输入缓冲区 (stdin) =] 未读。一个简单的函数就可以做到。只需使用 getchar() 读取每个字符,直到遇到 '\n'EOF,例如

void empty_stdin (void)
{
    int c = getchar();

    while (c != '\n' && c != EOF)
        c = getchar();
}

注意: 如果函数不带参数,则正确的声明包括 (void) 作为参数列表,以明确表示不需要参数。

现在有了正确处理删除保留在 stdin 中的字符的方法,您就可以查看输入函数了。

如果您使用 getNumbers()[1]getMoreNumbers() 的目的是简单地从 stdin 中读取一个整数,那么只需使用一个使用更具描述性的名称来完成这项工作,例如:

int readintstdin (int *value) {

    int rtn = 0;

    printf ("Enter digits: ");

    rtn = scanf ("%d", value);     /* read 1 integer */

    if (rtn == EOF)
        fputs ("(user canceled input.)\n", stderr);
    else if (rtn == 0)
        fputs (" readintstdin() error: invalid input.\n", stderr);

    empty_stdin();  /* remove all remaining chars from stdin */

    return rtn;
}

注意:使用scanf时,您负责处理三种情况:

  1. 用户通过按Ctrl+d(或Ctrl+z手动生成EOF取消输入windows);
  2. 一个匹配输入失败;和
  3. 良好的输入情况,然后您必须验证输入是否正确收到在预期范围内等..

考虑到这一点,您可以设计一个函数来读取 stdin 类似于:

int readintstdin (int *value) {

    int rtn = 0;

    printf ("Enter digits: ");

    rtn = scanf ("%d", value);     /* read 1 integer */

    if (rtn == EOF)
        fputs ("(user canceled input.)\n", stderr);
    else if (rtn == 0)
        fputs (" readintstdin() error: invalid input.\n", stderr);

    empty_stdin();  /* remove all remaining chars from stdin */

    return rtn;
}

注意两件事, 值的**地址作为参数传递,允许输入存储在该内存地址并在调用函数中可用(main() 这里)和 scanfreturn 值被 return 编辑为函数 return 表示 success/failure main() 中的回读。您必须始终使用适当的 return 类型来设计一个函数,该类型将在调用函数中指示函数的成功或失败。否则,就和scanf的return开始检查失败没什么两样。

您的下一个目标似乎是读取 "digits.txt" 中的所有值。虽然这取决于您,但一般方法是在调用者中打开 FILE* stream 以验证文件是否已打开以供读取,然后再将该文件流作为参数传递给函数。否则,如果打开失败,则无需调用开头的函数,例如:

int readfromfile (FILE *fp) {

    int value = 0, count = 0;

    while (fscanf (fp, "%d", &value) == 1) {    /* while valid int read */
        printf ("%d ", value);
        count++;
    }

    putchar ('\n'); /* tidy up with newline */

    return count;   /* return value indicating number of values read */
}  

这里表示从文件读取的success/failure,从文件读取的值的个数是returned。如果 return 是 0,则没有从文件中读取任何值。

接下来是您的菜单。您需要将这些相同的课程应用到您的菜单中。这意味着您需要处理 scanf 的所有三种情况,并在每次显示菜单之间删除 stdin 中保留的所有字符。将这些保护措施落实到位,您可以执行以下操作,例如

#define MAXC 1024
...
int main (void) {

    int input = 1,          /* initialize all variables */
        value = 0;
    char fname[MAXC] = "";  /* buffer to hold filename to read */
    FILE *fp = NULL;

    while (0 < input && input < 3) {
        int rtn = 0;    /* store scanf return */
        printf ("\nPlease select which method you'd like to access:\n"
                " 1) readintstdin()\n"
                " 2) readfromfile()\n"
                " (any other numeric input exits)\n\n"
                " choice: ");
        rtn = scanf ("%d", &input);
        if (rtn == EOF) {   /* user generated manual EOF */
            fputs ("(user canceled input)\n", stderr);
            break;
        }
        else if (rtn == 0) {    /* no integer input */
            fputs (" error: invalid integer input.\n", stderr);
            empty_stdin();  /* empty all chars from stdin */
            continue;       /* re-display menu, try again */
        }
        else    /* good integer input */
            empty_stdin();  /* empty all chars from stdin */
        ...

剩下的就是在 input 上处理 switch。第一个调用 readintstdin 的情况很简单,只需调用 readintstdin 传递 value 地址作为其参数并检查函数 return在打印 value 之前是 non-zero,例如

        switch (input) {    /* switch on integer input */
            case 1:
                if (readintstdin (&value))  /* read from stdin */
                    printf ("value: %d\n", value);
                break;

第二种情况需要多考虑一下。您将从用户那里获取文件名的输入(因此 main() 开头的变量 fname 的用途)。在这里,您只需提示并读取文件名(可能包含空格),验证读取,然后将文件名传递给 fopen 以验证文件是否已打开以供读取。一旦您确认文件已打开以供读取,您只需将打开的文件流传递给您的函数(检查函数的 return 是否为 success/failure),然后在完成后关闭文件。这可以类似于:

            case 2:
                printf ("\n enter filename: "); /* get filename to read */
                if (scanf ("%1023[^\n]", fname) != 1) {
                    fputs ("(user canceled input)\n", stderr);
                    empty_stdin();
                    break;
                }
                /* open/validate file open for reading */
                if ((fp = fopen (fname, "r")) == NULL) {
                    perror ("fopen-fname");
                    empty_stdin();
                    break;
                }
                if (!readfromfile (fp)) /* read/output integers */
                    fprintf (stderr, "error: no values read from '%s'\n",
                            fname);
                fclose (fp);        /* close file */
                break;

注意: 您的 default: 案例应该告知用户收到了一个不在可接受的菜单值范围内的值,然后您可以指示程序已完成。 (这只是一个小问题,您可以随意处理)

总而言之,您可以执行以下操作:

#include <stdio.h>

#define MAXC 1024

void empty_stdin (void)
{
    int c = getchar();

    while (c != '\n' && c != EOF)
        c = getchar();
}

int readintstdin (int *value) {

    int rtn = 0;

    printf ("Enter digits: ");

    rtn = scanf ("%d", value);     /* read 1 integer */

    if (rtn == EOF)
        fputs ("(user canceled input.)\n", stderr);
    else if (rtn == 0)
        fputs (" readintstdin() error: invalid input.\n", stderr);

    empty_stdin();  /* remove all remaining chars from stdin */

    return rtn;
}

int readfromfile (FILE *fp) {

    int value = 0, count = 0;

    while (fscanf (fp, "%d", &value) == 1) {    /* while valid int read */
        printf ("%d ", value);
        count++;
    }

    putchar ('\n'); /* tidy up with newline */

    return count;   /* return value indicating number of values read */
}  

int main (void) {

    int input = 1,      /* initialize all variables */
        value = 0;
    char fname[MAXC] = "";
    FILE *fp = NULL;

    while (0 < input && input < 3) {
        int rtn = 0;    /* store scanf return */
        printf ("\nPlease select which method you'd like to access:\n"
                " 1) readintstdin()\n"
                " 2) readfromfile()\n"
                " (any other numeric input exits)\n\n"
                " choice: ");
        rtn = scanf ("%d", &input);
        if (rtn == EOF) {   /* user generated manual EOF */
            fputs ("(user canceled input)\n", stderr);
            break;
        }
        else if (rtn == 0) {    /* no integer input */
            fputs (" error: invalid integer input.\n", stderr);
            empty_stdin();  /* empty all chars from stdin */
            continue;       /* re-display menu, try again */
        }
        else    /* good integer input */
            empty_stdin();  /* empty all chars from stdin */

        switch (input) {    /* switch on integer input */
            case 1:
                if (readintstdin (&value))  /* read from stdin */
                    printf ("value: %d\n", value);
                break;
            case 2:
                printf ("\n enter filename: "); /* get filename to read */
                if (scanf ("%1023[^\n]", fname) != 1) {
                    fputs ("(user canceled input)\n", stderr);
                    empty_stdin();
                    break;
                }
                /* open/validate file open for reading */
                if ((fp = fopen (fname, "r")) == NULL) {
                    perror ("fopen-fname");
                    empty_stdin();
                    break;
                }
                if (!readfromfile (fp)) /* read/output integers */
                    fprintf (stderr, "error: no values read from '%s'\n",
                            fname);
                fclose (fp);        /* close file */
                break;
            default:    /* handle invalid input */
                fputs ("Selection not menu entry.\n", stderr);
                break;
        }  
    }
    printf ("Program complete\n");

    return 0;
}

注意: 除非您使用 stdlib.h 提供的函数或常量,否则没有理由包含 header。它没有坏处,它只是揭示了对所需 header 文件的误解。

示例Use/Output

现在开始练习您的程序,并在每次提示时输入 improper/invalid,有意尝试打破您的输入例程。如果有东西坏了,去修理它。我没有尝试每一个极端情况,但了解你的输入函数是如何工作的,可以让你从一开始就防范大多数极端情况。查看第一个整数输入 "four-hundred twenty-one",程序是否中断?否。查看无效文件名 "somefile.ext",程序是否中断?不。为您的每个输入执行此操作。

$ ./bin/menureadfilestdin

Please select which method you'd like to access:
 1) readintstdin()
 2) readfromfile()
 (any other numeric input exits)

 choice: 1
Enter digits: four-hundred twenty-one
 readintstdin() error: invalid input.

Please select which method you'd like to access:
 1) readintstdin()
 2) readfromfile()
 (any other numeric input exits)

 choice: 1
Enter digits: 421
value: 421

Please select which method you'd like to access:
 1) readintstdin()
 2) readfromfile()
 (any other numeric input exits)

 choice: 2

 enter filename: somefile.ext
fopen-fname: No such file or directory

Please select which method you'd like to access:
 1) readintstdin()
 2) readfromfile()
 (any other numeric input exits)

 choice: 2

 enter filename: dat/digits.txt
1 10 299 1 200 15 3 150 13 2 150

Please select which method you'd like to access:
 1) readintstdin()
 2) readfromfile()
 (any other numeric input exits)

 choice: 1
Enter digits: 422
value: 422

Please select which method you'd like to access:
 1) readintstdin()
 2) readfromfile()
 (any other numeric input exits)

 choice: (user canceled input)
Program complete

希望这解决了您的大部分问题并帮助您避免 "really getting stressed out over WHAT I FEEL is a simple problem." 是的,但是 C 需要的理解深度远远超出 "I'll try this, if it doesn't work, I'll change something, recompile and try again..." 您可以控制整个机器并负责处理输入缓冲区的每个字节并确保程序处理的每个字节都有足够的存储空间。 C 上没有 training-wheels。这就是它获得超快速度的地方。但是,"with great power comes great responsibility".

脚注:

1. 虽然不是错误,但 C 通常会避免使用 camelCaseMixedCase 变量名以支持所有 lower-case 同时保留 upper-case 名称用于宏和常量。这是一个风格问题——因此完全取决于您,但不遵循它可能会导致在某些圈子中产生错误的第一印象。