The first version of the Rolodex database program, rolodexdb, expands the block I/O version, forming a simple but authentic demonstration of direct file access. It adds an interactive user interface and functions that dynamically manage a one-file database. It continues to rely on the block I/O functions while demonstrating the seek, tell, flush, and clear functions.
rolodexdb Architectures
The next evolutionary step of the Rolodex solution adopts an object-oriented architecture, benefiting from its encapsulation. Instances of the card class represent database records, one record for each Rolodex card. The rolodex class and its member functions manage card objects in the database file. The application, roldexdb, provides the user interface and manages the database through rolodex objects.
File
Description
card.h
The card class specification and the functions managing its instances.
rolodex.h
The rolodex class specification and member function prototypes.
rolodex.cpp
The rolodex member function definitions accessing the database file.
rolodexdb.cpp
The application functions implementing the database operations.
The rolodexdb files.
The four files separate and define the card, rolodex, and application parts of the database example.
The rolodex file architecture.
Each row in the rolodex file is an instance of the card class, a record with three fields: name, address, and phone number. The record numbers begin at 0 and increase in sequence. The byte offset of the first record is at 0, and subsequent records are offset in the file by a multiple of sizeof(card) bytes.
Contextualizing Database Features
Each file isolates and organizes some basic program functionality. All four files, the complete working program, are available at the bottom of the page. As only the rolodex member functions relate to direct file access, they are the only functions detailed here. The rolodex functions "sit" between the card and rolodexdb classes, called by a rolodexdb function and using the card class members. The text presents relevant features of these classes, providing a context for the following detailed function descriptions.
Symbolic constants for the lengths of the C-string members. Making them public makes them available to the application program.
Private C-string member fields for the name, address, and phone number.
Builds an "empty" card object.
Builds a filled card object.
Returns the name saved in a card.
Case-sensitive comparison of the target string and the name in a card object.
Prints the data stored in a card to the console.
Prints a card's record number and data.
card.h. Specifies the card class. An instance of the card class represents one card or record in the Rolodex database file. Although not illustrated in the figure, the example defines all card functions inline.
void rolodexdb::run()
{
rolodex contacts;
char name[card::NAME_SZ];
char address[card::ADDR_SZ];
char phone[card::PHONE_SZ];
while (true)
{
int operation = menu();
switch (operation)
{
case APPEND : // append new card at end
input("Name", name, card::NAME_SZ);
input("Address", address, card::ADDR_SZ);
input("Phone#", phone, card::PHONE_SZ);
contacts.append(name, address, phone);
break;
case SEARCH : // search for a card by name
input("Search name", name, card::NAME_SZ);
contacts.search(name);
break;
case EDIT : // edit a card
input("Search name", name, card::NAME_SZ);
input("New address", address, card::ADDR_SZ);
input("New phone#", phone, card::PHONE_SZ);
contacts.edit(card(name, address, phone));
break;
case REPLACE :
input("New name", name, card::NAME_SZ);
input("New address", address, card::ADDR_SZ);
input("New phone#", phone, card::PHONE_SZ);
contacts.replace(input("Record number"), card(name, address, phone));
break;
case LIST : // list all cards
contacts.list();
break;
case QUIT : // quit
return;
default:
cerr << "Unknown option: " << operation << endl;
break;
}
cout << endl;
}
}
The run function excerpted from rolodexdb.cpp.
The run function receives a user's command via the menu function and interprets it using a switch statement. The example's focus is on direct file access; consequently, some of the database operations are inelegant or incomplete.
The rolodex class.
The rolodex class implements a simple one-file database. The data file contains instances of the card class - each instance represents one card or record in a Rolodex database.
rolodex::rolodex()
{
data.open("rolodex", ios::binary | ios::in | ios::out); // (a)
if (!data.is_open()) // (b)
{
ofstream make("rolodex.bin"); // (c)
make.close(); // (d)
data.open("rolodex.bin", ios::binary | ios::in | ios::out); // (e)
}
if (!data.good()) // (f)
{
cerr << "Unable to open data file." << endl;
exit(1);
}
}
The rolodex constructor. Opens the database file, data, creating it first if it doesn't exist.
Attempts to open the data file in binary mode for reading and writing - this operation fails if the file does not already exist. The second parameter, needed to open the file in binary mode, replaces the fstream default open mode of in|out, so the modes must include these flags to open the file for both input and output.
Tests the file to see if it opened - is false if the file doesn't exist.
Creates a new file using a temporary ofstream object.
Closes the new file
Makes another attempt to open the data file in binary mode for reading and writing.
Aborts the program if the data file cannot be created or opened.
The append member function. Creates a new contact card and writes it at the end of the file
Constructs the card object.
Moves the "put" or write pointer to the end of the data file.
Writes the contents of the card to the data file.
Stream objects have an aggregated file buffer and the program only writes its contents when it's filled, closed, or flushed. The flush function forces the stream to write any pending (unwritten) data in the buffer to the file. This operation is necessary to ensure the data is available for a subsequent read.
void rolodex::search(char* name)
{
card contact; // (a)
streampos pos = find_name(name, contact); // (b)
if (pos == (streampos)-1) // (c)
cout << name << " not found in the Rolodex\n";
else
contact.print(); // (d)
data.clear(); // (e)
}
The search member function. Searches the data file for a record with a specific name. If it finds the name, it prints all the associated information to the console; otherwise, it prints a failure message.
Defines a temporary card object to use with the get_name function.
Searches the database file for name. It returns the position (byte offset) of the corresponding contact's card if it finds a match; it returns -1 otherwise. The example defines this function at the bottom of the page.
Tests the returned position (the cast is necessary).
Prints all contact information if it finds a match.
Clears or resets the eof and error flags, set if find_name doesn't find a match. Failing to reset the flags causes subsequent file operations to fail.
The list member function. Lists all of the information in the data file (i.e., displays each card or record as one row in a table printed on the console).
Defines a temporary card object to hold each record as it is read and printed.
Rewinds (i.e., moves to) the beginning of the data file.
Reads each record from the data file. Reaching the end of the file breaks out of the while loop.
Prints each record to the console.
Clears the end-of-file flag. The last read function call does not read any data, but it does set the EOF flag.
void rolodex::edit(card a_card)
{
card contact; // (a)
streampos pos = find_name(a_card.get_name(), contact); // (b)
if (pos == (streampos)-1) // (c)
{
cout << a_card.get_name() << " not found in the Rolodex\n";
data.clear();
return;
}
data.seekp(pos); // (d)
data.write((char *) &a_card, sizeof(card)); // (e)
data.flush(); // (f)
}
The edit member function.
Changes the address and phone number in an existing card by locating it in the file, and overwriting it with a new card if it finds a match. Although this is an inelegant (i.e., not a "production grade") function, it demonstrates how to read from and write to a file at a specific position.
Defines a temporary card object to use with the get_name function.
Searches the database file for name. It returns the position (byte offset) of the corresponding contact's card if it finds a matching record; it returns -1 otherwise. The example defines this function at the bottom of the page.
Tests the returned position (the cast is necessary). Prints an error message and clears the error flags if it doesn't find a record matching name.
Moves the "put" position pointer to pos in the database file.
Replaces or overwrites the address and phone number (name is unchanged) in the data file with the information in a_card.
Stream objects have an aggregated file buffer that they only write when it is full, closed, or flushed. The flush function forces the stream to write any pending (unwritten) data in the buffer to the file. This operation is necessary to ensure the data is available for a subsequent read.
void rolodex::replace(int record, card contact)
{
if (record < 0) // (a)
{
cout << "Block number must be >= 0\n";
return;
}
card temp; // (b)
data.seekg(record * sizeof(card)); // (c)
data.read((char *) &temp, sizeof(card));
if (!data.good()) // (d)
{
cout << record << " is beyond the file's end" << endl;
data.clear(); // (e)
return;
}
data.seekp(record * sizeof(card)); // (f)
data.write((char *) &contact, sizeof(card));
data.flush();
}
The replace member function.
Replaces the card in the data file at the indicated record number with the new contact. Programs can easily determine when a given record number is too low: it must be greater than or equal to 0. However, determining if a non-negative record number is valid is more challenging: programs typically don't "know" how many records a file contains, and it is legal to seek beyond the file's end. The function detects out-of-bounds record numbers by attempting to read the specified record and testing the error flags.
Rejects record numbers less than zero.
A temporary card the program uses to detect record numbers that are too large.
Moves the "get" or read position pointer to the record number and attempts to read the record.
Tests the stream's error flags. Flags set by the last read operation indicate an out-of-bounds record number. A failed test clears the error flags and returns.
If it doesn't detect any errors, replace moves the "put" or write position pointer to the specified record number, writes the record, and flushes the stream.
The find_name member function.
Searches the rolodex file for name. if found, it saves the corresponding card in contact and returns its record number. The function's sequential search is appropriate only for a few tens of small records. The next section presents a more efficient solution, better adapted to larger records and files.
Moves the "get" or read pointer to the beginning of the database file.
Sequentially reads each record in the file, saving it in contact. If the contact names in name and contact match, the function returns the record's position or byte offset in the file.
The function calculates the record's position as the difference between the current "get" position (advanced by the previous read) and the record's size (i.e., the number of bytes read).
The function returns -1 if name is not found in the file.