Zuweisung einfacher Objekte

Die Klasse Person ist recht einfach, so daß es sich nicht lohnt, dafür eine eigene CPP-Datei zu schreiben; die Methoden werden gleich in der Klasse und damit in der Headerdatei untergebracht.

Um die Unterschiede zwischen den drei Versionen klarer zu machen, sind in einem Quelltext alle geforderten Varianten untergebracht, und werden mit Präprozessordirektiven aktiviert (durch Aktivieren genau einer der drei Zeilen der Art #define AKTUELLEVERSION...):

// Time-stamp: "08.12.02 14:18 Person.h klaus@wachtler.de"
//
// Deklarationen (und Definitionen) für die class Person aus der
// C++-Übung A
//
// Musterlösung 08.12.2002 Klaus Wachtler

#ifndef __PERSON_H__
#define __PERSON_H__


// Welche Versionen gibt es?
#define FELDVERSION    1
#define STRINGVERSION  2
#define ZEIGERVERSION  3


// Welche Version soll es sein?
//#define AKTUELLEVERSION  FELDVERSION
//#define AKTUELLEVERSION  STRINGVERSION
#define AKTUELLEVERSION  ZEIGERVERSION


#include <iostream>


#if AKTUELLEVERSION==FELDVERSION
#include <cstring>
#include <string>
#include <stdexcept>
#define LAENGENAMEN    50 // maximale Länge inkl. '\0'
#elif AKTUELLEVERSION==STRINGVERSION
#include <string>
#elif AKTUELLEVERSION==ZEIGERVERSION
#include <cstring>
#include <cstdio>
#else
#error "AKTUELLEVERSION nicht definiert"
#endif // AKTUELLEVERSION...

class Person
{
public:

#if AKTUELLEVERSION==FELDVERSION

  // Konstruktor
  Person( const char *vn="",
          const char *nn="",
          unsigned short nLenze=0u
          )
  {
    // setze() macht eh das Selbe:
    setze( vn, nn, nLenze );
  }

  // (kein Destruktor nötig)

  // setzt den Inhalt komplett neu:
  void setze( const char *vn="",
              const char *nn="",
              unsigned short nLenze=0u
              )
  {
    // Länge der Namen prüfen, ggf. Ausnahme werfen:
    if( strlen( vn )>=LAENGENAMEN
        ||
        strlen( nn )>=LAENGENAMEN
        )
    {
      // Namen zu lang, abbrechen!
      throw std::out_of_range( "Name zu lang in Person::setze()" );
    }

    // ok, beide Namen können kopiert werden:
    strcpy( vorname, vn );
    strcpy( nachname, nn );

    alter = nLenze;
  }


#elif AKTUELLEVERSION==STRINGVERSION

  // Konstruktor
  Person( const char *vn="",
          const char *nn="",
          unsigned short nLenze=0u
          )
    : vorname( vn ),
      nachname( nn ),
      alter( nLenze )
  {
  }

  // (kein Destruktor nötig)

  // setzt den Inhalt komplett neu:
  void setze( const char *vn="",
              const char *nn="",
              unsigned short nLenze=0u
              )
    {
      vorname = vn;  // string läßt sich mit Zuweisung kopieren
      nachname = nn;
      alter = nLenze;
    }

#elif AKTUELLEVERSION==ZEIGERVERSION

  // Konstruktor
  Person( const char *vn="",
          const char *nn="",
          unsigned short nLenze=0u
          )
  {
    // setze() macht eh das Selbe, allerdings müssen vorname und
    // nachname zu NULL gesetzt werden, weil sonst delete[] mit deren
    // zufälligen bisherigem Inhalt aufgerufen wird, ein delete[]NULL
    // dagegen schadet nichts:
    vorname = NULL;
    nachname = NULL;
    setze( vn, nn, nLenze );
  }

  // Destruktor
  virtual ~Person()
  {
    // im ctor oder setze() allokierten Speicher wieder freigeben:
    delete[] vorname;  vorname = NULL; 
    delete[] nachname; nachname = NULL;
  }

  // setzt den Inhalt komplett neu:
  void setze( const char *vn="",
              const char *nn="",
              unsigned short nLenze=0u
              )
  {
    // Beide bisherigen Namen freigeben:
    delete[] vorname;  vorname = NULL;
    delete[] nachname; nachname = NULL;

    // Mit passender Länge neu allokieren:
    vorname = new char[strlen( vn ) + 1];
    nachname = new char[strlen( nn ) + 1];

    // new klappt immer, beide Namen können kopiert werden:
    strcpy( vorname, vn );
    strcpy( nachname, nn );
    alter = nLenze;
  }

  // Überschreiben der Funktion operator=() noch nicht
  // aktiviert; siehe Beschreibung der Musterlösung

//  // Zuweisung Person = Person
//  Person & operator=( const Person &rechteSeite )
//  {
//    // Beide bisherigen Namen freigeben:
//    delete[] vorname;  vorname = NULL;
//    delete[] nachname; nachname = NULL;
//
//    // Anhand der rechten Seite der Zuweisung neu allokieren
//    // und kopieren:
//    setze( rechteSeite.vorname,
//           rechteSeite.nachname,
//           rechteSeite.alter
//           );
//
//    return *this;
//  }

#else
#error "AKTUELLEVERSION nicht definiert"
#endif // AKTUELLEVERSION...

  // Ausgabe einer Person:
  void zeige()
  {
    // Ausgabe nach cout klappt mit char[], char* und string:
    std::cout << vorname << " "
              << nachname << ", "
              << alter
              << std::endl;
  }

private:

  // Die Namen:

#if AKTUELLEVERSION==FELDVERSION

  char          vorname[LAENGENAMEN];  // Feste Länge!
  char          nachname[LAENGENAMEN];

#elif AKTUELLEVERSION==STRINGVERSION

  std::string   vorname;  // wird automatisch verwaltet
  std::string   nachname;

#elif AKTUELLEVERSION==ZEIGERVERSION

  char         *vorname;  // nur Zeiger, rechtzeitig allokieren
  char         *nachname; // und wieder freigeben!

#else
#error "AKTUELLEVERSION nicht definiert"
#endif // AKTUELLEVERSION...

  // Das Alter:
  unsigned short alter;  // nicht mehr als 65535 Jahre möglich


}; // class Person...

#endif // ifdef __PERSON_H__

Bei der Verwendung von char-Feldern treten keine Probleme auf (abgesehen von der üblichen Platzverschwendung und der harten maximalen Länge der Namen natürlich).

Nach dem Initialisieren haben die beiden Objekte a und b die gewünschten Werte, nach der Zuweisung sind sie gleich, und nach dem erneuten Setzen hat b den gesetzten Wert:

klaus@lap2:~/skript_cpp > g++ -Wall test_person.cpp -o test_person
klaus@lap2:~/skript_cpp > test_person
(definieren)
Karl Moik, 72
Otto Waalkes, 55
(zuweisen)
Otto Waalkes, 55
Otto Waalkes, 55
(b setzen)
Otto Waalkes, 55
Eddi Stotter, 88

In der Version mit C++-strings funktioniert die Klasse ebenfalls wie erwartet, und erzeugt exakt dieselbe Ausgabe wie die Version mit Feldern (siehe oben).

Die Version mit Zeigern verhält sich dagegen anders (Beispiel unter Linux getestet, auf anderen Systemen kann es etwas anders aussehen, aber jedenfalls nicht wie bei den obigen Versionen):

klaus@lap2:~/skript_cpp > g++ -Wall test_person.cpp -o test_person
klaus@lap2:~/skript_cpp > test_person
(definieren)
Karl Moik, 72
Otto Waalkes, 55
(zuweisen)
Otto Waalkes, 55
Otto Waalkes, 55
(b setzen)
Eddi Stotter, 55
Eddi Stotter, 88
Segmentation fault

Daran sind zwei Punkte sehr befremdlich:

  1. Nach der Zuweisung sind die Objekte a und b zwar erwartungsgemäß gleich (Otto Waalkes, 55), aber nach dem Setzen von b auf einen neuen Wert haben sich Vor- und Nachname in a ebenfalls geändert!
  2. Am Ende des Programms (Ende von main()) stürzt das Programm ab (Segmentation fault).

Was ist passiert? Das Problem liegt daran, daß mit der Zuweisung a=b in main() alles, was innerhalb der Klasse definiert ist (vorname, nachname, alter) exakt von b nach a kopiert wird. Das bedeutet, daß auch die Zeiger umkopiert werden. Dadurch werden beide Zeiger a.vorname und a.nachname durch b.vorname und b.nachname ersetzt. Dagegen werden die eigentlichen Namen (also die Zeichen in den mit new allokierten Speicherbereichen) nicht kopiert.

Weil jetzt die Zeiger in a auf dieselben Stelle im Speicher zeigen wie die in b, wirkt sich eine Änderung der Namen in b auch auf a aus. (Mit dem Neusetzen der Namen in b wird freigegeben und neu allokiert; theoretisch könnte dabei ein Speicher an einer anderen Stelle im Speicher reserviert werden; dann würden die Zeiger in a nicht auf denselben Speicher zeigen, sondern auf bereits freigegebenen Speicher. Das scheint hier aber nicht der Fall zu sein, weil die neuen Namen nicht wesentlich länger sind als die alten und deshalb der mit delete freigegebene Speicher mit dem new gleich wieder reserviert wird).

Dies erklärt die Tatsache, daß sich eine Änderung der Namen in b auf die Namen von a auswirkt. Dieser Effekt ist sicher nicht erwünscht.

Aber warum stürzt das Programm zum Schluß ab? Dies liegt daran, daß am Ende der Lebensdauer von beiden Variablen jeweils ihr Destruktor aufgerufen wird, zuerst der von b. Hierin wird der Speicher für die beiden Namen freigegeben; danach wird der Destruktor von a aufgerufen. Weil durch die Zuweisung aber in a dieselben Zeiger stehen wie in b, wird versucht, den bereits im ersten Destruktoraufruf freigegebenen Speicher nochmals freizugeben. Dies führt zum Absturz. Andererseits kann der vor der Zuweisung für a allokierte Speicher nie vom Programm freigegeben werden, weil kein Zeiger mehr darauf existiert.

Nachdem die Probleme durch die vom Compiler unsinnig durchgeführte Zuweisung verursacht werden (auch wenn sie erst später in Erscheinung treten), liegt die Lösung darin, die Zuweisung zu verbessern.

Dazu kann man sich eine Funktion operator=() definieren, mit der man definiert, was bei einer Zuweisung genau passieren soll (siehe auch Überladen von Operatoren).

In der Musterlösung Person.h ist dies bereits (auskommentiert) enthalten. Wenn man die Kommentarzeichen vor der Funktion operator() entfernt, erhält man eine stabile Programmversion, die wie erwartet funktioniert und exakt die selbe Ausgabe ergibt wie die beiden ersten Versionen (mit Feldern beziehungsweise string).

Anstatt einfach die Zeiger zu kopieren, wird der Speicher der linken Seite freigegeben, in passender Länge neu allokiert, und dann werden die Zeichen der Namen mit strcpy() umkopiert.

AnyWare@Wachtler.de