Notes 4

Sie können diese Seite ausdrucken oder mit einem PDF-Drucker in ein PDF umwandeln, um Ihre eigenen Notizen hinzuzufügen.

Willkommen!

  • In den vergangenen Wochen haben wir darüber gesprochen, dass Bilder aus kleineren Bausteinen bestehen, die Pixel genannt werden.
  • Heute werden wir uns näher mit den Nullen und Einsen beschäftigen, aus denen diese Bilder bestehen. Insbesondere werden wir uns mit den grundlegenden Bausteinen befassen, aus denen Dateien, einschließlich Bilder, bestehen.
  • Außerdem werden wir besprechen, wie man auf die zugrunde liegenden Daten im Computerspeicher zugreifen kann.

Pixel Art

  • Pixel sind Quadrate, einzelne Farbpunkte, die in einem Raster von oben nach unten und von links nach rechts angeordnet sind.

  • Sie können sich ein Bild wie eine Karte aus Bits vorstellen, bei der Nullen für Schwarz und Einsen für Weiß stehen.

    Nullen und Einsen werden in einen schwarz-weißen Smiley umgewandelt
    smiley

  • RGB, oder rot, grün, blau, sind Zahlen, die den Anteil jeder dieser Farben darstellen. In Adobe Photoshop können Sie diese Einstellungen wie folgt sehen:

    Ein Photoshop-Panel mit RGB-Werten und hexidezimaler Eingabe
    hex in photoshop

    Beachten Sie, wie die Menge an Rot, Blau und Grün die ausgewählte Farbe verändert.

  • Anhand des obigen Bildes können Sie sehen, dass die Farbe nicht nur in drei Werten dargestellt wird. Am unteren Rand des Fensters befindet sich ein spezieller Wert, der sich aus Zahlen und Buchstaben zusammensetzt. Der Wert “255” wird als “FF” dargestellt. Warum ist das so?

Hexadezimal

  • Das Hexadezimalsystem ist ein Stellenwertsystem mit 16 Zählwerten. Sie sind wie folgt:

    0 1 2 3 4 5 6 7 8 9 a b c d e f

    Beachten Sie, dass “F” für “15” steht.

  • Hexadezimal ist auch als Base-16 bekannt.

  • Beim Zählen in Hexadezimal ist jede Spalte eine Potenz von 16.

  • Die Zahl “0” wird als “00” dargestellt.

  • Die Zahl “1” wird als “01” dargestellt.

  • Die Zahl “9” wird durch “09” dargestellt.

  • Die Zahl “10” wird dargestellt als “0A”.

  • Die Zahl “15” wird als “0F” dargestellt.

  • Die Zahl “16” wird als “10” dargestellt.

  • Die Zahl “255” wird als “FF” dargestellt, denn 16 x 15 (oder “F”) ist 240. Addiere weitere 15 und du erhältst 255. Dies ist die höchste Zahl, die man mit einem zweistelligen Hexadezimalsystem zählen kann.

  • Hexadezimal ist nützlich, weil es mit weniger Ziffern dargestellt werden kann. Mit dem Hexadezimalsystem können wir Informationen prägnanter darstellen.

Speicher

  • Vielleicht erinnern Sie sich an unsere Darstellung der Speicherblöcke in den vergangenen Wochen. Wenn Sie jeden dieser Speicherblöcke mit einer hexadezimalen Nummerierung versehen, können Sie sich diese wie folgt vorstellen:

    Speicherblöcke in Hex nummeriert
    Speicher hex

  • Sie können sich vorstellen, dass es Verwirrung darüber geben kann, ob der obige “10”-Block eine Stelle im Speicher oder den Wert “10” darstellt. Dementsprechend werden die Positionen im Speicher oft als hexadezimale Zahlen mit dem Präfix “0x” dargestellt:

    Speicherblöcke, die in Hexadezimalzahlen mit 0x nummeriert sind
    0x

  • Geben Sie in Ihrem Terminalfenster “code addresses.c” ein und schreiben Sie Ihren Code wie folgt:

#include <stdio.h>

int main(void)
{
    int n = 50;
    printf("%i\n", n);
}

