Die Programmiersprache C Grundlagen und Fortgeschrittenes Übersetzerbau mit C als Zwischensprache V 20.06.10

Inhaltsverzeichnis

ÜberblickProjektverwaltungGrundlagenEinsprungpunktVariablen und KonstantenSpeicherklassenSichtbarkeitKontrollstrukturenBedingte AnweisungenSchleifenSprungbefehleFunktionenSelbstaufrufeKoroutinenVariadische FunktionenMehrfache FunktionsdefinitionenDatentypenGanzzahlenGleitkommazahlen ZeichenWahrheitstypNichttypZeigerTypumschreibungTypkonvertierungZusammengesetzte DatentypenFelderZeichenkettenAufzählungen: enumVerbünde: structVarianten: unionPräprozessorMakrosKopfdateienModulare ProgrammierungDynamische SpeicherverwaltungSpeicherfunktionenDynamische DatenstrukturenEinfach verkettete ListeEin- und AusgabenKonsoleDateienFehlerbehandlungNebenläufige ProgrammierungAnhangOperatorenSchlüsselwörter und SyntaxLösen von NamenskonfliktenTypische ProgrammierfehlerAnmerkungen

Überblick

Merkmal
Grundparadigmaimperativ: strukturiert, prozedural
SyntaxSchweifklammern, Semikolons
Speicherverwaltunghändisch – Systemprogrammiersprache
Typsystemschwach, statisch, ausdrücklich, nominal
Übersetzungsmodellkompilierend
DateiendungenC: Quelldatei; H: Kopfdatei – en header
Ersterscheinungsjahr1972
ErfinderDennis Ritchie, Bell Labs
VorläuferAssembler, Algol 68, BCPL > B (Syntax)
StandardisierungenISO: C89 / C90 (aus ANSI C), C95, C99, C11, C18
freie UmsetzungenAOCC, GCC, CLang, TCC, CINT (Interpreter)
proprietäre UmsetzungenBorland C++Builder, Intel C++, Visual C++ (MS), Watcom C/C++, XL C (IBM)
ProjektverwalterGNU Make, CMake, QMake, Qbs, …
PaketverwalterConan (empfohlen), prinzipiell jede Linux-Distribution
weitere Werkzeugeldconfig, pkg-config
freie IDEAnjuta, CodeBlocks, CodeLite, Geany, KDevelop
proprietäre IDECLion, Visual Studio (Windows, macOS), Xcode (macOS)
Wichtige Bibliotheken und Schnittstellen

Projektverwaltung

mittels make

Innerhalb des Projektverzeichnis kann mit make das Programm samt aller Module kompiliert werden:

Grundlagen

Einsprungpunkt

Gemäß dem Standard muss ein erfolgreich beendetes Hauptprogramm 0 zurückliefern, während alle übrigen Ganzzahlen einen Problemfall signalisieren. Jedoch besteht kein Standard zum Deuten der Zahlenwerte.

Alternativ kann auch void der Rückgabetyp von main sein, wobei dennoch das Programm mit 0 abgeschlossen wird.

Anmerkung zu C++

In C++ ist void als Rückgabetyp nicht erlaubt.

Variablen und Konstanten

Deklaration
Ver­gabe eines Namens unter Zu­ord­nen ein­es Dat­en­typs.
Definition
Reser­vier­en von Speich­er­platz nach der Größe des Dat­en­typs.
Initialisierung
Zu­weisen ein­es Wert­es noch vor dem ersten Be­nutz­en.

Konstanten sollten mittels const festgelegt werden, da der Präprozessor nicht typsicher ist, zumal moderne Übersetzer wie der GCC mittlerweile mit const umzugehen wissen und sinnvoll optimieren.

Zuweisung

Bei einer Zuweisung wird der äußerste rechte Ausdruck (Wert) allen Variablen links von diesem zugewiesen. Ist die betreffende Variable bereits mit einem Wert belegt, erfolgt eine Überschreibung der Speicherstelle:

Die Klammerung ist statthaft, da Zuweisungen in C ungewöhnlicherweise Ausdrücke darstellen!

Kombinierte Zuweisungen

Ebenfalls zu den kombinierten Zuweisungen zählen die Inkrement- und Dekrement-Operatoren, da diese sowohl Ausdruck als auch Anweisung darstellen:

 

Speicherklassen

Speicherklassen (en storage classes) beschreiben Geltungsbereich, Sichtbarkeit und Lebensdauer von Variablen.

Auf Funktionen sind Speicherklassen nur bedingt anwendbar, da diese ausschließlich global definiert werden.

auto
Mit der Deklaration bereits voreingestellt; bewirkt eine lokale Beschränkung. Die Angabe ist somit überflüssig.
extern
Der Wirkungsbereich entfaltet sich auf das gesamte Programm. Die Deklaration erfolgt in einer Header-Datei, um Speicher in anderen Übersetzungseinheiten zu beschreiben, welcher in die gegenwärtige Übersetzungseinheit übernommen werden soll. Funktionen sind mit extern voreingestellt, sodass eine gesonderte Kennzeichnung nicht erforderlich ist.
register
Bezugsrahmen wie bei auto; veranlasst den Compiler, eine Variable für einen schnellstmöglichen Zugriff im Prozessorregister zu halten. Vom Gebrauch wird heutzutage abgeraten!
static
Für fortwährende Variablen mit einem sonst beschränkten Geltungsbereich. Der Speicher soll während der gesamten Laufzeit des Programms zur Verfügung stehen. Bei Funktionen bewirkt static eine Begrenzung auf die Definitionsdatei.

 

Sichtbarkeit

en static and dynamic scoping

 

 

Kontrollstrukturen

 

Ternäre Operator

 

Bedingte Anweisungen

 

Verzweigung

 

Fallunterscheidung

 

Schleifen

 

Zählschleife

 

Kopfgesteuerte Schleife

 

Fußgesteuertere Schleife

 

Endlosschleife

Jede Schleife, deren Bedingung stets wahr bleibt, ist eine Endlosschleife. Manche Sprachen bieten für ewige Wiederholungen einen eigenen Schleifentypen. Unter Ausnutzen des bestehenden Verhaltens kann in C mittels Macro eine neue Syntax für Endlosschleifen eingeführt werden.

Sprungbefehle

 

goto

Sprünge über Funktionsgrenzen hinweg sind nicht möglich; dafür bestehen die Funktionen setjmp und longjmp.

Mengenschleife

Ein als Makro implementierte Mengenschleife, welche jedes Element einer Reihung durchläuft; nachteilig hierbei ist die Beschränktheit auf Felder sowie das Erfordernis der ausdrücklichen Längenangabe, weshalb eine gewöhnliche Schleife nicht minder zweckdienlich erscheint:

 

 

Funktionen

 

Die Umsetzung des Prototyps kann an beliebiger Stelle erfolgen, um eine noch nicht definierte Funktion bereits zu verwenden.

Anweisungen nach return bleiben unausgeführt (Funktionsabbruch).

 

Parameterlose Funktionen

umgesetzt mit dem Nichttyp void

 

Funktionen ohne Rückgabewert

Mit dem Datentyp void lassen sich Funktionen ohne Übergabe definieren, welche in anderen Sprachen wie Pascal auch Prozeduren genannt werden:

 

Selbstaufrufe

Rekursive Funktionen verbrauchen aufgrund ihres vielfachen Selbstaufrufes deutlich mehr Speicher, sofern nicht endständig rekursiv (en tail call), weshalb imperative Lösungen wie Sprungbefehle und Schleifen zu bevorzugen sind. Dennoch kann in seltenen Fällen eine Rekursion die bessere Lösung sein, insbesondere bei stark verzweigten sowie tiefen Verschachtelungen.

Koroutinen

