Programmieren mit C


Inhalt


Willkommen

Installation

Das Terminal

Exkurs: Scanf und Sonderzeichen

Exkurs: continue & break

Statische Arrays

Exkurs: sizeof

Dynamische Arrays

Exkurs: Speicherbereiche

Funktionen

Exkurs: Funktionen & Arrays

Kommandozeilenparameter

Structs

Dateien

Anwendung: Textdatei einlesen

Hintergrund: Unicode

Stringbibliothek

Ausblick

C-ähnliche Sprachen

Übersicht

Einstieg in C++

Rust und Go

Dark Mode

Ausblick auf moderne Programmierung

Wir haben uns dieses Semester eine Übersicht über alle Werkzeuge der Sprache C und deren Nutzung verschafft. Dabei haben wir die Landschaft von Programmiersprachen, die seit C entstanden ist - die Sprache stammt aus den 70ern - völlig außer Acht gelassen. In dieser Vorlesung schauen wir uns noch einige neuere Sprachen und die Entwicklung der Syntax in C-ähnlichen Sprachen an.

C-ähnliche Sprachen

Schon seit der Entwicklung der ersten Hochsprachen in den 50ern gibt es verschiedene Ansätze. Einerseits gibt es zwei Unterscheidung bei den Programmierstilen. Sprachen wie C, oder noch früher Fortran, werden als imperativ bezeichnet, während zum Beispiel in LISP oder Haskell ein deklarativer Ansatz gewählt wurde. Während wir bei imperativen Sprachen genaue Anweisungen geben, wie unsere Vorgabe erfüllt werden soll, wird bei deklarativen Sprachen nur das Ziel der Operation.

Auf der anderen Seite wird der Umgang mit verschiedenen Typen unterschieden. In C legen wir Variablen mit Anweisungen wie int x=10; an. Hier legt das Keyword int den Datentypen der Variable x fest. Dieser Typ kann nicht mehr geändert werden und zur Umwandlung müssen wir Funktionen wie float() oder double() nutzen. Hierbei sprechen wir von starken Typen. Einen anderen Ansatz hat hier beispielsweise Python gewählt. Beim Anlegen von Variablen schreiben wir einfach x = 10, um eine Ganzzahl mit Wert 10 anzulegen. Danach können wir der Variable allerdings auch einen String zuweisen x=“Hallo”. Die Typisierung ist also schwach.

Mit C-ähnlich sind also imperative Sprachen mit starker Typisierung gemeint. Seit den 70ern sind auch viele neue Sprachen mit diesen Vorgaben und neuen Werkzeugen entstanden.

Übersicht

Verschaffen wir uns zunächst einen (unvollständigen) Überblick über das Feld. Mitte der 80er Jahre wurde die stark auf C basierende Sprache C++ entwickelt. In den 90ern kam die Sprache Java hinzu, die auch an C++ angelehnt ist. Mit der Verbreitung des Internets ist auch die Vielfalt von Programmiersprachen enorm angestiegen. Neben den weiterhin populärsten imperativen Sprachen C, C++ und Java, entstehen einige kleinere Sprachen, die aktuell an Popularität gewinnen. Die ursprünglichen Entwickler von C haben bei Google die Sprache Go, mit einem besonderen Fokus auf eine einfache Syntax, entwickelt. Einige Entwickler von Mozilla haben die Sprache Rust mit einem Fokus auf Performance und Speichersicherheit entworfen. Daneben sind auch aus Community-Projekten Sprachen wie Zig, V, oder Hare entstanden.

Alle genannten Sprachen haben verschiedene Ansätze und Vor-/Nachteile, können aber auch für Programmierprojekte aller Art genutzt werden. Im Folgenden konzentrieren wir uns auf drei der populärsten Optionen: C++, Go und Rust.

Einstieg in C++

Die älteste der Sprachen ist C++. Anders als der Name vermuten lässt, ist sie eine komplett eigenständige Sprache, die nur auf C basiert. Im Vergleich zu C wurden die objektorientierten Werkzeuge stark ausgebaut. Auch besitzt C++ eine der umfangreichsten Standardbibliotheken aller Programmiersprachen.

Ein- und Ausgabe

