2

Disclaimer. I've seen tons of questions including almost the exact same code snippets, but none seem to answer this question.

For an entry level CS class we are tasked with making a simple program that takes ID, name and age input from a user and saves it to a file. This was simple enough and I got it working pretty quick. The problem is, to get one part, the name input, working properly, I had to "cheat" my way around a problem I met.
The code snippet in question.

int id, age;
char name[40]={0};
printf("ID: ");
scanf("%i",&id);
printf("Name: ");
scanf("%*c");
scanf("%[^\n]%*c",name);
printf("Age: ");
scanf("%i",&age);

This works fine. But this line annoys me; scanf("%*c"); Its only purpose is disposing of a '\n' character lurking in the stream, probably from the previous input. For some reason I feel like this is cheating or that I'm doing something wrong if I have to use this workaround. Any tips appreciated.

joachimbf
  • 106
  • 8
  • 3
    General advice: don't use `scanf()`. Use `fgets()` to read a line, then parse it with `sscanf()`. – Barmar Oct 06 '20 at 20:21
  • 1
    And if you *have* to use `scanf()`, have *mercy* and check that return value, or you'll be running into undefined behavior pretty quickly. – DevSolar Oct 06 '20 at 20:37

2 Answers2

7

But this line annoys me; scanf("%*c"); Its only purpose is disposing of a '\n' character lurking in the stream, probably from the previous input.

scanf() is pretty heavyweight for reading a single character. I would typically go with getchar() for that, and just ignore the result if you don't care what it is.

For your particular case, however, I would go with the suggestion made by @OlafDietsche in comments: insert a space (or a newline or tab) at the beginning of the format string for the next scanf:

scanf(" %[^\n]%*c",name);

That will match any run of zero or more whitespace characters, and thus will eat up your newline, plus any subsequent blank lines and any leading whitespace on the next line that has any non-whitespace. That's the standard behavior for most other field directives anyway, which is why you don't have the same issue with your numeric fields, but it is intentionally not done automatically for %[ and %c fields, so that these can read and return whitespace.

That also means that you don't need to explicitly consume the newline at the end of the name line, either ...

scanf(" %[^\n]",name);

... because it will be consumed automatically by the %i directive in the subsequent scanf() call.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • `scanf(" %[^\n]",name);` is about as good as [`gets()](https://stackoverflow.com/questions/1694036/why-is-the-gets-function-so-dangerous-that-it-should-not-be-used) with its lack of width limit - even for a entry level CS class. – chux - Reinstate Monica Oct 07 '20 at 05:37
0

Consider using fget() to read a line of user input @Barmar, then parse.

char buffer[100];
int id, age;
char name[40]={0};

printf("ID: ");
fgets(buffer, sizeof buffer, stdin);
sscanf(buffer, "%i", &id);

printf("Name: ");
fgets(buffer, sizeof buffer, stdin);
sscanf(buffer, "%[^\n]", name);

printf("Age: ");
fgets(buffer, sizeof buffer, stdin);
sscanf(buffer, "%i", &age);

As one's skills improve, add error checking.

printf("ID: ");
if (fgets(buffer, sizeof buffer, stdin) == NULL) Handle_end_of_file();
if (sscanf(buffer, "%i",&id) != 1) Handle_non_numeric_input();
// also research strtol()

printf("Name: ");
if (fgets(buffer, sizeof buffer, stdin) == NULL) Handle_end_of_file();
if (sscanf(buffer, " %99[^\n]",name) != 1) Handle_space_only_name();
// Later, additional code to detect excessively long name 
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • Using a `getchar()` loop is IMHO cleaner than jumping through all the hoops needed to make `fgets()` work cleanly in cases where e.g. someone types more input than expected. – supercat Oct 09 '20 at 14:54
  • @supercat Std C, IMO, lacks a robust `read_a_line()`. I'd like a `int read_a_line( size_t sz, char *buf, size_t *non_LF_read_count)` that _always_ read an entire line (though not saving \n) and returned `EOF` on end-of-file/input error and 1/0 on success/failure (buffer too small). *nix `getline()` unfortunate allows the user to overwhelm memory resources. – chux - Reinstate Monica Oct 09 '20 at 16:04
  • 1
    I think I'd probably favor a flags argument to control some aspects of behavior, such as whether the implementation should when practical provide immediate feedback if the input is too long, whether it should store a trailing zero, etc. Different use cases benefit from slightly different corner case behaviors, and having a flags argument is probably cleaner than having multiple different functions to accommodate them or else requiring user code to work around the lack of clean handling within the standard function. – supercat Oct 09 '20 at 17:10
  • @supercat Good idea about flags. Hmmm. – chux - Reinstate Monica Oct 09 '20 at 17:30
  • Flags are a bit icky, but there are many usage cases where code would want a trailing zero byte added, but also many where code might want to receive input into a buffer which is precisely large enough to hold it [and which will be either zero-padded, have its length stored elsewhere, or be expected to always hold data of a certain length]. If a read-line routine doesn't add a trailing zero byte in the maximum-length-input case, user code would need to add one, but insisting upon storing a zero byte would make the routine useless for precise-fit scenarios. – supercat Oct 09 '20 at 17:34
  • @supercat Retention or not of the `'\n'`, line truncation ok or not, fail on reading \0 are other flags that come to mind. Sounds like a [LOTR](https://en.wikipedia.org/wiki/One_Ring) function - one function to rule them all. [Bru-ha-ha](https://en.wikipedia.org/wiki/Brouhaha) – chux - Reinstate Monica Oct 09 '20 at 17:40