Ubungspaket 28 ¨
Module und getrenntes ¨ Ubersetzen
Ubungsziele: ¨
1. Verteilen von Programmteilen auf mehrere Dateien 2. Richtige Verwendung der Header-Dateien
3. Richtiger Umgang mit dem C-Compiler Skript:
Kapitel: 55 und 39 sowie insbesondere auch ¨ Ubungspaket 17 Semester:
Wintersemester 2021/22 Betreuer:
Thomas, Tim und Ralf Synopsis:
Im Gegensatz zu vielen klassischen Programmiersprachen bietet C die
M¨ oglichkeit, das Programm auf mehrere Dateien zu verteilen. Diese
Verteilung gestattet es, nur diejenigen Dateien zu ¨ ubersetzen, die sich
auch wirklich ge¨ andert haben; die anderen .o-Dateien k¨ onnen bleiben,
wie sie sind. Diese Option f¨ uhrt zu ¨ ubersichtlichen Quelltext-Dateien
und kann die notwendige ¨ Ubersetzungszeit bei großen Programmen
deutlich reduzieren. Allerdings tritt beim Aufteilen des Quelltextes auf
mehrere Dateien das Problem der Konsistenzhaltung auf: es kann vor-
kommen, dass eine Funktion in einer Datei anders definiert ist, als sie
in einer anderen Datei aufgerufen wird. Hier hilft der richtige Um-
gang mit den Header-Dateien und den #include-Direktiven, was wir
im Rahmen dieses ¨ Ubungspaketes probieren wollen.
Teil I: Stoffwiederholung
Aufgabe 1: Fragen zum Compiler
Wie heißen die vier Teilprogramme, die der Compiler nacheinander aufruft?
1. Pr¨aprozessor 2. (eigentlicher) Compiler
3. Assembler 4. Linker/Binder
Welcher Teil des Compilers bearbeitet die #include-Direktiven? Der Pr¨aprozessor Welche Phasen beinhaltet der eigentliche ¨Ubersetzungsvorgang? Phasen 1-3 Mittels welcher Option h¨ort der Compilervor dem Binden auf? -c
Gib hierf¨ur ein Beispiel. gcc -c datei.c
In welcher Datei landet das Ergebnis der eigentlichen ¨Ubersetzung? datei.o
Was ”weiß“ der Compiler nach dem ¨Ubersetzen von datei.c? Rein garnichts.
Nehmen wir nun an, wir w¨urden zwei Dateien ¨ubersetzen. Was
”weiß“ der Compiler von der ersten Datei, beim ¨Ubersetzen der zweiten, wenn wir folgende Kommandos verwenden?
gcc -c datei-1.cund gcc -c datei-2.c Absolut nichts gcc -c datei-1.c datei-2.c Ebenso wenig
Erkl¨are nochmals kurz in eigenen Worten, was die #include-Direktive macht.
Der Compiler ersetzt die #include-Direktive 1:1 durch die angegebene Datei.
Vervollst¨andige die beiden folgenden Beispiele:
#include-Direktive Effekt
#include <math.h> F¨ugt die Datei math.h ein.
#include "vowels.h" F¨ugt die Datei vowels.h ein; diese wird zun¨achst im aktuellen Verzeichnis gesucht.
Teil II: Quiz
Aufgabe 1: ¨ Ubersetzen und Binden (Linken)
Im Folgenden gehen wir davon aus, dass wir eine Reihe von Dateien haben und diese mittels des C-Compilers ¨ubersetzen und/oder binden wollen. in der ersten Spalte steht jeweils das auszuf¨uhrende Kommando. In der zweiten soll jeweils stehen, welche Dateien ¨ubersetzt werden. In der dritten Spalte soll stehen, ob ein lauff¨ahiges Programm zusammengebunden wird und wie ggf. dessen Name lautet. Vervollst¨andige die fehlenden Spalten.
Kommando ubersetzte Dateien¨ gebundenes Programm
gcc datei-1.c datei-1.c a.out
gcc datei-1.c -o badman datei-1.c badman
gcc -c a.c b.c a.c, b.c ----
gcc a.o b.o ---- a.out
gcc -o temperatur x.c y.o x.c (y.ogibt es schon) temperatur gcc -c inge.c peter.c inge.c, peter.c ----
gcc -o supi supi.c supi.c supi
Teil III: Fehlersuche
Aufgabe 1: Eine einfache verteilte Anwendung
Chefprogrammierer Dr. Modell-Hammer hat versucht, eine erste kleine verteilte An- wendung zu schreiben. Dabei hat er folgende Struktur im Sinn gehabt:
test.c main()
hello.h hello.c hello()
¨Ubersetzen:
gcc -x test test.c hello.h hello.c
In dieser Struktur befindet sich in der Datei test.c das Hauptprogramm, das seinerseits die Funktion hello() aufruft, die einen netten Begr¨ußungstext ausgibt. Diese Funktion gibt den Geburtstag, der als Struktur definiert ist, an die aufrufende Stelle zur¨uck. Leider hat auch Dr. Modell-Hammerein paar kleine Fehler gemacht . . . er ist desperate . . . Das Hauptprogramm: test.c
1 # i n c l u d e < s t d i o . h >
2 # i n c l u d e " h e l l o . c "
3
4 int m a i n ( int argc , c h a r ** a r g v )
5 {
6 s t r u c t b i r t h d a t e b i r t h = h e l l o ( " f r i d a " ) ; 7 p r i n t f ( " my b i r t h d a y is : % d . % d . % d \ n " ,
8 b i r t h . day , b i r t h . month , b i r t h . y e a r ) ;
9 }
Die Header-Datei: hello.h
1 s t r u c t b i r t h d a t e h e l l o ( c h a r * n a m e ) ; Das Modul: hello.c
1 # i n c l u d e < s t d i o . h >
Zeile Fehler Erl¨auterung Korrektur hello.c: 4 struct
...
Das geht leider schief. Der Compiler kann die Datei hello.c einwandfrei ¨ubersetzen. Da aber im Regelfalle alle anderen Dateien nicht eine .c sondern eine .h-Datei einbinden, bekommt der Compiler dort nicht mit, was struct birthdate sein soll. Mit anderen Worten: Diese Definition geh¨ort in die .h-Datei, damit der Compiler auch bei den anderen.c-Dateien weiß, was mitstruct birthdate gemeint ist.
Verschieben nach
hello.h
. . . .
hello.h: 1 Fehlende Definition
Wie eben erl¨autert, muss hier die Definition f¨ur die Struktur birthdate hin.
dito.
. . . .
test.c: 2 hello.c Das Einbinden einer anderen.c-Datei wollen wir ja gerade nicht. Dann h¨atten wir hier gleich alle Zeilen der Datei hello.c hinschreiben k¨onnen.
Wir wollen ja gerade, dass die C-Anweisungen in getrennten Dateien bleiben. Daher bindet man hier ¨ublicherweise eine Header-Datei.hein. Diese dient dann dem Compiler, damit er weiß, was los ist und somit leichter Fehler entdecken kann, was wiederum uns bei der Programmentwicklungsehr hilft.
dito.
. . . .
Kommando hello.h zu viel
Beim Aufruf des Compilers m¨ussen wir die .c- Dateien angeben; die .h-Dateien werden durch die #include-Direktiven vom C-Compiler auto- matisch eingebunden.
hello.h streichen
Aufgrund seiner L¨ange haben wir das komplette Programm auf der n¨achsten Seite abge- druckt.
Das Hauptprogramm: test.c 1 # i n c l u d e < s t d i o . h >
2 # i n c l u d e " h e l l o . h "
3
4 int m a i n ( int argc , c h a r ** a r g v )
5 {
6 s t r u c t b i r t h d a t e b i r t h = h e l l o ( " f r i d a " ) ; 7 p r i n t f ( " my b i r t h d a y is : % d . % d . % d \ n " ,
8 b i r t h . day , b i r t h . month , b i r t h . y e a r ) ;
9 }
Die Header-Datei: hello.h
1 s t r u c t b i r t h d a t e { int day ; int m o n t h ; int y e a r ; };
2
3 s t r u c t b i r t h d a t e h e l l o ( c h a r * n a m e ) ;
Das Modul: hello.c
1 # i n c l u d e < s t d i o . h >
2 # i n c l u d e " h e l l o . h "
3
4 s t r u c t b i r t h d a t e h e l l o ( c h a r * n a m e )
5 {
6 s t r u c t b i r t h d a t e b i r t h ;
7 p r i n t f ( " h e l l o % s , my d a r l i n g !\ n " , n a m e ) ;
8 b i r t h . day = 30; b i r t h . m o n t h = 2; b i r t h . y e a r = 1 9 9 1 ;
9 r e t u r n b i r t h ;
10 }
Kommando zum ¨Ubersetzen: gcc -o test test.c hello.c
Teil IV: Anwendungen
Ziel dieses ¨Ubungspaketes ist es, ein kleines Programm zu entwickeln, das auf zwei Da- teien verteilt ist und dennoch zu einem lauff¨ahigen Programm zusammengebunden wird.
Um keine weiteren Probleme zu erzeugen, verwenden wir das Programm zum Z¨ahlen von Vokalen aus ¨Ubungspaket 24.
Vorbemerkung
Bevor wir mit der Arbeit loslegen, wiederholen wir hier als Lehrk¨orper nochmals drei wesentliche Punkte aus dem Skript:
1. Dem Compiler ist es es v¨ollig egal, wo sich welche Funktionen befinden. Er muss aber wissen, in welchen Dateien sie kodiert sind, damit er sie ¨ubersetzen und am Ende zu einem Programm zusammenbinden kann.
2. Wenn der Compiler mehrere Dateien ¨ubersetzt, egal ob mittels eines oder mehrerer Kommandos, dann ¨ubersetzt er die angegebenen Dateien nacheinander. Nach dem Ubersetzen jeder einzelnen Datei schreibt er das Ergebnis in eine Ergebnis-Datei¨ (meist eine.o-Datei) und vergisst sofort alles! Das bedeutet folgendes: Der Compiler merkt sich nicht irgendwelche Definitionen und/oder Funktionsk¨opfe; wir m¨ussen selbst daf¨ur sorgen, dass er alles zur richtigen Zeit mitbekommt.
3. Die Verwendung von .h-Dateien ist nicht notwendig aber hilft uns und dem Com- piler. In einer .h-Datei stehen ¨ublicherweise Konstantendefinitionen (beispielswei- se #define SIZE 10) und Funktionsdeklarationen (beispielsweise int my ia prt(
FILE *fp, int *a, int size );), die auch Prototypen genannt werden. Durch diese Deklarationen kann der Compiler Inkonsistenzen feststellen und uns diese mit- teilen, wodurch die Programmentwicklung f¨ur uns stark vereinfacht wird.
Aufgabe 1: Getrenntes ¨ Ubersetzen am Beispiel
1. Aufgabenstellung
Wir wollen wieder die Anzahl Vokale z¨ahlen, die sich in einer Zeichenkette befinden.
Damit wir nicht von vorne anfangen m¨ussen, greifen wir auf die Programme aus den Ubungspaketen¨ 24 (Anwendungsteil, Aufgaben 2) und 25 zur¨uck. Aber diesmal pa- cken wir die damals entwickelte Funktionint vokale( char * str )in einegeson- derte Datei. Beide Dateien wollen wir getrennt ¨ubersetzen und zu einem Programm zusammenbinden. Das Testen erledigen wir mittels desargc/argv-Mechanismus.
Aufgabe : Trennen von Funktion (Funktionalit¨at) und Hauptprogramm in meh- rere Dateien
Eingabe : keine; die Testdaten werden ¨uberargc/argv ¨ubergeben Ausgabe : Zahl der Vokale je Zeichenkette
Sonderf¨alle : keine 3. Entwurf
Da wir diesmal die
”Programmzeilen“ trennen wollen, ben¨otigen wir ein paar ¨Uber- legungen zum Thema Entwurf, wobei wir ein wenig helfen wollen ;-)
Wir wollen also das Hauptprogramm (main) vom Z¨ahlen der Vokale (vokale()) tren- nen. Ferner wollen wir sicherstellen, dass der Compiler ¨uberall die selbe Vorstellung bez¨uglich Funktionstyp und Parameter (Signatur) von der Funktion vokale() hat.
Wie viele Dateien ben¨otigen wir? Drei Dateien
Definiere die ben¨otigten Dateinamen: main.c,vokale.c und vokale.h 4. Implementierung
Hier brauchen wir uns keine weiteren Gedanken machen, da wir bereits alle notwen- digen Programmzeilen in den ¨Ubungspaketen 24und 25 entwickelt haben.
5. Kodierung
Zuerst die Schnittstelle vokale.h:
1 /*
2 * d a t e i : v o k a l e . h
3 * das h i e r ist die s c h n i t t s t e l l e f u e r v o k a l e . c 4 */
5
6 int v o k a l e ( c h a r * str ) ; // t h a t ’ s it ! s i m p l e as t h i s Da wir diese Datei jetzt ¨uberall mittels #include "vokale.h" einbinden, kann der
Nun die Implementierung der Funktion vokale():
1 /*
2 * d a t e i : v o k a l e . c
3 * das ist die i m p l e m e n t i e r u n g der f u n k t i o n v o k a l e () 4 */
5
6 # i n c l u d e " v o k a l e . h "
7
8 int v o k a l e ( c h a r * str )
9 {
10 int cnt ;
11 if ( str != 0 ) // if ( str ) w o u l d be f i n e as w e l l
12 {
13 for ( cnt = 0; * str ; str ++ )
14 if ( * str == ’ a ’ || * str == ’ A ’ ||
15 * str == ’ e ’ || * str == ’ E ’ ||
16 * str == ’ i ’ || * str == ’ I ’ ||
17 * str == ’ o ’ || * str == ’ O ’ ||
18 * str == ’ u ’ || * str == ’ U ’ )
19 cnt ++;
20 }
21 e l s e cnt = -1;
22 r e t u r n cnt ;
23 }
Und schließlich das Hauptprogramm main():
1 /*
2 * d a t e i : m a i n . c
3 * h i e r b e f i n d e t s i c h das h a u p t p r o g r a m m 4 */
5
6 # i n c l u d e < s t d i o . h >
7 # i n c l u d e " v o k a l e . h " // n i c h t v o k a l e . c !!!
8
9 int m a i n ( int argc , c h a r ** a r g v )
10 {
11 int i ;
12 for ( i = 1; i < a r g c ; i ++ )
13 p r i n t f ( " str = ’% s ’ v o w e l s =% d \ n " ,
14 a r g v [ i ] , v o k a l e ( a r g v [ i ] ) ) ;
15 }