How to Read/Write Std::String Values From/To Binary Files

How to read/write std::string values from/to binary files

The two lines:

outf.write( reinterpret_cast<char *>( &temp ), sizeof( Item ) );

and

inf.read( reinterpret_cast<char *>( &temp ), sizeof( Item ) );

are wrong. You are writing the binary layout of the object, including std::string instances. This means you are writing the value of pointers to a file and reading them back.

You cannot simply read pointers from a file and assume they point to valid memory, especially if it was held by a temporary std::string instance, which should have freed the memory in it's destructor when it went out of scope. I'm surprised you got this to run "correctly" with any compiler.

Your program should write the content and read it back using your operator<< and operator>> methods. It should look like the following:

void write_to_file( const string& fn, const Item& item )
{
fstream outf( fn.c_str(), ios::binary | ios::out );
outf << item << std::endl;
outf.close();
}

void read_from_file( const string& fn, Item& item )
{
fstream inf( fn.c_str(), ios::binary | ios::in );
if( !inf )
{
cout << "What's wrong?";
}
inf >> item;
inf.close();
}

BONUS: There are a few quirks with your code.

This statement is, thankfully, presently unused in your program (the method is not called).

return ( string& )"";

It is invalid because you will be returning a reference to a temporary string object. Remeber that a string literal "" is not a std::string object and you can't get a reference to it of type std::string&. You should probably raise an exception, but you could get away with:

string& operator []( int x )
{
static string unknown;
if ( 0 == x )
return itemID;
if ( 1 == x )
return itemName;
if ( 2 == x )
return itemState;
return unkonwn;
}

This is a poor solution given that the string is returned by reference. It can be modified by the caller, so it might not always return the "" value you thought it would. However, it will remove undefined behavior for your program.

The .close() method invocations on std::fstream objects are not necessary, the destructor calls it automagically when the object goes out of scope. Inserting the call there is extra clutter.

Also, what's with the redundant naming convention?

class Item
{
private:
string itemID;
string itemName;
string itemState;
// ...
};

What's wrong with calling them ID, name and state?

c++ - properly writing std::string to binary file

Your code can be written as

ofstream file1("lastServers.bin", ios::out | ios::binary);
if (file1.good()) {
file1.write(ip.c_str(), ip.size());
file1.write(port.c_str(), port.size());
file1.close();
}
else {
std::cout << "file error write" << endl;
}

string::c_str() returns a const pointer to the text in the string.
string::size() returns the number of characters in the string.

You don't need to concatenate the data before writing to the file, writing one then the other has the same result.

If you wanted to write C type code rather than C++, you can use strlen(p_IP) to get the length of the IP string rather than using sizeof.

The sizeof operator gives you the size of the class instance, i.e. the size of the object BUT the string object's size is never affected by the size of the string it manages.

In C++, objects that manage something (think strings managing characters, containers managing their contents, etc.) usually have a method to determine the size of what they're managing. For std::string and other STL containers that method is size().

Note that writing these strings in this format means you can't tell where one string ends and another one starts. Two options to consider are using a terminating character that you know won't appear in any strings, or writing the length of the string to the file before the text of the string itself. I won't elaborate here as it was not asked in the original question.

Reading and write strings in binary files c++

You've not sufficiently specified the format of the binary file.
How do you represent an int (how many bytes, big-endian or
little-endian), nor the encoding and the format of the
characters. The classical network representation would be
a big-endian four byte (unsigned) integer, and UTF-8. Since
this is something you're doing for your self, you can (and
probably should) simplify, using little-endian for integer, and
UTF-16LE; these formats correspond to the internal format under
Windows
. (Note that such code will not be portable, not even
to Apple or Linux on the same architecture, and the there is
a small chance that the data become unreadable on a new system.)
This is basically what you seem to be attempting, but...

