So, before I give you a solution, let's briefly talk about what is going on here:
ofs.write((char*)&t1,sizeof(t1));
What you are doing is casting t1 to a pointer to char, and saying 'write to ofs the memory representation of t1, as is'. So we have to ask ourselves: what is this memory representation of t1?
- You are storing a (implementation defined, most probably 4 byte) integer
- You are also storing a complex std::string object.
Writing the 4 byte integer might be OK. It is definitely not portable (big-endian vs little endian), and you might end up with the wrong int if the file is read on a platform with different endianness.
Writing the std::string
is definitely not OK. Strings are complex object, and they most often allocate storage on the heap (although there is such a thing as small string optimization). What this means is that you're going to serialize a pointer to a dynamically allocated object. This will never work, as reading the pointer back would point to some location in memory that you have absolutely no control of. This is a great example of undefined behavior. Anything goes, and anything might happen with your program, including 'appearing to work correct' despite deeply seated problems.
In your specific example, because the Telephone object that was created is still in memory, what you get is 2 pointers to the same dynamically allocated memory. When your temp
object goes out of scope, it deletes that memory.
When you return to your main function, when t1
goes out of scope, it tries to delete the same memory again.
Serializing any kind of pointers is a big no-no. If your object internals consist of pointers, you need to make a custom solution of how those pointers will be stored in your stream, and later read to construct a new object. A common solution is to store them 'as if' they were stored by value, and later, when reading the object from storage, allocate memory dynamically and put the contents of the object in the same memory. This will obviously not work if you are trying to serialize the case where multiple objects point to the same address in memory: if you try to apply this solution, you would end up with multiple copies of your original object.
Fortunately, for the case of a std::string
this problem is easily solved, as strings have overloaded operator<<
and operator>>
, and you don't need to implement anything to make them work.
edit: Just using operator<<
and operator>>
won't work for std::string
, explained a bit later why.
How to make it work:
There are many possible solutions, and I'm going to share one here.
The basic idea is that you should serialize every member of your Telephone structure individually, and rely on the fact that every member know how to serialize itself. I am going to ignore the problem of cross-endianness compatibility, to make the answer a bit briefer, but if you care about cross platform compatibility, you should think about it.
My basic approach is to override operator<<
and operator>>
for the class telephone.
I declare two free functions, that are friends of the Telephone class. This would allow them to poke at the internals of different telephone objects to serialize their members.
class Telephone {
friend ostream& operator<<(ostream& os, const Telephone& telephone);
friend istream& operator>>(istream& is, Telephone& telephone);
// ...
};
edit: I initially had the code for serializing the strings wrong, so my comment that it's fairly straightforward is plain-out wrong
The code for implementing the functions has a surprising twist. Because operator>>
for strings stops reading from the stream when encountering a whitespace, having a name that is not a single word, or with special characters would not work, and put the stream in a state of error, failing to read the phone number. To go around the problem, i followed the example by @Michael Veksler and stored the length of the string explicitly. My implementation looks as follows:
ostream& operator<<(ostream& os, const Telephone& telephone)
{
const size_t nameSize = telephone.name.size();
os << nameSize;
os.write(telephone.name.data(), nameSize);
os << telephone.phno;
return os;
}
istream& operator>>(istream& is, Telephone& telephone)
{
size_t nameSize = 0;
is >> nameSize;
telephone.name.resize(nameSize);
is.read(&telephone.name[0], nameSize);
is >> telephone.phno;
return is;
}
Please note, that you must make sure that the data you write matches the data you're going to later try to read. If you store a different amount of information, or the arguments are in the wrong order, you will not end up with a valid object. If you later do any kind of modifications to the Telephone class, by adding new fields that you would want saved, you'll need to modify both functions.
To support names with spaces in them, the way you read the names from cin should be modified as well. One way would be to use std::getline(std::cin, name);
instead of cin >> name
Finally, how you should serialize and deserialize from those streams:
Don't use the ostream::write()
and istream::read()
functions - use instead the operator<<
and operator>>
that we have overriden.
void getData() {
Telephone temp;
ifstream ifs("Sample.txt",ios::in|ios::binary);
ifs >> temp;
temp.displayData();
}
void storeData(const Telephone& telephone) {
ofstream ofs("Sample.txt",ios::out|ios::binary);
ofs << telephone;
}