Tutoriumsinhalt

  • Pointer
  • Dynamischer Speicher
  • sizeof
  • Strukturen
  • File I/O
  • Pipes
  • Gruppenbeispiel

Pointer

  • Pointer »zeigen« auf Variablen
  • Pointer enthält die Adresse einer Variable
  • Deklaration mit "*"
    <datentyp>* name;
  • Zugriff auf die Variable durch Dereferenzieren ebenfalls mit "*"
    *name = <value>;
  • Zuweisen einer Adresse durch den Adressoperator "&"
    name = &<variable>;

Pointer und Pointee

Pointer - Praxisbeispiel

1int main() { 2 int a = 3; 3 int x = 2; 4 int* ptr = &a; 5 *ptr = 4; 6 ptr = &x; 7 *ptr = 8; return 0; }
0x1000
0x1004
0x1008
0x100C
0x1010
a   0x10003
0x1004
0x1008
0x100C
0x1010
a   0x10003
x   0x10042
0x1008
0x100C
0x1010
a   0x10003
x   0x10042
0x1008
ptr   0x100C0x1000
0x1010
a   0x10004
x   0x10042
0x1008
ptr   0x100C0x1000
0x1010
a   0x10004
x   0x10042
0x1008
ptr   0x100C0x1004
0x1010
a   0x10004
x   0x10048
0x1008
ptr   0x100C0x1004
0x1010

Verwendung von Pointern: Dynamischer Speicher

  • Wird zur Laufzeit angefordert
  • Hat keinen »Namen«, nur eine Adresse → Zugriff über Pointer
  • Angeforderter Speicher muss auch wieder freigegeben werden
  • void* malloc(size_t size) fordert Speicher der Größe size an (in Bytes angegeben)
  • void free(void* ptr) gibt angeforderten Speicher wieder frei
  • void* realloc(void* ptr, size_t new_size) vergrößert einen angeforderten Speicherbereich

Beispiel

1int main() { 2 int* mem = (int*)malloc(sizeof(int) * 2); 3 if(mem == NULL) return 0; 4 *mem = 1; 5 *(mem + 1) = 2; 6 mem[0] = 3; 7 mem[1] = 4; 8 free(mem); return 0; }
0x1000
0x1004
0x1008
0x100C
0x1010
0x1000
0x1004
0x1008
0x100C
mem   0x10100x1000
0x1000
0x1004
0x1008
0x100C
mem   0x10100x1000
0x10001
0x1004
0x1008
0x100C
mem   0x10100x1000
0x10001
0x10042
0x1008
0x100C
mem   0x10100x1000
0x10003
0x10042
0x1008
0x100C
mem   0x10100x1000
0x10003
0x10044
0x1008
0x100C
mem   0x10100x1000
0x1000
0x1004
0x1008
0x100C
mem   0x10100x1000

Call-by-Reference

  • Bei Funktionsaufruf nicht Variable, sondern Adresse übergeben
  • mehrere Rückgabewerte möglich
  • z.B. nützlich damit eine Funktion einen Wert sowie einen Fehlercode liefern kann

Beispiel

4void getHM(int minutes, int* h, int* m) { 5 *h = minutes / 60; 6 *m = minutes % 60; } 1int main() { 2 int hour = 0, minutes = 0; 3 getHourMinutes(320, &hour, &minutes); 7 printf("%d:%d\n", hour, minutes); return 0; }
0x1000
0x1004
0x1008
0x100C
0x2000
0x2004
0x2008
0x200C
hour   0x10000
minutes   0x10040
0x1008
0x100C
0x2000
0x2004
0x2008
0x200C
hour   0x10000
minutes   0x10040
0x1008
0x100C
min   0x2000320
h   0x20040x1000
m   0x20080x1004
0x200C
hour   0x10000
minutes   0x10040
0x1008
0x100C
min   0x2000320
h   0x20040x1000
m   0x20080x1004
0x200C
hour   0x10005
minutes   0x10040
0x1008
0x100C
min   0x2000320
h   0x20040x1000
m   0x20080x1004
0x200C
hour   0x10005
minutes   0x100420
0x1008
0x100C
min   0x2000320
h   0x20040x1000
m   0x20080x1004
0x200C
hour   0x10005
minutes   0x100420
0x1008
0x100C
0x2000
0x2004
0x2008
0x200C
stdout5:20

Valgrind

  • Tool zum Finden von
    • Memory Leaks
    • unitialiserten Variablen
    • Off-by-one Fehlern
    • Segmentation Faults
  • Testsystem verwendet Valgrind → Valgrind Fehler geben Punkteabzug!
  • Nur für Linux, aber auf Pluto schon installiert

