C Grundlagen

Ursprünge von C

Die Programmiersprache C gehört zu den bedeutendsten Sprachen der Informatikgeschichte. Sie entstand Anfang der 1970er Jahre in den Bell Laboratories in den USA.
Dennis Ritchie, Informatiker bei den Bell Labs, gilt als „Vater von C“. Gemeinsam mit Ken Thompson arbeitete er an dem Betriebssystem Unix.
Ursprünglich wurde Unix in Assembler entwickelt. Assembler hat den Vorteil, dass es sehr direkten Zugriff auf die Hardware ermöglicht und äußerst effizient ist. Gleichzeitig bringt diese Nähe zur Maschine aber große Nachteile mit sich: Der Code ist schwer lesbar, aufwendig zu warten und nahezu vollständig an die jeweilige Prozessorarchitektur gebunden. Ein Programm, das für einen bestimmten Rechner geschrieben wurde, lässt sich deshalb nicht ohne Weiteres auf einer anderen Hardware ausführen – man müsste den gesamten Assemblercode anpassen.
Genau dieses Problem löste die Einführung von C. Mit C konnte Unix fast vollständig neu geschrieben werden. Der neue Quellcode war nicht nur wesentlich leichter verständlich und strukturierter, sondern vor allem auch portierbar. Das bedeutete, dass Unix nun relativ einfach auf unterschiedliche Rechnerarchitekturen übertragen werden konnte.
Diese Eigenschaft machte C zu einem entscheidenden Meilenstein in der Softwareentwicklung und trug maßgeblich dazu bei, dass sich Unix – und damit auch C – weltweit verbreiten konnte.

Bedeutung von C

C ist die „Mutter“ vieler späterer Sprachen:

und viele andere.
Bis heute nutzen fast alle modernen Programmiersprachen Ideen und Syntax-Elemente von C. Dennis Ritchie und Ken Thompson bekamen dafür 1983 den Turing Award (die höchste Auszeichnung der Informatik).

Einsatzgebiete von C

Die Sprache C wurde nicht nur als akademisches Projekt entwickelt, sondern hat seit den 1970er Jahren unzählige Anwendungen gefunden. Der große Vorteil von C liegt darin, dass es direkten Zugriff auf die Hardware erlaubt und gleichzeitig in einer hochsprachlichen Syntax geschrieben wird, die für Menschen lesbarer ist als Assembler. Schwerpunkte für den Einsatz von C sind sowohl für die Entwicklung von Systemsoftware als auch die Programmierung von Mikrocontrollern und eingebetteten Systemen (Embedded Systems).

C, die Brücke zu Mikrocontrollern

Mikrocontroller sind im Grunde vollständige Computer, die in einem einzigen Chip integriert sind. Im Unterschied zu den leistungsstarken Prozessoren in PCs oder Smartphones verfügen sie meist nur über sehr begrenzte Ressourcen: wenige Kilobyte bis wenige Megabyte Speicher, geringe Taktfrequenzen und stark eingeschränkte Rechenleistung.
Mikrocontroller bilden sie das Herz zahlloser elektronischer Geräte des Alltags. Sie übernehmen Steuerungsaufgaben in Autos, werten Sensordaten in Haushaltsgeräten aus, treiben kleine Displays an oder kontrollieren ganze Produktionsmaschinen. Auch in Robotern und modernen Smart-Home-Geräten sorgen sie dafür, dass Sensoren und Aktoren zuverlässig zusammenarbeiten.
Weil Mikrocontroller mit wenig Ressourcen auskommen müssen, ist eine effiziente Programmierung entscheidend.
Hier zeigt C seine Stärken: Der direkte Zugriff auf Speicher und Hardware ermöglicht es, den vorhandenen Platz optimal zu nutzen und Programme so klein und schnell wie möglich zu halten.

Das Arduino Framework nutzt intern C/C++.
Vorteil für Einsteiger: Arduino versteckt die komplizierten Teile (Compiler, Toolchain), man schreibt „fast wie in C“ und bekommt direkten Zugriff auf die Hardware.

Die Grundstruktur eines C Programms

Ein C-Programm folgt immer einer klaren Grundstruktur. Diese besteht aus drei wichtigen Teilen:

