Step 1 of 7 · Reading · ~4 min
Learn
File I/O and the Preprocessor
File I/O
Everything you have printed so far went to stdout. A file is the same idea with the destination named: you fopen a path to get a FILE *, use the same printf-shaped calls on it, and fclose when you are done. What C adds — and it is the whole lesson — is that each of those three steps can fail or be forgotten, and none of them will tell you.
Open, check, close
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) { /* fopen reports failure by returning NULL */
perror("data.txt"); /* -> data.txt: No such file or directory */
return 1;
}
/* ... read from fp ... */
fclose(fp);
The NULL check is not defensive extra credit, it is the only report you get: a missing file, a permission you do not have, a process out of descriptors — every one of them arrives as NULL. perror prints your label followed by the system's explanation of the most recent failure, which is why it belongs immediately after the call that failed.
fclose matters for a reason beginners find surprising. Writes are buffered: fprintf(fp, ...) normally copies your bytes into memory and returns, and they reach the disk when the buffer fills or the stream is closed. A program that exits normally flushes whatever is still open, which is exactly why forgetting fclose seems harmless in a twenty-line exercise — and why it is not in a long-running program that opens files in a loop until it runs out of descriptors.
The mode string decides everything
| Mode | Opens for | If the file exists | If it does not |
|---|---|---|---|
"r" | reading | reads from the start | fails, returns NULL |
"w" | writing | truncates it to nothing | creates it |
"a" | writing | appends at the end | creates it |
"r+" | reading and writing | starts at the beginning | fails, returns NULL |
"w" emptying the file the instant you open it is the mode mistake with the widest blast radius: the old contents are gone before your first write, and it presents as "my program lost the file". Reach for "a" when you meant to add. Appending a b ("rb", "wb") selects binary mode — on Linux it changes nothing, on Windows it stops newlines being rewritten in transit, and portable code writes it anyway.
Reading a line at a time
char line[1024];
while (fgets(line, sizeof line, fp) != NULL) { /* NULL means end of file */
printf("got: %s", line); /* no \n of our own — line carries its own */
}
fgets(buf, n, fp) stops at a newline, at n - 1 characters, or at end of file, whichever comes first, and always writes the terminating '\0'. Two consequences worth memorising: the newline it stopped on is kept in the buffer — the strip step you wrote in the Strings lesson exists because of this — and the second argument is the buffer's size, so sizeof line is the thing to pass. A number larger than the buffer here is a buffer overflow with a loop attached.
The rest of the family
fprintf(fp, "...", ...) and fscanf(fp, ...) are printf and scanf with a stream in front. fwrite(buf, size, count, fp) and fread(buf, size, count, fp) move raw bytes instead — count elements of size bytes each, no formatting, no conversion — and return how many elements actually moved, which is how you notice a short read. Use them for images, structs and dumps; use the formatted calls for anything a human will read. And stdin, stdout and stderr are ordinary pre-opened FILE * values, so fprintf(stderr, "warning: %s\n", msg) works with everything above and keeps diagnostics out of the output your program is actually producing.
Your exercise
Write some lines to a file, then re-open that file and count the lines in it. The counting loop is the fgets-until-NULL shape above with a counter that starts at 0 before it, and the printed line has to match lines: <n> character for character.
Notice what the starter does before handing the job over: the second fopen gets the same NULL check as the first, even though that file was written moments earlier and is certainly there. Nothing in this exercise can punish you for dropping that check — which is exactly why it is worth keeping. fgets on a NULL stream is undefined behaviour, and the machine where the open finally fails will not be this one.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…