C development on Linux – Basic I/O – VIII.

Introduction

With this part of our C development on Linux article we are getting ready to get out of the theoretical zone and enter the real life one. If you followed the series until this point and tried to solve all the exercises, you will now have some idea about what C is about, so you need to get out in the wild and do some practical stuff, without which theory doesn’t have much value. Some of the concepts you’ll see below are already known, but they are extremely important for any C program on any Unix-like OS. Yes, the information is valid regardless of the OS, as long as it’s some kind of Unix, but if you’ll stumble onto something Linux-specific, you will know. We will treat concepts like standard input, output and error, in-depth printf() and file access, among others.

Basic I/O

Before we go any further, let’s take some time and see what this I/O is about. As many of you know, the term stands for Input/Output and has a broad meaning, but in our case we are interested on how to print messages to the console and how to get input from the user, plus more advanced topics in the same vein. The standard C library defines a series of functions for this, as you will see, and after reading a bit you’ll notice that you will find it pretty hard to live without, unless you want to re-write said functions for fun. It better be clear from the start that the facilities this material talks about aren’t part of the C language per se; as I said, the standard C library offers them.

Standard I/O

In short, the above subtitle means “get input from the user, print characters on the standard output and print errors on standard error”. Nowadays, the main input source, at least at this level, is the keyboard, and the device the system prints on is the screen, but things weren’t always like this. Input was made on teletypes (by the way, the device name tty comes from that), and the process was slow and clunky. Any Unix-like system still has some historical leftovers regarding, but not only, I/O, but for the rest of this article we will treat stdin as the keyboard and stdout/stderr as the screen. You know that you can redirect to a file, by using the ‘>’ operator offered by your shell, but we aren’t interested in that for the time being. Before we begin the article finally, a little reminder: Mac OS up to version 9 has some unique features regarding our subject that pushed me to read some documentation before starting development on it. For example, on all Unix(-like) systems the Enter key generates a LF (line feed). On Windows it’s CR/LF , and on Apple up to Mac OS 9 it’s CR. In short, every commercial Unix vendor tried to make their OSs “unique” by adding features. Speaking of documentation, your system’s manual pages will prove invaluable, although maybe arid at times, and also a good book on Unix design will look good at your side.

We’ve seen printf() in our previous installments and how to print text on the screen. We’ve also seen scanf() as a means to get text from the user. For single characters, you can count on getchar() and putchar(). We’ll see now some useful functions from headers included in the standard library. The first header we will talk about is ctype.h, and it contains functions useful for checking the case of a character or changing it. Remember that every standard header has a manual page, explaining what functions are available, and said functions in turn have man pages, detailing the return types, arguments and so on. Here’s an example that converts every character in a string to lowercase, using tolower(). How would you attain the opposite?

#include <stdio.h>
#include <ctype.h>

int main()
{
  int c; /* the character read*/
  while ((c = getchar()) != EOF)
    putchar (tolower(c));
  return 0;
}

Another question for you is: in what way should the code be modified so that it prints the lower-cased result only after a sentence? That is, provided the sentence is always ended by a dot and a space.

printf() in detail

Since it’s a function so widely used, I only felt that it deserves a sub-section of its’ own. printf() accepts arguments prefixed with the ‘%’ symbol and followed by a letter (or more), thus telling it what kind of input it should expect. We’ve worked before with ‘%d’, which stands for decimal, which is appropriate when working with integers. Here’s a more complete list of printf()’s format specifiers:

  • d, i – integer
  • o – octal, without the prefixing zero
  • x, X – hexadecimal, without the prefixing 0x
  • u – unsigned int
  • c – char
  • s – string, char *
  • f, e, E, g, G, – float – check your system’s printf() manual
  • p – pointer, void *, implementation-dependent, standard between Linux distros

I highly recommend you to take some time to play with these specifiers, and the fact that I didn’t get into more detail like precision is because you will have to do some reading for yourself. While you’re at it, pay special attention to the variable argument list part, and note that Linux has a command named printf, as part of coreutils, so make sure that you use the section 3 manpage (Linux-specific, as other Unices may have the manual sections laid out differently).

scanf() is the opposite of printf, in that it takes input from the user instead of outputting to the user. The format specifiers are almost the same, with certain exceptions regarding floats and the fact that it doesn’t have a %p. Why do you think that is? It also supports variable arguments lists, just like printf().

