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 3b - Programmatic Analysis

I'll bet you didn't think I could top that endless example from part I, eh? Well, that's okay, there's not a lot of new stuff. Dialog box creation is just a pain in the arse. But I think you know how that works by now, so let's stick to the relevant parts of the code...

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

#ifndef USE_TI89
    #define USE_TI92PLUS
    #define USE_V200
#endif

First off, we need to make sure you are compiling the correct version. Although they work the same on both calculators, the dialog boxes will be cut off on the TI-92+/V200 and vice-versa. If you are compiling the TI-89 version, you can leave it as is. I assume most people have a TI-89, so I leave that as the default. If however, you are using a TI-92+/V200, make sure to COMMENT OUT THAT TOP LINE!

We're going to skip the _main() method altogether since it should be second-hand to you now. We will start with the addEntry() function which is called when we choose to add a new entry from the main menu.

#define ENTRY_BUFFER_SIZE	131
// add a new address book record
void addEntry(void) {
	char buffer[ENTRY_BUFFER_SIZE];
	
	// erase the buffer string
	memset(buffer,0,ENTRY_BUFFER_SIZE);
	
	// now display the add new entry dialog box
	doEntryDialog(buffer,NEW_ENTRY);
}

Adding and editing entries are very similar, since the dialog box interface assumes that all variables are strings, and non-empty strings should be default values in input boxes, so, the only thing we need to do in an add/edit routine is setup a buffer to hold the data, then tell the write routine how to interpret the data, either as an edit or a database addition. The ENTRY_BUFFER_SIZE is the size our buffer string needs to be. Since we use this value in several places, it's a good idea to give it a name, and only change the value in one place. This helps minimize stupid coding errors.

Since the functions are much the same, we will look at the edit function too before examining how they do their work inside the doEntryDialog() function.

// edit the current entry
void editEntry(void) {
	char buffer[ENTRY_BUFFER_SIZE];
	char *name = buffer + 0, *address = buffer + 26, *city = buffer + 52, 
	char *state = buffer + 78, *zipcode = buffer + 81, *phone = buffer + 87;
	char *email = buffer + 100;

	// we can't edit if there are no entries
	if (recordCount == 0) {
		dlgError(RECORD_ERROR,NO_RECORDS_ERROR);
		return;
	}
	
	// erase the buffer string
	memset(buffer,0,ENTRY_BUFFER_SIZE);
	
	// copy the current information into the buffer string
	strcpy(name,p->name);
	strcpy(address,p->address);
	strcpy(city,p->city);
	strcpy(state,p->state);
	strcpy(zipcode,p->zipcode);
	strcpy(phone,p->phone);
	strcpy(email,p->email);
	
	// now display the edit dialog box
	doEntryDialog(buffer,EDIT_ENTRY);
}

The function looks more complicated, but it's not really. The only difference is that we need to fill the input buffer with the data from the record we want to edit. So, just copy the data we have in our global PERSON structure to the buffer the dialog box needs. We have setup a series of pointers to address different parts of the string buffer. So, instead of treating it as a single piece of string, we can treat it like all the elements of the PERSON structure. This is helpful so we only have to do the math once to find which parts of the string carry which pieces of data, then assign those pieces names.

In addybook.h, we defined a global PERSON structure pointer called p. The variable p contains the address book record we are currently working on (adding, editing, removing, displaying, etc.) So, if we want to copy the data we have to our string buffer, we copy it from the PERSON structure p. Then we call the doEntryDialog() function.

Now the doEntryDialog() function takes care of creating the dialog box and saving the data we've collected from the user to the address book file. Let's take a look at that now.