Beachten Sie, dass “n” nun im Speicher mit dem Wert “50” gespeichert ist.

  • Wie dieses Programm diesen Wert speichert, können Sie sich wie folgt vorstellen:

    der Wert 50 in einem Speicherplatz mit hex gespeichert
    hex

  • Die Sprache “C” verfügt über zwei Operatoren, die sich auf den Speicher-Ort, also die Adresse von Variablen beziehen:

    • & liefert die Adresse von etwas, das im Speicher gespeichert ist.
    • * weist den Compiler an, eine Stelle im Speicher aufzusuchen.
  • Wir können unseren Code wie folgt abändern:

#include <stdio.h>

int main(void)
{
    int n = 50;
    printf("%p\n", &n);
}

Beachten Sie das “%p”, mit dem wir die Adresse einer Speicherstelle anzeigen können. &n kann wörtlich übersetzt werden als “die Adresse von n”. Die Ausführung dieses Codes gibt eine Speicheradresse aus, die mit 0x beginnt.

Pointer

  • Ein Pointer (deutsch: Zeiger) ist eine Variable, die die Adresse eines bestimmten Wertes enthält. Kurz gesagt, ein Pointer repräsentiert eine bestimmte Adresse im Speicher Ihres Computers.

  • Betrachten Sie den folgenden Code:

    int n = 50;
    
    int *p = &n;

    Beachten Sie, dass “p” ein Pointer ist, der die Adresse einer Ganzzahl “n” enthält.

  • Ändern Sie Ihren Code wie folgt:

#include <stdio.h>

int main(void)
{
    int n = 50;
    int *p = &n;
    printf("%p\n", p);
}

Beachten Sie, dass dieser Code die gleiche Wirkung hat wie unser vorheriger Code. Wir haben einfach unser neues Wissen über die Operatoren & und * genutzt.

  • Um die Verwendung des *-Operators zu veranschaulichen, betrachten Sie das Folgende:
#include <stdio.h>

int main(void)
{
    int n = 50;
    int *p = &n;
    printf("%i\n", *p);
}

Beachten Sie, dass die “printf”-Zeile die Ganzzahl an der Stelle “p” ausgibt. int *p erzeugt einen Pointer, der die Aufgabe hat, die Speicheradresse einer Ganzzahl zu speichern.

  • Sie können sich unseren Code wie folgt vorstellen:

    Gleicher Wert von 50 an einer Speicherstelle mit einem an anderer Stelle gespeicherten Pointerwert
    pointer

    Beachten Sie, dass der Pointer recht groß erscheint. In der Tat wird ein Pointer normalerweise als 8-Byte-Wert gespeichert. p speichert die Adresse von “50”.

  • Sie können sich einen Pointer als einen Wert an einer Adresse im Speicher vorstellen, der auf eine andere Adresse zeigt:

    Ein Pointer als Pfeil, der von einer Speicherstelle zu einer anderen zeigt
    pointer

Strings

  • Da wir nun ein mentales Modell für Pointer haben, können wir eine Ebene der Vereinfachung zurücknehmen, die zuvor in diesem Kurs angeboten wurde.

  • Erinnern Sie sich, dass eine Zeichenkette einfach eine Reihe von Zeichen ist. Zum Beispiel kann String s = "HI!" wie folgt dargestellt werden:

    Die Zeichenkette HI mit einem im Speicher abgelegten Ausrufezeichen
    hi

  • Aber was ist das “s” wirklich? Wo wird das “s” im Speicher abgelegt? Wie Sie sich vorstellen können, muss “s” irgendwo gespeichert werden. Sie können sich die Beziehung von “s” zur Zeichenkette wie folgt vorstellen:

    Gleiche Zeichenkette HI mit einem Pointer, der auf sie zeigt
    hi pointer

    Beachten Sie, dass ein Pointer namens “s” dem Compiler mitteilt, wo sich das erste Byte der Zeichenkette im Speicher befindet.

  • Ändern Sie Ihren Code wie folgt:

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    string s = "HI!";
    printf("%p\n", s);
    printf("%p\n", &s[0]);
    printf("%p\n", &s[1]);
    printf("%p\n", &s[2]);
    printf("%p\n", &s[3]);
}

