Computer Systems Organization


Start Lecture #14

Remark: Midterm will be tues after vacation. Practice midterm will be available next week.

A Relative of printf(): sprintf()

The function

    int sprintf(char *string, char *fmt, ...);
  
is very similar to printf(). The only difference is that, instead of sending the output to stout (normally the screen), sprintf() assigns it to the first argument specified.
    char outString[50];
    int d = 14;
    sprintf(outString, "The value of d is %d\n", d);
  
For example, the code snippet on the right results in the first 23 characters (assuming I counted correctly) of outString containing The value of d is 14 \n\0 while the remaining 27 characters of outString continue to be uninitialized.

Since the system cannot in general check that the first argument is big enough, care is needed by the programmer, for example checking that the returned value is no bigger than the size of the first argument. Even better use snprintf(), which like strncpy(), guarantees than no more than n bytes will be assigned (n is an additional parameter to strncpy).

7.3: Variable-length Argument Lists

As we mentioned, printf() takes a variable number of arguments. But remember printf() is not special, it is just a library function, not an object define by the language or known to the compiler. That is you can write a C program with declaration

    int myfunction(int x, float y, char *z, ...)
  
and it will have three named arguments and zero or more unnamed arguments.

There is some magic needed to get the unnamed arguments. The magic is needed by the author of the function; not by a user of the function.

7.4: Formatted Input—scanf

This function is to printf() as putchar() is to getchar(). As with printf(), scanf() accepts one required argument (a format string) and a variable number of additional arguments. Since this is an input function, the additional arguments give the variables into which input data is to be placed.

Consider the code fragment shown on the top frame to the right and assume that the user enters on the console the lines shown on the bottom frame.

int n;
double x;
char str[50];
scanf("%d  %lf  %s %20s", &n, &x, str);

22 37.5 no-blanks-here

A Relative of scanf(): sscanf()

The function

    int sscanf(char *string, char *fmt, ...);
  
is very similar to scanf(). The only difference is that, instead of getting the input from stdin (normally the keyboard), sscanf() gets it from the first argument specified.

7.5: File Access

So far all our input has been from stdin and all our output has been to stdout (or from/to a string for scanf()/sprintf).

What if we want to read and write a file?
As I mentioned in class you can use the redirection operators of the command interpreter (the shell), namely < and >, to have stdin and/or stdout refer to a file.

But what if you want to input 2 or more files?

Opening and Closing Files; File Pointers

Before we can specify files in our C programs, we need to learn a (very) little about the file pointer.

Before a file can be read or written, it must be opened. The library function fopen() is given two arguments, the name of the file and the mode; it returns a file pointer.

Consider the code snippet on the right. The type FILE is defined in <stdio.h>. We need not worry about how it is defined.

  FILE *fp1, *fp2, *fp3, *fp4;
  FILE *fopen(char *name, char *mode);
  fp1 = fopen("cat.c", "r");
  fp2 = fopen("../x", "a");
  fp3 = fopen("/tmp/z", "w");
  fp4 = fopen("/tmp/q", "r+");
  1. The file cat.c in the current directory is opened for reading and some information about this file is recorded in *fp1.
  2. The file x in the parent directory is opened for appending; x is created if it doesn't exist
  3. The file z in /tmp is opened for writing. Previous contents of z are lost.
  4. The file q in /tmp is opened for reading and/or writing. (When mixing reads and writes, care is needed.)

After the file is opened, the file name is no longer used; subsequent commands (reading, writing, closing) use the file pointer.

The function fclose(FILE *fp) breaks the connection established by fopen().

getc()/putc(): The File Versions of getchar()/putchar()

Just as getchar()/putchar() are the basic one-character-at-a-time functions for reading and writing stdin/stdout, getc()/putc() perform the analogous operations for files (really for file pointers). These new functions naturally require an extra argument, the file pointer to read from or write to.

Since stdin/stdout are actually file pointers (they are constants not variables) we have the definitions

    #define getchar()    getc(stdin)
    #define putchar(c)   putc((c), stdout)
  

I think this will be clearer when we do an example, which is our next task.

An Example cat.c

#include <stdio.h>
main (int argc, char *argv[argc]) {
  FILE *fp;
  void filecopy(FILE *, FILE *);
  if (argc == 1) // NO files specified
    filecopy(stdin, stdout);
  else
    while(--argc > 0)  // argc-1 files
      if((fp=fopen(*++argv, "r")) == NULL) {
	printf ("cat: can't open %s\n", *argv);
	return 1;
      } else {
	filecopy(fp, stdout);
	fclose(fp);
      }
  return 0;
}
void filecopy (FILE *ifp, FILE *ofp) {
  int c;
  while ((c = getc(ifp)) != EOF)
    putc(c, ofp);
}

The name cat is short for catenate, which is short for concatenate :-).

If cat is given no command line arguments (i.e., if argc=1), then it just copies stdin to stdout. This is not useless: for one thing remember < and >.

If there are command line arguments, they must all be the names of existing files. In this case, cat concatenates the files and writes the result to stdout. The method used is simply to copy each file to stdout one after the other.

The copyfile() function uses the standard getc()/putc() loop to copy the file specified by its first argument ifp (input file pointer) to the file specified by its second argument. In this application, the second argument is always stdout so copyfile() could have been simplified to take only one argument and to use putchar().

Note the check that the call to fopen() succeeded; a very good idea.

Note also that cat uses very little memory, even if concatenating 100GB files. It would be an unimaginably awful design for cat to read all the files into some ENORMOUS character array and then write the result out to stdout.

7.6: Error Handling—Stderr and Exit (and Ferror())

Stderr

A problem with cat is that error messages are written to the same place as the normal output. If stdout is the screen, the situation would not be too bad since the error message would occur at the end. But if stdout were redirected to a file via >, we might not notice the message.

Since this situation is common there are actually three standard file pointers defined: In addition to stdin and stdout, the system defines stderr. Although the name suggests that it is for errors and that is indeed its primary application, stderr is really just another file pointer, which (like stdout) defaults to the screen).

Even if stdout is redirected by the standard > redirection operator, stderr will still appear on the screen.

There is syntax to redirect stderr, which can be used if desired.

Exit()

As mentioned previously a command should return zero if successful and non-zero if not. This is quite easy to do if the error is detected in the main() routine itself.

What should we do if main() has called joe(), which has called f(), which has called g(), and g() detects an error (say fopen() returned NULL)?

It is easy to print an error message (sent to stderr, now that we know about file pointers). But it is a pain to communicate this failure all the way back to main() so that main() can return a non-zero status.

Exit() to the rescue. If the library routine exit(n); is called, the effect is the same as if the main() function executed return n. So executing exit(0) terminates the command normally and executing exit(n) with n>0 terminates the command and gives a status value indicating an error.

Ferror()

The library function

    int ferror(FILE *fp);
  
returns non-zero if an error occurred on the stream fp; For example, if you opened a file for writing and sometime during execution the file system became full and a write was unsuccessful, the corresponding call to ferror() would return non-zero.

7.7: Line Input and Output

The standard library routine

    char *fgets(char *line, int maxchars, FILE *fp)
  
reads characters from the file fp and stores them plus a trailing '\0' in the string line. Reading stops when a newline is encountered (it is read and stored) or when maxchars-1 characters have been read (hence, counting the trailing '\0', at most maxchars will be stored).

The value returned by fgets is line; on end of file or error, NULL is returned instead.

The standard library routine

    int fputs(char *line, FILE *fp)
  
writes the string line to the file fp. The trailing '\0' is not written and line need not contain a newline. The return value is zero unless an error occurs in which case EOF is returned.