Search code examples
cfilereadfile

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


So to simplify my problem, I created this more modular program that asks for user input first in my switch case in my 'main()' and in two other methods as well. I have this code underneath, and in my readDigits() method I am wondering how to get specific values from value into 'firstDigit and secondDigit. Say when value == 5, I would want '5' to get into 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;
}

I understand that this is not nearly the hardest thing ever, however, I am new to C and am really getting stressed out over WHAT I FEEL is a simple problem.

My file of digits.txt has the values:

1
10
299
1
200
15
3
150
13
2
150


Solution

  • Thank you for posting your input file. The reason your question has been less than well-received is that it is quite difficult to determine what it is you were trying to achieve. While it is apparent you need help, exactly where to start is a bit of a mystery.

    Why? For starters, your getNumbers()[1] and getMoreNumbers() functions do exactly the same thing, simply using a different global variable. There is no reason to use global variables in any of your functions, as they can be quite easily passed as parameters to any function that needs them. More generally, you want to avoid the use of global variables unless absolutely necessary (there are legitimate uses of globals, but none you are likely to encounter as you are initially learning C -- avoid them)

    Next, if you learn nothing else from this answer, learn to always validate user input by at minimum checking the return of the function used. Otherwise you are inviting Undefined Behavior by blindly using the input variable without any verification that it actually contains valid data.

    Stepping back and trying to understand what you are waning to do, it appears your goal is twofold, (1) read an integer from stdin based on a menu selection, or (2) read from a file "digits.txt" if an alternative menu selection was made. However, your readDigits() function is never called. Further, what if you want to read from a file other than "digits.txt"? Why not simply prompt the user for a filename and then open that file (handling any error), read all integers from the file, and then close the file when done?

    So how do we accomplish those two goals, either reading an integer from stdin, or reading all integers from the filename given by the user?

    To begin, you have to completely validate all input given by the user and then act on that input handling any error that may arise. scanf (and its family of functions) are full of pitfalls for the new C programmer. For example, what happens in the user accidentally types an alpha-character instead of a "digit" at any of your scanf ("%d", ...) function calls? Try it.

    When using scanf, if the input does not match the conversion-specifier used a matching failure occurs. When a matching failure occurs, reading from the input buffer stops, no further characters are extracted, leaving all characters as they existed at the time of the failure unread in the input buffer (stdin in your case), just waiting to bite you again on your next attempted read. Further, only numeric conversion specifiers and "%s" consume leading whitespace.

    Meaning any other conversion specifier (or line-oriented input function for that matter) used for input with either fail to read anything (stopping when they encounter the '\n' generated by the user pressing Enter and left in stdin), or it will read '\n' as the user input. Neither is what you want.

    How do you ensure that each input taken from the user will be what the user input? You clean up after yourself by emptying the input buffer after each input to insure there are no stray or additional characters left in stdin from the prior input attempt. (and this is also why new C programmers are encouraged to avoid scanf for user input and instead use a line-oriented input function like fgets() (with an adequate buffer size) or POSIX getline that will read and allocate as necessary to read any line. Both line-oriented functions read and include the '\n' in the buffers they fill eliminating the chance it remains in the input buffer unread.

    If you choose to go forward with scanf, then at least manually empty the input buffer (stdin) with a simple function each time there is the possibility of characters remaining in stdin unread. A simple function will do it. Just read each character with getchar() until the '\n' or EOF is encountered, e.g.

    void empty_stdin (void)
    {
        int c = getchar();
    
        while (c != '\n' && c != EOF)
            c = getchar();
    }
    

    note: if a function takes no parameters, then the proper declaration include (void) as the parameter list to make it explicit that no parameters are expected.

    Now with a way to properly handle removing characters that remain in stdin, you are ready to look at your input functions.

    If your intent with getNumbers()[1] and getMoreNumbers() was to simply read an integer from stdin, then simply use one function with a slightly more descriptive name to do the job, such as:

    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;
    }
    

    Note: when using scanf, you are responsible for handling three cases:

    1. the user cancels input by manually generating EOF by pressing Ctrl+d (or Ctrl+z on windows);
    2. a matching or input failure; and
    3. the good input case, where you then must validate whether the input received was within the expected range, etc..

    With that in mind you can craft a function to read from stdin similar to:

    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;
    }
    

    note two things, the **address of value is passed as the parameter allowing the input to be stored at that memory address and available back in the calling function (main() here) and the return value for scanf is returned as the function return indicating success/failure of the read back in main(). You must always craft a function with an appropriate return type that will indicate success or failure of the function back in the calling function. Otherwise, it is no different that failing to check the return of scanf to begin with.

    Your next goal appears to be reading all values from "digits.txt". While it is up to you, the general approach is to open the FILE* stream back in the caller to validate a file is open for reading before passing that file stream as a parameter to the function. Otherwise, if the open fails, there is no need to call the function to begin with, e.g.:

    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 */
    }  
    

    Here to indicate success/failure of the read from the file, the number of values read from the file is returned. If the return is 0, then no values were read from the file.

    Next, your menu. You need to apply these same lessons to your menu. Meaning you need to handle all three cases for scanf and remove any characters that remain in stdin between each display of the menu. Putting those safeguards in place, you could do something like the following, e.g.

    #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 */
            ...
    

    All that remains is handling switch on input. The first case calling readintstdin is trivial, just call readintstdin passing the address of value as its parameter and check the function return is non-zero before printing value, e.g.

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

    The second case takes a bit more thought. You will take input from the user for the filename (thus the purpose for the variable fname at the beginning of main()). Here you simply prompt and read the filename (which may contain whitespace), validating the read and then passing the filename to fopen validating that the file is open for reading. Once you have confirmed that the file is open for reading, you can simply pass the open file stream to your function (checking the return of your function for success/failure) and then closing the file when done. That could be done similar to:

                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;
    

    note: your default: case should advise the user that a value not within the accepted menu values was received, and then you can indicate the program was complete. (this is just a nit and you can handle it any way you like)

    Putting it altogether, you could do something like the following:

    #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;
    }
    

    note: unless you are using a function or constant provided by stdlib.h, there is no reason to include the header. It doesn't hurt, it just reveals a misunderstanding of the required header files.

    Example Use/Output

    Now go exercise your program and intentionally try and break your input routine by entering improper/invalid input at each of your prompts. If something breaks, go fix it. I didn't try every corner case, but understanding how your input functions work, lets you protect against most corner cases to begin with. Look at the first integer input "four-hundred twenty-one", does the program break? No. Look at the invalid filename "somefile.ext", does the program break? No. Do that for each of your inputs.

    $ ./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
    

    Hopefully this addressed most of your problems and helps you avoid "really getting stressed out over WHAT I FEEL is a simple problem." It is, but C requires a depth of understanding well beyond "I'll try this, if it doesn't work, I'll change something, recompile and try again..." You have control over the entire machine and are responsible for handling each byte of your input buffer and ensuring adequate storage for each byte your program handles. There are no training-wheels on C. That is where it derives its blazing speed. But, "with great power comes great responsibility".

    footnotes:

    1. While not an error, C generally avoids the use of camelCase or MixedCase variable names in favor of all lower-case while reserving upper-case names for use with macros and constants. It is a matter of style -- so it is completely up to you, but failing to follow it can lead to the wrong first impression in some circles.