Working with the Random File Structure in C++

Implementing a random file access is a challenge, but it can be accomplished by using C++ file processing functions. As we know, C++ does not impose structure on a file; if we want to use random access to files, we must create them programmatically using tools provided by the files and streams library. This article shows how we operate randomly yet consistently by using a simple file structure supported by C++ and using appropriate code examples.

Random File Access

An application can be created to access file information in a random fashion. In a sequential file, information is accessed in a chronological order whereas the random access provides instant fetching of a record and that too can in any order or no order at all. In random access, we must implement the capability to locate a record immediately. This is typical of any transaction system, be it banking, ticket reservation, and so forth.

But, note that C++ does not impose any structure on a file. As a result, an application that wants to implement random-access files must begin from scratch. There are many techniques to achieve this, and the simplest one is to have a fixed length record. It is easy to calculate the size of a record from a filed length structure. We can exactly locate a record in the file by calculating the size of the object from beginning of the file (see Figure 1).

Order of access of sequential vs random. [sizeof(PhoneBook) = 36 bytes]
Figure 1: Order of access of sequential vs random. [sizeof(PhoneBook) = 36 bytes]

By implementing a random-access mechanism, we can insert, update, and delete previously stored data in a file. We do not need to rewrite the entire file and operate upon a particular record like a database. We’ll implement the following operations:

  • Find a record stored in a file by an ID
  • Add new record
  • Modify existing record
  • Delete an existing record
  • Print all record in tabular format

A Quick Example

The PhoneBook class represents a record stored in a file. It is a simple class without any information about file operation. This means that the object of this class is not aware of its persistence in a binary file.

CRUD operations are implemented along with the main function, and represent the crux of the random-access mechanism represented by the application. The following program represents a minimal implementation of the idea and is unoptimized and elaborate to make the code mostly self-explanatory.

#ifndef PHONEBOOK_H
#define PHONEBOOK_H

#include <string>

class PhoneBook
{
public:
   PhoneBook(int=0,const std::string & = "", const
      std::string & = "", const std::string & = "");

   void setId(int);
   int getId() const;

   void setFirstName(const std::string &);
   std::string getFirstName() const;

   void setLastName(const std::string &);
   std::string getLastName() const;

   void setPhone(const std::string &);
   std::string getPhone() const;

   std::string toString() const;

private:
   int id;
   char firstName[10];
   char lastName[10];
   char phone[10];
};

#endif   // PHONEBOOK_H

#include "phonebook.h"
using namespace std;

PhoneBook::PhoneBook(int _id,
                     const std::string & _firstName,
                     const std::string & _lastName,
                     const std::string & _phone)
   :id(_id){
   setFirstName(_firstName);
   setLastName(_lastName);
   setPhone(_phone);
}

void PhoneBook::setId(int _id){
   id = _id;
}

int PhoneBook::getId() const{
   return id;
}

void PhoneBook::setFirstName(const string & _firstName){
   size_t len = _firstName.size();
   len = (len < 10 ? len : 9);
   _firstName.copy(firstName, len);
   firstName[ len ] = '\0';
}

string PhoneBook::getFirstName() const
{
   return firstName;
}

void PhoneBook::setLastName(const string & _lastName)
{
   size_t len = _lastName.size();
   len = ( len < 10 ? len : 9 );
   _lastName.copy( lastName, len );
   lastName[ len ] = '\0';
}

string PhoneBook::getLastName() const {
   return lastName;
}

void PhoneBook::setPhone(const string & _phone)
{
   size_t len = _phone.size();
   len = ( len < 10 ? len : 9 );
   _phone.copy( phone, len );
   phone[ len ] = '\0';
}

string PhoneBook::getPhone() const {
   return phone;
}

string PhoneBook::toString() const
{
   return "\nID: "+to_string(id)+
          " [ "+string(firstName)+
          " , "+string(lastName)+
          " , "+string(phone)+" ]";
}

