Eindimensionale Felder
Mehrdimensionale Felder
Zugriff auf Feldelemente
Initialisierung eines Feldes
Felder und Zeiger
C-Strings und Felder
Beispiel und Übung
Eine häufige Aufgabenstellung ist das Abspeichern von vielen Daten des selben Datentyps, wie z.B. das Ablegen einer Messwertreihe. Hierzu können u.a. Felder (Arrays) eingesetzt werden. Ein Feld wird wie folgt definiert:
| DTYP feldname[DIM]; |
DTYP kann ein beliebiger Datentyp sein, wie z.B. short. Der Feldname entspricht dem Variablennamen einer 'normalen' Variable. Innerhalb der eckigen Klammer steht die Feldgröße DIM. Sie gibt die maximale Anzahl der Daten an, die in einem Feld abgespeichert werden können. Selbstverständlich müssen alle im Feld abzulegenden Daten aber den gleichen Datentyp besitzen wie das Feld, oder zumindest in diesen Datentyp konvertierbar sein.
Für die Angabe der Feldgröße DIM gelten folgende Regeln:
Nachfolgend einige Beispiele für Felddefinitionen:
|
const int SIZE = 10; short int max = 20; // Felddefinitionen short values[40]; // Feldgrösse durch Lateral char text[SIZE]; // Feldgrösse durch Konstante long digits[max]; // Nicht erlaubt, Feldgrösse durch Variable |
Das erste Feld values kann maximal 40 short-Werte aufnehmen. Hier wird die Feldgröße über ein Literal definiert. Beim zweiten Feld text erfolgt die Definition der Feldgröße über eine benannte Konstante. Nicht erlaubt dagegen ist die dargestellte Definition des Feldes digits, und das obwohl die Variable max bei ihrer Definition initialisiert wird. Feldgrößen müssen durch einen konstanten Ausdruck definiert werden. Denn welche Größe sollte das Feld besitzen wenn später im Programm die Variable max z.B. auf 30 gesetzt wird?
Außer den vorgestellten eindimensionalen Felder können auch mehrdimensionale Felder wie folgt definiert werden:
| Datentyp Feldname[DIM1][DIM2]...; |
Die Anzahl der Dimensionen ist nur vom verfügbaren Speicherplatz abhängig.
|
const int XSIZE = 10; const int YSIZE = 50; short table[XSIZE][YSIZE]; // 2-dimensionales Feld char big[10][10][5]; // 3-dimensionales Feld |
Das erste Feld table im Beispiel ist ein 2-dimensionales Feld. Sie können sich unter einem 2-dimensionalen Feld eine Art Tabelle mit Zeilen und Spalten vorstellen. Das zweite Felder big ist ein 3-dimensionales Feld, also eine Art Karteikasten mit Tabellen auf den einzelnen Karteikarten. Das nachfolgende Bild soll diesen Sachverhalt nochmals verdeutlichen.

