C++ Kurs

Felder und C-Strings

Die Themen:

Eindimensionale Felder

Eine sehr häufige Aufgabe ist das Abspeichern von Daten desselben Datentyps, wie z.B. das Ablegen einer Messwertreihe. Hierzu können u.a. Felder (Arrays) eingesetzt werden. Ein Feld wird wie folgt definiert:

DTYP FNAME[DIM];

DTYP kann ein beliebiger Datentyp sein, wie z.B. short, und FNAME ist der Name des Feldes. Nach dem Feldnamen folgt innerhalb der eckigen Klammern die Feldgröße DIM. Sie definiert die maximale Anzahl der Daten, die in dem Feld abgelegt werden können. Und selbstverständlich müssen die später im Feld abzulegenden Daten vom gleichen Datentyp wie das Feld sein, oder sich zumindest in diesen Datentyp konvertieren lassen.

Für die Angabe der Feldgröße DIM gelten folgende Regeln:

Nachfolgend einige Beispiele für Felddefinitionen:

PgmHeader
const auto SIZE = 10;
auto 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 dritte Definition des Feldes digits, und dies 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?

HinweisDer GNU C++ Compiler erlaubt auch die obige Definition des Feldes digits[max]. Dies ist aber eine Erweiterung des GNU Compilers und im C++ Standard nicht definiert. Wenn Sie mehr dazu wissen wollen, suchen Sie im Internet einmal nach 'C++ VLA'. VLA steht für die Abkürzung variable length array.

Mehrdimensionale Felder

Außer den vorgestellten eindimensionalen Feldern können auch mehrdimensionale Felder durch mehrfache Größenangaben definiert werden:

DTYP FNAME[DIM1][DIM2]...;

Die Anzahl der Dimensionen ist nur vom verfügbaren Speicherplatz abhängig.

PgmHeader
constexpr auto XSIZE = 10;
constexpr auto 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 Element im Feld zuzugreifen, wird zuerst der Feldname angegeben, gefolgt von einem eckigen Klammerpaar. Innerhalb dieser Klammer steht dann der Index des gewünschten Feldelements, wobei das erste Element den Index 0 besitzt und das letzte Element den Index SIZE-1. So greifen die ersten beiden Zuweisungen im nachfolgenden Beispiel auf das erste und auf das letzte Element des Feldes myArray zu. Bei mehrdimensionalen Feldern sind entsprechend der Anzahl der Felddimensionen mehrere Indizes in Klammern anzugeben. So greift die letzte Zuweisung auf das letzte Element des Feldes lines zu.

PgmHeader
// Konstanten-Definitionen
const int XSIZE=20, YSIZE=20;
// Felddefinition
short myArray[40];
char lines[XSIZE][YSIZE];

// Feldzugriffe
myArray[0] = 10;
auto var = myArray[39];
auto actChar = lines[XSIZE-1][YSIZE-1];
AchtungUnd noch einmal, weil's so wichtig ist: Das erste Element im Feld hat immer den Index 0 und das letzte Element damit den Index SIZE-1! Beachten Sie dies beim Zugriff auf das Feld. Der Compiler überprüft nicht ob der angegebene Index auch innerhalb der Feldgrenzen liegt! Und auch zur Laufzeit erfolgt keine Überprüfung des Feldzugriffs.

Initialisierung eines Feldes

Genauso wie Variablen bei ihrer Definition initialisiert werden können, können auch Felder bei ihrer Definition initialisiert werden. Nur sieht die Syntax hier ein klein wenig anders aus:

DTYP FNAME[DIM] {Wert1, Wert2, ....};

Die Initialwerte werden hier in einer geschweiften Klammer, der sogenannten Initialisiererliste, eingeschlossen und durch Komma getrennt aufgelistet.

PgmHeader
short myArray[] {10, 20, 30, 40};
...
HinweisAnstelle der obigen Initialisierung hätten Sie auch die 'alte' C/C++ Schreibweise unter Verwendung des Zuweisungsoperators verwenden können:
short myArray[] = {10, 20, 30, 40};