Beachten Sie, dass oben die Speicherplätze jedes Zeichens in der Zeichenkette “s” ausgegeben werden. Das Symbol “&” wird verwendet, um die Adresse jedes Elements der Zeichenkette anzuzeigen. Wenn Sie diesen Code ausführen, sehen Sie, dass die Elemente 0, 1, 2 und 3 im Speicher nebeneinander liegen.

  • Ebenso können Sie Ihren Code wie folgt ändern:
#include <stdio.h>

int main(void)
{
    char *s = "HI!";
    printf("%s\n", s);
}

Beachten Sie, dass dieser Code die Zeichenkette darstellt, die an der Stelle von s beginnt. Dieser Code entfernt effektiv die Stützräder des string Datentyps, der von cs50.h angeboten wird. Dies ist roher C-Code, ohne das Gerüst der CS50-Bibliothek.

  • Sie können sich vorstellen, wie ein String als Datentyp erstellt wird.
  • Letzte Woche haben wir gelernt, wie man mit typedef einen eigenen Datentyp als struct erstellt.
  • Die CS50-Bibliothek enthält folgenden Code: typedef char *string
  • Dadurch kann man bei der Verwendung der cs50-Bibliothek einen eigenen Datentyp namens string verwenden.

Pointerarithmetik

  • Sie können Ihren vorherigen Code wie folgt ändern, um das Gleiche in einer längeren Form zu erreichen, also den String auszugeben:
#include <stdio.h>

int main(void)
{
    char *s = "HI!";
    printf("%c\n", s[0]);
    printf("%c\n", s[1]);
    printf("%c\n", s[2]);
}

Beachten Sie, dass wir jedes Zeichen an der Stelle von “s” drucken.

  • Außerdem können Sie Ihren Code wie folgt ändern:
#include <stdio.h>

int main(void)
{
    char *s = "HI!";
    printf("%c\n", *s);
    printf("%c\n", *(s + 1));
    printf("%c\n", *(s + 2));
}

Beachten Sie, dass das erste Zeichen an der Stelle von “s” gedruckt wird. Dann wird das Zeichen an der Stelle s + 1 gedruckt, und so weiter.

String-Vergleich

  • Eine Zeichenkette ist einfach eine Anordnung von Zeichen, die durch ihr erstes Byte identifiziert wird.
  • Zu Beginn des Kurses haben wir uns mit dem Vergleich von ganzen Zahlen beschäftigt. Wir könnten dies in Code darstellen, indem wir “code compare.c” in das Terminalfenster eingeben und den Code wie folgt schreiben:
#include <cs50.h>
#include <stdio.h>

int main(void)
{
    // Zwei Ganzzahlen holen
    int i = get_int("i: ");
    int j = get_int("j: ");

    // Ganzzahlen vergleichen
    if (i == j)
    {
        printf("Same\n");
    }
    sonst
    {
        printf("Anders\n");
    }
}

Beachten Sie, dass dieser Code zwei Ganzzahlen vom Benutzer entgegennimmt und sie vergleicht.

  • Im Falle von Zeichenketten kann man jedoch nicht zwei Zeichenketten mit dem Operator “=” vergleichen.
  • Wenn man mit dem Operator == versucht, Zeichenketten zu vergleichen, wird versucht, die Speicherstellen der Zeichenketten zu vergleichen und nicht die darin enthaltenen Zeichen. Wir müssen daher strcmp verwenden.
  • Um dies zu veranschaulichen, ändern Sie Ihren Code wie folgt:
#include <cs50.h>
#include <stdio.h>

int main(void)
{
    // Zwei Zeichenketten holen
    char *s = get_string("s: ");
    char *t = get_string("t: ");

    // Adressen der Zeichenketten vergleichen
    if (s == t)
    {
        printf("Same\n");
    }
    sonst
    {
        printf("Anders\n");
    }
}

Es fällt auf, dass die Eingabe von HI! für beide Zeichenketten immer noch die Ausgabe von Different ergibt.

  • Warum sind diese Zeichenfolgen scheinbar unterschiedlich? Sie können die folgende Grafik verwenden, um sich das zu verdeutlichen:

    zwei getrennt gespeicherte Zeichenfolgen
    zwei Zeichenfolgen

  • Daher prüft der obige Code für compare.c, ob die Speicheradressen unterschiedlich sind, nicht die Zeichenketten selbst.

  • Mit strcmp können wir unseren Code korrigieren:

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

