Toon posts:

[C++] String in binair opgeslagen klasse tonen.

Pagina: 1
Acties:

Verwijderd

Topicstarter
Ik heb laatst een C++ boek gekocht, doorgelezen, en veel van geleerd. Nu wilde ik voor mezelf een programmaatje schrijven, met o.a. het binair opslaan van klassen.
Nu heb ik een klasse met een string, die ik binair wil opslaan. De string van de klasse wordt echter niet getoond na een aanroep daartoe, gegeven na het halen van de klasse uit de string. Andere gegevens werken wel.
Voor een demonstratie volgt hier een aangepast stukje code uit het boek, met strings ipv cstrings (en ios_base::binary weggelaten, hoeft niet met linux). (overigens is niet uitgelegd wat de reinterpret_cast<char *> hier precies doet, dat snap ik ook niet helemaal dus wellicht dat ik daar wat fout doe):

Writeobj.cpp (schrijft klasse naar bestand.dat):
code:
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
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

const int AANTAL_OBJECTEN = 15;

class notitie
{
public:
    string naam;
    float schuld;
    void toon() {cout << naam << " moet nog " << schuld << endl;}
};

int main()
{
    notitie object_tabel[AANTAL_OBJECTEN];
    ofstream bestand("bestand.dat");

    if (!bestand.is_open()) {
        cerr << "Fout: Bestand niet kunnen aanmaken." << endl;
        exit(1);
    }

   object_tabel[0].naam = "Rudi Claes";
   object_tabel[0].schuld = 0.01;
   
   object_tabel[1].naam = "Jan Nathan";
   object_tabel[1].schuld = 312.59;
   
    bestand.write(reinterpret_cast<char *> (object_tabel), 
        AANTAL_OBJECTEN * sizeof(notitie));

    bestand.close();

    return 0;
}


Readobj.cpp (leest het weer uit):
code:
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
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

const int AANTAL_OBJECTEN = 15;

class notitie
{
public:
    string naam;
    float schuld;
    void toon() {cout << naam << " moet nog " << schuld << endl;}
};

int main()
{
    notitie object_tabel[AANTAL_OBJECTEN];
    ifstream invoerstroom("bestand.dat");

    if (!invoerstroom.is_open()) {
        cerr << "Fout: Bestand niet kunnen openen." << endl;
        exit(1);
    }

    invoerstroom.read(reinterpret_cast<char *> (object_tabel), 
        AANTAL_OBJECTEN * sizeof(notitie));

    object_tabel[0].toon();
    object_tabel[1].toon();

    invoerstroom.close();

    return 0;
}

Compiled perfect met g++.
Geen output bij writeobj.cpp (natuurlijk)
en readobj.cpp doet dit:

faberic@faberic:~/C++$ ./read
moet nog 0.01
moet nog 312.59

hij toont dus geen strings. Vervang je de strings door een cstring (en de initialisatie door een strcpy) dan werkt het wel.

[ Voor 15% gewijzigd door Verwijderd op 25-07-2005 19:19 ]


  • .oisyn
  • Registratie: September 2000
  • Laatst online: 18:07

.oisyn

Moderator Devschuur®

Demotivational Speaker

Ik denk dat je óf dat boek nog een keer goed moet doorlezen, óf een ander boek moet kopen. Je schrijft een string binair weg, maar in die string staat weer gewoon een pointer naar een stuk geheugen. Pointers opslaan is leuk, maar je hebt er nogal weinig aan als je de data waar de pointer naar wijst er niet al staat bij het uitlezen. En in die data ben je nou juist geinteresseerd, maar ja, die sla je weer niet op ;)

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


Verwijderd

Topicstarter
da wist ik niet. (geen idee hoe die klasse string in elkaar zit). Wat is een oplossing hiervoor? (en dan om toch strings te gebruiken)

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

waarschijnlijk bevat die string intern een pointer wat er dus voor zorgt dat op een 32bit machine de grootte van jouw bestand:
(4 + 4) * 15 = 120bytes is (controleer!)

wat je moet doen is op een of andere manier ervoor zorgen dat de INHOUD van je string uitgeschreven wordt. wat jij probeert heet ook wel (de-)serializen.

een gemakkelijke manier om dit te doen is met de << en >> operator's en overloading

ASSUME makes an ASS out of U and ME


Verwijderd

Topicstarter
die grootte klopt. Dus met << moet ik er voor zorgen dat die data naar iets links daarvan vliegt. (een cstring? maar dat kan ook met string.c_str() toch? of een bestand, maar dat wordt weer ingewikkeld want die string zit in een klasse.)

[ Voor 24% gewijzigd door Verwijderd op 25-07-2005 19:26 ]


  • .oisyn
  • Registratie: September 2000
  • Laatst online: 18:07

.oisyn

Moderator Devschuur®

Demotivational Speaker

Verwijderd schreef op maandag 25 juli 2005 @ 19:22:
da wist ik niet. (geen idee hoe die klasse string in elkaar zit).
Exact, en daarom kun je er simpelweg niet vanuit gaan dat je een klasse zomaar naar disk kunt schrijven en weer op kunt halen :). Doe dat zoals gezegd altijd door de inhoud op te vragen en dat weg te schrijven.