Schauen wir uns mal ein kurzes Programm an. Wir starten wieder mit include-Statements. Für den Anfang wollen wir, wie auch in C, die Ein- und Ausgabe ermöglichen.

#include <iostream>

int main() {

}

Der Aufbau des Programms ist bis hier wie gewohnt. Statt stdio.h heißt die erforderliche Bibliothek nun iostream, das include-Statement sieht allerdings in C und C++ gleich aus. Auch beim Hauptteil des Programmes, in der main() Funktion, gibt es keine Änderungen.

#include <iostream>

int main() {
    std::cout << "Hallo!" << std::endl;
}

Beim Aufruf der Ausgabe Funktion sehen wir nun einige Neuerungen. Das Analogon zu printf() ist nun die Funktion cout. Diese ist Teil der Standardbibliothek, also im Namespace std. Ein Namespace ist hierbei ein Raum, in dem die Namen von Funktionen, Variablen, … leben. Dieses Werkzeug ermöglicht verschiedenen Bibliotheken Objekte mit den gleichen Namen zu implementieren. Würden wir nun also eine andere Bibliothek mit dem Namen test importieren, die auch eine Funktion cout implementiert, so können wir diese per test::cout aufrufen. Die Ein- und Ausgabe wurde in Streams organisiert. Mit dem << Operator leiten wir ein zur Ausgabe bestimmtes Objekt an cout weiter. In obigem Beispiel ist das der String “Hallo!” und der Rückgabewert der Funktion endl(). Diese gibt uns die für das aktuelle Betriebssystem angepassten Zeichen für einen Zeilenumbruch zurück, im Falle von Unix also “\n”.

Wollen wir nun Variablen ausgeben, fügen wir diese nur dem Outputstream hinzu.

#include <iostream>

int main() {
    int x = 10;
    std::cout << x << std::endl;
}

Wollen wir stattdessen etwas in diese Variable einlesen, nutzen wir das gleiche Prinzip mit der Funktion cin().

#include <iostream>

int main() {
    int x = 10;

    std::cin >> x;

    std::cout << x << std::endl;
}

Um unser Programm zu kompilieren, können wir g++ nutzen. Dieser Compiler ist Teil des gcc Projekts, wir müssen also auch nichts zusätzlich installieren.

$ g++ -Wall mein_programm.cpp -o mein_programm
$ ./mein_programm

Die Optionen von gcc funktionieren hier auch analog.

Default arguments

Ein sehr nützliches neues Feature sind default arguments in Funktionen.

#include <iostream>

int getMax(int *arr, int N, int first=0) {
	int max_idx = first;
	for(int i=first+1; i<N; ++i) {
		if ( arr[i] > arr[max_idx] ) {
			max_idx = i;
		}
	}
	return max_idx;
}

int main() {
	int arr[] = { 6, 2, 3, 4 };

	std::cout << getMax(arr, 4) << std::endl;    // 0
    std::cout << getMax(arr, 4, 2) << std::endl; // 3
}

Wir haben eine Funktion geschrieben, die uns die größte Zahl in einem Array sucht. Der Aufbau ist analog zu C, allerdings hat unser drittes Argument first den Standardwert null. Rufen wir die Funktion also nur mit zwei Argumenten auf, wie für first der Wert null eingesetzt. Damit erhalten wir als Rückgabewert auch 0, da das erste Element im Array auch das größte ist. Rufen wir dieselbe Funktion nun mit den Argumenten arr, 4, 2 auf, so suchen wir erst ab dem 3. Element. Damit erhalten wir 3 also Rückgabewert, also die Position der Zahl 4 im Array.

Function overloading

Eine weitere Neuerung ist die Nutzung des selben Namens für Funktionen mit unterschiedlichen Argumenten.

#include <iostream>
#include <cmath>

double distance(double x1, double x2, double y1, double y2) {
	return std::sqrt( std::pow(x1-y1,2) + std::pow(x2-y2,2) );
}

double distance(double x1, double x2, double x3,
				double y1, double y2, double y3)
{
	return std::sqrt( std::pow(x1-y1,2)
					+ std::pow(x2-y2,2)
					+ std::pow(x3-y3,2) );
}

int main() {
	std::cout << distance(3.2, 4.2, -1.1, -2.2) << std::endl;
	std::cout << distance(3.2, 4.2, 5.5, -1.1, -2.2, 9.8) << std::endl;
}