int main(void)
{
    // Zwei Zeichenketten holen
    char *s = get_string("s: ");
    char *t = get_string("t: ");

    // Zeichenketten vergleichen
    if (strcmp(s, t) == 0)
    {
        printf("Same\n");
    }
    sonst
    {
        printf("Anders\n");
    }
}

Beachten Sie, dass strcmp 0 zurückgibt, wenn die Zeichenketten gleich sind.

  • Um weiter zu veranschaulichen, wie diese beiden Zeichenfolgen an zwei Orten leben, ändern Sie Ihren Code wie folgt:
#include <cs50.h>
#include <stdio.h>

int main(void)
{
    // Zwei Zeichenketten holen
    char *s = get_string("s: ");
    char *t = get_string("t: ");

    // Zeichenketten drucken
    printf("%s\n", s);
    printf("%s\n", t);
}

Beachten Sie, dass wir jetzt zwei getrennte Zeichenketten haben, die wahrscheinlich an zwei verschiedenen Orten gespeichert sind.

  • Mit einer kleinen Änderung können Sie die Positionen dieser beiden gespeicherten Zeichenfolgen sehen:
#include <cs50.h>
#include <stdio.h>

int main(void)
{
    // Zwei Zeichenketten holen
    char *s = get_string("s: ");
    char *t = get_string("t: ");

    // Adressen der Zeichenketten ausgeben
    printf("%p\n", s);
    printf("%p\n", t);
}

Beachten Sie, dass das “%s” in der Druckanweisung in “%p” geändert wurde.

Kopieren (malloc, free)

  • In der Programmierung ist es häufig erforderlich, eine Zeichenkette in eine andere zu kopieren.
  • Geben Sie in Ihrem Terminalfenster “code copy.c” ein und schreiben Sie den folgenden Code:
#include <cs50.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    // Abrufen einer Zeichenkette
    string s = get_string("s: ");

    // Kopieren der Adresse der Zeichenkette
    string t = s;

    // Ersten Buchstaben in der Zeichenkette großschreiben
    t[0] = toupper(t[0]);

    // String zweimal drucken
    printf("s: %s\n", s);
    printf("t: %s\n", t);
}

Beachten Sie, dass string t = s die Adresse von s nach t kopiert. Dies führt nicht zu dem, was wir wollen. Die Zeichenkette wird nicht kopiert - nur die Adresse.

  • Sie können sich den obigen Code wie folgt vorstellen:

    zwei Pointer, die auf denselben Speicherplatz mit einer Zeichenkette zeigen
    zwei Zeichenketten

    Beachten Sie, dass “s” und “t” immer noch auf dieselben Speicherblöcke verweisen. Es handelt sich nicht um eine echte Kopie einer Zeichenkette. Stattdessen handelt es sich um zwei Pointer, die auf dieselbe Zeichenkette zeigen.

  • Bevor wir dieses Problem angehen, müssen wir sicherstellen, dass wir in unserem Code keinen Segmentierungsfehler erleben, bei dem wir versuchen, string s nach string t zu kopieren, obwohl string t nicht existiert. Wir können die Funktion strlen wie folgt verwenden, um dabei zu helfen:

    #include <cs50.h>
    #include <ctype.h>
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        // Abrufen einer Zeichenkette
        string s = get_string("s: ");
    
        // Kopieren der Adresse der Zeichenkette
        string t = s;
    
        // Großschreibung des ersten Buchstabens in der Zeichenkette
        if (strlen(t) > 0)
        {
            t[0] = toupper(t[0]);
        }
    
        // String zweimal drucken
        printf("s: %s\n", s);
        printf("t: %s\n", t);
    }

    Beachten Sie, dass strlen verwendet wird, um sicherzustellen, dass string t existiert. Ist dies nicht der Fall, wird nichts kopiert.

  • Um eine richtige Kopie der Zeichenkette erstellen zu können, müssen wir zwei neue Bausteine einführen. Erstens: malloc erlaubt es Ihnen, einen Block im Speicher mit einer bestimmten Größe anzufordern und dort Daten abzulegen. Zweitens können Sie mit free dem Compiler mitteilen, dass er den zuvor zugewiesenen Speicherblock freigeben soll.

  • Wir können unseren Code wie folgt ändern, um eine richtige Kopie unserer Zeichenkette zu erstellen:

  #include <cs50.h>
  #include <ctype.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  
  int main(void)
  {
      // Abrufen einer Zeichenkette
      char *s = get_string("s: ");
  
      // Speicher für eine weitere Zeichenkette zuweisen
      char *t = malloc(strlen(s) + 1);
  
      // Zeichenkette in den Speicher kopieren, einschließlich '\0'
      for (int i = 0; i <= strlen(s); i++)
      {
          t[i] = s[i];
      }
  
      // Kopie großschreiben
      t[0] = toupper(t[0]);
  
      // Zeichenketten drucken
      printf("s: %s\n", s);
      printf("t: %s\n", t);
  }

