CS330 Binary I/0 in C++


Highlights of this lab:


Binary I/O Versus Text I/O in C++

First of all, you may be wondering what binary I/O is. Simply put, it is the input/output of a stream of bytes. It is useful for storing or "dumping" exactly what you have in "memory". By contrast, text I/O is good for introducing formatting.

The following table represents some of the features of text and binary I/O.

Text I/O Binary I/O

cout with insertion operator ( << )
cin with extraction operator ( >> )

write()
read()
human readable, formatted I/O machine readable
slower fast I/O with potential for manipulating large blocks of data

Still not clear? The following is a table meant to demonstrate a code example of text output versus binary output:

  Text Output Binary Output
code
#include <iostream>
#include <fstream>
using namespace std;

struct Person
{
   char name[20];
   int age;
};

int main()
{
   Person henry;
   strncpy(henry.name, "Henry", 6);
   henry.age=72;

   //open a file text.out for text output
   ofstream outFile("text.out");

   if (! outFile)
   {
      cerr << "cannot open \"text.out\" for output\n";
      return -1;
   }

   outFile << henry.name;
   outFile << henry.age;

   outFile.close();

   return 0;
}
#include <iostream>
#include <fstream>
using namespace std;

struct Person
{
   char name[20];
   int age;
};

int main()
{
   Person henry;
   strncpy(henry.name, "Henry", 6);
   henry.age=72;

   //open a file bin.out for binary output
   ofstream outFile("bin.out", ios::binary);

   if (! outFile)
   {
      cerr << "cannot open \"bin.out\" for output\n";
      return -1;
   }

   outFile.write(henry.name,sizeof(henry.name));
   outFile.write((char*)&(henry.age),sizeof(henry.age));
   // the following line could also be used instead of the two above
   //outFile.write((char*)& henry, sizeof(henry));

   outFile.close();

   return 0;
}
cat of file
Henry72
HenryÀ»áÑ>°
@H
hex dump
00000000  48 65 6e 72 79 37 32                              |Henry72|
00000007
00000000  48 65 6e 72 79 00 00 00  c0 bb e1 d1 3e 00 00 00  |Henry.......>...|
00000010  b0 0a 40 00 48 00 00 00                           |..@.H...|
00000018 
octal dump
0000000   H   e   n   r   y   7   2
0000007
0000000   H   e   n   r   y  \0  \0  \0 300 273 341 321   >  \0  \0  \0
0000020 260  \n   @  \0   H  \0  \0  \0
0000030

For your information:

Questions:


Overview of the Binary "read" and "write"

Before Reading and Writing Binary

Before, performing a read or write, you need to specify that your file is binary. You can either do that through the constructor call or through the open function. The following table summarizes this:

  For Reading For Writing
Through constructor ifstream is("indata.dat", ios::binary); ofstream os("outdata.dat", ios::trunc|ios::binary);
Through "open"

ifstream is;
is.open("indata.dat", ios::binary);

ofstream os;
os.open("outdata.dat", ios::trunc|ios::binary);

Notes:

The General Format of a "read" and "write" Are:

write(start_address, size);
read(start_address, size);

Because you are working with reading and writing bytes, the start address is a pointer to characters (Characters just happen to be one byte in length). If your address is not a pointer to a character, then you need to use a cast (char *) to "fake" it. From that address on, things will appear as a sequence of bytes.

For instance, using henry (the instance of Person) from the code example above:

outFile.write(henry.name,sizeof(henry.name));
outFile.write((char*)&(henry.age),sizeof(henry.age));

Note:


Reading and Writing Primitive Types

Single Primitives

#include <iostream>
#include <fstream>                           // for all file I/O
using namespace std;

int main()
{ 
   double buff=3.14;                         // buffer for integers
   int i;

   ofstream os("intdata.dat", ios::binary);  // create output stream os

   os.write((char*)&buff, sizeof(double));   // write to it
 
   os.close();                               // must close it before opening another
                                             // stream associated with the same file
   buff=0.0;                                 // clear buffer

   ifstream is("intdata.dat", ios::binary);  // create input stream is
   is.read((char*)&buff, sizeof(buff));      // read from it

   cout << buff << endl;                     // display it
  
   is.close();

   return 0;

} // end main

