Vector mit Strings in Binärdatei einlesen/auslesen

Soulwear

Grünschnabel
Guten Abend allerseits,

ich bin derzeit im 2. Semester Informatik und habe derzeit ein kleines Problem mit fstream.

Gefordert ist, dass ein Vector, welcher mit Strings gefüllt ist beim Aufruf des Programms aus einer Binärdatei eingelesen und am Ende des Programms wieder in selbige Datei abgespeichert werden soll.

Klar ist mir, dass sich Strings nicht ohne weiteres in eine Binärdatei abspeichern lassen, da sonst bei reinem einspeichern in die Datei letzten Endes nicht wiedererkannt werden kann, welche Länge der bestimmte String besitzt welchen man laden möchte.

Vorgehensweise ist hierbei nun, dass man in die Binärdatei zuerst die Länge des Strings abspeichert und darauf folgend den String selbst, so führt man das in einer Schleife fort, bist der Vector komplett abgespeichert wurde/die Binärdatei am EOF angelangt ist, je nachdem ob man einlesen oder abspeichern möchte.

Problematisch wird es bei mir nun bei der Umsetzung, ich habe recherchiert und versucht etwas dazu hinzubekommen.

Mein Code um es etwas zu veranschaulichen:

main.cpp

C++:
#include <cstdlib>
#include <iostream>
#include <vector>
#include <string>
#include "Navigation.h"
#include <fstream>

using namespace std;

/*
 *
 */
int main(int argc, char** argv) {

    int eingabe = 0;
    int counter = 0;
    string einlesestring;
    Ort navi;
    string orteingabe,searchort;
    int sizeofstring;
  
    ifstream binary_locations;
    binary_locations.open("binary_locations.bin", ios::in | ios::binary);
    if(!binary_locations)
        cerr << "Binärdatei konnte nicht geöffnet werden!" << endl;
  
    while(!binary_locations.eof()){
        counter++;
        binary_locations.read((char *)&sizeofstring, sizeof(sizeofstring));
        binary_locations.read((char *)&einlesestring, sizeofstring);
        navi.orteingabe(einlesestring);
    }
    binary_locations.close();
  
  
  
    while (eingabe != 7) {
        cout << "--------Navigationssystem--------" << endl;
        cout << "1. Neuen Ort (mit Namen) hinzufügen" << endl;
        cout << "2. Ort löschen" << endl;
        cout << "3. Neue Namen aus Textdatei einlesen" << endl;
        cout << "4. Alle Namen in Textdatei schreiben" << endl;
        cout << "5. Alle Namen ausgeben" << endl;
        cout << "6. Start- und Zielort eingeben" << endl;
        cout << "7. Beenden" << endl;
        cin >> eingabe;
        if (eingabe > 0 && eingabe < 8) {

            switch (eingabe) {
                case 1:
                    cout << "Geben Sie den Ort ein den sie speichern möchten!" << endl;
                    cin >> orteingabe;
                    navi.orteingabe(orteingabe);
                    cout << "Der Ort wurde erfolgreich gespeichert!" << endl;
                    break;
                case 2:
                    cout << "Geben Sie den Ort ein den sie löschen möchten!" << endl;
                    cin >> orteingabe;
                    navi.ortloeschen(orteingabe);
                    cout << "Der Ort wurde erfolgreich gelöscht!" << endl;
                    break;
                case 3:
                    cout << "Neue Namen werden aus der Textdatei eingelesen!" << endl;
                    navi.readTXT();
                    cout << "Die Namen wurden erfolgreich eingelesen!" << endl;
                    break;
                case 4:
                    cout << "Alle Namen werden nun in eine Textdatei geschrieben!" << endl;
                    navi.writeallTXT();
                    cout << "Alle Namen wurden erfolgreich in eine Textdatei geschrieben" << endl;
                    break;
                case 5:
                    cout << "Namen der gespeicherten Orte:" << endl;
                    navi.printall();
                    break;
                case 6:
                    cout << "Geben Sie den Startort ein!" << endl;
                    cin >> searchort;
                    if (!navi.searchort(searchort)) {
                        cout << "Der eingegebene Startort wurde nicht gefunden!" << endl;
                        break;
                    }
                    cout << "Geben Sie einen Zielort ein!" << endl;
                    cin >> searchort;
                    if (!navi.searchort(searchort)) {
                        cout << "Der eingegebene Zielort wurde nicht gefunden!" << endl;
                        break;
                    } else {
                        cout << "Start und Zielort wurden gefunden!" << endl;
                    }
                    break;
            }
        }
    }

  
    ofstream binary_locations_of;
    binary_locations.open("binary_locations.bin", ios::out | ios::binary);
    if(!binary_locations_of)
        cerr << "Binärdatei konnte nicht geöffnet werden!" << endl;
  
    for(int i = 0;i < navi.orte.size();i++){
        sizeofstring = navi.orte[i].size();
        binary_locations_of.write((char *)&sizeofstring, sizeof(sizeofstring));
        binary_locations_of.write((char *)&navi.orte[i], sizeofstring);
    }
    return 0;
}

Navigation.cpp