You're trying to write raw binary. The only standard way to do
this would be to use std::ofstream (and std::ifstream to
read), with the file opened in binary mode and imbued with the
"C" locale. For anything else, there will (or may) be some
sort of code translation and mapping in the std::filebuf.
Given this (and the fact that this way of writing data is not
portable to any other system), you may want to just use the
system level functions: CreateFile to open, WriteFile and
ReadFile to write and read, and CloseHandle to close. (See
http://msdn.microsoft.com/en-us/library/windows/desktop/aa364232%28v=vs.85%29.aspx).

If you want to be portable, on the other hand, I would recommend
using the standard network format for the data. Format it into
a buffer (std::vector<char>), and write that; at the other
end, read into a buffer, and parse that. The read and write
routines for an integer (actually an unsigned integer) might be
something like:

void
writeUnsignedInt( std::vector<char>& buffer, unsigned int i )
{
buffer.push_back( (i >> 24) & oxFF );
buffer.push_back( (i >> 16) & oxFF );
buffer.push_back( (i >> 8) & oxFF );
buffer.push_back( (i ) & oxFF );
}

unsigned int
readUnsignedInt(
std::vector<char>::const_iterator& current,
std::vector<char>::const_iterator end )
{
unsigned int retval = 0;
int shift = 32;
while ( shift != 0 && current != end ) {
shift -= 8;
retval |= static_cast<unsigned char>( *current ) << shift;
++ current;
}
if ( shift != 0 ) {
throw std::runtime_error( "Unexpected end of file" );
}
return retval;
}

For the characters, you'll have to convert your std::wstring to
std::string in UTF-8, using one of the many conversion routines
available on the network. (The problem is that the encoding of
std::wstring, nor even the size of a wchar_t, is not
standardized. Of the systems I'm familiarized, Windows and AIX
use UTF-16, most others UTF-32; in both cases with the byte
order dependent on the platform. This makes portable code a bit
more difficult.)

Globally, I find it easier to just do everything directly in
UTF-8, using char. This won't work with the Windows
interface, however.

And finally, you don't need the trailing '\0' if you output
the length.

Writing/Reading strings in binary file-C++

When reading the data back in you should do something like the following:

int result;
file.read(reinterpret_cast<char*>(&result), sizeof(int));

This will read the bytes straight into the memory of result with no implicit conversion to int. This will restore the exact binary pattern written to the file in the first place and thus your original int value.

How to store class object having string in binary file?

I would introduce a new level of indirection, i.e. functions from_binary and to_binary, and implement your Load and Store in terms of those:

template <class C>
bool Load(const char fileName[], C& obj) {
if (ifstream in{fileName, ios::in | ios::binary}) {
from_binary(in, obj);
return true;
}

return false;
}

template <class T>
bool Save(const char fileName[], T obj) {
if (ofstream out{fileName, ios::out | ios::binary}) {
to_binary(out, obj);
return true;
}

return false;
}

For POD data types, from_binary and to_binary will just do what you already did in Load/Store (beware, however: pointers are PODs but saving an address is pretty much meaningless):

template <class T, typename = enable_if_t<is_pod_v<T>>>
void from_binary(ifstream& in, T& obj) {
in.read(reinterpret_cast<char*>(addressof(obj)), sizeof(obj));
}

template <class T, typename = enable_if_t<is_pod_v<T>>>
void to_binary(ofstream& out, T const& obj) {
out.write(reinterpret_cast<char const*>(addressof(obj)), sizeof(obj));
}

As pointed out in the comments, std::string is not a POD type. I'm going to serialize it by saving the character count and then the actual characters:

void from_binary(ifstream& in, string& str) {
std::size_t stringSize{0};
from_binary(in, stringSize);

str.reserve(stringSize);
for (size_t i = 0; i != stringSize; ++i) {
char ch{};
in.read(&ch, 1);
str.push_back(ch);
}
}

void to_binary(ofstream& out, string const& str) {
auto const stringSize = str.size();
to_binary(out, stringSize);

auto const* cStr = str.c_str();
out.write(cStr, stringSize);
}

Also, I'm going to serialize/deserialize an array by calling to_binary/from_binary on each element of the array:

template <class T, size_t N>
void from_binary(ifstream& in, T (&obj)[N]) {
for (auto& elem : obj) from_binary(in, elem);
}

template <class T, size_t N>
void to_binary(ofstream& out, T const (&obj)[N]) {
for (auto const& elem : obj) to_binary(out, elem);
}

The above functions are enough to implement from_binary and to_binary for your Contact and Data classes:

#include <fstream>
#include <iostream>
#include <string>

using namespace std;

template <class T, typename = enable_if_t<is_pod_v<T>>>
void from_binary(ifstream& in, T& obj) {
in.read(reinterpret_cast<char*>(addressof(obj)), sizeof(obj));
}

template <class T, typename = enable_if_t<is_pod_v<T>>>
void to_binary(ofstream& out, T const& obj) {
out.write(reinterpret_cast<char const*>(addressof(obj)), sizeof(obj));
}

void from_binary(ifstream& in, string& str) {
std::size_t stringSize{0};
from_binary(in, stringSize);

str.reserve(stringSize);
for (size_t i = 0; i != stringSize; ++i) {
char ch{};
in.read(&ch, 1);
str.push_back(ch);
}
}

void to_binary(ofstream& out, string const& str) {
auto const stringSize = str.size();
to_binary(out, stringSize);

auto const* cStr = str.c_str();
out.write(cStr, stringSize);
}

template <class T, size_t N>
void from_binary(ifstream& in, T (&obj)[N]) {
for (auto& elem : obj) from_binary(in, elem);
}

template <class T, size_t N>
void to_binary(ofstream& out, T const (&obj)[N]) {
for (auto const& elem : obj) to_binary(out, elem);
}

template <class C>
bool Load(const char fileName[], C& obj) {
if (ifstream in{fileName, ios::in | ios::binary}) {
from_binary(in, obj);
return true;
}

return false;
}

template <class T>
bool Save(const char fileName[], T obj) {
if (ofstream out{fileName, ios::out | ios::binary}) {
to_binary(out, obj);
return true;
}

return false;
}

class Contact {
public:
int CompareTo(Contact obj) { return 1; }
string ss;
int rollNum;
};

void from_binary(ifstream& in, Contact& obj) {
from_binary(in, obj.ss);
from_binary(in, obj.rollNum);
}

void to_binary(ofstream& out, Contact const& obj) {
to_binary(out, obj.ss);
to_binary(out, obj.rollNum);
}

class Data {
public:
Data() {}
Contact arr[10];
};

void from_binary(ifstream& in, Data& obj) { from_binary(in, obj.arr); }

void to_binary(ofstream& out, Data const& obj) { to_binary(out, obj.arr); }

int main() {
const char fileName[] = "ContactMG.dat";

{
Data data;

auto const contactCount = sizeof(data.arr) / sizeof(data.arr[0]);
for (size_t c = 0; c != contactCount; ++c) {
data.arr[c].ss = "some name " + to_string(c);
data.arr[c].rollNum = c;
}

Save(fileName, data);
}

{
Data data;
Load(fileName, data);

for (auto const& contact : data.arr)
cout << "Contact: rollNum=" << contact.rollNum
<< ", ss=" << contact.ss << '\n';
}
}

Output:

Contact: rollNum=0, ss=some name 0
Contact: rollNum=1, ss=some name 1
Contact: rollNum=2, ss=some name 2
Contact: rollNum=3, ss=some name 3
Contact: rollNum=4, ss=some name 4
Contact: rollNum=5, ss=some name 5
Contact: rollNum=6, ss=some name 6
Contact: rollNum=7, ss=some name 7
Contact: rollNum=8, ss=some name 8
Contact: rollNum=9, ss=some name 9

Although this may solve your particular issue, the number of overloads you'll need for from_binary and to_binary will grow very rapidly as your project grows. So I'd definetely check if there's a more comprehensive (and well tested) library solution out there.

How to read/write string type member of a struct using binary file in/out in c++?

The only way that comes to mind is to write the following data separately:

  1. Length of the string.
  2. The array of characters of the string.
  3. The age.

and read them separately.

Create functions to write/read an instance of Data such that they are aware of each other's implementation strategy.

std::ostream& write(std::ostream& out, Data const& data)
{
size_t len = data.name.size();
out.write(reinterpret_cast<char const*>(&len), sizeof(len));
out.write(data.name.c_str(), len);
out.write(reinterpret_cast<char const*>(&data.age));
return out;
}

std::istream& read(std::istream& in, Data& data)
{
size_t len;
in.read(reinterpret_cast<char*>(&len), sizeof(len));

char* name = new char[len+1];
in.read(name, len);
name[len] = '\0';
data.name = name;
delete [] name;

in.read(reinterpret_cast<char*>(&data.age));
return in;
}

and use them similarly to your first approach.

Instead of using

people.write(reinterpret_cast<char *>(&person),sizeof(person));

use

write(people, person);

Instead of using

people.read(reinterpret_cast<char *>(&person),sizeof(person));

use

read(people, person);

Reading std::string from binary file

Biggest major problem that jumped out at me:

// Create a char pointer for temporary storage.
char* text = new char;
// ...
// Read [size] number of characters from the string and store them in text.
fread(text, 1, size, t_fp);

This creates text as a pointer to a single character, and then you try to read an arbitrary number of characters (potentially many more than one) into it. In order for this to work right, you would have to create text as an array of characters after you figured out what the size was, like this:

// UInt for storing the string's size.
unsigned int size;
// Read the size of the string from the file and store it in size.
fread(&size, sizeof(unsigned int), 1, t_fp);
// Create a char pointer for temporary storage.
char* text = new char[size];
// Read [size] number of characters from the string and store them in text.
fread(text, 1, size, t_fp);

Second, you don't free the memory that you allocated to text. You need to do that:

// Free the temporary storage
delete[] text;

Finally, is there a good reason why you are choosing to use C file I/O in C++? Using C++-style iostreams would have alleviated all of this and made your code much, much shorter and more readable.



Related Topics



Leave a reply



Submit