Programmieren mit C


Inhalt


Willkommen

Installation

Das Terminal

Exkurs: Scanf und Sonderzeichen

Eingabe mit scanf()

Sonderzeichen und chars

Speicherverletzungen

Exkurs: continue & break

Statische Arrays

Exkurs: sizeof

Dynamische Arrays

Exkurs: Speicherbereiche

Funktionen

Exkurs: Funktionen & Arrays

Kommandozeilenparameter

Structs

Dateien

Anwendung: Textdatei einlesen

Hintergrund: Unicode

Stringbibliothek

Ausblick

Light Mode

Scanf() und Sonderzeichen

Eingabe mit scanf()

In der Vorlesungen haben wir einige übliche Fehlerquellen bei der Eingabe mit scanf() kennengelernt. Hier nochmal eine kurze Zusammenfassung.

Sonderzeichen und chars

Bei der Eingabe von chars können Sonderzeichen, die sich noch im Buffer befinden, eingelesen werden. Das Beispiel aus den Vorlesungsfolien dazu:

int a=0, b=0;
char c='0';

scanf("%d", &a);
scanf("%c", &c);
scanf("%d", &b);

printf("%d %c %d\n", a, c, b);

Es sollen also eine Zahl, gefolgt von einem Buchstaben, gefolgt von einer weiteren Zahl eingelesen werden. Um leichter Fehler zu finden, initialisieren wir die Variablen auf bekannte Werte. Zufällige Werte können vor allem bei chars zu verwirrenden Ergebnissen führen. (Sonderzeichen!) Die Ausgabe dieses Programms ist:

2		<-- Eingabe
+		<-- Eingabe
2 		<-- Ausgabe
 0

Um zu verstehen was hier passiert, nutzen wir einige zusätzliche printf() statements.

int a=0, b=0; char c=‘0’;

scanf("%d", &a);
printf("a: %a\n", a);
scanf("%c", &c);
printf("c: %c\n", c);
scanf("%d", &b);

printf("a: %d c: %c b: %d\n", a, c, b);

Damit ändert sich die Ausgabe zu:

2		<-- Eingabe
a: 2		<-- Ausgabe
c: 		<-- Ausgabe
		<-- Ausgabe
+		<-- Eingabe
a: 2 c:		<-- Ausgabe
 b: 0

Das Einlesen von c wird scheinbar überspringen. Um das Verhalten zu verstehen, können wir uns die Eingabe im Tastaturbuffer anschauen. Beim Einlesen der Zahl a geben wir 2 ein und bestätigen mit Enter. Im Tastaturbuffer steht also 2\n, das Zeichen 2 gefolgt von einem Zeilenumbruch. Die Funktion scanf sucht durch den Identifier “%d” nach einer Zahl. Die 2 aus dem Buffer wird also eingelesen, der Zeilenumbruch \n wird im Buffer stehen gelassen.

Im nächsten Schritt soll ein character eingelesen werden. Im Buffer steht nun bereits ein passendes Zeichen, der Zeilenumbruch. Dieser wird in unsere Variable c eingelesen. Das erklärt auch die zwei Zeilenumbrüche in unserer Ausgabe nach c:, der Erste kommt durch die Ausgabe der Variable c zustande, der zweite aus dem Zeilenumbruch in unserer printf(“c: %c\n”, c) Anweisung.

Zuletzt geben wir das Zeichen + ein, das nun in die Ganzzahlvariable b eingelesen werden soll. Durch den Identifier “%d weigert sich die Funktion das Zeichen einzulesen und lässt die Variable unverändert. Die letzte Zeile

printf("a: %d c: %c b: %d\n", a, c, b)

ergibt somit:

a: 2 c:\n b: 0\n

Lösung: Es gibt die Möglichkeit, scanf() das Einlesen von Sonderzeichen zu verbieten. Dazu muss lediglich ein Leerzeichen vor den Identifier %c geschrieben werden. Der Aufruf lautet dann:

scanf(" %c", &c); 

Speicherverletzungen

Durch falsche Identifier können Speicherverletzungen auftreten. Diese Fehler sind oft sehr schwer zu finden und verursachen verwirrende Ausgaben. Um zu verstehen, was mit einer Speicherverletzung gemeint ist, schauen wir uns das Beispiel von oben nochmal an.