Overigens heb je je bestand niet als binary geopend maar als tekst, kans is dus groot dat het alsnog fout gaat. Aan de << en >> operatoren van een stream heb je weinig, die zijn bedoeld om een textuele representatie van het type weg te schrijven en op te halen, maar je wilt binair lezen en schrijven.

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


Verwijderd

Topicstarter
Neej dat gaat niet fout (Heb linux, daar is ios_base::binary niet nodig, integendeel)

  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 09-04 22:08
"integendeel" is weer wat overdreven, maar op een gangbare combinatie Linux/x86/ext2 is het niet nodig nee. Ga je daarentegen een native IBM mainframe mounten op Linux/S390 dan lijkt het me weer handig.

Om het antwoorde te geven op je impliciete vraag: reinterpret_cast< > betekent zoveel als "compiler, niet zeuren. Ik weet beter dan jij wat ik aan het doe ben.". Lijkt me logisch dat dat niet geld voor beginners ;) Voor std::string is kun je de (binaire) contents inderdaad gewoon met c_str() wegschrijven. Dat geeft je een const char*. Lezen is een groter probleem, want het is pointer naar const char's. Read-only does, en Linux wil dat bij gelegenheden wel eens met harde hand afdwingen (hangt van compilerdetails af, maar typische fout op Linux is een Segmentation Fault.)

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein


  • .oisyn
  • Registratie: September 2000
  • Laatst online: 18:07

.oisyn

Moderator Devschuur®

Demotivational Speaker

Verwijderd schreef op maandag 25 juli 2005 @ 19:39:
Neej dat gaat niet fout (Heb linux, daar is ios_base::binary niet nodig, integendeel)
Totdat je een streambuf gebruikt waarbij het wel uitmaakt. De opties zijn er niet voor niets, dat het op het door jouw gebruikte OS "toevallig" goed gaat betekent nog niet dat het dan ook degelijke code is.

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


Verwijderd

Topicstarter
MSalters schreef op maandag 25 juli 2005 @ 20:07:
"integendeel" is weer wat overdreven, maar op een gangbare combinatie Linux/x86/ext2 is het niet nodig nee. Ga je daarentegen een native IBM mainframe mounten op Linux/S390 dan lijkt het me weer handig.

Om het antwoorde te geven op je impliciete vraag: reinterpret_cast< > betekent zoveel als "compiler, niet zeuren. Ik weet beter dan jij wat ik aan het doe ben.". Lijkt me logisch dat dat niet geld voor beginners ;) Voor std::string is kun je de (binaire) contents inderdaad gewoon met c_str() wegschrijven. Dat geeft je een const char*. Lezen is een groter probleem, want het is pointer naar const char's. Read-only does, en Linux wil dat bij gelegenheden wel eens met harde hand afdwingen (hangt van compilerdetails af, maar typische fout op Linux is een Segmentation Fault.)
En die segmentation fault kom ik inderdaad constant tegen met een ander programma waar ik strings opsloeg, maar daar heb ik niets constant gebruikt. En een char* (pointer dus) slaat hij wel prima op, dat was zo in het voorbeeld uit dat boek. En waarom zou je gaan flippen als je een boek leest waar je niet in mag schrijven? (dat is wat linux doet bij die segmentation fault)

[ Voor 6% gewijzigd door Verwijderd op 26-07-2005 09:15 ]


  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 09-04 22:08
"Lezen" is in deze context "van disk inlezen", naar het geheugen. In dat verband maakt het wel uit dat het geheugen read-only is, want bij inlezen van disk schrijf je naar het geheugen.

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein


Verwijderd

Topicstarter
nu snap ik er nog minder van. Ik redeneer als volgt:
Je pakt een boek uit de kast (disk) waar je niet in mag schrijven (read-only). Je gaat zitten en slaat het boek open, om het te kunnen lezen (geheugen). Hier heb je een boek, waar je niet in mag schrijven, van de disk naar het geheugen "geschreven", zonder dat de bibliothecaris boos wordt.
Een normaal read-only bestand mag je toch ook openslaan en lezen? dan schrijf je het toch ook naar het geheugen?
Deze redenatie is vast fout, want ik denk niet dat jij er naast zit. Hoe zit dit dan wel?

  • farlane
  • Registratie: Maart 2000
  • Laatst online: 22:23
Het gaat om dit geval niet dat het bestand read only is, maar het geheugen waar je naar toe wil schrijven. Immer c_str() geeft een const char* terug.

Een string vullen moet met een van de constructors, of append of zoiets zodat de het string object zijn memory kan managen.

Somniferous whisperings of scarlet fields. Sleep calling me and in my dreams i wander. My reality is abandoned (I traverse afar). Not a care if I never everwake.


Verwijderd

Een oplossing zou kunnen zijn een serializable interface te definieren en die door alle classes waarvan de objecten binair naar disk geschreven/van gelezen moeten kunnen worden te laten implementeren :