C++:
#include "Navigation.h"
#include <fstream>
#include <iostream>

Ort::Ort(){
  
}

void Ort::orteingabe(const std::string& ortname){
    orte.push_back(ortname);
}

void Ort::ortloeschen(const std::string& ortname){
    for(int i = 0; orte.size() > i;i++){
        if(orte[i] == ortname){
            orte.erase(orte.begin() +i );
        }
    }
}

void Ort::printall(){
    for(int i = 0; orte.size() > i;i++){
        std::cout << i << ". " << orte[i] << std::endl;
    }
}

void Ort::readTXT(){
    std::ifstream locations;
    std::string ort;
  
    locations.open("Locations.txt", std::ios::in);
  
    if(!locations){
        std::cerr << "Datei kann nicht geöffnet werden!" << std::endl;
    }
  
    while(getline(locations, ort)){
        orte.push_back(ort);
    }
    locations.close();
}

void Ort::writeallTXT(){
    std::ofstream locationswrite;
    std::string ort;
  
    locationswrite.open("Locations.txt", std::ios::out);
  
    if(!locationswrite){
        std::cerr << "Datei kann nicht geöffnet werden!" << std::endl;
    }
  
    for(int i = 0; i < orte.size();i++){
        locationswrite << orte[i] << std::endl;
    }
    locationswrite.close();
}

bool Ort::searchort(const std::string& orteingabe){
    bool treffer = false;
    for(int i = 0; i < orte.size();i++){
        if(orte[i] == orteingabe){
            treffer = true;
        }
    }
    return treffer;
}

Ort::~Ort(){
  
}

Navigation.h

C++:
#ifndef NAVIGATION_H
#define NAVIGATION_H
#include <vector>
#include <string>
class Ort{
public:
    Ort();
    ~Ort();
    void orteingabe(const std::string& ortname);
    void ortloeschen(const std::string& ortname);
    void readTXT();
    void writeallTXT();
    void printall();
    bool searchort(const std::string& orteingabe);
    std::vector<std::string> orte;
};


#endif /* NAVIGATION_H */

So habe ich mir das gedacht, zuerst wird mit dem ersten read() die Länge des Strings als Integer geladen, dann wird diese Länge im darauffolgenden read() als Länge der zu lesenden Zeichen genommen.

Nach dem read() kommt irgendwann auch das write(), alles wie im Code dargestellt scheint der Compiler nicht zu bemängeln und führt alles auch super aus, wenn ich nun aber das Programm schließe und wieder öffne sind aber nun keine Strings eingelesen worden und auch in der Datei sind keine Werte abgespeichert worden, kann mir jemand helfen und sagen wo genau hierbei der Fehler liegt?

Ja, der Vector in der Klasse Ort sollte private sein, habe das zur Vereinfachung aber nun weggelassen.


Hoffe mein Problem ist verständlich! Falls nicht gerne nochmals drauf ansprechen, ich hoffe ihr könnt mir helfen!

Mit freundlichen Grüßen

Max
 
Zuletzt bearbeitet:

sheel

I love Asm
Hi

Ein allgemeiner Hinweise zum Programmdesign:
Warum wird das (binäre) Speichern/Einlesen nicht auch in Methoden von Ort gemacht?

Das Hauptproblem ist, dass du Instanzen von string (std::string) wie reinen Text behandelst, der direkt in die Datei geschrieben und daraus gelesen werden kann.
Falls du dich damit auch halbwegs auskennst; wenn die Texte mit char-Arrays/Pointern abgespeichert worden wären, und mit strcpy, strcat/malloc usw. gearbeitet wird, geht das. (Aber nicht deswegen umsteigen, ist umständlicher und gesamt gesehen noch fehleranfälliger für Anfänger).
std::string ist aber nicht der reine Text, sondern beinhaltet intern verschiedene Variablen wie zB. eine für die Länge, einen Pointer zum eigentlichen Text, und je nach OS/Implementierung noch mehr. Mit dem direkten Schreiben des Objekts schreibst du nur diese Sachen (also die Bytes der Längenvariable, eine Speicheradresse usw.), und je nachdem wie viel Byte geschrieben werden schreibst du auch noch ein Stück Speicher dass gar nicht mehr zum String gehört.

Auf sowas muss man in C++ sehr aufpassen: Das lann recht "lustige" Nebenwirkungen haben, zB. andere Variablen die ihren Wert grundlos von selbst ändern, usw., solche Fehler findet man später sehr schwer. Einfache Programmabstürze gibts auch, die erkennt man wenigstens. Und am schlimmsten (?) ist, dass manchmal auch alles wie erwartet funktionieren kann, aber morgen oder auf einem anderen Computer plötzlich nicht mehr.
(Es gibt andere Sprachen, die das besser prüfen und bei der Art von Fehlern immerhin immer Abstürze liefern, statt unerwartete Sachen, aber das hat auch einen Nachteil: DIe Programme sind etwas langsamer. C++ macht das absichtlich nicht, sondern stellt Geschwindigkeit in den Vordergrund).

