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
2018 ANSI C17 Keine

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 oder Modul (Übersetzungseinheit) besteht aus:

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

Makros definieren

Der Präprozessor (cpp) expandiart Makros nach folgenden Regeln, bevor der Compiler (cc) den Code sieht:

Header
#include <Datei> Sucht zuerst im Include-Pfad
#include "Datei" Sucht zuerst im Arbeitsverzeichnis
Makro
#define Name Ersetzung Symbolische Konstante durch Wert ersetzen
#define Name(...) __VA_ARGS__ Makro mit variabler Argumentliste
#define Name(Param, Args ...) ArgsMakro mit benannter variabler Argumentliste
#define Name(Param) #Param Parameter als Zeichenkette verwenden
#define Name(Param) ## Param Parameter konkatenieren
#undef Name Definition entfernen
Bedingung
#ifdef Ausdruck Übersetzt Block, wenn die Konstante definiert ist
#ifndef Ausdruck Übersetzt Block, wenn die Konstante nicht definiert ist
#if Ausdruck Übersetzt Block, wenn die Bedingung zutrifft
#elif Ausdruck Alternativer bedingter Block
#else Alternativer Block
#endif Ende des Blocks
Implementierung
#line Zeile Ändert die mitgeführte Zeilennummer
#line Zeile "Datei" Ä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
#pragma STD FP_CONTRACT ON Aktiviert Gleitkomma-Optimierungen
#pragma STD CX_LIMITED_RANGE OFF Ignoriert interne Überläufe
Konstanten
__FILE__ Aktueller Dateiname
__LINE__ Aktuelle Zeilennummer
__func__ Name der aktuelle Funktion
__DATE__ mmm dd yyyy Aktuelles Datum
__TIME__ hh:mm:ss Aktuelle Uhrzeit
__STDC__ 1 Standardkonformität des Compilers
__STDC_VERSION__201112L Version des C Standards
__STDC_HOSTED__1 Betriebsystem vorhanden
1
standardkonform
0
nicht
199409L
C95
199901L
C99
201112L
C11
1
Betriebsystem vorhanden
0
Kernelprogrammierung

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.

Typ Name = Wert;

Eine Aufzählung deklariert eine Gruppe zusammengehörender Konstanten.

enum Name { Name = Wert, … };

Eine Alternative interpoliert verschiedene Datentypen über dem gleichen Speicherbereich.

union Name { Typ Name, … };

Eine Struktur fasst eine Liste von Variablen zusammen.

struct Name { Typ Name, … } = { .Name = Wert, … };
Aufzählung
Alternative mit überlappenden Mitgliedern
Datenstruktur

Ein Array speichert eine Liste von gleichartigen Objekten. Arrays haben eine feste Größe.

Typ Name[Anzahl] = { Wert, … };

Eine Zeichenkette wird als Array 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:

Typ Name[Anzahl][Anzahl]… = { {… Wert, …}, …};

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 (Name) / sizeof (Name[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.

Typ Name (Argument, …) { Statement; … }
int main (int argc, char * argv[]) { printf ("Hello\n"); return 0; }

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 ! ~ ++ -- + - * & (Typ) sizeof
Multiplikation * / %
Addition + -
Bitverschiebung << >>
Vergleich < <= > >= == !=
Bitverknüpfung & ^ |
Logisches UND/ODER && ||
Ternärer Operator ? … :
Zuweisung = += -= *= /= %= &= ^= |= <<= >>=
Separator ,

Ablauf steuern

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

Zeiger dereferenzieren

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

Typ * Name = NULL;
Zeiger nach nirgendwo
Typ (* Name) (Argument, …);
Zeiger auf eine Funktion
Typ (* Name[Anzahl]) (Argument, …);
Array von Funktionszeigern

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

32-Bit 64-Bit Rechte Segment Beschreibung
0000:0000 0000:0000:0000 ----
0804:8000 0000:0040:0000 r-xp TEXT Programmcode und 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 und Parameter
rw-p CMD Kommandozeilenargumente
rw-- ENV Umgebungsvariablen
c000:0000 8000:0000:0000 --xp KERNEL Kernel Space

Stilvoll programmieren

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

Literatur

  1. Kernighan, Ritchie: The C Programming Language, Second Edition, Prentice Hall, 1988
  2. Kernighan, Pike: The Practice of Programming, Addison-Wesley, 1999
  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, The Art of Unix Programming, 2003
  7. Gustavo Duarte: Anatomy of a Program in Memory
  8. Pete Jinks: C Syntax in BNF