File access

This is another essential part of I/O and since C is relatively low-level, it allows you to read and write files to disk in a simple manner. The header that offers this simple functionality is stdio.h, and the function you will be using is fopen(). It takes the filename as the argument, as well as the mode it should be read (read/write (r,w). append (a) or binary(b), as opposed to text – but the latter’s implementation is system-dependent). fopen() returns a FILE pointer, which is a type. Before anything you will need a file pointer, as illustrated:

FILE *fp; /*file pointer */
fp = fopen("/home/user/testfile.txt", "w");
fprintf(fp, "My test file.")

Simple: I opened a file on my disk and wrote to it the string “My test file”. You might have guessed, I have some exercises. Would it make a difference if the file exists or not? What if it existed, but was empty? Should I have used append instead of write mode? Why?

After using the file, one must close it. This is important, because by closing your program tells the operating system “Hey, I’m done with this file. Close all dirty buffers and write my file to disk in a civilized manner, so no data loss occurs”.

fclose(fp);

Here’s a real life example of using file I/O from Kimball Hawkins’ yest program, which helps us remember two things: one, that because of the Unix design (everything is a file), stdin, stdout and stderr are files, so they can be used with file I/O functions, and two, that the next part treats stderr and exit.

void store_time()
{
    if ( time_ok == FALSE ) return;	/* No time information, skip it */

    /* Hour */
    if ( tfield[0] > 24 ) {
	fprintf(stderr, "ERROR: Bad input hour: '%d'\n", tfield[0]);
	exit(1);
    }
    theTime->tm_hour = tfield[0];

    /* Minute */
    if ( tfield[1] > 0 ) {
	if ( tfield[1] > 60 ) {
	    fprintf(stderr, "ERROR: Bad input minute: '%d'\n", tfield[1]);
	    exit(1);
	}
	theTime->tm_min = tfield[1];
    }
}

Treating errors with stderr and exit

Your program must have some way to deal with errors and let the OS and the user know something went wrong. While this part is in no way a dissertation on how to treat your possible situations in C, it deals with a very useful and well-thought element of Unix: output errors to another place, different than stdin, so that the user can separate the two when debugging the issue. Also, use exit codes so that the user knows when the program finished successfully and when it didn’t. This is why stderr exists, for the first part, and this is why exit() also exists, for the second part. The astute reader already got the idea from the code sample above, so all it takes is tell the system not to output text on the default/standard output, but to the special “channel” that exists especially for this. Regarding exit(), it works like this: zero for success, any other value between 1 and 255 in case of failure. It’s included in stdlib.h and does not return a value. It is up to you, as you can see in Kimball’s code above, to tell exit if there is a problem, so it can inform the parent function about the exit status.

Useful headers

Needless to say, knowing the standard C library is mandatory if you want to get serious with C development on Linux. So here are a few other headers that offer facilities related to I/O and more:

string.h

This header will prove very helpful when working with string conversions (strto*()), comparing strings (strcmp()) or checking a string’s length (strlen()).

ctype.h

Besides case conversion, ctype.h offers functions that check various properties of characters. Some of them are isalnum(), isupper(), isalpha() or isspace(), and you are invited to guess what they do and how they work.

math.h

Many functions needed for more than the four basic arithmetic operations are to be found here, including sin(), cos() or exp().

Further reading

The more experienced readers will nail me to the cross for not treating more advanced subjects like malloc() or size_t. As I repeatedly said, this series in not intended as a know-all online book for C development (there is no such thing, anyway), but rather a good starting point for beginners. I feel that the future C developer must be relatively well versed in pointers and how memory allocation works before he/she starts having malloc() nightmares. After the end of this series, you are recommended to get a in-depth book on C, after asking some opinions from the Old Ones (not H.P. Lovecraft’s Old Ones, I hope), so you avoid false or misleading information. While you’ll know about free() and malloc() until we finish, it’s probably best to get a printed book and sleep with it under your pillow.

Conclusion

The article that will follow this one shall be a little longer, as we will delve further into the Unix way of C programming, but a good understanding of what was said here is recommended for the next steps to be as smooth as possible.



Comments and Discussions
Linux Forum