Felder und C-Strings
Definition eines Feldes
Eine häufige Aufgabe ist das Abspeichern und Bearbeiten von mehreren, logisch zusammengehörigen Daten eines Datentyps, wie dies z.B. bei einer Messwertreihe der Fall ist. Hierfür können u.a. Felder eingesetzt werden.
Die C++-Standardbibliothek enthält eine Vielzahl von Container, um solche Daten abzulegen. Auf diese Container können Algorithmen aus der Standardbibliothek angewandt werden, um z.B. einen Wert zu suchen oder die Daten zu sortieren. Prüfen Sie deshalb vor dem Einsatz eines Feldes, ob sich zum Ablegen von Daten einer der vordefinierten Container besser eignet.
Container und Algorithmen aus der Standardbibliothek werden im Teil 3 behandelt.
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
constexpr auto SIZE = 10;
auto max = 20;
// Felddefinitionen
short values[40]; // Feldgrösse durch Literal
char text[SIZE]; // Feldgrösse durch constexpr
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.
Der GNU C++-Compiler erlaubt ebenfalls die obige Definition des Feldes digits[max]. Dies ist eine Erweiterung des GNU-Compilers und im C++-Standard nicht definiert. Wenn Sie mehr darüber wissen wollen, suchen Sie im Internet nach 'C++ VLA'. VLA steht für variable length array.
Mehrdimensionale Felder werden durch mehrfache Größenangaben definiert:
DTYP fName[DIM1][DIM2]...;
Die Anzahl der Dimensionen ist nur vom verfügbaren Speicherplatz abhängig.
constexpr auto XSIZE = 10;
constexpr auto YSIZE = 50;
short table[XSIZE][YSIZE]; // 2-dimensionales Feld
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 jeweils in Klammern anzugeben.
#include <print>
const int ROW = 5;
const int COL = 2;
int one[ROW]; // 1-dimensionales Feld
int two[ROW][COL]; // 2-dimensionales Feld
int main ()
{
// Felder mit Werten belegen
// Schleife ueber Zeilen
for (auto rindex=0; rindex<ROW; rindex++)
{ // In 1-dimensionalen Feld ablegen
one[rindex] = rindex;
// Schleife ueber Spalten fuer 2-dim Feld
for (auto cindex=0; cindex<COL; cindex++)
two[rindex][cindex] = rindex*10+cindex;
}
// 2-dim Feld ausgeben mit geschachtelter for-Schleife
for (auto rindex=0; rindex<ROW; rindex++)
{
std::print("\n{}. Zeile: ",rindex);
for (auto cindex=0; cindex<COL; cindex++)
std::print("{:3}, ",two[rindex][cindex]);
}
}
0. Zeile: 0, 1,
1. Zeile: 10, 11,
2. Zeile: 20, 21,
3. Zeile: 30, 31,
4. Zeile: 40, 41,
Der Compiler überprüft nicht, ob der angegebene Index innerhalb der Feldgrenzen liegt! Und auch zur Laufzeit erfolgt keine Überprüfung des Feldzugriffs.
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.
#include <print>
int main ()
{
// Feld initialisieren
short myArray[] {10,20,30,40};
for (auto index=0; index<4; index++)
std::println("{}. Wert: {}",index,myArray[index]);
}
0. Wert: 10
1. Wert: 20
2. Wert: 30
3. Wert: 40
Alternativ kann ein Feld wie folgt initialisiert werden:
short myArray[] (10, 20, 30, 40);
Der Unterschied liegt in der Interpretation der Daten in der Klammer. Werden die Daten in einer geschweiften Klammer aufgelistet, müssen sie exakt den Datentyp des Feldes besitzen. Bei der runden Klammer müssen sich die Daten 'nur' in den Datentyp des Feldes konvertieren lassen.
short myArray[] (10, 20.0, 30.4, 40); // ok
short myArray[] {10, 20.0, 30.4, 40}; // Fehler!
Anstelle der obigen Initialisierung ist auch die 'alte' C/C++ Schreibweise unter Verwendung des Zuweisungsoperators zulässig:
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] {};
Es ist nicht möglich, per auto den Datentyp eines initialisierten Feldes zu bestimmen. Folgende Anweisung erzeugt einen Fehler beim Übersetzen:
auto myArray[] {1,2,3};
Was aber funktioniert ist Folgendes:
auto myArray = {1,2,3};
myArray ist nun aber kein Feld, sondern eine std::initializer_list. Und auf die Elemente einer std::initializer_list kann z.B. nicht mehr indiziert zugegriffen werden, sondern nur über Iteratoren (wird später im Kapitel Iteratoren und Ranges erklärt).
Bei mehrdimensionalen Feldern werden die Initialwerte für die einzelnen Dimensionen zunächst in geschweiften Klammer zusammengefasst. Diese Klammern werden dann, durch Komma getrennt, aufgelistet. Zum Schluss wird die gesamte Initialisiererliste nochmals in eine übergeordnete geschweifte Klammer gepackt.
// 2-dimensionales Feld initialisieren
short twoDim[][3]
{
{ 1, 2, 3},
{10,20,30}
};
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 (constexpr).
Felder und Zeiger
Feldadressen
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.
// Felddefinitionen inkl. Initialisierungen
char single[] {'a','b','c','d'};
short multi[][3]
{
{ 1, 2, 3},
{10,20,30}
};
...
// Zeiger definieren und jeweils Adresse des ersten
// Feldelements im Zeiger ablegen
auto pSingle = single;
// Alternative:
auto pSingle1 = &single[0];
auto pMulti = &multi[0][0];
Beachten Sie die Zuweisung in Zeile 11, die ohne den Adressoperator! Sie entspricht genau der Zuweisung in Zeile 13. Merken Sie sich folgenden Satz:
Der Name eines eindimensionalen Feldes entspricht der Anfangsadresse des Feldes. Dies gilt aber nur für eindimensionale Felder!
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.
#include <print>
// Felddefinition und Initialisierung
short myArray[] {10,20,30,40};
// Feldgröße berechnen
constexpr auto SIZE = sizeof myArray/sizeof myArray[0];
// Feldende berechnen
constexpr auto AEND = myArray+SIZE;
int main ()
{
// Zeiger definieren und auf Feldanfang setzen
// So lange die Feldwerte ausgeben, solange der
// Zeiger nicht auf das Feldende zeigt
for (auto pValues = myArray;
pValues != AEND; pValues++)
{
std::print("{},",*pValues);
}
}
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. 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.
#include <print>
// Felddefinition und Initialisierung
short myArray[] {10,20,30,40};
int main ()
{
// Zeiger definieren und auf Feldanfang setzen
auto pValues =
reinterpret_cast<unsigned char*>(myArray);
// Zeiger auf das Ende des Feldes
auto pEnd = pValues+sizeof(myArray);
// Nun alle Werte ausgeben
// Beachte: Die Initialisierung des Schleifenindizes
// pValues erfolgt hier nicht in der for-Schleife
// sondern wird vorher schon durchgefuehrt
for (; pValues != pEnd; pValues++)
{
std::print("{:d},",*pValues);
}
}
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. Wie per Zeiger auf mehrdimensionale Felder zugegriffen werden kann wird im Kapitel Operatoren new und delete beschrieben.
range-for-Schleife
Zum sequentiellen Bearbeiten von Daten in Felder, Strings oder später auch Container, kann eine spezielle for-Schleife eingesetzt werden, die sogenannte range-for-Schleife. Sie hat folgende Syntax:
for ([INIT;]DTYP var: array)
ANWEISUNG;
Nach der Auswertung des optionalen Initialisierungsausdrucks INIT durchläuft die Schleife sequentiell den Datenbereich array und legt nacheinander 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.
#include <iostream>
#include <print>
// Feldgroesse
constexpr auto SIZE=10;
// Feld definieren
int feld[SIZE];
int main()
{
// Feld mit aufsteigenden Werten fuellen
// 'int val=1' ist der Initialsierungsdruck
for (int val = 1; auto& elem: feld)
elem = val++;
// Feld ausgeben
for (auto elem: feld)
std::print("{},",elem);
std::cout << '\n';
}
1,2,3,4,5,6,7,8,9,10,
Um mittels der range-for-Schleife mehrdimensionale Felder zu durchlaufen, muss var bei den äußeren Schleifen eine Referenzvariable sein.
#include <iostream>
#include <print>
// Feldgroesse
constexpr auto ROW=2;
constexpr auto COL=5;
// Feld definieren
int feld[ROW][COL];
int main()
{
// 2-dimensionales Feld ausgeben
for (auto& zeile: feld)
{
for (auto elem: zeile)
std::print("{},",elem);
std::cout << '\n';
}
}
0,0,0,0,0,
0,0,0,0,0,
C-Strings und Felder
Die nachfolgenden Ausführungen dienen nur zur Übung im Umgang mit char-Feldern. C++ besitzt in der Standardbibliothek einen speziellen Datentyp, welcher die Bearbeitung von Strings unterstützt. Mehr dazu im Kapitel String-Objekte I.
Um eine Verwechslung mit den später beschriebenen C++-Strings zu vermeiden, werden char-Strings, die mit einer binären 0 abgeschlossen werden, nachfolgend als C-Strings bezeichnet.
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"};
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.
Alle Funktionsdeklarationen der nachfolgenden C-String-Funktionen sind in der Header-Datei cstring enthalten, welche einzubinden ist.
Um einen C-String in ein char-Feld zu kopieren wird die Funktion
char* std::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.
#include <cstring>
#include <print>
// char-Felder definieren
char acFeld[40];
int main()
{
// C-String ins char-Feld kopieren
strcpy_s(acFeld,sizeof acFeld, "Ein C-String");
std::println("acFeld: {}",acFeld);
}
acFeld: Ein C-String
Weitere Informationen zur Bearbeitung von C-Strings finden Sie auf https://en.cppreference.com unter dem Stichwort Null-terminated byte strings.
Übungen
felder_01
Es ist eine Anzahl von Zufallszahlen in einem definierten Wertebereich zu klassifizieren. Bei der Klassifizierung wird der gesamte Wertebereich in kleinere Bereiche, den sogenannte Klassen, gleichmäßig 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
Legen Sie die Anzahl der zu klassifizierenden Daten, die Anzahl der Klassen sowie den Wertebereich der Daten über Konstanten fest. Starten Sie mit 10000 Daten, 10 Klassen und einem Wertebereich von 0 bis 99.
Das Programm soll die Verteilung der Zufallszahlen auf die einzelnen Klassen ausgeben. Zusätzlich ist die Klasse mit den wenigsten und den meisten Werten ausgegeben.
Wenn der Standard-Algorithmus zur Erzeugung von Zufallszahlen korrekt funktioniert, sollten alle Klassen etwa gleich viele Werte enthalten, nämlich Anzahl der Zufallszahlen dividiert durch Anzahl der Klassen.
Wenn Sie nicht mehr wissen, wie Zufallszahlen erzeugt werden, sehen Sie nochmals bei der ersten Übung zur if-Verzweigung nach.
Statistik-Daten:
================
Klasse 1 besetzt mit 995 Werten
Klasse 2 besetzt mit 1037 Werten
Klasse 3 besetzt mit 1062 Werten
Klasse 4 besetzt mit 1011 Werten
Klasse 5 besetzt mit 1015 Werten
Klasse 6 besetzt mit 955 Werten
Klasse 7 besetzt mit 991 Werten
Klasse 8 besetzt mit 927 Werten
Klasse 9 besetzt mit 973 Werten
Klasse 10 besetzt mit 1034 Werten
Kleinster/groesser Klassenwert: 927/1062
felder_02
Schreiben Sie eine kleine 'Tabellenkalkulation'. Für die Tabelle ist ein Feld mit 10x10 Einträgen anzulegen.
Füllen Sie dieses Feld mit Zufallszahlen im Bereich 0...9.
Geben Sie die Tabelle sowie die Zeilen- und Spaltensummen aus. 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