Soweit zur Definition eines Feldes. Sehen wir uns jetzt an, wie auf die Feldelemente zugegriffen wird. Um auf ein einzelnes Element im Feld zuzugreifen, wird zuerst der Feldname angegeben, gefolgt von einem eckigen Klammerpaar. In dieser Klammer steht dann der Index des gewünschten Feldelements, wobei das erste Element den Index 0 besitzt. So greifen die ersten beiden Zuweisungen im nachfolgenden Beispiel auf das erste und auf das letzte Element des Feldes array zu. Bei mehrdimensionalen Feldern sind entsprechend der Anzahl der Felddimensionen mehrere Indizes in Klammern anzugeben. Die letzte Zuweisung greift damit auf das letzte Element des Feldes lines zu.
|
// Konstanten-Definitionen const int XSIZE=20, YSIZE=20; // Felddefinition short array[40]; char lines[XSIZE][YSIZE]; // Feldzugriffe array[0] = 10; short var = array[39]; char actChar = lines[XSIZE-1][YSIZE-1]; |
|
|
Wie Sie sicher noch wissen, können Variablen bei ihrer Definition initialisiert werden. Und dies ist auch bei Feldern möglich. Nur sieht die Syntax hier ein klein wenig anders aus:
| DTYP Feldname[DIM] = {Wert1, Wert2, ....}; |
Die einzelnen Initialwerte werden hier in einer geschweiften Klammer eingeschlossen und durch Komma getrennt aufgelistet.
|
short array[] = {10, 20, 30, 40}; ... |
Bei eindimensionalen Feldern gibt's noch einen schönen 'Nebeneffekt'. Wie im obigen Beispiel ersichtlich, kann die Angabe der Feldgröße bei initialisierten Feldern entfallen. Das Feld wird dann vom Compiler genau so groß angelegt, dass die angegebenen Werte darin Platz finden.
Dabei stellt sich hier jedoch gleich das nächste Problem (das wir natürlich auch lösen). Wie können Sie herausfinden, welchen Index dann das letzte Element im Feld besitzt? Wenn Sie die Feldgröße über eine Konstante festlegen, so kann diese Konstante zur Berechnung des letzten Elements herangezogen werden. Was aber, wenn das Feld ohne explizite Größenangabe erstellt wurde? Hier hilft uns der sizeof Operator weiter. Wie Sie vielleicht noch wissen, liefert dieser Operator den von einer Variablen oder einem Datentyp belegten Speicherplatz in Bytes zurück. Und damit kann mit folgender Formel, sozusagen nachträglich, die Anzahl der Elemente in einem Feld berechnet werden:
| const size_t SIZE = sizeof(array) / sizeof(array[0]); |
Hier wird die Größe des Feldes (in Bytes) durch die Größe des ersten Elements im Feld (=Größe des Datentyps des Feldes) dividiert, was dann die Anzahl der Feldelemente ergibt. Diese Berechnung wird schon beim Übersetzen des Programms durchgeführt und nicht etwa erst zur Programmlaufzeit. Beachten Sie auch, dass die so berechnete Feldgröße als Konstante abgelegt wird.
|
const size_t SIZE = sizeof(array) / sizeof(Datentyp des Feldes); Diese Berechnung ist aber nicht ganz so elegant wie die vorherige Berechnung, da der Datentyp des Feldes direkt mit in die Berechnung eingeht. Ändern Sie zu einem späteren Zeitpunkt einmal den Datentyp des Feldes, so dürfen Sie dann nicht vergessen, bei der Berechnung der Feldgröße den Datentyp mit anzupassen. |
Sehen Sie sich nun einmal das nachfolgende Beispiel an. Dort wird das eindimensionale Feld array zur Aufnahme von 10 short-Werten definiert.
|
... short array[10] = {10, 20}; ... |
In der Initialisierungsliste stehen aber nur zwei Werte. In diesem Fall werden die ersten beiden Elemente mit 10 bzw. 20 initialisiert und die restlichen Elemente mit 0. Der damit schnellste Weg, ein eindimensionales Feld explizit mit 0 zu initialisieren, besteht in der Ausführung der Anweisung
| DTYP Feldname[DIM] = {0}; |
Die 0 in der Klammer müssen Sie immer mit angeben, da eine leere Klammer nicht zulässig ist.
So, sehen wir uns als nächstes an, wie mehrdimensionale Felder initialisiert werden. Bei mehrdimensionalen Feldern werden die Initialwerte für die einzelnen Dimensionen zunächst in geschweiften Klammern zusammengefasst. Diese Klammern werden dann, durch Komma getrennt, aufgelistet. Zum Schluss wird die gesamte Initialisierungsliste nochmals in eine übergeordnete geschweifte Klammer gepackt.
|
... long array[][3] = { { 1L, 2L, 3L}, {10L,20L,30L} }; |
Im Beispiel wird ein Feld mit der Dimension 2x3 definiert und entsprechend initialisiert. Beachten Sie bei mehrdimensionalen Feldern, dass Sie hier nur die erste Dimension weglassen dürfen.
Kommen wir jetzt wieder auf die so beliebten Zeiger zu sprechen. Sicher wissen Sie noch, dass Zeiger zur Aufnahme von Adressen dienen. Nun können in Zeigern aber nicht nur Adressen von 'einfachen' Variablen abgelegt werden, sondern auch von Feldelementen und sogar die Startadresse eines Feldes. Um die Adresse eines bestimmten Feldelements zu erhalten, wird wie üblich vor das Feldelement der Adressoperator & gestellt. Nachfolgend sehen Sie die Definitionen eines ein- und eines zweidimensionalen Feldes. Im Anschluss daran werden die entsprechenden Zeiger definiert, die natürlich mit dem Datentyp des Feldes übereinstimmen müssen. Nach der Definition der Zeiger wird ihnen die Adresse des jeweils ersten Feldelements zugewiesen, d.h. im Zeiger wird die Startadresse des Feldes abgelegt. Beachten Sie bitte die zweite Zuweisung, die ohne den Adressoperator! Sie entspricht genau der ersten Zuweisung.
|
// Felddefinitionen char single[40]; short multi[10][3]; // Zeigerdefinitionen char *pSingle; short *pMulti; // Adresse des ersten Feldelements im Zeiger ablegen pSingle = &single[0]; pSingle = single; pMulti = &multi[0][0]; // Adresse des letzten Feldelements pSingle = &single[39]; pMulti = &multi[9][2]; |
Merken Sie sich bitte folgenden Satz:
|
Der Name eines eindimensionalen Feldes entspricht immer der Anfangsadresse des Feldes. Dies gilt aber nur für eindimensionale Felder! |
Doch was fangen wir mit einem Zeiger auf ein Feld(-element) an? Wissen Sie noch, welche arithmetischen Operationen auf Zeiger erlaubt sind? Nur die arithmetischen Operationen Addition und Subtraktion sind erlaubt. Und eine Addition des Wertes X auf einen Zeiger erhöht diesen nicht um X, sondern um X*sizeof(Datentyp), z.B. einen short-Zeiger um X*2 oder einen long-Zeiger um X*4. Für die Subtraktion gilt entsprechendes. Da eindimensionale Felder kontinuierlich im Speicher abgelegt werden, kann nun wahlweise indiziert (über die eckige Klammer) oder mithilfe eines Zeigers auf die Feldelemente zugegriffen werden.
Sehen wir und dies an einem einfachen Beispiel an.
|
// Felddefinition und Initialisierung short array[] = {10,20,30,40}; // Feldgröße berechnen const int SIZE = sizeof(array)/sizeof(array[0]); // main() Funktion int main () { // Zeiger definieren und auf Feldanfang setzen short *pValues = array; // Nun alle Werte ausgeben for (int index=0; index<SIZE; index++) { cout << *pValues << " "; // Zeiger auf nächstes Feldelement pValues++; } } |
|
10 20 30 40 |
Im Beispiel wird ein Feld array definiert und mit Werten initialisiert. Beachten Sie, dass die Feldgröße hier automatisch vom Compiler so berechnet wird, dass alle Werte genau darin Platz finden. Da wir die Werte nachher in einer for-Schleife ausgeben, benötigen wir noch die Anzahl der Elemente im Feld. Dies wird in der darunter stehenden Anweisung mit der vorhin angegebenen Formel berechnet. In main() wird dann der Zeiger definiert, dem die Startadresse des Feldes zugewiesen wird. Der Datentyp des Zeigers muss natürlich mit dem Datentyp des Feldes übereinstimmen. Anschließend wird eine for-Schleife so oft durchlaufen, wie Elemente im Feld enthalten sind. Beachten Sie hier bitte, dass die for-Schleife bei 0 beginnt und folglich bei SIZE-1 enden muss, deshalb die Abfrage auf KLEINER_ALS. Innerhalb der for-Schleife werden durch Dereferenzierung des Zeigers die einzelnen Feldelemente ausgegeben und nach jeder Ausgabe der Zeiger um eins erhöht (entspricht einer Erhöhung um sizeof(short) Bytes, da der Zeiger vom Typ short ist).
Mithilfe eines kleinen 'Kniffs' könnten Sie aber auch den Inhalt des short-Feldes byteweise ausgeben. Dazu definieren Sie zunächst den Zeiger auf das Feld als unsigned char-Zeiger. Bei der Zuweisung der Anfangsadresse des Feldes an den Zeiger müssen Sie jetzt aber eine entsprechende Typkonvertierung vornehmen, da in unsigned char-Zeigern normalerweise auch nur Adressen von unsigned char-Daten abgelegt werden können. Zum Schluss muss noch die Anzahl der Durchläufe der for-Schleife angepasst werden. Beachten Sie bei der Ausgabe auch die Typkonvertierung des ausgelesenen unsigned char-Wertes. unsigned char-Werte werden standardmäßig als ASCII-Zeichen dargestellt, wir wollen hier aber den numerischen Wert ausgeben. Ferner sollten Sie bei solchen Aktionen immer daran denken, dass die Reihenfolge und Anzahl der Bytes bei den Datentypen nicht durch den C++ Standard vorgeschrieben ist. Die nebenstehende Ausgabe erhalten z.B. auf einem 32-Bit System mit einem INTEL-Prozessor. Bei Systemen mit MOTOROLA-Prozessoren würde die Ausgabe anders aussehen.
|
// Felddefinition und Initialisierung short array[] = {10,20,30,40}; // main() Funktion int main () { // Zeiger definieren und auf Feldanfang setzen unsigned char *pValues = reinterpret_cast<unsigned char>(array); // Nun alle Werte ausgeben for (int index=0; index<sizeof(array); index++) { cout << static_cast<int>(*pValues) << " "; // Zeiger auf nächstes Feldelement pValues++; } } |
|
10 0 20 0 30 0 40 0 |
Bei eindimensionalen Feldern ist die Sache mit dem Zugriff über Zeiger noch relativ einfach. Doch wie verhält dies sich nun bei mehrdimensionalen Feldern? Bei mehrdimensionalen Felder sollten Sie vermeiden, durch inkrementieren des Zeigers auf aufeinanderfolgende Feldelemente zuzugreifen. Mehrdimensionale Felder müssen laut C++ Standard nicht zusammenhängenden im Speicher liegen. Das einzige was der Standard garantiert ist, ist dass die Daten der Zeilen unmittelbar hintereinander im Speicher liegen.
|
|
Strings kennen Sie ja schon als Literale bzw. Konstanten bei der Ausgabe. Dort haben Sie bestimmt String-Literale verwendet, die in Anführungszeichen "..." eingeschlossen waren.
|
|
Ein C-String kann nun aber auch in einem Feld abgelegt werden. Da C-Strings in der Regel aus ASCII-Zeichen bestehen, werden sie in einem char-Feld abgelegt. Wenn Sie mit 16-Bit breiten Zeichen arbeiten (weil Sie z.B. den chinesischen Zeichensatz so mögen), müssen Sie anstelle eines char-Feldes ein wchar_t Feld verwenden. Alle Funktionsdeklarationen der nachfolgend beschriebenen Stringfunktionen sind in der Header-Datei cstring enthalten.
Bei der Definition eines char-Feldes können Sie dieses auch gleich mit einem C-String initialisieren, so wie nachfolgend dargestellt.
|
... char myText[] = "C-String"; ... |
Das char-Feld wird dann genau so groß dimensioniert, dass der C-String einschließlich der abschließenden 0 darin Platz findet. D.h. das obige Feld myText belegt 9 Bytes.
Um einen C-String in ein char-Feld zu kopieren wird die Funktion
| char* strcpy(char *pDest, const char *pSource); |
verwendet.
|
#include <cstring> // char-Felder definieren char acFeld[40]; int main() { ... // String ins char-Feld kopieren strcpy(acFeld,"Ein C-String"); } |
pDest ein char-Zeiger auf den Speicherbereich, in den der durch pSource adressierte String kopiert werden soll. pDest muss immer ein char-Zeiger auf ein Feld sein, während pSource entweder ein String-Literal (-konstante) oder die Adresse eines weiteren char-Feldes sein kann. Wenn pDest und pSource sich überlappen ist das Ergebnis des Kopiervorganges undefiniert.
Weitere Informationen zur Verarbeitung von C-Strings erhalten Sie in der Online-Hilfe zu Ihrem Compiler. Suchen Sie dort einfach nach Funktionen die mit str... beginnen (z.B. strcmp um zwei C-Strings zu vergleichen).
|
|
|
|
Statistik-Daten:
Klasse 1 besetzt mit 969 Werten
Kleinster Klassenwert: 947 |
|
// Beispiel zu Felder // Zuerst Dateien einbinden #include <iostream> #include <iomanip> #include <cstdlib> using std::cout; using std::endl; // main() Funktion int main () { // Anzahl der Klassen, Wertebereich und Anzahl der Werte const unsigned short ANZKLASSEN = 10; const unsigned short BEREICH = 100; const unsigned short ANZWERTE = 10000; // Klassenfeld definieren und mit 0 initialisieren unsigned short klasse[ANZKLASSEN] = {0}; // Wertebereich pro Klasse const unsigned short DELTA = BEREICH/ANZKLASSEN; // Zahlen nun erzeugen for (int index=0; index<ANZWERTE; index++) { // Zufallszahl im Wertebereich erzeugen unsigned short wert = rand()%BEREICH; // Klasse berechnen und Wert einsortieren unsigned short klassenIndex = wert / DELTA; klasse[klassenIndex]++; } // Variablen für die Aufnahem der Klasse mit den wenigsten // bzw. meisten Werten definieren unsigned short minKlasse, maxKlasse; // Min/Max Werte Werte bestimmen minKlasse = maxKlasse = klasse[0]; for (int index=1; index<ANZKLASSEN; index++) { if (klasse[index]<minKlasse) minKlasse = klasse[index]; else if (klasse[index]>maxKlasse) maxKlasse = klasse[index]; } // Statistikdaten ausgeben cout << "Statistik-Daten:" << endl; cout << "================" << endl << endl; for (int index=0; index<ANZKLASSEN; index++) cout << "Klasse " << std::setw(2)<< index+1 << " besetzt mit " << std::setw(5) << klasse[index] << " Werten\n"; cout << "\nKleinster Klassenwert: " << minKlasse << endl; cout << "Grösster Klassenwert: " << maxKlasse << endl; } |
Ihre Aufgabe ist es nun, eine kleine 'Tabellenkalkulation' zu schreiben.
Für die Tabelle definieren Sie ein Feld mit z.B. 10x10 Einträgen. Füllen Sie dieses Feld dann mit Zufallszahlen im Bereich 0...9 aus.
Geben Sie dann die Tabelle selbst so wie die Zeilen- und Spaltensummen aus (siehe nachfolgenden Programmausgabe). Achten Sie bei der Ausgabe auf eine saubere Formatierung.
|
1 7 4 0 9 4 8
8 2 4 : 47 |