Refactoring von Legacy-Code in C++: Eine Schritt-für-Schritt-Anleitung

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Refactoring von Legacy-Code in C++: Wie man einen "Zoo" in ein wartbares Projekt verwandelt

Die Arbeit mit Legacy-Code in C++ ist für viele Entwickler eine unvermeidliche Realität. Oft fehlt es solchem Code an Tests, er ist reich an undefiniertem Verhalten (UB), globalen Variablen und manueller Speicherverwaltung. Die Refaktorisierung von Legacy-Code in C++ ist jedoch nicht nur ein "Kämmen" von Dateien, sondern ein systematischer Prozess zur Steigerung von Zuverlässigkeit und Leistung. In diesem Artikel werden wir die wichtigsten Schritte durchgehen, die Ihnen helfen, ein altes Projekt sicher zu modernisieren, ohne alles zu zerstören.



1. Vorbereitung: Testen und Isolieren

Der erste und wichtigste Schritt ist die Schaffung eines "Sicherheitsnetzes". Ohne Tests wird jede Refaktorisierung zu einem Spiel mit dem Feuer.

  • Charakterisierungstests: Schreiben Sie Tests, die das aktuelle Verhalten des Codes festhalten. Auch wenn es fehlerhaft erscheint, halten Sie es zunächst fest.
  • Statische Analyse: Verwenden Sie Werkzeuge wie clang-tidy oder Cppcheck. Sie decken potenzielle Speicherlecks, ungenutzten Code und Stilverstöße auf.
  • Sanitizer: Führen Sie den Code mit AddressSanitizer und UndefinedBehaviorSanitizer aus. Dies hilft, versteckte UBs vor Beginn der Änderungen zu finden.

Beispiel für das Hinzufügen eines Sanitizers in CMake:

# CMakeLists.txtset(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,undefined -fno-omit-frame-pointer")


2. Beseitigung von "nackten" Zeigern und manueller Speicherverwaltung

Legacy-C++ leidet oft unter Speicherlecks und doppelten Löschungen. Modernes C++ (C++11 und neuer) bietet RAII-Wrapper.

  • Ersetzung von new/delete durch intelligente Zeiger: std::unique_ptr für Alleinbesitz, std::shared_ptr für geteilten Besitz.
  • Verwendung von std::vector anstelle von rohen Arrays: Dies reduziert das Risiko von Bereichsüberschreitungen.

Refactoring-Beispiel:

Vorher:

class LegacyBuffer {    char* data;    size_t size;public:    LegacyBuffer(size_t sz) : size(sz) {        data = new char[sz];    }    ~LegacyBuffer() { delete[] data; }    // Kopierkonstruktor und Zuweisungsoperator vergessen (Verletzung der "Dreierregel")};

Nachher:

class ModernBuffer {    std::vector<char> data;public:    ModernBuffer(size_t sz) : data(sz) {}    // Destruktor, Kopieren und Zuweisen werden automatisch vom Compiler generiert};


3. Beseitigung von Makros und globalen Zuständen

Makros (#define) sind eine häufige Ursache für schwer zu findende Fehler. Sie sollten durch constexpr, enum class oder Templates ersetzt werden. Globale Variablen erschweren Tests und Parallelisierung.

  • Ersetzung von #define BUFFER_SIZE 1024 durch constexpr size_t BUFFER_SIZE = 1024;
  • Kapselung von Globalen: Lagern Sie den globalen Zustand in eine Singleton-Klasse aus oder, besser, übergeben Sie Abhängigkeiten über den Konstruktor.

Beispiel mit einem Makro:

// Schlecht: Makro hat keinen Gültigkeitsbereich#define MAX_CONNECTIONS 100

// Gut: Konstante mit Typ und Gültigkeitsbereichnamespace Config { constexpr int maxConnections = 100;}

Verwendung von enum class anstelle von #define für Flags:

// Statt #define FLAG_READ 1enum class FileMode : uint8_t {    Read = 1,    Write = 2,    ReadWrite = 3};


4. Vereinfachung komplexer Funktionen und Entfernung von totem Code

Legacy-Funktionen sind oft "Textwüsten" mit 500+ Zeilen und vielen verschachtelten Bedingungen. Strategie:

  • Methodenextraktion: Zerlegen Sie große Funktionen in kleinere, benannte Einheiten.

Blogs

Buchempfehlungen