Getting Text In and Out

You built a string by hand last lesson; the library will now do it for you, and the choice of which function is one of the most consequential style decisions in C. This lesson covers %s on both sides of the I/O divide, the buffer-overrun trap built into naive reading, the function the standards committee eventually expelled from the language, and fgets, the tool that reads lines safely.

Printing: %s and Friends

printf("%s", word) walks the string to its terminator, printing every character. The chapter 4 format machinery applies unchanged: %10s right-justifies in ten columns, %-10s left-justifies, which is how the table layouts from that chapter handled their name columns. Printing is the easy direction; the terminator does all the work.

printf is not the only way out, and the alternatives are shorter to write on an exam paper:

Three identical lines of output, three spellings. puts(line) writes the string and then adds a newline, making it exactly printf("%s\n", line). fputs(line, stdout) writes the string and adds nothing, which is why the newline above needs its own call. That one difference is the whole of what exam papers ask about these two, and it runs the opposite way to the input side: fgets keeps the newline it finds, while fputs refuses to add one.

Reading a Word: scanf's %s

On input, %s skips leading whitespace, then reads characters up to the next whitespace, and appends the terminator for you:

Run it twice: type Ada, then run again and type Grace Hopper. The second run greets only Grace: %s reads one whitespace-delimited word, and Hopper stays in the stream. Note also what %s does not take: no & before name. Why an array needs no address-of operator is the pointers chapter's opening revelation; for now, the rule is that %s takes the array bare.

The number in %19s is the load-bearing character of this lesson. It is a field width: read at most 19 characters, leaving the twentieth byte for the terminator, capacity minus one exactly as in the hand-built loop. A bare %s has no such limit, and a user who types more than the buffer holds produces out-of-bounds writes, buffer overflow, the vulnerability class that made C infamous. The rule is absolute: every scanf %s carries a width one less than the buffer size.

The Cautionary Tale: gets

Old textbooks and old exams feature gets(buffer), which read a whole line into a buffer with no way to state the buffer's size at all. Every call was a potential overflow; the 1988 Morris worm exploited exactly this pattern, and after decades of damage the C11 standard removed gets from the language entirely, the only function ever expelled. On this platform it does not compile. If an exam asks, the answer has three parts: what it did, why it was unfixable (no size parameter), and what replaced it.

Papers introduce gets and puts together and expect them recited together, so be ready to say why only one of them survived. puts writes a string whose extent it can discover for itself by walking to the terminator; gets read into a buffer whose extent it could never discover, because the caller had no way to state it. Perfectly symmetrical on the page, opposite in safety, and the asymmetry is the answer.

Reading a Line: fgets

The replacement reads a line with a size, and it is the course's standard line reader from here on:

Type Grace Hopper and the whole line comes back, spaces included. The pieces of the contract: fgets(buffer, size, stdin) reads at most size - 1 characters, always terminates the string, and returns NULL when input has ended, the guarded-read pattern in its string form. stdin names the standard input stream; its full story arrives with files in chapter 12.

One quirk demands handling: fgets keeps the newline when the line fits, so "Grace Hopper\n" is what lands in the buffer. The scan-and-replace loop above finds the '\n' and overwrites it with '\0', trimming the string. Forgetting the trim is the fgets bug: output gains mysterious blank lines, and comparisons against "expected" fail because the stored string is "expected\n".

Which Reader, When

The division of labor, exam-ready: scanf("%Ns") for a single whitespace-free token, width mandatory; fgets for a whole line, spaces and all, trim mandatory; the hand-built getchar loop when you need custom behavior neither provides. And mixing scanf with fgets revives chapter 4's leftover-newline problem: a scanf leaves '\n' in the stream, and the next fgets reads it as an instantly-empty line. Prefer one style of reading per program; when forced to mix, a throwaway getchar loop clears the line first.

Key Takeaways

  • %s prints to the terminator; field widths and flags from chapter 4 apply.
  • puts(s) prints the string plus a newline; fputs(s, stdout) prints it with no newline added; puts outlived its input-side twin gets because a writer can find a string's end and a reader cannot find a buffer's.
  • scanf("%19s", name) reads one word into a 20-byte buffer: width equals capacity minus one, always, and the array is passed without &.
  • A bare %s is an invitation to buffer overflow, and gets was removed from C for making that unfixable; know its story for exams.
  • fgets(buffer, size, stdin) reads a line safely, returns NULL at end of input, and keeps the newline, which you trim by overwriting with '\0'.
  • Words: bounded %s. Lines: fgets plus trim. Custom: the getchar loop. Avoid mixing readers in one program.