Wie kann ich mit C++ Textdateien in einem Container speichern?

1 Antwort

Meines Erachtens gibt es bei deinem Code mehrere Punkte, die entweder verbesserungswürdig sind oder bei denen der Sinn hinterfragt werden muss.

1) Allgemein wäre es zunächst gut, sich an üblichen Konventionen zu orientieren:

  • Variablen- und Methodennamen beginnen mit einem Kleinbuchstaben. So lassen sie (insbesondere Variablen) sich später einfacher von Typnamen unterscheiden.
  • Sonderzeichen (wie ö, ü, ß) in Bezeichnern sollten vermieden werden. Nicht jeder Compiler nimmt die so direkt an. Ein Umlaut wie ö wird deswegen üblicherweise gegen oe ausgetauscht, ein ü gegen ue, ein ß gegen ss.
  • Einige deiner Variablen haben noch keinen gut gewählten Namen (z.B. Eigenschaft1 - das Feld soll ein Array repräsentieren, worauf der Name nicht hindeutet).
  • Bei der Definition von Interface und Implementation würde ich eine einheitliche Lösung wählen, denn das macht ein Projekt besser überschaubar. Entweder du teilst konsequent in Header- und Implementationsdatei auf (Container.h, Container.cpp, Personal.h, Personal.cpp, main.cpp) oder du verbindest Interface und Implementation immer, so wie du es bei der Container-Klasse getan hast.

2) Ein paar konkrete Codezeilen, auf die ich eingehen möchte:

a) Die Zuweisungen in deinem Personal-Konstruktor machen keinen Sinn.

Personal::Personal() {
  Name = Name;
  Position = Position;
  /* etc. */
}

Links- und rechtsseitig hast du jeweils dieselbe Variable. Du überschreibst also jedes Feld mit sich selbst.

Mehr Sinn würde es machen, wenn du einen Konstruktor hättest, der mittels Parametern deine Felder füllt.

Beispiel:

class Personal {
  private:
    string _name;
    int _alter;

  public:
    Personal(const string& name, const int alter) : _name(name), _alter(alter) {
    }
};

// invocation example:
Personal person("Lena", 22);

Die Unterstriche vor den Feldnamen sind nur eine verbreitete Konvention, um private Felder zu kennzeichnen. Die Werte werden über eine Initializationsliste gesetzt. Alternativ wäre auch so eine Konstruktordefinition möglich:

Personal(const string& name, const int alter) {
  _alter = alter;
  _name = name;
}

b) In der Hinzufügen-Methode braucht keine neue Personal-Instanz erstellt werden. Du kannst doch stattdessen die Instanz verwenden, an die die Methode bereits gebunden ist.

void Personal::hinzufuegen(Container<ofstream>& container) {
  cout << "Bitte geben Sie den Namen ein: ";
  cin >> _name;
  cout << "Bitte geben Sie das Alter der Person an: ";
  cin >> _alter;
  /* etc. */
  speichern(container);
}

Folglich brauchen die Daten nicht nochmal explizit an speichern weitergegeben werden, denn diese Methode ist an dasselbe Objekt gebunden und kann daher auf dieselben Zustände zugreifen.

void Personal::speichern(Container<ofstream>& container) {
  ofstream datei("Datei.csv");
  datei << "Name: " << _name << endl;
  datei << "Alter: " << _alter << endl;
  /* etc. */
}

c) Es ist besser, Variablen nur in dem Zugriffsbereich zu deklarieren, in dem sie tatsächlich gebraucht werden. In deiner main-Methode führst du beispielsweise mehrere Variablen ein, obwohl sie erst benötigt werden würden, wenn bestimmte Bedingungen eintreten.

int main() {
  string passwort { "Erfolg1234" };
  string passwortEingabe;
  cout << "Geben Sie das Passwort ein: ";
  cin >> passwortEingabe;

  if (passwort == passwortEingabe) {
    cout << "Wollen sie eine neue Position hinzufügen (1)" << endl;
    cout << "Wollen sie auf eine Position zugreifen (2)" << endl;
    int entscheidung;
    cin >> entscheidung;

    Personal person;

    if (entscheidung == 1) {
      Container<ofstream> container;
      person.hinzufuegen(container);
    }
    else if (entscheidung == 2) {
      person.ausgabe();
    }

    /* etc. */

Auf diese Weise kann dein Programmcode auch übersichtlicher werden. Zum einen lassen sich ab und an Deklaration und Definition zu einer Zeile zusammenziehen (Initialisierung) und es fällt schneller auf, wenn unbenutzte Variablen deklariert werden. Zum anderen können Variablen ihrem jeweiligen Kontext schneller zugeordnet werden: Wenn du schauen möchtest, wo bspw. entscheidung verwendet wird, verkleinert sich der Suchbereich. Bei meinem obigen Snippet braucht nur der if-Rumpf betrachtet werden. Bei deinem Code müsste vorerst davon ausgehen, dass die Variable in der gesamten main-Funktion einen Verwendungszweck findet.

d) In C++ gibt es eigene Containerklassen für Arrays oder Listen. Es gibt keinen Grund, stattdessen noch mit C-Arrays oder Pointern zu arbeiten.

Beispiel:

#include <array>
#include <iostream>
#include <vector>

class Dog {
  public:
    std::string name;

    Dog() {}
    Dog(const std::string& name) : name(name) {}
};

// main:
std::array<Dog, 10> dogs;
Dog pongo("Pongo");
dogs[0] = pongo;

std::cout << dogs[0].name << std::endl;

std::vector<Dog> dogList(10);
Dog perdita("Perdita");
dogList[0] = perdita;

std::cout << dogList[0].name << std::endl;

Arrays haben wie gewohnt eine statisch fixierte Größe. Im Gegensatz dazu stellt der std::vector-Typ eine dynamische Liste dar. Im Beispiel hat Letzterer eine initiale Größe zugewiesen bekommen. Für das Anhängen neuer Elemente (außerhalb des reservierten Platzes) wird die push_back-Methode eingesetzt.

e) Die main-Funktion kann kein Template sein.

