Die Sprache C
DIM Array
Während sich viele Sprachelemente von C, so ungewöhnlich die Sprache zunächst erscheinen mag, tatsächlich kaum von BASIC unterscheiden, gibt es bei bestimmten Datentypen einige Besonderheiten.
Eine nicht zu unterschätzende Schwierigkeit und Fehlerquelle besteht im Zusammenhang mit Feldern (Arrays), die in modernem BASIC zumeist sehr viel flexibler definiert (dimensioniert) und indiziert werden können. 1
Es sei ein Integer-Array von 20 Elementen angelegt. In BASIC ist dies durch Anweisungen wie
Dim intarr%(20) Dim intarr1%(0 To 19) Dim intarr2%(1 To 20)
aber auch
Dim intarr3%(1981 To 2000)
möglich, in C nur durch
int intarr[20];
womit also lediglich die Elementanzahl festgelegt wird. Die Indices reichen dann von 0 bis 19 (oder allgemein gesagt: immer von 0 bis Elementzahl − 1).
Eine falsche Array-Indizierung ist also recht leicht möglich (meines Erachtens fast schon naheliegend), wenn man die Flexibilität von BASIC gewöhnt ist oder auch einfach vergisst, die Indizierung bei 0 zu beginnen und vor dem Maximum zu beenden:
#define MAXIMUM 20 // ... int intarr[MAXIMUM]; // Array mit 20 Elementen // ... intarr[MAXIMUM] = 123; // schon falsch!!
Vor diesem Fehler dürfte uns der Compiler zwar warnen, weil der Index eine Konstante ist, die nicht mit der Array-Definition übereinstimmt. Zur Laufzeit sind aber »Fehlgriffe« möglich, die bei der Code-Übersetzung nicht erkannt werden.
Berühmt-berüchtigt sind Fehler bei der Definition von Schleifen, wie bspw.
for (n = 0; n <= MAXIMUM; ++n)
wobei der Index n über das Ende des Arrays hinausläuft.
Strings …
Der Unterschied zwischen BASIC und C ist vielleicht nirgendwo größer als bei dem Datentyp String. In BASIC ist ein String tatsächlich ein Objekt, dessen konkrete Verwirklichung – nützlicherweise – verborgen ist. Der fundamentale Datentyp String ist jedoch schlichtweg ein Speicherbereich einer bestimmten Länge. 2 Um diesen für (variablen) Text nutzbar zu machen, wird in C ein Null-Byte verwendet, um das Textende zu kennzeichnen. Die String-Funktionen in C interpretieren das (erste) Null-Byte als String-Ende und können folglich nicht auf beliebige Speicherauszüge angewandt werden. 3
In C gibt es keinen Datentyp String, sondern nur den eines einzelnen Zeichens (char). 4 Ein String ist folglich ein char-Array konstanter Länge:
char string[20];
Dass Strings in C schlichte Zeichenarrays mit der normalen Indizierung sind, wird leicht übersehen. Im folgenden Beispiel soll das letzte Zeichen eines Strings durch ein Ausrufezeichen »!« ersetzt (überschrieben) werden. Die möglichen Realisierungen in BASIC und C (jeweils ohne Deklarationen und String-Initialisierungen):
n% = Len(string$) Mid$(string$, n%) = "!"
n = strlen(string); string[−−n] = '!'; /* string[n] = '!'; <- das wäre falsch!! */
In C ist die Position (genauer: der Array-Index) des letzten Zeichens um 1 kleiner als die String-Länge (d.i.: die
Elementanzahl). Der Wert von n muss also erst entsprechend dekrementiert werden!
Der Dekrement-Operator −− wird hier als Präfix eingesetzt, weil die Dekrementierung vor der Bewertung des Ausdrucks erfolgen muss. Natürlich wäre stattdessen auch die Indexbezeichnung n − 1 möglich (womit der Wert von n unverändert bliebe).
Wer den komfortablen Umgang mit Strings in BASIC gewöhnt ist, auf den wirkt C in dieser Hinsicht wie eine kalte Dusche. C kennt (mit einer Ausnahme) überhaupt keine Operatoren, die auf einen String als Ganzes angewendet werden können. Die Übertragung eines BASIC-Codes wie
hallo$ = "Hallo, Leute!" salute$ = hallo$ + " Wie geht's?" If salute$ = greeting$ Then ' ...
kann einen Umsteiger erstmal zur Verzweiflung bringen. Effektive (und portable) Programme sind eben nicht »umsonst« zu haben.
Immerhin stellen die C-Standardbibliotheken eine Vielzahl von Stringfunktionen bereit, so dass die Übertragung dieses Codes nicht wirklich schwierig ist. Es ist hier letztlich die (transparente) dynamische String- bzw. Speicherverwaltung, durch die sich BASIC wesentlich von C unterscheidet. Ihr Fehlen zwingt den C-Programmierer einfach dazu, sich wenigstens ein paar Gedanken über Speicherreservierung und erforderliche Stringlängen zu machen.
Damit führt der (unfreiwillige) Verzicht auf manchen Komfort aber auch zu einem effektiveren Programmierstil. So wird sich der C-Programmierer – vielleicht eher als der BASIC-Programmierer – fragen, ob die verschiedenen Strings
und String-Operationen des obigen Beispiels wirklich notwendig sind. 5
Jedenfalls werden wir durch C mit der Tatsache konfrontiert, dass Operationen wie die Zuweisung (=), die Verknüpfung (+ oder &) und der Vergleich (=) von Strings nur scheinbar triviale Vorgänge darstellen. Dabei sind wir bei der Arbeit mit Strings durchaus nicht ohne Unterstützung, wie wir im Zusammenhang mit Stringkonstanten bereits gesehen haben. Auch in den folgenden Anweisungen werden wir durch den C-Compiler unterstützt:
char string[20] = {0}; // Initialisierung mit // 20 Null-Bytes
char string2[] = {"Hallo"}; // erzeugt ein Array mit 6 (!) // Zeichen (inkl. Null-Byte)
char *strptr; // Deklaration eines Zeigers, strptr = "Hallo!"; // der jetzt auf einen konstanten // String (inkl. Null-Byte) zeigt
Das letzte Beispiel erkläre ich gleich näher. Vorab sei gesagt, dass es jedenfalls den einzigen Operator zeigt, der in C auf Strings (und alle anderen Arrays) angewendet werden kann: die Zuweisung der Speicheradresse an eine Zeigervariable. Das ist aber nicht dasselbe, wie die Zuweisung
s$ = "Hallo!"
die uns von BASIC her vertraut ist!
Neben der fälschlichen Operatorenanwendung auf Strings (oder dem Vergessen des »blöden« Nullzeichens) lauern hier also einige Fallen.
… und Zeiger
Nun kommen wir zu einer anderen Besonderheit von C, die dem Neuling nicht weniger Ungemach bereitet: dem Zeiger.
Ein Zeiger (pointer) ist eine Variable, die auf ein Datenobjekt (oder eine Funktion) zeigt, also dessen Speicheradresse enthält. Die Deklaration ist der anderer Variablen fast gleich, allgemein erfolgt sie durch
typ *name;
wobei sich typ auf das referenzierte Datenobjekt, nicht auf den Zeiger selbst bezieht. 6 Der angegebene Datentyp bestimmt also die Verwendung des Zeigers. Der Stern (*) legt fest, dass es sich um einen Zeiger handelt und ist nicht Bestandteil des Variablennamens.
Mit den beiden Anweisungen des vorigen Beispiels
char *strptr; strptr = "Hallo!";
haben wir einen Zeiger auf Zeichen (char) deklariert und ihn auf die Stringkonstante zeigen lassen. Um diese Adresszuweisung zu verstehen, müssen wir uns vielleicht noch einmal klarmachen, dass C keinen Datentyp String kennt. Eine Stringkonstante ist ein (namenloses) char-Array! Der Verweis auf ein Array – als Ganzes! – ist aber in C gleichbedeutend mit dessen Anfangsadresse, einem Zeigerwert also. Wir könnten also das Beispiel um die Anweisung
strptr = string;
ergänzen und hätten damit die Adresse der Stringkonstanten Hallo! verworfen. Stattdessen zeigt unsere Zeigervariable nun auf das weiter oben definierte Array string, ihr Wert ist also die Adresse von string[0], dem ersten Array-Element.
Durch den Adressoperator & erhalten wir die Adressen für andere (beliebige) Datenobjekte:
char string[20]; int n; double summe; char *strptr; int *intptr; double *dblptr; intptr = &n; dblptr = &summe; strptr = &string[5];
Durch den Verweisoperator * erhalten wir wieder den Inhalt einer Adresse, salopp gesagt also das, worauf ein Zeiger zeigt:
int a, b, *ip; a = 5; ip = &a; // ip zeigt auf a b = *ip; // b = 5, entspricht b = *(&a) *ip = 0; // a = 0, ip selbst ist unverändert
Mit diesen Beispielen habe ich nur die allerwichtigsten Aspekte von Zeigern demonstriert. Anstatt weitere nützliche (und verwirrende) Eigenschaften von Zeigern aufzuführen, möchte ich einen einfachen, konkreten Fall vorstellen, bei dem Zeiger benötigt werden.
Erinnern wir uns, dass C-Funktionen ihre Parameter als Werte (by value) erhalten. Es sei ergänzt, dass dies nicht für Arrays zutrifft, hier erhält eine Funktion tatsächlich die Anfangsadresse. (Wie wir oben gesehen haben, entspricht ein Array-Name ohne Indizierung der Adresse des ersten Elements.)
Aber was ist, wenn wir einen anderen Datentyp by reference übergeben wollen, damit die Funktion den Wert ändern kann? Wir definieren den Parameter als Zeiger!
Die folgende Funktion vertauscht die Werte der beiden Parameter. (Das Beispiel ist in ähnlicher Form bei Kernighan/Ritchie [Programmieren in C, Hanser 1983] zu finden.)
void swap(int *a, int *b) { int tmp; tmp = *a; *a = *b; *b = tmp; }
Die Funktionsparameter sind vom Typ Zeiger auf int, so dass wir auf die Integerwerte durch den Verweisoperator zugreifen können.
Bei dem Funktionsaufruf verwenden wir den Adressoperator, um die Adressen der Variablen zu übergeben:
int i, n; i = 11; n = 2; swap(&i, &n);
Die Funktion gibt selbst keinen Wert zurück (void). In der Praxis benutzt man aber meistens den Funktionswert, um die Wertänderung eines Parameters zu erhalten. Funktionen, denen die Adressen elementarer Datentypen übergeben werden, sind deshalb eher selten.
Anders liegt der Fall natürlich, wenn die Parameter zusammengesetzte Datentypen sind. Die Funktion printf(), die in früheren Beispielen vorgekommen ist, erwartet (neben weiteren, optionalen Parametern) einen char-Zeiger, also die Adresse eines Strings. Deshalb können wir der Funktion ja eine Stringkonstante übergeben. Das gleiche gilt für viele andere Funktionen der C-Standardbibliothek.
Auch der Rückgabewert einer Funktion kann übrigens ein Zeiger sein.
String-Funktionen
Damit können wir noch einmal zu der Arbeit mit Strings zurückkehren. Die Exkursion in das Gebiet der Zeiger erfolgte aus zwei Gründen: Einmal besteht, wie wir gesehen haben, ein enger Zusammenhang zwischen Arrays (also auch Strings) und Zeigern. Zum anderen ist die Verwendung von Strings ohne die (explizite) Verwendung von Zeigern praktisch unmöglich.
Um dies noch etwas deutlicher zu machen, widmen wir uns der Übersetzung des weiter oben schon gezeigten BASIC-Codes (einer kleinen String-Orgie):
hallo$ = "Hallo, Leute!" salute$ = hallo$ + " Wie geht's?" If salute$ = greeting$ Then ' ...
Wie ich dort betont hatte, müssen wir uns bei der
Programmierung in C darüber klar sein, was wir wollen, während
wir in BASIC um einiges gedankenloser sein dürfen.
Gehen wir also davon aus, dass wir hallo$ und salute$ noch an
anderer Stelle benötigen. (Anderenfalls hätten wir die
Zuweisungen wohl vermieden und den Vergleich einfach nur mit
einer Stringkonstante vorgenommen.) Dabei soll hallo$ aber
konstant sein, während sich der Inhalt von salute$ ändern kann.
Deshalb müssen wir die maximale Länge des zusammengesetzten
Strings salute$ festlegen. Entscheiden wir uns für den Wert 100,
so bedeutet das, dass wir maximal 99 Zeichen plus einem Null-Byte
speichern können. Schließlich gehen wir davon aus, dass
greeting$ ein gültiger, anderswo (extern) definierter String
ist.
Solche Überlegungen müssen wir anstellen, bevor wir den Code nach C übersetzen können! Der kritische Punkt liegt bei den Stringkopien (und ‑verknüpfungen): C reserviert dafür keinen Speicher – es erwartet ihn!
Falls greeting$ modulextern definiert wurde, werden wir die Variable durch die Deklaration
extern char greeting[];
in diesem Modul bekanntmachen müssen.
Im Übrigen sieht die C-Version dann so aus:
char *hallo = "Hallo, Leute!"; char salute[100]; strcpy(salute, hallo); strcat(salute, " Wie geht's?"); if (strcmp(salute, greeting) == 0) // ...
Durch die Stringfunktionen der C-Bibliothek, deren Bezeichnungen zwar knapp, aber doch verständlich sind, können wir auch in C die gewünschten Operationen durchführen. Das Ergebnis ist dem des BASIC-Codes effektiv gleich – oder besser gesagt: scheinbar gleich. Diese Einschränkung muss ich machen, weil »hinter dem Vorhang« eben doch wichtige Unterschiede bestehen. Ein C-»String« ist, um es noch einmal zu sagen, kein dynamisch verwaltetes Datenobjekt, sondern ein Stückchen Speicher, das irgendwo ein Null-Byte als Endemarkierung enthält. Die C-Stringfunktionen bewirken folglich nicht dasselbe wie die Stringoperatoren in BASIC.
Zur Verdeutlichung mag das folgende Programm dienen, das die Header-Datei für die Stringfunktionen einbindet und in der Hauptfunktion eine – fehlerhafte – Stringkopie vornimmt.
#include <string.h> int main(void) { char hallo[10]; char hallo2[] = {"Dieser String ist aber länger!"}; strcpy(hallo, hallo2); return 0; }
Es wird Ihnen schon klar sein: Die Funktion strcpy() kopiert den Inhalt von hallo2[] (Quelle) nach hallo[] (Ziel). Ihre Parameter sind Zeiger, also schlichtweg Speicheradressen. Durch den fehlerhaften Kopiervorgang wird an der Zieladresse ein zu großer Speicherinhalt überschrieben, was in aller Regel zu Problemen beim weiteren Programmlauf führt.
Anmerkungen:
1) Auf die neueren Versionen von Visual Basic (VB.NET) trifft dies leider nicht mehr zu: Hier ist lediglich die Angabe des oberen Indexwertes für ein 0-basiertes Array zulässig bzw. gültig. Die Elementanzahl liegt dann um 1 darüber!
2) Die PC-Systemarchitektur definiert einen fundamentalen Datentyp String allerdings nur in der Weise, dass sie ihn durch spezielle Prozessoranweisungen (Stringbefehle) unterstützt.
3) Zur Verdeutlichung: BASIC »kennt« die Länge eines Strings. (Sie ist im String-Deskriptor enthalten.) Wo es diese Information nicht gibt, wird zwangsläufig eine Endemarkierung benötigt. Die Verwendung des Null-Zeichens (ASCII 0) zu diesem Zweck ist weit verbreiteter Standard. Wer sich mit DOS-Funktionen (Interrupt 21h) befasst, wird aber feststellen, dass die alte Funktion zur Stringausgabe (09h) hierfür das Dollarzeichen verwendet. (Gerüchte besagen, dass dies auf Bill Gates zurückgehen soll.)
Die C-konformen Strings werden häufig mit der Kurzbezeichnung ASCIIZ bedacht, wobei das »Z« für den Abschluss durch Zero (Null als Code-Wert) steht.
4) Wie schon gesagt, ist char ein numerischer Datentyp, der den 1-Byte-Wert eines Zeichens aufnimmt. Für Multi-Byte-Zeichen (insb. gem. Unicode bzw. UCS) ist zusätzlich der Datentyp wchar_t definiert, und die C-Standardbibliothek enthält entsprechend angepasste Stringfunktionen.
5) Es ist aber selbstverständlich so, dass dieser BASIC-Komfort die Konzentration auf die eigentliche Anwendungsentwicklung unterstützt. Dass dies auch in anderen Sprachen wünschenswert sein kann, zeigt etwa die Bereitstellung von Stringobjekten in C++-Klassenbibliotheken.
6) Die interne Repräsentation eines Zeigers ist grundsätzlich von der Systemumgebung abhängig. Auf PC-Systemen – mit unsegmentiertem Speichermodell – stellen Zeiger entweder 32- oder 64-Bit-Adressen dar. Die Unterscheidung zwischen near und far Pointern hat vor allem historische Gründe (früher: 16- oder 32-Bit-Adressen) und ist heute weitgehend bedeutungslos. (Beim segmentierten Speichermodell bezeichnet Far Pointer auch die Kombination aus einem Segmentselector und dem Adresswert, mithin einen 48-Bit-Zeiger.)