Bei eindimensionalen Feldern gibt's noch einen schönen Nebeneffekt. Wie im 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 jedoch gleich das nächste Problem (das wir natürlich auch lösen). Wie kann dann herausgefunden werden, welchen Index das letzte Element im Feld besitzt? Wenn die Feldgröße über eine Konstante festgelegt wird, 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 bekannte sein sollte, 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 auto SIZE = sizeof(myArray) / sizeof(myArray[0]);

Hier wird die Größe des Feldes (in Bytes) durch die Größe des ersten Elements im Feld (gleich Größe des Datentyps des Feldes) dividiert, was dann die Anzahl der Feldelemente ergibt. Und diese Berechnung wird schon beim Übersetzen des Programms durchgeführt und nicht etwa erst zur Programmlaufzeit.

HinweisAnstelle der obigen Berechnung hätten Sie auch schreiben können:
const size_t SIZE = sizeof(myArray) / 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.

Kehren wir wieder zurück zur Initialisierung von Feldern. Nachfolgend wird ein eindimensionales Feld myArray zur Aufnahme von 10 short-Werten definiert, aber in der Initialisiererliste stehen nur zwei Werte.

PgmHeader
...
short myArray[10] {10, 20};
... 

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 FNAME[DIM] {};
AchtungEs ist nicht möglich, per auto den Datentyp des Feldes mit Hilfe der Initialisiererliste zu bestimmen. D.h. folgende Anweisung erzeugt einen Fehler beim Übersetzen:
auto myArray[] {1,2,3};
Welchen Datentyp sollte auch das Feld bei folgender Anweisung besitzen: auto empty[] {};

Was aber funktionieren würde, ist folgendes:
auto myArray = {1,2,3};
myArray ist jetzt aber kein Feld sondern eine std::initializer_list. Und auf die Elemente einer std::initializer_list können Sie z.B. nicht mehr indiziert zugreifen sondern nur über Iteratoren (wird später noch erklärt).

Kommen wir nun zur Initialisierung von mehrdimensionalen Feldern. 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 Initialisiererliste nochmals in eine übergeordnete geschweifte Klammer gepackt.