Beachten Sie, dass malloc(strlen(s) + 1) einen Speicherblock erzeugt, der der Länge der Zeichenkette s plus eins entspricht. Dies ermöglicht die Aufnahme des Null-Terminators \0 in unsere endgültige, kopierte Zeichenkette. Dann geht die “for”-Schleife durch die Zeichenkette “s” und weist jeden Wert der gleichen Stelle in der Zeichenkette “t” zu.

  • Es stellt sich heraus, dass unser Code eine Ineffizienz aufweist. Ändern Sie Ihren Code wie folgt:

    #include <cs50.h>
    #include <ctype.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(void)
    {
        // Abrufen einer Zeichenkette
        char *s = get_string("s: ");
    
        // Speicher für eine weitere Zeichenkette zuweisen
        char *t = malloc(strlen(s) + 1);
    
        // Zeichenfolge in den Speicher kopieren, einschließlich '\0'
        for (int i = 0, n = strlen(s); i <= n; i++)
        {
            t[i] = s[i];
        }
    
        // Kopie großschreiben
        t[0] = toupper(t[0]);
    
        // Zeichenketten drucken
        printf("s: %s\n", s);
        printf("t: %s\n", t);
    }

    Beachten Sie, dass “n = strlen(s)” jetzt auf der linken Seite der “for”-Schleife definiert ist. Es ist ratsam, nicht benötigte Funktionen nicht in der mittleren Bedingung der “for”-Schleife aufzurufen, da diese immer wieder durchlaufen wird. Wenn Sie n = strlen(s) auf die linke Seite verschieben, wird die Funktion strlen nur einmal ausgeführt.

  • Die Sprache C hat eine eingebaute Funktion zum Kopieren von Zeichenketten namens strcpy. Sie kann wie folgt implementiert werden:

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

  int main(void)
  {
      // Abrufen einer Zeichenkette
      char *s = get_string("s: ");

      // Speicher für eine weitere Zeichenkette zuweisen
      char *t = malloc(strlen(s) + 1);

      // Zeichenkette in den Speicher kopieren
      strcpy(t, s);

      // Kopie großschreiben
      t[0] = toupper(t[0]);

      // Zeichenketten drucken
      printf("s: %s\n", s);
      printf("t: %s\n", t);
  }

Beachten Sie, dass strcpy die gleiche Arbeit leistet wie unsere for-Schleife zuvor.

  • Sowohl get_string als auch malloc geben NULL zurück, einen speziellen Wert im Speicher, für den Fall, dass etwas schief geht. Sie können Code schreiben, der auf diese NULL-Bedingung wie folgt prüft:
  #include <cs50.h>
  #include <ctype.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>

  int main(void)
  {
      // Abrufen einer Zeichenkette
      char *s = get_string("s: ");
      if (s == NULL)
      {
          return 1;
      }

      // Speicher für eine weitere Zeichenkette zuweisen
      char *t = malloc(strlen(s) + 1);
      if (t == NULL)
      {
          return 1;
      }

      // Zeichenkette in den Speicher kopieren
      strcpy(t, s);

      // Kopie großschreiben
      if (strlen(t) > 0)
      {
          t[0] = toupper(t[0]);
      }

      // Zeichenketten drucken
      printf("s: %s\n", s);
      printf("t: %s\n", t);

      // Speicher freigeben
      free(t);
      return 0;
  }

Beachten Sie, dass NULL zurückgegeben wird, wenn die erhaltene Zeichenkette die Länge 0 hat oder malloc fehlschlägt. Beachten Sie auch, dass free den Computer wissen lässt, dass Sie mit diesem Speicherblock, den Sie mit malloc erstellt haben, fertig sind.

