Filestream write() - Serialisierung von C++-Klassen
-
Katzenzunge_n schrieb:
Meine Frage ist im Prinzip, woher der Compiler weiß was er da überhaupt in welcher Reihenfolge in die Datei schreiben kann? Object ist ja eine beliebige Klasse.
Es wird einfach plump alles was an der Stelle steht in der Reihenfolge in der es da steht in die Datei geschrieben und zwar so als ob es chars wären.
Und was liefert eigentlich sizeof(Object)?
"sizeof" ist englisch für "Größe von". Die Größenangabe ist in Einheiten von chars.
Probieren wir's einfach mal aus:
#include<iostream> #include<string> using namespace std; struct Foo { char a[4]; int b; short c; short d; short e; string f; }; int main() { Foo foo={{'h','u','h','u'},12345,97,98,99,"Hallo Welt"}; cout.write(reinterpret_cast<char*>(&foo), sizeof(Foo)); cout << endl; }
Hier passiert allerlei undefiniertes Zeug, daher kann es sein, dass deine Ausgabe anders aussieht als meine, aber sie sollte zumindest so oder ähnlich sein:
huhu90abc(PS
Es ist festgelegt, dass die Member im Struct in der Reihenfolge gespeichert werden, in der sie deklariert worden sind. Es kann aber noch Füllzeichen dazwischen geben. Mein Compiler scheint hier keine eingeführt zu haben.
Daraus folgt, dass am Anfang auf jeden Fall das "huhu" kommen muss, welches schon als chars vorlag und so eingespeichert wurde.
Die "90" danach ist die Binärdarstellung von 12345 auf meinem System, als chars interpretiert. In einer ASCII-Tabelle schlägt man einfach nach, dass '9' = 57 und '0' = 48. Und tatsächlich ist 48*256 + 57 = 12345, was uns etwas darüber verrät, wie auf meiner Maschine Integerwerte gespeichert werden. (Ein aufmerksamer Leser wird nun bemerken, dass nur zwei Zeichen ausgegeben wurden. Ist sizeof(int) bei mir 2? Nein, die anderen chars haben bloß alle den Wert 0 und das Nullzeichen ist in der Ausgabe nicht sichtbar)
Danach folgen die shorts. Anscheinen hat mein Compiler wieder keine Füllzeichen eingebaut oder die Füllzeichen sind alle 0. Dieses Mal ist die ASCII-Tabelle umgekehrt zu benutzen. Ich habe die Werte 97,98,99 so gewählt, dass sie "abc" ergeben, was ebenfalls passt.
Als letztes kommt der String und er zeigt, warum das Verfahren bei nicht-POD überhaupt nicht funktioniert. Das "Hallo Welt" hat der String irgendwo im Freispeicher angelegt, was man hier sieht sind die internen Datenstrukturen des Strings die auf das Hallo-Welt verweisen, vermutlich ein paar Pointer. Diese haben irgendwelche Werte und werden von meiner Konsole als irgendwelche wilden Unicodezeichen interpretiert, die hier im Forum nicht darstellbar sind.
edit: Mittlerweile sind ein paar Fragen dazu gekommen:
Das heißt, sizeof liefert mir die Größe der Datenfelder in der Klasse? Das wären oben dann 16 Byte?
Nein, es wird alles zusammengezählt, inklusive eventueller Füllzeichen.
Was konkret bedeutet Padding hier? Padding kenne ich als Begriff nur aus der Kryptographie, wo es um das Auffüllen von Bytes geht.
Das sind Füllzeichen die der Compiler aus Optimierungsgründen einfügen darf. Aus gewissen technischen Gründen ist es nämlich günstiger, die Daten an bestimmten Vielfachen (meistens von 4 oder
zu orientieren. Wenn der erste Member nur 3 Byte groß ist, ist es daher günstig, vor dem zweiten ein Byte frei zu lassen.
Ist überhaupt vom C++-Standard gewährleistet, in welcher Byte-Reihenfolge (Byte-Order oder Endianness) geschrieben wird oder bestimmt das die Zielplattform bzw. der Compiler?
Nein, das ist überhaupt gar nichts garantiert. Und das ist ein großes Problem bei der Serialisierung. Daher macht man so etwas auch nicht mit write, sondern gibt alles hübsch als menschenlesbares ASCII (das verstehen (fast) alle Computer) aus oder benutzt für sein eigenes Programm eine Serialisierungsbibliothek, welche ein plattformunabhängiges Speicherschema benutzt.
-
Ok, danke für die Aufklärung, besonders an SeppJ.
Habe zum Thema Padding hier noch zwei hilfreiche Quellen gefunden.
http://msdn.microsoft.com/en-us/library/71kf49f1(v=vs.80).aspx
http://en.wikipedia.org/wiki/Data_structure_alignment#Data_structure_padding
-
Katzenzunge_n schrieb:
Inwiefern iteriert write() in einer Schleife?
void write(char* data, int size) { while(size--) write_char(*data++); }
So in etwa _könnte_ write funktionieren. Muss es aber nicht. Nirgens steht geschrieben, ob write nicht auch z.B. memmove verwenden könnte. Aber man kann sich das in etwa so vorstellen.
-
Was genau write intern verwendet, dürfte auch davon abhängen, welchen Stream du angegeben hast. Es dürfte wohl die streambuf::sputn() für die tatsächliche Ausgabe bemühen. Wenn das nicht für den speziellen Stream-Typ optimiert wurde, läuft diese wiederum auf eine Schleife für die byteweise Ausgabe hinaus.
(die Optimierungen können dann in einem direkten Aufruf von memcpy(), fwrite() oder std::string::append() bestehen - je nachdem, worüber du redest)
-
ofstream::write nutzt doch unter der Haube fwrite() ?
Zumindestens in der MSVC STl Implementierung.
-
314159265358979 schrieb:
Katzenzunge_n schrieb:
Inwiefern iteriert write() in einer Schleife?
void write(char* data, int size) { while(size--) write_char(*data++); }
So in etwa _könnte_ write funktionieren. Muss es aber nicht. Nirgens steht geschrieben, ob write nicht auch z.B. memmove verwenden könnte. Aber man kann sich das in etwa so vorstellen.
Hi!
Das ist mir schon klar.
Es ging dabei mehr um die Frage, wie write() über die Datenmember iteriert? SeppJ hat ja bereits erklärt das beim Casten einfach aus dem Speicher sequentiell geschrieben wird und er hat auch erklärt wie diese in einem Struct abgebildet werden.
Es ging also nicht darum wie write() funktioniert, sondern was von write() überhaupt beim Casten einer Klasseninstanz geschrieben wird.
-
Ethon schrieb:
ofstream::write nutzt doch unter der Haube fwrite() ?
Zumindestens in der MSVC STl Implementierung.Ist absolut erlaubt, aber vom Standard nicht gearantiert - genausogut könnte es hinter den Kulissen mit man: write(2) arbeiten oder mit einer speziell dafür vom Compiler-Hersteller geschriebenen Bibliothek.
-
Padding ist durchaus interessant.
#include <iostream> using namespace std; class A { public: char a; char b; int c; }; class B { public: char a; int c; char b; }; int main(void) { int sizeA = sizeof(A); int sizeB = sizeof(B); cout << "A = " << sizeA << endl; cout << "B = " << sizeB << endl; return 0; }
Mein VC++ liefert mir hier die Ausgabe:
A = 8 B = 12
sizeof(9 auf einer Klasse aufzurufen scheint wirklich ziemlich abenteuerlich zu sein.
-
Und jetzt füge Mal virtuelle Funktionen hinzu...
~Ich nehme an, dir ist klar, dass struct das selbe ist wie class, nur mit einem standardmäßigem public-Sichtbarkeitsbereich~
-
Oberon_0 schrieb:
Ich nehme an, dir ist klar, dass struct das selbe ist wie class, nur mit einem standardmäßigem public-Sichtbarkeitsbereich
Ist mir bekannt.
Oberon_0 schrieb:
Und jetzt füge Mal virtuelle Funktionen hinzu...
Interessant. sizeof() liefert mit einer virtielle Funktion nun 16, statt 12 zurück. Dürfte wohl der zusätzlich Pointer (0) sein?
-
Interessant. sizeof() liefert mit einer virtielle Funktion nun 16, statt 12 zurück. Dürfte wohl der zusätzlich Pointer (0) sein?
Ja, der Pointer zur Functiontable.
Und jetzt erbe die Klasse, und füg neue virtuelle Funktionen dazu.Aber ernsthaft: Sachen in eine POD-Representation bringen und direkt schreiben/lesen ist imo keine so schlechte Idee, man sollte halt compilerunabhängig Padding/Alignment festsetzen und auf die Endianess aufpassen.
-
Ethon schrieb:
Aber ernsthaft: Sachen in eine POD-Representation bringen und direkt schreiben/lesen ist imo keine so schlechte Idee, man sollte halt compilerunabhängig Padding/Alignment festsetzen und auf die Endianess aufpassen.
Meiner Ansicht nach ist davon eher strikt abzuraten, zumindest wenn man in ANSII C++ programmiert.
Dann lieber Boost verwenden oder den operator<< überladen und die Daten selbst im festen Format schreiben.
Alles andere sieht mir angesichts der Sachlage nach ziemlichen Murks aus.
-
Katzenzunge_n schrieb:
Ethon schrieb:
Aber ernsthaft: Sachen in eine POD-Representation bringen und direkt schreiben/lesen ist imo keine so schlechte Idee, man sollte halt compilerunabhängig Padding/Alignment festsetzen und auf die Endianess aufpassen.
Meiner Ansicht nach ist davon eher strikt abzuraten, zumindest wenn man in ANSII C++ programmiert.
Dann lieber Boost verwenden oder den operator<< überladen und die Daten selbst im festen Format schreiben.
Alles andere sieht mir angesichts der Sachlage nach ziemlichen Murks aus.
Die meisten Fileformate sind allerdings binär. Was auch einen Grund hat.
Geringerer Platzbedarf und man muss nicht parsen.Wer möchte denn 100MB große Mp3 Dateien haben, die mehrere Sekunden laden?
-
Nur sind solche Formate dann standardisiert und hängen nicht von Compiler und Plattform ab. Natürlich machen Binärformate Sinn, aber sobald du sowas portabel hinkriegen willst, musst du eh memberweise vorgehen. Zumal in C++ der Grossteil der Klassen keine PODs sind.
Du kannst das Ganze auch abstrahieren, wie das Boost.Serialization mit seinen Archiven macht. Auf der einen Seite schreibst und liest du die einzelnen Member, auf der anderen Seite hast du ein Archiv, welches die aufgerufenen Schreib-/Lesefunktionen umsetzt. Diese Umsetzung bestimmt dann das eigentliche Format. Das kann sowohl Text als auch binäre Daten sein.