C - From The Top (part 9 - The Filer II)

fprintf() and fscanf()

These two are analogous to the printf() and scanf() statements in that, instead of printing to the screen, the data is 'printed' to the disc.

For example, the following would print to disc a floating point value, integer and string. You will note, though, the extra parameter - this is the file pointer.

fprintf(filer,"%f %d %s",12345.123,1222,"Paul");

Reading back is simply a case of using an amended version of the scanf() statement. Again, it has the file pointer at the start.

fscanf(filer,"%lf%d%s",&ld,&d,string);

Binary files

Though fprintf() and fscanf() are nice, they are limited in their use - really, they're for text only. For binary files, C provides the commands fread() and fwrite(). Their protocols are a tad difficult to understand, but here goes.

size_t fread(void *buffer, size_t size, size_t num, FILE *file_pointer);

size_t fwrite (void *buffer, size_t size, size_t num, FILE *file_pointer);

fread works like this: it reads from the file associated with file_pointer, num number of objects with each object size bytes long into a buffer pointed to by buffer. It then returns the number of objects actually read, which may be less than the number requested if the end of the file has been reached or a read error has occurred. If the number returned by fread is less than we asked for, then the feof() and ferror() functions can be used to determine if it was a read error or an end of file.

fwrite works in the same way, except that it writes instead of reads. Again, it returns the number of objects written. If an error occurs, this will be less than the number sent.

The structure does pose a question: what is a pointer to a void doing in there? Simple, the void* is known here as a generic pointer - it means we don't have to set any particular cast (the upshot is that we never get a type mismatch error).

The final point is what the size_t does.

This is defined in stdio.h and is defined by ANSI C as being the largest object which the compiler will hold . In effect, a variable of type size_t holds positive numbers as large as we are ever going to need.

Obviously, when reading or writing a binary file, we have to use "wb", "rb" or "ab" as the attribute.

A final command, which is not really part of the filer as such, is sizeof(). This allows the correct output size of a variable or variable type e.g.

sizeof (int)
sizeof string_array

This is placed in the position occupied in the protocol at size_t size. The advantage of using the sizeof command over, say, 4 (for an int) is that of portability. PCs typically have ints of two bytes.

This example will output and input some data using fread and fwrite. Note the positioning of the sizeof command.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 FILE *filer;
 int i;
 if ((filer=fopen("<wimp$scrapdir>.file","wb"))==NULL)
 {
  printf("Can't open the file for saving\n");
  exit(1);
 }
 i=2001;
  if(fwrite(&i,sizeof(i),1,filer)!=1)
 {
  printf("Write error occurred\n");
  exit(1);
 }
 fclose(filer);
 if (filer=fopen("<wimp$scrapdir>.file","rb"))==NULL)
 {
  printf("Can't open the file for reading\n");
  exit(1);
 }
 if (fread(&i,sizeof(i),1,filer)!=1)
 {
  printf("Read error.\n");
  exit(1);
 }
 printf("i = %d",i);
 fclose(filer);
 return 0;
}

You can see that the error checking is performed as in integral part of the fread and fwrite process.

sizeof should always be used when writing any C application.

A bit of a recap here... Why is the &i used and not just i as the first fread() argument?

Remember that the first argument given in the fread() and fwrite() functions is a pointer to a void (a pointer to anything). This means that when using fwrite(), we don't give it a value of the data we want to store, just the address were it is stored. &i gives the address in memory where i is stored. This is very useful when reading and writing arrays of structure contents.

Random access

OriginMeaning
SEEK_SETseek from the start of the file
SEEK_CURseek from the current location
SEEK_ENDseek from the end of the file

Up until now, we have only dealt with sequential access; that is, we start at the beginning and keep going until we reach the end.

While with modern disc drives this is very fast, it would be nicer to be able to point at a place in a file and go straight to it. This method is known as random access.

Random access is performed with the commands fseek() and ftell(). The prototypes are

int fseek(FILE *filer, long offset, int origin);

long ftell(FILE *filer);

The pair are fairly easy to understand. fseek looks at the file pointed to by filer, at a position offset bytes from the origin.

Origin is defined by one of three macros.

For example, if you wished to search from 250 bytes in, you would use

fseek(filer,250,SEEK_SET);
fseek returns zero if the seek is successful or non-zero if not.
ftell is used to see where in a file you actually are.

File system functions

Files can be renamed using rename().

This has the prototype

int rename(char *oldname, char *newname);

It returns zero if the operation is OK, with non-zero if not.

You can wipe a file (delete it) using remove();

int remove(char *filename);

This returns zero if the operation is OK, with non-zero if not.

To reset the position you are currently at in a file back to the start, you use rewind();

void rewind(char *filer);

As this is a void prototype, there is no return value.

Finally, to flush the disc buffer, you use fflush();

int fflush(char *filer);

If successful, this returns zero, otherwise EOF is returned.

Standard streams

When a C program fires up, it generates three streams; stdin, stdout and stderr. stdin refers to the input device with stderr and stdout, the output device.

While it is usual for these to be from the keyboard and to the screen, these streams are file streams and so anything which uses the FILE * can also use these.

Right, we've been through a lot and don't worry if you've not quite caught the gist of these - there are a wad of examples on this month's cover disc for you to play with.

Until next time, add the following to the small database program you started a while back. It now needs a simple save and load system.

When saving, you should ask for the filename, and the same with loading. Have it check the file exists.

Next time, structures and unions.


Source: Archive Magazine 13.9
Publication: Archive Magazine
Contributor: Paul Johnson