C unterstützt weder Koroutinen noch vergleichbare Konstrukte; jedoch bestehen verschiedene Möglichkeiten, diese nachzustellen. Eine davon nutzt den Umstand aus, dass die einzelnen Fallunterscheidungen nicht auf derselben Ebene stehen müssen. Wird jedoch der Wert in einer statischen Variable vorgehalten, so kann im Unterschied zu echten Koroutinen stets immer nur eine Instanz ablaufen.

Variadische Funktionen

Da keine Möglichkeiten zur Funktionsüberladung bestehen, lassen sich nur mittels Ellipse und den über die Bibliothek stdarg bereitgestellten Makros variadische Funktionen verwirklichen.

Ein grundlegendes Problem bei diesem Mechanismus stellt die fehlende Information über Anzahl und Typ der übergebenen Argumente dar, sodass händisch Maßnahmen zu implementieren sind, damit die Argumente richtig gedeutet und als tatsächlich übergeben verarbeitet werden. Auch ist zu beachten, dass Übergabewerte bestimmter Datentypen einer automatischen Umwandlung unterliegen:

Mehrfache Funktionsdefinitionen

Funktionen, welche als inline markiert sind, dürfen in mehreren Übersetzungseinheiten enthalten sein, ohne gegen die One Definition Rule (ODR) zu verstoßen:

Datei: file1.c
Datei: file2.c
Konsolenaufruf: gcc

 

Datentypen

Ganzzahlen

en integer

Alle Ganzahlen sind als signed voreingestellt, sodass ein ausdrückliches Deklarieren als vorzeichenbehaftet überflüssig ist.

Folgende verkürzende Schreibungen sind erlaubt (Wertebereich bezogen auf 64 bit-Systeme):

BitmindSchreibweisen%Wertebereich
8unsigned charc / i1[0, 255]
8signed char / char [–128; 127]
16unsigned short [int]hu[0; 65.535]
16[signed] short [int]2h[–32.768; 32.767]
16 / 32[unsigned] intu[0; 232 – 1]
16 / 32signed int, signed, inti[– 231; 231 – 1]
32unsigned long [int]lu[0; 264 – 1]
32[signed] long [int]2li[– 263; 263 – 1]
64unsigned long long [int]ll[0; 264 – 1]
64[signed] long long[int]lli[– 263; 263 – 1]
 integers in octalo / O 
 unsigned integers in hexadecimalx / X 

In der ersten Spalte sind die vom C-Standard festgeschriebenen Mindestgrößen wiedergegeben; der tatsächliche Wertebereich kann jedoch systemabhängig höher ausfallen, bspw 32 Bit statt 16 Bit für int auf modernen Rechnern.

Anmerkungen
  1. Einzelzeichen werden von Compilern vor der Ausgabe zuvor in eine Ganzzahl umgewandelt (undefiniertes Verhalten, Fehlerquelle!).
  2. Alle Kombinationen mit short bzw long sind denkbar.

Gleitkommazahlen

en floating-point number

BitTyp%precWertebereich
4floatf61.2E-38 3.4E+38
8doublelf152.3E-308 1.7E+308
8 / 10long doubleLf193.4E-4932 1.1E+4932
4 / 8Exponentialdarstellunge / E  
Format
%.4 4 Nachkommastellen
%0 Vornullen
%- linksbündig
%+ stets Anzeige des Vorzeichens

Zeichen

en character

Ein einzelnes Zeichen wird als Ganzzahl abgespeichert, welche je nach Compiler für eine Kodierung in ASCII oder Unicode steht, wobei speziell für Unicode entsprechende Alternativ­daten­typen eingeführt wurden.

LiteralTyp%Byte
'a'charc1
L'a'wchar_tlc2

Da C lange vor Unicode entstand und somit der Breitzeichentyp erst nachträglich eingeführt wurde, lassen sich die Routinen zum Verarbeiten von Zeichen bzw Zeichenketten vom Typ char nicht mit dem Breitzeichentyp nutzen, sondern es muss alternativ zu <stdio.h>, <stdlib.h>, <string.h>, <time.h> sowie <ctype.h> auf die Gegenstücke in den Bibliotheken <wchar.h> und <wctype.h> zurückgegriffen werden. Zur Ausgabe von Unicode-Zeichen ist setlocale voranzustellen:

Wahrheitstyp

en Boolean type

In C besteht kein eigenständiger Datentyp für Wahrheitswerte, vielmehr gilt jede Zahl ungleich 0 als wahrer Wert.

Auch der mit dem C99-Standard eingeführte Datentyp bool ist nur eine Umschreibung für 0 / 1.

Nichttyp

en void type

Das Schlüsselwort void gilt nicht als echter Datentyp, sondern wird nur verwendet, um auszudrücken, dass eine Wertübergabe unerwünscht ist oder kein spezifischer Datentyp vorliegt (void-Zeiger).

Zeiger

en pointer

Als Zeiger wird eine an einer Variablen gebundene Speicheradresse bezeichnet. Hinter dieser Adresse können entweder Daten (Variablen) oder auch ganze Anweisungen stehen. Durch Dereferenzierung des Zeigers ist es möglich, auf die Daten oder Funktionen zuzugreifen.

Der Datentyp der Zeigervariable muss mit dem des Referenzobjekts übereinstimmen! Auch ist zu beachten, dass das Sternchen (*) sowohl zum Deklarieren eines Zeigers – als Typenbezeichner – als auch zum Zugreifen auf das dahinterliegende Objekt – als Dereferenzierer – dient.

Zeiger von Strukturen

Da der Punktoperator . einen höheren Stellenwert gegenüber dem Dereferenzierer * aufweist, muss der Strukturname eingeklammert werden. Als Vereinfachung dieser umständlichen Schreibweise wurde der sog Strukturoperator -> geschaffen: (*NAME).ELEMENT ≙ NAME -> WERT

Typumschreibung

Durch typedef werden keine neuen Datentypen erzeugt, sondern nur Synonyme (Aliase) für bestehende eingeführt.

Typkonvertierung

en type casting

Die automatische Typumwandlung in C führt häufig zu Fehlern oder unerwünschten Ergebnissen:

Zusammengesetzte Datentypen

Felder

en array

auch Aufstellung, Matrix / Matrize, (end­liche) Reihe / Reih­ung, Tabelle, Tupel (ein­dimen­sion­ales Feld)

Der Inhalt eines Feldes wird über die Reihenfolge, mit Null beginnend, identifiziert. Bei verschachtelten Feldern entspricht jeder Index eines Elements einer Dimension (Tiefe).

Ein Feldname in C ist lediglich ein Zeiger auf das erste Element, sodass dieser wie eine Speicheradresse gehandhabt wird:

Zeichenketten

en string

In C werden Zeichen­ketten als Daten­feld umgesetzt, wobei eine feste Größe (Index) bei Initialisierungen nicht zwinge nd angegeben werden muss. Um das Ende einer Zeichen­kette (vorzeitig) zu markieren, bedarf es einer binären Null \0.

Unterschied zwischen *X und X[]:

 char a[] = "Hallo!"char *p = "Hallo!"
ObjektartFeldZeigervariable
Größe in Bytesizeof(a) = 7 (Zeichenkette)sizeof(p) = 8 (Zeigergröße)
Adressierunga und &a verhalten sich gleich!&p liefert nur Speicheradresse
SpeicherortStapelp selbst auf dem Stapel;
Inhalt von p im Programmbereich
→ nur lesender Zugriff
Überschreibena = "Tach!"; // invalide!
a selbst ist eine Adresse!
p = "Tach!"; // valide!
Zeigerarithmetika++; // invalide!p++; // valide!
 a[0] = 'x'; // valide!p[0] = 'x'; // invalide!

 

Aufzählungen: enum

en enumerated type

Der Aufzählungstyp enum stellt eine Liste von Bezeichnern für ganzzahlige Konstanten dar, denen fortlaufend – beginnend mit 0 oder einer selbst festgelegten Ganzzahl als Startpunkt – ein Wert um 1 erhöht automatisch zugewiesen wird.

