C

C ist eine prozedurale Sprache für die Systemprogrammierung. Ein Compiler erzeugt optimierten Maschinencode für eine bestimmte Hardware-Architektur.

Versionen

Datum Version Neuerungen
1972 NB Urversion aus den Bell Labs
1978 K&R C The C Programming Language, 1st edition
1989 ANSI C89 Aufzählungen, Zuweisung von Strukturen
1994 ANSI C95/NA1 Internationalisierung, Multibyte Zeichenketten
2000 ANSI C99 Variable Arrays, Designierte Initialisierer
2011 ANSI C11 Multithreading

Beispiel

hello.c
#include <stdio.h> int main (int argc, char * argv[]) { if (argc == 1) { printf ("Hello, World!\n"); } else for (int i = 1; i < argc; i++) { printf ("Hello, %s!\n", argv[i]); } return 0; }

Übersetzen und ausführen mit:

gcc -std=c11 -o hello hello.c && ./hello
Hello, World!

Syntax

Der Quellcode für ein C-Programm (die sogenannte Übersetzungseinheit) besteht aus:

  1. Direktiven für den Präprozessor,
  2. Deklarationen von globalen Variablen und Funktionen, sowie der
  3. Definition dieser Funktionen

Variablen deklarieren

Die Deklaration einer primitiven Variable legt Speicherklasse, Zugriff, Vorzeichen, Größe, Typ, Name und den initialen Wert fest.

Speicherklasse Zugriff Vorzeichen Größe Typ Bits Literal
auto
register
extern
static
thread_local
const
volatile
restrict
void
_Bool 1 true, false
signed
unsigned
char 8 'A'
short
long
int 16/32/64 0
_Complex
_Imaginary
double 64/80 0.1
float 32 0.1f
Variable auf dem Stack ablegen. Standard für lokale Variablen und Parameter von Funktionen.
Variable wenn möglich im Prozessorregister halten. Moderne Compiler ignorieren das Schlüsselwort, weil sie bei der Optimierung bessere Resultate erzielen.
Funktion oder globale Variable wird in einer anderen Übersetzungseinheit definiert.
Variable im statischen Programmspeicher ablegen. Globale Symbole sind nur in der Übersetzungseinheit sichtbar.
Variable im statischen Threadspeicher ablegen. Variable wird vor Beginn des Threads initialisiert.
Variable ist unveränderlich.
Variable kann sich spontan ändern, verzichte auf Optimierungen.
Zeiger unterliegt keinem Aliasing, aktiviere Optimierungen.
Vorzeichenbehaftete Ganzzahl
Vorzeichenlose Ganzzahl
Gleitkommazahl mit imaginärem Anteil
Imaginäre Gleitkommazahl
Ganzzahl (int) mit minimaler Größe
Ganzzahl (int) mit maximaler Größe oder Gleitkommazahl mit maximaler Genauigkeit
Anonymer Typ für Zeiger, Funktionen ohne Rückgabewert und leere Parameterlisten.
Wahrheitswert kann die Werte true und false annehmen.
Zeichencode abhängig vom eingestellten Zeichensatz
Ganzzahl
Gleitpunktzahl mit einfacher Genauigkeit
Gleitpunktzahl mit doppelter Genauigkeit

Mit der Deklaration sollte auch gleich die Zuweisung eines Wertes erfolgen.

type identifier = value;

Aufzählungen deklarieren eine Gruppe zusammengehörender Konstanten:

enum identifier { name = value, … };

Alternativen interpolieren verschiedene Datentypen über dem gleichen Speicherbereich:

union identifier { type name, … };

Strukturen fassen eine Liste von Variablen zusammen:

struct identifier { type name, … } = { .name = value, … };
Aufzählung
Alternative mit überlappenden Mitgliedern
Datenstruktur

Arrays in C speichern Listen von gleichartigen Objekten. Sie haben eine feste Größe.

type identifier[size] = { value, … };

Zeichenketten werden als Arrays vom Typ char abgelegt. Folgende einheitliche Notation hilft, den Durchblick zu behalten.

char * buf [xxxxxxxx____________]     Puffer
size_t len |--------^                 Füllstand
size_t cap |--------------------^     Kapazität
            ^       ^           ^
            beg     pos         end   Zeiger

Mehrdimensionale Arrays decken Matrix und Tensor ab:

type identifier[size][size]… = { {… value, …}, …};

Ein häufig verwendetes Idiom bei der Iteration macht sich die Möglichkeit zu Nutze, die Anzahl der Elemente zu berechen:

