Techno-Plaza
Site Navigation [ News | Our Software | Calculators | Programming | Assembly | Downloads | Links | Cool Graphs | Feedback ]
 Main
   Site News

   Our Software

   Legal Information

   Credits

 Calculators
   Information

   C Programming

     Introduction to C

     Keyboard Input

     Graphics Intro

     Slider Puzzle 1

     Functions

     Pointers

     Dynamic Memory

     Slider Puzzle 2

     Structures

     Bit Manipulation

     Advanced Pointers

     File I/O

       Part I

       Part II

       Part III

       Part IV

     Graduate Review

   Assembly

   Downloads

 Miscellaneous
   Links

   Cool Graphs

   Feedback Form

C Programming Lessons

TIGCC Programming Lessons

Lesson 10: File I/O

Step 2b - Program Analysis

Now comes the fun part. I know this looks like a complicated program, but 90% of the code you've already seen in one form or another. The file I/O part is a very small subset of this program.

This is the first of many programs which I will consider 'advanced'. I will no longer write extremely short pointless examples to illustrate a single concept while shying away from any actual writing. Though this program is largely pointless, it could be viewed as the beginning of an address book program, which we will write by the end of this lesson.

Since the program is so much larger than most examples, we should start off with a description of what the program does. Most parts of the code I will not be covering because you've seen most of it before. I will focus on the parts we're trying to learn here. File I/O, the menus used in the dialog box, and the preprocessor directives used to distinguish between TI-89 and TI-92+/V200 versions (due to screen size differences).

This program takes a structure called PERSON and allows us to modify that structure, and save/load that data to and from a file. So, we can store personal information about a person. We can save this data to a file. And we can later load this data from that file and display our person record again. The data is all presented in dialog boxes to provide an easy-to-use interface.

Okay, now that we know what the program's designed to do, let's see how it accomplishes it.

#include <dialogs.h>
#include <ctype.h>
#include <string.h>
#include <mem.h>
#include <stdio.h>
#include <kbd.h>
#include <stdlib.h>
#include <alloc.h>
#include <args.h>
#include <menus.h>

