C from the top
Part 8 - Saving and Loading
I left you last time with a problem in recursion using two functions interacting and asked why it worked how it worked. It's quite easy.
Each time f1() is called, it checks to see if a = 0. If not, it calls f2() with a-1. f2() prints a and checks to see if b = 0. If not, it calls f1() with b-1. This carries on until b = 0 and the functions unravel causing f1() to print 0 to 30 in twos.
Alright, the filer. There is one major concept to get over first, that is that file i/o is split into the stream and the file.
The stream provides the information to the programmer (it doesn't matter how this is done, e.g. the monitor displays what I am typing. This is a stream). The device for providing the i/o is the file (a stream is therefore a logical interface to the file). C defines the term file as any of the physical devices (tape, CD, VDU etc). As you can see, files differ greatly, but the stream stays the same - it's just a way of sending information.
There are two types of stream : text and binary. When a text file is written, there is occasionally a change in what is written to what is actually in memory (e.g. a return will be saved as a line feed character). The binary stream can be used with any kind of data - there are no translations and so what is stored on the file is what is sent by the stream.
Streams are linked to files using an open operation. A close operation ends this link.
One final point is the idea of current position. This is the location where the next file access occurs. For example, if you've saved a file 3K in length and a third of the file has been read in, the current position is at the 1K point.
Now that's over with, let's see how we actually do it.
The prototype to initiate the filer is
FILE *fopen (char *filename, char *mode);
fopen is the command to open the access to the output device (for us, it's the disc drive). filename is the filename - this can either be inputted or be of the format
"<wimp$scrapdir>.filename" (RISC OS users only - PC, Mac and Linux users would have something different)
mode is the type of access. The types are listed below.
Mode | Meaning |
"r" | open text file for reading |
"w" | create text file for writing |
"a" | append to a text file |
"rb" | open a binary file for reading |
"wb" | open a binary file for writing |
"ab" | append to a binary file |
"r+" | open a text file for read/write |
"w+" | create a text file for read / write |
"a+" | append or create a text file for read / write |
"rb+" | open a binary file for read / write. Can also use "r+b" |
"wb+" | create a binary file for read / write. Can also use "w+b" |
"ab+" | append or create a binary file for read / write. Can also use "a+b" |
If the file open is successful, fopen returns a valid file pointer. If it fails, a NULL is returned. It is very important that a valid pointer is returned and can be checked thus
FILE *myfile if ((myfile=fopen(filename,"r"))==NULL) { printf("Error opening file\n"); exit (1); }
You will notice that the if statement is quite complex. Unlike BASIC, C can actually perform assignment operations as part of a condition that is being tested. The condition being tested in this case, is whether something equals NULL (remember that testing for equality requires two equal signs). But what is the value on the left of the equality test? The statement is grouped using brackets, so what ever is inside the innermost brackets are evaluated first (in this case myfile=fopen(filename,"r"). The fopen operation is performed, it's value is placed in myfile and then that value is tested for equality with NULL.
It is this difference between = and == which causes more bugs in source files than enough. Remember, = is an assignment (e.g. n = 10) whereas == is an equality test (e.g. if (n == 10)).
Thankfully, the above segment can be rewritten so that it is broken down into smaller steps :
myfile = fopen(filename,"r");
if (myfile==NULL) ......
There is nothing wrong in using the long hand version just you are more likely to see the shorthand version in other people's source code.
There is a big difference to an append and a write. In either case, if the file is not there, it will be created. However, if the file is there and you open with an append, the data is written at the end of the last piece of data, the original is left intact. If you open with a write, you are in effect wiping the contents of the old file and writing the new contents. Keep this in mind!
To close a file, use the function
fclose (myfile);
Reading and writing are performed by a number of commands;
size_t fread(void *buffer, size_t size, size_t num,FILE *file);
size_t fwrite(void *buffer, size_t size, size_t num,FILE *file);
int fprintf(FILE *file,char *control_string, ...);
int fgetc(FILE *file);
int fputc(int ch, FILE *file);
int fscanf(FILE *file,char *control_string, ...);
with error checking provided by
int ferror(FILE *file);
int feof(FILE *file);
fgetc & fputc
fgetc() reads the next byte from the filename as an unsigned char and returns it as an integer (chars and ints are almost interchangeable, though such a conversion will result in all but the bottom 8 bits will disappear. This means that unless the value is between 0 and 255, all but the bottom 8 bits will go (i.e. you have a number > 255, only upto 255 will be stored. The numbers stored in the char then become 256 = 0, 257 = 1, 258 = 2 etc)). If an error occurs, EOF is returned (this usually equals -1)
fputc() writes a byte to filename as an unsigned char. The fputc() function returns the character if all is well, otherwise EOF is returned.
The following code demonstrates this
#include <stdio.h> #include <stdlib.h> int main(void)< { char string[80] = "StrongARM RPCs really kick"; FILE *filer; char *p; int i; if ((filer=fopen("<wimp$scrapdir>.Temp","w"))==NULL) { printf("Filer error\n"); exit (1); } p=string; while (*p) { if (fputc(*p,filer)==EOF) { printf("Write error"); exit (1);< } p++; } fclose(filer); if ((filer=fopen("<wimp$scrapdir>.Temp","r"))==NULL) { printf("Read open error"); exit(1); } for (; ;) { i=fgetc(filer); if (i==EOF) break; putchar (i); } fclose (filer); }
putchar simply places a single character onto the screen.
It is also not exactly the most elegant of programs. However, it is returning the correct values for EOF (remember, feof returns an int, so it makes sense to store the value as an int. While a char may be used and work fine under RISC OS, it is certainly not correct, nor is it portable.
Keeping the variable assigned to read feof as an int also has the advantage of being able to be used for binary files as well. The int also is helpful as if feof is expecting a 255 to terminate the file (as it would if the variable type was set to a char) and the reader comes across a 255, the file will stop being loaded - not much use if you have saved a 255 somewhere near to the start of your data!
This can be further improved upon by reversing the if statement.
Reversing an if statement is very common. It is far better practice to assume everything is okay and act if it isn't than the other way around. For instance take the following piece of BASIC
IF MID$(a$,a%,3) = "are" THEN PRINT "Equals" ELSE a%+=1 ...loop around again.... until a% > len(a$) that is!
This would be far better looking like this
IF MID$(a$,a%,3)<>"are" a%+=1
with the whole structure inside of a loop.
The same can be performed here. Rather than saying if fputc = EOF, take it as read, that it doesn't until it does.
while ((ch=fgetc(filer))!=EOF) putchar(ch);
(ch has been taken to be an int)
This checks the returned value to ch from the read and if it's not equal to EOF, print it to the screen. The loop will carry on until ch == EOF.
Using this method of seeing if something has gone wrong is a bit messy, so C provides two error trapping commands; feof() and ferror().
When working on binary files, all values read in are valid, so an EOF may be read, but is it really an EOF?
feof returns a non zero if it really the end of file, otherwise it returns zero. ferror returns a nonzero if there is a problem with the file, otherwise, it returns zero.
You would use feof() as follows
FILE *file; while (!feof(file)) ch=fgetc(file);
Remember, the !feof means that unless the value is zero, keep going.
While this is nice, it doesn't provide for any error checking. The following does
FILE *file; while(!feof(file)) { ch=fgetc(file); if (ferror(file)) { printf("File error\n"); break; } }
I think I'll call it quits here and carry on with the filer in the edition. What you can be getting on with is this.
The piece of code for input and output was not very well written on purpose. Rewrite this so that the code is as small as possible and add comments.
Comments can be added using either a double // or /* .... */ You should be able to trim off a good few lines.