close

Anmelden

Neues Passwort anfordern?

Anmeldung mit OpenID

6 Wie kann man Programme übersichtlicher gestalten I ? (Un

EinbettenHerunterladen
6
Wie kann man Programme übersichtlicher gestalten I ? (Unterprogramme)
L
Ein Unterprogramm bietet dem Programmierer eine Leistung, die an den geeigneten
Stellen abgerufen werden kann. In C gibt es
“
(Echte) Funktionen, die ein Ergebnis an der Stelle ihres Aufrufs zurückliefern
“
Anweisungsfunktionen oder Prozeduren, die ggf. über Parameterveränderungen Werte an ihre Umgebung zurückgeben. Ihr Kennzeichen ist der Rückgabetyp void.
Es ist schlechter Programmierstil, wenn eine Funktion auch Parameter ändert.
L
Syntax:
Unterprogrammdefinition =
[ static | extern ] [ Typ ] Bezeichner ”(” Parameterliste ”)”
”{”
{ Deklaration_lokale_Variable }
{ Anweisung }
”}”
Für Prozeduren und Funktionen gibt es kein kennzeichnendes Schlüsselwort. Der
Compiler erkennt sie an der Parameterliste und dem nachfolgenden Rumpf. Daher
müssen die Klammern auch stehen, wenn die Parameterliste leer ist und es folgt kein
Strichpunkt. Innerhalb der Parameterliste gibt es keinen Strichpunkt. Komma trennt
verschiedene Deklarationen. Daher muss der Typ jedes Parameters einzeln angegeben
werden:
int f (typ1 a, typ2 b, typ3 c) { ... }
Da die alte Schreibweise auch noch möglich ist, bei der die Parameter nach der
schließenden Klammer deklariert werden, muss eine leere Parameterliste explizit als
(void) geschrieben werden!
Variable Parameterlisten (wie für scanf und printf) können drei Punkte ". . ."
(Ellipse) an letzter Stelle enthalten. Natürlich kann der Compiler dann keine Typprüfung mehr vornehmen.
Eine echte Funktion sollte wenigstens eine return-Anweisung haben.
46
L
Semantik:
“
Die static-Angabe wird später noch behandelt. Sie bestimmt, ob die Funktion in einem anderen Modul bekannt ist bzw. verwendet werden kann. Dadurch
wird eine gewisse Privatheit (information hiding) ermöglicht. Voreinstellung
ist extern.
“
Wird void angegeben, handelt es sich um eine Prozedur, sonst sollte wenigstens int angegeben werden (Voreinstellung). Eine Funktion sollte eine
return-Anweisung von dem Typ besitzen, der vor dem Funktionsbezeichner
steht.
“
Bezeichner klar
“
Die Parameterliste spezifiziert Bezeichner, die beim Aufruf der Funktion mit
den aktuellen Bezeichnern in Verbindung gebracht werden.
“
Die Variablen sind nur lokal, also innerhalb des Unterprogramms bekannt. Es
dürfen allerdings auch die globalen Variablen verwendet werden.3
“
Anweisungen: Es gibt zwei Möglichkeiten, sich die Wirkung eines Funktionsaufrufs zu veranschaulichen: Entweder stellt man sich den Anweisungsteil der
Funktion an die Stelle des Aufrufs kopiert vor oder man betrachtet den Aufruf
der Funktion als eine Sprung in den Programmtext der Funktion. In beiden
Fällen wird bei der Anweisung nach der Beendigung des Funktionsaufrufs
fortgefahren.
3
Auf die Sichtbarkeit von Variablen wird später noch eingegangen.
47
L
Beispiel für den Gebrauch eines Unterprogramms als Funktion:
/* Aufsummieren von Zahlen als Funktion*/
long int summe (int bis)
{
int lv ;
long int zwsumme = 0 ;
/* Ein Unterprogramm sollte die Bedingungen, unter
denen es korrekt arbeitet, genauestens pruefen. */
if (bis < 1) {
printf("Falsche Eingabe fuer Funktion summe!\n") ;
exit (1) ;
}
}
for (lv = 1 ; lv <= bis ; lv++) zwsumme += lv ;
return zwsumme ;
int main (void)
{
int von_1_bis = 37 ;
printf("Die Summe der Zahlen von 1 bis %d = %ld\n",
von_1_bis, summe(von_1_bis)) ;
}
von_1_bis = 139 ;
printf("Die Summe der Zahlen von 1 bis %d = %ld\n",
von_1_bis, summe(von_1_bis)) ;
exit(0) ;
48
L
Beispiel für den Gebrauch eines Unterprogramms als Prozedur:
/* Aufsummieren der ersten 37 Zahlen als Prozedur*/
void summe (int bis, long int *ergebnis)
{
int lv ;
long int zwsumme = 0 ;
/* Ein Unterprogramm sollte die Bedingungen, unter
dem es korrekt arbeitet genauestens pruefen. */
if (bis < 1) {
printf("Falsche Eingabe fuer Funktion summe!\n") ;
exit (1) ;
}
}
for (lv = 1 ; lv <= bis ; lv++) zwsumme += lv ;
*ergebnis = zwsumme ;
int main (void)
{
int von_1_bis = 37 ;
long int resultat = 0 ;
summe(von_1_bis, &resultat) ;
}
L
printf("Die Summe der Zahlen von 1 bis %d = %ld\n",
von_1_bis, resultat) ;
Ein Unterprogramm muss zum Zeitpunkt der Verwendung (im Hauptprogramm oder
in einem anderen Unterprogramm) bekannt sein. Deshalb muss sie syntaktisch entweder vorher vollständig definiert sein oder als Prototyp vorher deklariert.4 Letzteres ist
dann notwendig, wenn das Unterprogramm in einem anderen Modul (Datei) definiert
wird oder wenn sich Unterprogramme gegenseitig aufrufen. (Dann müssen beide
vorher spezifiziert sein.)
Bei der Deklaration - manchmal auch Spezifikation genannt - werden nur der Name,
die Parametertypen und ggf. der Ergebnistyp angegeben. Das ist quasi die Schnittstelle. Mit
void summe (int, long int *) ;
vor main, dürfte die Definition von summe auch nach main stehen oder in einer
anderen Datei, die aber gleichzeitig mit dem Restprogramm zu übersetzen ist.
4
Manchmal wird die Terminologie exakt umgekehrt verwendet.
49
L
Die Operatoren & bzw. * werden später genauer erklärt. Ohne ihre Verwendung liegt
eine Wert-Parameterübergabe (call by value) vor, mit ihnen realisiert man eine
Referenz-Parameterübergabe (call by reference). Streng genommen kennt C nur die
Wert-Parameterübergabe. Bei der Referenz-Parameterübergabe wird als Wert die
Adresse übergeben.
Die Funktionsweise der Parameterübergabemechanismen macht man sich am Besten
durch die Variablengraphen klar.
L
Vektoren / Felder und Zeichenketten, so viel sei bereits hier erwähnt, werden auch per
Referenz übergeben. Eine Unterprogrammdefinition hat dann z.B. folgende Gestalt:
void sortiere (int *v, int anzahl) { ... }
bzw.
void sortiere (int v[], int anzahl) { ... }
L
Vektoren als Ergebniswert können Probleme machen, weil lokale Variable nach dem
Ende der Prozedur vom Stapel verschwinden und dann ein Zeiger auf einen Speicherbereich für den Vektor sinnlos ist. Eine solche lokale Vektorvariable muss dann als
static deklariert sein.
L
Unterprogramme können sich direkt oder indirekt aufrufen (Rekursion5). Beispiel hier
die Fakultätsfunktion, für die gilt: f(0) = f(1) = 1 und f(n) = n*f(n-1). Man beachte die
Terminierungsbedingungen!
/* Berechnung der Fakultaet */
long int fakultaet(int i)
{
return (i==0 || i==1 ? 1 : i*fakultaet(i-1)) ;
}
int main (void)
{
int eingabe ;
}
5
do {
do {
puts("Eine Zahl groesser/gleich 1 eingeben!") ;
printf("Abbruch bei 0 > ") ;
scanf("%d", &eingabe) ;
} while (eingabe < 0) ;
printf("%d! = %ld\n", eingabe, fakultaet(eingabe)) ;
} while (!eingabe == 0) ;
exit (0) ;
„Iteration ist menschlich, Rekursion ist göttlich.” (Alte Informatikerweisheit)
50
Während man Aufrufe von nichtrekursiven Prozeduren als Sprünge in die jeweiligen
Programmtexte interpretieren kann, muss man beim rekursiven Aufruf von Prozeduren mit dem Kellerungsprinzip arbeiten. Man stelle sich vereinfacht vor, dass bei
jedem Aufruf eine neue Kopie der Prozedur angelegt wird („Ein Kasten in den aktuellen, die Funktion repräsentierenden Kasten gezeichnet wird.”) und das Zurückkommen von einer Inkarnation zu einer älteren geeignet organisiert wird.6
L
Und noch <n Beispiel, die Türme von Hanoi:
void verlege (int hoehe, char start, char ziel, char zwischen)
{
if (hoehe >= 1) {
/* Man tut so, als wuesste man, wie man n-1 Steine
richtig verlegen muss: Sie muessen von der
start-Position aus auf das Ziel "zwischen"-Position,
wobei ziel fuer den n-ten Stein frei bleibt. Ausserdem
wird die eigentliche ziel-Position fuer diesen
Vorgang als zwischen-Position verwendet. */
verlege(hoehe-1, start, zwischen, ziel) ;
/* Jetzt kann der unterste Stein auf die richtige
Position */
printf("Verlege Stein %2d von %c nach %c\n",
hoehe, start, ziel) ;
/* Jetzt muessen noch die n-1 Steine vom
Zwischenspeicherplatz auf den Endplatz. */
verlege(hoehe-1, zwischen, ziel, start) ;
}
}
int main (void)
{
int anzahl ;
printf("Wie hoch ist der Turm?> ") ;
scanf("%d", &anzahl) ;
puts("\n") ;
verlege(anzahl, 's', 'z', 'h');
exit (0) ;
}
6
De facto wird für einen Aufruf ein nur weiterer Aktivierungsblock, der hauptsächlich
aus einem neuen Satz aller Variablen sowie Parameter der Funktion besteht und noch ein paar
weitere Verwaltungsinformationen besitzt, auf den Stapel gelegt.
51
L
Jede Funktion sollte in einer eigenen Testumgebung auf Korrektheit überprüft werden, bevor man sie in den Programmverband einfügt! Die Investition von wenig Zeit
an dieser Stelle erspart Sunden bis Tage des Fehlersuchens! Sonst bleibt oft nur noch
der Einsatz des Debuggers.
52
7
Wie klassifiziert C die Welt? (benutzerdefinierte Datentypen)
Um die Problemwelt zu modellieren, kann man drei Kategorien von Typen in C verwenden:
L
Die bereits bekannten einfachen Typen int, char, float, ....
Zu diesen kann man noch die Aufzählungstypen hinzunehmen, die aber nichts weiter
als eine bequeme Abkürzung sind und zu den int-Typen gehören:
Aufzählungstyp = ”enum” [ Bezeichner ] ”{” Aufzählungsliste ”}”
enum Stellung { Chef,
Abteilungsleiter,
Gruppenleiter = 7 ,
Sachbearbeiter
} ;
hat die Wirkung von
const
const
const
const
int
int
int
int
Chef = 0 ;
Abteilungsleiter = 1 ;
Gruppenleiter = 7 ;
Sachbearbeiter = 8 ;
Wie immer bei C, ist auch hier Vorsicht geboten. Hätte man statt 7 in der enum-Deklaration 1 geschrieben, hätten sowohl Abteilungsleiter als auch Gruppenleiter den Wert 1 angenommen.
const zeigt an, dass z.B. Chef kein Wert zugewiesen werden darf.
L
Zeigertypen, unterteilbar in echte Zeiger und Vektoren (auch Felder genannt)
L
strukturierte Typen, unterteilt in struct und union.
Will man ein Objekt einer dieser Kategorien verwenden, muss es definiert7 werden.
7
Die Bezeichnung ist hier nicht einheitlich. Im Englischen wird das Wort declaration
verwendet, wo im Deutschen oft Definition eingesetzt wird. Der Unterschied macht sich nur bei
den Unterprogrammen bemerkbar: Eine Unterprogrammdefinition umfasst die Schnittstelle plus
die Anweisungen (nebst lokalen Variablen). Die Unterprogrammdeklaration beschreibt nur die
Schnittstelle (Namen und Parametertypen).
53
7.1
Deklarationen / Definitionen von Variablen
L
In Variablendeklarationen folgt nach dem Grundtyp eine Liste von Deklaratoren.
Dabei werden Operatoren um die Bezeichner gruppiert, die angeben, was man tun
muss, um den Grundtyp zu erhalten. Die nachgestellten Operatoren () und [] haben
höhere Priorität als vorangestellte Operatoren wie *.
Beispiel. Das Programmstück
int i, a[10] ;
deklariert i als int (aber nicht als Feld bzw. Vektor!8) und a als eine Reihung von 10
int-Größen. Analog deklariert
int *p, i ;
i wieder als int (aber nicht als Zeiger!) und p als einen Zeiger auf int. Man muss
also nur die Operatoren kennen, aber das kann trotzdem beliebig kompliziert werden:
char *(*feld[10])(void) ;
ist ein Feld von zehn (Zeigern auf) parameterlose Funktionen, die einen Zeiger auf
char liefern.
L
Typdefinitionen werden durch das Schlüsselwort typedef eingeleitet und sehen
völlig analog aus:
typedef char *(*feldtyp[10])(void) ;
definiert jetzt einen Typ feldtyp statt einer Variablen.
feldtyp feld ;
hat dann dieselbe Wirkung wie die vorangegangene Deklaration.
L
Bei Funktionstypen gibt es zwei Besonderheiten. Zum einen dürfen - wie bereits erwähnt - in der formalen Parameterliste die Namen fehlen.9 Zum anderen zeigt die
Parameterliste () an, dass es sich um eine Funktion handelt, bei der Anzahl und Typ
der Parameter nicht geprüft werden.
8
Später Genaueres
9
Man muss sich klar machen, dass die Parametertypen trotzdem eindeutig bestimmt
sind!
54
L
Die Attribute const und volatile dürfen so ziemlich überall in einer Deklaration
auftreten, sowohl im Typ (und beziehen sich dann auf alle folgenden Deklaratoren)
als auch in Deklaratoren.10 const teilt dem Compiler mit, dass ein Wert nicht verändert wird. Das ungefähre Gegenteil ist volatile: Eine Variable kann sich außerhalb der Kontrolle des Programms verändern. Dann darf der Compiler keine Optimierung vornehmen und die Variable z.B. im schreibgeschützen Bereich des Speichers
anlegen.
Beispiel. Die Programmstücke
char const *string1 = "bc" ;
oder
const char *string2 = "abc";
demonstrieren, dass die Elemente, auf die string1 bzw. string2 verweisen, nicht
geändert werden dürfen. Also, er sagt nicht, dass der Zeiger konstant ist, sondern das
Objekt, auf das string1 zeigt, ist konstant. Somit kann string1 kann durch Wertzuweisung auf eine andere Zeichenkette verweisen. Hingegen vereinbart
char *const string = "abc" ;
string als konstanten Zeiger. Ihm darf nichts mehr zugewiesen werden, auch nicht
Teilen, wie etwa durch
string[2] = 'd' ;
Bei Unterprogrammen darf ein mittels const deklarierter Parameter z.B. in
int f(const int *p) { .......}
nicht verändert werden. Dadurch sichert man ab, dass eine ungewollte Veränderung
des aktuellen Parameters nicht erfolgen kann.
L
In C ist es möglich, Variablen bei der Deklaration zu initialisieren. Das sieht aus wie
eine Wertzuweisung und wurde bereits in einigen Beispielen benutzt. Das Gleichheitszeichen im vorangegangenen Abschnitt war übrigens auch keine Konstantendefinition, sondern eine initialisierte Variablendeklaration. Globale Variablen (dazu
zählen auch alle Variablen der Speicherklasse static) dürfen nur mit konstanten
Werten initialisiert werden. Wird nichts angegeben, so werden sie mit Nullen gefüllt.
Für Felder und Verbunde11 gibt es eine spezielle Aggregatschreibweise: In geschweiften Klammern stehen, durch Komma getrennt, der Reihe nach die Werte für die einzelnen Komponenten (die ihrerseits wieder Aggregate sein können).
10
Leider ist nicht immer klar, ob ein Attribut zum Typ oder zum ersten Deklarator
11
Später Genaueres
gehört!
55
Bei Feldern, die auf diese Weise initialisiert werden, darf sogar die Anzahl der Komponenten offengelassen werden - sie wird aber in keinem Fall geprüft.
Beispiele.
“
int i = 0 ;
ist äquivalent zu
int i ;
“
i = 0 ;
int a[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } ;
oder
int a[] =
“
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } ;
char a[4] = "abc" ;
(Man beachte die Zählung! Ein Feldelement muss hinzu für das Zeichenkettenendezeichen '\0' !) was gleichwertig ist mit
char a[] =
“
"abc" ;
char *a = "abc" ;
ist etwas anderes, nämlich
char *a; a = "abc" ;
Hier wird für die Zeichenkette abc Speicherplatz auf der sogen. Halde (engl.
heap) angelegt und die Adresse des ersten Zeichens im Wert des Zeigers a
gespeichert. Trotzdem sind noch immer Zugriffe wie a[2] möglich.
L
Auch bei formalen Feldparametern darf die erste Dimension offengelassen werden; in
diesem Fall sind ... a[] und ... *a gleichwertig.
56
L
Natürlich gibt es auch komplexere Deklarationen wie
int **x ;
was einen Zeiger auf einen Zeiger darstellt. Bei
double *x[10] ;
ist x ein Vektor mit 10 Elementen vom Typ Zeiger auf double. * hat Priorität 2, []
dagegen 1 !12 Hingegen bei
double (*x)[10] ;
ist ein x Zeiger auf ein Feld mit 10 double. Weiter ist im folgenden f ein Zeiger auf
ein Unterprogramm:
int (*f) (.....) ;
und
int *f (......) ;
liefert einen Zeiger auf eine int-Größe.
12
Klingt etwas unlogisch: [] hat höhere Priorität als *; also ist „1 höher als 2”.
57
7.2
Zeiger
L
Wie bereits erwähnt, ist eine Variable eine Kombination aus einem externen Bezeichner, der ein Synonym für eine nicht veränderbare interne Adresse / ein interner Bezeichner ist, in/an der der eigentliche Wert der Variablen steht. Für C ist ganz typisch
das Arbeiten mit Zeigern. Ein Zeiger hat auch eine interne Adresse, sein Wert stellt
aber selbst eine Adresse - quasi die Hausnummer - im Speicher dar. Mit dem Namen
greift man auf den Wert einer Größe zu, mit dem Adressoperator & auf die Adresse,
also dem inneren Bezeichner einer Variablen oder eines Zeigers. Mit
printf(”%p\n”, &x) ;
kann man sich die aktuelle Adresse der Variablen x (in Hexadezimalform) ansehen.
L
Typische Kombinationen sind:
int x, y, *p ;
......
p = &x ;
......
y = *p ;
.......
*p = 7 ;
wobei mittels der ersten Wertzuweisung der Wert von p die Adresse von x ist. Die
Anwendung von * auf p bewirkt das Dereferenzieren. Dabei steht *p für den Wert
der Variablen, auf die p zeigt. Somit kann man auf den Wert von x sowohl mittels x
selbst als auch mit *p zugreifen. *p kann auch l-Wert sein.
L
Es gibt einen ausgezeichneten Zeiger NULL, mit dessen Hilfe man ausdrücken kann,
dass ein Zeiger auf keine sinnvolle Größe zeigt.
int *z = NULL ;
Man darf ihn auch in Abfragen wie if (p==NULL){....} verwenden.
L
Bei Zeigern auf Zeiger hört oft die Klarheit oft auf.
L
Mit Zeigern kann man Arithmetik betreiben. Ist z.B. p ein Zeiger, so ist p++ eine
Position im Speicher, die je nach Speicherbedarf des Grundtyps von p aus gerechnet
mal 1, 2, 4 oder 8 Bytes weiter zeigt und *(p++) gibt den dort gespeicherten Wert
zurück. Die Addition/Subtraktion einer ganzen Zahl zu/von einem Zeiger ändert den
Wert in Einheiten der Größe des referenzierten Typs. Die Differenz zweier Zeiger
gleichen Typs, die in einen zusammenhängenden Speicherbereich verweisen, ist
gleich der Anzahl der Elemente zwischen diesen.
58
L
Ein besonderer Typ ist void*. Er ist quasi ein abstrakter / neutraler Zeiger und kompatibel mit jedem Zeiger eines konkreten Typs. Eine Zeigervariable p, die mittels
void *p ;
deklariert wurde, kann nicht dereferenziert werden. Das ist klar, denn schon zur Übersetzungszeit muss der Compiler für Dereferenzierung Sorge tragen, aber erst zur
Laufzeit wird festgelegt, auf welche Variable p zeigen soll. Zeigerarithmetik ist auch
nicht möglich.
L
Zeiger auf Funktionen sind in manchen Fällen wichtig. Mit ihnen kann man eine weitere Funktion als Parameter übergeben.
double quadrat (double x) {
return x*x;
}
double zweimal (double x) {
return 2*x;
}
void tabelliere (double start, double ende, double steps,
double (*fktn)(double)) {
double i;
for (i=start; i<=ende; i=i+steps)
printf("%10.2f %20.2f \n", i, (*fktn)(i));
}
int main (void) {
printf("Quadrat: \n");
tabelliere(1.0, 10.0, 1.0, quadrat);
printf("\n\n\nDoppel: \n");
tabelliere(1.0, 10.0, 1.0, zweimal);
exit (0);
}
Man achte auf die Angabe z.B. von quadrat. Hier steht kein Klammerpaar danach!
59
7.3
Vektoren / Felder
L
In anderen Programmiersprachen heißen Vektoren üblicherweise Felder. Sie sind
Reihungen von Daten des gleichen Typs. Die Definition erfolgt durch [].
int feld [10] ;
deklariert eine Reihung von 10 int-Größen. Auf die erste wird mit feld[0], auf
die zweite mit feld[1], usw. zugegriffen.
L
feld[0] und *feld sind gleichwertig, weil C Vektoren wie Zeiger behandelt. So ist
zu erklären, dass die Auswahl einer Komponente a[i] als Abkürzung des Ausdrucks
*(a + i) zu verstehen ist
L
Ein Vektor kann initialisiert werden, indem man eine Liste von Werten angibt.
int a[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } ;
Wenn die Liste kürzer ist als die Länge des Vektors, wird der Rest auf 0 gesetzt.
L
Es gibt auch mehrdimensionale Vektoren:
int b[5][7] ;
Merke: „Zeilen zuerst, Spalten später!” Hier ist b eine Matrix mit 5 Zeilen und 7
Spalten.
Im Hauptspeicher werden erst die Werte zeilenweise abgespeichert.
L
Übergabe von eindimensionalen Vektoren bei Unterprogrammaufrufen:
double v [10] ;
..........
void up (double *feld, int anzahl) {.....}
..........
up (v, 10) ;
Wenn die Feldlänge z.B. durch ein #define MAX 10 bekannt ist, reicht auch der
Unterprogrammaufruf in der Form
up(v) ;
60
L
Die Übergabe von mehrdimensionalen Vektoren bei Unterprogrammaufrufen ist in C
unbefriedigend gelöst:
#define ZEILEN 10
#define SPALTEN 20
.......
void up1 (double *feld, int zeilen, int spalten) {.....}
...
double matrix [ZEILEN][SPALTEN] ;
......
up1(matrix, ZEILEN, SPALTEN);
Hier muss der Programmierer selbst mittels Adressenrechnung und dem Merksatz
“Zeilen zuerst, Spalten später”dafür sorgen, auf die richtigen Positionen zuzugreifen.
Allerdings ist up1 in jeder Umgebung zu gebrauchen.
Alternative:
#define ZEILEN 10
void up2 (double (*feld)[SPALTEN], int zeilen){...}
Dementsprechend aufzurufen mit:
double matrix [ZEILEN][SPALTEN] ;
......
up2(matrix, ZEILEN) ;
Das Unterprogramm up2 benötigt eine SPALTEN-Definition, hat also eine gewisse
Kontextabhängigkeit..
61
7.4
Zeichenketten
L
Zeichenkettenkonstante sind in Doppelhochkomma eingeschlossene Folgen von Zeichen.
L
Es ist eine fest verankerte Konvention in C, dass Zeichenketten durch ein Feld von
Zeichen dargestellt werden, dessen letztes Element den (ASCII-) Wert Null hat. Die
Zeichenkette "abc" enthält also vier (!) Elemente, nämlich die Zeichen 'a ', 'b ', 'c ' und
'\0', das Terminierungszeichen. Daran muss man denken, wenn man Zeichenketten
in einem Feld abspeichert oder Speicherplatz von der Halde anfordert.
char zeichenkette [100] = ”kurz” ;
ist korrekt aber eine Platzverschwendung.
char zeichenkette [] = ”kurz” ;
ist speicherökonomischer.
Alternativ (aber mit unterschiedlichen Zugriffsmöglichekiten):
char *p ;
p = ”kurz” ;
Die Anweisung
printf(“%c”, p[1]) ;
liefert u.
L
C bietet eine Vielzahl von Funktionen an, um mit Zeichenketten zu arbeiten. Dazu ist
es notwendig, die erforderliche Standardbibliothek in den Programmtext einzubeziehen.
#include <string.h>
Die am häufigsten gebrauchten Funktionen sind strlen (liefert die Länge einer Zeichenkette), strcpy (kopiert eine Zeichenkette von einem Platz an einen anderen und
liefert die Kopie), strcat (wie strcpy, nur wird die zweite Zeichenkette an die
erste gehängt) und strcmp (vergleicht zwei Zeichenketten und liefert einen ganzzahligen Wert kleiner, gleich oder größer Null je nachdem, ob die erste Zeichenkette
lexikografisch kleiner, gleich oder größer ist als die zweite) importiert. Außerdem
gibt es die Funktionen sscanf und sprintf, die als zusätzlichen ersten Parameter
eine Zeichenkette erwarten und ansonsten wie scanf bzw. printf funktionieren.
Diese Funktionen stehen in stdio.h (vergleiche fscanf und fprintf).
62
7.5
Strukturen
L
Während ein Vektor eine homogene Datenstruktur ist, handelt es sich bei einer Struktur oder einem Verbund ist eine benutzerdefinierte Zusammenfassung von unterscheidlichen also heterogenen Datentypen.
L
Beispiel:
struct complex {
double re, im ;} zahl ;
vereinbart zahl als komplexe Variable. Nebenbei wird ein neuer Datentyp definiert,
nämlich struct complex. Das Schlüsselwort struct darf nicht fehlen! Will man
den Datentyp complex definieren, so muss das über typedef geschehen, z. B.
typedef struct {
double re, im ;
} complex ;
Jetzt geht es weiter mit
complex zahl1, zahl2 ;
L
Der Zugriff auf die einzelnen Komponenten erfolgt durch den Punktoperator:
zahl1.re = 1.5 ;
zahl1.im = 3.6 ;
(Es gibt mächtige Bibliothekfunktionen für komplexe Zahlen.)
L
Nötig ist der Zusatz struct bei rekursiven Datentypen.13
typedef struct Eintrag *Liste ;
struct Eintrag {
int info ;
Liste next ;
} ;
L
Der Strichpunkt nach der struct-Deklaration wird leicht vergessen.
L
Mittels typedef lassen sich Abkürzungen für neue Datentypen einführen. Natürlich
dürfen in Strukturen auch wieder Strukturen auftreten.
13
Diese werden später genauer behandelt.
63
L
Verwendet man den Datentyp complex von oben
complex x, *px ;
px = &x ;
und möchte man nun via px an die x-Komponenten Zuweisungen machen, verwendet man den Pfeiloperator -> (statt z.B. (*px).im):
px -> re = 1.5 ;
px -> im = 3.6 ;
L
Strukturobjekte s1 und s2 desselben Typs kann man in ihrer Gesamtheit einander
zuweisen:
s1 = s2 ;
Andere Operationen, die man auf Strukturen ausführen kann, sind die bereits erwähnte Selektion mittels des Punktoperators. Natürlich lässt sich auch die Größe eines
Strukturobjekts mittels sizeof bestimmen oder seine Adresse mittels &.
Initialisierung von Strukturen ist auch möglich:
struct punkt {
float x, y ;
} p1 = {1.0, 1.0} ;
struct punkt p2 = {2.0, 2.0} ;
64
7.6
Die Vereinigung / Union / Variante
L
Eine union ist eine Speicherbereich für heterogene Objekte. Trickreich ausgenutzt
kann ein Unterprogramm über eine return-Anweisung mit einem Ergebnis vom Typ
union mehrere typverschiedene Werte liefern.
union variant {
int i ;
double r ;
char str [100] ;
} v1, v2 ;
Es wird bei einer Union der maximal notwendige Speicher verfügbar gemacht. Vom
Benutzer ist dafür zu sorgen, dass er bei verschiedenen Zuweisungen an eine unionVariable über den aktuellen Typ auf dem Laufenden ist.
L
C ist auch bei union orthogonal, d.h. eine union kann auch Bestandteil einer struct
sein oder es darf auch Vektoren von Varianten geben. Die Selektion der einzelnen
Komponenten erfolgt wieder über den Punktoperator.
struct {
char *name ;
int i ;
union {
int ival ;
float fval ;
char *straval ;
} uval ;
} tabelle[100] ;
Mittels
tabelle[7].uval.ival
*tabelle[7].uval.strval
greift man im ersten Fall auf die ival-Komponente von uval zu, im zweiten auf das
erste Zeichen der Zeichenkette, auf die strval zeigt.
L
Initialisierung von union-Größen erfolgt nur über die erste Komponente.
65
7.7
Bitbereiche
L
Bitfelder treten innerhalb von Strukturen auf. Sie dienen der hardwarenahen Programmierung, wo Bausteine über Bitmasken gesteuert werden. Nach dem Standard darf
der Typ nur int, signed int oder unsigned int sein. Vieles ist aber compilerherstellerabhängig. Nach einem Doppelpunkt kommt ein konstanter Ausdruck, der
die Anzahl der zu belegenden Bits angibt. Ein Bitbereich kann nicht größer als ein
Wort sein. Aufeinander folgende Bitbereiche werden zu einem Wort zusammengepackt, solange noch Platz ist. Ein Bitbereich, der nicht mehr hineinpasst, beginnt an
der nächsten Wortgrenze. Die Angabe :0 besagt, dass an der nächsten Wortgrenze
weiter gemacht werden soll (Alignment). Es gibt nach Standard keine Zeiger auf und
keine Felder von Bitbereichen.
struct bits {
int b1 : 2 ;
int b2 : 3 ;
}
66
8
Die Halde
L
Die eigentliche Macht des Zeigerkonzepts entwickelt sich bei der benutzerdefinierten
Speicherverwaltung. Die Reservierung von Speicher geschieht mit den Funktionen
malloc und die Rückgabe von Speicher mit free. malloc erwartet die Größe des
gewünschten Speicherbereichs als Parameter und liefert einen Zeiger. Ist dieser
NULL, so steht nicht mehr genügend Speicherplatz zur Verfügung.
L
Beispiel: Felder, deren Größe erst zur Laufzeit festgelegt werden, lassen sich folgendermaßen simulieren:
int *p ;
if ((p = (int *) malloc(10 * sizeof(int))) != NULL) {
/* Tue was */
} else {
printf (”Nicht genuegend Speicher verfuegbar\n”) ;
exit (1) ;
}
Bei der Typumwandlung (cast) stehen ein Typ und ein Deklarator, bei dem der deklarierte Bezeichner gestrichen wurde, in runden Klammern. Nach dem Größenoperator sizeof darf entweder ein Ausdruck oder ein Typ in Klammern (sieht aus wie ein
cast) folgen. Da malloc den Typ void * liefert, ist eine Anpassung des Ergebnisses
eigentlich unnötig. Im allgemeinen ist jedoch nicht gesichert, dass beliebige Zeigertypen ineinander umgewandelt werden dürfen.
L
free gibt den Speicherbereich, der dem übergebenen Zeiger zugeordnet ist, wieder
frei.
L
Zusätzlich gibt es die Funktion realloc. Als erster Parameter wird, neben der gewünschten Größe, ein Zeiger auf einen bereits reservierten Speicherplatz übergeben.
Die Größe dieses bereits reservierten Speicherbereichs wird angepasst (notfalls durch
Anfordern eines neuen und Kopieren der Daten) und ein entsprechender Zeiger geliefert.
L
Um selbst im Speicher zu hantieren, gibt es eine Reihe weiterer brauchbarer Funktionen wie
memcpy()
memmove()
memcmp()
memchr()
memset()
67
L
Der durch malloc angeforderte Speicherbereich wird aus dem Teil des dem Programm zugeordneten Speichers requiriert, den man die Halde oder Heap nennt. Die
lokalen Daten des Haupt- und der Unterprogramme liegen auf dem Stapel oder Stack.
Der Programmcode liegt „unter” dem Stapel. Stapel und Halde laufen aufeinander zu
so dass es zum gefürchteten stack overflow kommen kann.
Die Arbeit mit Zeigern ist sehr fehleranfällig. Man muss immer genau testen, was
man getan hat, um nicht erst bei der Integration des Programmteils Fehler suchen zu
müssen.
68
Document
Kategorie
Technik
Seitenansichten
9
Dateigröße
201 KB
Tags
1/--Seiten
melden