The following code implements a random access mechanism. It implements CRUD like-operation, such as adding new a PhoneBook record, updating the phone number of the existing record, and deleting a record.

#include <iostream>
#include <fstream>
#include <iomanip>
#include <cstdlib>
#include "phonebook.h"

using namespace std;

enum Options{ADDNEW = 1, UPDATE, DELETE, SHOW, STOP};
const string fileName = "phonebook.dat";

PhoneBook findById(int id){
   ifstream file(fileName, ios::binary);
   if(!file){
      cerr<<"Error. Cannot open file."<<endl;
      exit(EXIT_FAILURE);
   }
   PhoneBook pb;
   while(file.read(reinterpret_cast<char *>(&pb),
         sizeof (PhoneBook))){
      if(pb.getId()==id)
         break;
      pb.setId(0);
   }
   file.close();
   return pb;
}

void _create(){
   ofstream file(fileName, ios::out|ios::app|ios::binary);
   if(!file){
      cerr<<"Error. Cannot open file."<<endl;
      exit(EXIT_FAILURE);
   }
   int id = 0;
   string fn,ln,ph;
   while(1){
      cout<<"Enter ID(1-100). Any other number to stop: ";
      cin>>id;
      if(id<1 || id>100) break;
      PhoneBook pb;
      pb =findById(id);
      if(pb.getId()==id){
         cout<<"Record exists: "<<pb.toString()<<endl;
         continue;
      }
      cout<<"Enter firstname, lastname, phone\n?";
      cin>>setw(10)>>fn>>setw(10)>>ln>>setw(10)>>ph;
      pb.setId(id);
      pb.setFirstName(fn);
      pb.setLastName(ln);
      pb.setPhone(ph);
      file.write(reinterpret_cast<const char *>(&pb),
         sizeof(PhoneBook));
   }
   file.close();
}

void _update(){
   int id;
   cout<<"Enter ID to modify: ";
   cin>>id;
   fstream file(fileName, ios::in|ios::out|ios::binary);
   if(!file){
      cerr<<"Error. Cannot open file."<<endl;
      exit(EXIT_FAILURE);
   }
   PhoneBook pb;
   string phno;
   bool flag = false;
   while(file.read(reinterpret_cast<char *>(&pb),
         sizeof (PhoneBook))){
      if(pb.getId()==id){
         cout<<pb.toString()<<endl;
         cout<<"Enter new phone number:";
         cin>>phno;
         pb.setPhone(phno);
         long pos = -1*sizeof (PhoneBook);
         file.seekp(pos, ios::cur);
         file.write(reinterpret_cast<const char *>(&pb),
            sizeof (PhoneBook));
         flag = true;
         break;
      }
   }
   if(flag==false)
      cout<<"No record found for modification."<<endl;
   else
      cout<<"Record modified successfully."<<endl;
   file.close();
}

void _delete(){
   int id;
   cout<<"Enter existing ID:";
   cin>>id;
   PhoneBook pb;
   fstream file(fileName, ios::in|ios::out|ios::binary);
   if(!file){
      cerr<<"Error. Cannot open file."<<endl;
      exit(EXIT_FAILURE);
   }
   bool flag=false;
   while(file.read(reinterpret_cast<char *>(&pb),
         sizeof (PhoneBook))){
      if(pb.getId()==id){
         pb.setId(0);
         pb.setFirstName("");
         pb.setLastName("");
         pb.setPhone("");
         long pos = -1*sizeof (PhoneBook);
         file.seekp(pos, ios::cur);
         file.write(reinterpret_cast<const char *>(&pb),
            sizeof (PhoneBook));
         flag=true;
         break;
      }
   }
   if(flag==true)
      cout<<"Record successfully deleted."<<endl;
   else
      cout<<"Record not found."<<endl;
   file.close();
}

