MINT lernen

Textaufgaben: Klassen in C++

Eine Mensakarte, die ihr Guthaben hütet, und ein Modell-Parkhaus, das nie mehr Autos einlassen darf, als Plätze da sind.

Dein Fortschritt:
0 / 0 Aufgaben
1

Die Mensakarte

AFB I–II

An der Schule von Mo wird das Mittagessen mit einer aufladbaren Karte bezahlt. Die Informatik-AG bildet die Karte in C++ als Klasse nach; das Guthaben wird in Cent gespeichert, damit keine Rundungsfehler entstehen.

C++ · mensakarte.h
#ifndef MENSAKARTE_H
#define MENSAKARTE_H

class Mensakarte {
private:
    int guthaben;   // in Cent
public:
    Mensakarte(int start) {
        guthaben = start;
    }
    void aufladen(int betrag) {
        if (betrag > 0) {
            guthaben = guthaben + betrag;
        }
    }
    bool bezahlen(int preis) {
        if (preis <= guthaben) {
            guthaben = guthaben - preis;
            return true;
        }
        return false;
    }
    int getGuthaben() {
        return guthaben;
    }
};

#endif

In einem Testprogramm (mit #include <iostream>, #include "mensakarte.h" und using namespace std;) stehen in main nacheinander die folgenden fünf Anweisungen:

C++ · Ausschnitt aus main
Mensakarte k(500);          // (1)
k.guthaben = 10000;         // (2)
Mensakarte m;               // (3)
k.aufladen(-200);           // (4)
cout << k.getGuthaben;      // (5)

Mo behauptet, nach diesen Anweisungen habe er genau 1,40 € auf seiner Karte:

C++ · Mos Tag in der Mensa
Mensakarte mo(1000);
mo.bezahlen(420);
mo.bezahlen(650);
mo.aufladen(-300);
mo.bezahlen(440);
  1. Erstellen Sie ein UML-Klassendiagramm der Klasse Mensakarte.
  2. Ordnen Sie jede der Anweisungen (1) bis (5) einer der Kategorien „wird übersetzt“ oder „Übersetzungsfehler“ zu.
  3. Bestätigen Sie Mos Behauptung über sein Guthaben.

Hinweise

Hinweis zu Aufgabe a)
Ein Klassendiagramm hat drei Felder: Name, Attribute, Methoden. - steht für private, + für public; der Typ steht nach einem Doppelpunkt hinter dem Namen.
Hinweis zu Aufgabe b)
Prüfen Sie jeweils: Ist das benutzte Attribut bzw. die Methode öffentlich? Gibt es einen passenden Konstruktor? Fehlen Klammern beim Methodenaufruf?
Hinweis zu Aufgabe c)
Verfolgen Sie den Wert von guthaben Anweisung für Anweisung. Achtung: Nicht jeder Aufruf verändert das Guthaben.

Erwartungshorizont

Erwartungshorizont zu Aufgabe a)
Klassendiagramm Mensakarte
Mensakarte- guthaben: int+ Mensakarte(start: int)+ aufladen(betrag: int): void+ bezahlen(preis: int): bool+ getGuthaben(): int
Der Konstruktor hat keinen Rückgabetyp.

Bewertet werden: drei Felder, Sichtbarkeiten -/+, Parameter- und Rückgabetypen, Konstruktor ohne Rückgabetyp.

Erwartungshorizont zu Aufgabe b)
Nr.KategorieBegründung
(1)wird übersetztKonstruktor mit einem int vorhanden
(2)Fehlerguthaben ist private — g++: „is private within this context“
(3)Fehlerkein Konstruktor ohne Parameter — „no matching function for call to Mensakarte::Mensakarte()“
(4)wird übersetztöffentliche Methode; sie ändert wegen betrag > 0 aber nichts
(5)FehlerKlammern fehlen: k.getGuthaben() — „invalid use of non-static member function“
Erwartungshorizont zu Aufgabe c)
AnweisungRückgabeguthaben (Cent)
Mensakarte mo(1000);—1000
mo.bezahlen(420);true580
mo.bezahlen(650);false (650 > 580)580
mo.aufladen(-300);—580 (negativ, ignoriert)
mo.bezahlen(440);true140