Notice the use of & and sizeof in the code.

Primitives in Arrays

#include <iostream>
#include <fstream>                           // for all file I/O
using namespace std;

const int MAX = 100;                         // number of ints

int main()
{ 
   int buff[MAX];                            // buffer for integers
   int i;

   for(i=0; i<MAX; i++)
      buff[i] = i;                           // put some data into the buffer

   ofstream os("intdata.dat", ios::binary);  // create output stream os

   os.write((char*)buff, MAX*sizeof(int));   // write to it
 
   os.close();                               // must close it before opening another
                                             // stream associated with the same file
   for(i=0; i<MAX; i++) 
      buff[i] =0;                            // clear buffer

   ifstream is("intdata.dat", ios::binary);  // create input stream is
   is.read((char*)buff, MAX*sizeof(int));    // read from it

   for(i=0; i<MAX; i++)
      cout << buff[i] << endl;               // display it
   is.close();

   return 0;

} // end main

Questions:


Reading and Writing Objects

The following partial code shows the idea of writing and reading an entire object. This works if your object does not contain pointers or stl strings (as one example). Think of shallow copy versus deep copy (copying just the address versus copying all of the items).

#include <fstream>                           // for all file I/O
using namespace std;

class MyClass
{
   private:
      // private data
   public:
      // public functions such as getData() and showData()
};

int main()
{ 
   MyClass myobj;

   ofstream os("objfile.dat", ios::binary);  // create output stream os
   os.write((char*)&myobj, sizeof(MyClass)); // write to it

   os.close();

   ifstream is("objfile.dat", ios::binary);  // create input stream is
   is.read((char*)&myobj, sizeof(MyClass));  // read from it

   is.close();
   return 0;

} // end main

Note:


Reading and Writing Several Objects

#include <fstream>                                     // for all file I/O
using namespace std;

class MyClass
{
   private:
      // private data
   public:
      // public functions such as getData() and showData()
};

int main()
{ 
   char ch;
   MyClass myobj;

   ofstream fsobj("objfile.dat", ios::app|ios::out|ios::in|ios::binary);
      // Create file stream fsobj
      // ios::app will append new data to end of file
      // ios::out will open for output
      // ios::in  will open for input
      // Therefore, there is both read/write access

   do
   { 
      cout << "\nEnter data: ";
      myobj.getData();

      fsobj.write((char*)&myobj, sizeof(MyClass));     // write to file
      cout << Enter another?(y/n) ";

      cin >> ch;
   } while(ch=='y');

   fsobj.seekg(0);                                     // reset to start of file
   fsobj.read((char*)&myobj, sizeof(MyClass));         // read from file

   while(!fsobj.eof())                                 // quit on EOF
   { 
      myobj.showData();
      fsobj.read((char*)&myobj, sizeof(myclass));      // read another
   }
   
   fsobj.close();
   return 0;

} // end main

Notice the seekg; we will be discussing this in the section on randomly accessing data.

As a couple of comments:


Note on Reading and Writing Strings

If your class contains data (or fields) that are dynamic such as pointers to data and stl strings, then you can not write out the entire object in one step. You will have to write out the individual fields and also create a copy of whatever is pointed to.

If you do not do this, it will likely appear that your data is missing. Don't say that I didn't warn you!!!

The partial code solution below is taken from http://www.gamedev.net/community/forums/topic.asp?topic_id=104950:

#include <iostream>
#include <fstream>
#include <string>
using namespace std;


class CTest
{
   // Private class only variables, etc.
   private:
      int m_iStrlen;                   // Used to hold the length of the string
      string m_strName;                // String
  
   // . . . Nova's comment: code has been omitted here

   // Disk Saving/Loading
   public:
      void Save(string val);           // saves the name to a file
      void Load(string val);           // loads the name from a file

   // . . . Nova's comment: code has been omitted here
};

// . . . Nova's comment: code has been omitted here

