Wilkommen beim zweiten Teil des C-Kurses. |
Wie am Ende des ersten Teils versprochen wurde, befassen wir uns heute
mit der Datenein- und Datenausgabe. Und wie immer: Erstmal etwas Theorie
:-) Kein Panik, ist nicht schlimmer als im ersten Teil.
DIE VARIABLEN |
Variablen bestehen aus einem Variablennamen und einem Variablentyp.
Das kann man vergleichen mit einem Behälter, wo drauf steht, was drin
ist. Durch den Variablennamen geben wir dem Behälter einen eindeutigen
Namen, denn wir wollen ja den Behälter später wieder finden.
Wie immer ein kleines Beispiel:
#include <stdio.h> void main ( void )
} |
Ich sehe schon die Fragezeichen aufleuchten: "Was bitte soll das %d hier ?" Wie immer: Mysterien sind da, um gelöst zu werden. Am Anfang wird die Variable Anzahl erzeugt. Dies geschieht, in dem nach dem Variablentyp die zu erzeugende Variable, hier Anzahl, steht. Danach wird der Variablen der Wert 100 zugewiesen. Wir haben nun einen Behälter (Variable) von einer bestimmten Größe (int). Den Behälter füllen wir nun mit einem Wert, in dem Fall 100. Danach wird der Wert ausgegeben. Nach dem starten des Programms erscheint also
Grundlegende Variablentypen |
Wir machen kurz einen Werbeblock für die Behälterindustrie: C kennt folgende Grundtypen einer Variable, die dem folgenden Wertebereich entsprechen:
|
[integer] Ganzzahlen (Zahlen ohne Nachkommaanteil) z.B. 2 / -3 / 5 / u.s.w. |
|
[character] ein Zeichen z.B. a / k / z , aber auch die RETURN-Taste |
|
[floating point] Fließkommazahlen (Zahlen mit Nachkommaanteil) z.B. 3.23 / 4.32 / 3.01 |
|
Fließkommazahlen mit doppelt so großem Wertebereich wie der Float-Typ. |
Dem Variablentypen 'int' kann nun noch eins der Schlüsselwörter
'signed' oder 'unsigned' vorangehen. Steht 'signed'
davor, kann das Programm Vorzeichen unterscheiden; steht 'unsigned'
davor, dürfen nur positive Werte benutzt werden. Wo da der Sinn liegt
? Die Variablen belegen je nach Type eine bestimmte Menge Speicher, den
sie benötigen um eine Variable abzuspeichern. Damit alles gleichmäßig
verteilt ist, ist der Anteil der negativen Zahlen genauso groß wie
der, der positiven. Steht ein 'signed' davor, so benutzt das Programm
die positiven und negativen Wertebereiche. Steht ein 'unsigned'
davor, wird nur der positive Teil betrachtet. Moment, was passiert mit
dem restlichen negativen Anteil ? Genau, da er uns auch etwas bringen soll,
benutzen wir ihn einfach mit und hängen ihn an den positiven Wertebereich
hinten an. Damit könenn wir also doppelt soviele positive Zahlen darstellen,
wie mit dem 'signed' Variablentyp. Das kann nützlich sein,
wenn man eh keine negativen Zahlen benötigt. Bei den int-Variblentypen
kann zusätzlich noch 'long' oder short 'short' vergesetzt
werden. Wie unten zu sehen ist, gibt es zwei int-Wertebereiche;
einmal für 16-Bit Maschinen und 32-Bit Maschinen. Sowas ist echt ärgerlich
und kann einen ganz schön ins Fettnäpfchen treten lassen. Wenn
'short' davor steht, werden die 16-Bit Wertebreiche und bei 'long'
die 32-Bit Wertebereiche verwendet.
Grundsätzlich sind alle 'int' - Variablen 'signed' , wenn nichts
explizit angegeben ist!
Hier nun die Wertebereiche der Variablentypen: |
Da jeder Computer nur in gewissen Grenzen rechnen kann, stellen diese
auch die Grenzen der Zahlenbereiche dar, die durch Variablentypen dargestellt
werden können.
|
- 2 147 483 648 ... 2 147 483 647 (32 Bit Maschinen) |
|
0 ... 4 294 967 295 (32 Bit Maschinen) |
|
|
|
|
|
|
|
Wie das bei 'signed short int' und 'signed long int' aussehen könnte ist Hausaufgabe :) |
|
das sind alle Zeichen, die man auf dem Bildschirm darstellen kann |
|
Wie das zustande kommt, am Ende der Tabelle - löst sich in wohlgefallen auf! Versprochen! |
|
|
|
|
Um kurz das 'unsigned char' - Problem zu lösen einen kleinen Ausflug in die Rechnerwelt von Vorgestern. Früher gab es zwei Arten ASCII-Zeichen, das sind die darstellbaren Zeichen, zu kodieren. Da Rechner intern nur mit Zahlen arbeiten, mußten die Zeichen in irgendeiner Form als Zahlen dargestellt werden. Die einen sagten: Wir nehmen den Wertebereich von -128 ... 127 . Die anderen sagten sagten sich: Was soll´s, ist eh nur eine Aufzählung, laßt uns bei 0 beginnen , wie es sich gehört :-) Dieser Wertebereich ging also von 0 ... 255. Um diesem Problem der Unterschiedlichen ASCII-Werte zu entgehen, wurde der 'unsigned char' Typ eingeführt, welcher nur Werte von 0 ... 255 kennt. Prinzipiell wird heute nur noch mit 'unsigned char' gearbeitet, das heißt dieses Problem werden wir einfach unter den Tisch kehren. Ist Ihnen noch was aufgefallen ? Der positive Wertebereich hat scheinbar eine Zahl weniger :-) Nein, nicht wirklich, die 0 wurde einfach den positiven Zahlen zugeordnet. So, jetzt haben wir genausoviele positive wie negative Zahlen. Zurück zu unserem Programm, schon abgetippt und gestartet ? Auf dem Bildschirm müßte jetzt stehen:
Toll! Aber was sollte das %d dort ? Wie im letzten Teil
schon erwähnt ist das % ein Sonderzeichen, genau wie der \
. Das % sagt dem Programm, das dort der Wert einer Variable
stehen soll. In dem Fall %d wird eine integer-Variable angezeigt.Diese
muß innerhalb des printf-Befehls innerhalb der Klammer stehen.
Aufzählungen von Variablen werden mittels eines Kommas getrennt!
Variablenzuweisung |
Variablen werden generell durch ' = ' mit einem Wert abhängig vom
Variablenwert belegt.
Variable = Ausdruck |
Zu beachten ist, das der Ausdruck vom selben Typ wie die Variable sein
muß. Es wäre ja unsinnig einer Zahl einen Buchstaben zuzuweisen.
Zu beachten ist, das ASCII-Zeichen mittels des einfachen Hochkommata zugewiesen
werden.
char Zeichen = 'x' ; |
Wenn Zahlenwerte zugewiesen werden, kann dies entweder direkt oder über
mathematische Ausdrücke geschehen. Diese können auch geklammert
oder mit Hilfe von anderen Variablen berechnet werden. Ein mathematischer
Ausdruck kann folgende Symbole enthaöten
+ | Addition |
- | Subtraktion |
* | Multiplikation |
/ | Division |
% | Modulo |
Die geläufigen + , - , * und / brauch man nicht weiter
beschreiben, den sie gehören zum Grundwissen. Generell gilt auch hier:
Punkt- vor Strichregel. Bei einer Division von Integerwerten wird der Nachkommaanteil
abgeschnitten . Die Modulo-Funktion liefert als Ergebnis den ganzzahligen
Divisionsrest zweier Zahlen zurück. x % y liefert als Ergebnis den
Divisionsrest von x / y .
#include <stdio.h> void main(void)
int zahl_b = 2; int div_rest; div_rest = zahl_a % zahl_b;
|
Wie wir wissen, gilt 5 / 2 = 2 Rest 1. Wenn wir das obere Programm etwas
umschreiben und die Eingangsbemerkung über Integerdivision anwenden,
haben wir ein Programm um Ganzzahldivisionen durchführen zu lassen
und dazu auch den Restbetrag. Wie es sich gehört, werden wir es gleich
mit Eingabeaufforderungen aufpeppen.
eine Division ausführen. Divisionsrest wird auch angezeigt. #include <stdio.h> void main(void)
int zahl_b; int div_rest; int div_ergebnis; printf ("\nBitte Zahl a eingeben : ");
div_ergebnis = zahl_a / zahl_b;
printf ("\n%d / %d = ",zahl_a, zahl_b);
|
Wer befürchtet, das man in C nur diese Grundrechenarten kennt,
der irrt zum Glück. Es gibt z.B. eine Bibliothek, mit der man dann
auch Cosinus, Exponenten und andere Werte berechnen kann. Dies
wird aber in einem späteren Kapitel besprochen.
Bitmanipulation |
Für den Computer sind Zahlen nichts anderes als Ströme, die fließen oder nicht fließen. Diese kleinsten Einheiten werden als Nullen und Einsen dargestellt. Eine Zahl die auf der Basis 2 dargestellt wird (stellvertretend für Strom an/aus) wird im sogenannten Binärformat dargestellt, wobei die beiden Zustände 0 oder 1 sein können.
Diese Grundzustände werden Bits genannt. Die Numerierung der Stellen erfolgt vom letzten, dem niederwertigsten Bit (oder LSB = least significant bit), hin zum höchstwertigsten Bit (oder MSB = most significant bit), also von rechts nach links.
Welchen Wert stellen nun Zahlen im Binärformat dar ? Die Umrechnung erfolgt ganz einfach. Es werden die Stellen von 0 beim niederwertigsten Bit hin zum höchswertigsten hochgezählt. Danach werden alle Stellen, an denen eine 1 steht diese durch 2Stellenwert ersetzt und abschließend alle Werte miteinander addiert.
Das Umrechnen kann man auch durch folgende kleine Module erfolgen lassen.
Auf das Modulkonzept und die Schleifenprogrammierung
wird in späteren Kapiteln eingegangen, aber aus ersichtlichen Gründen
werden die Module hier kurz vorgestellt. Belibige Werte von 0 - 255 werden
hierbei berücksichtigt, da nur die letzten 8 Bits (Bit 0 - 7) dargestellt
werden. Wer Lust am üben hat, kann das Programm ändern, das auch
größere Werte dargestelt werden. Die Funktionsweise ergibt sich
aus den im folgenden besprochenen Befehlen.
/*
Umrechnung Dezimal -> Binaer*/ #include <stdio.h> void main(void)
int DezZahl;} |
Wenn wir das Programm austesten, so erhalten wir beispielsweise folgenden Bildschirmdialog
Für spätere Experimente bilden wir ein Modul , welches wir
zum testen einsetzen werden. Keine Bange, es wird wirklich später
noch zu Genüge auf Module eingegangen ;-)
|
/*
Bitweise ausgeben einer*/ #ifndef _zeig_binaer_h
void zeig_binaer ( unsigned int Zahl )
int i;} #endif |
Wie kann man nun diese Einzelinformationen auf Bitebene verändern
oder abfragen ? Es wird zur Darstellung folgendes Programm benutzt,
welches für die einzelnen Fälle abgewandelt wird. Es kann zur
Grundlage einiger Selbstversuche benutzt und ausgebaut werden.
#include <stdio.h>
#include "zeig_binaer.h" void main(void)
int Zahl_a = 15;} |
Doch nun genug der Vorworte und anderer Dinge, lassen wir lieber taten
und Befehle sprechen.
~Zahl | es erfolgt eine Bitweise Negation |
Alle Bits werden umgedreht, d.h. aus einer 0 wird eine 1 und aus einer
1 eine 0.
Zahl_a & Zahl_b | beide Zahlen werden bitweise verundet |
Die Bits werden an ihren Stellen miteinander verglichen, beispielsweise
die Bits beider Zahlen an 3. Stelle. Sind beide Werte 1 so ist das Ergebnis
1, ansonsten 0.
Zahl_a & Zahl_b | beide Zahlen werden bitweise verodert |
Die Bits werden an ihren Stellen miteinander verglichen. Ist ein Wert
der Zahlen 1 so ist das Ergebnis 1, asonsten 0.
Zahl_a ^ Zahl_b | beide Zahlen werden bitweise exklusiv verodert |
Die Bits werden an ihren Stellen miteinander verglichen. Ist ein Wert
der Zahlen 1 und der andere 0 so ist das Ergebnis 1, ansonten 0.
Zahl << X | Die Bits der Zahl werden um X stellen nach links verschoben |
Die Bits werden nach nach links verschoben und die freiwerdenden Stellen
mit einer Null besetzt.Ein nach links schieben um x entspricht einer Multiplikation
mit 2x .
Zahl >> X | Die Bits der Zahl werden um X stellen nach rechts verschoben |
Die Bits werden nach nach rechts verschoben und die freiwerdenden Stellen
mit einer Null besetzt.Ein nach rechts schieben um x entspricht einer Division
mit 2x Die Multiplikation und Division mittels schieben
erfolgt immer schneller als die mittels * und / .
Typenwandlung (Casting) |
Was tun, wenn man eine integer-Variable hat und diese einmal
als
double und einmal als float in benötigt ? Man verpackt
sie und tut so, als ob die Variable den gewünschten Variablentyp entspricht.
Wenn man die beliebte Darstellung der Variablentypen als Behälter
mit einer Beschriftung verwendet, würde dies bedeuten, das wir einfach
einen anderen Aufkleber auf den Behälter kleben. Dies erreicht man
indem man der eigentlichen Variablen in Klammern den gewünschten Variablentyp
voranstellt:
( Zieltyp ) Variable |
Noch sieht das ganze etwas verworren aus, deshalb schnell ein Praxisbeispiel.
void main ( void )
int i; double fliesz_zahl = 2.34; double d; /*
des Typs integer kopiert, indem sie als Integervariable erscheint i = ( int ) fliesz_zahl; /*
Variablentyp double d = (double) ganze_zahl; |
Vorsicht: Das geht nicht mit Zeigern!
(kleiner Vorgriff meinerseits) Zu beachten ist: Wenn z.B. eine Typenumwandlung
von double zu
integer
erfolgt , so wird der Nachkommateil
abgetrennt. Generell werden Informationen, wenn von einem Variablenbereich
in einen gecastet wird, der eine weniger genaue Darstellung besitzt, immer
Informationen abgeschnitten werden, z.B. der Nachkommaanteil.
Datenausgabe |
Zurück zu unserem Beispielprogramm. DieAusgabe haben wir mit Hilfe
von Hilfszeichen durchgeführt, die alle mit % beginnen.. So
können wir auf diese Weise verschiedene Datentypen darstellen.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
in Abhängigkeit vom Wert |
|
|
|
|
|
|
In unserem Programm steht also, das an der Stelle, wo das %d steht,
die Variable vom Typ Ganzzahl (int) dort eingesetzt werden soll.
Probieren sie ruhig das folgende Beispiel aus und versuchen sie zu
verstehen, was es macht:
/* Wie kann ich ein Zeichen darstellen */
#include <stdio.h> void main ( void )
zeichen='d'; printf ("\nDas Zeichen %c ist im ASCII-Code ein %d \n",zeichen,zeichen); |
Nach dem Start sollte folgendes auf dem Bildschirm zu sehen sein:
Hier sehen wir zwei interessante Dinge, einmal das die Zeichen zwar als char deklariert sind, intern jedoch als Zahlen dargestellt werden. Diese entsprechen den vorher kurz erwähnten ASCII-Zeichen, das sind normierte Zahlendarstellungen für Zeichen.Eine kleine Falle stellen immer die Zahlen dar. Das Zeichen '0' hat als ASCII-Zeichen den Wert 48 und nicht 0, wie man annehmen könnte.
Ein anderes Beispiel:
/* Floats sind auch nur Zahlen */
#include <stdio.h> void main ( void )
printf ("\nEine Floatzahl einmal so %f und einmal so %e \n",zahl,zahl); |
Nach dem starten...
Hier haben wir einmal ausprobiert, wie die Ergebnisse sich unterscheiden,
wenn man verschiedene Darstellungen für die float-Variablen anwendet.
Bei unserem letzten Beispiel haben wir noch etwas neues gemacht: Der Variablen
wird gleich ein Wert bei deren Erzeugung zugewiesen! Wir können also
statt:
float zahl;
zahl=102.033; |
auch schreiben:
float zahl=102.033; |
Dateneingabe |
Wie gewohnt ein Beispiel , an dem wir uns dann entlangseilen werden.
/* Variablen zu lesen macht auch Spasz */
#include <stdio.h> void main ( void )
printf ("\nBitte eine Integer-Zahl eingeben : ");
printf ("\n\nSie gaben %d ein\n",anzahl); |
Versuchen sie zu erkennen, was das Programm macht. Das ganze Programm dürfte für sie ein Kinderspiel sein, bis auf: scanf (...). Dieser Befehl dient der Dateneingabe. Als Ausgabe steht nur, welche Variablentypen eingelesen werden und an welche Variablen die Daten übergeben werden.
In unserem Beispiel: mittels des %d erkennt der scanf-Befehl, das eine Integer-Variable eingelesen werden soll. Standartmäßig wird dieser Wert von der Tastatur eingelesen. Tippen wir einfach mal 42 ein und schließen das mit Druck auf die Return-Taste ab. Das Programm hat nun einen Wert ( 42 ) von uns, den es irgendwo ablegen muß. Mit '&anzahl' sagen wir dem Programm, das der Wert in den Speicherplatz der Variable anzahl geschrieben werden soll. Danach besitzt die Variable den von uns eingegebenen Wert, den wir , freundlich wie wir numal sind, auch auf dem Bildschirm wieder ausgeben.Später werden wir uns noch näher mit den sogenannten Adressoperatoren beschäftigen. Das sollte dann in etwa so aussehen:
Wie wir schon sehen, haben wir, ähnlich wie bei printf ,
ein formatiertes einlesen, wo wir jeweils angeben welche Typen eingelesen
werden sollen.
|
int einlesen in dezimaler Schreibweise |
|
int einlesen, entweder oktal (mit führender 0) oder hexadezimal (mit führendem 0x oder 0X) |
|
int ganzzahlig oktal einlesen (mit 0 am Anfang) |
|
unsigned int , d.h. ohne Vorzeichen |
|
int ganzzahlig hexadezimal einlesen (führende 0x oder 0X muß nicht angegeben werden) |
|
char Zeichen wird eingelesen |
|
Zeichenkette char[] oder char* , wobei als letztes Zeichen \0
ist
Wird die Zeichenkette in char[] abgelegt, so wird nur bis zum letzten Feldelement eingelesen. Das Leerzeichen ist hier ein Trennzeichen für verschiedene Zeichenketten |
|
Floatzahlen mit optionalen Vorzeichen, optionalen Dezimalpunkt und optionalen Exponenten |
Wenn man also eine Hexadezimalzahl einlesen will, kann man dies z.B.
so tun.
in Hexadezimal-, Okta- und Dezimalform #include <stdio.h> void main (void)
printf ("\nBitte Zahl in Hexadezimal eingeben : ");
printf ("\nHexadezimal : %X\n",hex_zahl);
|
Und erhält z.B. folgenden Dialog auf dem Bildschirm.
getchar und putchar |
Zusätzlich gibt es noch Befehle, die das einlesen und Ausgeben
eines einzelnen Zeichens über die Standardeingabe
und -ausgabe erlauben, also in der Regel dem einlesen über Tastatur
und dem Ausgeben auf dem Bildschirm.Zu dem, was sich hinter diesen Kanälen
verbirgt später mehr. getchar() liefert ein char-Zeichen
zurück und putchar() wird ein
char-Zeichen übergeben.
Das sieht dann Beispielsweise so aus.
#include <stdio.h> void main (void)
char abbruch = '#'; /*
getchar liefert ein char-Wert zurück, puchar (char);
es wird solange ein Zeichen eingelesen,
while (( zeichen = getchar() ) != abbruch)
|
Das Programm gibt solange alle eingegebenen Zeichen aus, bis # gedrückt wurde. Das einlesen der Zeichen wird mit putchar realisiert. Die Schleifenbedingung while , die hier verwendet wurde, wird später erklärt. Sie braucht es also nicht zu beunruhigen, da auf sie noch später eingegangen wird. Starten wir das Programm, so können wir folgenden Dialog erhalten:
Soviel zu unserer schönen Theorie. An diesem Beispiel können
wir auch schon sehen, wie unser System Daten verarbeitet. Alle Ein- und
Ausgaben werden in der Regel durch die Betriebssysteme gepuffert. Geben
wir also unseren Text ein, so wird er nicht zeichen für Zeichen übergeben,
sondern ersteinmal in einen Zwischenspeicher, einen sogenannten Puffer,
geschrieben. Nach einem return wird der Puffer an das Programm übergeben
und nacheinander gibt getchar() die Zeichen an das Programm weiter.
Wie wir sehen, kümmert sich das System herzlich wenig darum, welche
Abbruchbedingung in unserem Programm gesetzt wurden. Der Vollständigkeit
halber hier noch die generelle Darstellung von getchar und putchar.
char getchar(); |
char putchar(char); |
Momentmal, wird hier einer sagen, wieso bekommen wir von putchar
einen Wert zurück ? Das liegt daran, das auf diese Weise eine Fehlerkontrolle
möglich ist. In der Regel prüft man seltenst, ob alles bei der
Ausgabe korrekt verlief, aber man hat zumindest die Möglichkeit es
zu prüfen. Wenn das zurückgegebene Zeichen gleich dem übergebenen
Zeichen ist, konnte es ordnungsgemäß ausgegeben werden. Wenn
der zurückgegebene Wert EOF ist, so ist ein Fehler aufgetreten.
Veranschaulichen wir es uns am Beispiel des vorigen Programmes, was mit
dieser Formulierung gemeint ist.
#include <stdio.h> void main (void)
char abbruch = '#'; char check; while (( zeichen = getchar() ) != abbruch)
if (check == EOF)
printf ("\n\nDanke fuer den Programmstart\n\n"); |
Übungsaufgabe |
Folgendes Beispielprogramm:
/*
Einlesen zweier Floatzahlen die addiert werden*/ #include <stdio.h> void main ( void )
printf ("\n\nBitte geben sie zwei Flieszkommazahlen ein [z.B.
2.34 , 5.23] ");
zahl3 = zahl1 + zahl2; printf ("\n\n %f + %f = %f \n\n",zahl1, zahl2, zahl3); |
Kleiner Hinweis: Die zwei einzugebenden Zahlen sind durch ein Komma zu trennen.
1) Was macht das Programm ? Versuchen sie Zeile für Zeile zu verstehen.
2) Ändern sie das Programm so ab, das keine Floatgrößen,
sondern Ganzzahlige (int) Werte eingelesen und ausgegeben werden.
3) Probieren sie selbiges einmal mit char-Variablen und überlegen
sie, warum das Ergebnis so aussieht und was es mit der Addition von Buchstaben
auf sich hat.
...das Obligatorische |
Autor: Sebastian Cyris
Dieser C-Kurs dient nur zu Lehrzwecken! Eine Vervielfältigung ist ohne vorherige Absprache mit dem Autor verboten! Die verwendete Software unterliegt der GPL und unterliegt der Software beiliegenden Bestimmungen zu deren Nutzung! Jede weitere Lizenzbestimmung die der benutzten Software beiliegt, ist zu beachten!