Am Ende stehen 140 Cent = 1,40 € auf der Karte — Mos Behauptung stimmt. Mit einem Testprogramm und getGuthaben() nach jeder Zeile ergibt sich dieselbe Folge 580, 580, 580, 140.

2

Ein Parkhaus mit Schranke

AFB II–III

Im Projekt „Smarte Stadt“ bauen Jana und Luis ein Modell-Parkhaus mit Schranke und Anzeigetafel. Die Software soll die Zahl der belegten Plätze verwalten. Sie einigen sich auf das folgende Klassendiagramm:

Klassendiagramm Parkhaus
Parkhaus- kapazitaet: int- belegt: int+ Parkhaus(kap: int)+ einfahren(): bool+ ausfahren(): void+ freiePlaetze(): int+ istVoll(): bool
- private, + public

Vereinbart ist: Ein neues Parkhaus ist leer. einfahren() gibt false zurück und ändert nichts, wenn das Parkhaus voll ist; sonst wird ein Platz belegt und true zurückgegeben. ausfahren() darf die Belegung nie unter 0 senken.

Luis meint: „Das private: können wir uns sparen und einfach struct schreiben. belegt wird ja sowieso nur über einfahren() und ausfahren() geändert, also kann nichts schiefgehen.“

Jana schlägt dagegen vor, statt einfahren() und ausfahren() einfach eine öffentliche Methode setBelegt(int anzahl) anzubieten, „dann ist die Klasse kürzer und man kann alles einstellen“.

  1. Implementieren Sie die Klasse nach dem Klassendiagramm, aufgeteilt in parkhaus.h und parkhaus.cpp, sowie ein kurzes main zum Testen. Geben Sie auch den Befehl zum Übersetzen an.
  2. Widerlegen Sie Luis’ Aussage.
  3. Bewerten Sie Janas Vorschlag, einfahren() und ausfahren() durch setBelegt(int anzahl) zu ersetzen.

Hinweise

Hinweis zu Aufgabe a)
In die Header-Datei gehört nur die Klassendeklaration mit Prototypen (Semikolon nach der schließenden Klammer!). In der .cpp-Datei beginnt jede Methode mit Parkhaus::. einfahren() kann istVoll() benutzen.
Hinweis zu Aufgabe b)
Worin unterscheiden sich class und struct in C++? Überlegen Sie, wer außer den eigenen Methoden noch auf belegt zugreifen könnte, wenn es öffentlich ist. Ein einziges Gegenbeispiel genügt.
Hinweis zu Aufgabe c)
Mögliche Kriterien: Kann die Klasse ungültige Zustände verhindern? Passt die Schnittstelle zu dem, was in der Wirklichkeit passiert (ein Auto fährt ein)? Wie viel Logik muss dann der aufrufende Code selbst übernehmen?

Erwartungshorizont

Erwartungshorizont zu Aufgabe a)
C++ · parkhaus.h
#ifndef PARKHAUS_H
#define PARKHAUS_H

class Parkhaus {
private:
    int kapazitaet;
    int belegt;
public:
    Parkhaus(int kap);
    bool einfahren();
    void ausfahren();
    int freiePlaetze();
    bool istVoll();
};

#endif
C++ · parkhaus.cpp
#include "parkhaus.h"

Parkhaus::Parkhaus(int kap) : kapazitaet(kap), belegt(0) {
}

bool Parkhaus::einfahren() {
    if (istVoll()) {
        return false;          // Schranke bleibt zu
    }
    belegt = belegt + 1;
    return true;
}