PgmHeader
...
long myArray[][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 bei der Felddefinition weglassen dürfen.

Felder und Zeiger

Zeiger dienen ja bekanntermaßen zum Ablegen von Adressen. Nun können in Zeigern aber nicht nur Adressen von Variablen abgelegt werden, sondern auch von Feldelementen oder auch die Startadresse eines Feldes. Um die Adresse eines bestimmten Feldelements zu erhalten wird vor das Feldelement der Adressoperator & gestellt. Im nachfolgenden Beispiel werden zunächst zwei Felder definiert sowie die entsprechenden Zeiger für den Zugriff auf die Elemente des Feldes. In der Regel sollte ein solcher Zeiger auch den gleichen Datentyp besitzen wie das Feld, auf das er verweist. Nach der Definition der Zeiger wird ihnen die Adresse des jeweils ersten Feldelements zugewiesen.

PgmHeader
// Felddefinitionen
char  single[40];
short multi[10][3];

// Adresse des ersten Feldelements im Zeiger ablegen
decltype(&single[0]) pSingle1 = &single[0];
auto pSingle2 = single;
auto pMulti  = &multi[0][0];

Beachten Sie auch die zweite Zuweisung, die ohne den Adressoperator! Sie entspricht genau der ersten Zuweisung. Merken Sie sich stets folgenden Satz:

AchtungDer Name eines eindimensionalen Feldes entspricht der Anfangsadresse des Feldes. Dies gilt aber nur für eindimensionale Felder!

Der Datentyp eines Zeigers auf ein Feld lässt sich, außer mittels auto, auch mit Hilfe des decltype Operators automatisch bestimmen. Als Argument muss dann aber eine Referenz auf ein Feldelement angegeben werden und nicht das Feld selbst. Würde hier das Feld angegeben werden, so würde dies im obigen Beispiel zur Zeigerdefinition char[40] *pSingle; führen, was zu einem Fehler beim Übersetzen des Programm führt.

Zugriff auf Felder mittels Zeiger

Doch was fangen wir mit einem Zeiger auf ein Feld(-element) an? Wie Kapitel über Zeiger erwähnt, sind mit Zeiger nur die arithmetischen Operationen Addition und Subtraktion erlaubt. Und eine Addition des Wertes X auf einen Zeiger erhöht diesen nicht um X, sondern um X*sizeof(Datentyp), also einen short-Zeiger um X*2 oder einen long-Zeiger um X*4. Und für die Subtraktion gilt entsprechendes. Da eindimensionale Felder kontinuierlich im Speicher abgelegt werden, kann nun wahlweise indiziert (über die eckige Klammer) oder mit Hilfe eines Zeigers auf die Feldelemente zugegriffen werden.

Sehen wir und dies an einem einfachen Beispiel an.

PgmHeader
// Felddefinition und Initialisierung
short myArray[] {10,20,30,40};
// Feldgröße berechnen
constexpr int SIZE = sizeof(myArray)/sizeof(myArray[0]);

// main() Funktion
int main ()
{
   // Zeiger definieren und auf Feldanfang setzen
   auto pValues = myArray;

   // Nun alle Werte ausgeben
   for (auto index=0; index<SIZE; index++)
   {
      cout << *pValues << " ";
      // Zeiger auf nächstes Feldelement
      pValues++;
   }
}
Programmausgabe 10 20 30 40

Im Beispiel wird ein Feld myArray definiert und mit Werten initialisiert, wobei die Feldgröße vom Compiler so berechnet wird, dass genau alle Werte darin Platz finden. Da die Werte nachher in einer for-Schleife ausgeben werden sollen, wird noch die Anzahl der Elemente im Feld benötigt. Dies wird in der darunter stehenden Anweisung mit der vorhin angegebenen Formel berechnet. In main() wird dann ein entsprechender Zeiger definiert und ihm die Startadresse des Feldes zugewiesen. Anschließend wird eine for-Schleife so oft durchlaufen, wie Elemente im Feld enthalten sind. Dabei ist zu beachten, 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).

Mit Hilfe eines kleinen Kniffs kann der Inhalt des short-Feldes aber auch byteweise ausgegeben werden. Dazu ist zunächst ein unsigned char-Zeiger zu definieren, der mit der Anfangsadresse des Feldes initialisiert wird. Bei der Initialisierung des Zeigers muss jetzt aber eine entsprechende Typkonvertierung vorgenommen werden, da ein unsigned char-Zeiger normalerweise auch nur Adressen von unsigned char-Daten aufnehmen kann. In der for-Schleife ist anschließend noch die Anzahl der Schleifendurchläufe entsprechend anzupassen. Da unsigned char Daten standardmäßig als ASCII-Zeichen ausgegeben werden, muss für die numerische Ausgabe des Feldinhalts nochmals eine entsprechende Typkonvertierung erfolgen. Ferner ist bei solchen Aktionen immer daran zu denken, dass die Reihenfolge und die Anzahl der Bytes pro Datentyp nicht durch den C++ Standard vorgeschrieben ist. Die nachfolgende Ausgabe bezieht sich auf eine Plattform mit einem INTEL-Prozessor. Bei Plattformen mit anderen Prozessoren könnte die Ausgabe durchaus auch anders aussehen.

PgmHeader
// Felddefinition und Initialisierung
short myArray[] {10,20,30,40};

// main() Funktion
int main ()
{
   // Zeiger definieren und auf Feldanfang setzen
   auto pValues = reinterpret_cast<unsigned char*>(myArray);

   // Nun alle Werte ausgeben
   for (size_t index=0; index<sizeof(myArray); index++)
   {
      cout << static_cast<int>(*pValues) << " ";
      // Zeiger auf nächstes Feldelement
      pValues++;
   }
}
Programmausgabe 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 Feldern sollte vermieden werden, durch inkrementieren des Zeigers auf die Feldelemente zuzugreifen. Mehrdimensionale Felder müssen laut C++ Standard nicht zusammenhängenden im Speicher liegen. Das Einzige was der Standard garantiert ist, dass die Daten der "Zeilen" unmittelbar hintereinander im Speicher liegen.