Außerdem fehlt eine konkrete Rückgabe. C++ kann das zwar auch implizit handhaben, sicherer (im Bezug auf die Funktionalität auf verschiedenen Zielplattformen) ist allerdings eine explizite Rückgabe.

return 0;

In stdlib gibt es alternativ dazu auch noch eigene Makros.

f) Bei deiner Schleife vergleichst du Werte mit unterschiedlichen Datentypen.

for (int i = 0; i < Größe; i++) {

Das Feld hat den Typ size_t, während i ein int ist. Nutze besser nur einen Typ (size_t).

for (size_t i = 0; i < groesse; ++i) {

3) Du möchtest über dein Programm offensichtlich Personendaten abbilden, die in einer Datei gespeichert und wieder ausgelesen werden können. Demzufolge gibt es eine Modelklasse (Personal) und einen Serializer (Container). Letztgenannte Klasse würde ich anders (eindeutiger) benennen (z.B. PersonalVerwalter) und es wäre gut, beide Klassen nur mit den jeweiligen Eigenschaften/Methoden auszustatten, die tatsächlich für sie relevant sind.

Die Modelklasse dient nur der reinen Datenspeicherung. Also hält sie lediglich die Felder für Name, Alter, u.ä..

class Personal {
  private:
    string _name;
    string _position;
    int _alter;
    double _gehalt;

  public:
    Personal() {
    }

    Personal(const string& name, const string& position, const int alter, const double gehalt) :
      _name(name),
      _position(position),
      _alter(alter),
      _gehalt(gehalt) {
    }

    const string& gibName() const { return _name; }

    int gibAlter() const { return _alter; }

    void setzeName(const string& name) { _name = name; }

    void setzeAlter(const int alter) { _alter = alter; }

    /* etc. ... */
};

Alles was die Verwaltung betrifft (Speichern und Auslesen) gehört hingegen ausschließlich in die Verwalterklasse.

class PersonalVerwalter {
  private:
    string _dateiname;

  public:
    PersonalVerwalter() {
      _dateiname = "Datei.text";
    }

    Personal* liesPersonaldaten() {
      ifstream datei(_dateiname);

      if (!datei.is_open()) {
        return nullptr;
      }

      Personal* person = new Personal();
      /* read lines into Personal object ... */

      datei.close();
      return person;
   }

   void speicherePersonaldaten(const Personal& person) {
     ofstream datei(_dateiname);

     if (!datei.is_open()) {
       return;
     }

     /* append data to stream ... */
     datei.close();
   }
};

Bezüglich des Datenformats würde ich mir dabei noch einmal Gedanken machen. Zum einen würde ich nur die reinen Daten (ohne Beschriftung) speichern, andernfalls musst du schauen, dass du beim Auslesen die Beschriftung wieder vom Wert trennst. Sofern jede Eigenschaft in eine eigene Zeile geschrieben wird, brauchst du auch kein CSV-Format. Das würde sich eher lohnen, wenn du wirklich entsprechend strukturierst, z.B. so:

Name,Position,Alter,Gehalt
Timon,Manager,38,50000 

So ein Format wäre auch gut geeignet, wenn du mehrere Personen in der Datei speichern möchtest (jede Zeile spiegelt die Daten einer Personalie wieder).

Beim Auslesen der Datei müsstest du dann die erste Zeile (Header) ignorieren und die weiteren Zeilen anhand des Kommas auftrennen (z.B. via sstream).

Alles was das Auslesen der Daten von der Konsole sowie die Ausgabe betrifft, wäre in einer jeweils eigenen Funktion besser aufgehoben, die in main später aufgerufen wird.

void speicherePersonal(PersonalVerwalter& verwalter) {
  string sollSpeichern;
  cout << "M\u00F6chten sie die Person speichern: " << endl;

  if (sollSpeichern == "Ja") {
    string name;
    cout << "Bitte geben Sie den Namen ein: ";
    cin >> name;

    /* etc. ... */

    Personal person(name, position, alter, gehalt);
    verwalter.speicherePersonaldaten(person);
  }
  /* etc. ... */
}

void gibPersonalDatenAus(PersonalVerwalter& verwalter) {
  Personal* person = verwalter.liesPersonaldaten();

  if (!person) {
    return;
  }

  cout << "Name: " << person->gibName() << endl;
  /* etc. ... */
}

Diese klare funktionale Trennung sorgt für mehr Flexibilität (die Modelklasse könnte noch für andere Fälle benutzt werden, ohne eine Abhängigkeit zu irgendeinem Stream zu haben) und Überblick (der Code ist logisch strukturiert).