Binary IO

Binary I/O

Binary I/O in the .NET Framework uses the BinaryReader and BinaryWriter classes, which read and write .NET primitive types in binary format. As with the TextReader and TextWriter classes, the binary I/O classes use an underlying Stream object to provide a byte stream. Both BinaryReader and BinaryWriter have a BaseStream property that gives access to the underlying Stream.

The BinaryWriter Class

The following table lists the methods provided by BinaryWriter.




Closes the writer and the underlying stream


Releases all unmanaged resources used by the writer, and optionally releases managed resources as well


Causes all buffered data to be written to the underlying device


Sets the seek position within the underlying stream


Writes a value to the stream


Writes a 32-bit integer in a compressed format

If you look at the Visual Studio .NET documentation, you’ll see that the Write function has no fewer than 18 overloads for you to cope with when writing the various basic types provided by the .NET Framework. Because not all the types provided by .NET are compliant with the Common Language Specification (CLS), you need to be careful when using some of the Write methods if you intend the data to be read from code written in other .NET languages.


The CLS defines types that all .NET languages must support. The signed byte and unsigned integer types are not included in the CLS, so they might not be usable from some .NET languages. The most important of these is Microsoft Visual Basic .NET, which doesn’t support any of the non-CLS-compliant types.

The BinaryReader Class

The following table describes the functions provided by BinaryReader.




Closes the writer and the underlying stream


Releases all unmanaged resources used by the writer, and optionally releases managed resources as well


Fills the internal buffer with a number of bytes read from the underlying stream


Reads the next character but doesn’t advance the seek pointer


Reads one or more bytes or characters from the stream


Reads a 32-bit integer that was written in a compressed format


Reads a Boolean from the stream

ReadByte, ReadBytes

Reads one or more bytes from the stream

ReadChar, ReadChars

Reads one or more characters from the stream


Reads a decimal value from the stream

ReadDouble, ReadSingle

Reads a double or single precision floating point value from the stream

ReadInt16, ReadInt32, ReadInt64

Reads an integer type from the stream


Reads a signed byte from the stream; not CLS-


Reads a string from the stream

ReadUInt16, ReadUInt32, ReadUInt64

Reads an unsigned integer type from the stream; not CLS-compliant

Unlike BinaryWriter, BinaryReader provides separate functions to read each of the basic types.

