Assignment Chef icon Assignment Chef
All German tutorials

Programming lesson

Playlist-Manager in C++: Sicheres Löschen des aktuellen Songs aus einer doppelt verketteten Liste

Lerne, wie du in deinem C++ Playlist-Manager den aktuellen Song sicher löscht – egal ob er am Kopf, Ende oder in der Mitte der doppelt verketteten Liste steht. Inklusive Pointer-Updates, Stack- und Queue-Bereinigung sowie Memory-Management.

Playlist Manager C++ doppelt verkettete Liste löschen aktuellen Song löschen C++ C++ Speichermanagement dangling pointers vermeiden Stack und Queue bereinigen COP3504C Lab 1 Lösung Objektorientierte Programmierung C++ C++ Playlist Manager Tutorial sicheres Löschen verkettete Liste C++ Pointer Übungen Memory Leaks vermeiden C++ Studentenprojekt C++ Playlist C++ Datenstrukturen lernen Playlist-Manager mit KI Analogie C++ für Anfänger Playlist

Einleitung: Warum sicheres Löschen in einem Playlist-Manager wichtig ist

Stell dir vor, du entwickelst eine Musik-Playlist-App wie Spotify oder Apple Music – nur als Konsolenanwendung in C++. Dein Professor hat dir die Aufgabe gegeben, einen Playlist-Manager zu programmieren, der Songs hinzufügen, löschen, überspringen und in einer Warteschlange organisieren kann. Dabei kommen Objektorientierte Programmierung (OOP), dynamische Speicherverwaltung und lineare Datenstrukturen wie doppelt verkettete Listen, Stacks und Queues zum Einsatz. Besonders knifflig ist das Löschen des aktuellen Songs. Wenn der Benutzer den gerade abgespielten Song entfernt, musst du sicherstellen, dass keine dangling pointers entstehen und der Speicher korrekt freigegeben wird. In diesem Tutorial zeige ich dir Schritt für Schritt, wie du das machst – mit vielen Codebeispielen und einem Augenzwinkern zu aktuellen Trends: Stell dir vor, du verwaltest die Playlist für das Finale der Fußball-Europameisterschaft 2024 – jeder Song ist ein entscheidender Moment, und ein falscher Pointer könnte das ganze Spiel zum Absturz bringen!

Grundlagen: Die doppelt verkettete Liste im Playlist-Manager

Bevor wir ins Löschen einsteigen, wiederholen wir kurz die Struktur. Eine doppelt verkettete Liste besteht aus Knoten (Song-Objekten), die jeweils einen Zeiger auf den vorherigen (prev) und den nächsten Knoten (next) haben. Die PlaylistManager-Klasse enthält einen Zeiger current, der auf den aktuell abgespielten Song zeigt. Außerdem gibt es einen std::stack<Song*> recentlyPlayed für die zuletzt gespielten Songs und eine std::queue<Song*> upcoming für die als Nächstes geplanten Songs. Das ist wie bei einer KI-gesteuerten Playlist, die deine Stimmung errät – nur dass du hier die Kontrolle hast.

class Song {
public:
    std::string title;
    Song* prev;
    Song* next;
    Song(const std::string& t) : title(t), prev(nullptr), next(nullptr) {}
};

class PlaylistManager {
private:
    Song* head;
    Song* tail;
    Song* current;
    std::stack<Song*> recentlyPlayed;
    std::queue<Song*> upcoming;
    // ...
};

Szenarien für das Löschen des aktuellen Songs

Der aktuelle Song kann sich an drei Positionen befinden:

  • Am Kopf der Liste (head)
  • Am Ende der Liste (tail)
  • In der Mitte (irgendwo dazwischen)

Jeder Fall erfordert eine andere Behandlung der Zeiger. Außerdem müssen wir die recentlyPlayed- und upcoming-Container bereinigen, um dangling pointers zu vermeiden. Das ist wie beim Datenmanagement in einer Finanz-App: Ein falscher Eintrag kann das ganze System durcheinanderbringen.

Fall 1: Der aktuelle Song ist der Kopf (head)

Wenn current == head, dann ist der Song der erste in der Liste. Nach dem Löschen muss head auf den nächsten Song zeigen (falls vorhanden). Außerdem muss der prev-Zeiger des neuen Kopfes auf nullptr gesetzt werden. Der current-Zeiger sollte auf den nächsten Song verschoben werden (oder auf nullptr, wenn die Liste danach leer ist).

void PlaylistManager::deleteCurrentSong() {
    if (!current) return; // Kein Song zum Löschen

    Song* toDelete = current;
    Song* nextSong = current->next;
    Song* prevSong = current->prev;

    // Fall 1: Kopf
    if (current == head) {
        head = nextSong;
        if (head) head->prev = nullptr;
        // Wenn die Liste nur einen Song hatte, wird tail auch nullptr
        if (!head) tail = nullptr;
        current = nextSong; // Gehe zum nächsten Song (oder nullptr)
    }
    // ... andere Fälle folgen
}

Fall 2: Der aktuelle Song ist das Ende (tail)

Wenn current == tail, dann ist der Song der letzte. Nach dem Löschen muss tail auf den vorherigen Song zeigen, und dessen next-Zeiger wird auf nullptr gesetzt. Der current-Zeiger sollte auf den vorherigen Song gesetzt werden (oder auf nullptr).

    // Fall 2: Ende
    else if (current == tail) {
        tail = prevSong;
        if (tail) tail->next = nullptr;
        current = prevSong; // Gehe zum vorherigen Song
    }

Fall 3: Der aktuelle Song ist in der Mitte

Wenn der Song weder Kopf noch Ende ist, müssen wir die prev- und next-Zeiger der benachbarten Songs aktualisieren. Der prevSong zeigt dann auf nextSong und umgekehrt. Der current-Zeiger wird auf den nächsten Song gesetzt (du könntest auch den vorherigen nehmen – hier wählen wir den nächsten).

    // Fall 3: Mitte
    else {
        prevSong->next = nextSong;
        nextSong->prev = prevSong;
        current = nextSong;
    }

Bereinigung von Stack und Queue

Nachdem wir die Liste aktualisiert haben, müssen wir alle Verweise auf den gelöschten Song in recentlyPlayed und upcoming entfernen. Das ist essenziell, um dangling pointers zu vermeiden. Stell dir vor, du hast eine KI-Playlist, die Songs basierend auf deiner Stimmung vorschlägt – ein veralteter Pointer könnte zu einem Absturz führen. Wir durchsuchen beide Container und entfernen den Pointer manuell. Da std::stack und std::queue keine direkte Methode zum Entfernen eines bestimmten Elements bieten, müssen wir sie in Hilfscontainer umkopieren.

    // Bereinige recentlyPlayed (Stack)
    std::stack<Song*> tempStack;
    while (!recentlyPlayed.empty()) {
        Song* s = recentlyPlayed.top();
        recentlyPlayed.pop();
        if (s != toDelete) {
            tempStack.push(s);
        }
    }
    // Stack wiederherstellen
    while (!tempStack.empty()) {
        recentlyPlayed.push(tempStack.top());
        tempStack.pop();
    }

    // Bereinige upcoming (Queue)
    std::queue<Song*> tempQueue;
    while (!upcoming.empty()) {
        Song* s = upcoming.front();
        upcoming.pop();
        if (s != toDelete) {
            tempQueue.push(s);
        }
    }
    upcoming = tempQueue;

Speicher freigeben und Memory Leaks vermeiden

