Zur Organisation der Variablen und der Datenübergabe
In dem ganzen Programm gibt es keine einzige Globale Variable.
Was wir haben, sind globale Variablen, die als globale Konstanten verwendet werden (read only), obwohl sie nicht so deklariert sind. Dabei wäre es praktisch und auch ungefährlich, würde man etwa den sehr häufig verwendeten Buffer struct datensatz{} zumindest mit einem Datensatz global machen. Dann könnten alle Funktionen auf die Datenfelder zugreifen, die eine setzt das Datum ein, die andere den Brutto-Betrag und so weiter, selbst wenn dieser eine Datensatz beschädigt würde, würde nicht viel geschehen, weil es bei der Überprüfung vor dem Abspeichern herauskäme.
Wenn man darauf verzichtet, global zu deklarieren, hat man im Übrigen ein Problem mit der Werteübergabe. Zwar kann man auch sehr große Structures CALL BY VALUE übergeben, dann wird eine Kopie des Datensatzes oder des Datensatz-Arrays auf den Stack gelegt, aber große Datenmengen zu kopieren verschlechtert die Performance und führt möglicherweise irgendwann zum STACK-OVERFLOW.
Will man also konsequent auf Globale Variablen verzichten, bleibt nur die Werteübergabe mit einem Pointer/bzw. Adreßoperator.
Was ist an Globalen Variablen so schlimm?
Nun, ich seh das so, daß alles Globale grundsätzlich die Stabilität des Programms gefährdet. Der Grund ist nicht, daß man ein Programm nicht so schreiben könnte, daß es nicht laufen würde, sondern der Grund ist, daß es mit einem einfachen Programmgerüst beginnt, aber nicht dabei bleibt, es kommt Funktion um Funktion dazu, und die anfängliche Übersicht geht mit der Zeit zunehmend verloren. Im Prinzip ist es so, daß man über die früher geschriebenen Programmbestandteile irgendwann überhaupt keine Übersicht mehr hat. Man muß sich blind drauf verlassen können, daß diese als stabile Bausteine in spätere Funktionen problemlos eingebunden werden können. Selbst wenn es bis jetzt noch alles gut geht, kommt der Punkt der Wahrheit immer dann, wenn durchgreifende Änderungen durchgeführt werden müssen. Man wird nicht drumherumkommen. Man muß es entweder, oder man will es, um den Code kompakter zu machen.
Welches sind nun die Nachteile einer Organisation mit Globalen Variablen?
Die Liste wird lang und hat es in sich.
Die Hauptfehlerquelle in ANSI-C ist mit großem Abstand der Bereichsüberlauf. Das heißt, es werden mehr Daten geschrieben, als Platz vorgesehen ist. Das passiert z. B., wenn ein kleiner Datentyp mit einem großen überschrieben wird, beim Array-Überlauf insbesondere auch der char-arrays. Das Ergebnis ist, daß das Programm mit nur EINER EINZIGEN SOLCHEN ANWEISUNG praktisch unbrauchbar ist. Man erkennt es gelegentlich, daß bei der Ausgabe ein seltsames ASCII-Zeichen erscheint. Das ist ein WARNHINWEIS darauf, daß wir Bereichsüberlauf haben. Leider erkennt man es nicht immer, manchmal gar nicht.
Und ein Programm kann man, nach anerkannter Meinung, niemals zu 100Prozent austesten, ob es stabil läuft. Weil es keine Maschine bzw. keinen vorstellbaren Algorithmus gibt, die in einem Programm alle denkbaren Zustände provozieren und austesten kann. Es ist gerade bei C-Programmierung ein SEHR GUTER GEDANKE, alles zu vermeiden, was auch nur am Rande der Vorstellung zu Instabilitäten führen KÖNNNTE, bzw. sich stets die stabilere Alternative herauszusuchen, selbst wenn die andere "cooler" aussieht und kompakter ist.
Nun zu dem, warum Globale Variablen die Programmstabilität negativ beeinflussen.
1. Verwechslung
Es kann in C derselbe Bezeichner für mehrere Variablen verwendet werden. Wenn eine Variable foreward deklariert ist, ist sie Global für alle Funktionen, es können aber Variablen auch zwischen den Blöcken stehen, sie sind dann Global für alle nachfolgenden Funktionen, und innerhalb der Funktion. Zur Laufzeit wird diejenige Variable verwendet, die "am lokalsten" ist. Es kann dann sein, insbesondere nach Programmänderungen, daß ein anderer als der gewünschte Wert verändert wird.
2. Programmiertechnisch falsche Zuweisung/Bereichsüberlauf
Wenn der Typ der Variable verwechselt wird, kann es sein, daß man dieser einen logisch richtigen Wert zuweist vom falschen (zu großen oder unpassenden) Datentyp. Das ist sehr schnell passiert. Ein Sonderfall ist sprintf()
Die Variable int i kann mit sprintf("%s",i) mit einem Wert für ein char-Array überschrieben werden, also vom falschen Typ. Ergebnis: Bereichsüberlauf und Vernichtung des vorherigen Wertes. Bei sprintf() erhalten wir zur Laufzeit KEINE Fehlermeldung.
3. Logisch falsche Zuweisung
Es wird der Wert mit einem korrekten Datentypen zugewiesen, aber (meistens aufgrund von Programmänderungen) die Zuweisung ist logisch falsch. Da andere Funktionen auch auf diese globale Variable zugreifen, geschehen nun die tollsten Dinge.
4. Bereichsüberlauf einer Nachbarvariable
Wenn im Speicherbereich der globalen Variable andere globale Variablen abgelegt sind (nach meiner EInschätzung relativ häufig, ich schätze, die liegen in einem Block alle zusammen), führt die Bereichsüberschreitung bei einer anderen Variable dazu, daß im globalen Block mehr Schaden auftritt, also auch andere Globale Variablen betroffen sind. Dies ist im Hinblick auf eine logische Fehlersuche sehr schwierig einzukreisen.
5. Vergessene bzw. historische Anweisungen. Diese befinden sich als Datenmüll noch als ÜBerbleibsel vorangegangener Versionen in irgendwelchen Funktionen, oftmals nur als eine Zeile oder als Operand innerhalb einer Zeile. Sowas ist je nachdem sehr schwer zu finden.
6. Bereichsüberlauf infolge Änderung des Datentypes, ist besonders bei Structures sehr häufig. Wenn man die einzelnen Datenfelder ändert, in Länge oder Reihenfolge, läuft die Wertezuweisung n die nächsten Felder über. Das ist je nachdem auch nicht einfach zu finden.
7. Bereichsüberlauf infolge Fehldefinition oder falschem Zugriff (Verwechselung INDEX mit Anzahl Elemente). Ein in C SEHR HÄUFIGER FEHLER
Beispiel:
char datum[10]="01.01.2012" ist ein Feld zu
klein, kein Platz für die Stringterminierung. Die Stringterminierung läuft also über.
Beispiel für falschen Zugriff: int wochentage[7]={1,2,3,4,5,6,7}; Wer nun auf den Wochentag[7] zurückgreift, greift ins Klo.
Nun, einige dieser Fehler können bei LOKALEN VARIABLEN ebenso geschehen.
Warum wäre das weniger schlimm?
1. Speicherort Stack
Das ist schon von daher erstmal "weniger" schlimm ("weniger" schlimm ist aber auch schlimm), weil lokale Variablen erst beim Funktionsaufruf erzeugt, auf den Stack gelegt und anschließend wieder freigegeben werden. Der Stack ist ein separater Speicherbereich, und ein Bereichsüberlauf einer Variablen im Stack hat wenig Chancen, andere Variablen zu beschädigen, da LIFO-Stack, last in first out, sich hinter der letzten Variable im Stack nichts "sensibles" befindet. Das ist absolut behauptet nicht ganz richtig, aber im Prinzip ist es so. Bereichsüberlauf einer Variable im Stack endet im Niemandsland und richtet keinen (weiteren) Schaden an.
2. Bessere Eingrenzung bei der Fehlersuche
Da bei auftretenden Fehlern bei lokalen Variablen als "Täter" nur die aufrufende Funktion infrage kommt, hat man diese ziemlich schnell an den Hammelbeinen.
3. Bessere Stabilität bei Programmänderungen
Ändert man die Definitionen von Variablen (bei struct{} ist das relativ häufig), sind dadurch alle bearbeitenden Funktionen betroffen. Die müßte man nun allesamt heraussuchen und ändern. Es gilt hier das, was schon oben gesagt wurde.
Beispiel: Würde man das Namensfeld innerhalb einer Struct von char name[50] auf char name[20] verkürzen, kann natürlich sehr leicht ein Bereichsüberlauf die Folge sein, welcher in die Folgefelder hineinschwappt, oder auch über den Datensatz hinaus auf andere Variablen im Block der Globalen. Organisieren wir lokal, haben wir die Deklaration der Variablen im lokalen Block, und das Ergebnis auf dem Stack. Der Bereichsüberlauf kann also höchstens sich selbst beschädigen, und ist so leichter zu finden.
Zusammengefaßt: GLOBAL vs. LOKAL kann man sagen, daß die lokale Organisation folgende Vorteile hat:
1.) Die Schadensausbreitung bei fehlerhaftem Code wird stark begrenzt
2.) ist wg. Effekt 1) die Fehlersuche wesentlich einfacher als wenn man global organisiert.
3.) ist das Programm fehlerbehaftet, d.h., gelingt es nicht, es VOLLSTÄNDIG fehlerfrei zu machen (DAMIT MUSS MAN EIGENTLICH ALS NORMALFALL RECHNEN),
läuft ein Programm mit Fehlern, welches lokal organisiert ist, zu 99,xx Prozent dennoch fehlerfrei, während bei der globalen Organisation die berühmten
"seltsamen Effekte" auftreten, die SEHR SCHWER eingrenzbar sind.
Jetzt kommt jemand und sagt: aber ein Programm muß doch absolut fehlerfrei laufen!
Dem ist zu antworten:
Solche Programme kann es im Prinzip gar nicht geben. Jedenfalls nicht, wenn es sich um komplexe Programmierung handelt.
Und im Gegensatz zu einem Werkstück, welches sagen wir auf 0.01 Toleranz gefertigt ist, hat man bei einem Programm nicht die Möglichkeit, es zu
"vermessen", weil die Programmzustände nicht allesamt vorausberechnet werden können. Ich glaube, der Turing hat das schon vor sehr langer Zeit
vorausgesagt.
Es gilt sozusagen die alte Zimmermanssweisheit, mit der die Engländer ihre Schiffe bauten: nimm alte englische Eiche und mache es stabiler, als es muß.
---------------------------
Jetzt mal ein kleiner Blick auf die praktische Handhabung der Variablen Definition und Deklaration.
Wenn ich eine Variable deklariere, nehme ich den Typ und weise ihr Speicherplatz zu:
int i; // schafft 4 Byte Speicher in einem 32 Bit-System für die Werte von i, ca. -32000 bis + 32000
Wenn ich eine Struktur benötige, muß ich den Datentyp erst definieren:
struct adresse{
char name[50];
char ort[50];
int telefon;
}; // (1)
Ich kann ihn auch so definieren, als Datentyp:
typedef struct{
char name[50];
char ort[50];
int telefon;
} adresse; // (2)
Das sind beides nur DEFINITIONEN, keine DEKLARATIONEN, weil beides keinen Speicher anfordert, also keine Daten aufnehmen kann.
Der Unterschied ist, den Speicher bekomme ich einmal mit:
struct adresse a; // (1)
bzw.:
adresse a; // (2)
Für meinen Geschmack ist die Version (1) besser. Die 2. ist zwar kürzer, aber man sieht bei der Deklaration nicht, daß es sich hier um eine struct
handelt. Es könnte leicht eine Typvewechslung stattfinden, was bei (1) nicht zu erwarten ist.
Die Definition hat mit der Frage GLOBAL oder LOKAL nichts zu tun. Ich kann Definitionen Global machen, also an den Anfang der Datei setzen oder foreward
erklären. Da mit der Definition aber noch kein Speicher angefordert ist, kann nichts passieren. Der Compiler nimmt die Definition wahrscheinlich nicht
mal in den EXE-Code der Datei auf, weil er bemerken dürfte, wenn mit der Definition nichts passiert.
Speicher gibt es erst mit der DEKLARATION.
Lokal wäre das so:
void mache_irgendwas()
{
struct adresse a; ...
Dann liegt der Speicherplatz auf dem Stack.
Ich kann natürlich auch sagen:
struct adresse a[50000];...
Und habe damit Platz für 50.000 Mitarbeiter auf den Stack gelegt (sowas empfiehlt sich eher nicht, da Platz auf dem Stack begrenzt, und die performance
geht irgendwann in die Knie, vor allem aber deshalb nicht, weil wir so auch viel Platz verschwenden, wenn es z.B. 20000 wären).
Um den Platz aus der Funktion heraus nicht auf dem Stack, sondern auf dem Heap anzufordern, müßte es heißen:
void mache_irgendwas()
{
struct adresse *a = malloc(50000*sizeof (struct adresse));
}
Was dasselbe bringt, nur nicht als indizierte Liste, sondern man muß mit dem Pointer drüberlaufen (ist intern sowieso dasselbe).
Man sieht, daß die Definition [50] kritisch ist. 50 ist irgendwas. Man könnte die mit define etwas eleganter hinbringen:
#define MAXLEN 50
Dann hieße es:
struct adresse{
char name[MAXLEN];
char ort[MAXLEN];
int telefon;
}; // (3)
und würden wir bei der Programmänderung andere Feldlängen wünschen, müßtenn wir nur schreiben:
#define MAXLEN 20
Richtig elegant ist aber weder das eine noch das andere. Denn in dem einen Fall sind die 20 zu knapp, in der Regel aber zuviel.
Viel besser wäre es, zur Laufzeit folgende Struktur verfügbar zu haben:
struct adresse{
char *name;
char *ort;
int telefon;
}; // (4)
Also einen Pointer auf den Anfang der char-arrays zu setzen, womit wir beliebig lange aber auch beliebig kurze Felder verfügbar hätten, es würde zur
Laufzeit kein Platz verschwendet, und die Anforderung
malloc(sizeof(adresse)) würde nur noch mindestens 12 Byte anfordern, nämlich 4 für int und jeweils 4 für die beiden Pointer, anstelle, wenn man jetzt
die Adressfelder noch komplettiert, mehrere hundert Byte.
Wenn wir mit malloc() arbeiten, können wir uns die geschätzten 50000 sparen, sondern fordern den Platz an, wie wir ihn brauchen (dynamisch zur
Laufzeit).
Die gesamte Funktion:
void mache_irgendwas()
{
struct adresse *a= malloc(sizeof (struct adresse));
a->name="Mueller";
printf("%s\n",a->name);
}
Hat die Bildschirmausgabe:
MuellerPress any key to continue . . .
Daß die Zuweisung mit dem = Operator möglich ist, hat damit zu tun, daß wir keine Zeichenkette, sondern eine Adresse zuweisen, nämlich die Adresse von
"Mueller". *a ist ja kein char-array, sondern ein Pointer auf ein char-array.
Diese Zuweisung mit Pointern (worauf die gesamte Sprache C ja ursprünglich ngelegt ist), vereinfacht auch Funktionsaufrufe, immer im Hinblick auf den
unbefriedigenden Sachverhalt, daß wir nicht wissen, wieviel Platz wir brauchen, und dafür ZUVIEL Platz vorhalten:
void mache(char info[255] ... // CALL BY VALUE
läßt sich eleganter formulieren mit
void mache(char *info ... // CALL BY REFERENCE
Das wäre alles gut und schön, müßten wir die Datensätze nicht speichern. Dann hat sich das.
Weil nämlich:
fwrite(&buffer, sizeof(...
den Sizeof Operator benutzt, und wir für unseren Pointer, der auf die Textkette zeigt, nur 4 Byte reserviert haben, würden wir die Datei zwar so
abspeichern können, aber eben mit der Adresse auf den Text, zur Laufzeit zum Zeitpunkt der Speicherung. Starten wir das Programm am nächsten Tag neu und
wollen das zurücklesen, sind die Daten natürlich verloren.
Um einen Datensatz zu speichern, kommen wir also von der Definition char text[n] nicht weg. Das läßt sich mit char *text eben nicht ersetzen, logisch.
Und zur Laufzeit mit anderen Datentypen zu handeln als die, die wir speichern wollen, nun ja. Denkbar ist vieles, nur nicht alles, was denkbar ist, ist
auch sinnvoll.
Womit wir beim letzten Punkt wären, wenn man auf GLOBALE VARIABLEN TOTAL VERZICHTEN WILL, bei der Werteübergabe.
Wir übergeben keine Werte mehr, sondern nur noch Adressen.
Damit sind wir sehr C-like, weil C legt es ja darauf an, obwohl (innerer Widerspruch) im Normalfall in C CALL BY VALUE vorgesehen ist, aber nicht
durchgängig, weil bereits der Aufruf einer Funktion:
mache(char info[255] ...
intern sowieso mit einem Pointer erfolgt. Es wird keine Textkopie übergeben, sondern die Adresse.
Man bemerkt das, wenn man
mache (char *info ...
mit dem Adreßoperator aufruft, nämlich:
mache (&text ...
Was zu einer Fehlermeldung führt, weil wir nicht die Adresse des Char-Arrays, sondern die Adresse des Pointers übergeben, welcher auf das char-array
zeigt.
Besonders Sinn macht die Werteübergabe per Adresse (CALL BY REFERENCE) nicht bei einfachen Datentypen, wie int, float, c, aber bei komplexen Datentypen
wie Strukturen. Als Folge davon brauchen die gar keinen Rückgabewert mehr, können VOID erklärt werden, es sei denn, man wollte wissen, ob das ERgebnis
OK oder NOTOK wäre.
Folgefunktion(en):
void mache_noch_mehr(struct adresse *aktuell) // Adreßoperator auf den zu bearbeitenden Datensatz
{
...
}
Aufrufende Funktion:
void mache_irgendwas()
{
struct adresse *buffer; // Lokale Deklaration buffer der Globalen Definition struct adresse
mache_noch_mehr(&buffer); // Adreßübergabe der lokalen Variable, Bei char-arrays verboten, dort ohne & Operator
Das sind die Gründe, warum das PRogramm ohne jede Globale Variable auskommt.
Ich hoffe, ich habe mich einigermaßen verständlich ausgedrückt (bin mir da selbst nicht immer so sicher).
Der Beitrag wurde von sharky bearbeitet: 26.02.2012, 15:49 Uhr