Beispiel

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
  char* text = (char*)malloc(6 * sizeof(char));
  strcpy(text, "Hallo!");
  printf("%s\n", text);
  return 0;
}
Kompilieren:
gcc -Wall -g -o valgrind_test valgrind_test.c
Valgrind:
valgrind --leak-check=full ./valgrind_test

Weitere Valgrind-Beispiele

Häufige Fehler / Best Practice

  • Pointer auf NULL initialisieren
  • Pointer nach free auf NULL setzen
  • malloc/realloc auf NULL überprüfen
  • Bei realloc Hilfspointer verwenden:
    char* text = (char*)malloc(8 * sizeof(char));
    char* new_text = (char*)realloc(text, 16 * sizeof(char));
    if(new_text != NULL)
    {
      text = new_text;
    }
    else
    {
      free(text);
      printf("Out of memory!\n");
    }
    

Warum nicht einfach?

1int* x = (int*)malloc(3); 2x = (int*)realloc(x, 16); 3if(x == NULL) { 4 free(x); 5 printf("Out of memory!"); }
x   0x10000x1008
0x1004
0x1008
0x100C
0x1010
x   0x1000NULL
0x1004
0x1008
0x100C
0x1010
x   0x1000NULL
0x1004
0x1008
0x100C
0x1010
x   0x1000NULL
0x1004
0x1008
0x100C
0x1010
x   0x1000NULL
0x1004
0x1008
0x100C
0x1010
stdoutOut of memory!

Deshalb mit Hilfspointer

1int* x = (int*)malloc(3); 2int* new_x = (int*)realloc(x, 16); 3if(new_x != NULL) { x = new_x; } 4else { 5 free(x); 6 printf("Out of memory!\n"); }
x   0x10000x1008
0x1004
0x1008
0x100C
0x1010
x   0x10000x1008
new_x   0x1004NULL
0x1008
0x100C
0x1010
x   0x10000x1008
new_x   0x1004NULL
0x1008
0x100C
0x1010
x   0x10000x1008
new_x   0x1004NULL
0x1008
0x100C
0x1010
x   0x10000x1008
new_x   0x1004NULL
0x1008
0x100C
0x1010
x   0x10000x1008
new_x   0x1004NULL
0x1008
0x100C
0x1010
stdoutOut of memory!

Der sizeof Operator

  • Operator (keine Funktion), nur für Größen, die zur Kompilierzeit bekannt sind

Funktioniert

  • int charsize = sizeof(char);
  • int ptrsize = sizeof(int*);
  • int size = sizeof("Hallo");
  • int x[20];
    int len = 
      sizeof(x) / sizeof(x[0]);
    

Funktioniert nicht

  • int* x = (int*)malloc(100);
    int len = sizeof(x);
  • int x[20];
    int* y = x;
    int len = 
      sizeof(y) / sizeof(y[0]);
    
  • char* hallo = "Hallo";
    int size = sizeof(hallo);
    

Verwendung von sizeof

  • Soll bei malloc/realloc verwendet werden, da diese die Größe in Byte verlangen
  • Beispiel: Integer Array mit 10 Elementen
  • Falsch
    int* x = (int*)malloc(10);
    
  • Richtig
    int* x = (int*)malloc(10 * sizeof(int));
    

Strukturen

  • Zusammenfassung mehrer Variablen zu neuem Datentyp
  • Zugriff mit ».« bzw. »->« bei Pointern
  • #include <stdio.h>
    struct _Person_ {
      char* name;
      int age;
    };
    
    int main()
    {
      struct _Person_ petra;
      petra.name = "Petra";
      petra.age = 17;
      return 0;
    }
    

Strukturen

  • Kürzere Schreibweise mit typedef:
    [...]
    
    struct _Person_ {
      char* name;
      int age;
    };
    typedef struct _Person_ Person;
    
    [...]
    
    Person petra;
    petra.name = "Petra";
    petra.age = 17;
    
    [...]
    

Strukturen dynamisch anlegen

  • Strukturen können ebenso wie andere Daten dynamisch angelegt werden
  • [...]
    struct _Person_ {
      char* name;
      int age;
    };
    typedef struct _Person_ Person;
    
    [...]
    Person* petra = (Person*)malloc(sizeof(Person));
    petra->name = "Petra";
    petra->age = 17;
    
    [...]
    free(petra);
    

Selbstreferenzierende Strukturen

  • Struktur kann Pointer auf Objekte vom eigenem Typ enthalten
  • struct _Person_ {
      char* name;
      int age;
      struct _Person_* father;
      struct _Person_* mother;
    };
    
  • Funktioniert, da Pointer immer gleich groß sind (4 bzw. 8 Byte)
  • Für viele Datenstrukturen benötigt (z.B. Baum, Linked List)