The exercise that follows shows you how to use the BinaryReader and BinaryWriter classes to write binary data to a file and read it back. It uses a class, Customer, which represents a bank customer who has a name, an account number, and a current balance. The program writes customer details to a file in binary and reads them back.

  1. Create a new Visual C++ Console Application (.NET) project named CppBinRead.

  2. Add the using declaration for System::IO to the start of the code, like this:

    using namespace System::IO;
  3. Add a new class definition before the _tmain function.

    // The Customer class __gc class Customer { String* name; long accNo; double balance; public: // Constructors Customer() : name(0), accNo(0), balance(0.0) {} Customer(String* s, long l, double b) : name(s), accNo(l), balance(b) {} // Write object data to a BinaryWriter void Write(BinaryWriter* bw) { bw->Write(name); bw->Write(accNo); bw->Write(balance); } // Read object data from a BinaryReader void Read(BinaryReader* br) { name = br->ReadString(); accNo = br->ReadInt32(); balance = br->ReadDouble(); } // Properties to retrieve the instance variables __property String* get_Name() { return name; } __property long get_Account() { return accNo; } __property double get_Balance() { return balance; } };

    The class has three data members: a String for the name, a long for the account number, and a double for the balance. There are constructors to create default and fully populated objects, and there’s a set of read-only properties to allow access to the data members.

    The Read and Write functions use BinaryReader and BinaryWriter objects to read and write the state of the object in binary format.

  4. Edit the _tmain function so that it uses the command-line argument parameters, as follows:

    int _tmain(int argc, char* argv[])
  5. Add the following code to _tmain to check that the user passes in a file name and save the path as a String:

    // Check for required arguments if (argc < 2) { Console::WriteLine(S"Usage: CppBinRead path"); return -1; } // Save the path String* path = new String(argv[1]);

    This code is very similar to the argument-handling code that has been used in other exercises in this chapter. Note that for simplicity I’m not checking the path for validity, but it’s easy—and advisable—to add such a check in a real application.

  6. Create some Customer objects.

    // Create some customers Customer* c1 = new Customer(S"Fred Smith", 1234567, 100.0); Customer* c2 = new Customer(S"Bill Jones", 2345678, 1000.0); Customer* c3 = new Customer(S"Dave Davies", 3456789, 5000.0);

  7. To write the objects, you need a BinaryWriter and a FileStream to do the output to the file.

    try { // Create a FileStream FileStream* fstrm = new FileStream(path, FileMode::Create, FileAccess::ReadWrite); // Create a BinaryWriter to use the FileStream BinaryWriter* binw = new BinaryWriter(fstrm); } catch(System::Exception* pe) { Console::WriteLine(pe->ToString()); }

    The FileStream will write to a file, creating it if necessary, and the file will be opened with read/write access because you’ll be reading from it later in the program. Once again, it’s good practice to put the I/O class creation code in a try block to catch any problems that might occur.

  8. Writing the object data to the file is simply a case of calling the Write function, passing in a pointer to the BinaryWriter. Add the following code at the end of the try block:

    // Write the customters to the file c1->Write(binw); c2->Write(binw); c3->Write(binw);
  9. Because the file was opened with read/write access, you can now read from the file. To do so, create a BinaryReader object and attach it to the same FileStream, as shown here:

    // Create a BinaryReader that reads from the same FileStream BinaryReader* binr = new BinaryReader(fstrm);
  10. Before you can read from a file that you’ve written to, you have to move the position of the seek pointer.

    // Move back to the beginning binr->BaseStream->Seek(0, SeekOrigin::Begin);

    Notice that this code uses the BaseStream property and its associated seek pointer to get at the underlying Stream object. If you haven’t met seek pointers before, see the explanation in the following sidebar.

    start sidebar
    Streams and Seek Pointers

    Every stream in .NET has a seek pointer associated with it, which represents the position in the stream at which the next read or write operation will take place. This pointer is automatically repositioned when you use Stream class methods to read or write the stream, but it’s also possible to move this pointer yourself if you need to (and if you know what you’re doing).

    The most likely time you’ll need to move the pointer is when you open a stream for read/write access. Once you’ve written to the stream, the seek pointer will be positioned at the end, ready for the next write. If you want to read from the stream, you’ll have to reposition the pointer.

    You reposition the pointer by using the Seek method of the Stream object, giving it an offset in bytes and a position where the offset should be applied. Offsets can be positive or negative, the sign reflecting whether the offset should move toward the start (negative) or end (positive) of the stream. The possible positions are members of the SeekOrigin enumeration, and they can be SeekOrigin::Current (the current position), SeekOrigin::Begin (the start of the Stream), or SeekOrigin::End (the end of the Stream).

    end sidebar

  11. Create a new Customer, and read its details from the file, as follows:

    // Create a new customer, and read details from the file Customer* c4 = new Customer(); c4->Read(binr); Console::WriteLine(S"Balance for {0} (a/c {1}) is {2}", c4->Name, __box(c4->Account), __box(c4->Balance));

    The new Customer object has all its fields set to default values. The call to Read tells it to read its data from the current position in the file.

    The obvious potential problem is that the Read function will read from wherever the BinaryReader is currently positioned. If it isn’t at the beginning of a Customer object’s data, you can expect to get an exception thrown.


    If you want to save the state of objects in a real-world program, you wouldn’t do it manually like this. The System::Runtime::Serialization namespace contains classes that help you save and restore the state of objects in an efficient way.

  12. Build and run the application, providing a suitable file name.

Microsoft Visual C++  .NET(c) Step by Step
Microsoft Visual C++ .NET(c) Step by Step
ISBN: 735615675
Year: 2003
Pages: 208 © 2008-2017.
If you may any questions please contact us: