Programmierung in C – Teil II: Umfassende Zusammenfassung für Einsteiger
Diese Zusammenfassung richtet sich an C-Anfänger und bietet einen ausführlichen Überblick über die Inhalte des zweiten Teils der Vorlesung “Programmierung in C”. Ziel ist es, die Konzepte verständlich und strukturiert zu vermitteln, sodass auch Laien einen Einstieg in die Materie finden.
Kurzzusammenfassung
- Vom Quelltext zur ausführbaren Datei:
- Übersetzungsprozess:
- Präprozessor verarbeitet Direktiven wie
#include
,#define
.- Compiler übersetzt den präprozessierten Code in Assemblercode.
- Assembler wandelt Assemblercode in Objektdateien (
.o
) um.- Linker verbindet Objektdateien und Bibliotheken zu einer ausführbaren Datei.
- Bibliotheken werden während des Linkens eingebunden.
- Die GNU Compiler Collection (GCC):
- Führt standardmäßig alle Phasen des Übersetzungsprozesses aus.
- Wichtige Optionen:
-c
: Nur Kompilieren, keine Verlinkung.-E
: Stoppt nach dem Präprozessor.-S
: Erzeugt Assemblercode.-o <Dateiname>
: Legt den Namen der Ausgabedatei fest.-Wall -Wextra
: Aktiviert ausführliche Warnungen.-g
: Fügt Debugging-Informationen hinzu.-std=c99
: Legt den zu verwendenden C-Standard fest.- Modularisierung in C:
- Ziel: Aufteilung in unabhängige Module mit klar definierten Schnittstellen.
- Vorteile:
- Erhöhte Übersichtlichkeit und Wartbarkeit.
- Erleichtert Zusammenarbeit und Wiederverwendung von Code.
- Headerdateien (
.h
):
- Enthalten Deklarationen von Funktionen, Variablen, Makros und Typen.
- Werden mit
#include
in Quellcodedateien eingebunden.- Include Guards:
- Verhindern Mehrfacheinbindungen.
- Syntax:
- Alternative:
#pragma once
(einfachere Syntax, aber nicht standardisiert).- Erstellen eigener Headerdateien:
- Deklaration von Funktionen und Makros in der Headerdatei.
- Definition der Funktionen in der entsprechenden
.c
-Datei.- Beispiel:
geometrie.h
: Enthält Funktionsprototypen und Makros.geometrie.c
: Implementiert die Funktionen.main.c
: Nutzt die Funktionen ausgeometrie.h
.- Bibliotheken in C:
- Statische Bibliotheken (
.a
):
- Werden zur Kompilierzeit in das Programm eingebunden.
- Erstellung mit
ar rcs libname.a objektdateien
.- Dynamische Bibliotheken (
.so
):
- Werden zur Laufzeit geladen.
- Erstellung mit
gcc -shared -o libname.so objektdateien
.- Einbindung von Bibliotheken:
- Beim Kompilieren mit
-L<Pfad>
und-l<name>
angeben.- Beispiel:
gcc main.c -L. -lmymath -o main
.LD_LIBRARY_PATH
: Umgebungsvariable für dynamische Bibliothekenpfade.- Typische Linkerfehler und Behebung:
- Fehlende Implementierung:
- Fehler:
undefined reference to 'funktion'
.- Lösung: Fehlende
.c
-Datei beim Kompilieren angeben.- Fehlende Bibliothek:
- Fehler:
cannot find -lbibliothek
.- Lösung: Korrekte Pfadangabe mit
-L
und Bibliotheksname mit-l
.- Makefiles und Automatisierung:
- Zweck: Automatisieren das Kompilieren und Linken von Programmen.
- Grundstruktur:
- Beispiel-Makefile:
- Verwendung von Variablen (
CC
,CFLAGS
).- Definition von Zielen (
all
,clean
).- Regeln für das Erstellen von Objektdateien und der ausführbaren Datei.
- Prozesse in Unix/Linux:
- Definition: Ein in Ausführung befindliches Programm mit eigenen Ressourcen.
- Prozesshierarchie:
- Jeder Prozess (außer
init
) hat einen Elternprozess.- Waisenprozesse: Elternprozess beendet, Prozess wird von
init
adoptiert.- Zombieprozesse: Beendet, aber nicht von Elternprozess abgeholt.
- Prozesskontrolle mit
fork()
:
pid_t fork(void);
erzeugt einen Kindprozess.- Rückgabewerte:
- Elternprozess: PID des Kindprozesses (> 0).
- Kindprozess: 0.
- Fehlerfall: -1.
- Verhalten:
- Kindprozess ist Kopie des Elternprozesses.
- Beide Prozesse setzen Ausführung nach
fork()
fort.- Beispiele:
- Mehrfaches
fork()
erhöht die Anzahl der Prozesse exponentiell.- Fallunterscheidung anhand des Rückgabewerts ermöglicht unterschiedliche Logik.
- Die
exec
-Familie von Funktionen:
- Zweck: Ersetzen des aktuellen Prozesses durch ein anderes Programm.
- Varianten:
execl()
,execv()
,execvp()
, etc.- Verwendung:
- Wird häufig nach einem
fork()
im Kindprozess aufgerufen.- Rückgabeverhalten:
- Bei Erfolg kehrt die Funktion nicht zurück.
- Bei Fehler Rückgabe von -1.
- Beispiel:
- Kindprozess ersetzt sich selbst durch Aufruf von
execlp()
und führt ein anderes Programm aus.- Threads in C:
- Definition: Leichtgewichtige Prozesse innerhalb eines Prozesses.
- Unterschied zu Prozessen:
- Teilen sich den gleichen Adressraum und Ressourcen.
- Geringerer Overhead beim Erzeugen und Verwalten.
- POSIX-Threads (
pthread
):
- Thread-Erstellung:
pthread_create(&thread, NULL, funktion, arg);
- Thread-Beendigung:
pthread_exit(retval);
- Thread-Synchronisation:
- Warten auf Thread:
pthread_join(thread, &retval);
- Notwendigkeit von Synchronisationsmechanismen (z. B. Mutexes) bei gemeinsam genutzten Daten.
- Beispiel:
- Erzeugen eines Threads, der eine Funktion ausführt, und Warten auf dessen Beendigung.
- Weiterführende Informationen:
- Dokumentation:
- Manpages (
man
) für detaillierte Beschreibungen von Funktionen und Systemaufrufen.- Online-Tutorials und Ressourcen:
- Vertiefung des Wissens über Threads und Prozessmanagement.
- Beispiele und Übungen zur praktischen Anwendung.
Inhaltsverzeichnis
- Vom Quelltext zur ausführbaren Datei
- Die GNU Compiler Collection (GCC)
- Modularisierung in C
- Bibliotheken in C
- Makefiles und Automatisierung
- Prozesse in Unix/Linux
- Die
exec
-Familie von Funktionen - Threads in C
Vom Quelltext zur ausführbaren Datei
In der Programmierung ist es wichtig zu verstehen, wie aus dem geschriebenen Quellcode letztendlich eine ausführbare Datei entsteht. Dieser Prozess umfasst mehrere Schritte:
- Quellcode: Der vom Programmierer geschriebene Code in
.c
-Dateien. - Präprozessor: Verarbeitet Direktiven wie
#include
und#define
und modifiziert den Quellcode entsprechend. - Übersetzer (Compiler): Wandelt den präprozessierten Code in Maschinensprache um.
- Assembler: Übersetzt eventuellen Assemblercode in Objektcode.
- Linker: Verbindet die Objektdateien mit notwendigen Bibliotheken zu einer ausführbaren Datei.
Visualisierung des Prozesses:
Quellcode → Präprozessor → Modifizierter Code → Compiler → Assemblercode → Assembler → Objektdatei
↑
Bibliotheken
↓
Linker → Ausführbare Datei
Die GNU Compiler Collection (GCC)
GCC ist eine weit verbreitete und leistungsfähige Compiler-Sammlung, die viele Programmiersprachen und Prozessorarchitekturen unterstützt. Für die Programmierung in C ist gcc
das zentrale Werkzeug.
Phasen des Übersetzungsvorgangs
- Präprozessor: Führt Makroersetzungen durch und inkludiert Headerdateien.
- Compiler: Übersetzt den präprozessierten Code in Assemblercode.
- Assembler: Wandelt den Assemblercode in Objektcode (
.o
-Dateien) um. - Linker: Verbindet Objektdateien und Bibliotheken zu einer ausführbaren Datei.
Anmerkungen:
- Nicht alle Phasen müssen bei jedem Aufruf von
gcc
durchlaufen werden. Es gibt Optionen, um den Prozess an bestimmten Punkten zu stoppen oder nur bestimmte Phasen auszuführen.
GCC-Optionen und Parameter
- Kompilieren ohne Linken:
gcc -c
erzeugt nur Objektdateien. - Präprozessor-Ergebnisse anzeigen:
gcc -E
stoppt nach dem Präprozessor. - Assemblercode erzeugen:
gcc -S
stoppt nach der Übersetzung in Assemblercode.
Typische Parameter:
- Ausgabedatei angeben:
-o <Dateiname>
(Standard ista.out
). - Warnungen aktivieren:
-Wall -Wextra
schaltet umfassende Warnungen ein. - Debugging-Informationen einbinden:
-g
ermöglicht das Debuggen mit Tools wiegdb
. - C-Standard festlegen:
-std=c99
oder-std=c11
legt den zu verwendenden Sprachstandard fest.
Beispiel:
Modularisierung in C
Modularisierung ist ein Schlüsselkonzept in der Softwareentwicklung. Es ermöglicht die Aufteilung eines Programms in kleinere, überschaubare und wiederverwendbare Einheiten.
Headerdateien und ihre Verwendung
-
Headerdateien (
.h
-Dateien) enthalten Deklarationen von Funktionen, Variablen, Makros und Typen. -
Sie dienen als Schnittstelle zwischen verschiedenen Modulen eines Programms.
-
Einbinden von Headerdateien:
- Systemheader:
#include <stdio.h>
sucht im Systempfad. - Benutzerdefinierte Header:
#include "meinHeader.h"
sucht im aktuellen Verzeichnis oder im angegebenen Pfad.
- Systemheader:
Beispiele für Standard-Headerdateien:
stdio.h
: Standard-Ein- und Ausgabefunktionen.stdlib.h
: Funktionen für Speicherverwaltung und Prozesssteuerung.math.h
: Mathematische Funktionen.time.h
: Funktionen für Datum und Zeit.
Eigene Headerdateien erstellen
Beispiel:
geometrie.h
- Inhalte:
- Makrodefinitionen:
#define PI (3.1415926)
- Funktionsdeklarationen: Prototypen der Funktionen, die in der Implementierungsdatei definiert sind.
- Makrodefinitionen:
geometrie.c
- Implementierung der in
geometrie.h
deklarierten Funktionen.
main.c
- Verwendung der Funktionen aus
geometrie.h
.
Include Guards und #pragma once
Include Guards
-
Schützen vor mehrfacher Einbindung einer Headerdatei.
-
Syntax:
-
Funktionsweise: Wenn das Makro (z. B.
DATEINAME_H
) nicht definiert ist, wird es definiert und der Inhalt der Headerdatei eingebunden. Andernfalls wird der Inhalt übersprungen.
#pragma once
- Alternative zu Include Guards.
- Verwendung: Einfach am Anfang der Headerdatei
#pragma once
hinzufügen. - Hinweis: Nicht Teil des C-Standards, aber von den meisten Compilern unterstützt.
- Vorteil: Weniger fehleranfällig, da keine Makronamen verwaltet werden müssen.
Bibliotheken in C
Bibliotheken sind Sammlungen von Funktionen und/oder Variablen, die von verschiedenen Programmen gemeinsam genutzt werden können.
Statische Bibliotheken
- Dateiendung:
.a
(Archivdateien). - Merkmale:
- Werden zur Compile-Zeit in das Programm eingebunden.
- Führt zu größeren ausführbaren Dateien, da der Code der Bibliothek integriert wird.
- Keine Abhängigkeit zur Laufzeit.
Erstellung einer statischen Bibliothek:
-
Objektdateien erstellen:
-
Bibliothek erstellen:
ar
: Archivierungswerkzeug.rcs
: Optionen zum Erstellen oder Aktualisieren eines Archivs.
Dynamische Bibliotheken
- Dateiendung:
.so
(Shared Objects). - Merkmale:
- Werden zur Laufzeit geladen.
- Führt zu kleineren ausführbaren Dateien.
- Ermöglicht das Teilen von Bibliotheken zwischen mehreren Programmen.
Erstellung einer dynamischen Bibliothek:
-
Objektdateien erstellen:
-fPIC
: Erzeugt positionsunabhängigen Code (Position Independent Code), notwendig für dynamische Bibliotheken.
-
Bibliothek erstellen:
-shared
: Erzeugt eine dynamische Bibliothek.
Einbindung von Bibliotheken
Beim Kompilieren angeben:
-
Statische Bibliothek:
-
Dynamische Bibliothek:
- Hinweis: Der Linker bevorzugt standardmäßig dynamische Bibliotheken.
- Pfad angeben: Mit
-L<Pfad>
wird der Pfad zu den Bibliotheken angegeben. - Bibliotheksname: Mit
-l<Library>
wird der Name der Bibliothek angegeben (ohnelib
-Präfix und Dateiendung).
Laufzeitumgebung:
- LD_LIBRARY_PATH: Umgebungsvariable, die zusätzliche Pfade für dynamische Bibliotheken zur Laufzeit angibt.
- Überprüfung: Mit dem Befehl
file <Datei>
kann überprüft werden, ob es sich um eine statische oder dynamische Bibliothek handelt.
Typische Linkerfehler und deren Behebung
Fehlende Implementierung
Fehlermeldung:
/usr/bin/ld: main.c:(.text+0x80): undefined reference to `kreisflaeche'
collect2: error: ld returned 1 exit status
Ursache:
- Die Funktion
kreisflaeche
wurde zwar deklariert, aber nicht definiert (die Implementierung fehlt).
Lösung:
-
Sicherstellen, dass die Implementierungsdatei (
geometrie.c
) beim Kompilieren angegeben wird:
Fehlende Bibliothek
Fehlermeldung:
/usr/bin/ld: cannot find -lmymath: No such file or directory
collect2: error: ld returned 1 exit status
Ursache:
- Die angegebene Bibliothek
libmymath.a
oderlibmymath.so
wurde nicht gefunden.
Lösung:
-
Den Pfad zur Bibliothek mit
-L
angeben: -
Überprüfen, ob die Bibliothek im angegebenen Verzeichnis vorhanden ist.
Makefiles und Automatisierung
Makefiles sind ein Werkzeug zur Automatisierung des Kompilierungsprozesses. Sie ermöglichen es, nur die tatsächlich notwendigen Dateien neu zu kompilieren, basierend auf definierten Abhängigkeiten.
Grundlagen von Makefiles
-
Aufbau:
-
Regeln:
- Ziel: Das zu erzeugende Artefakt (z. B. eine ausführbare Datei oder eine Objektdatei).
- Abhängigkeiten: Dateien, von denen das Ziel abhängt (z. B. Quellcode oder Headerdateien).
- Befehl: Der auszuführende Befehl, um das Ziel zu erzeugen.
-
Variablen: Können definiert und im Makefile verwendet werden, um Wiederholungen zu vermeiden.
-
Aufruf:
- Ohne Angabe eines Ziels wird das erste Ziel im Makefile ausgeführt.
Beispiel eines Makefiles
Erklärungen:
-
Variablen:
CC
: Compiler (hiergcc
).CFLAGS
: Compiler-Flags (hier Debugging-Informationen und alle Warnungen aktivieren).
-
Ziele und Abhängigkeiten:
all
: Standardziel, kompiliert das Programmmain
.main
: Hängt von den Objektdateienmain.o
,geometrie.o
undalgebra.o
ab.- Jede Objektdatei hängt von ihrer Quellcode-Datei und den benötigten Headerdateien ab.
-
Befehle:
- Kompilieren der Objektdateien mit
-c
. - Linken der Objektdateien zum ausführbaren Programm
main
.
- Kompilieren der Objektdateien mit
-
Clean-Ziel:
- Entfernt die erzeugten Objektdateien und das ausführbare Programm.
Prozesse in Unix/Linux
Ein grundlegendes Verständnis von Prozessen ist für die Systemprogrammierung in C unerlässlich.
Grundlagen von Prozessen
-
Definition: Ein Prozess ist ein laufendes Programm, das vom Betriebssystem verwaltet wird.
-
Eigenschaften:
- Ressourcen: Jeder Prozess besitzt eigene Ressourcen wie Speicher, Dateien und Geräte.
- Isolierung: Prozesse sind voneinander isoliert, um Stabilität und Sicherheit zu gewährleisten.
Prozesshierarchie und Zustände
-
Prozessbaum:
- Init-Prozess: Der erste Prozess (PID 1), von dem alle anderen Prozesse abstammen.
- Eltern- und Kindprozesse: Jeder Prozess (außer
init
) hat einen Elternprozess und kann Kindprozesse erzeugen.
-
Zustände von Prozessen:
- Laufend: Der Prozess wird gerade ausgeführt.
- Wartend: Der Prozess wartet auf eine Ressource oder ein Ereignis.
- Beendet: Der Prozess hat seine Ausführung abgeschlossen.
-
Spezielle Prozesse:
-
Waisenprozess:
- Entsteht, wenn ein Elternprozess beendet wird, bevor der Kindprozess endet.
- Der
init
-Prozess übernimmt die Rolle des Elternprozesses.
-
Zombieprozess:
- Ein Prozess, der beendet ist, aber dessen Elternprozess das Beenden noch nicht registriert hat.
- Belegt noch einen Eintrag in der Prozessliste, aber keine Ressourcen.
-
Prozesskontrolle und der fork
-Aufruf
fork
-Funktion
-
Deklaration:
-
Funktion:
- Erzeugt einen neuen Prozess (Kindprozess).
- Der Kindprozess ist eine Kopie des Elternprozesses mit eigenem Adressraum.
-
Rückgabewerte:
- Im Elternprozess: Die PID des Kindprozesses (> 0).
- Im Kindprozess: 0.
- Bei Fehler: -1.
Beispiel
Erklärungen:
getpid()
: Liefert die PID des aktuellen Prozesses.getppid()
: Liefert die PID des Elternprozesses.
Beispiele zur Prozessvererbung
Beispiel I: Mehrfaches fork()
Frage: Wie oft wird “Fork-Test” ausgegeben?
Analyse:
-
Erster
fork()
:- Erstellt einen Kindprozess, sodass nun 2 Prozesse laufen.
-
Zweiter
fork()
:- Jeder der beiden Prozesse führt
fork()
aus, sodass insgesamt 4 Prozesse laufen.
- Jeder der beiden Prozesse führt
-
Ausgabe:
- Jeder der 4 Prozesse führt
printf("Fork-Test\n");
aus.
- Jeder der 4 Prozesse führt
-
Antwort: “Fork-Test” wird 4-mal ausgegeben.
Die exec
-Familie von Funktionen
Die exec
-Funktionen dienen dazu, den aktuellen Prozess durch ein neues Programm zu ersetzen.
Ersetzen von Prozessabbildern
-
Funktionen:
execl()
,execv()
,execle()
,execlp()
,execvp()
,execve()
.
-
Funktionsweise:
- Laden eines neuen Programms in den aktuellen Prozess.
- Der aktuelle Prozess wird ersetzt, es kehrt nicht zur aufrufenden Funktion zurück.
-
Rückgabewert:
- Bei Erfolg: Kehrt nicht zurück.
- Bei Fehler: -1.
Beispiel: Ausführen eines anderen Programms
printargs.c
- Funktion: Gibt die übergebenen Argumente aus.
exec.c
Erklärungen:
-
fork()
: Erzeugt einen Kindprozess. -
Im Kindprozess:
execlp()
: Ersetzt den Kindprozess durch das Programmprintargs
mit den Argumentena
,b
,c
.- Wichtig: Bei Erfolg kehrt
execlp()
nicht zurück.
-
Im Elternprozess:
waitpid()
: Wartet darauf, dass der Kindprozess beendet wird.
Threads in C
Threads ermöglichen parallele Ausführung innerhalb eines Prozesses.
Unterschied zwischen Prozessen und Threads
-
Prozess:
- Eigener Adressraum.
- Ressourcen werden nicht standardmäßig geteilt.
- Kommunikation über Interprozesskommunikation (IPC) notwendig.
-
Thread:
- Gemeinsamer Adressraum innerhalb eines Prozesses.
- Ressourcen wie Variablen und Dateien werden geteilt.
- Effizienter bei Kontextwechseln.
POSIX-Threads (pthread
)
- Bibliothek:
pthread.h
- Voraussetzung: Beim Kompilieren muss die
pthread
-Bibliothek eingebunden werden (-pthread
).
Grundlegende Thread-Operationen
Thread erstellen
-
Parameter:
thread
: Zeiger auf einepthread_t
-Variable, in der die Thread-ID gespeichert wird.attr
: Attribute des Threads (kannNULL
sein).start_routine
: Zeiger auf die Funktion, die der Thread ausführt.arg
: Argumente für die Startfunktion.
Thread beenden
- Funktion: Beendet den aktuellen Thread und liefert einen Rückgabewert.
Auf Thread warten
- Funktion: Wartet darauf, dass der angegebene Thread beendet wird.
Beispiel
Erklärungen:
pthread_self()
: Liefert die ID des aktuellen Threads.- Synchronisation: Wichtig bei der Arbeit mit Threads, um Dateninkonsistenzen zu vermeiden (z. B. durch Mutexes).
Weiterführende Informationen
-
Threads und Nebenläufigkeit:
- Nebenläufige Programmierung erfordert ein Verständnis für Synchronisation und mögliche Probleme wie Race Conditions.
- Mutexes: Verhindern gleichzeitigen Zugriff auf gemeinsame Ressourcen.
- Semaphoren: Kontrollieren den Zugriff auf Ressourcen durch Zählung.
-
Empfohlene Lektüre:
- Pthreads-Tutorial: Vertiefung des Wissens über POSIX-Threads.
- Dokumentation: Manpages (
man pthread_create
) bieten detaillierte Informationen.
Hinweis: Diese Zusammenfassung soll als Einstieg in die Programmierung in C dienen. Es wird empfohlen, die Beispiele selbst auszuprobieren und weiterführende Literatur zu konsultieren, um ein tieferes Verständnis zu erlangen.
Viel Erfolg beim Programmieren!