Einführung in die Programmiersprache C: HelloWorld und erste Programme

Die wichtigsten Konzepte der Programmiersprache C werden an einfachen Programm-Beispielen erläutert: HelloWorld-Anwendung zum Testen der Arbeitsumgebung, Konsolenein- und ausgaben, Deklaration und Initialisierung von Variablen, arithmetische und logische Operationen, Ablaufsteuerung mit Bedingung und Alternative. Zudem werden einige methodische Hinweise gegeben, wie man gerade als Anfänger beim Programmieren strukturiert vorgehen sollte.

Inhaltsverzeichnis

Einordnung des Artikels

Einführung

Da hier nur eine kurze Einführung in die Programmiersprache C gegeben werden soll, werden die Voraussetzungen zum Programmieren nicht besprochen:

Stattdessen wird vorausgesetzt, dass Sie über eine Arbeitsumgebung verfügen, die erlaubt C-Programme zu erstellen, zu übersetzen und auszuführen. Entwicklungsumgebungen, die dies ermöglichen und die es bereits mit einem "eingebauten" Compiler gibt, sind etwa Code::Blocks oder Eclipse.

Ebenso werden nicht alle Konzepte von C ausführlich vorgestellt. Es werden lediglich Beispiel-Programme für die wichtigsten Konzepte vorgestellt und erläutert.

In diesem ersten Abschnitt wird – wie immer – mit dem Programm HelloWorld begonnen. Es dient weniger dazu, erste Programmier-Konzepte einzuüben, vielmehr soll damit getestet werden, ob die Programmier-Umgebung fehlerfrei arbeitet.

Weiter werden einige Programme vorgestellt, in denen:

Hello World

Um zu testen, ob in der Arbeitsumgebung alle Schritte zum erfolgreichen Programmieren ausgeführt werden können, wird zunächst eine einfache HelloWorld-Anwendung erstellt.

Die nötigen Schritte sind:

Um die Entwicklungsumgebung und ihre Arbeitsweise besser kennenzulernen, sollten Sie bei allen Schritten beobachten, welche Dateien angelegt werden. Dies sollten Sie nicht nur in der Entwicklungsumgebung beobachten, sondern – und dies ist sogar wichtiger – im Datei-Explorer. Denn dort sehen Sie auch diejenigen Dateien, die die Entwicklungsumgebung anlegt und zur Verwaltung des Projektes verwendet.

Anlegen eines ersten Projektes in der Entwicklungsumgebung

Aufgabe:

Beschreiben Sie, welche Dateien im Laufe dieser Schritte im Projekt-Ordner erzeugt wurden und welche Funktion diese Dateien haben.

Können Sie eine Erklärung geben, warum die .exe-Datei ein Vielfaches des Speicherplatzes der Quelltext-Datei benötigt!

Identifizieren Sie diejenige Datei, die zur Projekt-Verwaltung verwendet wird. Öffnen Sie diese Datei mit einem Text-Editor und versuchen Sie die Einträge zu verstehen. Editieren sollten Sie die Datei besser nicht – womöglich wird dadurch das Projekt "zerstört".

Der Quelltext für HelloWorld