Präprozessor und Präprozessor-Direktiven in C

Der Präprozessor – was ist das?

Bevor der eigentliche Compiler den C-Code übersetzt, läuft ein eigenes kleines Programm: der C-Präprozessor.
Der Päprozessor bereitet den Quellcode auf, fügt externe Bausteine (Bibliotheken) hinzu, entfernt Kommentare (die für den Compiler unwichtig sind), ersetzt Symbole oder Konstanten durch Werte. Erst danach wird der vorbereitete Code an den Compiler übergeben, der daraus in einem mehrstufigen Prozess der Maschinencode erzeugt.

Was sind Präprozessor-Direktiven?

Präprozessor-Direktiven sind Anweisungen für den Präprozessor, die mit # beginnen.
Sie steuern wie der Quellcode vorbereitet wird. Sie gelten also als Compiler-Direktiven (weil sie den Ablauf der Übersetzung beeinflussen).

Wichtige Präprozessor-Direktiven

#include – Bibliotheken (Libraries) einbinden

Damit bindet man fertigen Code ein, der in einer separaten Datei (Bibliothek / Library) gespeichert ist.

Beispiel:

#include <stdio.h>   // Standard Input/Output Bibliothek

#define – Konstanten und Makros

Mit #define legt man symbolische Namen fest.

Beispiel:

#define PI 3.14159

Im Quellcode wird jedes PI vor dem Compilieren mit dem Wert 3,14159 ersetzt.

#ifdef / #endif – bedingte Übersetzung

Man kann mit dieser Direktive bestimmte Codebereiche nur dann übersetzen lassen, wenn eine Bedingung erfüllt ist.

Beispiel:

#define DEBUG

#ifdef DEBUG
printf("Debug-Modus aktiv!\n");
#endif

Ausgabe mit printf() erscheint nur, wenn DEBUG definiert ist.

Vom Quellcode zum Programm
Vom Quellcode zum Programm

Eigene Funktionen (optional)

In C kann man man zusätzliche Funktionen definieren. Funktionen sind Programmbausteine, die eine bestimmte Aufgabe erfüllten. Vorteil: Wiederverwendbarkeit und bessere Übersichtlichkeit.
Funktionen müssen in C immer definiert werden bevor sie im Quellcode das erste Mal verwendet werden.

Beispiel:

void begruessung() {
    printf("Hallo, willkommen im Programm!\n");
}

Die Funktion begruessung() kann nun im Quellcode verwendet werden.

Hauptfunktion main()

Die main() Funktion ist Pflicht in jedem C-Programm. Sie ist der Startpunkt des Programms. Das Betriebssystem sucht beim Start eines C Programms immer zuerst nach main().

Beispiel:

int main() {
    // Hier beginnt das eigentliche Programm
    printf("Programm startet!\n");

    return 0;   // Rückgabe ans Betriebssystem
}

Ablauf beim Programmstart

Wenn man ein C-Programm startet, passiert im Hintergrund:

  1. Das Betriebssystem lädt das Programm in den Speicher.
  2. Es sucht nach der Funktion main().
  3. Ab hier laufen alle Anweisungen in der festgelegten Reihenfolge.
  4. Wenn main() endet (durch return oder durch das Erreichen des Blockendes), wird das Programm beendet.

Beispiel für ein einfaches C Programm:

#include <stdio.h>   // Bibliothek einbinden

// Eigene Funktion
void begruessung() {
    printf("Hallo, willkommen im Programm!\n");
}

// Hauptprogramm
int main() {
    begruessung();            // Funktionsaufruf
    printf("Programm läuft...\n");

    return 0;                 // Ende des Programms
}

Ablauf des Programms:

  1. Der Präprozessor fügt die Bibliothek stdio.h ein.
  2. Die Funktion begruessung() wird definiert.
  3. Das Programm startet in main().
  4. main() ruft begruessung() auf → Textausgabe.
  5. Weitere Ausgabe.
  6. Ende mit return 0;.

Einfache Bildschirmausgabe mit C

