Array of Chars #1

Informatik Klausur steht demnächst auch noch an, also mal ein bisschen vertiefen.

Vorab eine gute Seite mit Funktionsreferenzen und Beispielen, mein persönlicher Favorit:
stdlib.h, Free & Malloc
stdio.h
Alle Codebeispiele von mir gibt es zum Download.

Möchte ich irgendwas speichern, habe ich grundsätzlich zwei Möglichkeiten: Stack oder Heap.
1. Stack (statisch)
int i[10];
2. Heap    (dynamisch)
int * i = (int *)malloc(sizeof(int) * 10);

Merkmale:
1. Stack

  • läuft, brauche mir um Sachen wie Memory-Leaks keine Gedanken machen
  • Nur im aktuellen Scope gültig
  • einfach zu benutzen


2. Heap

  • Ich muss mich selbst darum kümmern, den von der Variable okkupierten Speicherplatz wieder zu löschen, wenn ich sie nicht mehr brauche.
  • Überall gültig
  • nicht mehr ganz so intuitiv benutzbar
  • Größe zur Laufzeit festlegbar


Zum Anfang noch nicht mit char, sondern einfach mit integer, ist einfacher, warum, darauf werde ich später noch zurück kommen.
Nehmen wir an, ich möchte eine Tabelle mit folgenden Werten ausgeben:













Spalte 0Spalte 1
0xA00xB0
0xA10xB1
0xA20xB2

Abstrakter dargestellt möchte ich so auf die einzelnen Elemente zugreifen können:













Spalte 0 a[0]Spalte 1 a[1]
a[0][0]a[1][0]
a[0][1]a[1][1]
a[0][2]a[1][2]

Hierbei sei a das Array.

Zwei Spalten, mehrere Zeilen, das schreit also nach einem zweidimensionalen Array.

Zugrunde liegender Quellcode der nachfolgenden Erklärung *klick*..
Realisiert wird ein 2-dimensionales-integer-Array auf dem Stack/Heap so:
1. Stack
Definition und Deklaration sind hier in einem Schritt erledigt:

byte auf_dem_stack[2][3] = {
 {0xA0, 0xA1, 0xA2}, // Spalte 0
 {0xB0, 0xB1, 0xB2} // Spalte 1
};

Danach kann sofort mit den Werten gearbeitet werden, wie im Biespiel werden in einer doppelten for-Schleife alle Werte ausgegeben.
Die Syntax ist hier auch nicht schwer, man könnte das Array einfach um eine weitere Spalte erweitern oder gar eine dritte Dimension hinzufügen, mit einer weiteren {geschweiften Klammer}.
Um irgendwas kümmern muss man sich als Programmierer auch nicht mehr - Speicherreservierung und -freigabe passieren automatisch, d.h. nachdem das Scope (deutsches Wort dafür wäre wohl Geltungsbereich) der Variablen verlassen wurde, wie bei einem Rücksprung aus einer Funktion, ist diese vernichtet, der Speicher automatisch wieder freigegeben.
Das kann aber auch problematisch sein, wenn mit dem Wert einer Variable in einer anderen Funktion weiter gearbeitet werden soll.

2. Heap
Zum Arbeiten auf dem Heap sind zwei weitere Schritte erforderlich: Erst malloc und später free.
Speicherreservierung mit malloc:

byte ** auf_dem_heap = (byte **) malloc(sizeof(byte**) * SPALTEN);
for(i = 0; i < SPALTEN; i++)
{
     auf_dem_heap[‌i] = (byte *) malloc(sizeof(byte*) * ZEILEN);
     for(j = 0; j < ZEILEN; j++)
     {
          auf_dem_heap[‌i][j] = 0xA0+(0x01*j)+(0x10*i);
          auf_dem_heap[‌i][j] = 0xA0+(0x01*j)+(0x10*i);
     }
     
}

Hier gilt es die * "Sternchen" zu beachten, auch als Dereferenzierung bezeichnet.
Eine weitere, noch abstraktere Darstellung der Tabelle dürfte verdeutlichen, warum:

















byte ** a  
byte * a[0]byte * a[1]
byte a[0][0]byte a[1][0]
byte a[0][1]byte a[1][1]
byte a[0][2]byte a[1][2]

Vereinfach bewirkt

// Achtung! "string"
byte test[] = {'1','2','3','4'};
byte * test = "1234";

die obere Zeile dasselbe wie die untere, insofern kann man sich für
*
immer ein
[]
denken - das wäre hier:
byte a[][]

















byte a[][]  
byte a[0][]byte a[1][]
byte a[0][0]byte a[1][0]
byte a[0][1]byte a[1][1]
byte a[0][2]byte a[1][2]

Folglich ist a** ein Zeiger, der auf einen Zeiger zeigt, welcher auf das erste Element des Arrays zeigt.
a**->a*->a[0].
Dies ist bei allen Datentypen so.
Ich würde empfehlen, der Übersicht wegen, noch verbleibende [] in * unzuschreiben, beim Arbeiten mit Zeigern.
Bekanntes Beispiel nebenbei:

int main(int argc, char *argv[])
int main(int argc, char ** argv)

ist gleich ein und dasselbe.

Zeiger werden nicht automatisch gelöscht, der Programmierer trägt hier die Verantwortung, deren Speicherplatz nach Gebrauch wieder freizugeben.
Im Gegensatz zum allokiertem (Informatik auf deutsch ist böse, allocated) Speicher auf dem Stack kann der Zeiger und dazugehörige Werte beliebig weiterverarbeitet werden, quer durch alle Funktionen.

Die Freigabe danach ist ähnlich aufwändig, wie die Reservierung zuvor.

for(i = 0; i < SPALTEN; i++)
{
 free(auf_dem_heap[‌i]);
}
free(auf_dem_heap);

Auf den ersten Blick sieht es viel kürzer, fast schon unkomplizierter aus, es ist ja eine for-Schleife weniger abzuarbeiten. Trotzdem liegt diesem Code hier ein häufiger Fehler für Memory-Leaks zugrunde.
Es muss nämlich zuerst der zuletzt mit malloc reservierte Speicher freigegeben werden.

Memory-Leaks können leicht gefunden und behoben werden, indem man den verdächtigen Code in einer for-Schleife laufen lässt, hierbei erhöht man langsam die Durchläufe immer um das Zehnfache und behält bei den Arbeitsspeicherverbauch des Programms im Auge. Steigt dieser exorbitant an, hat man noch wo einen Memory-Leak.

Bei mehrfacher Ausführung des Beispielprogramms 1_heap_stack_unterschied_int_2d_array.c
ist folgende Ausgabe zu beobachten:









Stack: Heap:


auf_dem_stack, 2x3 Array, Wert 28FF26, Zeiger 28FF26.
auf_dem_stack[0]--- (Adresse: 28FF26) = 28FF26
auf_dem_stack[0][0] (Adresse: 28FF26) = A0
auf_dem_stack[0][1] (Adresse: 28FF27) = A1
auf_dem_stack[0][2] (Adresse: 28FF28) = A2
auf_dem_stack[1]---     (Adresse: 28FF29) = 28FF29
auf_dem_stack[1][0] (Adresse: 28FF29) = B0
auf_dem_stack[1][1] (Adresse: 28FF2A) = B1
auf_dem_stack[1][2] (Adresse: 28FF2B) = B2

auf_dem_stack, 2x3 Array, Wert 28FF26, Zeiger 28FF26.
auf_dem_stack[0]--- (Adresse: 28FF26) = 28FF26
auf_dem_stack[0][0] (Adresse: 28FF26) = A0
auf_dem_stack[0][1] (Adresse: 28FF27) = A1
auf_dem_stack[0][2] (Adresse: 28FF28) = A2
auf_dem_stack[1]--- (Adresse: 28FF29) = 28FF29
auf_dem_stack[1][0] (Adresse: 28FF29) = B0
auf_dem_stack[1][1] (Adresse: 28FF2A) = B1
auf_dem_stack[1][2] (Adresse: 28FF2B) = B2

auf_dem_stack, 2x3 Array, Wert 28FF26, Zeiger 28FF26.
auf_dem_stack[0]--- (Adresse: 28FF26) = 28FF26
auf_dem_stack[0][0] (Adresse: 28FF26) = A0
auf_dem_stack[0][1] (Adresse: 28FF27) = A1
auf_dem_stack[0][2] (Adresse: 28FF28) = A2
auf_dem_stack[1]--- (Adresse: 28FF29) = 28FF29
auf_dem_stack[1][0] (Adresse: 28FF29) = B0
auf_dem_stack[1][1] (Adresse: 28FF2A) = B1
auf_dem_stack[1][2] (Adresse: 28FF2B) = B2




auf_dem_heap, 2x3 Array, Wert 3C2730, Zeiger 28FF20.
auf_dem_heap[0]--- (Adresse: 3C2730) = 3C2788
auf_dem_heap[0][0] (Adresse: 3C2788) = A0
auf_dem_heap[0][1] (Adresse: 3C2789) = A1
auf_dem_heap[0][2] (Adresse: 3C278A) = A2
auf_dem_heap[1]--- (Adresse: 3C2734) = 3C27A0
auf_dem_heap[1][0] (Adresse: 3C27A0) = B0
auf_dem_heap[1][1] (Adresse: 3C27A1) = B1
auf_dem_heap[1][2] (Adresse: 3C27A2) = B2

auf_dem_heap, 2x3 Array, Wert 662730, Zeiger 28FF20.
auf_dem_heap[0]--- (Adresse: 662730) = 662788
auf_dem_heap[0][0] (Adresse: 662788) = A0
auf_dem_heap[0][1] (Adresse: 662789) = A1
auf_dem_heap[0][2] (Adresse: 66278A) = A2
auf_dem_heap[1]--- (Adresse: 662734) = 6627A0
auf_dem_heap[1][0] (Adresse: 6627A0) = B0
auf_dem_heap[1][1] (Adresse: 6627A1) = B1
auf_dem_heap[1][2] (Adresse: 6627A2) = B2

auf_dem_heap, 2x3 Array, Wert 362730, Zeiger 28FF20.
auf_dem_heap[0]--- (Adresse: 362730) = 362788
auf_dem_heap[0][0] (Adresse: 362788) = A0
auf_dem_heap[0][1] (Adresse: 362789) = A1
auf_dem_heap[0][2] (Adresse: 36278A) = A2
auf_dem_heap[1]--- (Adresse: 362734) = 3627A0
auf_dem_heap[1][0] (Adresse: 3627A0) = B0
auf_dem_heap[1][1] (Adresse: 3627A1) = B1
auf_dem_heap[1][2] (Adresse: 3627A2) = B2




Die auf dem Stack initialisierten Variablen haben immer exakt gleiche, statische Adressen. Und falls als Wert nur "Müll" dasteht, also eine andere Adresse, ist es die gleiche wie im darauf folgenden erste Element des Arrays.
Das selbe gilt beim Heap, jedoch ändern sich hier die Adressen bei jedem Programmneustart, da diese jedes mal dynamisch neu zugewiesen werden.

"Array of Chars" kommt dann in Teil 2...
Hat doch etwas länger gedauert, als gedacht.

Update 02.07.2015: Teil 2