// display the dialog box to add/edit a record entry
void doEntryDialog(char *buffer, short int action) {
	HANDLE dlg = H_NULL;
	char *name = buffer + 0, *address = buffer + 26, *city = buffer + 52;
	char *state = buffer + 78, *zipcode = buffer + 81, *phone = buffer + 87;
	char *email = buffer + 100;
	const char *strings[] = {"Add New Record","Record Added Successfully",
				"Edit Record","Record Edited Successfully"};
	const char *title = NULL, *msg = NULL;
	
	// allocate memory for the dialog box
	if ((dlg = DialogNewSimple(DLG_DISPLAY_WIDTH,DLG_DISPLAY_HEIGHT)) == H_NULL) {
		dlgError(DMA_ERROR,MEM_ERROR);
		return;
	}
	
	// set the title strings based on the action we're taking (add or edit)
	if (action == NEW_ENTRY) {
		title = strings[0];
		msg = strings[1];
	} else {
		title = strings[2];
		msg = strings[3];
	}
	
	// create the dialog box
	DialogAddTitle(dlg,title,BT_NONE,BT_NONE);
	DialogAddRequest(dlg,5,15,"Name:",0,25,15);
	DialogAddRequest(dlg,5,25,"Address:",26,25,15);
	DialogAddRequest(dlg,5,35,"City:",52,25,15);
	DialogAddRequest(dlg,5,45,"State:",78,2,5);
	DialogAddRequest(dlg,5,55,"Zipcode:",81,5,10);
	DialogAddRequest(dlg,5,65,"Phone:",87,12,15);
	DialogAddRequest(dlg,5,75,"Email:",100,30,15);

	// display the dialog box
	if (DialogDo(dlg,CENTER,CENTER,buffer,NULL) == KEY_ENTER) {
		// erase PERSON structure
		memset(p,0,sizeof(PERSON));
		
		// increment the record count if we're adding a new one
		if (action == NEW_ENTRY) {
			recordCount++;
			record = recordCount;
		}
		
		// copy the entered data into our person structure
		p->id = record;
		strcpy(p->name,name);
		strcpy(p->address,address);
		strcpy(p->city,city);
		strcpy(p->state,state);
		strcpy(p->zipcode,zipcode);
		strcpy(p->phone,phone);
		strcpy(p->email,email);

		// save the new data
		if (writeRecords(action)) {
			DlgMessage((char *)title,(char *)msg,BT_OK,BT_NONE);
		}
	}
	
	// free the dialog memory
	HeapFree(dlg);
}

Dialog box creating may still seem a little new to some people, but once you get the pattern down, it's very easy. Refer back to lessons 6 and 7 if you're having trouble with dialogs.

Okay, assuming you are comfortable with the dialog box, once the box has the data, all we have to do is copy it to our PERSON structure p, then save the data. Also note that if we are adding a new record, we only add things at the end (for simplicity sakes -- no alphabetization, no sorting or searching -- plain and simple). So, when adding new entries, we must increase the count of records in our book. The address book information is stored in two global variables, record which indicates the current record we are editing, viewing, adding, removing, etc., and recordCount which is the total number of records in our book. When we add a new record, both of these must be incremented, since they both start at 0. Our first record will be #1, and when we have it, we will have 1 record(s). Hopefully that made sense. It's fairly well commented, so this shouldn't be too problematic, so long as you take a look at addybook.h and the init() function first (which we didn't do, but you should).

Now that we have a way of getting data for an address book entry, we need some way of storing those entries. Again, for simplicity (and for memory concerns, since we have so little of it), we only keep one record in memory at any time. The rest of the records (so long as we have enough space to store them on the file) are stored in a file outside the program memory. We read them back into memory (only one at a time) when we need them. So, let's take a look at the writeRecords() function, which comprises a major part of the program, and is the core of the file I/O section.

