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

We continue to use a Rolodex to demonstrate I/O operations, but we replace the line-oriented operations of the previous examples with block I/O functions. Where past Rolodex examples used a line of text to represent a record or a Rolodex card (i.e., all the information about a person), the following example uses a structure named card. Instances of the card structure replace the delimiter-separated fields for name, address, and phone number with three separate structure members.

const int NAME_SZ = 20;				// (a)
const int ADDRESS_SZ = 35;
const int PHONE_SZ = 20;

struct card					// (b)
{
	char name[NAME_SZ];
	char address[ADDRESS_SZ];
	char phone[PHONE_SZ];
};
b-rolodex declarations. An instance of the card structure represents one record or entry in the Rolodex file - that is, the information for one person. The most notable feature of the card structure is that the fields are C-strings, which are much easier to use with the block I/O functions than are instances of the string class. In a "real" program, the declarations, including the structure specification, would be placed in a header file and #included in all source code (i.e., .cpp) files.
  1. Symbolic constants to denote the size of each field (i.e., the size of each character array).
  2. The structure specification.

The program's goal is the illustration of the block I/O functions and necessary supporting code, so the examples are unrealistically simplified. Furthermore, the code is grouped into functional units to facilitate the presentation; the complete program is available at the bottom of the page. To provide some context, especially the definition of the file streams, we begin with a partial main function.

#include <iostream>						// (a)
#include <fstream>
#include <iomanip>
#include <cstring>
//#include card constants and structure
using namespace std;


int main()
{
	ofstream out("rolodex", ios::binary);			// (b)

	if (!out.good())					// (c)
	{
		cerr << "Unable to open \"rolodex\"\n";
		exit(1);
	}

	// code to define variables and write the data		// (d)

	out.flush();						// (e)
	out.close();



	ifstream in("rolodex", ios::binary);			// (f)

	if (!in.good())						// (g)
	{
		cerr << "Unable to open \"rolodex\"\n";
		exit(1);
	}

	// code to define variables and read the data		// (h)

	return 0;
}
b-rolodex.cpp.
  1. Header files for the supporting classes and functions. For simplicity, the example program is written in a single file; in a more realistic program, the symbolic constants and structure specification would be placed in a separate header file.
  2. The file output stream is opened in binary mode.
  3. Makes sure that the file opened correctly.
  4. Create and write card objects to the rolodex file. Three options are presented below. Which option is used depends on the exact problem being solved.
  5. Most of the time a program doesn't need to explicitly flush and close a file as the destructor performs these operations automatically. However, for the purpose of demonstrating block I/O, the first part of the demonstration program writes structure objects to the file and the second part reads those same objects from the file. In order for the read operations to succeed, the write operations must be complete. The flush and close functions force the operating system to finish the output operations before the program attempts to read the objects.
  6. The file input stream is opened in binary mode.
  7. Makes sure that the file opened correctly.
  8. Illustrates how to read structure objects from a file.

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. Three different options for writing the objects to the file are described below.

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 option 1: using the block write function to output one structure object to a file. A "real" program would likely define a single structure object, fill it with data (perhaps as a user enters it), and use a single write function call to output the object to a file. Such a code pattern would be appropriately placed in an application function or in a loop. Here, multiple variables and function calls demonstrate how objects are written sequentially to a 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 each structure object
    2. The address of the object is cast to a character pointer, which becomes the first parameter for the write function call
    3. The sizeof operator finds the size, measured in bytes, of the structure object, which becomes the second parameter for the write function call
card deck[] =									// (a)
{
	{ "Bill Gates", "1 Microsoft Way, Redmond, WA", "(403) 123-4567" },
	{ "Cranston Snort", "1600 Pennsylvania Ave", "(306) 678-9876" },
	{ "Albert Einstein", "Princeton, NJ", "(456) 123-8765" },
	{ "John Smith", "123 Elm St.", "801-555-1234" }
};

for (int i = 0; i < sizeof(deck) / sizeof(card); i++)				// (b)
	out.write((char *) &deck[i], sizeof(card));
write option 2: writing an array to a file. Some problems call for an array of objects. For example, arrays may be reorganized, sorted, and searched. When all processing is finished, arrays are easily written to a file with a for-loop.
  1. The array definition and initialization illustrated here is just for demonstration purposes. Data is typically stored in an object as it is input - entered by a person or read from a file.
  2. Writing an array of objects to a file with a for-loop. Note that in a "real" program that the expression sizeof(deck) / sizeof(card) would likely be replaced with a variable storing the number of objects in the array - that is, the number of array elements that are actually used.
card deck[] =									// (a)
{
	{ "Bill Gates", "1 Microsoft Way, Redmond, WA", "(403) 123-4567" },
	{ "Cranston Snort", "1600 Pennsylvania Ave", "(306) 678-9876" },
	{ "Albert Einstein", "Princeton, NJ", "(456) 123-8765" },
	{ "John Smith", "123 Elm St.", "801-555-1234" }
};

out.write((char *) deck, 4 * sizeof(card));					// (b)
write option 3: writing arrays to a file with a single write function call. Writing a whole array to a file is very easy.
  1. Same as Figure 4(a).
  2. Recall that all array elements are stored contiguously in memory. The write function can take advantage of this organization and write the entire array as one block. Note that the value "4" in the calculation of the second parameter is replaced by the number of elements in the array.
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 option 1: reading multiple objects one at a time. We can easily process a file containing structure objects with a while-loop. We see the code pattern illustrated here in many file processing problems. In this example, once the program reads an object, only display it on the console; replace the output statements to provide the processing needed for a specific problem.
  1. Defines a single structure variable that is reused during each iteration of the loop.
  2. Reads each structure object one at a time in order.
    1. the address of operator finds the address of the structure variable
    2. the address of the variable is cast to a character pointer, forming the first parameter
    3. the sizeof operator finds the number of bytes in the structure, which forms the second parameter
    4. the read function reads one object from the file and stores it in the structure variable
    5. the read function returns a reference to in and the overloaded conversion operator, operator bool(); converts it into a Boolean value that drives the loop
card	array[4];						// (a)
in.read((char *) array, 4 * sizeof(card));			// (b)

for (int i = 0; i < 4; i++)					// (c)
{
	cout << left << setw(NAME_SZ) << array[i].name;
	cout << setw(ADDRESS_SZ) << array[i].address;
	cout << setw(PHONE_SZ) << array[i].phone << endl;
}
read option 2: reading multiple objects with a single function call. The code pattern illustrated here is much less common than the previous one because it requires programmers to know how many objects the file contains before defining the array and calling the read function. However, if the number of objects is known in advance, this is a convenient and compact way of quickly filling an array of objects with data read from a file.
  1. Defines an array of four card structure objects.
  2. The name of an array is its address, which is cast to a character pointer to form the first parameter. The sizeof operator provides the number of bytes in a single card object, which is multiplied by the number of objects that the read function reads from the file.
  3. Once the objects are stored in an array, they may be processed however the underlying problem dictates, for example, with a for-loop.

Downloadable File

b-rolodex.cpp