Reading and Writing an array to a binary file

I've been stuck on this for a few nights now and although I have found some examples for pointer arrays and single objects, I have not found a solution.

I have a larger assignment that needs to be able to read and write from a binary file, however, as I read from what I think is written correctly I either receive garbage (perhaps not written correctly) or segfault.

Oddly enough, the "record count" routine I modified seems to return the correct amount after the .dat file is initialized, so my assumption is I'm reading it back in incorrectly

Any pointers, no pun intended, would be appreciated:

main.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <iostream>
#include <fstream>
#include <string>
#include "person.h"
using namespace std;

int recordCount();

int main(int argc, char *argv[])
{
  int ARRAY_SIZE = 200;
  Person pers[ARRAY_SIZE];

  cout << "Records: " << recordCount() << endl;

  // load from disk?
  int records = 0;
  cout << "Loading from disk?" << endl;
  fstream file;
  file.open("people.dat", ios::in | ios::binary);
  if(!file)
  {
    // file does not exist, init a file
    cout << "Data people.dat is missing" << endl;
    // get 5 Persons
    for(int i = 0; i < 2; i++)
    {
      pers[i].getData();
    }

    cout << "Writing file" << endl;
    file.open("people.dat", ios::out | ios::binary);
    for(int i = 0; i < ARRAY_SIZE; i++)
    {
        file.write( (char*)(&pers[i]), sizeof(Person));
    }
    file.close();
    cout << "Done initializing .dat file, please re-run the application." << endl;
    return 0;
  }

  Person p;
  file.read( (char*)(&p), sizeof(Person) );
  while(!file.eof())
  {
    cout << endl << records << ") PERSON LOADED." << endl;
    p.showData();
    pers[records] = p;
    file.read( (char*)(&p), sizeof(Person) );
    records++;
  }
  file.close();

  // echo 5 Persons
  cout << endl << endl << "****************" << endl;
  for(int i = 0; i < 5; i++)
  {
    pers[i].showData();
  }

  return 0;
}

// **************************************************
// **************************************************
int recordCount() {
  ifstream ifile;
  ifile.open("people.dat", ios::binary);
  if(ifile) {
    ifile.seekg(0, ios::end); // go to end
    return (int)ifile.tellg() / sizeof(Person);
  }
  else return 0;
}


Person.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef PERSON_H
#define PERSON_H

#include <string>
using namespace std;

class Person
{
protected:
  string name;
  int age;
public:
    Person();
    void getData();
    void showData();
};

#endif // PERSON_H 


Person.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include "person.h"

using namespace std;

Person::Person()
{
}

void Person::getData()
{
  cout << "\nEnter name: "; cin >> name;
  cout << "Enter age: "; cin >> age;

  showData();
}
void Person::showData()
{
  cout << "\nName: " << name;
  cout << "\nAge: " << age;
}
You shouldn't do this file.write( (char*)(&pers[i]), sizeof(Person)); for two reasons.

1. If you switch the declaration order of name and age in your Person class, it will be written/read from the file differently, and
2. sizeof(Person) and sizeof(std::string) return 36 bytes and 32 bytes, respectively (on my computer). What if a Person has a name that takes up more than 32 bytes? 36 bytes will still (incorrectly) be written to the file! You can only trust sizeof(Person) if your Person class contains plain old data.

Instead, to write to the file, what I would do is:

1. Write the length of the name (excluding '\0') as an int (so you know how many chars to read)
2. Write the char* contained in the name
3. Write the age as an int

Then, to read:

1. Read the length of the name
2. Read the char* (with the length read above). Put it into a std::string
3. Read the age
4. Assign the name and age to a Person instance with a Set method
Is this because string can be of any size (ie grows)?

It's apparently working great with the following modifications to Person, namely changing the string to char[50] then setting it to '' in the constructor so when I write out 200, some don't get garbage.

Person.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef PERSON_H
#define PERSON_H

class Person
{
protected:
  char name[50];
  int age;
public:
    Person();
    void getData();
    void showData();
};

#endif // PERSON_H 


Person.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include "person.h"

using namespace std;

Person::Person()
{
    // init the variables as not to get
    // garbage when reading unset Persons
    strcpy(name, "");
    age = 0;
}

void Person::getData()
{
  cout << "\nEnter name: "; cin >> name;
  cout << "Enter age: "; cin >> age;

  showData();
}
void Person::showData()
{
  cout << "\nName: " << name;
  cout << "\nAge: " << age;
}
Is this because string can be of any size (ie grows)?

Yes, you can't trust sizeof (in this context) because of this. Apparently, on my computer the internals of std::string add up to give it a size of 32 bytes. But, no matter the length of the string, it always gives 32 bytes. Also, file.write was probably just writing the internals of the string, and thus just a pointer to the first character when it came to write the actual char*. Since that pointer is no longer valid upon reading back from the file, you got garbage.

It works fine with your current implementation because now you've got plain old data as its members (and a const limit on the length of the char array, name). Again, if you change the order of the member variables, your code won't be able to read from a file that was written to with a different ordering (or when run on a computer with a different endian scheme).
Last edited on
Topic archived. No new replies allowed.