====== Strukturen ====== * Was sind Strukturen? * Strukturen definieren * Strukturen verwenden * Strukturen übergeben * Call-by-Value * Call-by-Pointer * Größe eines Datentyps * Speicher für Strukturen anfordern * Arrays von Strukturen * Das Semikolon nach der Definition ===== Was sind Strukturen? ===== Wir haben uns bisher mit einfachen Datentypen beschäftigt, wie rationale Zahlen ([[c:type:float]]) und abzählbare Zahlen ([[c:type:int]]), bzw. die Sonderform abzählbarer Zahlen für Buchstaben ([[c:type:char]]). Dann haben wir uns mit [[Arrays]] beschäftigt, also der Hintereinanderlegung von Zahlen oder eben Buchstaben, aus denen sich dann Strings ergeben. Die bisherigen Datentypen werden als "Primitive" bezeichnet. Damit können wir einfache Datentypen bilden, wie beispielsweise Bildschirmkoordinaten oder die Auflösung. Nehmen wir an, die Bildschirmauflösung beträgt 1280 * 800 Pixel und wir wollen diese Daten speichern: int main( void ) { unsigned int width; unsigned int height; width = 1280; height = 800; return 0; }; Da die Datentypen identisch sind, können wir ein Array daraus machen. int main( void ) { unsigned int screenResolution[2]; screenResolution[0] = 1280; screenResolution[1] = 800; return 0; }; Jetzt haben wir die zusammengehörigen Informationen in einer einzigen Variablen - aber leider wissen wir nicht mehr, ob width bzw. height nun im 0. oder 1. Element des Arrays liegt. Strukturen bilden hier die Möglichkeit einen Datentyp zu definieren, der mehrere Datensätze zusammenfasst - in unserem Fall also zwei Datensätze vom Typ ''unsigned int'', wobei jeder für sich einen eigenen Namen hat - hier also "width" und "height". Wir wollen Daten strukturieren, statt sie einfach hintereinander zu packen. Wir wollen Strukturen. ===== Strukturen definieren ===== Eine Struktur ist ein Datentyp. Wir beschreiben also die Werte, die wir uns merken wollen. Aber in der Beschreibung der Werte können wir noch keine Werte speichern. Die Beschreibung sieht ähnlich aus, wie die vorherige Definition von "width" und "height" in der Funktion main() weiter oben. Da es nur eine Beschreibung ist, können wir keine Werte zuweisen. struct Resolution { unsigned int width; unsigned int height; }; Am Ende einer Strukturdefinition findet sich - im Gegensatz zu einer Funktion - ein Semikolon, um die Definition abzuschließen. Das Semikolon ist notwendig, um dem Compiler zu sagen, dass die Anweisung hier endet. Weshalb erkläre ich gleich. Es lassen sich innerhalb von Strukturen beliebige Datentypen verwenden - auch Strukturen: struct Address { char street[64]; unsigned int houseNumber; unsigned int zipCode; char town[64]; }; struct Person { char firstName[64]; char familyName[64]; unsigned int age; struct Address mainAddress; struct Address secondaryAddress; }; ===== Strukturen verwenden ===== Bisher haben wir nur beschrieben wie Daten aussehen sollen. Wenn wir Werte speichern wollen, dann müssen wir entsprechende Variablen anlegen. Das geht genauso wie bei den primitiven Variablen: int main( void ) { struct Person person; return 0; } Hiermit werden zwei char-Arrays mit je 64 Elementen für Vor- und Nachname, das Integer für Alter und auch jeweils zwei Strukturen ''Address'' reserviert. Für jede der beiden ''Address''-Variablen wird wiederum jeweils Speicher für die beiden Arrays und die zwei Integer reserviert. Alles zusammen ist über die Variable ''person'' erreichbar. Auf Elemente der Struktur greift man mit dem Punkt-Operator ('.') zu. Um Daten in ein char-Array zu kopieren, verwenden wir die Funktion [[c:lib:string:strcpy()]]. #include struct Address { char street[64]; unsigned int houseNumber; unsigned int zipCode; char town[64]; }; struct Person { char firstName[64]; char familyName[64]; unsigned int age; struct Address mainAddress; struct Address secondaryAddress; }; int main( void ) { struct Person person; strcpy( person.firstName, "Hans" ); strcpy( person.familyName, "Mustermann" ); person.age = 20; strcpy( person.mainAddress.street, "Hauptstrasse" ); person.mainAddress.houseNumber = 1; person.mainAddress.zipCode = 12345; strcpy( person.mainAddress.town, "Musterstadt" ); return 0; } (svn://svn.proggen.org/wiki/c/tutorial/struct/input.c) ===== Strukturen übergeben ===== Eine Struktur beschreibt Daten in einer eindeutigen Form, denn jeder Datensatz im Datensatz hat einen eigenen Namen. Das bedeutet, dass Verwechslungen eigentlich nicht möglich sind. So lassen sich Datensätze auch eindeutig an Funktionen übergeben. ==== Call-by-Value ==== Werden die Daten direkt übergeben, spricht man von **[[glossary:callbyvalue|Call By Value]]**. Um das zu verdeutlichen, übergebe ich den Datentyp ''struct Person'' hier an eine Funktion, die ich "callByValue" nenne: void callByValue( struct Person p ) { printf( "Name: %s %s (%d)\n", p.firstName, p.familyName, p.age ); printf( "Adresse: %s %d; %d %s\n", p.mainAddress.street, p.mainAddress.houseNumber, p.mainAddress.zipCode, p.mainAddress.town ); } int main( void ) { struct Person person; strcpy( person.firstName, "Hans" ); strcpy( person.familyName, "Mustermann" ); person.age = 20; strcpy( person.mainAddress.street, "Hauptstrasse" ); person.mainAddress.houseNumber = 1; person.mainAddress.zipCode = 12345; strcpy( person.mainAddress.town, "Musterstadt" ); callByValue( person ); return 0; } (svn://svn.proggen.org/wiki/c/tutorial/struct/callbyvalue.c) Wird das Programm ausgeführt (hier [[c:tutorial:struct:callbyvalue|vollständig]]), erscheint folgende Ausgabe auf dem Bildschirm: Name: Hans Mustermann (20) Adresse: Hauptstrasse 1; 12345 Musterstadt Was passiert? Zunächst wird die Struktur ''person'' aus der Hauptfunktion - nennen wir sie "main::person" - kopiert, und zwar auf die Variable p, die als Parameter der Funktion ''callByValue'' verwendet wird. Wichtig ist zu verstehen, dass der Wert der Variable **kopiert** wird. Call-by-Value - Funktionsruf mit Wert. Wenn wir in einer Funktion, der wir die Werte kopieren, Teile der Struktur ändern, so gilt das nur für die lokale Kopie: void overwriteAgeByValue( struct Person p ) { p.age = 99; print( "Alter von %s %s auf %d geändert\n", p.firstName, p.familyName, p.age ); } int main( void ) { struct Person person; /* wie zuvor */ callByValue( person ); overwriteAgeByValue( person ); callByValue( person ); return 0; } (svn://svn.proggen.org/wiki/c/tutorial/struct/overwritebyvalue.c) Entsprechend gibt das Programm folgendes aus: Name: Hans Mustermann (20) Adresse: Hauptstrasse 1; 12345 Musterstadt Alter von Hans Mustermann auf 99 geändert Name: Hans Mustermann (20) Adresse: Hauptstrasse 1; 12345 Musterstadt Die lokale Kopie overwriteAgeByValue::p wird modifiziert, aber das interessiert das Original main::person überhaupt nicht. Beim zweiten Aufruf von ''callByValue'' wird erneut die unveränderten Werte von main::person kopiert, entsprechend ist bei der Ausgabe innerhalb callByValue Hans Mustermann wieder jugendliche 20 Jahre alt. Call-by-Value hat also den Vorteil, dass wir Werte an eine Funktion geben können und sicher sein können, dass die Originalwerte nicht verändert werden. Das ist aber genauso ein Nachteil, wenn wir Werte verändern wollen. Leider hat Call-by-Value weiterhin den sehr großen Nachteil, dass die Werte alle kopiert werden müssen und das kostet Zeit. Wer schnelle Programme schreiben möchte, sollte auf das Kopieren also lieber verzichten. ==== Call-by-Pointer ==== Um auf das Kopieren zu verzichten, übergeben wir in C üblicherweise einfach einen Pointer auf die Daten. Dies wird in vielen C-Lehrbüchern als "Call-by-Reference" beschrieben, aber genau genommen unterstützt C [[glossary:callbyreference|Call-by-Reference]] gar nicht. Faktisch ist Call-by-Pointer das gleiche wie Call-by-Value, nur, dass der Wert in dem Fall eben der Pointer ist, der kopiert wird. Weil wir eigentlich aber nicht am Pointer interessiert sind, sondern an den Daten, wohin er zeigt, drücke ich diesen Unterschied mit dem Begriff "Call-by-Pointer" aus. "Call-by-Reference" gibt es erst in C++ und das ist eben nicht identisch mit "Call-by-Pointer". Dies ist ein C-Tutorial, also schauen wir, was wir mit C machen können. Wir sind bei Call-by-Value auf zwei Probleme gestoßen: Den Aufwand, dass die Werte kopiert werden (man stelle sich ein großes Array als Teil einer Struktur vor...) und dass die Originaldaten nicht verändert werden können. Hier kommt jetzt "Call-by-Pointer" ins Spiel und ein neuer Operator, der Pfeiloperator ('->') ins Spiel: void callByPointer( struct Person * p ) { printf( "Name: %s %s (%d)\n", p->firstName, p->familyName, p->age ); printf( "Adresse: %s %d; %d %s\n", p->mainAddress.street, p->mainAddress.houseNumber, p->mainAddress.zipCode, p->mainAddress.town ); } void overwriteAgeByPointer( struct Person * p ) { p->age = 99; printf( "Alter von %s %s auf %d geändert\n", p->firstName, p->familyName, p->age ); } int main( void ) { struct Person person; strcpy( person.firstName, "Hans" ); strcpy( person.familyName, "Mustermann" ); person.age = 20; strcpy( person.mainAddress.street, "Hauptstrasse" ); person.mainAddress.houseNumber = 1; person.mainAddress.zipCode = 12345; strcpy( person.mainAddress.town, "Musterstadt" ); callByPointer( &person ); overwriteAgeByPointer( &person ); callByPointer( &person ); return 0; } Beginnen wir recht weit unten bei den Aufrufen: callByPointer( &person ); overwriteAgeByPointer( &person ); callByPointer( &person ); Statt die Variable selbst zu übergeben, übergeben wir nur den Zeiger auf die Variable. Dieser Zeiger wird per Call-by-Value an die Funktionen übergeben. Um auf ein Element in der Struktur zuzugreifen, kennen wir den Punkt-Operator. Da wir aber einen Zeiger haben, muss zunächst dereferenziert werden, um aus dem Zeigerdatentyp wieder auf den eigentlichen Datentyp zurückzukommen, so dass wir den Punkt-Operator verwenden können. Der Operator zum Dereferenzieren muss zusätzlich geklammert werden, damit der Compiler versteht, dass man die Struktur dereferenzieren möchte und nicht das Element auf das man zugreift. Das sieht dann so aus: (*p).firstName Das gefiel den C-Entwicklern vermutlich auch nicht, deswegen wurde dafür der Pfeil-Operator eingeführt, der die gleiche Bedeutung hat, aber besser zu lesen ist: p->firstName Schauen wir uns das ganze in der Funktion an: void callByPointer( struct Person * p ) { printf( "Name: %s %s (%d)\n", p->firstName, p->familyName, p->age ); printf( "Adresse: %s %d; %d %s\n", p->mainAddress.street, p->mainAddress.houseNumber, p->mainAddress.zipCode, p->mainAddress.town ); } Hier fällt vielleicht auf, dass hier Pfeiloperator und Punktoperator bei ''mainAddress'' kombiniert auftreten: zum Beispiel ''p->mainAddress.street''. Das ist einfach zu erklären: Der Operator bezieht sich auf das linksstehende Objekt. Um an das Element ''mainAddress'' zu gelangen, muss der Zeiger von p mit dem Pfeiloperator dereferenziert werden. Und dann hat man die ''struct Address'', genauso als würde man von einer ''struct Person'' mit dem Punktoperator auf ''mainAddress'' zugreifen. Der Punkt hinter ''mainAddress'' bleibt also unverändert stehen. Anders wäre es, wenn die Struktur statt einer Instanz einer Adresse nur einen Zeiger auf eine Adresse enthalten würde. Um zu entscheiden, ob der Punkt- oder der Zeigeroperator verwendet wird, ist also das Objekt links vom Operator entscheidend: Ist es ein Zeiger oder nicht? Name: Hans Mustermann (20) Adresse: Hauptstrasse 1; 12345 Musterstadt Alter von Hans Mustermann auf 99 geändert Name: Hans Mustermann (99) Adresse: Hauptstrasse 1; 12345 Musterstadt === Const-Correctness === Die Funktion callByPointer muss keine Daten einer Person verändern. Wie wir bereits bei den Strings gelernt haben, ist es sinnvoll, diese Information dem Compiler mitzugeben, indem wir den Übergabeparameter als ''const'' deklarieren. void callByPointer( struct Person const * p ) { printf( "Name: %s %s (%d)\n", p->firstName, p->familyName, p->age ); printf( "Adresse: %s %d; %d %s\n", p->mainAddress.street, p->mainAddress.houseNumber, p->mainAddress.zipCode, p->mainAddress.town ); } Nun weiß der Compiler, dass er die Funktion callByPointer auch mit konstanten Personen-Objekten aufrufen darf und der Programmierer, dass ein Personen-Objekt mit dieser Funktion nicht verändert wird. Das geht auch bei Call-By-Value, allerdings spielt es da seltener eine Rolle, wenn das kopierte Objekt verändert wird, da es schließlich nach dem Aufruf sowieso wieder zerstört wird, ohne das Original zu beeinflussen. ===== Größe eines Datentyps ===== Wir haben bisher Strukturen einfach als lokale Variable verwendet: int main( void ) { struct Person person; ... Wir können aber auch Speicher für eine Struktur mit [[c:lib:stdlib:malloc()]] anfordern. Dafür müssen wir jedoch die Größe einer solchen Struktur wissen. Bisher haben wir nur chars alloziiert, die zufälligerweise genauso groß sind, wie ein Byte. Wir müssen also herausfinden, wie viele Bytes wir für eine Struktur benötigen. Hierfür gibt es den sizeof()-Operator. unsigned int size = sizeof( struct Person ); ===== Speicher für Strukturen anfordern ===== Und damit können wir nun Speicher anfordern und erhalten einen Zeiger zurück, auf den wir wieder mit dem Pfeil-Operator zugreifen können: #include int main( void ) { struct Person * person; person = malloc( sizeof( struct Person )); // Sollte die vorherige Zeile nicht funktionieren die folgende verwenden: // person = (struct Person *) malloc( sizeof( struct Person )); // Erklärung dazu folgt in einem späteren Kapitel. if( person ) { /* Wir haben Speicher bekommen */ strcpy( person->firstName, "Hans" ); strcpy( person->familyName, "Mustermann" ); person->age = 20; strcpy( person->mainAddress.street, "Hauptstrasse" ); person->mainAddress.houseNumber = 1; person->mainAddress.zipCode = 12345; strcpy( person->mainAddress.town, "Musterstadt" ); callByPointer( person ); free( person ); } return 0; } Abschließend müssen wir den Speicher mit free() wieder frei geben. ===== Arrays von Strukturen ===== Natürlich lassen sich auch Arrays von Strukturen anlegen und sich dafür Speicher anfordern: int main( void ) { struct Person personen[10]; struct Person * weiterePersonen; weiterePersonen = (struct Person*) malloc( 10 * sizeof( struct Person )); if( weiterePersonen ) { /* ... */ callByPointer( &weiterePersonen[0] ); free( weiterePersonen ); } callByPointer( &personen[0] ); return 0; } ===== Das Semikolon nach der Definition ===== Da war noch was - das Semikolon nach der Definition: struct ScreenResolution { unsigned int width; unsigned int height; }; // <- Semikolon void function() { unsigned int width; unsigned int height; } // <- kein Semikolon Natürlich ist die Funktion ''function'' nicht sinnvoll, da sie mit den beiden Variablen nichts macht. Aber nichts zu tun, ist ja noch nicht falsch: Die Funktion ist gültig und im Gegensatz zur Struktur, gibt es hier kein Semikolon - wieso also hinter der Strukturdefinition? Schauen wir uns kurz die Definition der Struktur ScreenResolution und die Definition einer Variable vom Typ ''struct ScreenResolution'' an: struct ScreenResolution { unsigned int width; unsigned int height; }; // <- Semikolon struct ScreenResolution myResolution; Am Ende der Definition der Variablen ''myResolution'' findet sich - wie hinter jeder Anweisung - ein Semikolon. Das ganze lässt sich zusammenfassen, denn die Definition der Struktur ist gleichzeitig die Aufforderung Variablen dieses Typs zu definieren, so dass sich die zwei Anweisungen zu einer einzigen Anweisung zusammenfassen. struct ScreenResolution { unsigned int width; unsigned int height; } myResolution; Damit der C-Compiler weiß, ob das nächste Wort nun der Name einer neuen Variablen ist, die vom Datentyp ''struct ScreenResolution'' definiert werden soll oder irgendetwas anderes, wie der Name einer anderen Variable oder Funktion, die verwendet werden soll, muss hier ein Semikolon stehen. Eben auch dann, wenn keine Variable vom Typ ''ScreenResolution'' definiert wurde. Und wenn man keine neue Variable definieren möchte, dann folgt das Semikolon direkt der schließenden Klammer. ====== Fazit ====== An dieser Stelle des Tutorials haben wir das Wissen durchgegangen, das erforderlich ist, um die meisten Anwendungsprogramme zu schreiben. Wir können Probleme in kleinere Funktionen zerkleinern und können Daten in Strukturen zusammenfassen und ihnen einen ihrer Semantik (ihrer Bedeutung) entsprechenden Namen geben: zum Beispiel ScreenResolution oder Address. Wir können Speicher für strukturierte Daten anfordern und somit soviele Daten anfordern, wie Speicher vorhanden ist. Das Wissen ist da, nun fehlt uns die Erfahrung, das Wissen anzuwenden. Das werden wir uns in [[c:tutorial:files|der folgenden Lektion ansehen]], die sich mit dem Laden und Speichern von Daten beschäftigen wird.