int a=0, b=0;
char c='0';

scanf("%d", &a);
scanf("%lf", &c);
scanf("%d", &b);

printf("%d %c %d\n", a, c, b);

Hier haben wir nun, statt %c als Identifier für unseren char, fälschlicherweise %lf genutzt. Geben wir nun für c eine Zahl ein, verändert sich auch der Wert von a. Schauen wir uns das wieder Schritt für Schritt an.

int a=0, b=0;
char c='0';
printf("%p %p %p\n", &a, &c, &b); // Adressen der Variablen ausgeben

scanf("%d", &a);
printf("%d %c %d\n", a, c, b);

scanf("%lf", &c);
printf("%d %c %d\n", a, c, b);

scanf("%d", &b);
printf("%d %c %d\n", a, c, b);

Hier bekommen wir zum Beispiel folgende Ausgabe.

0x7ffceb724adc 0x7ffceb724ad7 0x7ffceb724ad8
2				<-- Eingabe
a: 2 c: 0 b: 0
1000000				<-- Eingabe
a: 4271748 c:  b: -2147483648
2				<-- Eingabe
a: 4271748 c:  b: 2

Werfen wir zunächst einen Blick auf die Adressen der Variablen. Die ersten 11 Ziffern der Hexadezimalzahlen sind bei Allen gleich, wir müssen uns also nur die letzte Ziffer ansehen. Es ergibt sich folgendes Speicherbild.

Speicheradresse: | 7 || 8 | 9 | A | B || C | D | E | F |
                 ---------------------------------------
       Variable: | c || b | b | b | b || a | a | a | a |

Der char c liegt im Speicher an erster Stelle 0x…7 mit einer Größe von einem Byte. An zweiter Stelle liegen die Integer b und a mit jeweils 4 Byte Größe.

Bei einem erfolgreichen Programmlauf würden wir zwei Zahlen und einen Buchstaben an diese Speicherstellen schreiben. Nehmen wir als Beispiel 2 + 2.

Im ersten Schritt schreiben wir die Zahl zwei in die erste Integer a.

Speicheradresse: | 7 || 8 | 9 | A | B || C | D | E | F |
                 ---------------------------------------
         Inhalt: | 0 || 0 | 0 | 0 | 0 || 0 | 0 | 0 | 0 |
                                                     ^
                                                     2

Im zweiten Schritt schreiben wir das Zeichen ‘+’ in die Variable c.

| 7 || 8 | 9 | A | B || C | D | E | F |
---------------------------------------
| 0 || 0 | 0 | 0 | 0 || 0 | 0 | 0 | 2 |
  ^
 '+'

Im letzten Schritt schreiben wir die Zahl 2 noch in die Variable b.

| 7 || 8 | 9 | A | B || C | D | E | F |
---------------------------------------
|'+'|| 0 | 0 | 0 | 0 || 0 | 0 | 0 | 2 |
                   ^
                   2

Das Problem in unserem fehlerhaften Code liegt in der Größe der Variablen. Mit dem Identifier %lf sagen wir scanf(), dass wir eine double-Variable, also eine Kommazahl mit einer Größe von 8 Byte speichern wollen. Unsere Variable c hat als char aber nur 1 Byte. Da c im Speicher direkt vor b und a steht, werden die restlichen 7 Byte also in den für die anderen beiden Variablen reservierten Bereich geschrieben. Im Speicherbild:

| 7 || 8 | 9 | A | B || C | D | E | F |
---------------------------------------
|'+'|| 0 | 0 | 0 | 0 || 0 | 0 | 0 | 2 |
  ^    ^   ^   ^   ^    ^   ^   ^
  ?    ?   ?   ?   ?    ?   ?   ?

Das Überschreiben von Speicherbereichen außerhalb der eigentlichen Variable wird Speicherverletzung genannt. Das größte Problem an Fehlern dieser Art ist aber, dass sie auf verschiedenen Computern verschiedene Ergebnisse liefern können. Auf meinem Computer legt das Betriebssystem den Speicher in dieser speziellen Reihenfolge an. Auf eurem Rechner kann die Reihenfolge aber auch anders sein, sodass kein reservierter Speicherbereich überschrieben wird und kein Fehler auftritt. Der Überbegriff für diese Art von Verhalten ist undefined behaviour.