// write the address book records to file
// action tells us how to handle the current data (new record, remove old, or edit current)
short int writeRecords(short int action) {
	FILE *in = NULL, *out = NULL;
	short int tempRecordCount = recordCount, adjust = FALSE, copy = TRUE;
	PERSON temp;
	
	// rename the address file to a temp filename
	rename(ADDRESSBOOK,ADDRESSTEMP);
	
	// open the input file to copy the old records
	in = fopen(ADDRESSTEMP,"rb");
	
	// open the new address book file
	if ((out = fopen(ADDRESSBOOK,"wb")) == NULL) {
		fclose(in);
		return FALSE;
	}
	
	// decrement the record count if we're removing an entry
	if (action == REMOVE_ENTRY) {
		tempRecordCount--;
	}
	
	// write the new record count
	fwrite(&tempRecordCount,sizeof(short int),1,out);

	// if we need to copy over old data...
	if (in != NULL) {
		// find out how many records are already stored
		fread(&tempRecordCount,sizeof(short int),1,in);
		
		// loop through the old records and recopy them
		while (tempRecordCount-- > 0) {
			// read in the old record		
			memset(&temp,0,sizeof(PERSON));
			fread(&temp,sizeof(PERSON),1,in);
			
			// are we removing or editing this record?
			if (temp.id == p->id) {
				if (action == REMOVE_ENTRY) {
					// skip this entry -- adjust additional id's
					adjust = TRUE;
					copy = FALSE;
				} else if (action == EDIT_ENTRY) {
					// replace old entry data with new data
					memcpy(&temp,p,sizeof(PERSON));
				}
			}
			
			// decrement the id's of successive items (after the removed one)
			if (adjust) {
				temp.id--;
			}
			
			// if we're copying the record, write it
			if (copy) {
				// write the old record to the new book file
				fwrite(&temp,sizeof(PERSON),1,out);
			} else {
				// only one reason not to copy, so reenable copying after
				// we skip the one we're not copying
				copy = TRUE;
			}
		};
		
		// close the input file
		fclose(in);
	}
	
	// if we're adding an entry, write it to the file at the end
	if (action == NEW_ENTRY) {
		fwrite(p,sizeof(PERSON),1,out);
	}
	
	// now write the file tag
	fputc(0,out);
	fputs("ADDY",out);
	fputc(0,out);
	fputc(OTH_TAG,out);
	fclose(out);
	
	// remove the temporary records
	unlink(ADDRESSTEMP);
	
	// if we didn't have an error up to now, we're good to go
	return TRUE;
}

The core of the file I/O section looks very complicated, but it is not. It merely takes some time to look over to understand how it works together in three different capacities. This function will be called if we want to add a new record, remove an old record, or edit a record. Since we have a very limited memory set to use, it is better to keep only one entry in memory at any one time, and load others from 'disk' as needed.

Since we will need to remove entries as well as edit them, we will need to keep two copies of the address book. One will be for reading the old records in that we don't have in memory, and the other is for permanent storage. The first copy is just a temp. The filenames are stored in addybook.h with the names ADDRESSBOOK and ADDRESSTEMP for simplicity.

Since we will be reading from one file to copy into another file, we will need two file handles, *in and *out. Remember that file handles are always pointers because the memory is dynamically allocated by the fopen() function. This is also why we must never forget to close a file handle, not just because it's bad style, but because we'll lose memory every time the program is run.

// rename the address file to a temp filename
rename(ADDRESSBOOK,ADDRESSTEMP);

The first step is to rename our permanent book to the temp file. This is so we can read in the old entries from that file and copy them over. Since we only keep one entry in memory at a time, we will need to read the old entries in and copy them over. The other reason we do this is due to a 'feature' of the TIGCC library which does not allow in-file editing with custom tags. If we overwrite data inside a file, we will lose the file tag (even if we rewrite it). I have been told by Zeljko Juric that this is not a bug. Nonetheless, we also must deal with removing file entries, and it's hard to cut data out of a file. It's easier to simply recopy the relevant parts to a new file.

We should probably do error checking to make sure the temp file name is not taken, but since it's unlikely, we'll ignore it for now. Now (since we are assuming the rename was successful), we now have a file we can read the old entries from, and write them to the permanent address book file later, which we will be doing very soon.

