C++ Tutorial

Felder und C-Strings

Felddefinition

Eine häufige Aufgabe ist das Abspeichern und Bearbeiten von mehreren Daten desselben Datentyps, wie dies z.B. bei einer Messwertreihe der Fall ist. Hierfür können u.a. Felder (Arrays) eingesetzt werden. 

Ein Feld wird wie folgt definiert:

DTYP fName[DIM];

DTYP ist der Datentyp der im Feld abzulegenden Daten und fName ist der Name des Feldes. Danach folgt innerhalb einer eckigen Klammer die Feldgröße DIM.

Für die Angabe der Feldgröße DIM gelten folgende Einschränkungen:

  • Die Feldgröße muss eine Ganzzahl sein und
  • sie muss ein konstanter Ausdruck sein
1: constexpr auto SIZE = 10;
2: auto max = 20;
3: // Felddefinitionen
4: short values[40];    // Feldgrösse durch Literal
5: char text[SIZE];     // Feldgrösse durch constexpr
6: long digits[max];    // Nicht erlaubt, Variable!

Das erste Feld values kann maximal 40 short-Werte aufnehmen, wobei die Feldgröße über ein Literal definiert wird. Beim zweiten Feld text erfolgt die Definition der Feldgröße über eine constexpr. 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.

Mehrdimensionale Felder werden durch mehrfache Größenangaben definiert:

DTYP fName[DIM1][DIM2]...;

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

1: constexpr auto XSIZE = 10;
2: constexpr auto YSIZE = 50;
3: short table[XSIZE][YSIZE];    // 2-dimensionales Feld
4: char big[10][10][5];          // 3-dimensionales Feld

Zugriff auf Feldelemente

Um auf ein Feldelement zuzugreifen, wird zuerst der Feldname angegeben, gefolgt vom Index in eckigen Klammern. Das erste Element besitzt den Index 0 und das letzte Element demzufolge SIZE-1. 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.

1: // Konstanten-Definitionen
2: const auto XSIZE=20, YSIZE=20;
3: // Felddefinitionen
4: short myArray[40];
5: char lines[XSIZE][YSIZE];
6:
7: // Feldzugriffe
8: myArray[0] = 10;          // 1. Element beschreiben
9: auto var = myArray[39];   // letztes Element auslesen
10: auto actChar = lines[XSIZE-1][YSIZE-1];

Initialisierung eines Feldes

Genauso wie Variablen können auch Felder bei ihrer Definition initialisiert werden.

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

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

short myArray[] {10, 20, 30, 40};

Bei eindimensionalen Feldern gibt es noch einen schönen Nebeneffekt. Wie in den Beispielen ersichtlich, kann die Angabe der Feldgröße bei initialisierten Feldern entfallen. Das Feld wird dann so groß angelegt, dass die angegebenen Werte darin Platz finden.

Und was passiert, wenn ein eindimensionales Feld myArray zur Aufnahme von 10 short-Werten definiert wird, in der Initialisiererliste aber lediglich zwei Werte stehen?

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 mit 0 zu initialisieren, besteht in der Ausführung der Anweisung

DTYP fName[DIM] {};

Bei mehrdimensionalen Feldern werden die Initialwerte für die einzelnen Dimensionen zunächst in einer geschweiften Klammer zusammengefasst. Diese Klammern werden dann, durch Komma getrennt, aufgelistet. Zum Schluss wird die gesamte Initialisiererliste nochmals in eine übergeordnete geschweifte Klammer gepackt.

long myArray[][3]
{
   { 1L, 2L, 3L},
   {10L, 20L, 30L}
};

Im Beispiel wird ein Feld mit der Dimension 2x3 definiert und initialisiert. Beachten Sie bei mehrdimensionalen Feldern, dass nur die erste Dimension bei der Felddefinition entfallen kann.

sizeof Operator

Um die Anzahl der Elemente bei (initialisierten) Feldern zu berechnen, hilft der sizeof-Operator. Er liefert den von einem Datum oder Datentyp belegten Speicherplatz in Bytes zurück. Und damit kann mit folgender Formel die Anzahl der Elemente in einem Feld berechnet werden:

constexpr 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, welches dann die Anzahl der Feldelemente ergibt. Und diese Berechnung wird beim Übersetzen des Programms durchgeführt und nicht erst zur Programmlaufzeit.

Felder und Zeiger

Feldadresse

Um die Adresse eines Feldelements zu erhalten, wird vor das Feldelement der Adressoperator & gestellt. Im nachfolgenden Beispiel werden zwei Felder definiert sowie die entsprechenden Zeiger für den Zugriff darauf. In der Regel sollte ein solcher Zeiger den gleichen Datentyp besitzen, wie das Feld auf das er verweist.

1: // Felddefinitionen
2: char single[40];
3: short multi[10][3];
4:
5: // Zeiger definieren und Adresse des ersten Feldelements
6: // im Zeiger ablegen
7: decltype(&single[0]) pSingle1 = &single[0];
8: auto pSingle2 = single;
9: auto pMulti = &multi[0][0];

Beachten Sie die Zuweisung in Zeile 8, die ohne den Adressoperator! Sie entspricht genau der ersten Zuweisung. Merken Sie sich folgenden Satz:

Der 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 mithilfe des decltype Spezifizierers bestimmen, so wie in Zeile 7 im Beispiel. Als Argument ist dann aber eine Referenz auf ein Feldelement anzugeben und nicht das Feld.

Zugriff auf Felder mittels Zeiger

Wie im 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. Für die Subtraktion gilt Entsprechendes. Da die Elemente eines eindimensionalen Feldes fortlaufend im Speicher liegen, kann wahlweise indiziert (über die eckige Klammer) oder mithilfe eines Zeigers auf die Feldelemente zugegriffen werden.

1: #include <iostream>
2: #include <print>
3:
4: // Felddefinition und Initialisierung
5: short myArray[] {10,20,30,40};
6: // Feldgröße berechnen
7: constexpr int SIZE = sizeof myArray/sizeof myArray[0];
8:
9: // main() Funktion
10: int main ()
11: {
12:    // Zeiger definieren und auf Feldanfang setzen
13:    // und dann alle Werte ausgeben
14:    for (auto pValues = myArray;
15:         pValues < (myArray+SIZE); pValues++)
16:    {
17:        std::print("{},",*pValues);
18:    }
19: }

10,20,30,40,

In der for-Schleife wird ein entsprechender Zeiger definiert und ihm die Startadresse des Feldes zugewiesen. Anschließend wird die Schleife so lange durchlaufen, solange die im Zeiger abgelegte Adresse auf ein gültiges Feldelement verweist (letztes Feldelement ist myArray+SIZE-1!). Innerhalb der for-Schleife werden durch Dereferenzierung des Zeigers die einzelnen Feldelemente ausgegeben.

Mithilfe eines kleinen Kniffs kann der Inhalt des short-Feldes z.B. 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 ist eine entsprechende Typkonvertierung durchzuführen. Die Anzahl der Schleifendurchläufe der for-Schleife entspricht jetzt exakt der Größe des Feldes in Bytes und damit dem Wert des sizeof-Operators. 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.

1: #include <iostream>
2: #include <print>
3:
4: // Felddefinition und Initialisierung
5: short myArray[] {10,20,30,40};
6:
7: // main() Funktion
8: int main ()
9: {
10:    // Zeiger definieren und auf Feldanfang setzen
11:    auto pValues = reinterpret_cast<unsigned char*>(myArray);
13:    // Nun alle Werte ausgeben
14:    for (size_t index=0; index<sizeof myArray; index++)
15:    {
16:        std::print("{:d},",*pValues);
17:        // Zeiger auf nächstes Feldelement
18:        pValues++;
19:    }
20: }

10,0,20,0,30,0,40,0,

Bei eindimensionalen Feldern ist die Sache mit dem Zugriff über Zeiger relativ einfach. Doch wie verhält sich dies bei mehrdimensionalen Feldern? Mehrdimensionale Felder müssen laut C++-Standard nicht zusammenhängenden im Speicher liegen. Das Einzige, das der Standard garantiert ist, dass die Daten der "Zeilen" unmittelbar hintereinander im Speicher liegen.

range-for-Schleife

Zum sequentiellen Bearbeiten von Daten in Felder, Strings oder später auch Container, kann eine spezielle for-Schleife, die sogenannte range-for-Schleife, eingesetzt werden.

Die range-for-Schleife hat folgenden Aufbau:

for ([INIT;]DTYP var: array)
    ANWEISUNG;

Nach der Auswertung des optionalen Initialisierungsausdrucks INIT durchläuft die Schleife sequentiell den Datenbereich array und legt jedes Element in der Variablen var ab. Aus diesem Grund sollte der Datentyp DTYP gleich dem Datentyp der array-Elemente sein. Sollen die Elemente während des Schleifendurchlaufs verändert werden, muss DTYP eine entsprechende Referenz sein.

1: #include <iostream>
2: #include <print>
3:
4: // Feldgroesse
5: constexpr auto SIZE=10;
6: // Tabelle definieren
7: int feld[SIZE];
8:
9: int main()
10: {
11:    // Feld mit aufsteigenden Werten fuellen
12:    for (int val = 1; auto& elem: feld)
13:       elem = val++;
14:
15:    // Feld ausgeben
16:    for (auto elem: feld)
17:        std::print("{},",elem);
18:    std::cout << '\n';
19: }

Um mittels der range-for-Schleife ein mehrdimensionales Feld zu durchlaufen, muss var bei den äußeren Schleifen eine Referenzvariable sein.

1: // 2-dimensionales Feld ausgeben
2: for (auto& zeile: feld)
3: {
4:    for (auto elem: zeile)
5:       std::print("{},",elem);
6:    std::cout << '\n';
7: }

C-Strings und Felder

Da ein C-String in der Regel aus ASCII-Zeichen besteht, wird er in einem char-Feld abgelegt. Sollen Zeichen eines erweiterten Zeichensatzes bearbeitet werden, ist anstelle eines char-Feldes ein wchar_t-Feld zu verwenden.

Bei der Definition eines char-Feldes kann das Feld mit einem C-String initialisiert werden.

char myText[] {"C-String"};

Paragraph

Das char-Feld wird dann so dimensioniert, dass der C-String einschließlich der abschließenden 0 darin Platz findet. D.h., das obige Feld myText besitzt 9 Elemente.

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

char* strcpy_s(char *pDest, size_t maxcopy, const char *pSource);

verwendet. pDest ist ein char-Zeiger auf den Speicherbereich, in den der durch pSource adressierte C-String kopiert wird und maxcopy bestimmt die maximale Anzahl der zu kopierenden Zeichen. maxcopy muss größer als "Länge des zu kopierenden C-Strings+1" sein und die Bereiche von pDest und pSource dürfen sich nicht überlappen.

1: #include <cstring>
2: // char-Felder definieren
3: char acFeld[40];
4:
5: int main()
6: {
7:    // C-String ins char-Feld kopieren
8:    strcpy_s(acFeld,sizeof acFeld, "Ein C-String");
9: }

Weitere Informationen zur Bearbeitung von C-Strings finden Sie unter https://en.cppreference.com unter dem Stichwort Null-terminated byte strings.


Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz