Hello again! In this video, we are going to look at binary files. A binary file is much closer to
the computer than a text file, so we need to work with the computer. And computers are very literal.
You need to say exactly what you mean, and you need to mean exactly what you say! To open a binary file,
we use binary mode.
So we have seen this before.
We use fstream double colon binary. And then we have our open() call.
So this is going to open the image dot BMP file in binary. Or we could just pass that to the constructor.
We cannot use the shift operators for working with binary files.
They perform conversions between numeric data and text.
If we are doing output, they format the text data and, if we are doing input, they throw away whitespace
characters.
So the data in memory does not correspond to what is in the file.
So we need to always use the read and write operators when we are working with binary files.
If you look at a binary file, it is all 1's and 0's.
There is no obvious indication what all that data means.
So normally we use some kind of file format, to give the file a structure and make the data meaningful
to applications
that are going to work with it.
Often, we can use standard formats.
If we are creating an image, we could use JPEG.
If we are creating a compressed file, we can use ZIP. Or we can make our own formats, if the standard ones
do not provide what we need.
The best way of working with a binary file is to create a struct which represents the file format.
So each member of the structure represents one field in the file format. And then you can just grab this
struct, in memory, and write it straight to the file. And for going the other way, you can just take the
data out of the file and read it straight into your struct.
We are going to use this very simple struct, just to talk about it.
So this has a member which is a single character, and it has two members which are ints.
And here we run into the first problem with binary data.
The size of an int is dependent on the implementation.
If, for example, we write a binary file on a 32-bit system and read it on a 64-bit system, then the
data is going to be in the wrong place and we are going to get the wrong results.
So we need to make sure that all the integers have the same size on every system, so we use the fixed
size integers.
In this case, we are using 32-bit integers.
When we call read() or write(), the first argument is the address of the data. The start of the data.
So this is going to be the address of the point object.
We need to cast that 3to a pointer to char.
We could use the old C cast, which is just char star inside brackets.
But there is a specific cast for this in C++, which is reinterpret cast.
So this says we are throwing away all the type information and this is just binary data. All ones and zeros.
The argument to this is the address of the data, so that will be the address of the point object.
The second argument to read() or write() will be the number of bytes in the object.
So that is the size of the struct.
And then our calls are going to look like this.
So for write(), we are going to take the address of the point struct, and cast it, and pass the size
of the point.
And similarly for the read() operation.
So this will have data in the point and it is going to write it to the file.
This will read the data from the file and store it in the point object.
Before we rush off and start reading and writing binary files, there are some more problems we need to
think about.
We need to think about memory alignment and padding.
In modern computers, the memory circuits are optimized for accessing data, which is so called "word-
aligned".
So this means that each object in the data is at a multiple of the word size.
If we have a 32-bit system, for example, and then the first object is at, let's say, 1000 hex in memory,
then the next object must be at 4 or 8 or 16 or some multiple of 4, and so on.
And that data is word-aligned.
If the data is not word-aligned, then it is all over the place.
So the first one could be at 1000, the next one at 2 and so on.
And accessing memory this way [word-aligned] is much faster than accessing it that way [not word-aligned].
In fact, there are some systems on which you cannot actually access memory like that [not word-aligned].
So that is word alignment. We need to have the data on multiples of four.
So what happens if we have a struct which is not word-aligned? Like ours, which has a character followed
by an int.
In this case, the compiler will usually add extra bytes to make sure that the data is laid out at
multiples of four.
So in this case, the compiler is going to add three bytes on a 32-bit system.
So we have char, which is 1 byte, then we have these 3 bytes, so that makes 4.
So the int is going to be at a multiple of 4.
So if this is 1000, then this is going to be 4 bytes further. And then the int is 32 bits.
So that is four bytes again.
So the next one is four bytes further along.
So that is all correctly aligned.
Some people think they can be clever and use these padding points for their own nefarious purposes.
This is not a good idea.
This is not portable. If you have a different system,
For example, if this is 64-bits, then there are going to be 7 bytes here, not 3. To make it
8 bytes.
And then we will have some extra padding bytes here, which will not be on the 32-bit one.
And also, these are internal to the compiler. So it is possible that the compiler could do something
itself with these. In which case your code is going to conflict with it.
So, as far as you are concerned, these bytes are inaccessible. And they may not even exist!
If we have a file format which expects data fields to be at offsets which are not multiples of 4,
then we have a slight problem.
For example, the bitmap format is very old.
It's designed for 16-bit computers, so it has everything gets at 2-byte offsets.
If we just go ahead and write the file, then the compiler is going to add the padding bytes, which will
put everything on 4 bytes. And then the data will not match up with the bitmap format.
So if you generate a bitmap like this and then try to display it, the program will not understand
it. It will probably say that the data is corrupt, or it is not in the correct format.
So compilers provides a means for affecting the way that it pads the data.
There is a directive called hash pragma, which means everything after this is non-standard. So
we have the nonstandard instruction pack which will change the alignment.
And then we put push.
And then the second argument is the number of bytes.
So if we have 1 as the argument, then we have 1-byte alignment.
So in effect, we have no padding
at all. Sll the data just follows straight after the other.
If we put 2 in here, then we have 2-byte alignment, so the data elements will be at multiples
of 2.
And then when we have finished, when we get to the end of our struct, we need to reset back to the normal
alignment.
So we just do a pop operation.
If we do not do that, then all the code which follows will still be using the one byte alignment that
we set up here.
And if we call some library code, which assumes that we are using the standard alignment and we are using a different
one, then we could well have problems.
So this is non-standard, but it actually works with all the main compilers.
So it is a kind of "de facto" standard, if you like.
And the compilers also provide a compiler option you can use.
But this will set all the the code in the source file.
So that will be something on the command line or in the IDE, or you can add it to a Makefile.
And then finally, there is a standard way which do this in C++ 11, which is the alignas keyword.
So you can actually do this on a member by member basis. So you can say that you want this int to be on
a multiple of four bytes.
Unfortunately, this binary works for multiples of the word size. If you want to have an alignment
which is less than the word size, if you want 1 byte or 2 bytes, then you cannot do that.
The reason for this is that, this is standard.
It has to work on every computer. And there are some computers which do not support unaligned access.
Okay, so let's have an example of this.
We are using the fixed size integers, so we include the cstdint header. Here is our point structure.
We are going to try it first without the pragma's, to see what effect that has. In the main() function.
we are creating an object of this point structure.
So we have 'c' is the character
'a', x is 1 and 'y' is 2.
Then we open this file, file dot bin, in binary mode.
We check it is open and we do the write().
So we are casting the address of this object to a point to char, and we are passing the number of bytes
in this structure as the argument.
And then we close the file, because we are about to read from it and we want to make sure that all the
data gets written to disk.
Then we open the file, in binary mode again. We create another point object.
So we are going to read the data into this object.
Then we check the file is open.
We read the data into this object.
Then we close the file. Then we call the gcount() member of the stream to find out how much data we read and then
we print out what we got.
So let's see what happens.
So we read 12 bytes. And we get x equals 1, y equals 2.
If we look at this file that we have generated, we see we get 61. So that is the ASCII code for the letter
'a'. Then we get to these three padding bytes.
Then we get the value
1 for x, which is a 4 bytes int (If my mouse can reach that far!)
So this int is four bytes into the file, so it is on a multiple of four.
And then two for y.
And again, that is 8 bytes into the file. So
that is also a multiple of 4.
If we try it again with these pragmas...
So then we get 9 bytes.
So in this case, the ints follow directly after the character. There are not any padding points between
the character and the first int.
And if we look at the file we see - there it is.
There's the letter 'a'. And the value 1 follows straight after it.
So this is actually on offset 2.
There is no padding here.
OK, so that is it for this video.
I will see you next time.
Meanwhile, keep coding!