Zum Schluss geben wir den Speicher des gelöschten Songs mit delete frei. Achte darauf, dass toDelete nicht mehr von irgendeiner anderen Stelle referenziert wird. Nach dem Löschen setzen wir current auf den neuen Song (oder nullptr). Wenn die Liste leer ist, müssen wir auch head und tail auf nullptr setzen.

    delete toDelete;
    // Wenn die Liste jetzt leer ist, current bereits nullptr setzen
    if (!head) {
        current = nullptr;
    }
}

Vollständige Funktion: deleteCurrentSong()

Hier ist die gesamte Funktion zusammenhängend:

void PlaylistManager::deleteCurrentSong() {
    if (!current) return;

    Song* toDelete = current;
    Song* prevSong = current->prev;
    Song* nextSong = current->next;

    // Fall 1: Kopf
    if (current == head) {
        head = nextSong;
        if (head) head->prev = nullptr;
        if (!head) tail = nullptr;
        current = nextSong;
    }
    // Fall 2: Ende
    else if (current == tail) {
        tail = prevSong;
        if (tail) tail->next = nullptr;
        current = prevSong;
    }
    // Fall 3: Mitte
    else {
        prevSong->next = nextSong;
        nextSong->prev = prevSong;
        current = nextSong;
    }

    // Bereinige recentlyPlayed
    std::stack<Song*> tempStack;
    while (!recentlyPlayed.empty()) {
        Song* s = recentlyPlayed.top();
        recentlyPlayed.pop();
        if (s != toDelete) tempStack.push(s);
    }
    while (!tempStack.empty()) {
        recentlyPlayed.push(tempStack.top());
        tempStack.pop();
    }

    // Bereinige upcoming
    std::queue<Song*> tempQueue;
    while (!upcoming.empty()) {
        Song* s = upcoming.front();
        upcoming.pop();
        if (s != toDelete) tempQueue.push(s);
    }
    upcoming = tempQueue;

    // Speicher freigeben
    delete toDelete;

    // Wenn die Liste leer ist, current auf nullptr setzen
    if (!head) current = nullptr;
}

Testen mit Beispiel-Playlist

Angenommen, deine Playlist enthält die Songs „Bohemian Rhapsody“, „Stairway to Heaven“ und „Hotel California“. Der aktuelle Song sei „Stairway to Heaven“ (in der Mitte). Nach dem Löschen sollte die Liste korrekt verknüpft sein: „Bohemian Rhapsody“ zeigt auf „Hotel California“ und umgekehrt. Der current-Zeiger zeigt nun auf „Hotel California“. Wenn du vorher „Bohemian Rhapsody“ gespielt hattest, war dieser im recentlyPlayed-Stack – er bleibt erhalten. Ein geplanter Song „Imagine“ in der upcoming-Queue bleibt ebenfalls unberührt. So stellst du sicher, dass deine Playlist-Manager-App stabil läuft – ähnlich wie eine Live-Streaming-Plattform während eines großen Events.

Fazit: Sicheres Löschen als Schlüsselkompetenz

Das Löschen des aktuellen Songs aus einer doppelt verketteten Liste ist eine typische Aufgabe in der Systemprogrammierung mit C++. Du lernst dabei, Zeiger korrekt zu setzen, Container zu bereinigen und Speicherlecks zu vermeiden. Diese Fähigkeiten sind nicht nur für dein COP3504C Lab 1 wichtig, sondern auch für professionelle Softwareentwicklung – sei es in der Spieleentwicklung, bei KI-Anwendungen oder in Finanzsystemen. Mit diesem Wissen kannst du jetzt deine Playlist-Manager-Aufgabe erfolgreich abschließen und deinen Professor beeindrucken. Viel Erfolg!

Zusätzliche Tipps für deine Abgabe

  • Teste alle drei Fälle (Kopf, Mitte, Ende) mit verschiedenen Playlist-Größen.
  • Überprüfe auf Speicherlecks mit Tools wie Valgrind.
  • Nutze const-Methoden für Getter, um die Integrität zu wahren.
  • Dokumentiere deinen Code – das zeigt dein Verständnis.