Erster Teil des Intensivkurses C
Inhalte des ersten Termins (2009-11-16)
Vorbemerkung: Bei Bedarf können auch Perl-Programme „übersetzt“ werden
- Allgemeine Grundbegriffe der Programmierung
- Spezielle Grundbegriffe der C-Programmierung
- "Each of these escape sequences shall produce a unique" "When a value is stored in an object of structure or" "9899:1999 (E)"
- Programm mit »abort aus stdlib.h«
- /usr/bin/gcc main.c -x c -W -Wall -Wsequence-point -Wconversion -Wno-parentheses -pedantic -std=c99 -lm
- Die Formatierung von Quelltext (Wörter und deren Trennung, lexikalische Einheiten, Zeilen, Kommentare)
- Include-Direktiven
- Operationsausdrücke, Operationen in der Standardschreibweise (abort aus <stdlib.h>, ÜA: getchar)
- Ausdrucksanweisung, Blockanweisung (ÜA: Semikolon, „one, two“)
- Einrückungsstil
- Zeichen/»char«, Zeichenfolgen/»char *« (ÜA: »"Hallo", sagte er.«, Quelltext ausgeben)
- Werte, Literale und Typen
- int, 0x2ECA, 012, C als abstrakte Sprache (int-Wertebereich); Es ist fast irrelevant, ob das Programm „läuft“
- Wertausdrücke, neben Literalen auch in der Standardschreibweise: getchar, »int« gegenüber »void« in der Dokumentation), Werteverwurf von Literalen und Standardausdrücken
- Klammertests für Anweisungen und Ausdrücke als grobe Heuristiken
- Anweisung (hat Wert) oder Ausdruck (einer der schon behandelten Typen)? { ; } — 0 — 0; — 0; 0 — 0; 0; — { 0; 0; } — {} — { {} } — {{ 0 }} — (( 0 ))
- unbestimmte Operationen wie putchar, int-Literale als Argumente, (ÜA: exit, Ausgabe eines eingelesenen Zeichens)
- printf mit einigen grundlegenden Aspekten der Formatierungsnotation (wie in perl)
Inhalte des zweiten Termins (2009-11-18)
- Empfehlenswerte Literatur zur Programmiersprache C
- Programmieren in C; Brian W. Kernighan, Dennis M. Ritchie; ISBN-10: 3-446-15497-3; ISBN-13: 978-3-446-15497-1; http://www.hanser.de/buch.asp?isbn=3-446-15497-3
- Informationen zu vim
- vim cheat
- vim tutorial
- man vim
- :help
- Einige häufig verwendete Kommandos im Grundmodus
- i Einfügemodus (verlassen mit Esc)
- A Am Ende einer Zeile anfügen
- r ein einzelnes Zeichen ändern
- R zum Überschreibemodus wechseln
- x ein einzelnes Zeichen löschen
- c w Rest des aktuellen Wortes verändern
- 〈Zahl 〉 Wiederholung des folgende Kommandos
- u Änderung zurücknehmen
- 0 Zum Anfang der Zeile gehen
- $ Zum Ende der Zeile gehen
- : Zur Eingabe für erweiterte Kommandos wechseln
:
〈Zahl 〉 Zur angegebenen Zeile gehen:w
Datei speichern:w
〈Name 〉 Datei 〈Name 〉 speichern:w!
Datei überschreiben:q
vim beenden:q!
vim ohne speicher beenden:wq
Speichern und beenden- / Zur Eingabe für Suchtext wechseln
/
〈Text 〉 〈Text 〉 suchen
- double, 22E22, 1234567890123456789012345678901234567890.
- Mehrere Parameter, pow, ÜA: hypot (ggf, sonst fmod, remainder, remquo, copysign, nextafter, fdim, fmax, fmin, fma)
- Wahrheitswerte: 0 und nicht 0, isalpha als Beispiel und isprint als Übung
- Typtoleranz von Parametern
- cast-Ausdrücke
- Grundrechenarten, Vorzeichen, Klammern und Prioritäten, ( 1 / 2 / 3 ), ( 1.1 * 1.1 ) verglichen mit ( 1.21 ), 1e20 + 1 - 1e20
- Terme
1 3 3 + 4 3
--- - ---, -------, 2 ---
2 4 5 - 6 7
- Sieben Prozent von 180; Der Preis von 2 kg, wenn 3 kg 6,50 Euro kosten; 140 Kelvin in Grad Celsius angegeben
- rand() benutzen, um einen Würfel (1..6) zu erzeugen
- Typanpassung
- Vergleichsoperatoren, Priorität, 9 > 8 > 7, ( float )3.7, -1 < 3000000000? -1 < 2000000000?
- Verneinung !, Priorität, !!, !( x != y ), !( x < y ), !( x <= y ), !!( x >= y ),
- Ganzzahlige Division und der Rest
- Der ternäre Operator für bedingte Ausdrücke
- Definition einfacher Wirkfunktionen (Uebungsaufgabe)
Tomaten
Rotkohl Gruenkohl
Gurken
Tomaten
Rotkohl Gruenkohl
Spinat
Tomaten
Rotkohl Gruenkohl
Kohlrabi
- Benennungsstil
- Deklarationen und Definitionen
- Wertrückgabe, int main(void), ÜA: rand
Inhalte des dritten Termins (2009-11-23)
- Hinweis zu Kurszielen am Ende dieser Seite
- Literatur?
- Nachbesprechung der Übungsaufgabe zu einem Ausdruck für Zufallszahlen
- Hinweis zur Verbreitung von Implementation von „C99“ und „C90“, im Folgenden wird zunächst weniger Gewicht auf spezielle Eigenarten von C99 gelegt werden
- Nachholen einer Übung: „Wie weit kann man mit den Grundrechenarten rechnen?“
>721377 Zeilenformatierung in C
>721251 Zahlenformatierung in C
>721411 Dokumentationskommentare in C
>720988 Präprozessor *
>721386 Vorverarbeiterdirektiven in C
>721413 Laufzeitoptimierte Funktionsaufrufe in C
>721422 Wertrückgabe
>721507 Die Wertrückgabe in C
- Übungsaufgabe Funktion, die eine Zufallszahl zwischen 1 und 6 zurückgibt
>720570 getrennte Übersetzung *
>721419 getrennte Übersetzung in C
>721209 Konstantendefinitionen
>721502 Konstantendefinitionen in C
>721513 Konstanten mit Dateigültigkeitsbereich in C
>721467 Blockkonstanten
>721519 Blockkonstanten in C
>720836 Parameter
>720840 Parameter-Definitionen in C
Inhalte des vierten Termins (2009-11-30)
- Rückfrage: Wofür werden in der Numerik allozierte Graphenstrukturen benötigt?
Parameter-Definitionen in C
Übungsaufgaben
- Eventuell Nachbesprechung der Übungsaufgaben zu Konstanteneinführung und Konstantenelimination.
- Vorstellen einer rekursiven Funktion zur Berechnung der Fakultät einer Zahl
- / Parametrisierter Würfel (Nachbesprechung frühestens 2009-12-02)
- Schreiben Sie eine Definition einer Funktion »dice« mit zwei Parametern (jeweils vom Typ »int«) und einem Ergebnis vom Typ »int«, so daß der Wert des Ausdrucks »dice(〈i 〉,〈j 〉)« eine (ungefähr gleichverteilte) ganzzahlige Zufallszahl zwischen 〈i 〉 (einschließlich) und 〈j 〉 (einschließlich) ist.
- / Ganzzahliger Mittelwert zweier ganzzahliger Werte (Nachbesprechung frühestens 2009-12-02)
- Schreiben Sie eine Definition einer Funktion »avg« mit zwei Parametern (jeweils vom Typ »int«) und einem Ergebnis vom Typ »int«, so daß der Wert des Ausdrucks »avg(〈i 〉,〈j 〉)« die größte ganze Zahl kleiner-gleich dem Mittelwert der Werte der beiden Argumentausdrücke »〈i 〉« und »〈j 〉« (jeweils vom Typ »int«) ist.
- Testen Sie Ihre Definition unter anderem an Hand der Werte der in der folgenden Liste vor dem Semikolon stehenden Ausdrücke (nach »#include <limits.h>«):
- »avg(0,0)«; 0
- »avg(1,0)«; 0
- »avg(0,1)«; 0
- »avg(1,1)«; 1
- »avg(INT_MIN,INT_MIN)«; INT_MIN
- »avg(INT_MIN,INT_MAX)«; Unter einer typischen C -Implementation wird ein Wert nahe 0 (also 0, +1 oder −1) erwartet
- »avg(INT_MAX,INT_MIN)«; Unter einer typischen C -Implementation wird ein Wert nahe 0 (also 0, +1 oder −1) erwartet
- »avg(INT_MAX,INT_MAX)«; INT_MAX
- Der gesuchte Mittelwert soll aber natürlich nicht nur für diese acht Testwerte, sondern für alle möglichen int -Werte richtig berechnet werden. Um dies zu testen, könnten daher noch weitere Testwerte herangezogen werden.
Aufrufe von gcc
Allgemeine Warnung Wenn eine Datei erzeugt wird, dann wird eine schon unter dem Namen dieser Datei vorhandene Datei oft ohne Nachfrage ersetzt, also gelöscht. Damit enthalten viele Kommandos ein verstecktes Löschkommando. Deswegen muß man nicht nur vorsichtig sein, wenn man »rm« direkt aufruft.
- Übersetzen und Binden einer Quelldatei (nach »a.out«)
gcc example.c
- Übersetzen ohne Binden (erzeugt die Objektdatei »example.o«)
gcc -c example.c
- Binden der Objektdatei (erzeugt die ausführbare Datei »a.out«)
gcc example.o
- Ausgabe in die Datei »example« statt »a.out«
gcc -o example example.c gcc -o example example.o
- Binden mehrerer Dateien, gegebenenfalls mit vorherigem Übersetzen
gcc example.c example1.c example2.o example3.o
Einige Optionen
-c
Nur in Objektdatei übersetzen (nicht binden)
-lm
Mathematik-Bibliothek dazubinden
-x c
Angabe der Programmiersprache C (Alternativen sind unter anderem »-x c«, »-x c-header«, »-x c++«)
-std=c99
Aktuelle C Version (Eine Alternative ist unter anderem »-std=c89« für eine ältere Version)
-pedantic
Genauere Prüfungen der Einhaltung der Sprachspezifikation
-Wall
Mehr Prüfungen für Warnungsmeldungen durchführen
-Wextra
Zusätzliche weitere Prüfungen für Warnungsmeldungen durchführen
- Option Index
- Beschreibung weiterer Kommandozeilenoptionen
- http://gcc.gnu.org/onlinedocs/gcc/Option-Index.html
Eine einfache Makefile
In einer Makefile müssen die eingerückten Zeilen mit einem TAB-Zeichen (Zeichenkennzahl 7) eingerückt sein. Manche Editoren interpretieren die TAB-Taste aber als Kommando oder ersetzen TAB-Zeichen durch Leerzeichen, so daß das Editieren einer Makedatei nicht immer ganz einfach ist.
vim
:set noexpandtab
:set nosmarttab
Eine Makefile mit impliziten Regeln
Makefile
CC=gcc CFLAGS=-Wall example: example.o clean:
rm -f example example.o- Kommando zum Erzeugen
make
- Kommando zum Aufräumen
make clean
Eine Makefile mit expliziten Regeln
Makefile
CC=gcc CFLAGS=-Wall example: example.o gcc -o example main.o example.o: example.c gcc -c -o example.o example.c
- Kommando
make
Objekte in C
- Objekte: typisierte Speicherbereiche, nicht mit einem Namen versehen
Abgrenzung zu Objekt im Sinne von „objektorientierte Programmierung“
Objekte haben eine Lebensdauer (lifetime): statisch, automatisch oder alloziert.
statisch Das Objekt existiert vom Programmstart bis zum Programmende
automatisch Das Objekt existiert vom Beginn der Ausführung seiner Definition in einer Blockanweisung bis zum Ende der Ausführung dieser Blockanweisung
alloziert Das Objekt wird durch eine spezielle Auswertung erzeugt und später durch eine andere Auswertung eines Ausdrucks an einer beliebigen Stelle im Programm wieder aufgelöst - Variablen als benannte Objekte
Namen haben einen Gültigkeitsbereich (scope): sie gelten innerhalb einer Quelldatei, eines Block, oder eines Prototyp (jeweils ab dem Ort ihrer ersten Deklaration).
Namen haben eine Bindung (linkage): extern, intern oder keine (sie werden vom Binder demselben Namen in anderen Modulen zugeordnet oder nicht oder der Binder sieht sie gar nicht erst).
Eine Variable ist eine Verbindung aus einem Namen (mit Gültigkeitsbereich und Bindung) und einem Objekt (mit Lebensdauer). Sie hat also Lebensdauer, Gültigkeitsbereich und Bindung. Die Unterscheidung nach Lebensdauer, Gültigkeitsbereich und Bindung ist also genauer als die in manchen Programmiersprachen, in denen man nur zwischen „lokalen“ und „globalen“ Variablen unterscheidet, auch wenn nicht alle diese Eigenschaften in C ganz unabhängig voneinander sind.
Zuweisungen in C
- Ausdruck »〈L 〉 = 〈R 〉«
- Wert der Zuweisung Der Wert 〈R 〉 gewandelt in den Typ von 〈L 〉
- Wirkung einer Zuweisung Der Wert der Zuweisung wird nach 〈L 〉 geschrieben
Übungsaufgaben
- / Wert einer Zuweisung
- void f( int const w ){ char v; printf( "%d\n",( v = w )== 〈? 〉 ); }
- Welcher Ausdruck sorgt an der Stelle des Fragezeichens 〈? 〉 dafür, daß bei der Auswertung des Ausdrucks »f(〈zulässiger Ausdruck 〉)« stets »1« ausgegeben wird?
Verkürzte Zuweisungen in C
- Ausdruck: »〈L 〉 += 〈R 〉« bedeutet »〈L 〉 = 〈L 〉 + 〈R 〉« (entsprechend für andere binäre Operatoren), jedoch wird dabei der Ausdruck 〈L 〉 nur einmal ausgewertet.
- »++〈L 〉« bedeutet »〈L 〉 += 1«, »〈L 〉++« hat dieselbe Wirkung, aber den Wert von »〈L 〉«
Links- und Rechtswerte in C
Es ist verständlich, daß die Zuweisung »v = 5« möglich ist, aber die Zuweisung »5 = v« nicht möglich ist, da »5« kein veränderbares Objekt angibt. Man sagt dazu in C auch, daß »5« kein „Lwert“ sei, sondern nur ein „Rwert“ sei. Ein Variablenname, wie »v«, kann sowohl Lwert als auch Rwert sein, so daß »v = v« möglich ist, dabei steht »v« links für das Objekt »v« und rechts für den darin gespeicherten Wert.
Undefiniertes Verhalten in C
Wenn eine Spezifikation einer Programmiersprache für einen bestimmten Fall kein bestimmtes Verhalten verlangt, so wird dieses als „undefiniertes Verhalten “ bezeichnet. Da es bei undefiniertem Verhalten auch zulässig ist, wenn eine Implementation dieser Programmiersprache die Festplatte löscht oder irgendetwas Beliebiges mit beliebig katastrophalen Folgen macht, gilt es als schwerer Fehler, wenn ein Programmierer Quellcode schreibt, dessen Ausführung zu undefiniertem Verhalten führt. Das ist in C jedoch leider oft recht einfach.
Das Erkennen von undefiniertem Verhalten ist schwierig, da es weder immer durch Fehlermeldungen gemeldet wird, noch durch Ausprobieren oder Testen zuverlässig erkannt werden kann. Die besten Mittel gegen undefiniertes Verhalten sind eine gute Ausbildung und die nötige Sorgfalt.
Weniger schlimm ist unspezifiziertes Verhalten, bei dem die Spezifikation einige verschiedene Möglichkeiten zuläßt (aber nicht alles Beliebige).
Sequenzpunkte in C
In C gibt es sogenannte „Sequenzpunkte“, wie beispielsweise das Ende einer Anweisung, zu denen alle Wirkungen einer Auswertung eingetreten sind. Es führt jedoch zu „undefiniertem Verhalten“, wenn versucht wird, ein Objekt zwischen zwei Sequenzpunkten mehr als einmal zu verändern. Der Ausdruck »(i = ++i)« enthält gar keinen Sequenzpunkt, sein Auswertung hat also undefiniertes Verhalten. Auch die Anweisung »i = ++i;« ist daher falsch.
Übungsaufgaben zu Variablen
- / Vertauschen der Werte zweier Objekt
{ int x = 22;
int y = 47;
/* Hier die Werte von x und y vertauschen */
printf( "%d %d\n", x, y ); /* Ausgabe: 47 22 */ }- / Vertauschen der Werte zweier Objekt mit einer Funktion
{ int x = 22;
int y = 47;
swap( x, y ); /* Ist solch eine Funktion moeglich? */
printf( "%d\n%d\n", x, y ); /* Ausgabe: 47 22 */ }
Parametrisierte Makros des C -Präprozessors
- Makros mit Parameter,
- Bekannte Fehlerträchtigkeit solcher Makros
Namen für Objekte mit statischer Lebensdauer
»extern int name;« ist eine Deklaration für einen Namen »name« mit externer Bindung und vom Typ »int«, entsprechend auch für andere Namen und Datentypen. Eine Deklaration in dieser Schreibweise ist stets eine reine Deklaration ohne Objektdefinition. Sie stellt also keinen Speicher bereit.
Eine Deklaration ohne »extern« und mit Initialisierung, wie »int alpha = 0;«, ist stets immer auch eine Objektdefinition, sie stellt also auch Speicherplatz bereit.
Bei getrennter Übersetzung sollte es zu einem Namen genau eine Objektdefinition in einer Übersetzungseinheit geben und kann es beliebig viele reine Deklarationen desselben Namens geben, die dann vom Binder als Bezüge auf das einmal definierte Objekt aufgelöst werden.
Andere Schreibweisen, wie etwa »int alpha;« (sowohl ohne »extern« als auch ohne Initialisierung) oder »extern int alpha = 0;« (mit »extern« und Initialisierung) sind komplizierter zu interpretieren und sollten daher vermieden werden. (Innerhalb eines Blocks kann die erste Schreibweise aber legitim sein.)
Die Deklaration »static int alpha = 0;« deklariert ein Objekt mit statischer Lebensdauer und dem Anfangswert 0. Steht sie außerhalb eines Blocks, so hat der Name interne Bindung, steht sie innerhalb eines Blocks, so hat der Name keine Bindung.
Übungsaufgaben zu Objekten mit statischer Lebensdauer
- / Aufrufzähler
- Schreiben Sie eine Definition einer Funktion, die mitzählt wie oft sie aufgerufen wurde. Dabei soll der Gültigkeitsbereich der dazu verwendeten Zählvariablen möglichst klein sein.
Verzweigungen
- 〈selection-statement 〉 ::=
- "if" "(" 〈condition 〉 ")" 〈statement 〉.
- 〈selection-statement 〉 ::=
- "if" "(" 〈condition 〉 ")" 〈statement 〉 "else" 〈statement 〉.
- Im Gegensatz zu Perl muß kein Block verwendet werden, dieser kann aber nötig werden, wenn mehrere Anweisungen zu einer zusammengefaßt werden sollen.
- Semantik
bedingte Anweisung
int main( void )
{ if( 0 )printf( "ab\n" );
if( -2 )printf( "cd\n" ); }- Ausgabe? Übersetzung in Maschinensprache?
- Übungsaufgabe Ein Programm soll „Hallo, Welt!“ ausgeben und dessen Quelltext soll kein Semikolon enthalten.
Kehrwert
int kehrwert( int const wert )
{ if( wert )printf( "%g\n", 1. / wert );
else printf( "nicht definiert\n" ); }- Die folgende Funktionsdefinition soll ein Prädikat definieren, das ergibt „ob ein Wert 0 ist“. Es sollen eventuell vorhandene Fehler korrigiert, und es soll die Definition vereinfacht werden.
is0
bool is0( int const v )
{ if( v = 0 )result = 1;
else result = 0;
return result; }- Die folgende Anweisung soll vereinfacht werden (»zahl« habe den Datentyp »int«).
Nicht Null
if( zahl != 0 )printf( "nicht Null\n" );
- / Einfache Rekursion
- Schreiben Sie eine Definition einer Funktion »ast« mit einem Parametern vom Typ »int«, so daß bei der Auswertung von »ast(〈i 〉)« 〈i 〉 Sternchen ausgegeben werden (beispielsweise »***«, wenn 〈i 〉 den Wert 3 hat).
Schleifen
- 〈iteration-statement 〉 ::=
- "while" "(" 〈condition 〉 ")" 〈statement 〉.
- Semantik
- for-Schleifen
- do-Schleifen
- Eine Funktion »kgv«, die zwei Parametern ein Funktionsergebnis zuordnet, entsprechend dem folgenden Struktogramm in C implementieren.
Funktion kgv
mit ganzzahligem Ergebnis,
dem ganzzahligen Eingangsparameter a und
dem ganzzahligen Eingangsparameter b
.--------------------------------------------------.
|Mit den ganzzahligen Variablen x und y. |
| .-----------------------------------------------|
| | x := a; y := b; |
| |-----------------------------------------------|
| | Wiederhole solange x ungleich y |
| | .--------------------------------------------|
| | | Ist x < y? |
| | | Ja | Nein |
| | |---------------------|----------------------|
| | | x := x + a | y := y + b |
| |-----------------------------------------------|
| | Das Ergebnis ist x. |
'--------------------------------------------------'
- Übungsaufgabe zum nächsten Termin Näherungsweise Integration der Funktion „x²“ im Intervall [0,1), durch Summieren der n Rechtecke der Breite 1/n, deren Höhe der Wert der Funktion „x²“ an linken Rand dieser Rechtecke ist. Vergleich des Ergebnisses für n=1E5 mit dem entsprechenden bestimmten Integral 1/3 (http://www.wolframalpha.com/input/?i=integrate+x%C2%B2+dx+from+x%3D0+to+1).
Inhalte des fünften Termins (2009-12-02)
Ergänzungen zum Editor „vim“
- Weitere Bewegungen (zusätzlich zu h, j, k und l)
- g g Textanfang
- G Zeile (beispielsweise 1 2 G), G alleine: Textende
- ctrl-o Zurückgehen
- 0 Zeileanfang
- ^ Textzeilenanfang
- $ Zeilenende
- b Wortanfang
- e Wortende
- w nächstes Wort
- % zugehörige Klammer
- t〈x 〉 Zeichen 〈x 〉 (ausschließlich)
- f〈x 〉 Zeichen 〈x 〉 (einschließlich)
- Textspeicher-Kommandos
- y Kopieren („yank“)
- d Ausschneiden („delete“)
- p Einfügen hinter der aktuellen Position („paste“)
- P Einfügen vor der aktuellen Position („paste“)
Diesen Kommandos folgt ein Bewegungskommando, wie 2 w („zwei Wörter“). d i w löscht, c i w ändert das gesamte Wort.
Die Textspeicher-Kommandos können aber auch einer visuellen Selektion folgen.
v visuelle Selektion (danach: Esc, y, oder d)
:registers
Anzeige der Register
- Interaktion mit Register a
- " a p Einfügen aus Register a
- " a y Einfügen in das Register a
- Interaktion mit der Systemablage (sollte aber nicht über ssh möglich sein)
- shift-insert Einfügen aus der Systemablage
- " * p Einfügen aus der Systemablage
- " + y Einfügen in die Systemablage
- Löschen ohne Registerveränderung
- " _ d… Löschen ohne Veränderung eines Registers
- Suchen
- * aktuelles Wort
/
〈muster 〉 Textmuster 〈muster 〉 suchen- n nächstes Vorkommen
- N früheres Vorkommen
:set incsearch
Inkrementelle Suche- Spracheunterstützung
:syntax on
Syntaxeinfärbung aktivieren:set filetype=C
Sprache festlegen- Mehr oder weniger häufig benötigte Kommandos
- ~ Groß- und Kleinschreibung vertauschen
- x p Zwei Zeichen vertauschen (cut and paste)
- d w w P Zwei Wörter vertauschen
- g f Wort als Dateinamen laden
- . Letztes Kommando wiederholen
- @ : Letztes Zeilenkommando wiederholen
:tabedit
〈filename 〉 Tab-Edit, shift-Page:ab dns Desoxyribonukleinsäure
Abkürzungvim .exrc
Bearbeiten der Startdatei- Aufzeichnungen
:earlier 4m
4 Minuten zurück:later 45s
45 Sekunden vor:undo 5
5 Schritte zurück:undolist
Liste ansehen- A Byte of Vim
- Swaroop C H
- http://www.swaroopch.com/notes/Vim
Reihungen und Zeiger
- Reihungen werden auch „Felder“ genannt, was hier wegen der Mehrdeutigkeit des Begriffs „Feld“ vermieden wird.
- »int a[ 2 ];« deklariert eine Reihung mit zwei Komponenten.
- »int a[ 2 ]={ 1, 2 };« deklariert und initialisiert eine Reihung mit zwei Komponenten.
- Die erste Komponente kann nach »int * p = a;« durch den Zeiger »p« referenziert werden.
- Die erste Komponente (also Objekt) ist »*p« („Inhalt von p“, „Dereferenzieren“).
- Wenn »T *«der Typ von »q« ist, dann ist »T« der Typ von »*q«.
- »p« ist „die Adresse“ von »*p«, »*p« ist „der Inhalt“ von »p«. Jedenfalls die erste Sprechweise ist allgemein üblich.
- »p + 1« ist „die Adresse des Objekts direkt hinter dem Objekt »p«“ („Adreßarithmetik“, „Zeiger-Arithmetik“).
- »*( p + 1 )« ist das Objekt direkt hinter dem Objekt »*p«.
- »p[ i ]« bedeutet »*( p + i )« und somit auch »i[ p ]«.
- Fast immer gilt der Name einer Reihung als Adresse ihres Anfangs, so daß oben auch überall »a« statt »p« verwendet werden kann.
- Ausnahme: »sizeof a«, »sizeof p«. Ausdruck der Anzahl der Komponenten eines vollständigen Reihungstyps mit sizeof.
- »size_t« (stddef.h), »%z«
- Die Adresse eines Objekts »o« »&o« („Adresse von o“).
- Wenn »T«der Typ von »o« ist, dann ist »T*« (sprich „T Zeiger/pointer“?) der Typ von »&q«.
- Beispiel: »p == &a[ 0 ]«, »p + 1 == &a[ 1 ]« (Prioritäten der Operatoren beachten!).
- »a[ 2 ]« ist ein gefährlicher Fehler, vor dem ein C-Programmierer letztendlich nie richtig geschützt ist.
- Die Ausgabe einer Adresse kann mit dem Formatspezifizierer »%p« erfolgen.
- Die Übergabe eine Reihung an eine Funktion oder die Rückgabe kann über einen entsprechenden Zeiger- oder Reihungstyp erfolgen.
- unvollständige Typen bei getrennter Kompilierung: extern int A[]; int A[10];
- Übungsaufgabe Eine Funktion vertauscht die beiden Komponenten einer Reihung vom Typ int[ 2 ]
- Übungsaufgabe Eine Funktion vertauscht die Inhalte zweier int-Variablen.
- Übungsaufgabe zum nächsten Termin Eine Funktion sortiert eine int-Reihung, deren Größe ihr ebenfalls übergeben wird.
- Eine Nullzeigerkonstante ist immer »0«, aber nicht immer »NULL«
Zeichenfolgen
- Jedes Zeichenfolgenliteral gehört zu einem Objekt aus den Zeichen des angegebenen Textes und einem zusätzlichen Nullzeichen (mit dem Wert 0) als Endmarkierung. Der Typ ist »char *«.
- Direkte aufeinanderfolgende Zeichenfolgenliterale werden verbunden
- Der Wert eines Zeichenfolgenliteral ist die Adresse seines ersten Zeichens.
- Beispiel: Implementation einer strlen-Funktion
- Übungsaufgabe: Implementation einer strcchr-Funktion.
- Übungsaufgabe: Implementation einer strcmp-Funktion.
- Übungsaufgabe: Implementation einer strcpy-Funktion.
Inhalte des 6. Termins (2009-12-14)
Nachtrag zu Prototypen
Im allgemeinen ist in C (nicht aber in C++ ) »int main( void )« besser als »int main()«, da die erste Schreibweise einen „Prototyp“ definiert und es so dem Compiler erlaubt, Aufrufe besser zu prüfen. »int main()« würde eine Funktion mit einer unbestimmter Zahl von Argumenten mit unbestimmten Typ deklarieren.
- Konsole
$ head test.c | cat -n 1 int f( void );
2 int g();
3 int main( void )
4 {
5 g( 1 );
6 f( 2 );
7 }
8 $ mcc test.c:2: warning: function declaration isn't a prototype
test.c: In function 'main':
test.c:6: error: too many arguments to function 'f'
Die Verwendung von »void« in »int main( void )« wird so auch ausdrücklich in ISO/IEC 9899:1999 (E), 5.1.2.2.1#1 angegeben:
- Zitat aus ISO/IEC 9899:1999 (E), 5.1.2.2.1#1
- It shall be defined with a return type of int and with no parameters:
- int main(void) { /* ... */ }
- or with two parameters (referred to here as argc and argv, though any names may be used, as they are local to the function in which they are declared):
- int main(int argc, char *argv[]) { /* ... */ }
- or equivalent; or in some other implementation-defined manner.
Allerdings findet sich in derselben Norm in Beispielen auch »int main()«, was tatsächlich dann tolerabel ist, wenn »main« nicht im Rest des Programms aufgerufen werden soll.
Nachbesprechung von Übungsaufgaben
- avg(int,int)
- Sortieren einer int-Reihung
- Implementationen von strcmp und strcpy
Nachtrag zu Makros
- Makros sollten in Großbuchstaben geschrieben werden, damit sie als solche erkannt werden, etwa wegen mehrfacher Auswertung ihrer Argumente. Einige Standardfunktionen dürfen von einer C -Implementation aber auch als Makros implementiert werden, obwohl sie kleingeschrieben werden.
Nachtrag zu Zeichen
- Der Typ eines Zeichenliterals (wie »'a'«) ist »int« (6.4.4.4#10).
- Der Typ »char« bezeichnet Werte der kleinsten adressierbaren Einheit, die daher auch „Bytes“ genannt werden können.
Nachtrag zu Zeigern und Ketten
- Bei Methoden wie »strlen« wurde früher oft »register« für Variablen in inneren Schleifen eingesetzt
- Nachtrag zum Beispiel einer strlen -Funktion: Mögliche Verbesserung durch Aufgabe des Zählers in der Schleife zugunsten der Berechnung einer Differenz nach der Schleife, Der storage-class-specifier »register« suggests that access to the object be as fast as possible (6.7.1#4) The extent to which such suggestions are effective is implementation-defined, kein ®ister
- Nachtrag zur Lösung einer strchr-Funktion: In vielen Fällen kann es erwünscht sein, auch noch ein bestimmtes Verhalten für den Fall vorzusehen, daß das Zeichen nicht gefunden wird.
- Nachtrag zur Aufgabenstellung der Implementation einer strcpy-Funktion: Als Ziel des Kopierens wird hier Speicher benötigt, der ausreichend groß ist. Es wird aber normalerweise nicht als Aufgabe der strcpy-Funktion angesehen, diesen Speicher bereitzustellen oder auch nur zu prüfen, ob er ausreichend ist. Vielmehr soll dieser Speicher von außen an diese Funktion übergeben werden. Er kann beispielsweise als char-Reihung erhalten werden. Dabei ist es dann allerdings offen, wie sichergestellt werden kann, daß die Größe der Reihung ausreicht. Für diese Übungsaufgabe kann dieses Problem aber zunächst noch offengelassen werden, da ja nur die Implementation einer strcpy-Funktion verlangt ist, nicht aber deren Aufruf.
- Nachtrag zur Ausgabe eines Wertes vom Typ »size_t« (»stddef.h« oder »stdio.h«): Dieser Typ ist immer vorzeichenlos, daher ist die richtige Umwandlungsspezifikation (“conversion specification ”, fprintf in ISO/IEC 9899:1999 (E) ) tatsächlich »%zu«. Die Umwandlungsspezifikation »%zd« wäre für den Typ »ssize_t«, der Werte mit Vorzeichen enthält, passend; dieser Typ wird aber nicht in ISO/IEC 9899:1999 (E) definiert, sondern könnte höchstens in einzelnen C -Implementationen definiert werden. Der größte Wert vom Typ »size_t« ist »SIZE_MAX« (»stdint.h«)
- Die Differenz zweier Zeiger hat den Typ »ptrdiff_t«, mit der printf -Umwandlungsspezifikation »%td«.
- Speicher wird in char -Objekten gemessen. Daher ist »sizeof( char )« in C immer 1, und die Verwendung von »sizeof( char )« gilt manchem sogar als Zeichen von Unwissen.
- Ein Zeiger darf nur in ein Objekt zeigen, bei einer Reihung auch noch auf die nächste Position direkt hinter der Reihung. Er darf nur dereferenziert werden, wenn er auch auf ein Objekt zeigt.
Die switch -Anweisung
- 〈selection-statement 〉 ::=
- "switch" "(" 〈expression 〉 ")" 〈statement 〉.
Der ganzzahlige Ausdruck 〈expression 〉 bestimmt, daß die Ausführung bei einer bestimmten Anweisung innerhalb des Rumpfs 〈statement 〉 fortgesetzt wird. Nämlich bei einer case -Markierung mit dem Wert dieses Ausdrucks, die sich in der folgenden Anweisung befindet und zu der diese switch -Anweisung die nächste umgebende ist. Eine solche case -Markierung ist auch nur innerhalb einer switch -Anweisung erlaubt.
- 〈labeled-statement 〉 ::=
- "case" 〈constant-expression 〉 ":" 〈statement 〉.
Dabei ist die ganzzahlige 〈constant-expression 〉 ein konstanter Ausdruck.
Wird kein passender Wert gefunden, wird die Ausführung bei einer Fehlmarke (»default«) fortgesetzt, die erlaubt ist, wo auch eine case -Marke erlaubt wäre.
- 〈labeled-statement 〉 ::=
- "default" ":" 〈statement 〉.
- Beispiel
#include <stdio.h> /* printf */ void f( int const x ){} int main( void )
{ int const expr = 2; /* 1, 0 */
int const a = 1; /* a ist keine "constant expression" */
switch( expr )
{ int i = 4; f( i ); switch( i ){ case 1:; }
/* case a: i = 9; */
case 0:
i = 17;
{ case 2: i = 3; }
default:
printf( "%d\n", i ); }}
In dem obigen Programmbeispiel ist der Wert der Variablen »i« unter Umständen unbestimmt, und die Funktion »f« wird nie aufgerufen.
Sprunganweisungen für Schleifen
- 〈jump-statement 〉 ::=
- "break" ";".
Hiermit wird die nächste umgebende switch -Anweisung oder Schleife verlassen (beendet).
- Beispiel einer break -Anweisung nach einer case -Marke:
case 0:
{ i = 17;
break; }- 〈jump-statement 〉 ::=
- "continue" ";".
Hiermit wird zum Ende des Rumpfs (also oft bis zur schließenden Klammer »}«) der nächsten umgebenden Schleife gesprungen.
Die for -Anweisung
- 〈iteration-statement 〉 ::=
- "for" "(" [〈clause 〉] ; [〈condition 〉]; [〈expression 〉] ")" 〈statement 〉.
Die Klausel 〈clause 〉 wird einmalig am Anfang der Schleife ausgewertet, falls sie ein Ausdruck ist. Sie kann auch eine Deklaration sein, die dann für den Rest der Schleife gilt.
Der Ausdruck 〈condition 〉 wird wie in einer while-Schleife interpretiert. Falls er fehlt gilt er als ungleich 0.
Der Ausdruck 〈expression 〉 wird nach jeder Ausführung der Anweisung 〈statement 〉 ausgewertet, falls er vorhanden ist.
Mit »for(;;)« kann dementsprechend eine Art von Endlosschleife eingeleitet werden. Nach »#define ever (;;)« kann man auch schreiben »for ever«, was aber umstritten ist.
- Beispiel
#include <stdio.h> /* printf */ int main( void )
{ { int i = 0; while( i < 4 ){ printf( "%d\n", i ); ++i; continue; ++i; }}
for( int i = 0; i < 4; ++i ){ printf( "%d\n", i ) ;++i; continue; }}- stdout
0
1
2
3
0
2
Die do -Anweisung
- 〈iteration-statement 〉 ::=
- "do" 〈statement 〉 "while" "(" 〈expression 〉] ")" ";".
Diese Anweisung ist eine Schleife, die sich fast wie eine while -Anweisung verhält, nur daß die enthaltene Anweisung 〈statement 〉 zunächst immer einmal begonnen (und unter Umständen mehr oder weniger ganz ausgeführt) wird und die Anweisung 〈expression 〉 erst danach zum ersten Mal ausgerwertet wird, um zu ermitteln, ob eine Wiederholung folgen soll.
Sieht man ein »while« mit Semikolon, wie »while( i++ );«, so muß dies also nicht unbedingt eine while -Anweisung mit leerem Rumpf sein, es kann auch das Ende einer do -Anweisung sein.
Weitere Typen für ganze Zahlen
Ganzzahlige Datentypen und Zeigertypen werden zusammen auch als „skalare Typen“ bezeichnet (sie haben offenbar Gemeinsamkeiten, etwa die Möglichkeit, ihre Differenz anzugeben).
Die Programmiersprache C kennt zunächst folgende ganzzahlige Datentypen: »char«, »short int«, »int«, »long int«, »long long int«.
Diese können noch mit »signed« und »unsigned« modifiziert werden. Unmodiziert enthalten sie ein Vorzeichen, außer »char«, das als »signed char« oder »unsigned char« implementiert sein kann. In manchen Fällen kann die Angabe »int« entfallen: »short«, »unsigned«, »long« und »long long«.
- / Bestimmung des Verhaltens anhand der Spezifikation
- Für die beiden folgenden Anweisungen soll jeweils mit Quellenangabe ermittelt werden, welches Verhalten die Spezifikation der Programmiersprache C, ISO/IEC 9899:1999 (E), für sie festlegt. Dabei kann einer der im Web vorhandenen Entwürfe verwendet werden.
{ int i = INT_MAX; printf( "%d\n", i + 1); printf( "a\n" ); } { unsigned int i = UINT_MAX; printf( "%u\n", i + 1); printf( "a\n" ); }
Programmierstil: Vorbedingungen prüfen
Viele Funktionen der Standardbibliothek haben undefiniertes Verhalten für den Fall, daß ihre Vorbedingungen verletzt sind, also beispielsweise im Fall von Argumentwerten außerhalb eines bestimmten Bereichs. Dies erlaubt es ihnen schnell zu arbeiten, ohne erst noch ihre Vorbedingungen prüfen zu müssen.
Sollten Funktionsdefinitionen auch so geschrieben werden?
Eine Prüfung von Vorbedingungen kann dem „defensiven“ Programmieren entsprechen, aber zeitverschwenderisch sein, wenn diese außerhalb einer Schleife schon einmal geprüft wurde.
- Beispiel
if( i != 0 )while( j = 0; j < 32767; ++j )div( j, i )
Als Kompromiß können Sicherheitsprüfungen nur während der Programmentwicklung aktiviert werden.
- Beispiel
#ifdef DEBUG
if( i == 0 )printf( "Division by zero in 'div'.\n" );
#endif
Außerdem sollten Werte, die von außen in ein Programm gelangen immer mindestens einmal geprüft werden. In diesem Abschnitt ging es nur um die Frage zusätzlicher Prüfungen, für programminterne Werte, die bei korrekter Programmierung immer im richtigen Bereich liegen sollten.
Einlesen mit scanf
Die Standardfunktion »scanf« implementiert die Analyse und Umwandlung von Texten, die erst zur Laufzeit eingelesen werden.
Für die Analyse von Eingaben in länger eingesetzten Programmen ist es oft robuster, die Eingabe selber zu analysieren, wenn sich diese nicht mehr auf die von scanf erledigten Standardfälle beschränken.
Im einfachsten Fall liest man mit der Direktive »%d« ein Dezimalnumeral in ein int -Objekt und mit »%lf« ein Gleitkommanumeral in ein double -Objekt. Die Direktive »%31s« liest einen Text mit bis zu 31 Zeichen in ein char[] -Objekt, wobei das Einlesen bei Leerraum endet und an das Ende des eingelesenen Textes ein Nullzeichen »'\0'« angefügt wird.
Die Objekte werden jeweils durch ihre Adressen angegeben.
In dem folgenden Programmbeispiel stellt »fflush( stdout )« die Sichtbarkeit der zuvor ausgegebenen Textes sicher.
Falls alle Einträge korrekt gelesen werden konnten, ergibt »scanf« die Anzahl der Einträge. Ein robustes Programm wird auch nur in diesen Fall, von den Werten der Objekte so Gebrauch machen als ob das Einlesen gelungen sei.
- Beispiel
#include <stdio.h> /* printf, fflush, stdout, scanf */ int main( void )
{ int i; double x; char s[ 8 ]; double * const p = &x;
printf( "? " ); fflush( stdout );
if( 3 == scanf( "%d%lf%7s", &i, p, s ))
printf( "%d\n%f\n%s\n", i, x, s ); }- stdin
? 3 3.7
0123456789- stdout
3
3.700000
0123456
Ein häufiger Flüchtigkeitsfehler ist das Vergessen des Adreßoperators in scanf -Aufrufen. Wie man in dem Beispiel sieht, kann man aber auch nicht schematisch sagen, daß dieser Operator dort immer verwendet werden muß.
- / Bestimmung des Verhaltens anhand der Spezifikation
- Eine Funktion soll eine „Würfelzahl“ (eine Zahl zwischen 1 und 6) einlesen und zurückgeben. Bei falschen oder unverständlichen Eingaben soll der Bediener die Eingabe erneut versuchen müssen.
- / Würfelsimulation
- Ein Programm erstellt eine Statistik der Verteilung von 1000 simulierten Würfelwürfen. (Diese Aufgabe sollte eigentlich schon weiter oben stehen, da sie nichts mit Einlesen zu tun hat.)
- / Zahlenliste beliebiger Länge umkehren (schwierig)
- Einlesen einer Folge nichtnegativer Zahlen, die durch die Eingabetaste getrennt und durch eine negative Eingabe beendet werden, Ausgabe in umgekehrter Reihenfolge (Im Idealfall sollte das Programm weder eine bestimmte feste Grenze für die Anzahl der eingegebenen Zahlen festlegen, noch diese zuvor vom Bediener erfragen.)
4
6
12
-1
12
6
4- / Eine Raute
- Ein Programm liest die „Höhe“ einer Raute ein (dies ist eigentlich die Höhe der oberen Hälfte) und gibt eine entsprechende Raute mit Sternchen aus.
Eine Raute
Hoehe?
4
*
***
*****
*******
*****
***
*
Allozierter Speicher
Der Aufruf von »malloc( size )« ergibt einen Zeiger auf allozierten Speicher der Größe »size« (Diese Erklärung wurde vorläufig etwas vereinfacht).
- malloc
#include <stdlib.h>
void * malloc( size_t size );
Der Zeigertyp »void *« ist ein „neutraler Zeigertyp“, der in jeden anderen Typ gewandelt werden kann. Anders als andere Zeigertypen zeigen seine Werte nicht auf Objekte bestimmter Größe.
Da eine explizite Typwandlung bei »void *« nicht nötig ist und sie bestimmte Fehler verdecken kann, gilt sie unter Kennern als schlechter Stil, ist aber doch weitverbreitet, vielleicht auch wegen des Einflusses von C++, wo dies wieder anders ist.
- malloc, guter Stil in C
double * p = malloc( sizeof( double ));
- malloc, schlechter Stil in C
double * p =( double * )malloc( sizeof( double ));
Anwendungsgebiete Speicher wird alloziert, wenn die Größe des benötigten Speichers zur Schreibzeit noch nicht bekannt ist, oder wenn zur Schreibzeit noch nicht bekannt ist, ob dieser Speicher überhaupt benötigt wird.
- / Präsenzübung
- Ein Programm soll die prozentualen Anteil von Stimmen einer Wahl ausgeben. Da die Anzahl der Parteien zur Schreibzeit noch nicht bekannt ist, wird sie vom Benutzer eingelesen.
Anzahl der Parteien? 3
100
200
300 Prozentuale Anteile:
16.6667
33.3333
50- Zusatzaufgabe Verschönerung der Ausgabe, so daß stets zwei Nachkommastellen erscheinen, gleiche Dezimalstellen immer gleiche Spalten haben und jedem Prozentwert auch ein Prozentzeichen folgt.
- / Übung
- Eine Variante »strdup« der Funktion »strcpy« soll einen Zeiger auf eine Kopie eines (ihr als Argument übergebenen) Textes zurückliefern, die sie zuvor in der passenden Größe alloziert hat.
- / Übung (etwas schwieriger als die vorherig, da mehr Recherchen nötig)
- Eine ganze Zahl wird in einen Text gewandelt, wobei für diesen Text genau der benötigte Speicher alloziert wird. Dieser kann zunächst mit »snprintf« ermittelt werden, bevor die Wandlung dann mit »snprintf« oder »sprintf« erledigt wird.
- / Übung (Zusatzaufgabe, weniger wichtig, etwas aufwendiger)
- In einer Schleife werden zu Testzwecken int-Reihungen zufälliger Größe und mit zufälligen Inhalten erzeugt und dann von der zuvor geschriebenen Sortierfunktion sortiert. Danach prüft eine weitere Funktion ob die Reihung sortiert ist und noch dieselben Werte mit derselben Häufigkeit enthält, wie die unsortierte Reihung und berichtet gegebenenfalls gefundene Fehler.
Ressourcenverwaltung: Das Scheitern von Anforderung
Das Ergebnis von »malloc« kann auch ein Nullzeiger sein, der dann nicht dereferenziert werden darf. Er zeigt ja auf kein Objekt. Es gibt zwei Möglichkeiten, wie diese Möglichkeit berücksichtigt werden kann.
Ignorieren Wenn ein Programm für den internen Gebrauch erstellt werden soll, dann kann diese Möglichkeit bei der Programmierung ignoriert werden. Damit wird bewußt ein mangelhaftes Programm geschrieben, wenn der Aufwand für korrekte Programmierung zu groß wäre. Im besten Fall kommt es dann bei der Dereferenzierung eines Nullzeigers zu einem Programmabbruch, im schlechtesten Fall zu einem fehlerhaften Ergebnis: „If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined.“. — Diese Vorgehensweise kann als Ergebnis einer bewußten Entscheidung manchmal richtig sein. Im folgenden Programmbeispiel soll der Aufruf der Funktion »memset« den Speicher mit Nullwerten füllen, er wäre aber undefiniertes Verhalten, wenn p ein Nullzeiger wäre.
- Allozieren einer nullgefüllten char -Reihung mit Ignorieren
void * alloc( size_t const size )
{ void * p = malloc( size );
memset( p, 0, size );
return p; }
Behandeln Wenn ein Programm für den Gebrauch durch Dritte entwickelt wird oder es wichtig ist, daß das Programm sich in vorhersehbarer Weise und zuverlässig verhält (also kein durch den Programmierer vermeidbares undefiniertes Verhalten zeigt), dann muß es durch die Programmierung sichergestellt sein, daß ein Nullzeiger nie dereferenziert wird.
- Allozieren einer nullgefüllten char -Reihung mit Behandlung
void * allocate( size_t const size )
{ void * p = malloc( size );
if( p )memset( p, 0, size );
return p; }
Leider gibt es sogar neue Lehrbücher, in denen unkritisch (also ohne diese Problematik überhaupt zu diskutieren [wenn ich nichts übersehen habe]) auf die erste Weise „Ignorieren“ programmiert wird, wie etwa “Advanced data structures ” von Peter Brass. In diesem Buch finden sich passenderweise auch gleich die berüchtigten cast -Operatoren vor malloc -Aufrufen, »sizeof( char )«, »(int)'\0'« und »get_node()« in einer Deklaration (»http://www-cs.ccny.cuny.edu/~peter/dstest/ar_n_trie.c«). Teilweise ist eine ganze Generation von C -Programmierern „durch C++ verdorben“ worden – es ist nicht grundsätzlich falsch zwei (ähnliche) Sprachen zu erlernen, aber man muß dafür ungefähr das Doppelte der Zeit aufwenden, die man zum Lernen einer Sprache alleine einsetzen würde und darf nicht glauben, daß man mit der einen nun die andere schon fast kennt.
Hier ist insbesondere auch der Vorstellung „programmiersprachenunabhängiger Algorithmen“ entgegenzutreten: Es ergibt sich meist schlechter C -Code, wenn zunächst in Java „gedacht“ und dann in C geschrieben wird (oder umgekehrt), denn in C ist die Verwaltung von Speicher integraler Bestandteil von Funktionen zur Verarbeitung von Graphen (wie Listen oder Bäumen), in Java hingegen nicht. Es ist in solchen Fällen nicht möglich, einmal einen „sprachunabhängigen Algorithmus“ zu erlernen und diesen dann nur noch ohne weiteres Nachdenken in C oder Java zu formulieren.
Funktionen, die Datenverabeitungsaufgaben haben, aber nicht mit der Benutzeroberfläche eines Programms zu tun haben, also beispielsweise Rechenfunktionen, wie »sin«, werden hier auch (entsprechend der MVC-Architektur) als „Model -Funktionen“ bezeichnet.
Eine Model-Funktion kann einen Fehler nicht direkt an die Benutzeroberfläche oder Umgebung melden, da sie diese nicht kennt (nicht kennen sollte).
Eine Funktionen wie die folgende ist weniger wiederverwendbar, weil sie nicht in Programmen mit einer graphischen Benutzeroberfläche eingesetzt werden kann (wo es keinen Konsole gibt, welche printf -Ausgaben anzeigt). Die Fehlermeldung würde auch kein Sinn bei Hintergrundprogrammen ohne Benutzeroberfläche ergeben, oder bei Programmen ohne einen Bediener, der die Konsolenausgaben liest.
- Allozieren einer nullgefüllten char -Reihung mit Fehlermeldung
void * allocate_console( size_t const size )
{ void * p = malloc( size );
if( p )memset( p, 0, size );
else printf( "malloc ergab 0.\n" );
return p; }
Statt dessen sollte sie den Fehler an ihren Aufrufer melden. Die oben definierte allocate -Funktion erfüllt diese Anforderung, da sie, falls malloc einen Nullzeiger zurückliefert, selber auch einen Nullzeiger zurückliefert, woraus der Aufrufer erkennen kann, daß kein Speicher erhalten werden konnte.
Berichtsprinzip Wenn ein untergeordneter Programmteil wegen einer gescheiterten Ressourcenanforderung seine Aufgabe nicht erfüllen kann, so berichtet er dies seinem Aufrufer, der dann entscheidet, wie weiter zu verfahren ist. Dieser Bericht kann beispielsweise durch den Rückgabewert erfolgen.
Abbrechen Damit eine Model-Funktion möglichst universell (als eine Bibliotheksfunktion) in verschiedenen Programmen eingesetzt werden kann, sollte sie nicht selbständig beschließen, das ganze Programm zu beenden. (Diese Entscheidung gehört nach MVC in die Controller -Schicht einer Anwendung). Jedoch kann genau das manchmal ein sinnvoller Kompromiß zwischen „Ignorieren“ und „Behandeln“ sein, weil es einerseits undefiniertes Verhalten durch den Programmabbruch vermeidet, aber andererseits die Programmierung erleichtert, weil der sofortige Abbruch die aufwendigere Weiterleitung von Fehlerwerten überflüssig macht (jedoch sind dadurch Datenverluste möglich). Bei der folgenden Funktion kann sich der Aufrufer sicher sein, daß erfolgreich Speicher alloziert wurde, falls sie überhaupt zurückkehrt.
- Allozieren einer nullgefüllten char -Reihung mit Abbrechen
void * allocate_or_abort( size_t const size )
{ void * p = malloc( size );
if( !p )abort();
memset( p, 0, size );
return p; }
Für jedes Programmierprojekt sollte eine bewußte Entscheidung über die Art der Behandlung des Scheiterns einer Laufzeitanforderung getroffen werden. Die Vorgehensweise „Behandlung “ läßt alle Optionen offen und empfiehlt sich für hochwertige Programme und Programmteile, die oft wiederverwendet werden sollen. Die Vorgehensweise „Ignorieren “ erleichtert die Programmierung und macht den Quellcode übersichtlicher, kann aber im schlimmsten Fall zu falschen Ergebnissen führen. Die Vorgehensweise „Abbrechen “ könnte in manchen Fällen ein praktikabler Kompromiß sein. Für den Rest des Intensivkurses soll in den Programmbeispiele in der Regel die Vorgehensweise „Behandlung “ verwendet werden.
Auch andere Aufrufe, die nicht immer gelingen müssen, wie das weiter oben beschriebene Einlesen mit »scanf« können als Anforderungen in diesem Sinne verstanden werden.
Ausweglose Zustände? In manchen Fällen wird die Auffassung vertreten, daß bei bestimmten Bedingungen ohnehin keine Hilfe mehr möglich ist: Wenn ein Versuch zur Ausgabe auf die Konsole scheitert, dann kann auch keine Fehlermeldung mehr ausgegeben werden: »if( 3 != printf( "abc" ))…«?. Die Prüfung des Ergebnisses von »printf« wird tatsächlich meist weggelassen. Wenn kein Speicher mehr vorhanden ist, dann wird ein Programmteil zur Fehlerbehandlung vielleicht auch daran scheitern. Doch kann bei Mangel an alloziertem Speicher immer noch ausreichend automatischer Speicher verfügbar sein.
Ressourcenverwaltung: Das Zurückgeben von Ressourcen
Ressourcen sollten zur Wiederverwendung zurückgegeben werden, sobald sie nicht mehr benötigt werden.
Allozierter Speicher kann mit »free« zurückgegeben werden. Obwohl diese Funktion auch einen Nullzeiger akzeptiert, ist es im allgemeinen (bei anderen Typen von Ressourcen, die später behandelt werden sollen) nötig, darauf zu achten, Ressourcen nur dann wieder freizugeben, wenn sie zuvor auch erhalten wurden.
- Naives Allozieren, Nutzen und Zurückgeben einer Ressource
{ int * p = malloc( sizeof( int ));
*p = 7;
free( p ); }- Korrektes Allozieren, Nutzen und Zurückgeben einer Ressource
{ int * p = malloc( sizeof( int ));
if( p )
{ *p = 7;
free( p ); }}
Auch diese Aufgabe kann machmal ignoriert werden: Wenn ein Programm während seiner gesamten Laufzeit nur wenig Speicher anfordert oder nur kurzzeitig läuft, dann ist die explizite Rückgabe des Speichers mit »free« nicht so wichtig, weil der mit »malloc« angeforderte Speicher am Ende des Programmlaufs normalerweise automatisch wieder freigegeben wird, auch wenn das Programm »free« nicht selber aufruft. Aber bei Programmen, die viel Speicherbedarf haben und die längere Zeit laufen sollen ist die möglichst frühzeitige Speicherfreigabe wichtig, damit der Speicher nicht unnötig blockiert wird. Genauso sollten Programmteile in Bibliotheken, die auch in solchen Programmen verwendbar sein sollen, diese Speicherfreigabe erledigen oder ermöglichen.
Wenn ein Programm mehrere Ressourcen anfordert, dann muß es genau diejenigen, die es erfolgreiche erhalten hat, auch wieder freigeben, aber es darf im allgemeinen nicht versuchen, Ressourcen freizugeben oder zu benutzen, die es gar nicht erfolgreich erhalten hat (»free« toleriert den Aufruf mit einem Nullzeiger, was aber bei anderen Ressourcenfreigaben nicht immer entsprechend sein muß). Die Sicherstellung dieser Regeln wird auch als „Ressourcenverwaltung“ bezeichnet. Sie ähnelt dem Umgang mit einem Buch, das einer Leihbibliothek gehört: Man darf bei der Planung nicht einfach davon ausgehen, daß man das Buch entleihen kann (weil es schon an jemand anders verliehen worden sein könnte) und man muß es nach Ablauf einer bestimmten Frist wieder zurückgeben.
Beim Anfordern vieler Ressourcen kann deren Verwaltung unübersichtlich werden. Leider kann sie den C -Programmierer in manchen Programmen ziemlich belasten, so daß die korrekte Ressourcenverwaltung manchmal die Hauptlast einer Programmieraufgabe ausmacht.
Für jedes Programmierprojekt sollten einheitliche Regeln zum Umgang mit Ressourcen festgelegt und beachtet werden.
Ein Problem bei der Durchsetzung eines einheitlichen Stils zur Verwaltung von Ressourcen können verwendete Bibliotheken von dritter Seite sein, bei denen man nicht beinflussen kann, wie sie mit Ressourcen umgehen. Dann sollte der Stil wenigstens für bestimmte Projektteile festgelegt werden, und außerdem sollte man sich für jede Bibliothek und jeden Programmteil bewußt sein, welcher Stil zum Umgang mit Ressourcen dort gepflegt wird.
Ressourcenverwaltung: Fingerzeige für die Wahl von Regeln
[Neu]
In einem „Hauptprogramm“ reicht es den für eine jeweilige Situation als angemessenen Aufwand zu betreiben. Beispielsweise könnte ein Programm beim Scheitern einer Anforderung einfach abgebrochen werden.
Bestimmte Ressourcen werden am Programmende automatisch freigegeben: Dies gilt für geöffnete Datei und meistens auch für allozierten Speicher. Es gibt jedoch auch Ressourcen, die bei Programmende nicht automatisch freigegeben werden (etwa Ressourcen, die von anderen Prozessen erhalten wurden, weil diese Prozesse ja noch weiterlaufen). Sie müssen dann auf jeden Fall ordentlich verwaltet werden. Da dies nicht mehr möglich ist, wenn das Programm an einigen Stellen abgebrochen wird, empfiehlt sich dann eine ordentliche Buchführung und Verwaltung von Ressourcen. Das Offenhalten dieser Möglichkeit von Anfang an kann eine Motivation dafür sein, die Ressourcenverwaltung gleich ordentlich zu programmieren, auch wenn dies (noch) nicht als nötig erscheint.
Funktionen, die in unbekannten Kontexten wiederverwendet werden können sollen, sollten möglichst „regelneutral“ sein, also dem Klienten keine Regeln aufzwingen. Sie dürfen Fehler also weder ignorieren noch das Programm abbrechen, sondern sollten Transaktionen sein (die entweder alle nötigen Ressourcen, die einen Aufruf überdauern sollen, beschaft haben oder keine), die den Klienten bei ihrem Scheitern nur informieren.
Dateizugriffe
- Kopieren von stdin nach stdout mit »getchar« und »putchar«.
- #include <stdio.h>
EOF - #include <stdio.h>
int getchar(void);
EOF bei Fehler oder Stromende. - #include <stdio.h>
int putchar(int c);
EOF bei Fehler, das geschriebene Zeichen sonst.
Inhalt des 7. Termins (2009-12-16)
Das Schreiben einer Funktion zum Kopieren wird etwas später wieder aufgegriffen werden.
Nachtrag zu Ressourcen
- Allgemein ist eine Ressources jedes Ding, das aus einer endlichen (beschränkten) Gesamtheit von Dingen zur Verwendung entnommen werden kann. Da Ressourcen beschränkt sind, muß man bei einem Entnahmeversuch damit rechnen, keinen Ressource zu erhalten, und sollte eine entnommene Ressource nach Gebrauch frühestmöglich wieder zurückgeben.
- Einige der bisher verwendeten Wörter schreiben sich im Englischen oder im Deutschen jeweils manchmal mit Doppelbuchstaben und manchmal nicht, was gelegentlich zu Schreibfehlern führt:
- “resource ”
- „Ressource“
- “model ”
- „Modell“
- “controller ”
- „Steuerung“
Übungsaufgaben
- / Zur Vertiefung von Anweisungen und Ausdrücken:
void pr( int const x )
{ if( x == 2 )printf( "a\n" );
if( x == 4 )printf( "b\n" );
else printf( "c\n" ); }- Die oben definierte Funktion »pr« soll die Zeile »a« ausgeben, wenn x gleich 2 ist; die Zeile »b«, falls x gleich 4 ist; und die Zeile »c« sonst.
Welcher Fehler wurde gemacht?
Nachdem dieser Fehler korrigiert ist: - Können Sie eine Definition einer Funktion, die dasselbe Verhalten zeigt, aber die Wiederholung von »x« vermeidet, schreiben?
- Können Sie eine Definition einer Funktion, die dasselbe Verhalten zeigt, aber die Wiederholung von »printf« vermeidet, schreiben (Hierbei darf »x« dann wieder wiederholt werden)?
- / Präsenzübung, Lesen der Definition einer »strcpy«-ähnlichen Funktion
#include <stdio.h> void strcpy_( char * p, char const * q ){ while( *p++ = *q++ ); } int main( void )
{ char target[ 4 ]; char const * const source = "abc";
strcpy_( target, source );
printf( "%.4s\n", target ); }- Da die Namen der Standardbibliotheksfunktionen reserviert sind, wurde ein etwas anderer Name als »strcpy« verwendet.
- Können Sie erklären, wieso die Definition dieser Funktion eine Kette kopieren kann?
- Eine alternative Möglichkeit zur Definition (nur C99):
void strcpy_( char * restrict p, char const * restrict q ){ while( *p++ = *q++ ); }
Die Verwendung von »restrict« in obiger Deklaration bedeutet, daß bei jeder Ausführung dieser Funktion auf einen Textblock, auf den durch einen der beiden Parameter zugegriffen wird, nicht auch durch den anderen Parameter zugegriffen wird. Damit kann »strcpy« nicht verwendet werden, um überlappende Objekte zu kopieren, aber der Compiler kann bestimmte Optimierungen durchführen. - Ein Programm mit »restrict« arbeitet auch weiterhin korrekt, wenn »restrict« überall entfernt wird. Es dient nur der Optimierung. Daher ist ein aktiver Gebrauch von »restrict« nicht nötig, um korrekte C -Programme schreiben zu können. Da dieses Schlüsselwort aber in der Dokumentation verwendeter Funktionen vorkommen kann, sollte es zumindest bekannt sein.
- Warum könnte das Kopieren einander überlappender Objekte mit »strcpy_« (auch ohne Verwendung von »restrict«) Schwierigkeiten bereiten?
- / Präsenzübung, Schreiben der Definition einer »strcmp«-ähnlichen Funktion
- Es soll eine (etwas vereinfachte) Variante der Funktion »strcmp« definiert werden, die genau dann 0 ergibt, wenn die zu vergleichenden Texte gleich sind.
- / Berühmte Themen der C -Programmierung: Duff's Device (Zusatzaufgabe)
- Bereiten Sie eine kurze mündliche Erklärung der Bedeutung des Begriffs “Duff's Device ” vor.
VLAs (nur C99)
Bei der Einführung der Funktion »malloc« wurde als Motivation erklärt, daß damit Speicherbereiche angefordert werden können, deren Größe erst zur Laufzeit bekannt ist. Tatsächlich ist dies seit der aktuellen C -Version dann nicht unbedingt nötig, da sie mit den “variable length arrays ” Reihungen bietet, deren Größe erst zur Laufzeit festgelegt wird. Jedoch gibt es weiterhin noch genug Fälle, in denen malloc -Aufrufe nötig sind:
- Wenn die Lebensdauer der Objekte nicht auf einen Block beschränkt sein soll
- Wenn eine C -Implementation verwendet wird, welche den aktuellen C -Standard noch nicht unterstützt
- Wenn der Speicherbereich für automatische Objekte geschont werden soll
Dateizugriffe
- / Einfaches Kopieren
- Schreiben Sie ein Programm das solange wie möglich Zeichen von stdin nach stdout kopiert. (Das Kopieren von Zeichen ist dann nicht mehr möglich, wenn das Ende [»EOF«] des Eingabestroms erreicht wurde.)
- Nennen Sie diese Programm »copy« und benutzen sie es zum Kopieren von einer Datei in eine andere Datei, zum Schreiben der Tastatureingaben in eine Datei, zum Weiterleiten der Tastatureingaben in das Programm »cat -n« und zum Ausgeben des Inhalt eines Datei.
- Ändern Sie das Programm so ab, daß es nur Buchstaben kopiert (alle anderen Zeichen werden sozusagen „gelöscht“).
- Verwenden Sie das Programm dann in vim , um die Buchstaben in einem bestimmten Zeilenbereich zu extrahieren.
Weitere Inhalte
- Übungsaufgaben? (avg, Zahlenfolge invertieren).
Formen der Interaktion zwischen Prozessen
Schon behandelt wurde das Umlenken von »stdin« und »stdout« in Kommandozeilen.
Die Ausgabe in Gravis eingeschlossener Kommandos wird in Kommandozeilen eingefügt.
- Konsole
$ date
Sa 19. Dez 19:01:11 CET 2009 $ echo alpha
alpha $ echo `date`
Sa 19. Dez 19:01:21 CET 2009 $ echo `echo alpha`
alpha $ gcc -S test.c $ ls -rt | tail -1
test.s $ head -1 `ls -rt | tail -1`
.file "test.c"
Ein Programm kann einen ganzzahligen Kennwert zurückgeben, der bei erfolgreicher Ausführung normalerweise 0 ist. Dieser kann dann ignoriert, angezeigt oder in Skripten verwendet werden.
- sh
$ echo alpha >tmp $ grep alpha tmp >/dev/null
$ echo $?
0 $ grep beta tmp >/dev/null
$ echo $?
1- csh
% echo $status
1- Windows 9X
C:\>command /Z Microsoft(R) Windows 98
(C)Copyright Microsoft Corp 1981-1999.
Rückgabecode (ERRORLEVEL): 0
ACHTUNG: COMMAND.COM vorübergehend neu geladen C:\>echo alpha >tmp C:\>grep alpha tmp >NUL
Rückgabecode (ERRORLEVEL): 0 C:\>grep beta tmp >NUL
Rückgabecode (ERRORLEVEL): 1 C:\>exit
Perl hat als Skriptsprache den Vorteil, unter allen Linux -Kommandozeileninterpretierern und auch unter Windows gleich zu sein.
- Perl
use strict;
use warnings; print `grep -q alpha tmp`;
print $? >> 8, "\n"; print `grep -q beta tmp`;
print $? >> 8, "\n"; print system( "grep -q alpha tmp" )>> 8, "\n";
print system( "grep -q beta tmp" )>> 8, "\n";- STDOUT
0
1
0
1
Ein C -Programm kann einen Ausgangskennwert (“exit status ”) zurückgeben, indem es diesen als Ergebnis der Funktion »main« zurückgibt. Um portabel Erfolg oder Scheitern zu signalisieren können dafür zwei Makros »EXIT_SUCCESS« und »EXIT_FAILURE« durch »#include <stdlib.h>« definiert werden. Ein Programm kann auch mit »exit« verlassen werden, was aber etwas schlechterer Stil wäre. Wird ein Programm mit »abort« verlassen wird ein Fehlerwert zurückgegeben, der Scheitern anzeigt.
- Beispiel
#include <stdlib.h> /* abort, exit, EXIT_SUCCESS, EXIT_FAILURE */ int main( void )
{ abort();
exit( EXIT_FAILURE );
return EXIT_SUCCESS; }
Auch in C selber kann der Wert eines Prozesses ermittelt werden.
- C
#include <stdlib.h> /* system, EXIT_SUCCESS */
#include <stdio.h> /* printf */ int main( void )
{ printf( "%d\n", system( "grep -q alpha tmp" )>> 8 );
printf( "%d\n", system( "grep -q beta tmp" )>> 8 );
return EXIT_SUCCESS; }- stdout
0
1
»system«
Hinweis Alle Beschreibungen von Eigenschaften der Programmiersprache C, insbesondere auch von Funktionen und anderen Standardentitäten, können in diesem Text vereinfacht worden sein. Eine ausführlichere und genauere Beschreibung findet sich ja in der C -Sprachspezifikation.
- Synopsis
#include <stdlib.h>
int system(const char *string);- Wirkung Übergibt des an einen implementationsspezifischen Kommandointerpretierer.
- Wert Ein implementationsspezifizierter Wert.
- Beispiel »sytem( "alpha" );« gibt den Text »alpha« aus.
Kommandozeilenargumente
int main( int argc, char *argv[] ){ /* ... */ }
argc
Gibt die Anzahl vorhandener Kommandozeilenargumente an, falls positiv enthalten diese Programmnamen (»argv[ 0 ]«) sowie Kommandozeilenargumente (»argv[ 1 ]« bis »argv[ argc - 1 ]«)
»argv« ist als Zeiger „ein Zeiger auf einen Zeiger“, jedoch kann man diese Komplikation durch „ein Zeiger auf eine Kette“ anschaulicher formulieren. Für einen Anfänger erscheinen „Zeiger auf Zeiger auf Zeiger …“ vielleicht als verwirrend, wenn man sich einmal an sie gewöhnt hat, dann sind sie aber nicht schwieriger zu verstehen als beispielsweise Unterverzeichnisse („Verzeichnisse von Verzeichnissen“).
- Es gilt laut ISO/IEC 9899:1999 (E)
- »argc >= 0«
- »argv[ argc ]« ist ein Nullzeiger.
- / Einfacher „Taschenrechner“
- Ein Programm soll die Summe der angegebenen Argumente ausgeben.
./a.out 12 34 56 102
- Zum Umwandeln der Ketten in int -Werte ist die Funktion »sscanf« nützlich.
#include <stdio.h>
/* ... */ int i; sscanf( "123", "%d", &i );