Navigation

Programme richtig beenden

Programme richtig beenden

Es gibt die goldene Regel, daß ein Programm bei seiner Beendingung alle belegten Resourcen, wie z.B. geöffnete Dateien, belegten Speicher, Sockets usw., freizugeben hat. Leider wird diese Regel hin und wieder von Programmierern gebrochen, meist mit dem Argument, daß das Betriebssystem ohnehin dafür sorge tragen würde, daß am Ende des Programms aufgerämt wird.

Dieser Artikel legt dar, warum man jedenfalls selbst im Programm alle Resourcen freigeben sollte. Es wird erläutert, warum sich C++ Programme hier anders verhalten als C Programme und wo die Fallstricke dabei liegen.

1. Warum ist es wichtig, selbst aufzuräumen?

Viele Betriebssysteme putzen hinter einem Prozeß hinterher: wenn der Prozeß beendet wird, sorgt das Betriebsystem dafür, daß der Speicher des Programms freigegeben wird, noch immer geöffnete Datei geschloßen werden, und bestimmte Resourcen freigegeben werden. Eigens zu diesem Zweck verwaltet das Betriebssystem intern Listen, in denen die belegten Resourcen aufgelistet sind. Wenn also das Betriebssystem so ordentlich ist, dann kann man doch selbst unordentlich sein, oder? Im wesentlichen stehen zwei Gründe dagegen, die Aufräumarbeiten dem Btriebssystem zu überlassen:

  1. Portable Programme müssen generell selbst aufräumen. Denn der Ordnungswahn von Betriebssystemen ist nirgends standardisiert und es gibt eine ganze Reihe von Beispielen, wo niemand aufräumt: das schlechte alte MS DOS z.B. hat niemals automatisch Speicher freigegeben, viele Embedded Systeme vertrauen ebenfalls auf sauber aufräumende Programme. Aus Sicht des ANSI C bzw. ISO C++ Standards ist es einfach nicht definiert, ob ein Betriebssystem aufräumt und daher muß man es selbst erledigen.
  2. Nun schreiben viele Leute ja Programme, die nicht portabel sind bzw. sein sollen. Und auch hier sollte man sich vergegenwärtigen, daß die Betriebssysteme eben nicht alles können: Denkt man z.B. an Resourcen wie Semaphoren, Shared Memory usw.; also Dinge, welche tendenziell von mehreren Prozeßen gleichzeitig genutzt werden, so ist es schnell um die Saubermachqualitäten des Betriebssystems geschehen: die meisten Systeme räumen diese Art von Resourcen eben nicht auf. Aber auch Resourcen, wo für das Betriebssystem nicht ohne weiteres erkennbar ist, ob sie absichtlich belegt bleiben, werden nicht weggeräumt: man denke z.B. an temporäre Dateien, welche nach und nach die Festplatte zumüllen.

Es lohnt sich also generell Gedanken darüber zu machen, was mit den belegten Resourcen geschehen soll, wenn das Programm sich beendet und geeignete Strategien haben, diese Resourcen eigenständig freizugeben.

2. Unterschiedliche Strategien in C und C++

C und C++ verwenden tendenziell unterschiedliche Strategien zur Resourcenverwaltung und damit Freigabe: in C++ werden um Resourcen - wie z.B. Dateien - meisten Klassen "herumgebaut", so daß ein geeigneter Destruktor für die Freigabe sorgt. Dagegen muß in C die Resourcenverwaltung meist komplett händisch erledigt werden: Im folgenden C Programm, welches die Datei quelle.txt nach ziel.txt kopiert, müssen die Dateien explizit mittels fclose geschlossen werden (vgl. Zeilen 12 und 13):










10 
11 
12 
13 
14 
#include <stdio.h>

int main() {
    FILE* in = fopen("quelle.txt"r);
    FILE* out = fopen("ziel.txt"w);
    int c;

    while((c=fgetc(in)) != EOF) {
        fputc(c, out);
    }

    fclose(out);
    fclose(in);
}

Beim korrespondierenden C++ Programm sind die Aufräumarbeiten nicht explizit notwendig: die Destruktoren der std::fstream Klassen erledigen das Schliessen der Dateien implizit:










10 
11 
12 
#include <fstream>
#include <algorithm>
#include <iterator>