// open the input file to copy the old records
in = fopen(ADDRESSTEMP,"rb");
	
// open the new address book file
if ((out = fopen(ADDRESSBOOK,"wb")) == NULL) {
	fclose(in);
	return FALSE;
}

Unless we are working with text files, we always work in binary mode. Since we will be reading from the temp file and writing to the address book file, we open the *in file for read-binary mode, and the *out file for write-binary. If for some reason we cannot open the *out file, then we have a big problem, and should return false.

// decrement the record count if we're removing an entry
if (action == REMOVE_ENTRY) {
	tempRecordCount--;
}
	
// write the new record count
fwrite(&tempRecordCount,sizeof(short int),1,out);

The next step is to determine how many entries the address book has. We keep track of that in the recordCount variable, but you will see that we can't change that right away when we remove a record, so we keep a temporary count stored inside the function. If we are removing the current entry, we obviously have one less than we had a minute ago.

The address book, as all binary files must, has a very rigid file structure. This is so we know how to interpret the data. All data is meaningless if we don't know how to interpret it. For the purposes of this program, I've chosen a simple structure which stores the number of records in the address book, followed by each entry in the book sorted by their id number (from 1 to whatever). This way, when we load the program and run the init() routine, we can find out how many records are in the book, and read in the first one without having to go through the whole file. For more complicated data storage, you might store an entire record structure with information about the file at the top. This is called a file header.

I hope you remember how the fwrite() function works, because it's very important in file I/O. Supply a pointer to the data, the size of the data, how many sets of the data exist, and the file pointer to write the data to. This is documented in the TIGCC docs if you forget.

