learn c the hard way ex25 confusing about fgetc stdin

Leo Huang Source

I am working on learn c the hard way ex25 by Zed A. Shaw

ex25

#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include "dbg.h"

#define MAX_DATA 100

int read_string(char **out_string, int max_buffer)
{
    *out_string = calloc(1, max_buffer + 1);
    check_mem(*out_string);

    char *result = fgets(*out_string, max_buffer, stdin);
    check(result != NULL, "Input error.");

    return 0;

error:
    if (*out_string) free(*out_string);
    *out_string = NULL;
    return -1;
}

int read_int(int *out_int)
{
    char *input = NULL;
    int rc = read_string(&input, MAX_DATA);
    check(rc == 0, "Failed to read number.");

    *out_int = atoi(input);

    free(input);
    return 0;

error:
    if (input) free(input);
    return -1;
}

int read_scan(const char *fmt, ...)
{
    int i = 0;
    int rc = 0;
    int *out_int = NULL;
    char *out_char = NULL;
    char **out_string = NULL;
    int max_buffer = 0;

    va_list argp;
    va_start(argp, fmt);

    for (i = 0; fmt[i] != '\0'; i++) {
        if (fmt[i] == '%') {
            i++;
            switch (fmt[i]) {
                case '\0':
                    sentinel("Invalid format, you ended with %%.");
                    break;

                case 'd':
                    out_int = va_arg(argp, int *);
                    rc = read_int(out_int);
                    check(rc == 0, "Failed to read int.");
                    break;

                case 'c':
                    out_char = va_arg(argp, char *);
                    *out_char = fgetc(stdin);
                    break;

                case 's':
                    max_buffer = va_arg(argp, int);
                    out_string = va_arg(argp, char **);
                    rc = read_string(out_string, max_buffer);
                    check(rc == 0, "Failed to read string.");
                    break;

                default:
                    sentinel("Invalid format.");
            }
        } else {
            fgetc(stdin);
        }

        check(!feof(stdin) && !ferror(stdin), "Input error.");
    }

    va_end(argp);
    return 0;

error:
    va_end(argp);
    return -1;
}

int main(int argc, char *argv[])
{
    char *first_name = NULL;
    char initial = ' ';
    char *last_name = NULL;
    int age = 0;

    printf("What's your first name? ");
    int rc = read_scan("%s", MAX_DATA, &first_name);
    check(rc == 0, "Failed first name.");

    printf("What's your initial? ");
    rc = read_scan("%c\n", &initial);
    check(rc == 0, "Failed initial.");

    printf("What's your last name? ");
    rc = read_scan("%s", MAX_DATA, &last_name);
    check(rc == 0, "Failed last name.");

    printf("How old are you? ");
    rc = read_scan("%d", &age);

    printf("---- RESULTS ----\n");
    printf("First Name: %s", first_name);
    printf("Initial: '%c'\n", initial);
    printf("Last Name: %s", last_name);
    printf("Age: %d\n", age);

    free(first_name);
    free(last_name);
    return 0;
error:
    return -1;
}

I am confused about the first parameter in read_scan in line 109

The original code works fine. The output:

What's your first name? zed
What's your initial? A
What's your last name? shaw
How old are you? 18
---- RESULTS ----
First Name: zed
Initial: 'A'
Last Name: shaw
Age: 18

However, if I delete the '\n' in line 109 rc = read_scan("%c", &initial);, it will skip the next question and I cannot figure out.

What I think about the influence of '\n' is that the for loop will not go into line 83 fgetc(stdin);

The output will be:

What's your first name? zed
What's your initial? A
What's your last name? How old are you? shaw
---- RESULTS ----
First Name: zed
Initial: 'A'
Last Name:
Age: 0

Thanks for help!

c

Answers

answered 3 months ago James McPherson #1

The first parameter is the type and size of the format string which read_scan() needs to process

rc = read_scan("%c\n", &initial);

In this case, it's telling read_scan() to read (a) any character AND (b) a newline character. If you look at the flow in the function, it checks for "%", if found it increments the counter i and then checks the next character in the format string. For line 109, that character is c, so the fgetc() function is called. This reads a single character from the file descriptor stdin and places it into out_char. Then the loop continues, and since we're not reading a string, we go to the else case on line 82 - which reads another character (which is '\n' for newline). The important difference with the fgetc call on line 83 is that we throw away the result.

answered 3 months ago Jonathan Leffler #2

The characters in the "%c\n" to read_scan() mean read and capture one character, and read and discard another character. You could have an X or @ instead of the \n and it would work the same. When you delete the newline from the format, the newline after the character is left in the input. Then the next call to read_scan() with "%s" invokes fgets(), which reads up to the next newline, but the next newline is the already in the input stream, so it returns immediately.

Note that if you typed a word instead of an initial, or if you have no middle initial (me!), things go wrong in different ways.

comments powered by Disqus