File I/O

  • Textdateien und Binärdateien
  • Textdateien
    • enthalten nur Text
    • können mit dem normalen Editor gelesen werden
    • z.B. .txt Dateien
  • Binärdateien
    • enthalten beliebige binäre Daten
    • sind nicht mit dem normalen Editor lesbar
    • z.B. Bilder, Musik

File I/O

  • Datei öffnen
    • fopen(const char * filename, const char * mode);
    • read
    • write
    • append
  • Daten lesen
    • fread(void * dst, size_t size, size_t items, FILE * file); 
  • Daten schreiben
    •  fwrite(const void * data, size_t size, size_t items, FILE * file); 

struct in Datei speichern

  • structs können in Binärdateien gespeichert werden
  • [...]
    struct _Phone_ {
      int country_code;
      int number;
    }__attribute__((packed));
    
    [...]
    struct _Phone_ number;
    number.number = 1234567;
    number.country_code = 43;
    
    FILE* file = fopen("phone_numbers", "wb"); // wb = write binary
    fwrite(&number, sizeof(struct _Phone_), 1, file);
    fclose(file);
    

struct aus Datei lesen

  • structs können aus Binärdateien geladen werden
  • [...]
    struct _Phone_ {
      int country_code;
      int number;
    }__attribute__((packed));
    
    [...]
    struct _Phone_ number;
    
    FILE* file = fopen("phone_numbers", "rb"); // rb = read binary
    fread(&number, sizeof(struct _Phone_), 1, file);
    fclose(file);
    

Wie schaut ein int in einer Datei aus?

  • Integer hat 4 Byte
  • Jedes der Bytes kann wie eine »Ziffer« gesehen werden
  • Die Umrechung zurück in einen Integer erfolgt dann wie in jedem anderen Zahlensystem
  • Achtung Endianness: Intel ist Little-Endian, d.h. die Byte-Reihenfolge ist »verkehrt«
  • Beispiel: Integer 0x12345678 (= 305.419.896) in einer Datei: 78 56 34 12

Beispiel zur Umrechnung

  • Inhalt der Datei als char Array eingelesen: 02 01 00 00
  • Umrechnung ins Dezimalsystem:
    2560256125622563
    02010000
  • = 0x02 * 2560 + 0x01 * 2561 + 0x00 * 2562 + 0x00 * 2563
  • = 258

Geht das auch einfacher?

  • Natürlich, wir lassen den Compiler für uns arbeiten
  • Direktes Einlesen in einen Integer mit fread:
    int length;
    fread(&length, sizeof(int), 1, file);
  • Über Pointer, indem wir den Speicherinhalt neu »interpretieren« lassen:
    // buffer is the char array containing the file
    int length = *((int*)buffer);
  • Mit memcpy in einen Integer kopieren:
    // buffer is the char array containing the file
    int length;
    memcpy(&length, buffer, sizeof(int));

Dateigröße ermitteln

  • Keine C Funktion um die Dateigröße zu ermitteln
  • Folgende Vorgehensweise:
    1. Dateizeiger an das Ende der Datei setzen
    2. Abfragen, wo der Dateizeiger steht
    3. Dateizeiger wieder auf den Anfang der Datei setzen
  • Der Code dazu:
    long size;
    fseek(file, 0, SEEK_END);
    size = ftell(file);
    fseek(file, 0, SEEK_SET);
    

Pipes

Möglichkeit, die Ausgabe der einzelnen Befehle umzuleiten bzw. an andere Befehle weiterzuleiten.

  • Umleiten der Ausgabe in Datei
  • Standardeingabe von Datei lesen
  • Ausgabe als Eingabe für anderes Programm

Pipes

Ausgabe umleiten

./programm > ausgabe.txt

Eingabe umleiten

./programm < eingabe.txt

stderr umleiten

./programm 2> error.txt

Pipes

Beliebig kombinierbar

./programm < input.txt 1> output.txt 2> error.txt

Stdout als stdin eines anderen programms

./programm | tail

Returncode des letzten Programms

echo $?

Gruppenbeispiel - Tipps

  • Gut und viel testen!
  • Rückgabewerte der File I/O Funktionen prüfen!
  • Nie vergessen, bei malloc und realloc auf NULL zu prüfen!
  • String-Bibliothek Funktionen anschauen (strtok), es gibt jedoch nicht alle Funktionen die man braucht!
  • Codingstandard beachten!
  • Valgrind verwenden!

Best Practice

  • Programm sinnvoll in Funktionen einteilen
  • Aussagekräftige Variablennamen verwenden
  • Keine Variablen Recyclen
  • Variablen initialisieren
  • Lieber zu viele Klammern als zu wenig
  • Komplizierte Stellen kommentieren, aber keine unnötigen Kommentare

Viel Erfolg beim Ass2!

Abgabeschluss:

  • 7.12.2017 14:00:00 Uhr