Scanf and a manual function to get a string when they work together

advertisements

I have a trouble with scanf and a manual function to get string in the input.

Here is my manual function to get a line of string in input (I also get the [nl] character):

void getln(char *a) {
    int i,c;
    i=0;
    do {
        c=getchar();
        a[i]=(char)c;
        i++;
    } while(c!='\n');
}

Then, I using it like this (char hs.school[40]; char hs.pc[20]; int hs.age;):

printf("Import age: ");
scanf("%d",&hs.age);
printf("Import personal code: ");
getln(hs.pc);
printf("Import school: ");
getln(hs.school);

The output:

Import age: 18
Import personal code: Import school: Vo Thi Sau

Why the getln call right after scanf call is ignored? (But the next getln works well) Can you explain me the details and suggest me how to fix this bug. Thanks!

Edited: Here is my full code that take the user inputs and export that inputs back to the screen, which is run well after I did a little trick, but I decide to make a question, mainly for expanding my knowlegde ^_^ Thanks for your answers.

#include<stdio.h>

void getln(char *);
void putstr(char *);

int main(void) {
    struct Student {
        struct Fullname {
            char first[10],middle[20],last[10];
        }fu;
        struct Native {
            char social[30],district[30],province[30];
        }na;
        struct Score {
            double maths,physics,chemistry;
        }sc;
        char pc[20],school[40];
        int age;
    }hs;
    printf("Import stage:\n");
    printf("- Import full name:\n");
    printf("++ First name: ");
    getln(hs.fu.first);
    printf("++ Middle name: ");
    getln(hs.fu.middle);
    printf("++ Last name: ");
    getln(hs.fu.last);
    printf("- Import native living place:\n");
    printf("++ Social: ");
    getln(hs.na.social);
    printf("++ District: ");
    getln(hs.na.district);
    printf("++ Province: ");
    getln(hs.na.province);
    printf("- Import school: ");
    getln(hs.school);
    printf("- Import personal code: "); // I have done a little trick
    getln(hs.pc);                       // before I post the question,
    printf("- Import age: ");           // which swaped these two stage,
    scanf("%d",&hs.age);                // but it's works like a charm ^_^
    printf("- Import scores:\n");
    printf("++ Mathematics: ");
    scanf("%lf",&hs.sc.maths);
    printf("++ Physics: ");
    scanf("%lf",&hs.sc.physics);
    printf("++ Chemistry: ");
    scanf("%lf",&hs.sc.chemistry);
    printf("\nExport stage:\n");
    printf("- Full name: ");
    putstr(hs.fu.first);
    printf(" ");
    putstr(hs.fu.middle);
    printf(" ");
    putstr(hs.fu.last);
    printf(".\n");
    printf("- Native living place: ");
    putstr(hs.na.social);
    printf(", ");
    putstr(hs.na.district);
    printf(", ");
    putstr(hs.na.province);
    printf(".\n");
    printf("- School: ");
    putstr(hs.school);
    printf(".\n");
    printf("- Personal code: ");
    putstr(hs.pc);
    printf(".\n");
    printf("- Age: %d.\n",hs.age);
    printf("- Scores (Mathematics, Physics, Chemistry): %.2lf, %.2lf, %.2lf.\n",hs.sc.maths,hs.sc.physics,hs.sc.chemistry);
    return 0;
}

void getln(char *a) {
    int i,c;
    i=0;
    do {
        c=getchar();
        a[i]=(char)c;
        i++;
    } while(c!='\n');
}
void putstr(char *a) {
    int i;
    i=0;
    while(a[i]!='\n') {
        putchar(a[i]);
        i++;
    }
}


Can you explain me the details ...

I'll try not to use confusing terms such as buffer.

You probably already know that "%d" corresponds to a set of decimal digit characters which get transformed into an int. When you press 'Enter' as others have suggested, the '\n' character is transmitted via stdin. '\n' isn't a decimal digit character, so it gets placed back onto the stream for your getln function to discover later on...

In reality, your "getln call right after scanf call" probably isn't ignored; it's probably just reading the trailing '\n' and seeing an empty line.

That is assuming the other problem isn't coming into play. getln can't see how many bytes a points to, so it can't tell when it's about to overflow, and hence makes no attempt to prevent buffer overflows... You've basically rewritten gets. If your input is lengthy enough, then I suppose this could also cause your problem... A buffer overflow is undefined behaviour, and the consequences of using undefined behaviour are undefined.

On the topic of undefined behaviour, since getln isn't technically producing a string, I do hope you're not using it as input for a standard string function later on...

Also on the topic of undefined behaviour, what do you suppose might happen if the user enters something that isn't a set of decimal digits? scanf conveys input errors via the return value... so never ignore the return value. You can (and should, at some point) find more information about this in the scanf manual.


... and suggest me how to fix this bug.

It doesn't make a whole lot of sense to discard user input, but unfortunately you can't expect a solution that results in better user experience without blowing your code size (and this explanation) well out of proportion.


You can discard the remainder of the line (which is probably just a '\n') following the set of decimal digits using scanf like so: scanf("%*[^\n]"); getchar();... Following the "%d" scanf call, of course... You could even merge the two together, like so:

if (scanf("%d%*[^\n]", &hs.age) != 1) {
    puts("ERROR: EOF or file access error.");
    exit(0);
}
getchar();

Unfortunately, if your user uses the spacebar key rather than enter, he or she probably won't find out about the problems with this until it's slightly too late...


As for the buffer overflow problem, I recommend using fgets rather than gets. fgets also has failure modes, which are conveyed via the return value and the contents of the array. The return value is used to convey EOF and file access errors, and the presense (or rather lack of) of a '\n' in the return value is used to convey when the input line was too large to store in the array. We can notify the user of the overflow (which I'm sure they'll appreciate) and discard the excess using the same scanf trick used earlier...

if (fgets(hs.pc, sizeof hs.pc, stdin) == NULL) {
    puts("ERROR: EOF or file access error.");
    exit(0);
}

size_t size = strcspn(hs.pc, "\n");
if (hs.pc[size] != '\n') {
    printf("WARNING: MAXIMUM SIZE OF %zu EXCEEDED! LINE TRUNCATED.\n", sizeof hs.pc - 1);
    scanf("%*[^\n]");
    getchar();
}

hs.pc[size] = '\0';

I suppose it would make sense to wrap these solutions into functions, except that the functions would then promote the discarding of user input. Nonetheless, the later one is lengthy enough that you'd most likely benefit from abstraction...

void getln(char *a, size_t a_size) {
    if (fgets(a, a_size, stdin) == NULL) {
        puts("ERROR: EOF or file access error.");
        exit(0);
    }

    size_t size = strcspn(a, "\n");
    if (a[size] != '\n') {
        printf("WARNING: MAXIMUM SIZE OF %zu EXCEEDED! LINE TRUNCATED.\n", a_size - 1);
        scanf("%*[^\n]");
        getchar();
    }

    a[size] = '\0';
}

... and now you can use that like so: getln(hs.pc, sizeof hs.pc);