14.7.1. b-rolodex.cpp: Block I/O Example

Review
  1. strcmp
  2. Ordering Functions and qsort
  3. I/O stream classes (streambuf)

This example continues using the Rolodex problem to demonstrate I/O operations by making two significant changes. First, it changes how the program represents a Rolodex card from a delimiter-separated line to a structure named card, whose fields correspond to a name, address, and phone number. Second, it replaces the line-oriented operations with block read and write functions.

The example's primary goal is demonstrating the block read and write functions. Consequently, it oversimplifies the initial data collection and omits the searching and deletion operations supported by most rudimentary database programs. Subsequent versions provide more robust and authentic implementations. The following figure excerpts the opening and validation operations, providing a context for the I/O operations and making the example easier to present.

#include <iostream>
#include <fstream>
#include <iomanip>
//#include <cstring>						// Version 2: strcmp
//#include <stdlib.h>						// Version 2: qsort
#include "rolodex.h"
using namespace std;

//int order(const void* e1, const void* e2);			// Version 2: function prototype

int main()
{
	ofstream out("rolodex", ios::binary);			// (a)
	if (!out.good())
	{
		cerr << "Unable to open \"rolodex\"\n";
		exit(1);
	}

	// code to define variables and write the data

	out.close();						// (b)



	ifstream in("rolodex", ios::binary);			// (c)
	if (!in.good())
	{
		cerr << "Unable to open \"rolodex\"\n";
		exit(1);
	}

	// code to define variables and read the data

	return 0;						// (d)
}
b-rolodex.cpp. This code fragment manages the files and provides a context for the I/O operations.
  1. Opens and validates the output file in binary mode.
  2. The close function writes any pending output (i.e., any data remaining in the output stream's buffer), preventing any conflicts with the following read operation.
  3. Opens and validates the input file in binary mode.
  4. The istream destructor automatically closes the input stream.

Each card or record entered into the file is an instance of the card structure. So, the file representing the Rolodex looks very much like Figure 2(a) where each square is an instance of card.

card c0 = { "Bill Gates", "1 Microsoft Way, Redmond, WA", "(403) 123-4567" };		// (a)
card c1 = { "Cranston Snort", "1600 Pennsylvania Ave", "(306) 678-9876" };
card c2 = { "Albert Einstein", "Princeton, NJ", "(456) 123-8765" };
card c3 = { "John Smith", "123 Elm St.", "801-555-1234" };

out.write((char *) &c0, sizeof(card));							// (b)
out.write((char *) &c1, sizeof(card));
out.write((char *) &c2, sizeof(card));
out.write((char *) &c3, sizeof(card));
write: Object output. Replacing the pink comment in Figure 2, the demonstration creates four objects and saves them in a file on secondary storage using the block write function. A more authentic program would define a single structure object, iteratively fill it with user input, and save it to the file.
  1. Defines and initializes four structure objects.
  2. Each function call writes one object to the file. However, several operations are taking place within each function call:
    1. The address-of operator finds the address of a structure object.
    2. The casting operator converts the object's address to a character pointer, forming the first write function argument.
    3. The program finds the second argument, the number of bytes the function writes to the file, with the sizeof operator, which returns the size, measured in bytes, of the structure object.
card	c;							// (a)

while (in.read((char *) &c, sizeof(card)))			// (b)
{
	cout << left << setw(NAME_SZ) << c.name;
	cout << setw(ADDRESS_SZ) << c.address;
	cout << setw(PHONE_SZ) << c.phone << endl;
}
read: Object input, version 1. Replacing the blue comment in Figure 2, this demonstration makes the typical assumption that the program "doesn't know" how many objects are in the file, so it iterates through all of them.
  1. The program defines a single structure object and reuses it during each loop iteration.
  2. The loop reads one structure object during each iteration:
    1. The address-of operator finds the address of the structure variable.
    2. The casting operator converts the address to a character pointer, forming the first argument.
    3. The sizeof operator finds the number of bytes in the structure, forming the second argument.
    4. The read function reads one object from the file, stores it in the structure variable, and returns a reference to in.
    5. The overloaded conversion operator, operator bool() converts the reference to a Boolean value, driving the loop.

The final block read example adds authenticity by reading the card objects into an array and sorting them alphabetically by name using the qsort library function. qsort compares card objects two at a time with an ordering function, rearranging them as necessary to affect the alphabetical ordering. The following figures illustrate the programmer-defined card ordering function and how qsort uses it.

int order(const void* e1, const void* e2)
{
    return strcmp(((card *)e1)->name, ((card *)e2)->name);
}
card ordering function. The qsort function can sort any object by any combination of its fields. Programmers achieve this flexibility by creating separate ordering functions for each object type and each required ordering. The card ordering function performs three distinct operations, with the first two applying individually to each argument:
  1. Void pointers are typeless memory addresses, so the function's first task is casting them to a "known" pointer type, card in this example.
  2. Next, the function extracts the data from the C-string name field with the arrow operator.
  3. Finally, the function compares the two names with the C-string strcmp function. The ordering function returns -1 if the two arguments are out of order, 0 if they order the same, and 1 if they are in order. Conveniently, strcmp follows the same protocol, allowing the ordering function to merely pass on its return value.
Including the function's prototype at the top allows programmers to place the function above or below main.
card cards[100];							// (a)
int  count = 0;

while (count < 99 && in.read((char *) &cards[count], sizeof(card)))	// (b)
	count++;

qsort(cards, count, sizeof(card), order);				// (c)

for (int i = 0; i < count; i++)						// (d)
{
	cout << left << setw(NAME_SZ) << cards[i].name;
	cout << setw(ADDRESS_SZ) << cards[i].address;
	cout << setw(PHONE_SZ) << cards[i].phone << endl;
}
read: Object input, version 2. The second read example, still replacing the blue comment in Figure 2, demonstrates reading an unknown number of objects into an array and sorting and displaying them.
  1. The statements define an array of card objects and a variable to count them as the program reads them from the file.
  2. The loop runs while the number of objects read is less than the array's length, and the read function successfully reads an object from the file.
  3. The quick sort function sorts the array elements alphabetically by name. The arguments are, left-to-right, the address of the array, the number of objects in the array, the size of each object, and a pointer to the ordering function.
  4. Once sorted, the program prints the cards in a tabular format.

Downloadable Files

ViewDownloadComments
rolodex.h rolodex.h The rolodex structure specification and associated field size definitions.
b-rolodex1.cpp b-rolodex1.cpp The first version reads and formats objects in a loop.
b-rolodex2.cpp b-rolodex2.cpp The second version reads objects into an array with a loop, sorts the array, and formats and prints the objects in a second loop.