// if we need to copy over old data...
if (in != NULL) {
	// find out how many records are already stored
	fread(&tempRecordCount,sizeof(short int),1,in);

Assuming our in-file is there, we probably have records to copy over (unless we have removed all the records from the book, in which case we still have a file, but it says there are 0 records inside it).

Now we need to find out how many old entries we have, so we will read the count from the temp file. Now we are ready to loop through the entries and copy them all over.

// loop through the old records and recopy them
while (tempRecordCount-- > 0) {

First, the while loop sets us up to loop until there are no more entries in the file. The semantics are very subtle, so they must be examined closely. First, we need to understand the difference between --var and var--. The operators do the same thing, but they have different precedence. First, if we look at the statement while (tempRecordCount-- > 0), we have 2 operators and 2 values. -- and > are the operators, and tempRecordCount and 0 are the values. Because we need to interpret the meaning, we need to know which ones get done at which time, and how. First, lookup the values of the variables. Let's say tempRecordCount is 3. So, we can rewrite the statement as (3-- > 0). Now, we still have to know which operator is handled first. If -- is handled first, then the statement should read (2 > 0), which is clearly true, but if > is handled first, then it becomes (3 > 0), and then afterwards, tempRecordCount becomes 2. Now, you may think this doesn't make any difference. Well, here's the subtlety. Let's say tempRecordCount is equal to 1. Now this would mean that there is 1 record in the file. Let's also assume -- is handled first. So, the statement would them read (0 > 0), which is not true, and the while body never would be executed. Now we've ruined it. We had a whole record we never evaluated. Fortunately, this is not the case. > has a higher precedence than var--. So, the statement would actually translate out to (1 > 0); tempRecordCount = tempRecordCount - 1; Now it evaluates to true, and we execute the body of the while loop. Now, here is the even more fun part. We did not use --var, but if we had, then the statement would read (0 > 0) and not execute the while loop. This is because --var has a higher precedence than >, and will thus be executed first.

Okay, now that we are finished with that complex description of operator precedence, let's move on to the body of the while loop.

// read in the old record		
memset(&temp,0,sizeof(PERSON));
fread(&temp,sizeof(PERSON),1,in);

Okay, the first step in the copy process is to erase our temporary PERSON structure and read in an entry from the file. Reading is just like writing, we just use fread() instead of fwrite(). The arguments are identical. Better code would check to make sure we didn't get any errors in reading data, but we'll leave it be for now.

// are we removing or editing this record?
if (temp.id == p->id) {
	if (action == REMOVE_ENTRY) {
		// skip this entry -- adjust additional id's
		adjust = TRUE;
		copy = FALSE;
	} else if (action == EDIT_ENTRY) {
		// replace old entry data with new data
		memcpy(&temp,p,sizeof(PERSON));
	}
}

This is where the three actions force us to alter what we do slightly. Remember that we are working with a sole entry record in memory, so the action is always referring to this entry. We are either adding it into the address book (a new record), removing it from the address book, or editing the record to update the information. If we are editing or removing, we need to check to see if the entry we are reading from the file matches the entry we are currently working with. If it does (i.e. the id's match on our current person structure p and our temp structure), then we need to set flags to either skip or replace this entry with the entry we have. We also need to set a mark here if we are removing things to change the id's of all successive entries. If we have entries 1, 2, 3, 4, and 5, and we remove 2, then 3, 4, and 5 should become 2, 3, and 4, just to keep it simple. It also helps us search for entries when we load them from the file, as you will see later. The memcpy() function copies the data in our current p PERSON to the temp person. We do this because we are going to write the temp person to the file, and there's no reason to make an exception here. It seems a little easier to copy the data here rather than write the data in this one special location, then tell it not to write the temp later for this special reason.

We haven't used memcpy before, but it is similar to memset. But rather than setting a number of bytes to a value, we copy a number of bytes to our variable. Consult the TIGCC docs for more information. It is defined in the mem.h header.

// decrement the id's of successive items (after the removed one)
if (adjust) {
	temp.id--;
}

This is just that part where we decrement the successive ids for entries that are past the removed entry. So, delete entry 2, and now 3 becomes 2, and 4 becomes 3, and so on.

// if we're copying the record, write it
if (copy) {
	// write the old record to the new book file
	fwrite(&temp,sizeof(PERSON),1,out);
} else {
	// only one reason not to copy, so reenable copying after
	// we skip the one we're not copying
	copy = TRUE;
}

Finally, we're ready to copy the old records. Remember that if we are removing a record, we don't want to copy it, so we check the copy variable to see if we need to copy. And since we only skip one record, we re-enable copying afterwards so it will copy successive entries past the removed one. So, skip 2, but not 3 or 4 or 5, etc.

The fwrite() function should be becoming second nature, but we'll go over it one more time. We pass the address of the data, the size of the data record, how many records are at that address (for arrays, it could be more than 1), and finally the file we are writing to, in this case *out. We already went over the case where removing an entry causes us not to copy it, but we only want to remove one, so we should re-enable copying after we are through.

// if we're adding an entry, write it to the file at the end
if (action == NEW_ENTRY) {
	fwrite(p,sizeof(PERSON),1,out);
}
	
// now write the file tag
fputc(0,out);
fputs("ADDY",out);
fputc(0,out);
fputc(OTH_TAG,out);
fclose(out);
	
// remove the temporary records
unlink(ADDRESSTEMP);

Okay, the last special case, adding a new entry causes us to add the entry at the end of the file. So, simply write the current entry last, and we're finished. Now we write the file tag, which in this case we will call 'ADDY', and close the *out file. Note that custom file tags can be 1-4 characters. Since we are done copying the old records, we no longer need the temp file, so we will remove it now. The unlink() function deletes a file. The name actually has meaning, but it's complicated, so just remember that unlink means to delete a file.

Now that we can add, remove, and edit entries, let's take a look at displaying entries.

// display the current address book entry
void displayEntry(void) {
	HANDLE dlg = H_NULL;
	char buffer[51];
	
	// we can't display if there is nothing to display
	if (recordCount < 1) {
		dlgError(RECORD_ERROR,NO_RECORDS_ERROR);
		return;
	}
	
	// allocate memory for the display dialog
	if ((dlg = DialogNewSimple(DLG_DISPLAY_WIDTH,DLG_DISPLAY_HEIGHT)) == H_NULL) {
		dlgError(DMA_ERROR,MEM_ERROR);
		return;
	}

	// setup the dialog box window	
	sprintf(buffer,"Address Book Record #%hd",p->id);
	DialogAddTitle(dlg,buffer,BT_OK,BT_NONE);
	
	sprintf(buffer,"Name: %s",p->name);
	DialogAddText(dlg,5,15,buffer);
	
	sprintf(buffer,"Address: %s",p->address);
	DialogAddText(dlg,5,25,buffer);
	
	sprintf(buffer,"City: %s",p->city);
	DialogAddText(dlg,5,35,buffer);
	
	sprintf(buffer,"State: %s Zip: %s",p->state,p->zipcode);
	DialogAddText(dlg,5,45,buffer);
	
	sprintf(buffer,"Phone: %s",p->phone);
	DialogAddText(dlg,5,55,buffer);
	
	sprintf(buffer,"Email: %s",p->email);
	DialogAddText(dlg,5,65,buffer);
	
	// display the dialog and free the memory when done
	DialogDo(dlg,CENTER,CENTER,NULL,NULL);	
	HeapFree(dlg);
}

Displaying an entry is very simple, but to make it look better, we will encapsulate displays in a dialog box. You should be very familiar with how dialog boxes work by now, so we won't spend too much time here. The important concepts to remember are that we already have a global PERSON structure p which contains the information we want to display. If we have no records yet, we don't display anything. There is never any time where PERSON p is empty unless we have no records. All we have to do is add text strings and display the box. Not even any complex input to work with.

Removing an entry is a little more complicated than I let on at first, so we next should examine the selectEntry() function which allows the user to select a different 'current' entry in the address book, so long as there are at least 2 records in the book.

// select a different entry from the book by id
short int selectEntry(void) {
	HANDLE dlg = H_NULL;
	char buffer[81];
	short int done = FALSE, entry;
	
	// we need more than 2 records to select a different record
	if (recordCount < 2) {
		dlgError(RECORD_ERROR,NEED_MORE_ERROR);
		return TRUE;
	}
	
	// open the dialog box
	if ((dlg = DialogNewSimple(DLG_MAIN_WIDTH,DLG_MAIN_HEIGHT)) == H_NULL) {
		return FALSE;
	}
	
	// add the dialog title
	DialogAddTitle(dlg,"Select Entry",BT_OK,BT_CANCEL);
	
	// add the dialog directions
	sprintf(buffer,"Choose an entry between 1 and %hu",recordCount);
	DialogAddText(dlg,5,15,buffer);
	
	// add the entry request
	DialogAddRequest(dlg,5,25,"Entry:",0,2,5);
	
	// erase the buffer variable
	memset(buffer,0,81);
	
	// ask for the entry id until we get one in a valid range
	while (!done) {
		if (DialogDo(dlg,CENTER,CENTER,buffer,NULL) == KEY_ENTER) {
			entry = atoi(buffer);
			--entry;
			
			if (entry >= 0 && entry < recordCount) {
				done = TRUE;
			}
		} else {
			// exit the loop without choosing an entry
			entry = -1;
			done = TRUE;
		}
	}
	
	// free the memory used by the dialog box
	HeapFree(dlg);
	
	// if we choose one, then load it from the file
	if (entry != -1) {
		return loadEntry(entry);
	}
	
	return TRUE;
}

The first step in selecting an entry is to create a selection dialog box. Maybe dialogs are overused, but they sure seem handy to me. So, we create the dialog box with a single entry for which address record the user wants to view. To keep it simple, we'll force the user to know which ID number the record is. A better address book would let them search by name or number or some such thing, but that would be a little much for this little example.

When the user selects an entry, we do two things. First, all dialog input comes in as a string, which you already knew, but we need it as a number. To convert a string to an integer, we use the atoi() function. Second, we are using numbers that start at 1. Most humans like to think that numbering starts at 1 and goes up, but computer science knows that numbers start at 0, so even though we tell the user numbers start at one, we need to treat the numbers as starting from 0. So, subtract the entry by 1 and we have a 0-based number. Now we can use math. This will help greatly in a moment.

Now that we have a selection, we need to make sure it's a valid selection. So just make sure it's between 0 and recordCount. That should be simple enough. Now, the final act, assuming we have a valid selection, is to load that entry from the file. We call the loadEntry() function to do that for us.

// load an address book record from file
short int loadEntry(short int entry) {
	FILE *f = NULL;
	
	// return FALSE if we can't open the file
	if ((f = fopen(ADDRESSBOOK,"rb")) == NULL) {
		return FALSE;
	}

	// seek out the correct entry in the file
	if (fseek(f,(sizeof(short int) + (entry * sizeof(PERSON))),SEEK_SET) == 0) {
		// erase the person structure
		memset(p,0,sizeof(PERSON));
		fread(p,sizeof(PERSON),1,f);
	}
		
	// close the file
	fclose(f);
	
	// reset the record to the current record entry
	record = entry + 1;
	
	return TRUE;
}

The loadEntry() function introduces a new function for file I/O that is very useful in files with multiple records. The fseek() function. fseek() allows us to go to a certain position (called an offset) in the file and start reading from there. Note that you should not overwrite data in a file with a custom file tag. This is because of the way fwrite() works. The reasons are complex, but if you overwrite data in a file with a custom file tag, it will lose the tag, and possibly some data in the file. This is just something you have to deal with. Our workaround is to read in all our old data and copy it over replacing data before it is added to the new file, instead of overwriting old data.

The fseek() function takes three arguments, the file pointer (*f in this function), how much data to skip, and where to skip from.

Since we have already covered the removal of an entry in the writeRecords() function, let's examine the removeEntry() function to complete the cycle. Remember that our file is stored in a very specific way. This is done so that we can use the data effectively. Binary data has NO meaning to a computer (er, calculator), so we need to know what the data means. It is because we have a consistent file structure that we know where in a file a certain record will be.

Remember that the file structure looks like this: first store the record count, then store each record in sequential order. So, first record 1, then 2, then 3, etc. So, if we want to find record 4, we need to skip the record count and the first three records. To tell the fseek() function this, we need to tell it how many bytes to skip. We know that the recordCount is a short int (look in addybook.h), and each person structure takes up some other amount of space. Well, the sizeof() operator can tell us exactly how much space a variable takes up. So, just tell it sizeof(short int) + (sizeof(PERSON) * entry). So, we skip x entries and one short int and we're right at the part we want to read. The last argument tells us where to start from in our seek. SEEK_SET means start at the beginning of the file, SEEK_END means start at the end of the file, and SEEK_CUR tells us to start wherever we are at currently. We will start most often from the beginning, so we can just use SEEK_SET. So, fseek(f,sizeof(short int) + (entry * sizeof(PERSON)),SEEK_SET) means skip x entries plus the record count and get to the entry we want to read. This is much easier than reading each one individually until we reach the right one.

All that is left is to read the entry in using fread(). Finally, we reset the current record pointer, since it will have changed after selecting a different entry. Now we can move on to the final task, removing an entry. We already covered the file I/O half, but we couldn't take a look at the pretext until we covered the other functions.

// remove the current entry from the address book
void removeEntry(void) {
	// we cannot remove from an empty list
	if (recordCount == 0) {
		dlgError(RECORD_ERROR,NO_RECORDS_ERROR);
		return;
	}

	// display the entry they are about to remove
	displayEntry();
	
	// allow the user to cancel by pressing ESC
	if (dlgError(CONFIRM_WARNING,CONFIRM_REMOVE) == KEY_ENTER) {
		// rewrite the address book file
		writeRecords(REMOVE_ENTRY);
		
		// decrement the record count
		--recordCount;
		
		// decrement the current record
		if (record > 0) {
			--record;
		}
	}
	
	// load the closest entry to that removed
	if (recordCount > 0) {
		// reload the entry closest to the one we removed
		loadEntry((record > 0) ? (record - 1) : record);
	}
}

The first step in removal is not to trust the user. Since people often make mistakes, it's a good idea to tell them exactly what the program will do if they press ENTER. In this case, we show them the data they are about to remove (using the displayEntry() function), then ask them if they are sure. Only then do we proceed.

The next steps are where it gets confusing though, because we are removing and changing entry id's, record counts, and the current record. But when we do all these things influences how the program will work. We can't change anything until the entry has been removed, so don't change the record count or the current record until after the call to writeRecords() is finished. Assuming we have an entry to remove, we can always decrement the record and recordCount, but what do we do now that we've removed an entry. Remember that when we selected an entry, we need to load another one from disk. Since we just removed the current record, if we still have more records, we should load another one from disk too. So, we call the loadEntry() function, but remember that in selectEntry() we had to give it record - 1, so we could do math correctly. Well, this is much the same, except that in our case, record might already be 0 (because if we were looking at record 1, and we decrement the record earlier, now it's at 0), so we need to pass either record, or record - 1. We can use the ternary operator to make sure we don't load an invalid entry. (record > 0) ? (record - 1) : record means if (record > 0), then send record - 1, otherwise, send record.

We haven't used the ternary operator before, but it's kind of a compact if statement. Its syntax is this: (condition) ? (do this if condition is true) : (otherwise do this).

That's all there is to this program. See, it was simple. In case you were wondering, this program took me roughly 8 hours to complete (that's actual work-time, not time from start to finish, because I had a lot of in-between time). This also includes debug time, not just write time. Write time was about 2 hours. The other 6 were debug time, so don't feel bad if you think it's taking forever to find a bug. I stuck this program away after the first night when I couldn't fix the bugs. Then I lost it for two months, then it took another 4 hours of debugging to get it working proper. I have rewritten the write and read routines at least 5 times each. So, don't feel bad if you have a program error. You will spend more than half your time debugging your work. Often times up to 90% of the development time is spent on debugging. Writing is not hard, it's fixing it that's hard.

Step 4 - Conclusions

This concludes your introduction to file I/O. It doesn't cover everything, but it covers the most important topics in file I/O. The other things you can look at are the fputs() and fprintf() functions, and we didn't even cover text mode. But most of the time you will be working with purely binary data, and it's much easier to read and write records. And unless you need to edit your files with the text editor, there's not a lot of use for text mode. Maybe I'll add that into some future lesson. We'll see.

File I/O is a very useful tool, and I've gotten questions on how to use it for a long time, way back before Zeljko Juric wrote us the ANSI file I/O routines that we are using here. Good thing he did because the AMS file system sucks, and I really don't want to work with it on its terms right now. However, there are many other things you can do with file I/O that may interest some people. Some day I'll probably write an advanced file I/O lesson, but that day is not today. Stick with it and keep reading the TIGCC documentation. It's the most useful guide you'll find. Maybe even more useful than my programming lessons... I hope it helps.

For the future... Okay, I know I promised a Nibbles review some two years ago when I started making assembly lessons, and someday maybe I'll do that. But I wanted to present a good working model of the game before I did that. I finished version 4.0 some time ago and now need to finish out the topics that I want to cover before adding the nibbles game as a lesson. But hopefully, it will come soon.

If you understand everything in the first ten lessons, can read the TIGCC docs, know how to find help on the TICT message board, and practice writing code, you should now be a competent TI-68k C programmer, able to handle any task.


Lesson 10: File I/O
Questions or Comments? Feel free to contact us.


 

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

Get Firefox!    Valid HTML 4.01!    Made with jEdit