malloc und Valgrind

  • Valgrind ist ein Werkzeug, das überprüfen kann, ob es speicherbezogene Probleme in Ihren Programmen gibt, bei denen Sie malloc verwendet haben. Insbesondere prüft es, ob Sie den gesamten zugewiesenen Speicher “freigeben”.
  • Betrachten Sie den folgenden Code für memory.c:
  #include <stdio.h>
  #include <stdlib.h>

  int main(void)
  {
      int *x = malloc(3 * sizeof(int));
      x[1] = 72;
      x[2] = 73;
      x[3] = 33;
  }

Beachten Sie, dass die Ausführung dieses Programms keine Fehler verursacht. Während malloc verwendet wird, um genügend Speicher für ein Array zuzuweisen, kümmert sich der Code nicht darum, den zugewiesenen Speicher wieder freizugeben.

  • Wenn Sie make memory gefolgt von valgrind ./memory eingeben, erhalten Sie einen Bericht von valgrind, der aufzeigt, wo durch Ihr Programm Speicher verloren gegangen ist. Ein Fehler, den valgrind aufdeckt, ist, dass wir versucht haben, den Wert 33 an der 4. Position des Arrays zuzuweisen, obwohl wir nur ein Array der Größe 3 zugewiesen haben. Ein weiterer Fehler ist, dass wir x nie freigegeben haben.

  • Sie können Ihren Code wie folgt ändern:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int *x = malloc(3 * sizeof(int));
        x[0] = 72;
        x[1] = 73;
        x[2] = 33;
        free(x);
    }

    Beachten Sie, dass die erneute Ausführung von valgrind nun keine Speicherlecks mehr aufweist.

Garbage Values („Müllwerte“)

  • Wenn Sie den Compiler nach einem Speicherblock fragen, gibt es keine Garantie, dass dieser Speicher leer ist.
  • Es ist sehr gut möglich, dass der von Ihnen zugewiesene Speicher bereits vorher vom Computer verwendet wurde. Dementsprechend können Sie Junk- oder Garbage-Werte sehen. Dies ist darauf zurückzuführen, dass Sie einen Speicherblock erhalten haben, ihn aber nicht initialisiert haben. Betrachten Sie zum Beispiel den folgenden Code für garbage.c:
  #include <stdio.h>
  
  int main(void)
  {
      int scores[1024];
      for (int i = 0; i < 1024; i++)
      {
          printf("%i\n", punkte[i]);
      }
  }

Beachten Sie, dass die Ausführung dieses Codes 1024 Speicherplätze für Ihr Array zuweist, aber die for-Schleife wird wahrscheinlich zeigen, dass nicht alle Werte darin 0 sind. Es ist immer die beste Praxis, sich des Potentials für Garbage-Werte bewusst zu sein, wenn Sie Speicherblöcke nicht auf einen anderen Wert wie Null oder anders initialisieren.

Swap

  • In der realen Welt besteht eine häufige Notwendigkeit in der Programmierung darin, zwei Werte zu vertauschen. Natürlich ist es schwierig, zwei Variablen ohne einen temporären Speicherplatz zu vertauschen. In der Praxis können Sie code swap.c eingeben und den folgenden Code schreiben, um dies in Aktion zu sehen:
  #include <stdio.h>

  void swap(int a, int b);

  int main(void)
  {
      int x = 1;
      int y = 2;

      printf("x ist %i, y ist %i\n", x, y);
      swap(x, y);
      printf("x ist %i, y ist %i\n", x, y);
  }

  void swap(int a, int b)
  {
      int tmp = a;
      a = b;
      b = tmp;
  }

