dcsimg

Working with the Sequential File Structure in C++

Because C++ imposes no structure on the data stored in files, they typically are stored in an unordered format. Therefore, if we want to set any form of order in the file data, we must do so programmatically. This article focuses on the basic file processing capability of C++ with the perspective to show how we can impose a simple record-like structure on files by using appropriate examples.

Overview

A file is the basic entity for permanent retention of data in secondary storage devices such as hard drives, CDs, DVDs, flash drives, and tapes. But, the problem is that the common file imposes no structure of data storage and is not fit for high level-data processing. Files store a sequence of data as raw data bytes. Programmatic processing of them is required to derive any meaning out of them. There is no question of a format or data that has any form of structure on it. Therefore, it poses a challenge in the storage and retrieval of a structured data in any specific format. However, the C++ Standard Library provides extensive support to deal with this type of problem. But, before getting into them, let us understand how C++ treats files.

Files and Streams

C++ treats each file as a sequence of bytes. The end of a file is either denoted by a end-of-file marker or a specific byte number. This technique is determined and maintained by the underlying operating system's file data structure; C++ has no role in file administration. In C++, typically a file is opened if it already exists or created as an object associated to the stream is created. The most common stream objects in C++ are: cin, cout, cerr, and clog. These objects are created as we include the iostream header into a program. The cin object associates with the standard input stream, cout with the standard output stream, and cerr and clog with the standard error stream. Note that the standard input or output stream does not always mean keyboard and screen/monitor. It can be any device that pertains to input or output such as touch screen devices. The error streams typically associate with the output devices.

File Processing

There are two indispensable headers that must be included to perform file processing in C++: <iostream> and <fstream>. There are three important stream class templates in the <fstream> header: the basic_ifstream for performing file input, basic_ofstream for file output, and basic_fstream for both file input and output. The speciality of each class template is that they provide predefined operations to handle character input/output. The built-in mechanism to transform sequence of bytes into character is often very useful in deriving meaningful data from a file.

Note that the ifstream, ofstream, and fstream objects that we often use ina C++ program is nothing but a specialization of basic_ifstream, basic_ofstream, and basic_fstream, respectively. The <fstream> library provides theses typedef aliases for the basic template specialization classes to provide convenience by performing character I/O to and from files.

Sequential Files

A file created with the help of C++ standard library functions does not impose any structure on how the data is to be persisted. However, we are able to impose structure programmatically according to the application requirement. Suppose a minimal "payroll" application stores an employee record in a sequential file as employee id, name of the employee, and salary. The data obtained for each employee constitutes a record. The records are stored and written to the file sequentially and retrieved or read from the file in the same manner. Following is an example.

#include <string>
#include <vector>
#include <iostream>
#include <iomanip>
#include <fstream>

using namespace std;

class Employee
{
public:
   Employee(int = 0, const string & = string(""), double = 0.0);

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

   void setName(const string &);
   string getName() const;

   void setSalary(double);
   double getSalary() const;

private:
   int id;
   string name;
   double salary;

};

Employee::Employee(int i, const string &n, double s)
   :id(i),name(n),salary(s){}

void Employee::setId(int id){
   this->id = id;
}

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

void Employee::setName(const string &name){
   this->name = name;
}

string Employee::getName() const{
   return name;
}

void Employee::setSalary(double salary){
   this->salary = salary;
}

double Employee::getSalary() const{
   return salary;
}

void writeToFile(const string &filename, Employee *e){
   ofstream fout(filename, ios::app);
   if(!fout){
      cerr<<"Cannot open file for writing"<<endl;
      exit(EXIT_FAILURE);
   }
   fout<<e->getId()<<' '<<e->getName()<<'
      '<<e->getSalary()<<endl;
   fout.close();
}

void readFromFile(const string &filename) {

   ifstream fin(filename, ios::in);
   if(!fin){
      cerr<<"Cannot open file for writing"<<endl;
      exit(EXIT_FAILURE);
   }
   int id = 0;
   string nm = "";
   double sal = 0.0;
   cout<<"------------------------------"<<endl;
   cout << left << setw( 10 ) << "ID" << setw( 13 )
   << "NAME" << "SALARY" << endl << fixed
      << showpoint;
   cout<<"------------------------------"<<endl;
   while(fin >> id >> nm >> sal){
      cout << left << setw(10) << id << setw(13)
            << nm << setw(7) << setprecision(2)
         << right << sal << endl;
   }
   cout<<"------------------------------"<<endl;
   fin.close();
}


int main()
{
   const string filename = "payroll.dat";

   writeToFile(filename,new Employee(101,"Mickey",5600.00));
   writeToFile(filename,new Employee(102,"Donald",2635.00));
   writeToFile(filename,new Employee(103,"Zairo",1200.00));

   readFromFile(filename);

   return 0;
}

Output

Output from the preceding code
Figure 1: Output from the preceding code

Opening a File

Here, we have opened the file using an instance of the ofstream class using two arguments: filename and opening mode. A file can be opened in different modes, as shown in the following table.

File Opening mode Description
ios::in Open file for reading data.
ios::out Creates file if it does not exist. All existing data is erased from the file as new data is written into the file.
ios::app Creates file if it does not exist. New data is written at the end of the file without disturbing the existing data.
ios::ate Open file for output. Data can be written anywhere in the file, similar to append.
ios::trunc Discard file content, which is the default action for ios::out.
ios::binary Open file in binary (non-text) mode.

There is an open function that also can be used to open a file and set the mode. This is particularly useful when we first create an ofstream object and then open it as follows:

ofstream ofile;
ofile.open(filename, ios::out);

After creating an ofstream or ifstream object, it typically is checked whether it has successfully opened or not. We do this with the following code:

if(!fin){
   cerr<<"Cannot open file for writing"<<endl;
   exit(EXIT_FAILURE);
}

Using the overloaded operator ! is a convenient way to determine whether the operation was successful. The condition returns true or false, according to the value set in either failbit or badbit. The failbit is set for stream due to format errors such as attempting to read a number when a string value is entered as input. The badbit is set for the stream error due to data loss.

Data Processing

Data processing occurs after a file has been opened successfully. The stream insertion (>>) and extraction (<<) operators are overloaded for convenient writing and reading data to and from a file. The file we have created is a simple text file and can be viewed by any text editor.

Closing a File

A file opened must be closed. In fact, as we invoke the close operation associated with the ofstream or ifstream object, it invokes the destructor, which closes the file. The operation is automatically invoked as the stream object goes out of scope. As a result, an explicit invocation of the close function is optional, but still is good programming practice.

Conclusion

This article tried to put in a simple manner how to work with the sequential file structure in C++. The <fstream> library contains numerous methods to handle data processing using flat files. By using the library, more complex operation on files can be handled efficiently. There are numerous methods and operators overloaded operations to make the data processing on files easy. Here, we have focused only on the preliminary aspect of sequential file processing.



This article was originally published on April 21st, 2020

Related Articles

Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date