for (size_t i = 0; i < sizeof (identifier) / sizeof (identifier[0]); i++) …

Zusammengehörende Bereiche im Speicher bezeichnet man als Objekte. Der Compiler führt Buch über den Typ dieser Objekte, etwa ob es sich um eine Ganzzahl, eine Datenstruktur oder um eine Funktion handelt.

Funktionen definieren

Da Funktionen ebenfalls Objekte sind, kann man deren Adresse als Zeiger speichern, sie als Elemente in Strukturen verwenden oder als Parameter und Rückgabewert in anderen Funktionen übergeben.

type identifier (argument, …) { statement; … };

Schlüsselwörter für Funktionen

inline
Rumpf expandieren. Lieber auf Compiler-Optimierungen verlassen.
static
Funktion ist nur in der Übersetzungeinheit sichtbar, das Symbol wird nicht exportiert.
_Noreturn
Funktion kehrt niemals zurück. Aktiviere Compiler-Optimierungen. Nur sinnvoll für void-Funktionen.

Primitive, Aufzählungen, Alternativen, Strukturen und Funktionen bevölkern jeweils einen eigenen Namensraum. Daher kann man gleiche Namen für verschiedene Klassen verwenden. Sprich: Eine Struktur kann genauso heißen wie eine Aufzählung.

Operationen ausführen

Die Präzedenz der Operatoren ist auf intuitive Verwendeung ausgelegt, so dass man häufig auf Klammern verzichten kann. Geklammerte Ausdrücke genießen immer Vorrang, gefolgt von Array-Indizierung, Zeiger-Dereferenzierung und Feldzugriff. In arithmetischen Ausdrücken werden zuerst unäre Operatoren, dann Multiplikation, Addition und schließlich Bitoperationen ausgewertet. Danach folgen logische Ausdrücke mit Vergleich, Bitverknpüfung, Wahrheitslogik und Entscheidungslogik mit dem ternären Operator. Am geringsten binden Zuweisungen.

Operator
Klammerausdrücke () [] -> .
Unäre Operatoren ! ~ ++ -- + - * & (type) sizeof
Multiplikation * / %
Addition + -
Bitverschiebung << >>
Vergleich < <= > >= == !=
Bitverknüpfung & ^ |
Logisches UND/ODER && ||
Ternärer Operator ? … :
Zuweisung = += -= *= /= %= &= ^= |= <<= >>=
Separator ,

Ablauf steuern

for (expression; condition; expression) …
Schleife für Iteration mit Initialisierung, Abbruchbedingung und Inkrement
while (condition) …
Schleife mit Abbruchbedingung
do … while (condition);
Schleife mit Abbruchbedingung am Ende
if (condition) … else …
Verzweigung
switch (identifier) { case value: …; break; default: …; break; }
Mehrfachverzweigung
continue;
Aktuellen Schleifendurchlauf beenden
break;
Schleife verlassen
goto label;
Unbedingter Sprung
return expression;
Rücksprung aus Funktion
asm { instructions }
Anweisungen in Maschinencode einbetten

Zeiger dereferenzieren

Ein Zeiger ist eine Variable, welche die Speicheradresse eines Objekts enthält.

type * identifier = NULL;
Zeiger nach nirgendwo
type (* identifier) (arguments);
Zeiger auf eine Funktion
type (* identifier[size]) (arguments);
Array von Funktionszeigern

Der Adressraum auf einem typischen Linux-System hat folgenden Aufbau:

32-Bit-Adresse 64-Bit-Adresse Zugriff Segment Beschreibung
0000:0000 0000:0000:0000 ----
0804:8000 0000:0040:0000 r-xp TEXT Programmcode, Konstanten


brk

RLIMIT_DATA
0000:0060:0000 r--p DATA Initialisierte globale Variablen
rw-- BSS Null-initialisierte globale Variablen
rw-p HEAP Dynamische Speicherverwaltung
rwxp MMAP Memory Mapped Files
(Shared Memory & Libraries)
RLIMIT_STACK

sp

rw-- STACK Lokale Variablen, Rücksprungadresse, Parameter
rw-p CMD Kommandozeilenargumente
rw-- ENV Umgebungsvariablen
c000:0000 8000:0000:0000 --xp KERNEL Kernel Space

Makros definieren

Der Präprozessor expandiart Makros nach folgenden Regeln:

Header
#include <filename> Sucht zuerst im Include-Pfad
#include "filename" Sucht zuerst im aktuellen Verzeichnis
Konstante
#define identifier replacement Symbolische Konstante durch Wert ersetzen
#define identifier(...) __VA_ARGS__ Makro mit variabler Argumentliste
#define identifier(parameter, args...)argsMakro mit benannter variabler Argumentliste
#define identifier(parameter) #parameter Parameter als Zeichenkette verwenden
#define identifier(parameter) ## parameter Parameter konkatenieren
#undef identifier Definition
Bedingung
#if expression Übersetzt Block, wenn die Bedingung zutrifft
#ifdef expression Übersetzt Block, wenn die Konstante definiert ist
#ifndef expression Übersetzt Block, wenn die Konstante nicht definiert ist
#elif expression Alternativer bedingter Block
#else Alternativer Block
#endif Ende des Blocks
Implementierung
#line line Ändert die mitgeführte Zeilennummer
#line line "filename" Ändert auch den mitgeführten Dateinamen
#error MESSAGE Stoppt den Compiler mit einer Fehlermeldung
#pragma STD FENV_ACCESS OFF Das Programm ändert die Gleitkomma-Umgebung (fenv.h)
#pragma STD FP_CONTRACT ON Aktiviert Gleitkomma-Optimierungen
#pragma STD CX_LIMITED_RANGE OFF Ignoriert interne Überläufe

Folgende Konstanten sind vordefiniert:

__FILE__
Dateiname der Übersetzungseinheit
__LINE__
Zeilennummer in der Quelldatei
__DATE__
Datum im Format mmm dd yyyy
__TIME__
Uhrzeit im Format hh:mm:ss
__STDC__
Standardkonformität des Compilers: 1 oder 0
__STDC_VERSION__
Version des C Standards: 199409L, 199901L oder 201112L
__STDC_HOSTED__
1 wenn ein Betriebsystem vorhanden ist
0 bei der Kernelprogrammierung
__func__
Name der aktuelle Funktion

Programmierstil

Quelltext dient in erster Linie der Kommunikation mit anderen Entwicklern. Er sollte daher leicht zu verstehen und leicht zu erweitern sein. Es ist die Aufgabe des Compilers, daraus für den Rechner optimierte Anweisungen zu erzeugen.

Typographische Konventionen

Programme sollte wie Bücher aufgebaut sein:

Struktur von Übersetzungseinheiten

Module fassen Datenstrukturen und alle darauf arbeitenden Funktionen zusammen.

Die UNIX-Philosophie

Modularität
Einfache Teile durch saubere Nahtstellen verbinden.
Klarheit
Klarheit gegenüber Gerissenheit bevorzugen.
Komposition
Programme für die Zusammenarbeit mit anderen Programmen auslegen.
Trennung
Richtlinien von Mechanismen trennen; Schnittstelle von der Maschinerie trennen.
Einfachheit
Einfachheit anstreben, Komplexität nur bei Bedarf hinzufügen.
Sparsamkeit
Große Programme vermeiden oder den Gegenbeweis erbringen.
Transparenz
Beobachtbarkeit einbauen um Prüfung und Fehlersuche zu vereinfachen.
Robustheit
Stabilität ist das Kind von Beobachtbarkeit und Einfachheit.
Repräsentation
Erkenntnisse in die Daten flechten, so daß das Programm dumm und stabil bleiben kann.
Erwartungskonformität
Bei Schnittstellen immer den am wenigsten überraschenden Weg wählen.
Schweigen
Wenn ein Programm nichts überraschendes zu sagen hat, nichts sagen.
Reparatur
Wenn etwas schiefgeht, geräuschvoll und alsbaldigst versagen.
Ökonomie
Programmierer sind teuer, Maschinen arbeiten billig.
Generierung
Handarbeit vermeiden, Programme schreiben die Programme schreiben.
Optimierung
Erst funktionierende Prototypen bauen, Flaschenhälse später optimieren.
Diversität
Misstrauen gegenüber dem einen wahren Weg bewahren.
Erweiterbarkeit
Für die Zukunft planen, sie kommt schneller als man denkt.

Literatur

  1. Kernighan, Ritchie: The C Programming Language, Second Edition, Prentice Hall, 1988
  2. Kernighan, Pike: The Practice of Programming
  3. Boswell, Foucher: The Art of Readable Code, O'Reilly 2012
  4. Rob Pike: Notes on Programming in C
  5. Linus Torvalds: Linux kernel coding style
  6. Eric S. Raymond, Basics of the UNIX Philosophy, TAOUP
  7. Pete Jinks: C Syntax in BNF
  8. Gustavo Duarte: Anatomy of a Program in Memory