Beachten Sie, dass dieser Code zwar ausgeführt wird, aber nicht funktioniert. Die Werte werden, auch nachdem sie an die Funktion “swap” gesendet wurden, nicht getauscht. Warum?

  • Wenn Sie Werte an eine Funktion übergeben, stellen Sie nur Kopien zur Verfügung. In den vergangenen Wochen haben wir bereits das Konzept des Scope besprochen. Die Werte von x und y, die in den geschweiften {}Klammern der Funktion main erzeugt werden, haben nur den Geltungsbereich der Funktion main. Betrachten Sie das folgende Bild:

    ein Rechteck mit Maschinencode an der Spitze, gefolgt von den Globals Heap und Stack
    stack and heap

    Beachten Sie, dass globale Variablen, die wir in diesem Kurs nicht verwendet haben, an einem bestimmten Ort im Speicher liegen. Verschiedene Funktionen und die dort genutzten Variablen werden in einem anderen Bereich des Speichers abgelegt, dem “Stack”.

  • Betrachten Sie nun das folgende Bild:

    ein Rechteck mit Hauptfunktion unten und Tauschfunktion direkt darüber
    frames

    Beachten Sie, dass main und swap zwei getrennte Frames oder Speicherbereiche haben. Daher können wir die Werte nicht einfach von einer Funktion an die andere übergeben, um sie zu ändern.

  • Ändern Sie Ihren Code wie folgt:

  #include <stdio.h>
  
  void swap(int *a, int *b);
  
  int main(void)
  {
      int x = 1;
      int y = 2;
  
      printf("x ist %i, y ist %i\n", x, y);
      swap(&x, &y);
      printf("x ist %i, y ist %i\n", x, y);
  }
  
  void swap(int *a, int *b)
  {
      int tmp = *a;
      *a = *b;
      *b = tmp;
  }

Beachten Sie, dass Variablen nicht als Wert, sondern als Referenz übergeben werden. Das heißt, die Adressen von a und b werden an die Funktion übergeben. Daher erfährt die Funktion swap wissen, wo sie Änderungen an den tatsächlichen a und b von der Hauptfunktion vornehmen muss.

  • Sie können sich das folgendermaßen vorstellen:

    a und b, die in der Hauptfunktion gespeichert sind, werden per Referenz an die Swap-Funktion übergeben
    swap by reference

Überlauf

  • Ein Heap Overflow liegt vor, wenn der Heap überläuft und Bereiche des Speichers berührt werden, die nicht dafür vorgesehen sind.
  • Von einem Stack Overflow spricht man, wenn zu viele Funktionen aufgerufen werden und dadurch der verfügbare Speicher überläuft.
  • Weiterhin gibt es den Pufferüberlauf, wenn man einen Buffer vorbereitet hat, der zu klein ist für das was eingelesen werden soll.

scanf

  • Für CS50 wurden Funktionen wie get_int entwickelt, um die Abfrage von Eingaben durch den Benutzer zu vereinfachen.
  • scanf ist eine eingebaute Funktion, die Benutzereingaben abrufen kann.
  • Wir können get_int ganz einfach mit scanf wie folgt neu implementieren:
  #include <stdio.h>
  
  int main(void)
  {
      int x;
      printf("x: ");
      scanf("%i", &x);
      printf("x: %i\n", x);
  }

Beachten Sie, dass der Wert von x in der Zeile scanf("%i", &x) an der Adresse von x gespeichert wird.

  • Der Versuch, get_string neu zu implementieren, ist jedoch nicht einfach. Betrachten Sie das Folgende:
  #include <stdio.h>
  
  int main(void)
  {
      char *s;
      printf("s: ");
      scanf("%s", s);
      printf("s: %s\n", s);
  }

Beachten Sie, dass kein “&” erforderlich ist, weil Zeichenketten etwas Besonderes sind. Trotzdem wird dieses Programm nicht funktionieren. Nirgendwo in diesem Programm weisen wir die Menge an Speicher zu, die für unsere Zeichenkette benötigt wird. In der Tat wissen wir nicht, wie lang eine Zeichenkette sein darf, die der Benutzer eingibt!

  • Ihr Code könnte nun wie folgt geändert werden. Für eine Zeichenkette müssen wir eine bestimmte Menge an Speicher vorab reservieren:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        char *s = malloc(4);
        if (s == NULL)
        {
            return 1;
        }
        printf("s: ");
        scanf("%s", s);
        printf("s: %s\n", s);
        free(s);
        return 0;
    }

    Beachten Sie, dass Sie bei einer Zeichenkette von sechs Bytes eine Fehlermeldung erhalten könnten (aber nicht unbedingt erhalten werden).

  • Wenn wir unseren Code wie folgt vereinfachen, können wir dieses wesentliche Problem der Vorabzuweisung besser verstehen:

  #include <stdio.h>
  
  int main(void)
  {
      char s[4];
      printf("s: ");
      scanf("%s", s);
      printf("s: %s\n", s);
  }