Im obigen Beispiel haben wir zwei Funktionen mit dem Namen distance, die die Entfernung zwischen zwei Punkten ausrechnen - einmal in 2D, einmal in 3D. Klassen Eines der wichtigsten Werkzeuge ist die Weiterentwicklung von structs, genannt Klasse. Die große Neuerung ist die Zuordnung von Funktionen zu Klassen.

#include <iostream>
#include <cmath>

class point {
	double x, y;

	public:
		point();
		point(double, double);

		double distance(point);
};

point::point() {
	x = 0.0; y = 0.0;
}
point::point(double x_in, double y_in) {
	x = x_in; y = y_in;
}

double point::distance(point other) {
	return std::sqrt( std::pow(x-other.x,2) + std::pow(y-other.y,2) );
}


int main() {
	point A, B(1.0, 1.0);

	std::cout << A.distance(B) << std::endl;
}

Wir starten mit der Definition unseres Datentyps. Wir wollen einen Punkt auf einer Fläche als Datentyp erstellen, also die x- und y-Koordinate gemeinsam speichern. Analog zu C erstellen wir eine Klasse mit zwei doubles.

class point {
    double x, y;
};

Zusätzlich können wir dieser Klasse nun Funktionen zuordnen. Diese können entweder public, oder private deklariert werden. Alle Teile der Klasse, die public sind, können von außen aufgerufen werden, während alle Teile, die als private definiert sind, nur innerhalb der zugehörigen Funktionen genutzt werden kann. Wollen wir also beispielsweise die wirklichen Daten nicht zugänglich machen, so könnten wir unsere Datenstruktur wie folgt abändern.

class point {
    private:
        double x, y;

    public:
        point();
        point(double, double);

        double distance(point);
};

Der Zugriff auf A.x und A.y ist so nicht mehr zulässig. Wenden wir uns zunächst den beiden Funktionen point() und point(double, double) zu. Diese werden auch als Konstruktor bezeichnet und werden immer aufgerufen, wenn eine Instanz der Klasse erstellt wird. Geht die Instanz out-of-scope wird der Destruktor aufgerufen, der mit ~point() definiert werden könnte. Diese Funktionen müssen wir nicht definieren, da C++ auch immer selbstständig beide erstellt. Oft ist es aber sehr nützlich.

In unserem Beispiel haben wir zwei verschiedene Konstruktoren deklariert. Im Ersten werden keine Argumente übergeben point(), im Zweiten dagegen doubles point(double, double).

Die Definition erfolgt außerhalb der Klasse. Um die Zugehörigkeit kenntlich zu machen, ist dem Namen der Funktion der Name der Klasse und :: vorangestellt. Unser erster Konstruktur point::point() weist der x- und y-Koordinate den Wert 0.0 zu. Der Zweite nimmt dagegen zwei doubles als Argument und weist diese beiden Werte den Koordinaten zu. Aufgerufen werden diese Funktionen in der ersten Zeile der main.

point A, B(1.0, 1.0);

Die Instanz A wird also ohne Werte erstellt, A.x und A.y sind also gleich null. Die Instanz B wird mit den beiden Werten B.x = 1.0 und B.y = 1.0 initialisiert. Zuletzt erstellen wir noch eine Funktion, die den Abstand zwischen zwei Punkten berechnet. Auch diese Funktion soll der Klasse zugewiesen werden. Wir deklarieren sie also im public Block und definieren sie als double point::distance(point other). Diese Funktion wird nun immer aus einer Instanz heraus aufgerufen. Die Werte dieser Instanz können einfach als x und y verwendet werden. Ein zweiter Punkt wird als Argument übergeben. Auf diese Werte können wir - wie gewohnt - mit other.x und other.y zugreifen. Dieser Zugriff ist durch die Einordnung von x,y als private zwar von außerhalb verboten, da wir hier aber eine Funktion innerhalb der Klasse schreiben, ist er erlaubt. Der Aufruf funktioniert nun analog zum Zugriff auf andere Teile der Klasse mit dem . Operator:

A.distance(B)

Rust und Go

Für diese beiden Sprachen sei nur auf die exzellente Dokumentation verwiesen.

Rust-Book Go Tour