C++:
1
2
3
4
5
6
7
class Serializable
{
   virtual const unsigned char *serialize ( ) const;
   // deserialize returns the point in the buffer where a potential next object begins
   virtual const unsigned char *deserialize ( const unsigned char *pbData );
   virtual unsigned long byteSize ( ) const;
}


Je moet dan alleen oppassen dat je niet met memory leaks komt te zitten maar je kan natuurlijk ook auto/magic pointers gebruiken...

Data wegschrijven / inlezen gaat dan met

C++:
1
2
3
4
5
6
7
8
9
10
11
unsigned char *pBuffer, *pCurrent;
MySerializableObject thisObjectIsSerializable, thisOneToo;
BinaryFile fBin;

fBin.write ( thisObjectIsSerializable.serialize ( ) , thisObjectIsSerializable.byteSize ( ) );
fBin.write ( thisOneToo.serialize ( ), thisOneToo.byteSize ( ) );
fBin.readall ( &pBuffer );
pCurrent = pBuffer;
pCurrent = thisObjectIsSerializable.deserialize ( pCurrent );
pCurrent = thisOneToo.deserialize ( pCurrent );
delete pBuffer;

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 18:07

.oisyn

Moderator Devschuur®

Demotivational Speaker

Dat gevogel met pointers vind ik een beetje een designflaw, waarom moet je zelf zorgen voor het lezen van de gehele file naar memory, het bijhouden van de huidige leespointer en het uiteindelijke opruimen van een buffer die je zelf nooit aangemaakt hebt (voor hetzelfde geldt doet je BinaryFile zijn memory management op een andere manier dan met new). In plaats daarvan kun je beter gewoon een onderliggende stream gebruiken waarvandaan alle objecten hun data inlezen.

Een ander nadeel is het moeten implementeren van een Serializable interface. Hoe ga je nou een std::string wegschrijven? Of een andere class die je niet zelf gemaakt hebt? Daarnaast ben je nog wat vergeten: serialize geeft wel mooi een const char * buffer terug, maar wie verwijderd die buffer weer? Zoals je code nu is moet dat in de Serializable zelf gebeuren, aangezien je de pointer na het aanroepen van write() weer kwijt bent. Allemaal weer extra te implementeren management dus.

Ik zou een algemene serializer class gebruiken waarmee je al je data serialized, en die class werkt hetzelfde voor zowel read en write en gebruikt intern gewoon een stream. Voor de implementatie van serialisatie voor een class zou ik de keuze willen hebben tussen een methode implementeren in je class (zelfs zonder overerven van een interface!) of het implementeren van een globale functie buiten de class.

En dan kom je al gauw op zoiets:
C++:
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
// de serializer class
class Serializer
{
public:
    enum mode { READ, WRITE };

    // construction
    Serializer (std::istream & in);
    Serializer (std::ostream & out);
    Serializer (std::iostream & ios, mode m);

    mode getMode() const;
    std::istream & getIn();
    std::ostream & getOut();

    // serialisatie proxies:
    template<class T> void operator () (T & t)     // enkel type
    {
        serialize (*this, t);
    }

    template<class T> void operator () (T *& t, unsigned & num)    // array van types
    {
        serialize (*this, num);

        if (getMode() == READ)
            t = new T[num];

        for (unsigned i = 0; i < num; i++)
            serialize (*this, t[i]);
    }

    void operator () (unsigned char * buffer, unsigned bytes) // binaire serialisatie
    {
        if (getMode() == READ)
            getIn().read(buffer, bytes);
        else
            getOut().write(buffer, bytes);
    }

private:
    // you figure it out ;)
};

// implementatie van standaard types
void serialize(Serializer & s, char & c)
{
    s (reinterpret_cast<unsigned char *> (&c), 1);
}

void serialize(Serializer & s, int & i)
{
    s (reinterpret_cast<unsigned char *> (&i), sizeof(int));
}
// etc.

// wegschrijven van standaard containers:
template<class T, class A> void serialize(Serializer & s, std::vector<T, A> & v)
{
    unsigned num = v.size();
    s(num);
    v.setsize(num);
    for (unsigned i = 0; i < num; i++)
        s(v[i]);
}

// proxy die de class method aanroept:
template<class T> void serialize (Serializer & s, T & t)
{
    t.serialize(s);
}


En dan het gebruik:
C++:
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
class MyClass
{
public:
    // construction, destruction, andere members

    void serialize(Serializer & s)
    {
        s (i);
        s (v);
    }

private:
    int i;
    std::vector<int> v;
};




int main ()
{
    MyClass c1, c2;

    // doe hier iets met c1...

    // schrijven:
    {
        std::ofstream out ("database.dat", std::ios::binary);
        Serializer s (out);
        s (c1);
    }

    // lezen:
    {
        std::ifstream in ("database.dat", std::ios::binary);
        Serializer s (in);
        s (c2);
    }

    // c2 is nu als het goed is gelijk aan c1
}


Uit de losse pols, dus geen garanties ;). Ook is dit een nogal simpel voorbeeld, ik zou dit uitbreiden met formatters e.d. zodat je bijvoorbeeld ook naar een xml file kunt schrijven, of whatever. Maar goed, dan kom je al snel uit bij boost::serialization ;)

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.

Pagina: 1