Beachten Sie, dass wir, wenn wir ein Array der Größe 4 vorab zuweisen, cat eingeben können und das Programm funktioniert. Eine Zeichenkette, die größer ist als diese, könnte jedoch einen Fehler verursachen.

  • Manchmal kann der Compiler oder das System, auf dem er läuft, mehr Speicher zuweisen, als wir angeben. Im Grunde genommen ist der obige Code jedoch unsicher. Wir können nicht darauf vertrauen, dass der Benutzer eine Zeichenkette eingibt, die in den von uns zugewiesenen Speicher passt.

File I/O

  • Sie können aus Dateien lesen und diese manipulieren. Betrachten Sie den folgenden Code für phonebook.c:
  #include <cs50.h>
  #include <stdio.h>
  #include <string.h>
  
  int main(void)
  {
      // CSV-Datei öffnen
      FILE *file = fopen("telefonbuch.csv", "a");
  
      // Name und Nummer abrufen
      char *name = get_string("Name: ");
      char *Nummer = get_string("Nummer: ");
  
      // Drucken in Datei - neu!
      fprintf(file, "%s,%s\n", name, number);
  
      // Datei schließen
      fclose(file);
  }

Beachten Sie, dass dieser Code Pointer für den Zugriff auf die Datei verwendet.

  • Sie können eine Datei mit dem Namen “phonebook.csv” erstellen, bevor Sie den obigen Code ausführen. Nachdem Sie das obige Programm ausgeführt und einen Namen und eine Telefonnummer eingegeben haben, werden Sie feststellen, dass diese Daten in Ihrer CSV-Datei bestehen bleiben.
  • Wenn wir sicherstellen wollen, dass die Datei “phonebook.csv” bereits vor der Ausführung des Programms existiert, können wir unseren Code wie folgt ändern:
  #include <cs50.h>
  #include <stdio.h>
  #include <string.h>
  
  int main(void)
  {
      // CSV-Datei öffnen
      FILE *file = fopen("telefonbuch.csv", "a");
      if (!file)
      {
          return 1;
      }
  
      // Name und Nummer abrufen
      char *name = get_string("Name: ");
      char *Nummer = get_string("Nummer: ");
  
      // Drucken in Datei
      fprintf(file, "%s,%s\n", name, number);
  
      // Datei schließen
      fclose(file);
  }

Beachten Sie, dass dieses Programm darauf prüft, ob fopen einen NULL-Pointer zurückgegeben habt. Wenn ja, ruft es return 1 auf.

  • Wir können unser eigenes Kopierprogramm implementieren, indem wir code cp.c eintippen und den Code wie folgt schreiben:
  #include <stdio.h>
  #include <stdint.h>
  
  typedef uint8_t BYTE;
  
  int main(int argc, char *argv[])
  {
      FILE *src = fopen(argv[1], "rb");
      FILE *dst = fopen(argv[2], "wb");
  
      BYTE b;
  
      while (fread(&b, sizeof(b), 1, src) !=0)
      {
          fwrite(&b, sizeof(b), 1, dst);
      }
  
      fclose(dst);
      fclose(src);
  }

Beachten Sie, dass diese Datei unseren eigenen Datentyp namens BYTE erzeugt, der die Größe eines uint8_t hat. Dann liest die Datei ein BYTE und schreibt es in eine Datei.

  • BMPs sind auch Datensammlungen, die wir untersuchen und manipulieren können. Diese Woche werden Sie genau das in der Übung tun.

Zusammenfassung

In dieser Vorlesung haben Sie etwas über Pointer gelernt, mit denen Sie auf Daten an bestimmten Speicherplätzen zugreifen und diese manipulieren können. Behandelt haben wir insbesondere…

  • Pixelbilder
  • Hexadezimalzahlen
  • Speicher
  • Pointer
  • Zeichenketten
  • Pointer-Arithmetik
  • String-Vergleiche
  • Kopieren
  • malloc und Valgrind
  • Garbage-Werte
  • Vertauschen von Variablen
  • Overflow-Fehlern
  • scanf
  • Datei-E/A

Bis zum nächsten Mal!