void _print(){
   int count=0;
   ifstream file(fileName,ios::binary);
   if(!file){
      cerr<<"Error. Cannot open file."<<endl;
      exit(EXIT_FAILURE);
   }
   cout<<"--------------------------------"<<endl;
   cout<<left<<setw(3)<<"ID"
      <<setw(10)<<"FName"
     <<setw(10)<<"LName"
    <<setw(10)<<"Phone"<<endl;
   cout<<"--------------------------------"<<endl;
   PhoneBook pb;
   while(file.read(reinterpret_cast<char *>(&pb),sizeof
         (PhoneBook))){
      if(pb.getId()!=0){
         count++;
         cout<<left<<setw(3)<<pb.getId()
         <<setw(10)<<pb.getFirstName()
         <<setw(10)<<pb.getLastName()
         <<setw(10)<<pb.getPhone()<<endl;
      }
   }
   cout<<"--------------------------------"<<endl;
   cout<<"Total: "<<count<<" Record(s)"<<endl;
   cout<<"--------------------------------"<<endl;
   file.close();
}

int main()
{
   while(1){
         cout << "\nEnter your choice" << endl
         << "1 - Add New record" << endl
         << "2 - update an existing record" << endl
         << "3 - delete a record" << endl
         << "4 - print all records" << endl
         << "5 - Quit\n? ";
         int opt;
         cin >> opt;
         switch(opt){
         case ADDNEW: _create(); break;
         case UPDATE: _update(); break;
         case DELETE: _delete(); break;
         case SHOW: _print(); break;
         case STOP: exit(EXIT_SUCCESS);
         default: cerr << "Invalid Option! Try again."
            <<endl; break;
      }
   }
   return 0;
}

File Handling Functions

The ostream write function is used to write fixed number of bytes to a specified stream. Here, the stream is associated with a file. The put file-position pointer decides the location of where to write the data. Similarly, the istream read function is used to read a fixed number of bytes from the specified stream, which is a file in this case. The get file-position pointer determines the location in the file to read from.

The idea of opening the file in binary mode is that it is ideal for writing fixed-length records.

Note that instead of using the insertion operator to store the integer id of the PhoneBook as follows:

file<<id;

we have cast the number into a character because a 4-byte integer may contain as many as 11 digits (10 digits + a sign byte). But, as we write:

file.write(reinterpret_cast<const char *>(&id),
   sizeof (id));

it stores a binary version of the integer, which is 4 bytes (on a machine that represents 4-bytes as an integer). The object in the memory is treated as a const char *; in other words, a pointer to a byte. As a result, the write function outputs a number of bytes. In much the same way and for the same reason, we have cast the object of the PhoneBook class as a stream of bytes with both read and write operations.

PhoneBook pb;
...
file.read(reinterpret_cast<char *>(&pb), sizeof
   (PhoneBook));
file.write(reinterpret_cast<const char *>(&pb), sizeof
   (PhoneBook));

To output objects, we must convert them to the type const char *; otherwise, the compile will not compile calls to the write function. In C++, the reinterpret_cast operator is used to cast a pointer of one type to an unrelated pointer type. This operator performs at compile time and requests the compiler to reinterpret the operand as a target type declared in the angular brackets. In our example, we have used reinterpret_cast to convert a pointer to PhoneBook to a const char *. The converts the object into a collection of bytes to be output in a file.

Note that in the program processing a random-access to a file, the read/write operation does not read/write a single field to a file; rather, they read/write the objects of a class as a sequence of bytes to and from the file.

The file manipulation functions, such as seekg and seekp, are used to position the get file-pointer and put the file-pointer in a strategic location within the file, respectively. For example, seekg sets the position of the next character to be extracted from the input stream and seekp sets the position of the next character to be inserted into the output stream.

Refer to the C++ <fstream> documentation for details on many such file manipulation functions.

Conclusion

There are many ways we can process a file. This is one of the techniques to manipulate persistent data. With random access to files, we can instantly retrieve and manipulate fixed-length records. We can treat a flat file model almost like a database, although not an efficient one.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read