Range-for Schleife 

Wie die meisten modernen Programmiersprachen stellt C++ auch eine spezielle for-Schleife zur Verfügung, um Felder, Strings und Objekte (wird später noch behandelt) sequentiell zu durchlaufen. Diese for-Schleife wird auch als range-for bezeichnet.

Eine range-for Schleife hat folgenden Aufbau:

for (VAR1: VAR2)
   Aktion;

VAR2 legt das zu durchlaufende Feld bzw. Objekt fest. In VAR1 werden dann nacheinander die einzelnen Elemente abgelegt. Daher muss VAR1 den gleichen Datentyp wie das Feld bzw. Objekt besitzen. Sollen die Elemente während des Schleifendurchlaufs auch verändert werden, so muss VAR1 eine entsprechende Referenz-Variable sein.

Und damit lässt sich der Inhalt eines Felds wie folgt ausgeben und auch verändern:

PgmHeader
// Felddefinition und Initialisierung
short myArray[] {10,20,30,40};

// main() Funktion
int main ()
{
   // Feldelemente nur ausgeben
   for (auto element: myArray)
   {
      cout << element << ' ';
   }
   // Feldelemente auch veraendern
   for (auto& element: myArray)
   {
      element *= 10;
   }
}

C-Strings und Felder

HinweisDie nachfolgenden Ausführungen dienen eigentlich nur zur Übung im Umgang mit Feldern. C++ besitzt eine Standard-Bibliothek welche die Bearbeitung von Strings erheblich vereinfacht. Mehr dazu aber gleich im nächsten Kapitel.

Alle Funktionsdeklarationen der nachfolgend beschriebenen C-String Funktionen sind in der Header-Datei cstring enthalten, die dann auch entsprechend einzubinden ist.

C-Strings waren bisher immer Literale, die in Anführungszeichen "..." eingeschlossen wurden.

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

Ein solcher C-String kann nun aber auch in einem Feld abgelegt werden. Da C-Strings in der Regel aus 1 Byte großen ASCII-Zeichen bestehen, werden sie in einem char-Feld abgelegt. Wenn 16-Bit große Zeichen verarbeitet werden sollen (weil Sie z.B. den chinesischen Zeichensatz so mögen), ist anstelle eines char-Feldes ein wchar_t Feld zu verwenden.

Bei der Definition eines char-Feldes kann das Feld auch gleich mit einem C-String belegt werden, so wie nachfolgend dargestellt.

PgmHeader
...
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 besitzt damit 9 Elemente.

Um einen C-String in ein char-Feld zu kopieren wird die Funktion

char* strcpy(char *pDest, const char *pSource);

verwendet. pDest ist ein char-Zeiger auf den Speicherbereich, in den der durch pSource adressierte C-String kopiert werden soll. pDest muss immer ein char-Zeiger auf ein Feld sein, während pSource entweder ein C-String Literal oder die Adresse eines weiteren char-Feldes sein kann. Wenn sich die Bereiche von pDest und pSource überlappen, ist das Ergebnis des Kopiervorganges undefiniert.

PgmHeader
#include <cstring>
// char-Felder definieren
char acFeld[40];

int main()
{
   ...
   // C-String ins char-Feld kopieren
   strcpy(acFeld,"Ein C-String");
}
AchtungAchten Sie unbedingt darauf, dass das Zielfeld ausreichend groß ist um den zu kopierenden C-String, einschließlich der abschließenden binären 0, aufnehmen zu können. Der Compiler überprüft dies nicht!

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

Beispiel und ÜbungUnd hier geht's zum Beispiel und zur Übung.