close

Anmelden

Neues Passwort anfordern?

Anmeldung mit OpenID

7 Wie klassifiziert C die Welt? (benutzerdefinierte Datenty- pen)

EinbettenHerunterladen
7
Wie klassifiziert C die Welt? (benutzerdefinierte Datentypen)
Um die Problemwelt zu modellieren, kann man drei Kategorien von Typen in C verwenden:
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.
Zeigertypen, unterteilbar in echte Zeiger und Vektoren (auch Felder genannt)
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
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.
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.
✁
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
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.
✂
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.
Auch bei formalen Feldparametern darf die erste Dimension offengelassen werden; in
diesem Fall sind ... a[] und ... *a gleichwertig.
56
✆
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
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.
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.
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.
Bei Zeigern auf Zeiger hört oft die Klarheit oft auf.
✝
✝
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
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.
Zeiger auf Funktionen sind in manchen Fällen wichtig. Mit ihnen kann man innerhalb
einer Funktion verschiedene andere aufrufen.
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 i;
for (i=start; i<=ende; i=i+steps)
printf("%10.2f %20.2f \n", i, (*fktn)(i));
}
void main()
{
printf("Quadrat: \n");
tabelliere(1.0, 10.0, 1.0, quadrat);
printf("\n\n\nDoppel: \n");
tabelliere(1.0, 10.0, 1.0, zweimal);
}
59
7.3
Vektoren / Felder
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.
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
✟
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.
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.
Ü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
✠
Übergabe von mehrdimensionalen Vektoren bei Unterprogrammaufrufen:
double v [10][20] ;
.......
void up (int spalten, double *feld) {.....}
oder
void up (double (*feld)[spalten]) {......}
Dementsprechend z.B.
up(20, v) ;
61
7.4
Zeichenketten
Zeichenkettenkonstante sind in Doppelhochkomma eingeschlossene Folgen von Zeichen.
✡
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:
✡
char *p ;
p = ”kurz” ;
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
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.
☛
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 ;
Der Strichpunkt nach der struct-Deklaration wird leicht vergessen.
☛
Der Zugriff auf die einzelnen Komponenten erfolgt durch den Punktoperator:
☛
zahl1.re = 1.5 ;
zahl1.im = 3.6 ;
Nötig ist der Zusatz struct bei rekursiven Datentypen.13
☛
☛
struct Eintrag {
int info ;
Liste next ;
}
typedef struct Eintrag *Liste ;
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
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 ;
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
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.
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.
Initialisierung von union-Größen erfolgt nur über die erste Komponente.
65
7.7
✍
Bitbereiche
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
Document
Kategorie
Technik
Seitenansichten
9
Dateigröße
66 KB
Tags
1/--Seiten
melden