void CTest::Save(string val)
{
   // No name set
   if (m_strName.length() <= 0 || m_strName.empty())
   {
        return;
    
   }//if

   // open file in output in binary mode
   ofstream f(val.c_str(), ios::out | ios::binary);
   
   // failed to open/create file
   if (f.fail())
   {
      return;
   }//if

   // First save the length of the string
   f.write((const char *)&m_iStrlen, sizeof(int));

   // Now save the string itself
   f.write(m_strName.c_str(), m_iStrlen);

   // Close the file, done with it
   f.close();

   return;
}

void CTest::Load(string val)
{
   // open file for binary input
   ifstream f(val.c_str(), ios::in | ios::binary);

   // Failed to open file
   if (f.fail())
   {
     return;
   }//if

   // First read the string length
   f.read((char*)&m_iStrlen, sizeof(int));

   // Resize (allocates) m_strName to fit the string to read
   // Nova's comment, this is required or else the size will be wrong
   m_strName.resize(m_iStrlen);

   // Read the string saved into m_strName
   // Nova's comment, this has been modified because you can not assume
   // the stl strings take up contiguous (one after the other) memory
   for (int i = 0; i < len; i++)
{ f.read((char *)(&m_strName[i]), sizeof(m_strName[i])); } // Close the file, done with it! f.close(); return; } // . . . Nova's comment: code has been omitted here

Another solution to the write is the following idea:

   len = str1.length();
   out.write((char *)(&len), sizeof(len));
   for (int i = 0; i < len; i++)
{ out.write((char *)(&str1[i]), sizeof(str1[i])); }

To summarize:

If you don't want to use these solutions with stl strings, then you always have the option of using C strings (in other words, arrays of characters). Think about this before doing this. What is the disadvantage of using C strings?


Randomly Accessing Data

Sometimes you want to jump to certain parts of a binary file. To do that the seek and tell functions, are useful. The following table summarizes which functions are useful for reading and writing.

  Set the File Pointer Examine the File Pointer
For Writing (put pointer) seekp() tellp()
For Reading (get pointer) seekg() tellg()

These functions work because every stream object has two integer values associated with it, the get pointer and the put pointer. These values specify the current position in the file (the byte number) where reading or writing will take place.

More to tell

The current position of the file pointer in a file can be obtained by calling tell. The following partial code illustrates the use of tellg()in conjunction with the seekg():

   //  mark the current position
   streampos mark = inFile.tellg();
         .
         .
         .
   if ( bSomeState ) //you want to return to the marked position
   {
      //  return to marked position
      inFile.seekg( mark );  // only one argument here
   }

Note:

tellg actually has great applications if you want to create a location table (array of streampos) of where your objects will be located in a file.

No Hiding with seek

As you might have gathered from the above example, seek allows you to jump to locations within a file. Consider how you can use it in conjunction with a location table to randomly access the data that you have stored in a file.

The formats for a seek are

ostream& seekp ( streampos pos );
ostream& seekp ( streamoff off, ios_base::seekdir dir );
istream& seekg ( streampos pos );
istream& seekg ( streamoff off, ios_base::seekdir dir );

where streamoff and streampos are integral types, but most likely long rather than int

seek usually takes two arguments. The first argument is a numeric value to adjust the file pointer, the second argument is one of:

  1. ios::beg -- the beginning of the file
  2. ios::cur -- the current position of the file pointer
  3. ios::end -- the end of the file

If the second argument is missing, then ios::beg is assumed. The following are some examples of using seek:

   // set put pointer 1 byte after beginning of file,
   // that is, to byte number 1
   outFile.seekp( 1, ios::beg );  
   // or equivalently:
   outFile.seekp( 1 );

   // set get pointer 20 bytes before current position
   inFile.seekg( -20, ios::cur );

   // set get pointer 25 bytes after current position
   inFile.seekg( 25, ios::cur );

   // set put pointer 35 bytes before end of file
   outFile.seekp( -35, ios::end );

If you have a class with no dynamic data (i.e. no stl strings), then you can move forward one MyClass object at a time using the current file position:

   inputFile.seekg( inputFile.tellg() + sizeof(MyClass) );

   //or, more efficiently
   inputFile.seekg( sizeof(MyClass), ios::cur );

Under the same conditions as above, if you want to access the beginning of the 25th MyClass object, you could write:

   inputFile.seekg( 24 * sizeof(MyClass), ios::cur );

References