Zu Beginn des Programmierens steht traditionell das berühmte „Hallo Welt!“-Programm.
Denn das Wichtigste ist zunächst, dass der Computer die Ergebnisse eines Programms auch sichtbar macht. Am einfachsten geschieht dies durch die Ausgabe von Text auf dem Bildschirm.

In C erfolgt die einfache Textausgabe über den Befehl printf(). Mit ihm können Zeichenketten, Zahlen oder kombinierte Ausdrücke auf dem Bildschirm dargestellt werden. Damit diese Funktion jedoch überhaupt zur Verfügung steht, muss zu Beginn des Programms die Standardbibliothek für Ein- und Ausgabe eingebunden werden.

Beispiel für "Hallo Welt!" in C:

#include <stdio.h>   // Bibliothek einbinden

int main() {
    printf("Hallo Welt!");
    return 0;                 // Ende des Programms
}

#include <stdio.h>

  • bindet die Bibliothek ein, die printf() bereitstellt.
  • stdio.h steht für Standard Input/Output Header und stellt wichtige Funktionen bereit – darunter printf() für Ausgaben und scanf() für Eingaben. Ohne diese Einbindung erkennt der Compiler den Funktionsnamen printf() nicht und meldet einen Fehler.

printf("...");

  • Gibt den Text auf dem Bildschirm aus.

return 0;

  • kehrt zum Betriebssystem zurück.
  • 0 (Fehlercode) steht für die fehlerfreie Ausführung des Programms

Dabei ist zu beachten, dass jeder Befehl in C mit einem Semikolon ; abgeschlossen werden muss! Andernfalls liefert der Compiler einen Fehler.

Grundlegende Syntax von C-Quellcode

Syntax von Befehlen in C

In C endet jede Anweisung mit einem Semikolon (;).
Fehlt dieses Abschlusszeichen, kann der Compiler den Befehl nicht korrekt verarbeiten und meldet einen Fehler.
Außerdem ist C case-sensitiv – das bedeutet, dass zwischen Groß- und Kleinschreibung unterschieden wird.
So sind etwa main, Main und MAIN drei völlig verschiedene Bezeichner. Dies ist vor allem bei der Benennung von Variablen und Funktionen zu beachten.

Code-Blöcke in C

In C lassen sich mehrere Anweisungen zu Blöcken zusammenfassen, wodurch Programme übersichtlich und hierarchisch strukturiert werden.
Ein Block beginnt stets mit einer öffnenden geschweiften Klammer { und endet mit einer schließenden geschweiften Klammer }.

Kommentare

Kommentare dienen dazu, den Quellcode für Menschen leichter verständlich zu machen. Sie haben keinen Einfluss auf die Ausführung des Programms, sondern sind ausschließlich für den Leser gedacht.

Besonders in größeren Projekten, an denen mehrere Personen zusammenarbeiten, ist eine klare und durchdachte Kommentierung unverzichtbar. Sie erleichtert nicht nur die Zusammenarbeit im Team, sondern auch die spätere Wartung und Weiterentwicklung des Codes – sei es durch andere Entwickler oder durch einen selbst, wenn man den eigenen Code nach längerer Zeit erneut betrachtet.

Die Sprache C kennt zwei verschiedene Arten von Kommentaren: einzeilige und mehrzeilige.

Beispiel: Kommentare in C

// Einzeiliger Kommentar
/* Mehrzeiliger
   Kommentar */

Besonders praktisch ist die Möglichkeit, mit einem mehrzeiligen Kommentar ganze Abschnitte des Quellcodes „auszukommentieren“. Dabei wird dieser Code vom Compiler vollständig ignoriert, ohne dass man ihn löschen muss.
Diese Technik ist im Alltag äußerst nützlich, etwa wenn man beim Testen von Programmen bestimmte Funktionen zeitweise deaktivieren möchte oder verschiedene Varianten von Code ausprobiert.

Variablen in C

Damit ein Programm Informationen verarbeiten kann, müssen Daten im Arbeitsspeicher des Computers abgelegt werden. Dieser Speicher besteht aus einer großen Menge einzelner Speicherzellen, die jeweils durch eine eindeutige Adresse gekennzeichnet sind.
Für den Programmierer wäre es jedoch unpraktisch und fehleranfällig, Daten direkt über solche numerischen Adressen zu verwalten.
Hier kommen Variablen ins Spiel. Eine Variable ist ein benannter Speicherbereich im Arbeitsspeicher (an einer bestimmten Adresse), in dem ein Wert abgelegt und später wieder verändert werden kann.
Der Variablen-Bezeichner (Variablenname) dient als verständlicher Verweis auf diesen Speicherbereich, sodass der Programmierer nicht mit reinen Speicheradressen arbeiten muss.

In der Programmiersprache C nimmt die Organisation und effiziente Nutzung des Arbeitsspeichers eine zentrale Rolle ein.
Anders als in vielen modernen Sprachen, die Speicherverwaltung teilweise oder vollständig automatisieren, gibt C dem Entwickler die direkte Kontrolle über:

Diese Nähe zur Hardware ist einer der Gründe, warum C bis heute besonders für Systemprogrammierung und Mikrocontroller-Anwendungen genutzt wird, wo die effiziente Verwaltung begrenzter Ressourcen entscheidend ist.

Datentypen in C

Ein Datentyp legt fest, welche Art von Daten in einer Variablen gespeichert werden können und wie groß der dafür benötigte Speicherbereich mindestens sein muss. Außerdem bestimmt er, wie der Inhalt im Arbeitsspeicher dargestellt wird (z. B. als Ganzzahl oder Kommazahl).

C kennt folgende Datentypen:

Datentyp

Mindestgröße laut Standard

Typische Größe (PC, 32/64 Bit)

Wertebereich (Beispiel, 32-Bit-Compiler)

char

8 Bit (1 Byte)

8 Bit

-128 … 127 oder 0 … 255

int

≥ 16 Bit

meist 32 Bit

-2.147.483.648 … 2.147.483.647

long

≥ 32 Bit

32 oder 64 Bit

abhängig von System und Compiler

long long

≥ 64 Bit

64 Bit

sehr große Ganzzahlen

float

ca. 32 Bit (mind. 6 Stellen Genauigkeit)

32 Bit

ca. 7 Nachkommastellen

double

ca. 64 Bit (mind. 10 Stellen Genauigkeit)

64 Bit

ca. 15 Nachkommastellen

long double

≥ wie double, oft 80 oder 128 Bit

Plattformabhängig

noch höhere Genauigkeit

Bei der Wahl des passenden Datentyps spielen drei Gesichtspunkte eine Rolle:

Speichergröße

Variablen belegen Platz im Arbeitsspeicher.

Genauigkeit

Performance

Zeichenketten in C

Wie man anhand der Datentypen sieht, liegt der Schwerpunkt bei C eindeutig auf Zahlen – ganze Zahlen, Kommazahlen, Vorzeichen und Genauigkeit spielen eine zentrale Rolle.
Doch Programme arbeiten nicht nur mit Zahlen, sondern müssen auch mit Texten umgehen können: Namen, Meldungen oder ganze Wörter.

In C gibt es dafür den Datentyp char für einzelne Zeichen. Mehrere char-Elemente hintereinander ergeben eine Zeichenkette (englisch: String).

Im Gegensatz zu vielen modernen Programmiersprachen wie Python oder Java gibt es in C keinen eigenen Datentyp string.

Deklaration und Initialisierung von Variablen

Deklaration von Variablen

Unter Deklaration versteht man die Bekanntmachung einer Variablen beim Compiler.
Dabei wird festgelegt:

Bei der Deklaration wird Speicherplatz reserviert, aber der Speicher-Inhalt ist noch undefiniert (beliebiger „Müllwert“ im RAM).

Regeln für Variablennamen in C

Für Variablen-Bezeichner sind nur bestimmte Zeichen erlaubt:

Wichtig!

Reservierte Schlüsselwörter

Wörter wie int, for, return sind fest für die Sprache C reserviert und dürfen nicht als Variablenname benutzt werden.

Initialisierung von Variablen

Unter Initialisierung versteht man das erste Zuweisen eines Werts für die Variable.
Die Initialisierung kann direkt bei der Deklaration geschehen oder später im Programmcode.

Wichtig! Variablen müssen zuerst deklariert werden bevor sie initialisiert werden.

Beispiel für die Deklaration und Initialisierung von Variablen in C:

#include <stdio.h>

int main() {
    int a;          // nur Deklaration, a enthält zufälligen Wert
    int b = 5;      // Deklaration + Initialisierung
    a = 3;          // nachträgliche Zuweisung (Initialisierung)

    printf("a = %d, b = %d\n", a, b);
    return 0;
}

Deklaration einer Zeichenkette in C

Eine Zeichenkette wird als Array von char deklariert.
Es gibt mehrere Möglichkeiten ein char-Array zu deklarieren und zu initialisieren.

Achtung! Die Initialisierung eines Arrays mit einer vorgegebenen Zeichenkette ist nur bei der Deklaration erlaubt!

Deklaration ohne Initialisierung

Bei der Deklaration ohne Initialisierung muss immer die Länge des Arrays angegeben werden.

Beispiel für die Deklaration einer Zeichenkette in C:

char Zeichenkette[21]  // Platz für bis zu 19 Zeichen + 1 Nullzeichen

Wichtige Punkte bei der Wahl der Array-Länge für Zeichenketten

Direkte Initialisierung mit einer Zeichenkette

Beispiel für die direkte Initialisierung bei der Deklaration einer Zeichenkette in C:

char text[] = "Hallo";

Der Compiler erzeugt automatisch das Array:

{'H','a','l','l','o','\0'}

Die Größe wird automatisch bestimmt (hier: 6 Zeichen inkl. \0).

Direkte Initialisierung mit einer Zeichenkette und fester Größe

Beispiel für die Initialisierung bei der Deklaration einer Zeichenkette in C:

char text[21] = "Hallo";

Das Array hat 21 Plätze, nur die ersten 6 werden belegt ("Hallo\0").

Die restlichen Plätze werden mit \0 aufgefüllt.

Formatierte Bildschirmausgabe in C

Die printf() Funktion, ermöglicht eine formatierte Bildschirmausgabe im Terminal.

Die printf() Funktion ist in der <stdio.h> Bibliothek enthalten. Diese muss vor der Verwendung eingebunden werden.

Syntax:

printf("Ausgabetext", Variable, Variable, );
"Ausgabetext": Text mit Formatanweisungen für die auszugebende Variable
Variable: Bezeichner der Variablen, deren Inhalt ausgegeben werden soll.

Formatanweisungen

%d

Ganzzahl (Integer), dezimal

%i

Ganzzahl, dezimal

%o

Ganzzahl, oktal

%h

Ganzzahl, hexadezimal

%u

Ganzzahl, vorzeichenlos (unsigned integer)

%c

Character

%s

String (nullterminertes Char-Array)

%f

Fließkommazahl (Float)

%f.2

Fließkommazahl (Float), angezeigt mit 2 Nachkommastellen

%p

Zeiger (Pointer), hexadezimal

Beispiel für die formatierte Ausgabe im Terminal:

#include <stdio.h>

int a = 42;
int main() {
  // Zeichenkette für die Formatierte Ausgabe //
  printf("Der Wert von a betraegt: %d",a);
}

Sonderzeichen in Zeichenketten

In C lassen sich in Zeichenketten Steuer- und Sonderzeichen verwenden.

Mathematische Berechnungen

C unterstützt alle Grundrechenarten:

Berechnungen mit Ganzzahlen (int)

Mit dem Datentyp int (Ganzzahlen) werden nur ganze Zahlen gespeichert.
Bei der Division von zwei Ganzzahlen wird das Ergebnis abgeschnitten. Die Nachkommastellen gehen verloren.

Division zweier Ganzzahlen:

#include <stdio.h>

int a, b, Ergebnis;

int main() {
  a = 12;
  b = 5;
  Ergebnis = a / b;
  printf("%d / %d = %d\n",a, b, Ergebnis);
}

Bildschirmausgabe:

2

Berechnungen mit Fließkommazahlen (float, double)

Mit float oder double (Fließkommazahlen) werden Nachkommastellen gespeichert.
Die Division liefert ein numerisch exaktes Ergebnis (so weit es der Datentyp erlaubt).

Division zweier Ganzzahlen:

#include <stdio.h>

float a, b, Ergebnis;

int main() {
  a = 12;
  b = 5;
  Ergebnis = a / b;
  printf("%f / %f = %f\n",a, b, Ergebnis);
}

Bildschirmausgabe:

2.4

Unterschied Integer vs. Float

Funktionen in C

Was sind Funktionen?

Eine Funktion ist ein eigenständiger Programmblock, der eine bestimmte Aufgabe erledigt.

Aufbau einer Funktion

Eine Funktion ist in C wie folgt aufgebaut:

Datentyp Funktionsname(Parameterliste) {
    // Anweisungen
    // Anweisungen
    return Wert;
}

Funktionen ohne Rückgabewert

Für Funktionen, die keine Werte zurückgeben existiert in C der Datentyp void.

Beispiel für eine Funktion ohne Rückgabewert:

#include <stdio.h>

void begruessung() {
    printf("Hallo, willkommen im Programm!\n");
}

int main() {
    begruessung();   // Funktionsaufruf
    printf("Programm läuft...\n");
    return 0;
}

Übergabeparameter

Übergabe-Parameter sind Werte, die vom Hauptprogramm an die Funktion übergeben werden. Sie liefern die notwendigen Eingangsdaten, damit die Funktion ihre Aufgabe erfüllen kann – zum Beispiel, um interne Berechnungen durchzuführen oder Ergebnisse individuell anzupassen.

Allgemeine Syntax einer Parameterliste

Beispiel für eine Funktion mit einem Parameter:

int quadrat(int x) {
    return x * x;
}
  • int : Datentyp des Parameters
  • x : Name des Parameters

Beispiel für eine Funktion mit mehreren Parametern:

int summe(int a, int b) {
    return a + b;
}

int main() {
    int x = 7, y = 3;
    printf("Summe: %d\n", summe(x, y));
    return 0;
}
  • Parameterliste: (int a, int b)
  • Jeder Parameter hat seinen eigenen Datentyp und Namen.
  • Es können auch unterschiedliche Datentypen übergeben werden.
  • Die Reihenfolge der Parameter ist verbindlich beim Funktionsaufruf.
  • Beim Aufruf werden die Werte in der gleichen Reihenfolge übergeben, wie sie in der Parameterliste definiert sind.

Gültigkeitsbereich von Variablen (Scope)

Der Gültigkeitsbereich (engl. Scope) einer Variablen bestimmt, wo im Programm diese Variable sichtbar und nutzbar ist.

In C unterscheidet man im Wesentlichen globale und lokale Variablen.

Globale Variablen

Globale Variablen werden außerhalb von Funktionen (meist am Anfang des Programms) deklariert.

Beispiel für globale Variablen:

#include <stdio.h>

int counter = 0;   // globale Variable

void zaehler() {
    counter++;     // Zugriff von überall möglich
}

int main() {
    zaehler();
    printf("Counter = %d\n", counter);
    zaehler();
    printf("Counter = %d\n", counter);
    return 0;
}

Bildschirmausgabe:

counter = 1
counter = 2

Lokale Variablen

Lokale Variablen werden innerhalb einer Funktion oder eines Blocks ({ ... }) deklariert.

Wichtig!
Wenn eine lokale Variable denselben Namen wie eine globale hat, verdeckt die lokale Variable die globale Variable im jeweiligen Block.

Beispiel für lokale Variablen:

#include <stdio.h>

void zaehler() {
    int x = 0;  // lokale Variable
    x++;
    printf("x = %d\n", x);
}

int main() {
    zaehler();  // Ausgabe: x = 1
    zaehler();  // Ausgabe: x = 1 (neu erzeugt!)
    return 0;
}

Bildschirmausgabe:

x = 1
x = 1

Beispiel für lokale und globale Variablen:

#include <stdio.h>

int counter = 5;  // globale Variable

void zaehler() {
    int counter = 0;  // lokale Variable
    counter++;
    printf("counter = %d\n", counter);
}

int main() {
    zaehler();  // Ausgabe: x = 1
    printf("counter = %d\n", counter);  // Ausgabe: counter = 5
    return 0;
}

Bildschirmausgabe:

counter = 1
counter = 5