In Eclipse wird als Schablone für ein HelloWorld-Projekt folgender Quelltext erzeugt:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    puts("Hello World!!!"); /* prints Hello World!!!

    return EXIT_SUCCESS;
}

In Code::Blocks wird ein ähnlicher Quelltext erzeugt:

#include<stdio.h>

int main(void) {
    printf("Hello World!");
    return 0;
}

Da man die Schablonen selber anpassen kann, ist es möglich, dass Sie in Ihrer Entwicklungsumgebung einen leicht abgewandelten Quelltext sehen – die wichtigsten Elemente müssen aber enthalten sein (die Zeilenangaben beziehen sich auf den Eclipse-Quelltext):

  1. Inkludieren der Datei stdio.h (Zeile 1).
  2. Implementierung der main()-Funktion (Zeile 4 bis 8).
  3. Eine Ausgabe der Zeichenkette HelloWorld (Zeile 5; wie auch immer es geschrieben wird).
  4. Das return-statement der main()-Funktion (Zeile 7).

Eine kurze Erklärung:

  1. Die Datei stdio.h wird benötigt, um Funktionen wie printf() oder puts() einsetzen zu können. Die Funktionen (und viele weitere mit verwandten Aufgaben) sind dort implementiert. Durch die Präprozessor-Direktive #include<stdio.h> wird die Datei geladen. Präprozessor-Direktive bedeutet, dass dies vor dem Compilieren geschieht.
  2. Die main()-Funktion hat ihre besondere Bedeutung als Einstiegspunkt eines Programms: wird das Programm ausgeführt, wird automatisch die main()-Funktion aufgerufen. Der Datentyp int vor dem Namen main() bedeutet, dass die Funktion eine ganze Zahl als Rückgabewert besitzt. Die Konvention ist, dass der Rückgabewert 0 für eine fehlerfreie Ausführung des Programms steht. Im Argument von main() steht void, der "leere Datentyp" – soll heißen, dass main() keinen Eingabewert erwartet.
  3. Die Funktionen puts() und printf() werden eingesetzt, um die Zeichenkette (alles innerhalb der Anführungsstriche) auf der Konsole auszugeben. Die Funktion printf() hat weitere Funktionalitäten, die in HelloWorld noch nicht verwendet werden.
  4. Durch das return-statement wird die Abarbeitung der Funktion main() abgeschlossen. Damit ist zugleich die Abarbeitung des Programms beendet.

Ausführen aus der Konsole

Aufgabe:

Versuchen Sie Ihr HelloWorld-Programm aus der Konsole zu starten.

Methodischer Hinweis: Wie geht man bei der Entwicklung eines Programms vor?

Gerade Anfänger machen oft den Fehler, dass sie eine Aufgabenstellung lesen, sich sofort an die Entwicklungsumgebung setzen – und dann mit Erstaunen feststellen, dass

Abbildung 1 soll die Vorgehensweise beim Programmieren erläutern und Sie sollen sich möglichst früh eine strukturierte Arbeitsweise angewöhnen. Dass man sich an die Entwicklungsumgebung setzt und "sofort loslegt" ist auch für erfahrene Programmierer die Ausnahme – das erlauben nur echte Routine-Aufgaben, nicht aber ernsthafte Programmier-Aufgaben.

Abbildung 1: Vorschlag für die Vorgehensweise beim Programmieren.Abbildung 1: Vorschlag für die Vorgehensweise beim Programmieren.

Links ist die grobe Vorgehensweise zu sehen:

  1. Sie sollten immer zuerst für Ihr Programm einen ersten Entwurf anstreben, in dem Sie das Problem möglichst unabhängig von einer Programmiersprache analysieren. Dazu gehört insbesondere, dass man sämtliche Formeln bereitstellt, die zur Lösung benötigt werden. Das Ergebnis dieser Entwurfsphase könnte ein Programmablaufplan (PAP) oder ein Struktogramm sein. Viele Programmierer begnügen sich auch mit einer Formulierung des Algorithmus in Pseudocode.
  2. Nachdem der Algorithmus trocken entwickelt wurde, sollten Sie sich fragen, welche Programmier-Konzepte in Ihrem Programm benötigt werden. Es ist keine Schande für einen Programmierer, wenn er sich eingestehen muss, dass er einige der benötigten Konzepte nicht ausreichend oder gar nicht kennt. Es ist umgekehrt eine wichtige Arbeitstechnik, dass man seine Lücken benennen und schnell schließen kann. Sie sollten daher immer Lehrbücher und eine Dokumentation der Programmiersprache zur Hand haben und vor dem eigentlichen Programmieren die nötigen Konzepte studieren.
  3. Erst dann sollten Sie sich an die Umsetzung des trocken entwickelten Algorithmus in die gewählte Programmiersprache machen.

Rechts in Abbildung 1 wird versucht, den Prozess des eigentlichen Programmierens darzustellen:

Es ist schwer hier eine zeitliche Abfolge anzugeben. Da sich meist erst bei der Umsetzung des Entwurfs zeigt, ob Sie die nötigen Programmier-Konzepte tatsächlich verstanden haben, und sich im Verlauf der Arbeit neue Probleme ergeben, werden Sie auch jetzt immer wieder in der Theorie nachschlagen müssen. Auch dies sollten Sie als den Normalfall ansehen – oder besser als Gelegenheit, um mit neuen Konzepten vertraut zu werden.

Zudem sollten Sie möglichst schnell jeden neu geschriebenen Quelltext testen – manchmal geschieht dies sogar bei jeder neuen Zeile. Erst erfahrene Programmierer schreiben längere Abschnitte ohne sie zu testen – aber auch nur, wenn sie mit den verwendeten Konzepten gut vertraut sind.

Zu den Tests gehört dann auch, dass man Grenzfälle untersucht, insbesondere Eingaben, die eigentlich nicht vorgesehen sind. Man muss immer davon ausgehen, dass der Nutzer die Implementierung nicht kennt und daher nicht weiß, welche Fehler auftreten können. Er wird irgendwann Eingaben machen, die nicht der eigentlichen Aufgabe entsprechen. Sie als Programmierer sind verantwortlich dafür, diese Fehler abzufangen und einen Programm-Absturz zu vermeiden.

Ein erstes Programm: arithmetische Operationen

Überblick über die Aufgabenstellung

Mit dem ersten eigenen Programm sollen Sie folgende Arbeitstechniken und Programmier-Konzepte kennenlernen:

  1. Ein- und Ausgaben auf der Konsole gemacht werden.
  2. Variablen deklariert und initialisiert werden.
  3. Arithmetische Operationen mit den Variablen ausgeführt werden.

Die Aufgabenstellung

  1. Legen Sie ein Projekt mit einem Namen wie etwa Arithmetik an. Da Sie schon in Ihrem Arbeitsverzeichnis eine geeignete Ordnerstruktur angelegt haben, können Sie dort das Projekt passend einordnen.
  2. Verwenden Sie beim Anlegen des Projektes die Voreinstellungen, die für HelloWorld verwendet wurden. Achten Sie jetzt beim Anlegen des Projektes genauer darauf, welche Voreinstellungen Sie auswählen und ob es vielleicht nützliche Alternativen oder zusätzliche Einstellungen gibt. Verfolgen Sie das Anlegen des Projektes wieder im Datei-Explorer.
  3. Das Programm soll eine Reihe von Anweisungen enthalten, die alle innerhalb der Funktion main() stehen. Diese Anweisungen lauten:

Führen Sie das Programm aus und testen Sie insbesondere:

Der grobe Entwurf ist größtenteils durch die Aufgabenstellung vorgegeben; für einen Programmier-Anfänger sind hier – gegenüber HelloWorld – zahlreiche neue Programmier-Konzepte enthalten.

Mit Ganzzahl-Division ist diejenige Division gemeint, die Sie zuerst in der Schule gelernt haben; so ist etwa

20 / 6 = 3 Rest 2, weil 3 · 6 + 2 = 20.

Aufgaben:

1. Benennen Sie die für die Lösung der Aufgabenstellung benötigten Programmier-Konzepte und beurteilen Sie, inwieweit Sie mit ihnen vertraut sind.

2. Entwickeln Sie das geforderte Programm!

Ein Lösungsvorschlag

Das folgende Listing macht einen Vorschlag, wie man die Programmier-Aufgabe lösen kann. Beachten Sie dabei:

#include <stdio.h>
#include <stdlib.h>

int main(void) {

    // Deklaration:
    int a;
    int b;

    // Eingabe:
    puts("Geben Sie zwei ganze Zahlen ein!");
    fflush(stdout);

    scanf("%i \n %i \n", &a, &b);
    fflush(stdin);
    
    // Ausgabe a, b:
    printf("a = %i, b = %i \n", a, b);
    
    // Arithmetische Operationen:
    printf("a + b = %i \n", a + b);
    printf("a - b = %i \n", a - b);
    printf("a * b = %i \n", a * b);
    printf("Ganzzahl-Division: a / b = %i \n", a / b);
    printf("Division: a / b = %f \n", (double) a / b);
    int r = a % b;
    printf("Rest: a %% b = %i \n", r);
    
    return EXIT_SUCCESS;
}

Für a = 20 und b = 6 lauten die Ausgaben:

a = 20, b = 6 
a + b = 26 
a - b = 14 
a * b = 120 
Ganzzahl-Division: a / b = 3 
Division: a / b = 3.333333 
Rest: a % b = 2

Welche Programmier-Konzepte werden hier verwendet, die über das HelloWorld-Programm hinausgeben?

  1. Es werden nicht nur Ausgaben auf der Konsole gemacht, sondern auch Eingaben (mit scanf()).
  2. Dadurch dass die Ein- und Ausgaben abwechselnd erfolgen, ist es nötig, die Ein- und Ausgabe-Puffer zu leeren (mit fflush()).
  3. Es werden Variable angelegt (hier mit Datentyp int) und zwar zuerst deklariert und mit der Konsoleneingabe initialisiert.
  4. Es werden arithmetische Operationen ausgeführt; die Divisionen beinhalten einige Spitzfindigkeiten, insbesondere eine Typumwandlung.

Aufgabe:

Diskutieren Sie: In Zeile 26 wird zur Berechnung des Restes die Variable r angelegt, die in Zeile 27 ausgegeben wird. Bei den anderen Operationen wurde keinen eigene Variable verwendet. Welches Verfahren ist vorzuziehen?

Lösung:

Da mit den Ergebnissen wie a + b nicht weitergerechnet wird, ist es nicht nötig, sie in einer Variable abzuspeichern. Dadurch wird nur Rechenzeit und Speicherplatz verschwendet.

Für den Leser ist lediglich besser nachzuvollziehen, welche Berechnungen durchgeführt werden, wenn eine Anweisung wie in Zeile 26 explizit angegeben wird.

In Zukunft sollten Sie bei allen Berechnungen überlegen, ob das Anlegen einer neuen Variable nötig ist; gerade wenn es in langen Schleifen vorkommt, kann dies die Speicherauslastung wesentlich beeinflussen.

Konsolenausgaben und -eingaben

Die Funktion printf()

Im HelloWorld-Programm wird die Funktion printf() eingesetzt wie puts(): sie erwartet eine Zeichenkette als Argument und diese Zeichenkette wird auf der Konsole ausgegeben.

Das Arithmetik-Programm oben zeigt die Verwendung von printf(), die puts() nicht leisten kann: Vergleicht man etwa Zeile 18 und Zeile 21, so sieht man, dass printf() auch zwei oder drei Argumente aufnehmen kann. Im Allgemeinen können es beliebig viele Argumente sein, allerdings mit der Einschränkung:

Dabei kann die Zeichenkette auch Formatierungsanweisungen enthalten.

Eine Anweisung wie printf("a = %i, b = %i \n", a, b); ist dann folgendermaßen zu lesen:

Damit sollte die Ausgabe a = 20, b = 6 verständlich sein.

Möchte man Fließkommazahlen ausgeben, lautet die Formatierungsanweisung %f (f wie floating), siehe etwa Zeile 25.

Es gibt noch eine Vielzahl weiterer Formatierungsanweisungen, die man in der C-Dokumentation zu printf() nachlesen kann.

Die Funktion scanf()

Die Funktion scanf() erwartet wie printf() eine Zeichenkette und eine Liste von Variablen wie in scanf("%i \n %i \n", &a, &b); (Zeile 14). Der Unterschied besteht aber darin, dass nicht die Variablen in der Liste stehen, sondern deren Adressen wie &a , was an dem "kaufmännischen UND" erkennbar ist. Was genau mit den Adressen der Variable gemeint ist, wird erst verständlich, wenn Zeiger besprochen werden. Zunächst sollte man es so verstehen: ein Anweisung wie in Zeile 14 sorgt dafür, dass die Variablen a und b die Werte annehmen, die auf der Konsole eingegeben werden.

Das Leeren des Ein- und Ausgabe-Puffers mit fflush()

Im HelloWorld-Programm war keine fflush()-Anweisung enthalten. Erst wenn abwechselnd Ein- und Ausgaben gemacht werden ist es nötig, die entsprechenden Puffer zu löschen, ganauer:

Aufgabe.

Testen Sie obiges Programm, wenn Sie die fflush()-Anweisungen weglassen!

Deklaration und Initialisierung von Variablen

Mit einer Anweisung wie int a; (siehe Zeile 7) wird eine Variable vom Datentyp int (Abkürzung für integer) deklariert. Damit ist gemeint, dass

  1. ein Speicherplatz reserviert wird, dessen Größe einer int-Variable entspricht, und
  2. dieser Speicherplatz mit Hilfe des Namens der Variable (hier a) angesprochen werden kann.

Die Initialisierung der Variable a geschieht oben mit Hilfe der scanf()-Anweisung, das heißt in der Speicherzelle von a wird der Wert abgelegt, der auf der Konsole eingegeben wird. Am häufigsten geschieht die Initialisierung sofort mit der Deklaration, etwa mit int n = 17; .

Aufgabe:

Testen Sie, welchen Wert man erhält, wenn man eine Variable deklariert, noch nicht initialisiert und sofort ihren Wert mit printf() ausgibt.

Testen Sie dies mit verschiedenen Variablen. Ist des Ergebnis reproduzierbar?

Man sagt nicht-initialisierte Variablen haben einen "unbestimmten Wert". Können Sie eine Erklärung geben, in welchem Sinn der Wert "unbestimmt" ist?

Arithmetische Operationen

Die Operationen + , - und * erklären sich selbst. Deutlich schwieriger ist die Division, da man zwischen der Ganzzahl-Division und der Division von Fließkommazahlen unterscheiden muss. In vielen Programmiersprachen gibt es eigene Operatoren für diese beiden Divisionen, in C wird immer das Symbol / verwendet, aber seine Bedeutung hängt vom Datentyp der Variablen ab, die dividiert werden.

Es gelten die Regeln:

  1. Sind Zähler und Nenner von ganzzahligem Datentyp, steht der Operator / für die Ganzzahl-Division.
  2. Sind Zähler oder Nenner (oder beide) Fließkommazahlen, dann steht der Operator / für die Division von Fließkommazahlen.

Daher wird in Zeile 25 die Typumwandlung (double) a vorgenommen, womit a in eine Fließkommazahl (double) verwandelt wird. Man hätte auch Zähler und Nenner umwandeln können.

Wirkungslos wäre aber (double) (a / b) ; denn jetzt wird zuerst die Ganzzahl-Division ausgeführt (mit ganzzahligem Ergebnis) und dann erst die Typumwandlung vorgenommen.

Der Rest einer Ganzzahl-Division wird mit dem Operator % berechnet (siehe Zeile 26). In der Mathematik bezeichnet man den Operator meist mit mod (Abkürzung für modulo).

Aufgabe:

Nehmen Sie jetzt Ihren eigenen Quelltext unter die Lupe und prüfen Sie:

Versuchen Sie Ihren Quelltext zu verbessern!

Methodischer Hinweis:

Auch ein gesundes Maß an Selbstkritik gehört zu den Arbeitstechniken eines Programmierers. Man sollte sich nicht mit einer Lösung zufrieden geben, die zwar den gestellten Anforderungen gerecht wird, aber offensichtlich "schlecht" programmiert ist. Gerade wenn Programme weiterentwickelt werden sollen, wird man auf "saubere" Quelltexte achten.

Ein zweites Programm: logische Operationen und Ablaufsteuerung

Um den Ablauf eines Programms zu steuern, setzt man

Die Bedingungsprüfung erfolgt mittels if, eine Alternative wird mit if else realisiert.

Das folgende Listing zeigt zwei einfache Beispiele:

// Bedingungsprüfung:

// x ist eine bereits initialisierte Variable
if(x < 0){
    x = -x;
}

// Alternative:

// x ist initialisiert, y deklariert
if(x < 0){
    y = -x;
} else {
    y = x;
}

In der Bedingungsprüfung if() kann natürlich auch ein komplexer Ausdruck stehen, der logische Operationen einsetzt. Wieder zwei einfache Beispiele:

// x und y initialisiert

// Logisches UND &&
if(x > 0 && y > 0){
    puts("x * y ist positiv");
}

// Logisches ODER ||
if( (x > 0 && y < 0) || (x < 0 && y > 0) ){
    puts("x * y ist negativ");
}

Die folgende Aufgabe soll den Umgang mit logischen Operationen und der Ablaufsteuerung einüben.

Die Aufgabenstellung: Sortieren dreier Zahlen

Der Nutzer soll drei ganze Zahlen eingeben. Die Zahlen sollen anschließend sortiert ausgegeben werden – und zwar mit der größten Zahl zuerst.

Vorschlag zur Lösung

Es gibt unzählige Möglichkeiten, wie die Lösung der Aufgabe aussehen kann. Im Folgenden wird eine Lösung gezeigt,

Sie sollten sich bereits jetzt merken:

#include <stdio.h>
#include <stdlib.h>

int main(void) {

    int a = 20;
    int b = 30;
    int c = 40;

    int max, med, min;

    if (a > b && a > c) {               // a ist Max.
        max = a;
        if (b > c) {                    // a > b > c
            med = b;
            min = c;
        } else {                        // a > c > b
            med = c;
            min = b;
        }
    } else {                            // b oder c ist Max.
        if (b > c) {                    // b ist Max.
            max = b;
            if (a > c) {                // b > a > c
                med = a;
                min = c;
            } else {                    // b > c > a
                med = c;
                min = a;
            }
        } else {                        // c ist Max.
            max = c;
            if(a > b){                  // c > a > b
                med = a;
                min = b;
            } else {                    // c > b > a
                med = b;
                min = a;
            }
        }
    }

    // Ausgaben:
    printf("a = %i, b = %i, c = %i \n", a, b, c);
    printf("max = %i, med = %i, min = %i", max, med, min);

    return EXIT_SUCCESS;
}

Bemerkung:

Laut Aufgabenstellung muss die Eingabe mit scanf() realisiert werden. Der Einfachheit halber wurde es vermieden – wie man dies realisiert, wurde in der letzten Aufgabe gezeigt.

Die Ausgaben lauten:

a = 20, b = 30, c = 40 
max = 40, med = 30, min = 20

Methodischer Hinweis:

Wie gesagt ist der Quelltext unnötig aufgebläht; mit den bis jetzt zur Verfügung stehenden Mitteln ist es aber schwer, eine einfachere Lösung anzugeben. Sie sollten aber aus dem Beispiel unbedingt lernen:

Eine konsequente Quelltext-Formatierung erleichtert das Lesen.

Zur Quelltext-Formatierung gehören: