MezData-Logo

C-Datentypen, Felder und Zeiger

C-Datentypen

Datentyp Alternativ Alternativ Bit Dezimalbereich Bemerkung
unsigned char uint8_t Byte 8 255 vorzeichenlose kleine Zahlen
char int8_t   8 -128..+127 kleine Zahlen mit Vorzeichen
unsigned int uint16_t Word 16 65535  
int int16_t   16 -32768..+32767  
unsigned long uint32_t Double Word 32 4294967295 *Hinweis: 9600UL
long int32_t   32 -2147483648..+2147483647 *Hinweis: 9600L
float     32 ±10-37 .. ±10+38  
void         leer oder unbestimmt

Die Bitbreite der Datentypen kann auf unterschiedlichen Systemen variieren, deshalb gibt es alternative Bezeichnungen, aus denen die Bitbreite hervorgeht.

sizeof() gibt die Byte-Grösse einer Variablen zurück.

Variabeln werden entweder in Registern oder im SRAM gespeichert. Durch die vielen GPR (General Purpose Register) werden bei wenigen einfachen Variablen zuerst diese verwendet, bevor auf das SRAM ausgewichen wird.

Definieren von Variablen

Hinweis: Hier wird davon ausgegangen, dass die Variabeln im SRAM gespeichert werden.

Beim Definieren von Variablen wird vom Compiler Speicherplatz entsprechend des Datentyps reserviert.

Unter dem Namen (Identifier) merkt sich der Compiler die Startadresse der Variable. Der Speicher hat dabei zunächst zufällige, alte, undefinierte Werte. Durch Initalisierung können Startwerte vorgegeben werden.

char zeichen = 3;
int zahl = 0x1234;

Bei zahl wird Little Endian Speicherung verwendet:
Das niederwertigste Byte zuerst speichern.

char feld[] = {1,2,3};
// feld    hat den Wert 0x0063
// feld[0] hat den Wert 1
Compilersicht Adresse Inhalt
Name: zeichen
Typ: char
sizeof(zeichen): 1
0x0060 3
Name: zahl
Typ: int
sizeof(zahl): 2
0x0061 0x34
0x0062 0x12

Name: feld
Typ: char[3]
sizeof(feld): 3
sizeof(feld[0]): 1

0x0063 1
0x0064 2
0x0065 3

Felder, Arrays unter der Lupe

char text[5]="Bla";

Für das Textfeld werden 5 Byte reserviert. Initialisiert wird es mit einem String, der nur 4 Byte lang ist: Drei Zeichen und eine 0 als Terminalsymbol um das Ende zu markieren. Byte 5 bleibt somit undefiniert.

char feld2[2][3] = {{1,2,3},{4,5,6}};
// feld2       hat den Wert 0x006b
// feld2[1][0] hat den Wert 4

Indexberechnungen

Der Compiler ermittelt den Inhalt von Feld-Elementen mit Rechnungen wie diesen:

text[n] = inhalt_von(text + n*sizeof(text[0]))

feld2[a][b] = inhalt_von(feld2+ a*sizeof(feld2[0])+b*sizeof(feld2[0][0]))

 

Die inhalt_von-Funktion ist der *-Operator siehe Zeiger

Compilersicht Adresse Inhalt
Name: text
Typ: char[5]
sizeof(text): 5
sizeof(text[0]): 1
0x0066 'B'
0x0067 'l'
0x0068 'a'
0x0069 '\0' = 0
0x006A ? = undefiniert
Name: feld2
Typ: char[2][3]
sizeof(feld2): 6
sizeof(feld2[0]): 3
sizeof(feld2[0][0]): 1
0x006B 1
0x006C 2
0x006D 3
0x006E 4
0x006F 5
0x0070 6

Zeiger, Pointer

Unter einem Zeiger (engl. Pointer) versteht man den Verweis, die Referenz auf eine Speicheradresse.

Ein Zeiger ist eine Variable, die sich eine Speicheradresse merken kann.

char *zeichen_ptr; // Zeiger auf char definieren
zeichen_ptr = &zeichen; // Adresse von zeichen zuweisen

*-Operator dereferenziert den Zeiger, greift auf Wert der referenzierten Speicheradresse zu.

&-Operator gibt die Adresse der Variablen zurück

// &zeichen    ergibt 0x0060
// zeichen_ptr ergibt 0x0060
// *zeichen_ptr ergibt 3
int zahlen[] = {1,2};
int *zahlen_ptr = &zahlen[1];
Compilersicht Adresse Inhalt
Name: zeichen_ptr
Typ: char*
sizeof(zeichen_ptr): 2
0x0071 0x60
0x0072 0x00
Name: zahlen
Typ: int[2]
sizeof(zahlen): 4
sizeof(zahlen[0]): 2
0x0073 1
0x0074 0
0x0075 2
0x0076 0
Name: zahlen_ptr
Typ: int*
sizeof(zahlen_ptr): 2
0x0077 0x75
0x0078 0

Aufgaben

Der Speicher beginnt bei 0x0060. Ermitteln Sie den Speicherinhalt (Tabelle) und die Ausgaben (mit cout) bei folgendem Code.

unsigned int muriel = 0x0815;
int *sister_ptr = &muriel;
cout << *sister_ptr; // welchen Wert hat *sister_ptr
*sister_ptr = 0x6666;
cout << muriel;
unsigned char tom = 128;
char *tom_prt = &tom;
cout << *tom_ptr;
char biene[3][2] = {{1,2},{3,4}};
cout << sizeof(biene);
char *bee_ptr = &biene[2][0];
cout << bee_ptr;
*bee_ptr = 7;
*(++bee_ptr) = 8;
cout << bee_ptr;
Lösung..
Compilersicht Adresse Inhalt
Wert
neu
Name: muriel
Typ: uint
sizeof(muriel): 2
0x0060 0x15 0x66
0x0061 0x08 0x66
Name: sister_ptr
Typ: int*
sizeof(sister_ptr): 2
sizeof(int*): 2
0x0062 0x60  
0x0063 0x00  
Name: tom
Typ: uchar
0x0064 128  
Name: tom_ptr
Typ: char*
0x0065 0x64  
0x0066 0x00  
Name: biene
Typ: char[3][2]
sizeof(biene): 6
sizeof(biene[0]): 2
sizeof(biene[0][0]): 1
0x0067 1  
0x0068 2  
0x0069 3  
0x006a 4  
0x006b ? 7
0x006c ? 8
Name: bee_ptr
Typ: char*
0x006d 0x6b 0x6c
0x006e 0x00 0x00

Kurztest

Welche Funktion hat sizeof(..)?

Beschreiben Sie, was bei der Definition einer Variablen geschieht?

Welche Funktion haben der *- und &- Operator bei Variablen?

Erklären Sie die Begriffe Zeiger, Pointer.

Erklären Sie diese Abkürzungen und ihre Funktion ROM, RAM, SRAM, DRAM, EEPROM.

Der freie RAM-Speicher beginnt bei Adresse 0x80. Erstellen Sie eine Tabelle [Compilersicht | Adresse | Inhalt] nachdem dieser Code durchlaufen wurde:

char a = 127+1;
int  b = 1024;
char c[4] = {2,3,4};
char *d = c[1];

Mit PROGMEM Daten im Flash-Programmspeicher speichern und lesen

Bei AVR-µC sind Daten- und Programmspeicher getrennt (Havard-Architektur) dies bereitet mit dem GCC einige Probleme, da dieser für die Von-Neumann-Architektur konstuiert wurde, bei dem von einem durchgehenden Speicherbereich ausgegangen wird. Bei zwei getrennten Speichern kommen Adressen doppelt vor, auch der Zugriff auf Daten im Flash funktioniert anders als beim SRAM. Mit PROGMEM können konstante Daten im Flash-Speicher untergebracht werden.
[mikrocontroller.net/articles/AVR-GCC-Tutorial] [<avr/pgmspace.h>: Program Space Utilities].

#include <avr/pgmspace.h>

const int zahl PROGMEM = 0x1234;
const char text[] PROGMEM = "Bo";
Compilersicht Adresse Inhalt
Name: zahl
Typ: int
sizeof(zahl): 2
0x0200 0x34
0x0201 0x12
Name: text
Typ: char[3]
sizeof(text): 3
0x0202 'B'
0x0203 'o'
0x0204 '\0'

Gelesen werden die Daten mit besonderen Funktionen, denen die Speicheradresse der Daten mittels des &-Operators übergeben wird.

// pgm_read_word(&zahl) ergibt 0x1234
// pgm_read_byte(&text[1]) ergibt 'o'
Ergebnis Aufruf Beschreibung
8 Bit pgm_read_byte(&name) Byte aus Programm-Flash
16 Bit pgm_read_word(&name) Wort aus Programm-Flash
32 Bit pgm_read_dword(&name) DWort aus Programm-Flash

Texte im Flash speichern und lesen

const char text1[] PROGMEM = "Danke OM";
const char text2[] PROGMEM = "Logoroe";
const char text3[] PROGMEM = "Wunderbar";
const char *const texte[] PROGMEM = { // Zeiger auf Texte in Zeigerfeld speichern
   text1,text2,text3
};

char anzahlTexte = sizeof(texte)/sizeof(texte[0]);

void ausgebenLCD(char i){ // den i-ten Text auf LCD ausgeben
  char buffer[20];        // Puffer in SRAM
  const char *texte_ptr = (const char*) pgm_read_word (&texte[i]); // Zeiger auf text
  strcpy_P(buffer, texte_ptr ); // Text in SRAM kopieren
lcd_string(buffer); // ausgeben }

Bei verschieden langen Strings wäre ein einfaches 2D-Array verschwendeter Platz. Deswegen ist ein Zeiger-Array auf Texte die sparsamere Lösung. Weil der GCC nicht mit dem getrennten Flash-Speicherbereich klar kommt müssen einige Klimmzüge gemacht werden. Die Daten werden dabei in den SRAM kopiert.