Mit enum und typedef lässt sich C um einen neuen Datentyp für Wahrheitswerte erweitern:

Dieser ist jedoch nicht typsicher, sondern verhält sich weiterhin wie eine Ganzzahl!

Verbünde: struct

en object composition, structure in C oder record in Pascal

Ein Verbund besteht aus einer oder mehreren Komponenten beliebigen Typs, welche selbst wiederum weitere Komponenten enthalten können.

Ein Vgl von Verbünden, bspw in einer Bedingung, ist nur über die einzelnen Komponenten möglich.

Typsynonyme für Strukturen

Mit typedef umgeschriebene Strukturen lassen sich wie gewöhnliche Typen zur Deklaration neuer Objekte verwenden:

 

Varianten: union

en union type

Eine Variante – auch Vereinigung genannt – ist ein Verbund, bei welchem alle Komponenten an der gleichen Speicher­adresse be­ginnen, so­dass ihre Speicher­bereiche ganz oder zu­mindest teil­weise über­lappt sind. Folglich richtet sich das Fassungs­vermögen nach der größten Komponente.

Zugriff und Wertzuweisung von Unionen ist wie bei Strukturen.

Varianten sind aufgrund ihrer Fehleranfälligkeit sowie erschwerten Portierbarkeit zu meiden.

Präprozessor

 

 

Makros

Makros in C sind ledig­lich reine Text­er­setz­ung­en mittels Prä­prozessor, wodurch keine echte Datenverarbeitung stattfindet. Dementsprechend muss der ersetzende Teil stets geklammert werden, damit die Auswertung wie beabsichtigt erfolgt. Dieser Umstand und die fehlende Typ­prüfung schränken die Möglichkeiten stark ein, bedingen aber eine schnellere Aus­führ­geschwindigkeit im Vgl zu Funktionen.

Kopfdateien

 

Modulare Programmierung

C besitzt nur ein rudimentäres Modulsystem, welches das Einfügen von Quelltexten aus anderen Dateien über den Präprozessor sowie das Kompilieren von ausgelagerten Quelltexten als eigenständige Übersetzungseinheiten gestattet, wobei der Linker die Kompilate zum fertigen Programm zusammenführt. Der Präprozessor dient hierbei vornehmlich zum Einbinden von sog Kopfdateien (en header files), welche lediglich Funktionsprototypen und Variablendeklarationen enthalten, damit der Übersetzer bereits vor dem Zusammenführen der Objektdateien die ausgelagerten Funktionen kennt und deren korrekte Anwendung überprüfen kann. Moderne Sprachen bedürfen keiner Kopfdateien, sondern erlauben den unmittelbaren Zugriff auf Module.

Um das Kompilieren zu vereinfachen, indem unter Anderem nur noch geänderte Dateien neu übersetzt werden, bestehen Erstellungswerkzeug wie Make, CMake oder Vergleichbares, welche alle Abhängigkeiten auflösen und den Bauvorgang zur fertigen ausführbaren Datei lenken.

Kopfdatei: palin.h
Implementierung zur Kopfdatei: palin.c
Hauptdatei des Projekts: main.c
Zugehöriges Makefile zum Projekt

Dynamische Speicherverwaltung

Stapelspeicher
Organisationsform von Speicher nach dem Last-In-First-Out-Prinzip (LIFO), um eine begrenzte Menge von Objekten effizient zu verwaltet. Neue Objekte werden stets auf den Stapel abgelegt und als erstes wieder heruntergenommen, währenddessen auf untere Objekte nur ein lesender Zugriff möglich ist.
Haldenspeicher / Freispeicher / Dynamischer Speicher
Organisationsform von Speicher, um zur Laufzeit eines Programms zusammenhängende Speicherabschnitte anzufordern und in beliebiger Reihenfolge wieder freizugeben. Die Freigabe kann sowohl manuell als auch mit Hilfe einer automatischen Speicherbereinigung erfolgen. Eine Speicheranforderung aus dem Heap wird auch dynamische Speicheranforderung genannt.
Laufzeitbibliothek
Teil einer höheren Programmiersprache, um BS-abhängige Befehle zu abstrahieren, bspw zur Ein- und Ausgabe.
Statische Variablen
Zu Beginn des Programms angelegte Variablen, welche erst mit Ende des Programms wieder gelöscht werden.

Jedes Programm besteht aus vier Bereichen, welche im tatsächlichen bzw virtuellen Arbeitsspeicher – oder auch im Festwertspeicher (zB Bios) – abgelegt sind:

Speicherfunktionen

   
void *malloc(size_t size);p = (double) malloc(sizeof(double)); 
   
   
   

 

// char[] — char*

 

Typische Fehler

hängender Zeiger dangling pointer

 

Dynamische Datenstrukturen

Unter einer Datenstruktur ist eine abstrakte Beschreibung zu verstehen, wie Daten zu speichern und verwalten sind. Die konkrete Umsetzung mit wohldefiniertem Wertebereich wird hingegen als Datentyp bezeichnet.

C bietet nur Felder als vorgegebene Datenstruktur, dessen Elemente statisch zur Übersetzungszeit auf dem Stapelspeicher abgelegt werden. Das dynamisches Gegenstück bilden verkettete Listen, welche vom Programmierer selbst zu definieren sind.

Einfach verkettete Liste

auch Lineare Listen

 

 

 

Ein- und Ausgaben

 

Konsole

 

 

 

 

 

Dateien

 

 

Fehlerbehandlung

 

 

 

Nebenläufige Programmierung

 

 

 

 

 

Anhang

 

Operatoren

 

Schlüsselwörter und Syntax

 

Lösen von Namenskonflikten

Vorgehen
  1. Die einfachste Lösung ist das Umbenennen von eigenen Funktionen, sofern möglich.

  2. Falls mehrere benötigte Fremdbibliotheken gleichnamige Bezeichner exportieren, sind in einem neuen Modul (Übersetzungseinheit) die jeweiligen Funktionen einer Bibliothek unter einem neuen Namen öffentlich zu referenzieren.

  3. Umbenennen von Symbolen der Objektdatei mittels des Programms objcopy

Typische Programmierfehler

Vergleiche

Anmerkungen

Ist C eine höhere Programmiersprache?

Streng nach Definition gilt C als höhere Programmiersprache, da ein in Standard-C geschriebenes Programm nicht unmittelbar von einer Maschine ausgeführt werden kann, sondern bereits unabhängig von der drunter liegenden Architektur arbeitet und erst von einem Übersetzer in maschinenabhängige Befehle umgewandelt werden muss. Jedoch ist C im Vergleich zu den übrigen höheren Programmiersprachen stark an den Begebenheiten von Maschinen ausgerichtet und weist überdies keinerlei hochsprachliche Abstraktionen auf, welche über strukturierte sowie prozedurale Konstrukte hinausgehen. Zudem enthalten C-Programme häufig integrierten Assembler, und sind damit nicht mehr völlig plattformunabhängig. Da Chip-Hersteller sogar gezielt auf C neben Assembler zur Steuerung zurückgreifen, muss C aus praktischer Sicht eher als mittelhohe Programmiersprache angesehen werden.

Externale und internale Bezeichner

External names are ones that are visible to other compilation units, like non-static functions and variables declared with the "extern" keyword. These names have to be exposed to linkers and loaders. In Ye Olden Days, some linkers and loaders could only handle very short names.

Internal names are ones that are not made visible outside of the module being compiled -- basically anything with "static" scope, or anything local to a function. C compilers have to handle these names, but not anything else

The C Standard sets the minimum number of significant characters that must be supported by all conforming implementations. Some implementations exceed these minimums, but taking advantage of that benefit can make your software hard to port to other implementations.

C11 Standard: same as C99