void Parkhaus::ausfahren() {
    if (belegt > 0) {
        belegt = belegt - 1;
    }
}

int Parkhaus::freiePlaetze() {
    return kapazitaet - belegt;
}

bool Parkhaus::istVoll() {
    return belegt >= kapazitaet;
}
C++ · main.cpp
#include <iostream>
#include "parkhaus.h"
using namespace std;

int main()
{
    Parkhaus p(3);
    for (int i = 1; i <= 4; i++) {
        if (p.einfahren()) {
            cout << "Auto " << i << ": Schranke auf" << endl;
        } else {
            cout << "Auto " << i << ": besetzt" << endl;
        }
    }
    p.ausfahren();
    cout << "Frei: " << p.freiePlaetze() << endl;
    p.ausfahren();
    p.ausfahren();
    p.ausfahren();   // leer: belegt bleibt 0
    cout << "Frei: " << p.freiePlaetze() << endl;
    return 0;
}
Übersetzen und Probelauf
$ g++ main.cpp parkhaus.cpp -o parkhaus
$ ./parkhaus
Auto 1: Schranke auf
Auto 2: Schranke auf
Auto 3: Schranke auf
Auto 4: besetzt
Frei: 1
Frei: 3

Bewertet werden: Include-Guard, private Attribute, Konstruktor setzt belegt auf 0, Klasse::methode-Schreibweise, beide Grenzfälle (voll, leer) abgefangen, Test mit mehr Autos als Plätzen. Statt der Initialisierungsliste ist auch eine Zuweisung im Konstruktorrumpf richtig.

Erwartungshorizont zu Aufgabe b)

Bei struct sind alle Mitglieder standardmäßig public. Dann kann jeder Programmteil belegt direkt ändern — an einfahren() und ausfahren() und ihren Prüfungen vorbei. Gegenbeispiel:

C++ · Gegenbeispiel
#include <iostream>
using namespace std;
struct Parkhaus {
    int kapazitaet;
    int belegt;
    Parkhaus(int kap) : kapazitaet(kap), belegt(0) {}
    int freiePlaetze() { return kapazitaet - belegt; }
};
int main()
{
    Parkhaus p(120);
    p.belegt = 150;
    cout << "Frei: " << p.freiePlaetze() << endl;
    return 0;
}
Ausgabe
Frei: -30

Das Programm wird fehlerfrei übersetzt, die Anzeigetafel zeigt aber −30 freie Plätze. Dass belegt „nur über die Methoden“ geändert wird, ist also eine Absprache, keine Garantie. Erst private lässt der Compiler solche Zugriffe nicht mehr zu (Kapselung).

Erwartungshorizont zu Aufgabe c)

Kürze: Die Klasse wird tatsächlich etwas kürzer, und für Sonderfälle (z. B. Zählung nach einem Stromausfall neu setzen) ist ein Setter praktisch.

Gültigkeit: Ohne Prüfung erlaubt setBelegt Werte wie −5 oder 500 — dasselbe Problem wie bei Luis’ struct. Mit Prüfung im Setter lässt sich das zwar verhindern, die Regel „nur, wenn nicht voll“ steckt aber dann im Aufrufer.

Verständlichkeit: p.einfahren() beschreibt das Ereignis an der Schranke; mit dem Setter müsste jeder Aufrufer selbst p.setBelegt(p.getBelegt() + 1) rechnen (dazu bräuchte es noch einen Getter) und vorher prüfen — mehr Code an vielen Stellen, mehr Fehlerquellen. Außerdem liefert einfahren() gleich die Antwort für die Schranke.

Urteil: Als Ersatz ist der Vorschlag schlechter, weil er die Kapselung schwächt und Logik aus der Klasse herauszieht. Vertretbar wäre ein zusätzlicher Setter mit Prüfung für Sonderfälle. Eine vollständige Antwort nennt mindestens zwei Kriterien und kommt zu einem begründeten Urteil.