C++ Kurs

Felder und C-Strings

Die Themen:

Eindimensionale Felder
Mehrdimensionale Felder
Zugriff auf Feldelemente
Initialisierung eines Feldes
Felder und Zeiger
C-Strings und Felder
Beispiel und Übung

Eindimensionale Felder

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?

Mehrdimensionale Felder

 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.

Zugriff auf Feldelemente

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];

Und noch einmal, weil's so wichtig ist: Das erste Element im Feld hat immer den Index 0 und das letzte Element damit den Index GRÖSSE-1! Beachten Sie dies beim Zugriff auf das Feld. Der Compiler überprüft nicht, ob der angegebene Index auch innerhalb de Feldgrenzen liegt! Und auch zur Laufzeit erfolgt keine Überprüfung des Feldzugriffs.

Initialisierung eines Feldes

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.

Anstelle der obigen Berechnung hätten Sie auch schreiben können:

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.

Felder und Zeiger

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!

Zugriff auf Felder mittels Zeiger

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.

C-Strings und Felder

Die nachfolgenden Ausführungen dienen eigentlich nur zur Übung im Umgang mit Feldern. C++ besitzt eine Standard Bibliothek die Bearbeitung von Strings erheblich vereinfacht. Mehr dazu aber gleich in der nächsten Lektion. Viele ältere (und leider auch neuere) C++ Programme legen Strings aber immer noch in char-Feldern ab.

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.

Zur Erinnerung: Das letzte Zeichen in einem String muss immer eine binäre 0 sein. Erst dadurch wird der String abgeschlossen! Damit eine Verwechselung mit den nachher gleich behandelten 'echten' C++ Strings vermieden wird, werden alle Strings die mit einer binären 0 abgeschlossen werden, in Zukunft als C-Strings bezeichnet.

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).

Achten Sie unbedingt darauf, dass das Ziel eine Kopiervorgangs stets groß genug ist um den resultierenden C-String einschließlich der abschließenden binären 0 aufnehmen zu können. Der Compiler überprüft dies nicht! Es gibt zum C Standard auch einen so genannten Technical Report TR 24731, welcher erweiterte, sicherere C-String Funktion definiert. Diese neuen C-String Funktionen besitzen den gleichen Namen wie ihre Standard C-Funktionen, haben aber als Suffix _s (z.B. strcpy_s).

Beispiel und Übung

Beispiel:

Das Beispiel zeigt die Anwendung eines Feldes anhand eines kleinen Statistikprogramms.

Eine Anzahl von Zufallszahlen in einem bestimmten Wertebereich soll klassifiziert werden. Hierbei wird der gesamte Wertebereich der Zahlen in kleiner Bereiche, Klassen genannt, unterteilt.

Beispiel:

Der Wertebereich der Daten beträgt 0...99 und soll in 20 Klassen unterteilt werden.
Daraus folgt:

Wertebereich der 1. Klasse : 0...4
Wertebereich der 2. Klasse : 5...9
Wertebereich der 3. Klasse : 10...14
...
Wertebereich der 20. Klasse: 95...99

Die Zufallszahlen werden entsprechend ihrem Wert in die einzelnen Klassen 'einsortiert', d.h. eine Klassen zählt wie oft eine Zahl in ihrem Bereich auftritt.

Das Programm gibt dann die Verteilung der Zufallszahlen auf die einzelnen Klassen aus. Zusätzlich wird die Klasse mit den wenigsten und meisten Werten ausgegeben. Wenn der Standard-Algorithmus zur Erzeugung von Zufallszahlen richtig funktioniert, so müssten alle Klassen etwa gleich viele Werte enthalten, nämlich Anzahl der Zufallszahlen dividiert durch Anzahl der Klassen (bei genügend großer Anzahl untersuchter Werte).

Im Beispiel wird die Anzahl der Klassen, der Wertebereich der Zufallszahlen und die Anzahl der Zufallszahlen durch Konstanten festgelegt. Dadurch können Sie diese Parameter relativ leicht abändern. Im Programm ein Feld definiert, dessen Feldelemente als Klassenzähler fungieren. Die Zufallszahlen werden dann in einer for-Schleife erzeugt und in die Klassen einsortiert.

Anschließend wird die Klasse mit den wenigsten und meisten Werte gesucht. Zum Schluss werden alle Klassen so wie die Klasse mit den wenigsten und meisten Werten ausgegeben.

Statistik-Daten:
================

Klasse 1 besetzt mit 969 Werten
Klasse 2 besetzt mit 947 Werten
Klasse 3 besetzt mit 1054 Werten
Klasse 4 besetzt mit 978 Werten
Klasse 5 besetzt mit 1027 Werten
Klasse 6 besetzt mit 989 Werten
Klasse 7 besetzt mit 1020 Werten
Klasse 8 besetzt mit 955 Werten
Klasse 9 besetzt mit 1021 Werten
Klasse 10 besetzt mit 1040 Werten

Kleinster Klassenwert: 947
Grösster Klassenwert: 1054


// 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;
}

Übung:

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
  5  5  1  7  1  1  5  2  7  6 : 40
  1  4  2  3  2  2  1  6  8  5 : 34
  7  6  1  8  9  2  7  9  5  4 : 58
  3  1  2  3  3  4  1  1  3  8 : 29
  7  4  2  7  7  9  3  1  9  8 : 57
  6  5  0  2  8  6  0  2  4  8 : 41
  6  5  0  9  0  0  6  1  3  8 : 38
  9  3  4  4  6  0  6  6  1  8 : 47
  4  9  6  3  7  8  8  2  9  1 : 57
-------------------------------
 49 49 22 46 52 36 45 38 51 60

Lösung ansehen!