Lösung beim Schreiben, relativ einfach:
Code:
(char *)&navi.orte[i]
durch
Code:
&navi.orte[i][0]
ersetzen.
navi.orte[ i ] ist der String. Obwohl das kein direktes Array ist kann man [0] machen, was für std::string letztendlich sowas wie ein Methodenaufruf wie navi.orte[ i ].get(0) ist; das liefert den ersten Buchstaben im String. Davon dann & ist die Adresse vom ersten Buchstaben... kurz, jetzt greift man auf den Speicher mit den Buchstaben selber zu, nicht auf die Verwaltungsdaten.

Das Lesen ist etwas umständlicher. Eines von den wichtigsten Features, die std::string besser machen als char-Arrays, ist, dass die Länge selbst verwaltet wird. Also eine interne Variable, wie viel Byte belegt sind, und je nach Bedarf Reservierung von weiterem Speicher wenn man was zum String hinzufügt. Das geht aber nur, wenn man es nicht absichtlich umgeht ... Wenn man einfach so die eingelesenen Datein in den Buchstabenspeicher schreibt wird a) die Längenvariable nicht angepasst und b) es könnte der Speicher zu klein sein, um die reingeschriebenen Daten auszuhalten. Wie oben beschrieben können da wieder unerwartete Nebenwirkugnen auftreten.
Was man machen kann, nachdem die Länge schon eingelesen wurde: Den String mit zB. so viel Leerzeichen oder so füllen. Damit passen Länge und Speichergröße dann schon, und nur die Werte ändern geht ohne Probleme.
Code:
binary_locations.read((char *)&sizeofstring, sizeof(sizeofstring));
einlesestring.resize(sizeofstring);
binary_locations.read(&einlesestring[0], sizeofstring);

...

Zwei Hinweise zu etwas mehr fortgeschrittenen Themen, weil manche anderen Sachen im Code genau genommen auch nicht ganz richtig sind (aber vermutlich hier keine Probleme machen):

C++ (und C, aus dem C++ entstanden ist, noch mehr) sind Sprachen, die auf vielen verschiedenen Betriebssystemen und Gerätearten funktionieren. Egal ob PC/Laptop/Tablet, Flugzeug, Waschmaschine, Elektronenmikroskop, usw. ... C ist unter den ganzen wichtigen Programmiersprachen so ziemlich an der Spitze, wie viel exotischen Sachen unterstützt werden. Daraus ergeben sich aber auch Probleme, weil manche Geräte bestimmte Sachen einfach nicht können.
Worauf ich hinaus will sind hier die Größen der Variablenarten. Du hast vielleicht gelernt, int hat 4 Byte, also 32bit, und kann damit 2^32 verschiedene Werte speichern. char hat 1 Byte, short hat 2 usw. ... Jetzt gibt es aber eben Geräte, wo zB. eine 2-Byte-Variable im Prozessor einfach nicht sinnvoll unterstützt wird. C (und C++) sagen da nicht "ok, für das Gerät gibt es eben kein C/C++", sondern gehen Kompromissen ein. Einer davon ist, dass garantiert ist, welche Werte welcher Typ "mindestens" speichern kann, falls es den Typ überhaupt gibt. zB. für int ist es mindestens 2^32 verschiedene Werte (auch die genauen Werte sind spezifiziert), aber ein int darf auch mehr können.
Warum das hier stört: Sachen wie die Länge eines std::string's werden nicht in int gemessen, sondenr in size_t. Das ist wie int,short usw. ein Ganzzahlentyp, aber der hat seine eigenen Größenwerte. Garantiert ist, dass size_t alle möglichen Stringlängen speichern kann (die die aktuelle Umgebung speichern kann). Aber size_t und int sind nicht unbedingt das Selbe, int kann auch kleiner sein als size_t. Wenn man jetzt einen langen String hat, dessen Länge in ein int nicht reinpasst... = Problem. Deswegen immer passende Typen verwenden. Außer size_t gibts noch eingie andere wichtige, aber das kommt mit der Erfahrung.

Falls du dir jetzt denkst "für solche Geräte programmier ich nicht", eine Warnung: Auch auf "ganz normalen" Computern kann das ein Problem werden, weil die Compiler annehmen dürfen, dass man solche theoretisch falschen Sachen nicht macht, und den Code entsprechend verarbeiten. Speziell bei Geschwindigkeitsoptionierungen tun sie das auch. Etwas theoretisch falsches, was am eigenen Computer funktionieren sollte, kann daher trotzdem falsch ausgeführt werden.

Thema 2, ähnlich zu oben: Wie ints usw. im RAM, in Bits, eigentlich ausschauen, variiert auch stark. Außer den Größen gibt es Unterschiede wie negative Zahlen gespeichert werden, in welcher Reihenfolge die Bits gespeichert werden, ob und wieviel "Abstand" zwischen zwei Arrayelementen ist usw.usw.
Warum das hier stört? Wegen deinen Dateidaten: Daten, die auf einem Computer geschrieben wurden, können nicht unbedingt auf einem anderen wieder ausgelesen werden. Korrekte Serialisierung ist deutlich schwerer als die paar Zeilen.

... wie gesagt, nicht für jetzt wichtig, aber besser im Kopf behalten.
 
Zuletzt bearbeitet: