Grundlagen
Warum zum Teufel C?
C ist – nach heutigen Maßstäben – eine uralte Programmiersprache, die für den Benutzer unkomfortabel ist und ihn wenig davor beschützt Fehler zu machen. Viele Dinge, die man in Java ein einigen Zeilen erledigen kann, benötigen in C umfangreiche Konstrukte und bereiten viel Aufwand, bis sie einwandfrei funktionieren. C ist ein Dinosaurier unter den Programmiersprachen.
Warum sollte man sich heute noch mit C beschäftigen? Oder noch kürzer „warum zum Teufel C?“.
- C ist gleichzeitig High- und Lowlevel-Sprache
- Bessere Kontrolle über die Maschine / das Betriebssystem
- Etwas bessere Performance als Java
- Für Echtzeitprogrammierung geeignet
- Geeignet für die Programmierung von Betriebssystemen
- Aber
- Explizite Speicherverwaltung
- Keine eingebaute Fehlerbehandlung
- Verzeiht keine Fehler
- Code ist umfangreicher als Java
- Viel alter Code ist in C geschrieben
- Linux, BSD
- Windows
- Java VMs
- Embedded Systems
- C hilft die Hardware zu verstehen
C ist auf keinen Fall irrelevant und wird heute noch in vielen Bereichen eingesetzt. Durch die Nähe zur Maschine und die explizite Speicherverwaltung kann man in C Dinge programmieren, die in anderen Sprachen nicht zu bewältigen wären, z. B. Betriebssysteme oder Echtzeitanwendungen. Eine Echtzeitanwendung, z. B. die Steuerung eines Airbags, wäre in Java undenkbar, da Java gelegentlich Pausen für die Garbage Collection einlegt und damit kein vorhersehbares Zeitverhalten hat. Ebenso benötigt man für die Entwicklung eines Betriebssystems eine Sprache, die direkten Zugriff auf die Hardware erlaubt – eine Java VM wäre hier auf jeden Fall im Weg.
Wenn man sich mit fortgeschrittenen Themen wie Reverse Engineering beschäftigt, also der Frage, wie man ein vorliegendes Programm im Maschinencode analysieren kann, ist C ein wichtiges Hilfsmittel. Wegen seiner Nähe zur Hardware kann man Maschinenprogramm einfach als C-Code darstellen, sodass man beim Reverse Engineering nicht die Assembler-Instruktionen lesen muss, sondern sich mit dem daraus erzeugten C-Code beschäftigen kann.
Geschichte von C
1960er Jahre
- Viele neue Sprachen
- COBOL für Geschäftsanwendungen
- FORTRAN für wissenschaftliche Berechnungen
- PL/I als Sprache der 2. Generation
- Lisp, Simula für Informatik-Forschung (AI)
- Assembler für Betriebssysteme und zeitkritischen Code
- Betriebssysteme
- OS/360
- MIT/GE/Bell Labs Multics (PL/I)
- Bell Labs wollte eigenes Betriebssystem schreiben (Ersatz für Multics)
- Multics war in PL/I geschrieben
- Ken Thompson entwickelte B auf Basis von BCPL auf der PDP-7 (16kByte RAM) → einfacher als PL/I und BCPL
- Dennis Ritchie entwickelt auf Basis von B die Sprache C (1969–1973)
Die Geschichte von C beginnt zu einer Zeit, als die Entwicklung der Computer noch in den Kinderschuhen steckte.
Nachdem zu Beginn alle Programme direkt in der Maschinensprache des jeweiligen Prozessors geschrieben wurde, entstanden in den 1960 Jahren neue Hochsprachen, welche die Programmierung und die Übertragung von Programmen auf neue Hardware vereinfachen sollten. Die neu entwickelten Sprachen konzentrierten sich jeweils auf spezifische Bereiche: COBOL für Geschäftsanwendungen, FORTRAN für mathematische Berechnungen und Lisp für die KI-Forschung – Lisp ist tatsächlich eine der ältesten Programmiersprachen überhaupt, mit einer ersten Spezifikation aus dem Jahr 1958.
Eine der wenigen Sprachen der damaligen Zeit, die für einen breiteren Anwendungsbereich vorgesehen wurde, war PL/I, die 1964 das Licht der Welt erblickte.
Hello2: proc options(main);
put list ('Hello, world!');
end Hello2;
Ebenso rudimentär wie bei den Programmiersprachen war die Lage bei den Betriebssystemen: Diese wurden ebenfalls in Assembler geschrieben und liefen ausschließlich auf der Hardware, für die sie Entwickelt wurden.
Aus dem Bedürfnis heraus, einen Nachfolger für Multics zu entwickeln, begannen Ken Thompson und Dennis Ritchie um 1965 mit der Entwicklung von Unix. Ein Ziel der beiden war, das Betriebssystem möglichst einfach auf neue Hardware portieren zu können, wobei kleinere Minicomputer wie die PDP/7 oder PDP/11 unterstützt werden sollten. Die verfügbaren Programmiersprachen waren dafür aber entweder zu hardwarenah (Assembler) oder zu schwergewichtig (PL/I), weswegen Thompson mit B) eine neue Sprache entwarf.
main()
{
auto c;
auto d;
d=0;
while(1)
{
c=getchar();
d=d+c;
putchar(c);
}
}
Ritchie entwickelte B dann weiter zur Sprache C, wie wir sie heute kennen. Er und Thompson verwendeten C, um das Betriebssystem Unix zu entwickeln. Die ersten Versionen von Unix waren noch in Assembler geschrieben, ab 1973 (Version 4 Unix) dann in C.
Von C zu Java
- C: Dennis Ritchie (1969–1973)
- Sprache zur Systemprogrammierung
- Macht das Betriebssystem hardwareunabhängig
- Nicht für Anwendungen gedacht (→ PL/I oder Fortran)
- C++: Bjarne Stroustrup (Bell Labs), 1980er
→ objektorientierte Erweiterung zu C - Java: James Gosling in den 1990ern
- Für eingebettete Systeme gedacht
- Objektorientiert (ähnlich C++)
- Syntax von C übernommen
Nachdem C in den 1970er Jahren entwickelt wurde, wurde es in den 1980er Jahren von Bjarne Stroustrup um Objektorientierung erweitert, woraus dann C++ entstand.
#include <iostream>
int main(int argc, const char * argv[])
{
std::cout << "Hello, world!\n";
}
Parallel zu C++ wurde von Tom Love und Brad Cox mit Objective-C eine andere objektorientierte Version von C entworfen, die sich bei Apple lange Zeit als Programmiersprache für macOS-Anwendungen gehalten hat, inzwischen aber von Swift abgelöst wird.
// FILE: hello.m
#import <Foundation/Foundation.h>
int main (int argc, const char * argv[])
{
/* my first program in Objective-C */
NSLog(@"Hello, World! \n");
return 0;
}
Die die Syntax der Sprache C diente als Vorbild für viele weitere Sprachen, die heute noch in Gebrauch sind. Neben Java und C# finden sich viele Ideen von C ebenfalls in Go, PHP und weiteren Sprachen wieder.
C vs. Java
Da Java bei uns als erste Programmiersprache dient, ist es sinnvoll einen Vergleich zwischen Java und C zu ziehen.
- Java ist eine objektorientierte Sprache von Mitte der 1990er Jahre
- C ist eine frühe prozedurale Sprache aus den frühen 1970er Jahren
- Vorteile von C
- Direkter Zugriff auf das Betriebssystem (System-Calls)
- Wenig Probleme mit Bibliotheken – läuft einfach
- Nachteile von C
- Sprache ist portabel, Schnittstellen zum Betriebssystem nicht
- Memory-Leaks drohen
- Präprozessor ist obskur (und die Fehler noch mehr)
Java ist mehr als 20 Jahre jünger als C und mit einem anderen Ziel entwickelt worden: Während C maschinen- und hardwarenah für die Betriebssystementwicklung entworfen wurde, hatte Java das Ziel möglichst maschinenunabhängig zu sein und sollte für die Entwicklung von Anwendungen – damals für Set-Top-Boxen – dienen. Außerdem ist Java konsequent objektorientiert, während C rein prozedural ist.
Die Entwickler von Java habe sich dafür entschieden, die Syntax von Java an C anzulehnen, um Vertrautes zu verwenden. Die Semantik von Java orientiert sich aber viel mehr an Smalltalk als an C. Java-Code sieht auf den ersten Blick C-Code ähnlich, funktioniert aber häufig anders.
Folgende Tabelle gibt einen Überblick über die Unterschiede zwischen den beiden Sprachen.
| Java | C |
|---|---|
| objektorientiert | prozedural |
| strikt getypt | getypt, kann aber übersteuert werden |
| polymorphismus | - |
| Klassen und Pakete für Namespaces | Flacher Namensraum |
| keine Makros | Makros Kern der Sprache |
| automatische Speicherverwaltung | manuelle Speicherverwaltung |
| keine Pointer | Pointer |
| by-value | by-value |
| Ausnahmebehandlung | Manuelle Fehlerbehandlung |
| Threads Teil der Sprache | Threads über Bibliotheken |
| Arrays kennen ihre Länge | Arrays kennen ihre Länge nicht |
| String als Datentyp | Ausschließlich char[] |
| Viele Bibliotheken | Bibliotheken vom Betriebssystem |
Aus der Tabelle sind vier Aspekte besonders herauszuheben, die Umsteigern von Java auf C häufig Schwierigkeiten bereiten:
- Speicherverwaltung: In C muss der Entwickler die gesamte Speicherverwaltung übernehmen. Sowohl das Reservieren, als auch das Freigeben von Speicher muss explizit erfolgen.
- Pointer: Pointer sind in C ein wichtiger Datentyp und können über die sogenannte Pointerarithmetik manipuliert werden. Ein sicherer Umgang mit Pointern ist unerlässlich, um C-Programme schreiben zu können.
- Strings: C kennt keine Strings. Zeichenketten werden als Arrays von Zeichen (
char[]) realisiert, deren Ende durch ein Nullbyte (0x00) gekennzeichnet ist. - Länge von Arrays: Ein C-Array kennt seine Länge nicht und es ist Aufgabe des Entwicklers, die Länge zu verwalten und an Funktionen, die Arrays verwalten zu übergeben.
Java-Programm
- Sammlung von Klassen
main-Methode einer Klasse wird aufgerufen- Java VM lädt Klassen bei Bedarf aus JAR-Archiven nach
C-Programm
- Sammlung von Funktionen
- Eine Funktion (
main) ist der Einstiegspunkt - Alle Funktionen sind zu einem Executable zusammengefasst
- Dynamische Libraries (
.dll,.so) enthalten gemeinsam genutzte Funktionen
Da C nicht objektorientiert ist, bestehen C-Programme nicht aus Klassen, sondern sind eine Sammlung von Funktionen, die sich gegenseitig aufrufen. Alle Funktionen eines C-Programms werden vom Linker (siehe unten) in ein sogenanntes Executable zusammengefasst. Gemeinsam genutzte Funktionen werden in dynamischen Libraries abgelegt, die unter Windows die Dateiendung .dll und unter Linux/Unix die Dateiendung .so bekommen.
Das erste C-Programm
Im Folgenden soll ein erstes C-Programm Schritt für Schritt analysiert werden. Hierzu dient das berühmte „Hello World“-Beispiel.
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
#include <stdio.h>
int main(int argc, char** argv) {
/* Greet the World */
printf("%s", "Hello, World!\n");
return 0;
}
#include <stdio.h>- Zeilen mit
#werden vom Präprozessor ausgewertet - lädt ein Header-File (→
importin Java) int main(int argc, char** argv) {- deklariert eine Funktion
- Rückgabetyp
int - zwei Parametern
argcundargvargcist die Anzahl der Argumenteargvist ein Array von Zeichen mit den Argumenten
Die Präprozessordirektiven werden mit # eingeleitet und sind eine Besonderheit von C-Programmen. Auf den Präprozessor wird später noch detailliert eingegangen. Hier sei allerdings bereits erwähnt, dass ein C-Programm in zwei Schritten kompiliert wird: Zuerst verarbeitet der Präprozessor den Quelltext und erzeugt Dateien, die dann vom C-Compiler verarbeitet werden. Dies bedeutet, dass der C-Compiler die Präprozessordirektiven gar nicht versteht, sondern dafür ein eigenes Werkzeug zuständig ist. Dieses Vorgehen kann man nur historisch aus den Beschränkungen der damaligen Hardware nachvollziehen.
Die zwei Sterne bei char **argv bedeuten, dass es sich um einen Pointer auf einen Pointer handelt, der auf ein Zeichen zeigt. Das ergibt auf den ersten Blick wenig Sinn, denn an der entsprechenden Stelle wird in Java ein Array von Strings übergeben. In C findet sich hier ebenfalls ein Array von Strings, die aber, da es keine Strings gibt, Arrays von Zeichen sind. Wir haben es somit mit einem Array von Arrays von Zeichen zu tun. In Java könnte man sich das dann als char[][] vorstellen. Der letzte Stein zum Verständnis der ** ist, dass ein Array in C als Pointer auf das erste Element verstanden werden kann, z. B. sind int a[] und int* a identisch. Der Zugriff kann entweder über den Index erfolgen a[3] oder aber über den Pointer *(a+3). Damit ist das Geheimnis von **argv für das Erste gelüftet. Details zu Arrays und Pointern werden später noch diskutiert.
Es bleibt noch die Frage, warum C eine Variable int argc hat, Java aber nicht? Der Grund ist, dass in C die Länge eines Arrays nicht bekannt ist und deshalb der main-Funktion mitgegeben werden muss, wie viele Parameter argv enthält. Für die Länge der Strings brauchen wir keine Längenangaben, weil diese Nullterminiert sind, also am Ende ein 0x00-Byte haben.
/* Greet the World */- Kommentar
- Achtung:
//ist kein Kommentarzeichen in ANSI-C (erst seit C99 und in C++) printf("%s", "Hello, World!\n");- gibt einen String aus (→
printfin Java) %sist der Platzhalter, der durchHello, World!\nersetzt wird\nfügt einen Zeilenvorschub einreturn 0;- beendet das Programm und gibt
0als Exit-Code an das Betriebssystem zurück (→System.exit(0)in Java)
Da fast alle C-Compiler C++-Compiler sind, akzeptieren sie // als Kommentarzeichen. Trotzdem ist // kein gültiger C-Kommentar. Da es immer passieren kann, dass ein C-Programm von einem Compiler übersetzt wird, der kein C++ beherrscht, sollte man auf // als Zeilenkommentar verzichten, insbesondere wenn man ältere Compiler antreffen könnte. In C99-Standard wurde // als Kommentarzeichen für Zeilenkommentare endlich auch in C aufgenommen.
Die Verwendung von Format-Strings ist bei C und Java nahezu identisch, was kein Wunder ist, da es Java einfach von C übernommen hat.
Am Ende übergibt jedes Programm einen Integer-Wert an das Betriebssystem um Fehler anzeigen zu können. Per Konvention bedeutet 0, das kein Fehler im Programm aufgetreten ist.