int main() {
    std::ifstream in("quelle.txt);
    std::ofstream out("ziel.txt);

    std::copy(std::istreambuf_iterator<char>(in),
              std::istreambuf_iterator<char>(),
              std::ostream_iterator<char>(out));
}

Der Unterschied zwischen expliziter Resourcenfreigabe in C gegenüber der impliziten Freigabe in C++ bleibt nicht ohne Konsequenzen, was folgender Abschnitt verdeutlichen wird:

3. exit vs. return

Ein Programm kann auf mehrere Arten beendet werden: entweder man führt eine return Anweisung in main aus oder man ruft die Funktion exit auf. In C macht es keinen gravierenden Unterschied, ob welche der beiden Alternativen man wählt: wie oben beschrieben müssen alle Resourcen explizit freigegeben werden, also entsprechende Funktionsaufrufe zur Freigabe explizit codiert werden. In C++ sieht die Sache jedoch anders aus: Während die return Anweisung dazu führt, daß die main Funktion normal verlassen wird und damit auch die Destruktoren zu allen lokalen Variablen ausgeführt werden, ist die Verwendung der Funktion exit kritischer: exit ist eine Funktion, welche nicht zurückkehrt. In letzter Konsequenz bedeutet dies, daß die Destruktoren der lokalen Variablen nicht aufgerufen werden und somit die Resourcen nicht freigegeben werden. Um diesen Umstand zu illustieren, wollen wir eine Klasse temp_file betrachten: der Konstruktor erzeugt eine temporäre Datei von 1KB Größe und der Destruktor löscht diese Datei (um die Sache kurz zu halten verzichten wir auf weitere Methoden):










10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
// temp_file.h
#ifndef TEMP_FILE_H_INCLUDED
#define TEMP_FILE_H_INCLUDED

#include <cstdlib>
#include <cstdio>
#include <sstream>

class temp_file {
    std::string name;
    FILE* handle;
public:

    temp_file() {
        // Konstruiere Datei name
        std::stringstream sn;
        sn<<time(NULL)<<".tmp;
        name = sn.str();

        // Öffne Datei
        handle = fopen(name.c_str(), "w);
        fseek(handle, 1024, SEEK_SET);
    }

   ~temp_file() {
        // Schliesse und lösche Datei
        fclose(handle);
        remove(name.c_str());
    }
};
#endif

Der Header temp_file.h enthält Deklaration und Implementierung dieser recht einfachen Klasse: der Konstruktor (Zeile 14-23) erzeugt auf Basis des aktuellen Datums eine temporäre Datei, der Destruktor (Zeile 25-29) löscht diese Datei wieder. Das folgende Programm wendet temp_file nun an:










10 
11 
#include "temp_file.h"
#include <iostream>

int main() {
    temp_file tf; / temp. Datei erzeugen

    std::cout<<"exit ist doof!<<std::flush;

    //  return 0;
    exit(0);
}

Verläßt man dieses Programm - wie oben zu sehen - mit exit, so bleibt im aktuellen Verzeichnis eine Datei stehen, etwa mit dem Namen "1108223417.tmp". Entfernt man hingegen die Kommentarzeichen in Zeile 9 und kommentiert Zeile 10 aus, so daß das Programm mit der return Anweisung beendet wird, bleibt keine solche Datei übrig. Wie die Ausgabe des Programms nahelegt ist die Verwendung von exit, jedenfalls in dieser naiven Form, in C++ eine eher nicht empfehlenswerte Sache: da man im Zweifel nicht weiß, welche Resourcen eine Klasse intern verwaltet und durch den Destruktor freigibt, verbietet sich die Anwendung von exit weil sie potentiell Resourceleaks produziert.

Befolgt man die Regel in C++ niemals exit zu verwenden, so hat man einerseits keine Probleme und - im Vergleich zu C auch sicherere Programme, weil die Destruktoren implizit all das erledigen, was man in C nochmals extra hinschreiben müsste, aber allzuoft vergißt. Wenngleich man also speziell im Falle von C++ generell nur mit return seine Programme beenden sollte, so ist dennoch die Frage interessant, wie man diese unangenehme Eigenschaft von exit umgehen kann (da in der Regel jedoch keine Garantie besteht, daß alle in einem Programm verwendeten Klassen den im folgenden demonstrierten Mechanismus nutzen, schützt er nicht generell vor Resourceleaks, sondern nur bei den Klassen, welche diesen Mechanismus nutzen).

4. atexit - Einbinden von Exithandlern

exit ist eine Funktion, welche prinzipiell an beliebiger Stelle im Programm auftauchen kann. Nur leider ist nicht an jeder Stelle bekannt, welche Resourcen denn nun bei Programmbeendigung freizugeben sind: wird exit etwa in einer tief verschachtelten Routine aufgerufen, müssten alle freizugebenden Resourcen wahlweise in globalen Variablen stehen oder als Parameter mit an die Funktion übergeben werden, damit man vor dem exit Aufruf die Resourcen auch freigeben kann. Beides Alternativen sind nicht wirklich praktikabel. ANSI C stellt stattdessen das Konzept der sogenannten Exit-Handler zur Verfügung. Ein Exithandler ist eine Funktion, welche keine Parameter entgegennimmt und keinen Rückgabewert hat, also mithin eine Funktion folgender Signatur:

void exit_handler(void);

Man kann mit Hilfe der Funktion atexit Exithandler registrieren, was bedeutet, daß der Exithandler aufgerufen wird, wenn das Programm durch den Aufruf der Funktion exit oder durch eine return Anweisung in main beendet wird. Die Anzahl der registrierbaren Exithandler ist begrenzt, jedoch sind mindestens 32 möglich. Wird ein Programm beendet, so werden alle registrierten Exithandler ausgeführt (der jeweils zuletzt registrierte Handler wird zuerst ausgeführt). Um die Wirkungsweise der Exithandler zu demonstrieren, wollen wir die oben eingeführte Klasse temp_file dahingehend erweitern, daß mit Hilfe eines Exithandlers alle temporären Dateien gelöscht werden, auch dann, wenn das Programm mit exit verlassen wurde. Die prinzipielle Idee der folgenden Implementierung ist, daß die Klasse temp_file intern eine Liste der erzeugten Instanzen verwaltet (Zeile 15): wird ein temp_file Objekt erzeugt, wird es in die Liste eingefügt (Zeile 36); wenn es destruiert wird, wird es aus der Liste genommen (Zeile 54). Ein Exithandler (18-22) geht nun durch diese Liste, und ruft für alle noch nicht destruierten temp_file Objekte den Destruktor auf. Der Exithandler wird einmalig bei der Konstruktion der ersten temp_file Instanz mittels atexit registriert (Zeile 29-33):










10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
// temp_file.h
#ifndef TEMP_FILE_H_INCLUDED
#define TEMP_FILE_H_INCLUDED

#include <cstdlib>
#include <cstdio>
#include <sstream>
#include <set>

class temp_file {
    // Initialisierungs flag
    static bool init_once_done;
    
    // Liste der offenen Dateien
    static std::set<temp_file*> open_files;

    // Exithandler - ruft Destruktor fuer alle offnen Dateien auf
    static void close_all_files(void) {
        while(open_files.size()>0) {
            (*open_files.begin())->~temp_file();
        }
    }

    std::string name;
    FILE* handle;
public:

    temp_file() {
        // Wenn noch nicht geschehen, registriere exit handler
        if(!init_once_done) {
            atexit(temp_file::close_all_files);
            init_once_done = true;
        }

        // Füge uns in Liste ein
        open_files.insert(this)

        // Konstruiere Datei name
        std::stringstream sn;
        sn<<time(NULL)<<".tmp;
        name = sn.str();

        // Öffne Datei
        handle = fopen(name.c_str(), "w);
        fseek(handle, 1024, SEEK_SET);
    }

    ~temp_file() {
        // Schliesse und lösche Datei
        fclose(handle);
        remove(name.c_str());

        // Entferne uns aus Liste
        open_files.erase(this);
    }
};
#endif

Benutzt man diese Implementierung in dem Beispielprogramm im vorherigen Abschnitt, so erkennt man, daß die temporären Dateien jedenfalls gelöscht werden, auch dann, wenn das Programm mit exit beendet wird. Wie bereits oben ausgeführt ist die Verwendung von Exithandlern jedoch nicht der Weisheit letzter Schluß: die meisten Klassen werden in der Regel keinen Exithandler verwenden und damit ohnehin potentielle Resourceleaks verursachen, wenn das Programm mit exit beendet wird. Angewendet im reinen C Kontext haben Exithandler jedoch sicherlich ihre Berechtigung und werden in ähnlicher Weise eingesetzt, wie oben demonstriert - nur eben mit Hilfe von reinen C Mitteln.

5. Fazit

Zusammenfassend kann man folgende Aussagen treffen:

  1. Generell sollte ein Programm - ob portabel oder nicht - Resourcen, welches es belegt hat, auch selbstständig freigeben.
  2. Speziell in C++ Programmen sollte auf die Verwendung von exit verzichtet werden.
  3. Zur Vereinfachung von der Resourcenfreigabe kann man sich der Funktion atexit bedienen, die einem das Leben - primär in C Programmen - leichter macht, wenn dort doch mal exit verwendet wurde.

Ihre Meinung ist mir wichtig

Haben Sie einen Fehler gefunden, wollen etwas anmerken oder fragen? Gefällt Ihnen etwas nicht oder wollen Sie ein Lob loswerden? - Nur raus damit!