First a word about the include files. Instead of including tigcclib.h like we normally do, we will start including only the header files that we need to accomplish our tasks. It makes compiling faster (the compiler doesn't have to work with unnecessary files) and is more standard in actual C programming. Normally, you will not have one header file which has the information for all the other header files inside it. I will mention a certain header file that is needed whenever we encounter a function that's not already part of our includes. You can find the functions that these header files contain in the TIGCC docs.

// comment this next line out for TI-92+/V200 support
#define USE_TI89

#ifndef USE_TI89
    #define USE_TI92PLUS
    #define USE_V200
#endif

This is another thing we have not used before, but which becomes increasingly necessary in more complex C programs; the preprocessor directives. Remember from way back that the preprocessor gets called to process the file before the compiler works on the code. It replaces certain items in our code with other things according to our directions. For example, wherever we have a #include directive, the preprocessor will replace that line with the contents of that file. We have also seen the #define directive before, used to define global values for our program. We have also seen #define directives in header files to make sure no header is included twice. Here we are using these defines to determine which calculator our program will be built for.

The reason we have done this is because the TI-92+ uses a different font for dialog boxes (the large font) and takes up more space than the TI-89. We could have used the same technique we used in lesson 8, but this is just another technique. The difference between the two is code size. More code is used in the lesson 8 method, but the program will run on either the TI-89 or the TI-92+/V200. Using the method we use here, the TI-89 program will only run correctly on the TI-89, and we have to compile two binaries instead of one.

#ifdef USE_TI89
    enum DialogConstants    {DLG_WIDTH = 140, DLG_HEIGHT = 85};
#else
    enum DialogConstants    {DLG_WIDTH = 200, DLG_HEIGHT = 90};
#endif

Here is the important part of our #define directives, the difference in our enumeration constants. So, if we defined USE_TI89, the program will keep the dialog constants for the TI-89 size, and if the TI-92+/V200 is what we want, then it keeps those constants.

typedef struct {
	char name[15];
	short int height;
	short int weight;
	char dob[11];
} PERSON;

This is the PERSON structure used for saving our record. It saves a name, height (in centimeters), weight (in kilograms), and a date of birth. I used the metric system since it seemed more universal, but it doesn't really matter. But we have validation functions that make sure the data is within 'reasonable' limits. We will see the validation functions later.

void _main(void) {
    char buffer[34];
    PERSON p;
    short int done = 0, option = MENU_DATA;
    HANDLE dlg = H_NULL, menu = H_NULL;

    // initialize the person structure
    initPerson(&p);

    // format the request buffer string
    formatRequestString(buffer,&p);

    if ((dlg = DialogNewSimple(DLG_WIDTH,DLG_HEIGHT)) == H_NULL) {
        dlgError(MEMERR1,MEMERR2);
        return;
    }

    if ((menu = PopupNew(NULL,0)) == H_NULL) {
        dlgError(MEMERR1,MEMERR2);
        HeapFree(dlg);
        return;
    }

    // create Dialog Menu
    PopupAddText(menu,-1,"Enter new Data",MENU_DATA);
    PopupAddText(menu,-1,"Read Data from File",MENU_READ);
    PopupAddText(menu,-1,"Save Data to File",MENU_WRITE);
    PopupAddText(menu,-1,"Print Information",MENU_PRINT);
    PopupAddText(menu,-1,"Exit",MENU_EXIT);

    DialogAddTitle(dlg,"Main Menu",BT_OK,BT_CANCEL);
    DialogAddPulldown(dlg,5,15,"Selection:",menu,0);

    do {
        if (DialogDo(dlg,CENTER,CENTER,NULL,&option) != KEY_ENTER) {
            option = MENU_EXIT;
        }

        switch (option) {
            case MENU_DATA:
                getData(&p,buffer);
                printData(&p);
                break;
            case MENU_READ:
                if (readData(&p)) {
                    dlgError(FILEIO4,FILEIO5);
                    formatRequestString(buffer,&p);
                    printData(&p);
                }
                break;
            case MENU_WRITE:
                if (writeData(&p)) {
                    dlgError(FILEIO4,FILEIO5);
                }
                break;
            case MENU_PRINT: printData(&p); break;
            case MENU_EXIT: done = TRUE; break;
        }
    } while (!done);

    // free the memory used by the dialog box and selector menu
    HeapFree(menu);
    HeapFree(dlg);
}

The main method is short in this program, as it should be in most programs. The _main() function is simply for starting the program and calling helper functions to do the work. We should rarely do any work in the main function. The first task is to initialize the person structure.

void initPerson(PERSON *p) {
    // terminate the person strings
    p->name[0] = 0;
    p->dob[0] = 0;

    // enter generic person information
    strcpy(p->name,"Dominic Silver");
    strcpy(p->dob,"06-02-1972");
    p->height = 190;
    p->weight = 87;
}

The person initializer function works very simply. We start by terminating the strings. This is necessary to use the strcpy function. The strcpy (STRing CoPY) function copies a string onto another string. You can look up more about it in the TIGCC docs if the usage is not clear here. strcpy is defined in the string.h header.

Our next task is the format the string buffer. Setting this buffer to keep our PERSON values stored inside it allows us to set default values to the dialog box. I'll explain how that works in the function.

void formatRequestString(char *temp, PERSON *p) {
    // erase the buffer string
    memset(temp,0,34*sizeof(char));

    // format the buffer string so the dialog box will have default values
    sprintf(temp,"%-15s%-11s%03hd %03hd",p->name,p->dob,p->height,p->weight);

    // add string separators
    temp[14] = 0;
    temp[25] = 0;
    temp[29] = 0;
    temp[33] = 0;
}

The first step is to erase the string. This is accomplished by using memset to set all our bytes to zero. I don't think we have used memset before, but it's quite simple. It takes an array, a value, and a length. Then sets that many bytes of the array to the value. It's a quick way to fill an array. Next we use sprintf to set the values inside our request string. memset is defined in mem.h, and sprintf is defined in stdio.h.

This string uses lots of format specifiers, so we need to be clear about how they work. %-15s means to add a string (s) here that is a least 15 characters long, and if the string is not at least 15 characters, add spaces to it until it is. The default is to add the spaces to the left, the - tells us to add those spaces to the right instead. I think you can guess how %-11s works. Then we have the %03hd values. We know it's a number because hd is a short integer, and 3 tells us it must be 3 digits long. However, we don't want to add spaces to the left, we want to add zeros, so we can use 03 to tell it to prefix zeros.

Finally, we must terminate the strings so the dialog box will know where to stop printing. Our first string has 14 characters, so the terminator must go in the 15th space, which is index 14 (counting starts at 0 remember). The next string is 10 characters, so 14 + 10 + 1 for the terminator is index 25. The next two strings are 3 characters each + 1 for the terminator, so 25 + 3 + 1 = 29, and 29 + 3 + 1 = 33. This can be tricky. In fact, when I first published this lesson, I did it wrong and the program stopped working in later TIGCC versions.

if ((dlg = DialogNewSimple(DLG_WIDTH,DLG_HEIGHT)) == H_NULL) {
    dlgError(MEMERR1,MEMERR2);
    return;
}

if ((menu = PopupNew(NULL,0)) == H_NULL) {
    dlgError(MEMERR1,MEMERR2);
    HeapFree(dlg);
    return;
}

Our next step is to create the dialog box. That is pretty standard, but we have a new function which creates a pulldown menu. It looks similar to creating a dialog, but with a new function. PopupNew creates a new popup menu with a title and a height. Since we are using it as a pulldown menu and not a popup menu, use use NULL for the name and a height of 0 (for auto-calculate the height). If we have any errors, free the dialog box memory used and exit the function. It's good to note that dialogs and menus use HANDLE's for their type. This is because they are both using memory to create their structures. Most operating systems work via a system of handles where handles are used to manage blocks of memory. Files are actually done in this manner too, but we will hide that fact with an encompassing structure. The Dialog* functions are defined in dialogs.h, while the Popup* functions are defined in menus.h.

// create Dialog Menu
PopupAddText(menu,-1,"Enter new Data",MENU_DATA);
PopupAddText(menu,-1,"Read Data from File",MENU_READ);
PopupAddText(menu,-1,"Save Data to File",MENU_WRITE);
PopupAddText(menu,-1,"Print Information",MENU_PRINT);
PopupAddText(menu,-1,"Exit",MENU_EXIT);

DialogAddTitle(dlg,"Main Menu",BT_OK,BT_CANCEL);
DialogAddPulldown(dlg,5,15,"Selection:",menu,0);

We add menu options to our pulldown menu by using the PopupAddText command. PopupAddText takes a popup menu handle, a link value (which for our purposes is always -1, because we're not using linked popup structures), a text string for the menu choice, and a value. You can see that we have a MenuConstants enumeration defined at the top that has these values.

So, when the user chooses an option from the menu, we will get one of these values back as a result.

The DialogAddTitle() should be simple enough, we've seen it before. The DialogAddPulldown is how we add our pulldown menu to the dialog box. We specify a dialog box handle, the dialog window coordinates (5,15), a text label, the popup menu handle, and the position in the array to store our answer. Remember that in dialog requests, we used a large text buffer where all the answers where stored. Following from this, if we have multiple menus, we must have multiple places to store the answer. Since all the options on the menu return integer values, we use an array of integers. However, since we only have one menu, we can use a single integer.

if (DialogDo(dlg,CENTER,CENTER,NULL,&option) != KEY_ENTER) {
    option = MENU_EXIT;
}

We have seen the DialogDo() function before, but we have new options this time. It's much the same except we use NULL for the RequestBuffer (since we have no text requests), and give the address of the option variable to store our menu options in. Also remember from our call to DialogAddTitle() that we added OK and CANCEL buttons, so if the user doesn't press ENTER, he must have pressed ESC to exit the dialog, meaning cancel. So, we then set the option to MENU_EXIT so the program will quit.

switch (option) {
    case MENU_DATA:
        getData(&p,buffer);
        printData(&p);
        break;
    case MENU_READ:
        if (readData(&p)) {
            dlgError(FILEIO4,FILEIO5);
            formatRequestString(buffer,&p);
            printData(&p);
        }
        break;
    case MENU_WRITE:
        if (writeData(&p)) {
            dlgError(FILEIO4,FILEIO5);
        }
        break;
    case MENU_PRINT: printData(&p); break;
    case MENU_EXIT: done = TRUE; break;
}

Here is the part where we evaluate the selection of the user. Remember that our option variable will contain the value returned from the dialog box. If they select Enter New Data (MENU_DATA), we call the getData() and printData() functions.

short int getData(PERSON *p, char *buffer) {
    HANDLE dlg = H_NULL;
    int done = FALSE;
    char *token;

    // create the dialog box
    if ((dlg = DialogNewSimple(DLG_WIDTH,DLG_HEIGHT)) != H_NULL) {
        // format the dialog box
        DialogAddTitle(dlg,"Personal Information",BT_OK,BT_NONE);
        DialogAddRequest(dlg,5,15,"Name:",0,14,18);
        DialogAddRequest(dlg,5,25,"DOB:",15,10,18);
        DialogAddRequest(dlg,5,35,"Height (cm):",26,3,5);
        DialogAddRequest(dlg,5,45,"Weight (kg):",30,3,5);

        while (!done) {
            // loop until the user presses ENTER
            while (DialogDo(dlg,CENTER,CENTER,buffer,NULL) != KEY_ENTER);

            // grab the name from the string buffer
            token = buffer;
            p->name[0] = 0;
            strcpy(p->name,token);

            // grab the DOB from the string buffer
            token = buffer + 15;
            p->dob[0] = 0;
            strcpy(p->dob,token);

            // grab the height from the string buffer
            token += 11;
            p->height = atoi(token);

            // grab the weight from the string buffer
            token += 4;
            p->weight = atoi(token);

            // we're done unless we fail one of our validity tests
            done = TRUE;

            // check for valid DOB entry (MM/DD/YYYY)
            if (!isValidDOB((const unsigned char *)p->dob)) {
                dlgError(DATA_ERROR,DOB_ERROR);
                done = FALSE;
            }

            // check for reasonable valid height
            if (!isValidHeight((const short int)p->height)) {
                dlgError(DATA_ERROR,HEIGHT_ERROR);
                done = FALSE;
            }

            // check for reasonable valid weight
            if (!isValidWeight((const short int)p->weight)) {
                dlgError(DATA_ERROR,WEIGHT_ERROR);
                done = FALSE;
            }
        }

        // free the memory used by the dialog
        HeapFree(dlg);
    } else {
        dlgError(MEMERR1,MEMERR2);
        return FALSE;
    }

    return TRUE;
}

This function looks complicated, but it's not. Most of the function is simply constructing and handling the dialog box. We ask the user to input their name, height, weight, and date of birth. However, more important in this function is our data verification functions. We force the data to be within certain ranges, or have certain values or formats. For example, we force the DOB (date of birth) to be MM-DD-YYYY. If the DOB is not in this format, we consider it to be invalid. In this way, we can make sure we have valid data (at least syntactically). The HeapFree function is defined in alloc.h.

// grab the name from the string buffer
token = buffer;
p->name[0] = 0;
strcpy(p->name,token);

I want to cover these segments real quick just to make sure we're up to speed on pointers. The variable token is a character pointer. We assign its value to be buffer, so now token and buffer are pointing at the same string. This is helpful in dividing our large buffer into our smaller pieces. Later, we can use this same technique, but we'll attach names to our variable pointers. That will make it seem more intuitive, although the process is the same.

// check for valid DOB entry (MM/DD/YYYY)
if (!isValidDOB((const unsigned char *)p->dob)) {
    dlgError(DATA_ERROR,DOB_ERROR);
    done = FALSE;
}

// check for reasonable valid height
if (!isValidHeight((const short int)p->height)) {
    dlgError(DATA_ERROR,HEIGHT_ERROR);
    done = FALSE;
}

// check for reasonable valid weight
if (!isValidWeight((const short int)p->weight)) {
    dlgError(DATA_ERROR,WEIGHT_ERROR);
    done = FALSE;
}

Here is where we make our validity checks. If any one of our checks fail, display an error message and set done to false. Remember that since we are in a while (!done) loop, we will keep displaying the dialog until we are satisfied. This should probably be adapted in a real program so we can cancel.

// check for valid date of birth (MM-DD-YYYY)
short int isValidDOB(const unsigned char *dob) {
    if (isdigit(dob[0]) && isdigit(dob[1]) && (dob[2] == '-' || dob[2] == DASH) &&
        isdigit(dob[3]) && isdigit(dob[4]) && (dob[5] == '-' || dob[5] == DASH) &&
        isdigit(dob[6]) && isdigit(dob[7]) && isdigit(dob[8]) && isdigit(dob[9])) {

        return TRUE;
    }

    return FALSE;
}

This is another example of a function that looks complex, but is really not. We simply verify that each character in our DOB string is either a number or a dash. The first two must be digits, as are the 4th and 5th, and the 7th through 10th. Characters 3 and 5 must be a dash. This is a relatively simplistic validity function for DOB, which only checks to make sure the data looks right. A better function would check the values to make sure they are reasonable. For example, I could enter my DOB at 13-32-1000, and it would return valid, but that's obviously not a valid DOB.

// check for valid height (in centimeters)
short int isValidHeight(const short int height) {
    if (height > 100 && height < 250) {
        return TRUE;
    }

    return FALSE;
}

// check for valid weight (in kilograms)
short int isValidWeight(const short int weight) {
    if (weight > 35 && weight < 180) {
        return TRUE;
    }

    return FALSE;
}

The next two validity checkers simply make checks on reasonable values. Height between 100 and 250 centimeters and weight between 35 and 180 kilograms. If you're not familiar with the metric system, 1 inch is 2.2 centimeters, and 2.2 kilograms is 1 pound. So, from 4 ft to 9 ft, and 75 to 400 lbs.

Now that we've entered our data, display it back so we can make sure what we entered is what we want.

void printData(PERSON *p) {
    char name[25],dob[20],height[20],weight[20];
    HANDLE dlg = H_NULL;

    if ((dlg = DialogNewSimple(DLG_WIDTH,DLG_HEIGHT)) != H_NULL) {
        // create the personal information strings
        sprintf(name,"Name: %s",p->name);
        sprintf(dob,"DOB: %s",p->dob);
        sprintf(height,"Height: %hd cm",p->height);
        sprintf(weight,"Weight: %hd kg",p->weight);

        // format the dialog box
        DialogAddTitle(dlg,"Personal Information",BT_OK,BT_NONE);
        DialogAddText(dlg,5,15,name);
        DialogAddText(dlg,5,25,dob);
        DialogAddText(dlg,5,35,height);
        DialogAddText(dlg,5,45,weight);

        // display the dialog box
        DialogDo(dlg,CENTER,CENTER,NULL,NULL);

        HeapFree(dlg);
    }
}

The printData() function is simple. We take our person structure, and create a dialog box with text strings representing our data. Then we display the dialog box. There is nothing here we haven't already seen many times before.

case MENU_READ:
    if (readData(&p)) {
        dlgError(FILEIO4,FILEIO5);
        formatRequestString(buffer,&p);
        printData(&p);
    }
    break;
case MENU_WRITE:
    if (writeData(&p)) {
        dlgError(FILEIO4,FILEIO5);
    }
    break;

The next two options in our switch statement control the reading and writing of our data. When we read the data back from the file, we format the request string and print the data we've read.

I'm just now noticing it looks like I completely glossed over any possible I/O problems. Looks like this program will fail silently if something bad happens. Let's hope it doesn't I guess.

short int readData(PERSON *p) {
    FILE *f = NULL;
    short int fileio = TRUE;

    // open file for reading in binary mode
    if ((f = fopen("TESTFILE","rb")) == NULL) {
        dlgError(FILEIO1,FILEIO2);
        fileio = FALSE;
    } else {
        // read data from file into PERSON structure
        if (fread(p,sizeof(PERSON),1,f) != 1) {
            dlgError(FILEIO1,FILEIO3);
            fileio = FALSE;
        }

        // close the file
        fclose(f);
    }

    return fileio;
}

Here is our first taste of actual file I/O. Although the AMS has its own built-in routines for file handling, they are a bit more complex than the standard C library. Fortunately for us, the TIGCC team has implemented the C File I/O routines so we can use them here. These include fopen, fclose, fread, fwrite, fprintf, fputc, fgetc, fputs, and fgets. The f* functions are defined in stdio.h.

To work with files, we use a special structure called a FILE struct. Because many functions need to change the data associated with a file struct, the FILE struct is always a pointer. We can get a pointer to a file structure by calling the fopen() function. The fopen() function opens a file for reading or writing and returns a FILE pointer. The modes we are most concerned with are the following:

  1. rb (read binary)
  2. rt (read text)
  3. wb (write binary)
  4. wt (write text)

Text files are the files we can edit with the text editor application. So, by using text files, we can edit the files directly. However, for record keeping of data (mixing strings, numbers, floats, etc), it is best to use binary. This is because storing text records force us to convert back and forth from text to binary every time we want to use the data, and vice-versa to store the data back in the file. So, we will focus on binary mode in this example. However, this doesn't mean text mode is useless. If you only need to store strings, and need an easy way to edit the strings, a text file might be ideal.

If there is a problem opening the file, NULL will be returned. So we must check for this occurrence. We can't read from a NULL pointer, and your program will crash if you try, taking the system with it. You will be tempted not to perform these little error checks. It's a lot of work for things that happen rarely. If you don't check for errors, errors seem to happen. File errors cause calc crashes, so keep that in mind. Try choosing the read option from the program before you save the data the first time and see what happens. (fear not, it won't crash)

Now, our next step now that we have an open file is to read the data in from that file. The fread() function takes a pointer (where it will store the data), the size of a data record (the size of our structure), how many records we want to read (in our case 1, but we can read any number we want), and the FILE struct pointer. It returns the number of records it successfully read. So, if fread() doesn't return 1, we didn't get what we were looking for.

To see how the error checking we have works, create any variable (expression, text, program, whatever) and name it testfile. Then try to have the program read data from the file. See what happens. Unless that file just happens to be the exact size of a PERSON struct, you will get an error.

Finally, for every fopen() call we make, we must make sure to make a call to fclose(). This is because our FILE pointer was allocated dynamically by fopen(), and so is the space for our file. If we forget to close a file, we will have a memory leak. The system has no way of knowing if you don't close a file, so just remember to make sure you fclose() every file you fopen().

short int writeData(PERSON *p) {
    FILE *f = NULL;
    short int fileio = TRUE;

    // open file for writing
    if ((f = fopen("TESTFILE","wb")) == NULL) {
        dlgError(FILEIO1,FILEIO2);
        fileio = FALSE;
    } else {
        // write structure data to the file
        if (fwrite(p,sizeof(PERSON),1,f) != 1) {
            dlgError(FILEIO1,FILEIO3);
            fileio = FALSE;
        }

        // append the file ID tag
        fputc(0,f);
        fputs("FIO",f);
        fputc(0,f);
        fputc(OTH_TAG,f);

        // close the file
        fclose(f);
    }

    return fileio;
}

For every read, we also need a corresponding write. Writing is nearly identical to reading, we just change fread() to fwrite(). However, since we are writing custom data, we must give our file a file type. This is so the AMS never tries to interpret our data.

Remember that rb has a corresponding wb for write binary data. If we open a file for writing that doesn't exist, the fopen() call will create the file for us. If we open a file for writing that already exists, we will overwrite the file.

The calls to fopen() and fwrite() are nearly identical to the ones in the readData() function, with fread() replaced with fwrite(). So let's move on to the file tag.

The fputc() function writes a single character to a file. So, we want to write the number 0 to the file. It's a terminator, like in strings. Then we put the file type (1-4 characters). We use fputs() for that, which prints a string (without the null terminator) to a file. We print a final terminator, then put an OTH_TAG (from args.h) for the miscellaneous file tag type. This lets the system know we are dealing with user-data. The file type is "FIO". This is displayed in the var-link screen.

The rest of the program should be rather self-explanatory. But remember that popup menus must undergo HeapFree(handle) before you exit the program just like malloc() variables and dialog boxes.

Step 2c - Program Conclusions

This program seemed complex, but it should have been very easy to you by now. File I/O is a simple process, provided you do the error checking. Now that we know how to use simple file I/O, we can expand on the process with multiple records and multiple data types in a single file. It shouldn't be hard to do this with your knowledge, but we will provide a nice example program just to make sure. Make sure you understand how the file I/O parts work, and all the background things before you proceed, it will be critical to your understanding of the next section.

Continue with Part III

 

Copyright © 1998-2007 Techno-Plaza
All Rights Reserved Unless Otherwise Noted

Get Firefox!    Valid HTML 4.01!    Made with jEdit