Computerpraktikum SS2009 I Einführung in Linux und der Programmierung in C++ Ein die Veranstaltung ”Computerpraktikum” ergänzendes Skript von Matthias Knauer, Jörg Benke und Hartmut Jürgens Bremen, 5. April 2009 ii iii Inhaltsverzeichnis Inhaltsverzeichnis Inhaltsverzeichnis iii Vorwort bzw. Disclaimer 1 Einführung in UNIX / Linux 1.1 Betriebssysteme . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.1.1 Aufbau eines Computers . . . . . . . . . . . . . . . . . . 1.1.2 UNIX / Linux . . . . . . . . . . . . . . . . . . . . . . . 1.2 Die Etikette im Rechnerpool des Fachbereichsnetzes . . . . . . 1.3 An- und Abmelden beim System . . . . . . . . . . . . . . . . . 1.4 Aufbau von UNIX-Kommandos . . . . . . . . . . . . . . . . . . 1.5 Editoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.6 Drucken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.7 Verzeichnisse und Dateien . . . . . . . . . . . . . . . . . . . . . 1.7.1 Grundgerüst des Verzeichnisbaumes . . . . . . . . . . . 1.7.2 Dateien und Pfade . . . . . . . . . . . . . . . . . . . . . 1.7.3 Navigation im Verzeichnisbaum . . . . . . . . . . . . . . 1.7.4 Ausgabe von Inhaltsverzeichnissen . . . . . . . . . . . . 1.7.5 Zugriffsrechte für Dateien und Verzeichnisse . . . . . . . 1.7.6 Numerische und symbolische Form der Rechteänderung 1.8 UNIX-Shells . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8.1 Sonderzeichen . . . . . . . . . . . . . . . . . . . . . . . . 1.8.2 Shell-Variablen . . . . . . . . . . . . . . . . . . . . . . . 1.8.3 Kommandos . . . . . . . . . . . . . . . . . . . . . . . . . 1.8.4 Platzhalter in Dateinamen . . . . . . . . . . . . . . . . . 1.8.5 Umleitung . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8.6 Vorverarbeitung der Kommandos . . . . . . . . . . . . . 1.9 Prozesse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.10 Arbeiten auf entfernten Rechnern . . . . . . . . . . . . . . . . . 1.11 Weitere nützliche Befehle . . . . . . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . . . . . . . . . . . 2 2 2 5 7 8 10 11 14 15 15 16 17 19 20 21 23 23 24 25 26 27 28 29 31 31 2 Algorithmen 35 2.1 Begriff . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 2.2 Sequenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 2.3 Auswahl (Selektion) . . . . . . . . . . . . . . . . . . . . . . . . . 37 2.4 Schleifen (Wiederholung, Iteration) . . . . . . . . . . . . . . . . . 38 2.4.1 Maximumsbestimmung (Bsp. zur 2. bzw. 1. Schleifenart) 40 2.4.2 Berechnung von Potenzen xn (Bsp. für Zählschleife) . . . 40 2.5 Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 2.6 Vom Algorithmus zum Programm . . . . . . . . . . . . . . . . . . 42 iv Inhaltsverzeichnis 3 Einführung in C/C++ 3.1 Überblick . . . . . . . . . . . . . . . . . . . . . . 3.1.1 Geschichte . . . . . . . . . . . . . . . . . . 3.1.2 Compiler und Tools . . . . . . . . . . . . 3.1.3 Das nullte und kürzeste C++-Programm 3.2 Hello, world! . . . . . . . . . . . . . . . . . . . . 3.3 Fundamentale Datentypen . . . . . . . . . . . . . 3.3.1 Deklaration . . . . . . . . . . . . . . . . . 3.3.2 Ganzzahltypen . . . . . . . . . . . . . . . 3.3.3 Grundrechenarten . . . . . . . . . . . . . 3.3.4 Bereichsüberschreitung . . . . . . . . . . . 3.3.5 Zuweisungsoperatoren . . . . . . . . . . . 3.3.6 Inkrement- und Dekrementoperatoren . . 3.3.7 Literale . . . . . . . . . . . . . . . . . . . 3.3.8 Bezeichner . . . . . . . . . . . . . . . . . 3.3.9 Fliesskommatypen . . . . . . . . . . . . . 3.3.10 Typumwandlungen . . . . . . . . . . . . . 3.3.11 Konstanten . . . . . . . . . . . . . . . . . 3.4 Ein- und Ausgabe . . . . . . . . . . . . . . . . . 3.4.1 Standardausgabe . . . . . . . . . . . . . . 3.4.2 Standardeingabe . . . . . . . . . . . . . . 3.4.3 Standardfehlerausgabe . . . . . . . . . . . 3.5 Anweisungen zur Auswahl . . . . . . . . . . . . . 3.5.1 Boolesche Werte . . . . . . . . . . . . . . 3.5.2 Logische Operatoren . . . . . . . . . . . . 3.5.3 Relationale Operatoren . . . . . . . . . . 3.5.4 if-Anweisungen . . . . . . . . . . . . . . 3.5.5 Bedingte Ausdrücke mit ?: . . . . . . . . 3.5.6 switch-Anweisungen . . . . . . . . . . . . 3.6 Funktionen . . . . . . . . . . . . . . . . . . . . . 3.6.1 Definition . . . . . . . . . . . . . . . . . . 3.6.2 Rückgabetyp . . . . . . . . . . . . . . . . 3.6.3 Funktionsparameter . . . . . . . . . . . . 3.6.4 Call by value . . . . . . . . . . . . . . . . 3.6.5 Call by reference . . . . . . . . . . . . . . 3.6.6 Gültigkeitsbereich von Variablen . . . . . 3.6.7 Rekursive Funktionen . . . . . . . . . . . 3.6.8 Mathematische Funktionen . . . . . . . . 3.7 Schleifen . . . . . . . . . . . . . . . . . . . . . . . 3.7.1 while-Schleifen . . . . . . . . . . . . . . . 3.7.2 for-Schleifen . . . . . . . . . . . . . . . . 3.7.3 Sprungbefehle break und continue . . . 3.7.4 Zusammenfassung Schleifen . . . . . . . . 3.7.5 Der Sprungbefehl goto . . . . . . . . . . . 3.8 Felder . . . . . . . . . . . . . . . . . . . . . . . . 3.8.1 Felder als Funktionsparameter . . . . . . 3.9 Präprozessoranweisungen . . . . . . . . . . . . . 3.9.1 #define . . . . . . . . . . . . . . . . . . . 3.9.2 #include . . . . . . . . . . . . . . . . . . 3.10 Formatierte Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 45 45 47 49 51 53 54 55 56 57 57 58 58 58 59 60 61 62 62 63 64 64 64 65 65 67 69 69 70 70 72 73 73 74 75 77 78 79 80 81 83 84 85 86 87 89 89 90 90 Programmverzeichnis v Bit-Operatoren . . . . . . . . . . . . . . . . . . . . . . . . Formatierung über Elementfunktionen . . . . . . . . . . . Manipulatoren . . . . . . . . . . . . . . . . . . . . . . . . Beispiel: Berechnung von π nach Leibniz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zeigervariablen . . . . . . . . . . . . . . . . . . . . . . . . Speicher reservieren mit new . . . . . . . . . . . . . . . . Speicher freigeben mit delete . . . . . . . . . . . . . . . Zugriff auf Zeigerwerte . . . . . . . . . . . . . . . . . . . . Benutzung des Referenzierungs- und Dereferenzierungsoperators . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.11.6 Zeiger als Funktionsparameter . . . . . . . . . . . . . . . 3.11.7 Weitere Zeiger-Typen . . . . . . . . . . . . . . . . . . . . 3.11.8 Zeigerarithmetik . . . . . . . . . . . . . . . . . . . . . . . 3.11.9 Sichere Allozierung . . . . . . . . . . . . . . . . . . . . . . 3.12 Zeichenketten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.12.1 Der Datentyp char . . . . . . . . . . . . . . . . . . . . . . 3.12.2 Die Funktionen von ctype.h . . . . . . . . . . . . . . . . 3.12.3 Steuerzeichen . . . . . . . . . . . . . . . . . . . . . . . . . 3.12.4 Zeichenketten . . . . . . . . . . . . . . . . . . . . . . . . . 3.12.5 Ein- und Ausgabe von Zeichenketten . . . . . . . . . . . . 3.12.6 Die Funktionen von string.h . . . . . . . . . . . . . . . 3.12.7 Argumente für die Funktion main . . . . . . . . . . . . . . 3.13 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.13.1 Konstruktoren für Strings . . . . . . . . . . . . . . . . . . 3.13.2 Verändern von Strings . . . . . . . . . . . . . . . . . . . . 3.13.3 Informationen über Strings . . . . . . . . . . . . . . . . . 3.13.4 Vergleiche und Tests . . . . . . . . . . . . . . . . . . . . . 3.13.5 Umwandeln in Zeichen . . . . . . . . . . . . . . . . . . . . 3.13.6 Suchen in Strings . . . . . . . . . . . . . . . . . . . . . . . 3.13.7 Sonstiges . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.14 Umgang mit Dateien . . . . . . . . . . . . . . . . . . . . . . . . . 90 91 94 95 96 96 97 98 99 3.10.1 3.10.2 3.10.3 3.10.4 3.11 Zeiger 3.11.1 3.11.2 3.11.3 3.11.4 3.11.5 100 100 102 105 106 107 107 108 109 109 111 111 112 113 114 114 116 116 117 118 118 119 A Header 122 A.1 Die Funktionen von ctype.h . . . . . . . . . . . . . . . . . . . . 122 A.2 Die Funktionen von string.h . . . . . . . . . . . . . . . . . . . . 123 A.3 Mathematische Funktionen . . . . . . . . . . . . . . . . . . . . . 124 B Kompatibilitäten zu C B.1 Ein-/Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . B.1.1 Formatierungsanweisungen für Ganzzahlen . . . . B.1.2 Formatierungsanweisungen für Fließkommazahlen . B.1.3 Eingabe mit scanf . . . . . . . . . . . . . . . . . . B.1.4 Ein-/Ausgabe in C-Strings . . . . . . . . . . . . . B.1.5 Einzelne Zeichen . . . . . . . . . . . . . . . . . . . B.2 Speicherverwaltung . . . . . . . . . . . . . . . . . . . . . . Programmverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 126 127 129 131 132 132 132 134 vi Programmverzeichnis 1 Vorwort bzw. Disclaimer Dieses Skript wurde in seiner erster Fassung von Matthias Knauer erstellt. Es begleitete den ersten Teil einer zweisemestriegen Blockveranstaltung Rechnerpraktikum I und II. Seit 2006 wurde die Veranstaltung schrittweise überarbeitet und weiter entwickelt. 2009 wird sie erstmals als eine im laufenden Semester gehaltene Vorlesung unter dem Titel ”Computerpraktikum” gehalten. Die Vorlesung orientiert sich in diesem Semester an einer ca. 350 seitigen Präsentation. Diese Unterlagen werden den Studierenden im Laufe der Veranstaltung als aktuelles Referenzmaterial sukzessive zur Verfügung gestellt. Das Skript kann als zusätzliche Information herangezogen werden. Es entspricht aber nicht immer dem Verlauf der Vorlesung, die den neuen Gegebenheiten angepasst werden musste. Obwohl die Authoren bei der Erstellung grosse Sorgfalt haben walten lassen, können gerade aufgrund der sukzessiven Anpassung Fehler im Skript verblieben sein. Für diese und eventuell daraus entstehende Folgen können wir Autoren weder eine juristische noch irgendeine Haftung übernehmen. Bremen, 5. April 2009 2 Kapitel 1: Einführung in UNIX / Linux 1 Einführung Linux in UNIX / 1.1 Betriebssysteme 1.1.1 Aufbau eines Computers Hardwarekomponenten eines Computer-Systems (ganz grob) Hardware ist der Oberbegriff für die maschinentechnische Ausrüstung eines Computersystems. Dazu gehören Prozessor und Arbeitsspeicher (inkl. Cache) und seine (internen und externen) Peripheriegeräte (z.B. Festplatte (intern oder extern), Grafikkarte (intern) oder Drucker (extern), Monitor (extern)). Vereinfacht gesagt gehört alles, was angefasst werden kann, zur Hardware. • Ein Computer besteht grundsätzlich aus Prozessor (CPU), Arbeitsspeicher (RAM) und Peripherie, welche über ein Bus-System (Leitungssytem) miteinander verbunden sind, um Daten und Steuersignale zu übertragen. • Wird ein Programm gestartet, so wird dieses z.B. von der Festplatte in den Arbeitsspeicher geladen und durch die CPU ausgeführt (ergibt Prozess, d.h. ein Programm in Ausführung). Ebenfalls werden dort die Daten abgespeichert, die die Programme erzeugen (z.B. Texte). • Ein Programm besteht aus einer endlichen Folge von Anweisungen (Befehlen) und strukturierten Daten, welche im Arbeitsspeicher abgelegt werden. Der Prozessor holt sich die Anweisungen des Programms nacheinander aus dem Speicher, verarbeitet sie, und legt die evtl. erzeugten Ergebnisse des Anweisungsschrittes wieder im Speicher ab. 1.1 Betriebssysteme 3 • Der Inhalt des Arbeitsspeichers ist flüchtig, d.h. nach dem Abschalten des Computers geht der Inhalt des Arbeitsspeichers komplett verloren. Somit wichtig: Speicherung von weiterhin benötigten Daten und Programmen auf externe Speichermedien (Festplatte, CD/DVD, ...). • Der interne Speicher (d.h. der Arbeitsspeicher) ist in Speicherzellen organisiert, die man sich nacheinander (sequentiell) aneinandergereiht vorstellen kann (Numerierung nacheinander von 0 aufwärts). Der Zugriff auf die Speicherzellen erfolgt über diese eindeutige Nummer (Adresse). • Die Peripherie bzw. die Peripheriegeräte sind alle Komponenten bzw. Geräte, die sich ausserhalb der Zentraleinheit und des Arbeitsspeicher eines Computers befinden, z.B. Ein- und Ausgabegeräte, Karten, etc. Zusammenfassung wichtiger Hardwarekomponenten eines Computers: • Prozessor (engl. CPU = Central Processing Unit) • Interne Speicher: RAM, Cache, . . . • Externe Speicher: Festplatte, CD/DVD, Diskette, Speicher-Stick, . . . • Eingabegeräte: Tastatur, Maus, Touchpad, Scanner, . . . • Ausgabegeräte: Bildschirm, Drucker, . . . • Karten (Grafik, Sound, Netzwerk, . . . ) • Gehäuse, Stromversorgung, Mainboard • ... Weitergehende Literatur zu diesem Thema im Netz: • http://www.wikipedia.de (Stichwort Computer, . . . ) • http://quantenleser.de/computerbuch/ (Einführende Behandlung, ca. 35 S.! Gut, um einen Überblick zu bekommen.) • http://www.thgweb.de/lexikon/Hauptseite (Texte teilweise aus Wikipedia übernommen) Software eines Computer-Systems Der Begriff Software bezeichnet alle nichtphysischen Funktionsbestandteile eines Computers bzw. eines jeden technischen Gegenstandes, der mindestens einen Mikroprozessor enthält. Dies umfasst vor allem Computerprogramme sowie die zur Verwendung mit Computerprogrammen bestimmten Daten (. . . ) (Wikipedia) Beispiele: • Betriebssysteme: Unix / Linux, Windows XP, . . . • Büroprogramme: MS Office (Word, . . . ), StarOffice (Writer, . . . ) 4 Kapitel 1: Einführung in UNIX / Linux • Webbrowser: Internet Explorer, Firefox, . . . • ... Unterscheidungsformen: • Systemsoftware (Computerprogramme, die für den Betrieb des Computers nützliche Funktionen ausführen, d.h. das Betriebssystem, systemnahe Software (z.B. Datenbankverwaltungswerkzeuge) und Übersetzungsprogramme (siehe unten) • Anwendungsprogramme (Computerprogramme, die eine für den Anwender nützliche Funktion ausführen, z.B. Büro-Programme, Datenbanken, CADProgramme, Buchhaltungsprogramme) • Compiler, Interpreter (Computerprogramme, die ein in einer Programmiersprache geschriebenes Programm (z.B. C), in eine für den Computer verständliche „Sprache“ übersetzen, d.h. Übersetzungsprogramme) All dies ist für uns nur sinnvoll nutzbar, weil es ein Betriebssystem gibt, das uns viel Arbeit abnimmt, z.B. • Tastatureingabe erkennen und weiterleiten, • Programme starten, deren ordnungsgemäßen Ablauf kontrollieren (z.B. dass sich 2 „parallel“ laufende Programme ihre Speicherinhalte nicht gegenseitig überschreiben) und das Programm beenden, • Dateien speichern und verwalten, • Verteilung der Prozessorzeit an die laufenden Prozesse beim Multitasking (Scheduling), • Synchronisation von Prozessen, um Zugriffskonflikte und Prozessorwartezeiten zu vermeiden. Somit bildet das Betriebssystem die Grundlage dafür, dass ein Benutzer den Rechner überhaupt sinnvoll benutzen kann, indem es Dienste zur Verfügung stellt und bestimmte Verwaltungsaufgaben übernimmt. Es verwaltet Betriebsmittel wie Speicher, Ein- und Ausgabegeräte und steuert die Ausführung von Programmen (Prozessverwaltung). Ein Betriebssystem besteht grundsätzlich aus 3 bzw. 4 Komponenten: • Kern (engl. kernel), d.h. der Teil des Betriebssystems, der mit der Hardware kommuniziert und grundlegende Dienste bereit stellt, z.B. Dateien öffnen oder schließen, Dateien schreiben oder lesen, Speicher- und Prozessverwaltung. Der Kern des Betriebssystems dient somit hauptsächlich der Verwaltung von Programmen, Dateien, dem Speicher, Festplatten, Netzwerken, etc. • Dienstprogramme (engl. utilities), wie z.B. UNIX-Kommandos zum Anzeigen von Verzeichnissen oder Dateiinhalten. 1.1 Betriebssysteme 5 • Shell, d.h. eine Benutzerschnittstelle, die Befehle bzw. Kommandos entgegennimmt (über Tastatur), diese mittels eines Kommandointerpreters ausführt und eine evtl. vorhandene Ausgabe anzeigt. Es gibt mehrere Shells, wobei die bekanntesten die Bourne-Again-shell ( bash) oder die Korn-Shell sind. • Manche Autoren zählen auch noch die grafische Benutzeroberfläche zum Betriebssystem (später). Beispiele für Betriebssysteme • UNIX und seine Derivate (BSD, Linux, FreeBSD, Solaris, Darwin / Mac OS X, . . . ) • MS-DOS, Windows XP, Windows 2000, . . . • MacOS (eigenständiges System bis Version 9), AmigaOS, TOS (Atari), ... 1.1.2 UNIX / Linux Merkmale • Programme können (scheinbar) gleichzeitig ablaufen (MultitaskingSystem) • Mehrbenutzerbetrieb, netzwerkfähig (Multiuser-System) • Rechnerunabhängigkeit, Portabilität (d.h. Unix kann leicht auf andere Rechner angepasst werden, da es modular aufgebaut und größtenteils in C geschrieben ist) • hohe Stabilität und Sicherheit • leistungsfähige Benutzerschnittstellen (Shells) Historische Entwicklung ∼1970 erste UNIX-Version, BellLabs (Entwicklungszentrum von AT&T) u.a Thompson, Ritchie 1973 Neuimplentierung des Unix-Kerns in C ∼1975 Weiterentwicklungen von Unix, u.a. University of Berkeley (eigene UnixVersion: Berkeley Software Distribution-Unix bzw. BSD-Unix) 1983 UNIX V5 (AT&T-Unix-Version), kommerzielle Version ab 1988 POSIX-Standardisierung (Portable Operating System Interface), d.h. Festlegung der Mindestanforderungen an ein UNIX-System (weitgehend abgeschlossen in 2001) 6 Kapitel 1: Einführung in UNIX / Linux ab1991 Entwicklung von LINUX als UNIX-Derivat für Intel386-Prozessor, Linus Torvalds u.v.a. im Rahmen der GNU/GPL, d.h. Open Source. ∼1993 FreeBSD (alternative Open Souce Entwicklung). ∼1999 Darwin (Apple Open Source Release of Unix u.a. FreeBSD basiert) Das Skript betrachtet ursprünglich primär Linux: • im Rahmen der Fedora-Distribution, • mit der grafischen Benutzeroberfläche KDE, wobei die einzelnen Distributionen (z.B. Fedora/Redhat, SUSE/Novell, Ubuntu/Canonical, Debian) alle Linux enthalten, und entsprechende zusätzliche (meist freie) Software, abhängig davon, für welchen Zweck sie zusammengestellt wurden. In 2009 wird das Praktikum auf Apple iMac durchgefuehrt (siehe nachfolgend). Grafische Benutzeroberfläche Früher kommunizierte der Benutzer über Kommandozeile mit dem System, z.B. MS-DOS, C64. Heute existieren eine Reihe grafischer Benutzeroberflächen (GUIs) für unterschiedlichste Systeme. Speziell für Linux sind dies z.B. KDE oder Gnome, welche prinzipiell ähnlich zu Windows aufgebaut sind bzw. ein ähnliches Verhalten aufweisen. Grafische Benutzeroberflächen sollen dem Benutzer einen intuitiven Umgang mit dem Computer ermöglichen. Kernbestandteil dieser grafischen Systeme ist das Fenster, d.h. ein rechteckiger Bereich, in dem z.B. Ausgaben dargestellt werden. Insgesamt wird der Bildschirm als Schreibtisch interpretiert, auf dem die Fenster dann übereinander gelegt werden, in Analogie zu einem Stapel Papier, auf dem Blätter übereinander liegen. Mit Hilfe von Maus und Tastatur können dann z.B. Eingaben gemacht oder Fenster in ihrer Größe verändert, verschoben oder geschlossen werden. Icons (Piktogramme) symbolisieren bestimmte Aktionen, die der Benutzer bei Auswahl durchführen kann, wie z.B. Start eines Programms oder Aufklappen eines Menüs. Jede grafische Benutzeroberfläche besitzt ein eigenes „Look & Feel “, d.h. sie bestimmt das Aussehen und das Verhalten der Elemente. Grundlegende Elemente bzw. Eigenschaften einer grafischen Oberfläche sind: • Navigation mit der Maus, (!!) • Icons, • verschiedene Fenster (für Editor, E-Mail, WWW, Terminal, . . . ), • Fenster verschieben, vergrößern, schließen, . . . • Pulldown-Menüs. 7 1.2 Die Etikette im Rechnerpool des Fachbereichsnetzes Mac OS X Das Betriebssystem Mac OS X ist im Kern ein Unix Betriebssystem. Der Kern liegt als OpenSource Variante unter dem Namen Darwin vor und wurde von FreeBSD abgeleitet. Dem Benutzer tritt Mac OS X aufgrund seiner ausgeprägten Graphischen Benutzeroberfläche nicht unmittelbar als Unix Betriebssystem gegenüber. Offenbar wird dieses z.B. erst, wenn man sich mit Hilfe des ”Terminals” oder der ”Console” auf die Unix Komandozeilen Ebene begibt. Deutlich wird dieses auch bei der Softwareentwicklung oder bei Verwendung von Programmen, die das X11 Window Management verwenden. Mac OS X ist heute die am weitesten verbreitete Unix Variante (Betriebssystem Marktanteile Anfang 2009: Windows ca. 88%, OS X ca. 9%, Linux knapp 1%, akuelle Quelle http : //marketshare.hitslink.com ) 1.2 Die Etikette im Rechnerpool Fachbereichsnetzes des • Transponderkarte beim Betreten und Verlassen der Ebene 0 benutzen (!). • Niemals einen Computer selbst ausschalten oder neustarten (!!). • Fehler oder Probleme an das Technik-Team melden. Dringende Fehler sofort (persönlich), alles andere per E-Mail an [email protected]. • Sorgsam mit Hardware, Software und Netzlast umgehen. • Keine personenbezogenen Daten verarbeiten oder ausspionieren. • Pfiffige Passwörter wählen (z.B. die Anfangsbuchstaben von einem Gedicht, ...) und geheimhalten. • Den Bildschirm nicht länger als ca. 10 Minuten blockieren. • Auf Druckaufträge achten und lange Druckaufträge außerhalb der Stoßzeiten starten. • Nur einen Arbeitsplatzrechner belegen. • Ruhe bewahren, die Nachbarn möchten sich auch auf ihre eigenen Aufgaben konzentrieren. • Abmelden nicht vergessen (!!). Weitere Informationen: http://www.informatik.uni-bremen.de/t/info/ 8 Kapitel 1: Einführung in UNIX / Linux 1.3 An- und Abmelden beim System Zu Beginn jeder Sitzung muss man sich mit seiner Benutzerkennung (account, login name) und seinem Passwort anmelden. Falls man noch keine Benutzerkennung besitzt, so holen !! Folgendes gilt für die Linux System: Nach der Anmeldung erscheint dann ein Hinweis auf dem Bildschirm, wie die graphische Oberfläche gestartet werden kann: Commands for starting the desktops: X.kde X.fvwm X.gnome $ Folgende GUIs verbergen sich dahinter: X.kde K Desktop Environment X.fvwm pure X11 X.gnome Gnome Desktop Für unsere Zwecke lautet die Eingabe dann $ X.kde (Bem: Das Dollarzeichen bezeichnet das Prompt, welches anzeigt, dass die Shell auf die Eingabe eines Kommandos wartet. Dieses Prompt kann unterschiedlich aussehen !) Zum Beenden verlässt man zunächst KDE (mit rechter Maustaste auf Desktop klicken und Kontextmenüpunkt „Logout "Benutzername"“ auswählen oder „KMenü->Logout. . . “ auswählen. Das Icon des K-Menüs findet sich unten-links auf dem Bildschirm). Damit ist man aber noch nicht endgültig abgemeldet (man hat nur die grafische Oberfläche beendet). Man muß anschließend noch die UNIX-Sitzung verlassen: $ exit Am besten ist es, KDE gleich so zu starten: $ X.kde ; exit Dann wird nach dem Beenden von KDE auch gleich die ganze Sitzung beendet. Abmelden ist sehr wichtig, da eine Nichtabmeldung zur Folge hat, dass andere Benutzer z.B. auf deine Daten zugreifen oder deine Mails lesen können! Rechner in den Praktikumsräumen nach dem ausloggen aber niemals ausschalten ! 1.3 An- und Abmelden beim System 9 Terminal - Eingabe von Kommandozeilen Nachdem eine graphische Benutzerschnittstelle gestartet wurde, ist die Eingabe von Unixbefehle nur noch in speziellen Fenstern möglich (in der sogenannten "Console" oder dem "Terminal"). Im K-Menü (unten links auf dem Bildschirm können Sie in dem Untermenü " System" das Programm "Terminal" auswählen. Das Fenster, das hierauf geöffnet wird, erlaubt die Eingabe von Unix Kommandos. Auf dem Mac finden Sie das Programm in den Ordner Hilfsprogramme (Utilities), der unter Programme (Applications) zu finden ist. Bildschirm sperren Wenn man den Rechner nur kurzzeitig (!) verlässt (d.h. nicht länger als 10 - 15 Minuten), sollte man ihn zur Sicherheit sperren: Mausklick rechts auf Desktop und Menüpunkt „Lock Screen“ im Kontextmenü auswählen oder „K-Menü->Lock Screen“ bzw. „K-Menü->Lock Session“. Passwörter (Achtung: nicht mehr aktuell) Passwörter dienen der eigenen Sicherheit und der des Rechnernetzes, deshalb • nicht vergessen, • nicht aufschreiben, • nicht weitergeben (besonders nicht per E-Mail). Die Sicherheit wird regelmäßig überprüft und Passwörter ggf. gesperrt, falls diese zu leicht zu knacken sind (z.B. Namen oder Wörter aus dem Lexikon). Zum Entsperren an die Technischen Mitarbeiter wenden. Zum Ändern seines Passworts gibt es einen UNIX-Befehl: $ yppasswd Changing NIS account information for [...] on bob. Please enter old password: Changing NIS password for [...] on bob. Please enter new password: Please retype new password: The NIS password has been changed on bob. $ Zunächst wird das alte Passwort eingegeben, dann zweimal das neue. Ein paar Tipps zur Wahl des Passworts: • Mindestens 8 Zeichen, davon mindestens 2 Buchstaben und 2 Nichtbuchstaben • Keine Namen, Geburtsdaten, Telefonnummern u.ä. • Z.B. Anfangsbuchstaben eines Gedichts/Liedes plus ein Sonderzeichen 10 Kapitel 1: Einführung in UNIX / Linux 1.4 Aufbau von UNIX-Kommandos Unter UNIX kommuniziert der Benutzer mit dem Computer über Kommandos, die in einem Terminal-Fenster (genauer in einer Shell, siehe Abschnitt 1.8) eingegeben werden. Ein Terminal öffnet man über das KDE-Menü bzw. über ein Icon im Kicker, man schließt es durch Eingabe von exit . Kommandos besitzen unterschiedliche Parameter und diverse Optionen. In der Regel folgen Kommandoaufrufe diesem Schema (wobei dieses nicht immer einheitlich ist): Kommando [-Optionen] [Argumente] Jedes Kommando wird durch Drücken der <RETURN> oder <ENTER>-Taste abgschlossen. Leerzeichen und Groß- und Kleinschreibung müssen beachtet werden! (Kein Windows!) Somit sollte man auch bei der Passworteingabe bzw. -änderung aufpassen, dass nicht ungewollt die Caps Lock-Taste (Feststell-Taste) aktiviert ist, so dass das Passwort nur aus Großbuchstaben besteht. CTRL + c beendet die laufende Kommandoausführung, CTRL + d beendet laufende Prozesse (auf einer deutschen Tastatur steht anstatt CTRL ein Strg (Abk. für Steuerung)). Manual Pages Wie ein Kommando aufgerufen wird, ist in den Manual Pages bzw. Manpages ausführlich dokumentiert: man Kommandoname wobei man das Kommando bezeichnet und Kommandoname das Argument. Zum Blättern in der Dokumentation benutzt man RETURN (1 Zeile vorwärts) , SPACE (1 Seite vorwärts) und b (zurück zum Anfang); mit q verlässt man sie. apropos Stichwort Kommandos auflisten, die zum Stichwort passen Die Manual Pages der verschieden Kommandos finden sich auch im Internet unter http://man.linuxquestions.org, und können so mit dem Browser angesehen werden. Allerdings kann sich die dort zu findende Beschreibung in einigen Fällen von der unterscheiden, die auf dem System zu finden ist. Massgebend ist die Beschreibung, die auf dem System vorhanden ist, also mit dem Befehl "man" abgefragt werden kann. Info System Viele Befehle sind auch ausführlich im „Info-Format“ dokumentiert. Mit info Kommandoname öffnet sich ein einfacher Textbrowser, mit dem man auf die jeweilige Hypertext-Hilfe zugreifen kann. Mit den Cursortasten (Zeile vor / 1.5 Editoren 11 zurück), SPACE (eine Seite vorwärts), PageUp , PageDown (Seite vor / zurück) navigiert man auf der Seite Links zu verwandten Themen sind mit * Linkname:: markiert. Diese lassen sich per Cursortasten ansteuern und dann per <RETURN> auswählen. RETURN h u n p Link folgen Hilfe zu info Up: Ein Level nach oben. Next: Nächste Seite (auf dieser Ebene) Previous: Vorherige Seite (auf dieser Ebene) q Ende Andere Benutzer who Wer arbeitet am gleichen Rechner wie ich? w ebenso, etwas ausführlicher finger name Suche nach Login- und Benutzernamen last Wer war zuletzt an diesem Rechner? 1.5 Editoren Um eine Text-Datei (z.B. den Quelltext eines Programms) zu erzeugen, zu bearbeiten und zu speichern, benötigt man einen Editor. Dabei geht es um einfache Dateien, keine Texte mit unterschiedlichen Schriftgrößen und -arten, Seitennumerierung, Kopf/Fuß-Zeile, eingebundenen Bildern u.ä. – dafür benutzt man LATEX, Word oder andere Programme. Alle Texteditoren verfügen über viele komfortable Funktionen: Suchen und Ersetzen, Ausschneiden und Einfügen, Undo, Syntaxhighlighting (abhängig von der benutzten Sprache), . . . Diese Funktionen lassen sich über die Menüleiste oder mittels Tastaturkürzeln (sog. Short-Cuts) ansprechen. Unter UNIX stehen für jeden Geschmack verschiedene Editoren zur Verfügung, z.B. 12 Kapitel 1: Einführung in UNIX / Linux nedit Der nedit ist ein vielseitiger Text-Editor, der gut für die Eingabe von Computer Programmen geeignet ist. Hier eine kurze Übersicht über seine Bedienung(selemente): Start Ende Neue Datei erstellen Datei öffnen Speichern Undo Cut Copy Paste Suchen Suchen fortsetzen Suchen + Ersetzen K-Menü->Debian->Apps->Editors->NEdit (oder nedit & aus der Shell bzw. Konsole heraus) Menü „File/Exit“ (oder CTRL + q , d.h. beide Tasten gleichzeitig gedrückt halten) Menü „File/New “ (oder CTRL + n ) Menü „File/Open...“ (oder CTRL + o ) und Datei im Minibuffer (untere Zeile) eingeben Menü „File/Save“ (oder CTRL + s ) Menü „Edit/Undo“ (oder CTRL + z ) Menü „Edit/Cut“ (oder CTRL + x ) Menü „Edit/Copy“ (oder CTRL + c ) Menü „Edit/Paste“ (oder CTRL + v ) Menü „Search/Find...“ (oder CTRL + F ) Menü „Search/Find again “ (oder CTRL + G ) Menü „ Replace ...“ (oder CTRL + r ) Eine ausführlichere Einleitung findet sich unter dem Menüpunkt Help. Hinweis: nedit ist unter Mac OS X nicht installiert. Es könnte zwar installiert werden, aber der Code ist offenbar gut 5 Jahre alt und enthaelt (zumindest in der OS X Version einge ärgerliche Fehler. Alternativ können Sie ”TextEdit” oder den ”Xcode” Editor (speziell für Programm Code) verwenden. kwrite Der kwrite ist ein einfacher Text-Editor, der ebenfalls recht gut für die Eingabe von Computer Programmen geeignet ist. Es ist gut in die KDE-Umgebung 1.5 Editoren 13 integriert und wird z.B. gestartet, wenn man eine C++ Datei in der KDE-GUI doppelklickt. Das Menü ist ähnlich wie bei nedit aufgebaut. emacs emacs ist ein sehr mächtiger Editor, ausgestattet mit extrem vielen Befehlen und einer eingebauten Programmiersprache (Emacs Lisp), mit der eigene Erweiterungen definiert werden können. Wichtige Befehle finden sich in der folgenden Auflistung, wobei in der EmacsHilfe das C für Ctrl und M für Alt steht. Start Ende Datei öffnen Neue Datei erstellen Speichern Speichern als ... Undo Cut Copy Paste einmaliges Suchen nochmalige Suche Suchen und Ersetzen Zeile ab Cursor bis Zeilenende löschen K-Menü->Debian->Apps->Editors->Emacs (oder emacs & aus der Shell bzw. Konsole heraus) Menü „File/Exit Emacs“ (oder CTRL + q CTRL + c , d.h. beide Tasten gleichzeitig nacheinander gedrückt halten) Menü „File/Open...“ (oder CTRL + o ), dann Datei in der Liste auswählen Siehe Datei öffnen. Menü „File/Save (current buffer)“ (oder CTRL + x CTRL + s ) Menü „File/Save Buffer As“ (oder CTRL + x CTRL + w ) Menü „Edit/Undo“ (oder CTRL + _ ) Menü „Edit/Cut“ (oder CTRL + w ) Menü „Edit/Copy“ (oder M + w ) Menü „Edit/Paste“ (oder CTRL + y ) Menü „Edit/Search/Search...“ Menü „Edit/Search/Repeat Search...“ Menü „Edit/Search/Replace “ (oder M+% ) CTRL+k ) 14 Kapitel 1: Einführung in UNIX / Linux Eine extrem ausführliche Einleitung findet sich wiederum unter dem Menüpunkt Help. Hinweis: Auf den Macs sollte die Emacs Version Aquamacs installiert sein. Diese Version bietet eine volle Intergration in die Graphische Benutzeroberfläche und ist gerade für gelegentliche Nutzer leichter zu nutzen. 1.6 Drucken Zum Ausdrucken von Dateien steht unter UNIX der Befehl lpr zur Verfügung. lpr dateiname Pro Semester hat jeder Student nur eine bestimmte Anzahl von freien Druckseiten, zurzeit 300. Um sein Druckkontingent ( Quota) nicht zu stark zu belasten, kann man mehrere Seiten einer Postskriptdatei verkleinert zusammenlegen, wobei zahl die Anzahl der Seiten ist, die auf einer Seite zusammengelegt werden sollen. psnup -n zahl datei.ps > datei_neu.ps In Ebene 0 stehen die Drucker lw0, lw2, lw3 (farbig) zur Verfügung. Standardmäßig wird lw0 benutzt, einen anderen Drucker kann man mit der -P-Option auswählen,z.B. lpr -Plw2 dateiname Wichtig! Druckt keine PDF-Dateien mit lpr aus, da ihr sonst einen unleserlichen Seitensalat bekommt, den ihr nicht gebrauchen könnt! Ausdruck von Dateien, die kein PS-Format besitzen, aus einem entsprechenden Programm heraus, z.B. Acrobat-Reader für PDF-Dateien. Das Kommando lpq gibt eine Liste aller Druckaufträge aus, etwa: Rank active 1st 2nd owner (class) benner jschmidt benner Job 79 83 84 Files titel.ps daten zv.text Total size 132 438 488 6 212 (Time) bytes bytes bytes Die Zahl in der Spalte Job gibt eine Identifikationsnummer (ID) an, mit der man den Druckauftrag löschen kann. Dies geschieht mit lprm jobid 1.7 Verzeichnisse und Dateien pacc 15 Information über aktuelles Druckkontingent Weitere Informationen finden sich unter http://www.informatik.uni-bremen.de/t/info/drucken.html 1.7 Verzeichnisse und Dateien Alle Daten, Texte, Programme usw. sind unter UNIX in einer baumartigen Struktur angeordnet: • Eine Datei (file) ist ein „Behälter“, der Daten, Texte, Programme enthält (UNIX-Kommandos sind auch Dateien). • Ein Verzeichnis (auch Ordner oder Schublade, directory) ist eine Zusammenfassung mehrerer Dateien und Ordner. Durch Verzeichnisse kann man zusammengehörige Dateien bündeln und Daten strukturieren. Verzeichnisse, die in Verzeichnissen enthalten sind, nennt man auch Unterverzeichnisse. • Sämtliche Dateien und Verzeichnisse in einem UNIX-Dateisystem befinden sich in Unterverzeichnissen eines großen Hauptverzeichnisses, dem sogenannten Wurzelverzeichnis (bzw. Wurzel). Dieses wird mit / bezeichnet, ansonsten dient der Schrägstrich zum Trennen von Verzeichnisnamen in einem Pfad. • Ein Verweis (link) verweist auf eine Datei oder ein Verzeichnis und verhält sich dann wie eine Datei bzw. ein Verzeichnis, nur dass der Speicherplatz nur einmal benötigt wird. Wir werden davon in dieser Einführung keinen Gebrauch machen. 1.7.1 Grundgerüst des Verzeichnisbaumes Der Verzeichnisbaum unter UNIX hat prinzipiell die Struktur, wie sie Abb. 1.1 zeigt. Benutzer legen ihre privaten Daten in ihrem Heimverzeichnis bzw. HomeVerzeichnis ab. Die Heimverzeichnisse aller Nutzer befinden sich im Verzeichnis /home. Das Heimverzeichnis des Benutzers mit dem Nutzerkürzel benke ist somit /home/benke. In der Shell-Variablen (Abschnitt 1.8) $HOME befindet sich der Pfad seines Heimverzeichnisses. Nach dem Einloggen befindet man sich im eigenen Heimverzeichnis. 16 Kapitel 1: Einführung in UNIX / Linux XW ffffjfj / RRWXRWXRWXRWXRWXWXWXWXXX fffjfjfjjjj f RRR WWWXWXWXXXX f f f j RRR WWWWXWXXXX ffff jjjjj f f f X RRR f j j ffff RR WWWWWXWXWXWXXXXXXX f j f f j f j f f ... usr etc lib bin home RR RRR jjjj j R j j R RRR jj RRR jjjj j RR j j j ... ... benke R k R RRR kk k k R k RRR kkk RRR kkkk R k k k k ... SKRIPTEPhysik AWI SKRIPTEMathe R RRR kkk k R k R RRR kk kkk RRR kkk RRR k k kk ... Analysis ... AnalysisRannacher/ AnalysisTretter.pdf analysis.txt ... Abbildung 1.1: Verzeichnisbaum 1.7.2 Dateien und Pfade Oft muss man einem Kommando den Namen einer Datei oder eines Verzeichnisses übergeben, mit dem das Kommando irgendetwas anstellen soll. Dieses soll anhand des Befehls more erläutet werden, der das seitenweise Anzeigen von Textdateien ermöglicht, wobei das zeilenweise Vorblättern mit <RETURN> und das seitenweise Vorblättern mit <SPACE> vonstatten geht. Ein komfortableres Anzeigeprogramm wäre less, welches mittels <PAGE DOWN> und <PAGE UP> ein seitenweises Vor- und Zurückblättern im Text ermöglicht. Die einfachste Möglichkeit ist, allein den Dateinamen anzugeben. Das funktioniert, wenn sich die Datei im aktuellen Verzeichnis (Abschnitt 1.7.3) befindet, d.h. in dem Verzeichnis, in dem man sich gerade befindet (Wichtig! HomeVerzeichnis und aktuelles Verzeichnis sind i. A. verschieden). Das aktuelle Verzeichnis ist in unserem Fall /home/benke/SKRIPTEMathe/Analysis/. more analysis.txt Angenommen die Datei befindet sich im direkt dem aktuellen Verzeichnis übergeordneten Verzeichnis (d.h. wir befinden uns bspw. in /home/benke/SKRIPTEMathe/Analysis/AnalysisRannacher/), dann kann man mit dem Verweis .. darauf zugreifen: more ../analysis.txt Das lässt sich auch kombinieren: 17 1.7 Verzeichnisse und Dateien more ../../editor.txt zeigt die Datei editor.txt an, welche /home/benke/SKRIPTEMathe/ befinden muss. sich im Unterverzeichnis Der Punkt . bezeichnet ein Verzeichnis selbst, more ./analysis.txt bedeutet das gleiche wie more analysis.txt Trotzdem ist der Punkt nicht überflüssig, denn wenn ein Kommando ein Verzeichnis als Argument erwartet, dann kann man mit einem einfachen Punkt das aktuelle Verzeichnis übergeben. Die bisher aufgezählten Pfade waren relative Pfade, denn sie gingen immer vom aktuellen Verzeichnis aus. Im Gegensatz dazu gibt es absolute Pfade. Diese beginnen mit einem Schrägstrich /, dem Wurzelverzeichnis. Die Aneinanderreihung von Verzeichnissen heißt Pfad, weil sie gewissermassen den Weg zu einer Datei beschreiben. more /home/benke/SKRIPTEMathe/Analysis/analysis.txt Dies bezeichnet einen absoluten Pfad. 1.7.3 Navigation im Verzeichnisbaum Um sich im Verzeichnis- bzw. Dateibaum bewegen zu können oder um Dateien und Verzeichnisse zu erzeugen oder zu löschen, existieren eine Reihe von Befehlen: Verzeichnisse wechseln pwd print working directory zeigt das aktuelle Verzeichnis an cd change directory cd ohne Argumente springt ins HomeVerzeichnis cd .. Wechsel ins übergeordnete Verzeichnis des aktuellen Verzeichnis (dafür steht das ..) 18 Kapitel 1: Einführung in UNIX / Linux cd verzeichnis Wechsel in ein Unterverzeichnis des aktuellen Verzeichnis (relativer Pfad) cd /home/benke/AWI/ Wechsel ins Unterverzeichnis von benke (absoluter Pfad) cd ∼/AWI dito, d.h. ∼ ist eine Abkürzung für /home/benke Dateien kopieren, verschieben, löschen cp datei1 datei2 copy Kopieren von Dateien – Vorsicht! Falls datei2 schon existiert, so wird ihr Inhalt mit dem Inhalt von datei1 überschrieben cp datei1 verzeichnis Kopiere Datei ins Unterverzeichnis – Vorsicht! Falls datei1 im Unterverzeichnis schon existiert, so wird ihr Inhalt mit dem Inhalt von datei1 überschrieben cp -i datei1 verzeichnis Kopieren mit Sicherheitsabfrage (nur falls identische Datei im Verzeichnis bereits existiert) cp verzeichnis/* . Kopiere alle Dateien aus Verzeichnis ins aktuelle Verzeichnis (Anwendung von ‚.‘) mv datei1 datei2 move Verschieben bzw. Umbenennen von Dateien – Vorsicht! Falls datei2 schon existiert, so wird ihr Inhalt überschrieben mv datei1 verzeichnis Verschiebe Datei ins Unterverzeichnis – Vorsicht (siehe oben)! mv -i datei1 verzeichnis Verschieben mit Sicherheitsabfrage rm datei remove Löschen einer Datei – Vorsicht! Keine vorherige Sicherheitsabfrage wie bei WindowsPapierkorb! rm -i datei Löschen mit Sicherheitsabfrage 19 1.7 Verzeichnisse und Dateien rm -r verzeichnis Rekursives Löschen von Verzeichnissen inkl. deren Inhalt, d.h. alle Dateien und alle Unterverzeichnisse in diesem Ordner – VORSICHT!! Damit könnt ihr im ungünstigsten Fall euer gesamtes Home-Verzeichnis löschen rm -ri datei Rekursives Löschen von Verzeichnissen mit Sicherheitsabfrage (hier findet man eine Kombination zweier Argumente vor) rm -rf verzeichnis Rekursives Löschen von Verzeichnissen ohne Sicherheitsabfrage und Fehlermeldungen – EXTREME VORSICHT! Verzeichnisse erstellen, löschen 1.7.4 mkdir verzeichnis make directory Anlegen von neuem Unterverzeichnis im aktuellen Verzeichnis rmdir verzeichnis remove directory Löschen von leerem Verzeichnis im aktuellen Verzeichnis Ausgabe von Inhaltsverzeichnissen Den Inhalt eines Verzeichnisses kann man sich mit ls ( list) oder ls -l bzw. ll ( list long) anschauen (Sortiert nach Namen; Großschreibung zuerst). Mit ls -a sieht man auch die „versteckten“ Dateien (Systemdateien, Voreinstellungen u.ä., d.h. Dateien, die mit ‚. ‘beginnen). total 1836 drwxr-xr-x -rw-r--r-drwx-----drwx-----drwxr-xr-x -rw-r--r-drwxrwxrwx -rw-r--r-drwx--x--x drwxrwxrwx drwxr-xr-x 5 1 18 4 11 1 16 1 20 3 17 Man sieht • die Zugriffsrechte, stoever stoever stoever stoever stoever stoever stoever stoever stoever stoever stoever wimi wimi wimi wimi wimi wimi wimi wimi wimi wimi wimi 512 35328 1024 512 1536 18874 512 1296336 2048 1536 2560 Oct Aug Feb Aug Dec Dec Jan Feb Feb Jan Feb 17 28 5 15 10 20 17 4 4 28 3 11:10 15:18 14:21 08:30 12:52 13:18 14:41 08:44 12:26 17:21 15:41 KUTTA Lab.doc Mail fortran images go.txt mfiles te1.tgz tfiles tmp word 20 Kapitel 1: Einführung in UNIX / Linux • einen Referenzzähler für die Verweise auf die Datei (i.d.R 1) bzw. das Verzeichnis (Anzahl ≥ 2, denn bei Verzeichnissen sind immer mind. 2 Links vorhanden, nämlich von ‚.‘und dem Verzeichnisnamen selbst), • den Besitzer der Datei oder des Verzeichnisses, • die Benutzergruppe, zu welcher die Datei gehört, • die Größe der Datei (in Bytes) bzw die Anzahl der Bytes, die zur Verwaltung der Verzeichniseinträge notwendig sind (Vielfaches von 512 Bytes). • das Datum der letzten Änderung und • den Namen der Datei oder des Verzeichnisses. Datei- und Verzeichnisnamen bestehen aus einer Folge von Buchstaben und Zahlen (bis zu 256 Zeichen) und dürfen auch Sonderzeichen enthalten, wobei es aber üblich ist, aus diesen hauptsächlich den Unter- und Bindestrich zu nehmen. Leerzeichen sind ebenfalls erlaubt, aber problematisch. Die Groß- und Kleinschreibung wird unterschieden (!!). Man sollte selbsterklärende Namen verwenden, damit man selbst (und vielleicht auch ein anderer) vermuten kann, was die Datei enthält. Häufig (und oft auch sinnvollerweise) sind Dateinamen als name.endung aufgebaut, wobei die Endung über den Typ der Datei Auskunft gibt, bzw. geben sollte, z.B. name.c C-Programmdatei (Sourcecode), name.o Objektdatei; wird vom Compiler aus einer .c-Datei erzeugt, name.tex TEX-Datei, name.ps Postscript-Datei, name.doc Word-Dokument, name.html Datei im HTML-Format, .name versteckte Datei: Systemdatei, Einstellungen, oder ähnliches Weitere wichtige Varianten von ls sind z.B. ls -la, ls -lt (Ausführliche Anzeige plus Sortierung der Dateien und Verzeichnisse nach Datum und Zeit). 1.7.5 Zugriffsrechte für Dateien und Verzeichnisse Die Zugriffsrechte legen fest, welche Nutzer wie auf Dateien und Verzeichnisse zugreifen dürfen. Dabei werden Dateien und Verzeichnisse im Wesentlichen gleich behandelt. In der Ausgabe von ls -l erkennt man ein Verzeichnis daran, dass an der ersten Stelle ein d (für directory) steht. Es gibt drei Klassen von Benutzern für die entsprechende Datei- bzw. Verzeichniszugehörigkeiten formuliert werden können: user group others Eigentümer der Datei Benutzergruppe andere (Rest der Welt) 1.7 Verzeichnisse und Dateien 21 Und drei Arten von Zugriffsrechten: r w x Read Write eXecute Leserechte Schreibrechte Ausführbarkeit bei Dateien, Zutritt zu Verzeichnissen Die Zugriffsberechtigung für Dateien ergibt sich aus der Kombination der Benutzerklassen und der Zugriffsarten, z.B. r-- r-- r-rwx --- --rw- r-- r-- Alle können Datei lesen, aber keiner darf auf sie (über-)schreibend zugreifen. Der Eigentümer darf alles, alle anderen nichts. Alle dürfen lesen, aber nur der Eigentümer darf (über-)schreiben. Bei Ordnern würde sich dann folgende Lesart ergeben r-- r-- r-- rwx --- --- rw- r-- r-- Alle können den Inhalt des Verzeichnisses mit ls auflisten, aber keiner darf in das Verzeichnis schreiben bzw. wechseln. Der Eigentümer darf den Inhalt des Verzeichnisses auslesen, in dieses wechseln oder Dateien anlegen oder verändern. Alle anderen schauen in die Röhre! Alle dürfen Inhalt auflisten, aber nur der Eigentümer darf schreiben (aber nicht ins Verzeichnis wechseln, d.h. kein cd, cp, ll,...). Der Besitzer einer Datei oder eines Verzeichnisses kann die Zugriffsrechte ändern. Die Zugriffsrechte kann man in numerischer oder symbolischer Form angeben. chmod rechte dateiname 1.7.6 change mode Numerische und symbolische Form der Rechteänderung 22 Kapitel 1: Einführung in UNIX / Linux Numerische Form 400 200 100 40 20 10 4 2 1 z.B. read write execute read write execute read write execute Leseberechtigung des Eigentümers Schreibberechtigung des Eigentümers Ausführungsberechtigung des Eigentümers Leseberechtigung für Gruppe Schreibberechtigung für Gruppe Ausführungsberechtigung für Gruppe Leseberechtigung für Rest der Welt Schreibberechtigung für Rest der Welt Ausführungsberechtigung für Rest der Welt rw- r-- r-- ergibt 400 + 200 + 40 + 4 = 644, also setzt chmod 644 datei1 die Rechte für diese Datei entsprechend, d.h. der Eigentümer der Datei darf sie auslesen und verändern bzw. überschreiben, während die Mitglieder der Gruppe und der Rest der Welt nur lesend darauf zugreifen können, bzw. bei Ordnern seinen Inhalt nur mittels ls auslesen können. Symbolische Form Die symbolische Form der Rechteänderungen besteht aus Benutzerklasse, Operation und Zugriffsrecht: [who] op permission Dabei bedeuten die einzelnen Ausdrücke das Folgende: who: op: permission: z.B.: Ändern von u g o a + − = r w x Eigentümer Gruppe andere Alle (Voreinstellung, falls „who“ weggelassen) Rechte hinzufügen Rechte wegnehmen nur diese Rechte einräumen und alle anderen widerrufen Leserechte Schreibrechte Ausführungsrechte rwx rw- r-rw- r-- r-- $ chmod u-x,g-w dateiname in geschieht durch 1.8 UNIX-Shells 23 1.8 UNIX-Shells Öffnet man ein Terminal-Fenster, dann läuft im Hintergrund eine Shell, die die eingegebenen Kommandos interpretiert und dann ausführt oder eine Fehlermeldung herausgibt. Die Shell bildet die Schnittstelle zwischen dem Benutzer und dem UNIX-Kern und legt sich somit wie eine Schale um den UNIX-Kern. Sie bildet somit eine Art Vermittler, wobei alle Eingaben (bzw. Kommandos) des Benutzers durch die Shell interpretiert werden ( Kommandointerpreter), d.h. sie „übersetzt“ die Kommandos in Aufrufe von Systemfunktionen an den UnixKernel, welcher diese dann ausführt und als Ergebnis Statusmeldungen zurückliefert, die dann die Shell auswertet. Ebenso besitzt die Shell eine eigene Sprache, in der man Programme schreiben (sog. Shell-Skripte) und sie in der Shell ablaufen lassen kann. Von den verschiedenen Shells wird hier standardmäßig die bash ( BourneAgain-Shell) benutzt. Ein paar Merkmale der bash: • Editieren in der Kommandozeile (mittels Cursortasten, Backspace) • Zurückholen bereits eingegebener Befehle mit den Cursor-Tasten (History) • Zurückholen bereits eingegebener Befehle mit passendem Präfix mit den Tasten “vorige Seite” und “nächste Seite” (History) • Vervollständigung von Dateinamen mit der TAB -Taste (wenn die Vervollständigung eindeutig möglich ist) • Definition eigener Namen für häufig benutzte Kommandos mit dem aliasMechanismus 1.8.1 Sonderzeichen Eine Reihe von Zeichen werden von der Shell besonders behandelt. 24 Kapitel 1: Einführung in UNIX / Linux Solche Sonderzeichen sind z.B. ; && || $ < > | & ‘cmd‘ t Zeilenende Kommandos hintereinander ausführen Kommandos hintereinander ausführen bis Fehler auftritt; alle folgenden Kommandos bleiben unberücksichtigt Folgendes Kommandos wird nur abgearbeitet, falls voriges Kommando mit Fehler endete Shell-Variable abrufen, Abschnitt 1.8.2 Eingabeumleitung aus Datei, siehe Abschnitt 1.8.5 Ausgabeumleitung in Datei Ausgabeweiterleitung zu anderem Programm Programm vom Terminal abkoppeln (!), siehe Abschnitt 1.9 Ausgabe von Kommando cmd als Argument benutzen Leerzeichen Kommando starten. Mit \ (backslash) wird die Bedeutung von Sonderzeichen aufgehoben. Setzt man einen Befehl oder einen Teil davon in einfache Hochkommata, wird dieser Text genauso, also ohne Ersetzungen übernommen. $ echo \$PATH ’$PATH’ $PATH $PATH $PATH /usr/bin:... 1.8.2 Shell-Variablen Die Shell verwaltet eine Reihe von Shell- und Environment-Variablen. ShellVariablen haben nur Auswirkungen auf das Verhalten der Shell (z.B. Variable PS1, welche für das Aussehen des Prompt- bzw. Eingabeaufforderungszeichens verantwortlich ist), während Environment-Variablen das Verhalten der Programme beeinflussen, welche aus der Shell heraus gestartet werden (z.B. Variable LANG, welche Programme abfragen, um abzufragen, in welcher Sprache die Programmausgabe erfolgen soll). Sie beeinflussen das Verhalten der Shell nicht. Manche Variablen werden sowohl von der Shell, als auch von den aus der Shell gestarteten Programmen benutzt (z.B. HOME). Der Zweck von ShellVariablen ist, dass man mit ihnen das Verhalten der Shell leicht konfigurieren kann, was die Arbeit sehr erleichtert. Eine Variable hat einen Namen und einen Wert. Der Wert einer Variablen kann abgefragt werden, indem man dem Namen der Variablen ein $ voranstellt, etwa $HOME. Existiert die Variable nicht, gibt es leider keine Fehlermeldung, sondern als Wert wird der leere Text zurück gegeben. Schreibfehler können dadurch nur schwer aufgedeckt werden. Mit 1.8 UNIX-Shells 25 $ BLA=fasel $ echo $BLA fasel wird eine Shell-interne Variable BLA gesetzt und mittels echo wieder ausgegeben. Mit printenv bekommt man eine Liste aller definierten Variablen mit ihrem aktuellen Wert. Mit export setzt man den Wert einer (auch selbstdefinierten) Variablen und macht sie dann zu einer Environment- bzw. Umgebungs-Variablen, mit echo kann man ihn dann ausgeben. $ export BLA=fasel $ echo $BLA fasel Einige interessante Shell-Variablen: HOME HOST SHELL DISPLAY PATH PS1 PWD USER Heimverzeichnis Rechnername Name der benutzten Shell Name des X-Window-Displays, bzw. Bildschirms, auf dem die Ausgabe erfolgen soll. Liste aller Verzeichnisse, in der die Shell die Programme finden kann. Format der Eingabeaufforderung (Prompt), z.B. \u@\h:\w> aktuelles Verzeichnis Benutzername Konvention: Shell-Variablen werden komplett groß geschrieben! 1.8.3 Kommandos Zu jedem UNIX-Kommando existiert eine entsprechende ausführbare Datei. Es gibt aber auch Shell-interne Kommandos, wie export. Eine ausführbare Datei ist in der Regel ein Maschinenprogramm, also kein Text, den man sich anschauen kann. Die meisten dieser Dateien liegen in Verzeichnissen wie /bin, /usr/bin. Damit die Shell den Befehl ausführen kann, muss sie wissen, in welchem Verzeichnis die dazugehörige ausführbare Datei liegt. Dafür hat sie einen Suchpfad, der sich aus ein paar festverdrahteten Orten wie /bin, /usr/bin und den in der Shellvariablen PATH aufgezählten Verzeichnissen zusammensetzt. Man kann sich diesen Pfad anzeigen lassen mit $ echo $PATH /usr/local/cm3/bin:/usr/lib/qt3/bin:/opt/kde3/bin 26 Kapitel 1: Einführung in UNIX / Linux Die verschiedenen Verzeichnisse werden mit Doppelpunkten getrennt. Ein weiteres Verzeichnis kann man dem Pfad z.B. mit $ export PATH=$PATH:/home/name/bin hinzufügen, was zu einem neuen Wert der Variablen PATH führt. $ echo $PATH /usr/local/cm3/bin:/usr/lib/qt3/bin:/opt/kde3/bin:/home/name/bin Nun schaut die Shell auch in /home/name/bin nach, ob dort eine ausführbare Datei des eingegebenen Namens existiert. Erfahrene UNIX-Nutzer schreiben und benutzen sogenannte Shell-Skripte. ShellSkripte bestehen aus einer Abfolge mehrerer UNIX-Kommandos. Sie werden (im Gegensatz zu C-Programmen) für die Ausführung nicht in Maschinenprogramme übersetzt. 1.8.4 Platzhalter in Dateinamen Durch Benutzung spezieller Sonderzeichen, der Platzhalter, auch Jokerzeichen oder Wildcards genannt, kann man Dateinamen abkürzen oder mehrere Dateien gleichzeitig ansprechen. Die Shell analysiert das verwendete Suchmuster, ersetzt es durch alle dazu passenden Dateinamen und übergibt das Ergebnis an das angegebene Kommando. ? * [ ] steht für genau ein Zeichen (?? für zwei, ??? für drei, etc.). steht für beliebig viele Zeichen (keins und eins eingeschlossen). steht für ein Zeichen aus einem begrenzten Zeichenvorrat. Beispiele: ls prog*.c listet prog.c, prog1.c, prog2.c, progx.c, . . . , soweit im Verzeichnis vorhanden ls prog?.c listet nicht prog.c, aber prog1.c, prog2.c, progx.c, . . . , soweit im Verzeichnis vorhanden ls prog[0-9].c listet nicht prog.c, aber progn.c, wobei n ∈ {0, 1, . . . , 9} ls prog[a-z].c listet progn.c, wobei n ∈ {a, b, . . . , z} rm *.ps löscht alle Postscript-Dateien (in diesem Verzeichnis)! 1.8 UNIX-Shells rm * 1.8.5 27 löscht alle Dateien (in diesem Verzeichnis)! Also vorsichtig mit * in manchen Situationen! Umleitung Bis jetzt haben wir Kommandos immer über die Tastatur ein- und das Ergebnis immer auf dem Bildschirm ausgegeben (genannt Standardeingabe (stdin) und -ausgabe (stdout)). Man kann aber sowohl Ein- wie Ausgabe umleiten. ll > datei Das Inhaltsverzeichnis wird in eine Datei geschrieben. cat datei1 datei2 > datei3 concatenate Verbinde datei1 und datei2, schreibe das Ergebnis in datei3 (Überschreiben) cat datei4 >> datei3 Hänge datei4 noch an datei3 an. mail nutzer < datei Schicke die Datei an den Nutzer. Eine besondere Form der Umleitung ist die Pipe (von Pipeline). Sie schaltet mehrere Kommandos hintereinander, wobei die Ausgabe des einen Kommandos als Eingabe des nächsten dient. ll | sort -r -k8 Das Inhaltsverzeichnis wird in alphabetischer Reihenfolge sortiert. Der Befehl sort gibt Textzeilen nach einem vom Benutzer gewählten Sortierungskriterium aus, wobei -r -k8 bedeutet, dass die Ausgabe umgekehrt wird (d.h. es wird vom Größten zum Kleinsten sortiert) und es wird nach der 8. Spalte der Ausgabe (d.h. den Dateinamen) sortiert. Man kann alle diese Möglichkeiten auch miteinander kombinieren, z.B. ll | sort -r -k8 | lpr Inhaltsverzeichnis sortieren und dann ausdrucken. (Bem: ‚|‘ ist links-assoziativ) ll | sort -r -k8 > datei Inhaltsverzeichnis sortieren und dann in eine Datei schreiben. Falls sie existiert, so wird sie überschrieben 28 1.8.6 Kapitel 1: Einführung in UNIX / Linux Vorverarbeitung der Kommandos Noch bevor das eigentliche Kommando (Programm) gestartet wird, erledigt die Shell einige Aufgaben. Es ist wichtig zu wissen, welche Aufgaben die Shell übernimmt und welche die Kommandos. Nachdem man eine Kommandozeile mit siert in etwa folgendes: RETURN abgeschlossen hat, pas- • Die Shell liest bis zum ersten Kommandotrenner (z.B. &, <, >). Weiterhin stellt die Shell fest, ob Variablenzuweisungen oder Ein/Ausgabeumlenkungen erfolgen sollen. • Die Shell zerlegt die Kommandozeile in einzelne Argumente (an der Stelle wo i.d.R. Leerzeichen oder Tabs stehen). • Shell-Variablen, denen ein $ vorangestellt wurde, werden durch ihren Inhalt ersetzt. echo $PWD könnte zu echo /home/benke werden. • Komandos, die in Apostrophen (“) stehen, werden ausgeführt und durch ihre Ausgabe ersetzt. • Standardeingabe und -ausgabe sowie die Standardfehlerausgabe werden auf ihre Ziele umgelenekt und die entsprechenden Dateien geöffnet. Im Falle der Ausgabe wird die entsprechende Datei geleert. • Texte, die Platzhalter enthalten, werden entsprechend der vorhandenen Dateien im aktuellen Verzeichnis durch eine durch Leerzeichen unterteilte Liste ersetzt. Gibt es keine Datei, die zu dem Muster passt, wird das Muster beibehalten. ls *.tex *.bla könnte zu ls skript.tex anhang.tex *.bla werden. Vorsicht: Die Shell weiß nichts über die Bedeutung der Argumente für ein Kommando, sie ersetzt stur alles was wie ein Dateinamenmuster aussieht, auch wenn es z.B. ein Suchmuster für grep ist. • Das eigentliche Kommando wird mit den umgewandelten Argumenten aufgerufen. Wie das Kommando letztlich aufgerufen wird, bekommt man zu sehen, wenn die bash mit der Option −x gestartet wird. Die Bequemlichkeiten, die eine Shell durch ihre Automatiken erlaubt, können sich auch schnell rächen. Beispiele: 1. Wir suchen alle Zeilen eines Textes in denen ein > vorkommt und schreiben: $ grep > text.txt Die Argumente > und text.txt erreichen das Programm grep allerdings nie, denn die Shell hat bereits die Datei text.txt zum Zwecke der Ausgabeumleitung gelöscht und ruft grep ohne jegliche Argumente auf. 1.9 Prozesse 29 2. Wir wollen eine Reihe von Dateien in ein anderes Verzeichnis verschieben, vergessen aber, das Zielverzeichnis anzugeben. $ mv *.tex Möglicherweise gibt es nur die Dateien datei1.tex und datei2.tex, die zu diesem Muster passen. Dann wird der Befehl mv datei1.tex datei2.tex ausgeführt, mit dem Ergebnis, dass datei2.tex unwiederbringlich durch datei1.tex ersetzt wird. 1.9 Prozesse UNIX ist nicht nur ein Mehrbenutzer- (Multi-User), sondern auch ein Multitasking-Betriebssystem, d.h. jeder kann (scheinbar) parallel mehrere Prozesse (Kommandos, Programme, . . . ) laufen lassen. Tatsächlich laufen ständig diverse, vom System ausgelöste Prozesse, ohne dass man es merkt. Um ein Terminal nicht zu blockieren, kann man Programme bzw. Prozesse auch so starten, dass sie im Hintergrund laufen, z.B. nedit & Es öffnet sich das nedit -Fenster und im Terminal kann weitergearbeitet werden. Startet man ein Programm ohne &, so ist das Terminal blockiert. Aber man kann das Programm mit CTRL + z anhalten und dann mit dem Kommando bg (und der Prozess- bzw Jobnummer) unabhängig laufen lassen („in den Hintergrund verlagern“ = background). Mit fg wird es wieder in den Vordergrund geholt und blockiert die Shell. Eine Übersicht über die laufenden Prozesse liefert der ps -Befehl. Durch ps -U nutzer bekommt man eine Liste seiner eigenen, aktiven Prozesse, z.B. 30 Kapitel 1: Einführung in UNIX / Linux PID 23629 23671 23686 23775 23804 24229 24233 26045 26516 26520 26852 TTY pts/1 ? ? pts/1 ? ? pts/4 pts/1 ? pts/5 pts/5 TIME 00:00:00 00:02:02 00:00:00 00:00:06 00:00:01 00:00:01 00:00:00 00:00:03 00:00:00 00:00:00 00:00:00 CMD bash mozilla-bin gconfd-2 nedit kdeinit kdeinit bash acroread kdeinit bash ps Dabei ist für jeden Prozess eine eindeutige Kennzahl PID (Process Identification) angegeben, die man zum außerplanmäßigen Beenden eines Prozesses benötigt (wenn CTRL + c nicht möglich ist): $ kill PID $ kill -9 PID Das kill -Kommando versucht den Prozess so ordentlich wie möglich zu beenden, d.h. der Prozess hat nach Erhalt des Termination-Signals noch die Möglichkeit, alle Dateien zu schließen, auf denen er gearbeitet hat und evtl. vorhandene temporäre Dateien zu löschen. Die zweite Variante (d.h. kill -9 PID) sollte nur im Notfall verwendet werden, weil dann der Prozess möglicherweise Resourcen nicht wieder frei gibt. Eine ausführlichere Übersicht über seine eigenen Benutzerprozesse erhält man mittels ps -ef | grep nutzer ps -ef gibt Informationen über alle auf dem Rechner laufenden Prozesse aus, z.B. auch root- Prozesse (Systemprogramme). Mittels grep werden dann die eigenen Benutzerprozesse „herausgefiltert“. Eine weitere Möglichkeit, Prozesse anzeigen zu lassen, wäre: aktuelle Programme interaktiv anzeigen lassen top Die durch top erhaltene Prozessliste wird in regelmäßigen Abständen aktualisiert. Bem: Mittels ps werden alle aktiven Benutzerprozesse angezeigt, wäh- rend jobs nur alle in der aktuellen Shell laufenden Prozesse bzw. von ihr gestarteten Prozesse anzeigt. $ jobs 1.10 Arbeiten auf entfernten Rechnern [1] [2] [3][6]+ [7] Running Stopped Stopped Stopped Running 31 nedit weiterebefehle.tex \& make make make acroread skript.pdf \& Mittels kill %JOBID kann man den Job bzw. das Programm abbrechen. 1.10 Arbeiten auf entfernten Rechnern Unter UNIX ist es problemlos möglich, Rechner so zu vernetzen, dass man auf einem anderen Rechner arbeiten kann, als dem, vor dem man sitzt. Auf einen fremden Rechner begibt man sich mit ssh nutzer@rechner Eventuell muss man noch sein Passwort eingeben. Ist man auf dem Rechner angelangt, kann man bereits einfache Kommandozeilenprogramme starten. Neben ls , cp , rm usw. wären das z.B. der Editor vi oder das Textsatzsystem latex . Es ist auch möglich, Programme mit grafischer Benutzeroberfläche auf dem fremden Rechner zu starten, aber die Ausgabe dann auf dem eigenen Rechner anzeigen zu lassen. Da alle Grafikdaten von dem entfernten Rechner zum eigenen übertragen werden müssen, benötigt das ganze viel Netzbandbreite. Um Grafikdaten zu übertragen, ruft man ssh zusätzlich mit der Option -X auf. 1.11 Weitere nützliche Befehle Ohne Anspruch auf Vollständigkeit seien noch ein paar wichtige UNIX-Befehle aufgelistet, wobei hier häufig nur wichtige Spezialformen behandelt werden. Bei weitergehendem Interesse kann man z.B. in den manpages oder einem UNIXBuch nachsehen. Konfigurieren source datei Einlesen einer Konfigurationsdatei alias la=’ls -la’ Neuen Befehl la definieren 32 Kapitel 1: Einführung in UNIX / Linux .bashrc Konfigurationsdatei für Shell, die beim Start jeder neuen Shell automatisch ausgeführt wird. Hier können Dinge gespeichert werden, die dauerhaft beim Start jeder Shell berücksichtigt werden sollen. .private_functions Ergänzung der Datei .bashrc für eigene Erweiterungen (fachbereichsinterne Regelung). Diese wird automatisch eingebunden. Suche find . -name ’*.c’ Suche nach Dateinamen grep suchmuster datei Suche nach Text in Datei bzw. Dateien, z.B. grep nedit editor.txt which kommando Suche nach dem Pfad von Kommandos, z.B. which ssh Speicherplatz quota Zeigt an, wieviel Plattenplatz man beansprucht und ob die Grenze überschritten worden ist. Diese liegt momentan bei 200 MByte für Studenten ( Quota). Bei Überschreitung der vorgegebenen Grenze kann man sein Benutzerkonto praktisch nicht mehr benutzen. Auch ist der Start z.B. von KDE nicht mehr möglich. Somit ... Löschen von Dateien ist angesagt! du [optionen] [dateien] Berechnet bzw. schätzt den Speicherplatz, der von den angegebenen Dateien bzw. Verzeichnissen (inklusive Unterverzeichnisse) belegt wird. Z.B. berechnet du -sk . den von aktuellen Verzeichnis belegten Plattenplatz und gibt ihn als Summe in KByte aus (ohne -s würde der Platzbedarf für jedes Unterverzeichnisse einzeln mit ausgegeben.) Komprimieren und Archivieren gzip datei Komprimiert datei (eine oder mehrere), Ausgabe jeweils datei.gz gunzip datei.gz Dekomprimiert gz-Dateien bzip2 datei analog gzip bunzip2 datei.bz2 analog gunzip 1.11 Weitere nützliche Befehle 33 tar -cvf archiv.tar ordner Legt Datei-Archiv von ordner an, wobei c für create (d.h. erzeuge Archiv), v für verbose (d.h. gibt die Namen der Dateien aus) und f archiv.tar den Dateinamen für das Archiv bezeichnet tar -tvf archiv.tar Listet den Inhalt des tar-Archivs, wobei t für table of contents steht tar -xvf archiv.tar Entpackt Archiv tar -cvzf archiv.tgz ordner Legt Datei-Archiv von ordner an und komprimiert es danach mit gzip; nur bei GNUtar tar -xvzf archiv.tgz Entpackt komprimiertes Archiv Die Funktion von ”gzip” und ”tar” sind in den Kommandos ”zip / unzip” quasi vereint: zip zipdatei name(n) Komprimiert die Datei(en) name(n) (eine oder mehrere Dateien oder auch Ordner) und speichert das Resultat unter zipdatei unzip zipdatei Dekomprimiert die Datei(en) und Ordner die in zipdatei gespeichert sind Zugriff auf andere Rechner scp nutzer@rechner:datei . Datei zwischen Nutzern oder Rechnern austauschen ssh nutzer@rechner Anmelden auf einem anderen Rechner Gruppen grp -setup boerse Anlegen der Gruppe boerse grp -invite boerse nutzer Einladen von Teilnehmern grp -show boerse Anzeigen der Daten der Gruppe grp -show In welchen Gruppen bin ich ? chgrp boerse BOERSE Ändern der Gruppenzugehörigkeit vom Verzeichnis BOERSE. Darf nur einer aus der Gruppe machen. 34 Kapitel 1: Einführung in UNIX / Linux chmod 770 BOERSE Nur Mitglieder der Gruppe dürfen in das Verzeichnis Disketten und USB-Sticks mount /floppy Diskettenlaufwerk Root) einbinden (evtl. nur cp /floppy/hallo.txt . Kopiere Datei von Diskette ins aktuelle Verzeichnis umount /floppy Laufwerkseinbindung lösen (evtl. nur Root) cat /etc/fstab Welche Laufwerke gibt es? Z.B. /media/usbdisk Dokumententypkonvertierungsprogramme pdf2ps datei.pdf Umwandlung von PDF- nach PS-Datei ps2pdf datei.ps Umwandlung von PS- nach PDF-Datei ... Anzeigeprogramme gv name.ps Anzeigen einer Postscript-Datei (Ghostview) acroread name.pdf Anzeigen einer PDF-Datei (Acrobat Reader) xdvi name.pdf Anzeigen einer DVI-Datei 35 2 Algorithmen 2.1 Begriff (Versuch einer) Definition: Ein Algorithmus ist eine endliche Vorschrift, die genau angibt, wie ein bestimmtes Problem in einzelnen Schritten zu lösen ist, die von dieser Vorschrift exakt vorgegeben sind. (Taschenbuch der Wirtschaftsinformatik, S.168) Beispiele für Algorithmen: Kochrezepte, Montageanleitungen, . . . Die Durchführung eines Algorithmus liefert einen Prozess. Ein Prozessor ist eine Einheit, die einen Algorithmus durchführt, z.B. ein Mensch oder die CPU des Computers. Der Algorithmus selbst ist unabhängig von seiner Umsetzung in einem Prozess und seiner Abarbeitung durch einen Prozessor (z.B. unabhängig von der Implementierung in einer Programmiersprache) Die Entwicklung eines Algorithmus ist um so schwieriger, je komplexer die zu lösende Aufgabe ist. Deshalb zerlegt man die Aufgabe in Teilprobleme, zu deren Lösung man dann Algorithmen entwickelt. Sofern nötig kann man die Teilaufgabe wiederum in Teilaufgaben zerlegen, usw. Diese Methode heißt schrittweise Verfeinerung. Ein Algorithmus besteht aus einer Folge von einzelnen Schritten bzw. Anweisungen, so dass – zu einem bestimmten Zeitpunkt nur ein Schritt ausgeführt wird (serieller/sequentieller Algorithmus im Unterschied zum parallelem Algorithmus), – jeder Schritt genau einmal ausgeführt wird, – die Reihenfolge der Ausführung der Schritte die gleiche Folge ist, in der sie niedergeschrieben sind, – mit Beendigung des letzten Schrittes der Algorithmus endet. Wichtige prinzipielle Fragen beim Algorithmenentwurf sind 36 Kapitel 2: Algorithmen • Berechenbarkeit: Existiert ein Algorithmus, der das gegebene Problem löst? • Korrektheit: Löst der Algorithmus die gegebene Aufgabe korrekt? • Komplexität: Vergleich von Algorithmen nach Zeit- und Speicherbedarf Zu einem Algorithmus gehören in der Regel Eingabe- und Ausgabedaten. Der Ausführungsteil besteht aus einer endlichen Folge von Anweisungen, die aus einer Reihe von typischen Bausteinen bestehen, welche man wiederum ineinander verschachteln kann. • Aneinanderreihung (Sequenz) • Auswahl (Selektion) • Schleife (Wiederholung, Iteration) • Funktion Diese werden im Folgenden beschrieben. 2.2 Sequenzen Eine Sequenz ist eine Abfolge bzw. Aneinanderreihung einzelner Anweisungen, wobei die Sequenz selbst wiederum als Anweisung anzusehen ist. Beispiel: Zubereitung einer Tasse Pulverkaffee 1. Koche Wasser 2. Gib Kaffeepulver in die Tasse 3. Fülle Wasser in die Tasse In dieser Sequenz findet sich sich eine Aneinanderreihung von 3 Anweisungen, wobei alle 3 zusammen wiederum eine Anweisung ergeben. 2.3 Auswahl (Selektion) 37 Verfeinerung des Algorithmus 1.1 1.2 1.3 1.4 2.1 2.2 2.3 2.4 3. Fülle Wasserkocher mit Wasser Schalte Wasserkocher an Warte bis es kocht Schalte Wasserkocher bei Bedarf aus Öffne Kaffeeglas Entnimm einen Löffel Kaffee Kippe Löffel in die Tasse Schließe Kaffeeglas Fülle Wasser in die Tasse Algorithmen, die nur aus einer Sequenz von Anweisungen bestehen, sind unßexibel und in ihrer Ausführung starr, da sie keine Alternativen enthalten, wenn z.B. ein Schritt nicht durchführbar ist. In unserem Beispiel tritt dieser Fall dann ein, wenn das Kaffeeglas leer ist. 2.3 Auswahl (Selektion) Man benötigt ein Konstrukt, das eine Auswahl unter 2 möglichen Teilalgorithmen vorsieht. FALLS DANN SONST Bedingung Anweisung1 Anweisung2 Falls die Bedingung erfüllt ist, wird Anweisung1 durchgeführt, während Anweisung2 vernachlässigt wird, d.h. nach Ausführung von Anweisung1 wird mit dem Algorithmus direkt nach Anweisung2 fortgefahren. Falls die Bedingung nicht erfüllt wird, so wird Anweisung2 ausgeführt und Anweisung1 nicht berücksichtigt. Die Anweisungen können wiederum aus Sequenzen, Auswahl, Wiederholung oder Funktionsaufrufen bestehen. Beispiel: Kaffeekochen mit Entscheidung Verfeinerung von Schritt 2.1 durch Auswahl 38 Kapitel 2: Algorithmen 2.1.1 Nimm Kaffeeglas aus dem Schrank 2.1.2 FALLS Kaffeeglas leer ist DANN Nimm neues Kaffeeglas 2.1.3 Öffne Kaffeeglas Der SONST-Teil kann hier entfallen, weil keine Alternativ-Aktion nötig ist. Auswahl-Aktionen können auch ineinander geschachtelt werden („nesting“): 2.1.1 Nimm Kaffeeglas aus dem Schrank 2.1.2 FALLS Kaffeeglas leer ist DANN FALLS Weiteres Kaffeeglas vorhanden DANN Nimm neues Kaffeeglas SONST Brich Kaffeekochen ab 2.1.3 Öffne Kaffeeglas Problem: Möglicherweise ist nicht von vornherein klar, wieviele Fälle unterschieden werden müssen! 2.4 Schleifen (Wiederholung, Iteration) Wir benötigen ein Konstrukt, in dem die Anweisungen solange wiederholt werden, bis die Teilaufgabe gelöst ist – Schleifen. Folgende drei Arten stehen in der Regel zur Verfügung: • SOLANGE Bedingung erfüllt FÜHRE AUS Anweisungsfolge While-Schleife, d.h. Schleife mit Vorabtest. Ist die Bedingung in dieser Schleife nicht mehr erfüllt, so wird der Schleifendurchlauf abgebrochen und mit der nächsten Anweisung fortgefahren, die auf das Schleifenkonsrukt folgt. Ist die Bedingung bereits beim ersten Durchlauf nicht erfüllt, so wird die Anweisungsfolge innerhalb der Schleife niemals ausgeführt, da die Prüfung der Bedingung jeweils vor dem Schleifendurchlauf stattfindet (Abweisende Schleife). Die Wiederholung ist selbst wiederum eine Anweisung. • WIEDERHOLE Folge von Anweisungen SOLANGE Bedingung 2.4 Schleifen (Wiederholung, Iteration) 39 erfüllt Do-While-Schleife, d.h. Schleife mit (weiterführendem) Nachtest. Der Schleifendurchlauf wird wiederum fortgeführt, solange die Bedingung erfüllt ist. Ist die Bedingung bereits beim ersten Durchlauf nicht erfüllt, so wird die Anweisungsfolge innerhalb der Schleife trotzdem einmal ausgeführt, da die Prüfung der Bedingung jeweils nach dem Schleifendurchlauf stattfindet (Aufnehmende Schleife). • WIEDERHOLE Folge von Anweisungen BIS Bedingung erfüllt Do-Until-Schleife, d.h. Schleife mit (beendendem) Nachtest. Der Schleifendurchlauf wird wiederholt, bis die Bedingung erfüllt ist (dann wird die Schleife abgebrochen. • FÜR Zähler VON Anfang BIS Ende FÜHRE AUS Anweisungsfolge For-Schleife, d.h. eine Zählschleife. Bei dieser Schleifenform wird eine (evtl. erst zur Laufzeit) vorgegebene Anzahl von Schleifendurchläufen absolviert. Achtung: Für alle Schleifenformen gilt, dass man die Abbruchbedingung so formulieren muss, dass die Schleife in jedem Fall nach endlich vielen Durchläufen beendet wird. Dies kann auch im Schleifenrumpf geschehen, sofern eine Endlosschleife realisiert wurde. Notfalls muss man eine maximale Anzahl an Schritten vorgeben. Ebenso ist zu vermeiden, dass die Schleife weiter durchlaufen wird, obwohl die Schritte in der Schleife nicht mehr ausführbar sind. Beispiel: Kaffeekochen mit Wiederholung Schritt 2.1 mit Wiederholungsschleife 2.1.1 WIEDERHOLE Nimm Kaffeeglas aus dem Fach BIS Kaffeeglas gefüllt oder keine Gläser mehr vorhanden 2.1.2 FALLS Weiteres gefülltes Kaffeeglas vorhanden DANN Öffne Kaffeeglas SONST Brich Kaffeekochen ab 40 2.4.1 Kapitel 2: Algorithmen Maximumsbestimmung (Bsp. zur 2. bzw. 1. Schleifenart) Eingabe: Liste von Zahlen Ausgabe: Größte Zahl in der Liste Setze erste Zahl der Liste als bislang größte Zahl WIEDERHOLE Lies nächste Zahl in der Liste FALLS Zahl größer als bisher größte Zahl DANN Setze diese Zahl als bislang größte Zahl BIS Liste erschöpft Schreibe die insgesamt größte Zahl raus Problem: Wenn die Liste nur aus einer Zahl besteht, dann kann die erste Anweisung des Schleifenrumpfs nicht ausgeführt werden (da dies eine aufnehmende Schleife ist). Ein Prozessor, der diesen Algorithmus ausführen soll, befindet sich so in einem undefinierten Zustand. Lösung: Schleife mit Vorabtest (und Auswahl) Setze erste Zahl der Liste als bislang größte Zahl SOLANGE weitere Zahl in Liste FÜHRE AUS Lies nächste Zahl in der Liste FALLS Zahl größer als bisher größte Zahl DANN Setze diese Zahl als bislang größte Zahl Schreibe die insgesamt größte Zahl raus 2.4.2 Berechnung von Potenzen xn (Bsp. für Zählschleife) Eingabe: Reelle Zahl x, natürliche Zahl n Ausgabe: xn Übernimm die Werte von x und n Setze Produkt auf 1 FÜR k VON 1 BIS n Multipliziere Produkt mit x Schreibe Produkt raus 2.5 Funktionen 41 2.5 Funktionen Funktionen erleichtern die schrittweise Verfeinerung eines Algorithmus und erlauben die wiederholte Verwendung von Algorithmenteilen, ohne diese Teile erneut eingeben zu müssen. Dies ist extrem vorteilhaft, da es Zeit und Platz spart und den Code sehr viel übersichtlicher macht. Beispiel: Zeichengerät/Plotter zeichnet zwei konzentrische Quadrate B A ZeichneQuadrat(10, A) ZeichneQuadrat(15, B) Verfügbare Funktionen Funktionalität forward(x) Bewege Stift um x Einheiten vorwärts (in die aktuelle Richtung) Drehe Zeichenrichtung um α Einheiten nach rechts Senke Stift auf den Punkt Y und setze die aktuelle Zeichenrichtung (0 = nach rechts) Hebe Stift vom Papier ab right(α) down(Y ,α) up ZeichneQuadrat(x, Y ) down(Y ,0) FÜR k VON 1 BIS 4 { forward(x) right(π/2) } up Man sieht hier, dass die Funktion ZeichneQuadrat zwei Argumente erhält. Einen Rückgabewert hat diese Funktion nicht. Die hier beschreibene Methode des Zeichnens heisst "Turtle Graphics" (man stelle sich eine laufende Schildkröte vor, die hinter sich einen in Tinte getränkten Schwanz herzieht). 42 Kapitel 2: Algorithmen 2.6 Vom Algorithmus zum Programm Um den Algorithmus durch einen Computer ausführen zu lassen, muss man ihn in einer Programmiersprache formulieren und diese dann in eine für den Computer verständliche Sprache, der Maschinensprache, übersetzen. Computer haben eine eigene, vom Typ der CPU abhängige Maschinensprache, d.h. eine Menge von Instruktionen und Daten in einem für den Menschen kaum lesbaren Binärcode (d.h. einer Folge von Bits). Die Assemblersprache wiederum formuliert die Prozessorbefehle des Maschinencodes als sog. Mnemonics in einer einfachen und leichter zu lesenden Syntax. Die Steuerbefehle sind durch Zeichen bzw. Worte statt durch pure Bits verschlüsselt, d.h. jede zulässige Bitfolge wird durch eine oder mehreren Befehlen (sog. Mnemonics) und evtl. zusätzlichen Zahlen dargestellt. Der Quelltext (welcher die Mnemonics enthält) wird dann von einem Assembler (d.h. dem Übersetzungsprogramm) nach der Programmerstellung nahezu eins zu eins in die Maschinensprache übersetzt. Ein Programm, dass in der Assemblersprache geschrieben ist, ist sehr schnell. Insbesondere „besitzt“ jeder Prozessortyp seinen eigenen Assemblercode. Größere Algorithmen sollten trotz des Geschwindigkeitsvorteils nur in Ausnahmefällen (geschwindigkeitskritische Teile des Codes) in Assembler geschreiben werden, da es zu aufwändig und zu fehleranfällig ist. Deshalb wurden (und werden) höhere Programmiersprachen entwickelt, die u.a. deutlich einfacher zu handhaben sind und sehr viel mächtigere Paradigmen besitzen. Zu diesen gehören z.B. • imperative bzw. prozedurale Sprachen C, Pascal, Modula-2, FORTRAN, Basic, Algol, . . . • imperative objektorientierte Sprachen Modula-3, Ada, C++, Java, . . . • funktionale Programmiersprachen Haskell, ML, OCaml, LISP, . . . • Programmiersprachen für spezielle Anwendungen SQL (Datenbanken), HPC Fortran (Parallelrechnen), . . . Höhere Programmiersprachen sind in der Regel rechnerunabhängig, ihre Beherrschung erfordert die Kenntnis folgender Elemente der entsprechenden Sprache: • Lexikalischen Elemente, d.h. dem Symbolvorrat der Sprache (Schlüsselwörter und Interpunktionszeichen, Operatoren, Bezeichner, Trenner, Literalkonstanten). Aus dieser Menge bilden sich dann die (korrekt gebildeten) Programme der Sprache -> analog Wörter, Satzzeichen und Trenner in menschlicher Sprache. • Syntax, d.h. welche Folgen von lexikalischen Elementen bilden zulässige Sätze in der betreffenden Programmiersprache (d.h. Programme, die der Compiler verstehen und übersetzen kann). Dieses wird durch die Synatx bzw. Grammatik geregelt -> analog zur menschlichen Sprache. 2.6 Vom Algorithmus zum Programm 43 • Semantik, d.h. Bedeutung und Sinnhaftigkeit formal korrekter Sätze. Sie beschreibt also, welche Aktion das Programm ausführt oder wie eingegebene Daten verarbeitet werden. Algorithmus ? Programmierung ? Programm in höherer Programmiersprache ? Übersetzung ? Programm in Maschinensprache ? Interpretation durch CPU Programm wird ausgeführt Damit ein Algorithmus auf einem Computer ausgeführt werden kann, muss dieser in einer Programmiersprache implementiert werden. Im nächsten Schritt muss das entsprechende Programm dann in die Maschinensprache des Prozessors übersetzt werden, d.h. es wird eine Folge von Maschinensprachebefehlen erzeugt, die der Prozessor versteht und ausführen kann. Dieses geschieht mittels folgenden Übersetzungswerkzeugen: • Compiler: Der gesamte Quelltext wird übersetzt und ein ablauffähiges Programm wird inkl. Link-Vorgang erzeugt. Beispiel: Übersetzte Rede als Manuskript. • Interpreter: Es wird je ein Befehl übersetzt und abgearbeitet (Ausführung ist i.d.R langsamer als compilierter Code). Beispiel: Simultanübersetzung. 44 Kapitel 2: Algorithmen • Mischformen: Just-In-Time-Compiler, Byte-Code-Interpreter Merke: Ein Algorithmus ist unabhängig von der Programmiersprache, d.h. der Algorithmus bleibt gleich, aber seine Implementierung ist i.d.R. von Programmiersprache zu Programmiersprache verschieden. Abschließend: Wie formuliere ich ein gegebenes Problem als Algorithmus ? Wichtig ... Nicht gleich Losprogrammieren, sondern erstmal zurücklehnen und sich das Problem klar machen (Problemanalyse). Bei größeren Problemen Problem in Teilprobleme zerlegen. Dann einen Grobentwurf des Algorithmus (eine Strategie) zur Lösung des Problems machen, d.h. grobe Skizze der Vorgehensweise (erstmal umgangssprachlich, aber schon strukturiert). Dann erzeuge feineren Entwurf (Verfeinerung), der wiederum umgangssprachlich formuliert oder auch mithilfe grafischer (veranschaulichender) Objekte oder Pseudo-Code erzeugt werden kann. Abschliessend wird dann das Program erstellt, welches dann getestet wird. Genau genomen heißt das, das man ein ausführbares Programm erstellt und dieses testet, und dabei evtl. weitere Fehler entdeckt, die man korrigieren muß. Danach wird dann wieder getestet. Dieser Zyklus kann theoretisch endlos sein, aber bei einem bestimmten (zufriedenstellenden) Qualitätsstand wird abgebrochen. 45 3 Einführung in C/C++ 3.1 Überblick 3.1.1 Geschichte C++ ist eine der verbreitetsten und industriell wichtigsten Programmiersprachen. Sie ist als Mehrzwecksprache konzipiert und unterstützt insbesondere effiziente und maschinennahe Programmierung, Datenabstraktion sowie prozedurale bzw. objektorientierte und generische Programmierung. C++ basiert auf der Programmiersprache C. Zusätzlich zu den in C vorhandenen Möglichkeiten (Schleifen, Funktionen, Zeiger usw.) bietet C++ weitere Datentypen, Klassen mit Vererbung, Ausnahmebehandlung, Templates (Schablonen), Namensräume, Überladen von Operatoren und Funktionsnamen, und Operatoren zur Freispeicherverwaltung. Mit der C++-Standardbibliothek steht zusätzlich eine erweiterte Bibliothek für verschiedene generische Container, Funktionen zu deren Manipulierung, Strings, Dateizugriffe, etc. zur Verfügung. 1972 In den Bell Labs entwickeln Ken Thompson und Dennis Ritchie C als Sprache für Systemprogrammierung unter Unix. 1973 Implementierung von UNIX in C 1978 Ritchie und Kernighan schaffen mit dem Buch „The C Programming Language“ einen Quasi-Standard für C (K&R-Standard). 1989 Der Standard ANSI1 -C vereinheitlicht die verschiedenen C-Dialekte (genannt C89). Dadurch sind C-Programme besonders portabel. Dieser Stan1 American National Standards Institute 46 Kapitel 3: Einführung in C/C++ dard wird 1990 von der ISO übernommen ( C90). 2 (bis auf kleinere Änderungen) vollständig 1980 Bei AT&T erweitert Bjarne Stroustrup C zu „C mit Klassen“. 1983 C mit Klassen wird in C++ umbenannt und die Sprache für Anwendungsprogrammierung weiterentwickelt. 1993 Nach einer Vorlage von Hewlett-Packard wird mit der Entwicklung einer C++-Standardbibliothek3 begonnen. 1998 Die endgültige Fassung der Sprache C++ wird von der ISO4 genormt. 2005 Das C++ standardization committee verabschiedet einen Technical Report mit Erweiterungen zur C++-Standardbibliothek. Eine weitere Liste ist in Arbeit. Keine Panik C++ ist zwar eine Weiterentwicklung von C, aber C ist keine echte Teilmenge von C++. In diesem Kurs werden wir sozusagen mit dem C-Teil von C++ beginnen. Man muss nicht alle Sprachelemente kennen, um zu programmieren. Diese lernt man nach und nach. Objektorientiertes Programmieren (Klassen, Vererbung, . . . ) erlaubt strukturiertes Programmieren. Die STL ist eine mächtige Ergänzung zu C++ und vereinfacht häufige Aufgaben. ...................................................................................................................................................................................... .. .. ... .... ... ... . ... ................................................................................................................. ... ... ... .. ... . . . . .. ... . . ... . . ... . . ... . . ... . . . .... .... ... .... ... . ... .... . .... ... ... .... .... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... .. ... ... ... ... ... ... ... ... ... .. ... ... ... ... ... ... ... ... ... ... ... ... .... ... ... ... ... ... ... ... ... ............................................................................................................... ... ... ... ... .. ... .. ... ..................................................................................................................................................................................... C++ STL C Abbildung 3.1: C, C++ und die STL 2 International Standardization Organization Standard Template Library 4 International Organization for Standardization 3 STL, 47 3.1 Überblick 3.1.2 Compiler und Tools Nachdem der Quelltext (engl. source code) mit einem Editor geschrieben wurde, wird der Quelltext (*.cpp oder *.cc) vom Compiler unter Verwendung eventueller Headerdateien (*.hh) in eine oder mehrere Zwischendateien (*.o, Objekt-Datei) übersetzt. Der Linker bindet die Zwischendateien mit weiteren Bibliotheken zu einem lauffähigen Programm, wobei Bibliotheken eine Sammlung bereits übersetzter Funktionen (z.B. sin aus der Standardmathebibliothek) sind. Falls sich im Quelltext ein Fehler befindet, so bricht der Compiler den Übersetzungsvorgang ab und gibt einen Fehler aus, welchen man dann wieder im Quelltext bereinigen muß. Dieser Editier-Übersetzungszyklus erfolgt so lange, bis der Compiler den Übersetzungsprozess einwandfrei beendet hat. Das auf den Übersetzungsprozess folgende Linken erzeugt das ausführbare Programm (sofern er alle Dateien und Bibliotheken gefunden hat). Übersetzung und Linken werden in dem von uns benutzten Compiler (scheinbar) in einem Schritt durchgeführt. Kleine Programme, die nur aus einer Datei bestehen, können in einem Schritt verarbeitet werden. Wie man größere Projekte handhabt, werden wir im SoSe 2007 sehen. Compiler ..... ........................................................................................................ ..... ...... ............ ..... .. ... ... ... ... ... ... ... ... ... ... ..................................... Linker ..... ................................................................................................. ..... .... ............ ....... ... .... ... ... .... .. ... ... ... ... ... ... ... ... ... ...................................... ausführbares Programm Bibliotheken Abbildung 3.2: Compiler und Linker Beliebte Compiler GNU-g++ Intel C++ Microsoft Visual C++ IDE Kommandozeilencompiler; sehr beliebt und mächtig! (kostenlos) Dieser Kommandozeilencompiler erzeugt hocheffizienten Code für Intel-Prozessoren (Windows, Linux) Verbreitetster Compiler unter Windows. Einfache Version auch kostenlos erhältlich. 48 Kapitel 3: Einführung in C/C++ Größere Programme werden in der Regel nicht mehr mit einfachen Texteditoren erstellt, sondern mit Integrierten Entwicklungsumgebungen (IDE), • die grundsätzlich aus den Programmierwerkzeugen Editor, Compiler, Linker und Debugger bestehen, • die mehrere Quelltexte bzw. große Projekte verwalten können, • die Quelltexte übersetzen und Programme erstellen, • die Sprachelemente farblich markieren, • Quelltexte einheitlich formatieren, • die grafische Benutzeroberflächen leicht erstellen lassen, • die die Fehlersuche vereinfachen (komfortable Debugger). Verbreitete IDEs sind KDevelop für Linux und Microsoft Visual C++ und MinGW-DEveloper-Studio für Windows (Entwicklung von MinGW liegt im Moment auf Eis). Hinweis für die Übungen SS2009: Die Entwicklungsumgebung unter Mac OS X heisst Xcode. Der Editor von Xcode wird standardmäßig aufgerufen, wenn ein C / C++ Datei mit einem Doppelklick geöffnet wird. Der Editor ist recht intuitiv zu verwenden und führt automatisch Formatierungen und eine farbliche Hervorhebung verschiedener Syntaxelemente durch. Das Compilieren kann man mit g++ von der Komandozeile aus erledigen. Für unsere Zwecke ist dieses im Prinzip der einfachste Weg. In der Vorlesung wird kurz besprochen, wie Xcode auch zum Compilieren der Übungsprogramme verwendet werden kann. Literaturauswahl und Links • Bjarne Stroustrup: „Die C++-Programmiersprache“, Addison-Wesley, 2000. (Klassiker von Mr. C++ persönlich) • Martin Schader, Stefan Kuhlins: „Programmieren in C++“, SpringerVerlag, 1998. (Sehr gut für Anfänger und Fortgeschrittene, u.a. da viele Aufgaben mit Lösungen) • Kyle Loudon: „C++ kurz & gut“, O’Reilly, 2003. (Nachschlagewerk) • Ray Lischner: „STL kurz & gut“, O’Reilly, 2004. (Nachschlagewerk) • Kernighan, Brian; Ritchie, Dennis: „Programmieren in C“, HanserVerlag. 3.1 Überblick 49 • Krüger, Guido: GoTo C-Programmierung, Addison-Wesley, 1998. • C Von A bis Z (http://www.pronix.de/pronix-4.html) (siehe auch http://www.galileocomputing.de und dort Menüpunkt „openbooks“.) Online-Resourcen • Tutorial C++: http://www.mathematik.uni-marburg.de/∼cpp • Tutorial C++: http://www.volkard.de/Cpp/index.html • Tutorial C++: http://de.wikibooks.org/wiki/C++-Programmierung • Referenz C++: http://www.cppreference.com • Referenz C++: http://www.cplusplus.com • Alles mögliche über C/C++: http://www.c-plusplus.de/cms/ • STL: http://www.sgi.com/tech/stl • Tutorial C: http://wwwuser.gwdg.de/∼kboehm/ebook/inhalt.html • Tutorials C: http://www.c-plusplus.de/cms/ (siehe unter Tutorials/C z.B. „Einführung in die Sprache C“ (!!) oder „Softwareentwicklung in C“) • Linkliste: http://www.rz.uni-freiburg.de/bera/c-kurs/scripten.html • Xcode: http://developer.apple.com/documentation/DeveloperTools/ Conceptual/XcodeQuickTour/qt_intro/qt_intro.html 3.1.3 Das nullte und kürzeste C++-Programm Ein minimales C++-Programm sieht so aus: zero.cpp: Das nullte und kürzeste C++-Programm 1 2 3 // zero . cpp : Das nullte C ++ - Programm int main () {} Das Programm schreiben wir in einem Texteditor, speichern es unter obigem Namen und übersetzen es dann mit dem Compiler g++: $ g++ zero.cpp 50 Kapitel 3: Einführung in C/C++ Dies erzeugt das Programm a.out, das man mit $ a.out bzw. $ ./a.out ausführen kann. Probiere zuerst die erste Version, dann die 2. (falls die 1. nicht funktioniert). Wir sehen: Beim Programmaufruf passiert (natürlich) nichts. Prinzipiell besteht der C++-Quelltext (C++-source code) aus Kommentaren, Präprozessorbefehlen und Anweisungen, welche wiederum aus Schlüselwörtern und Interpunktionszeichen, Operatoren, Literalen (Konstanten), Trennern und Bezeichnern (gehören alle zu den lexikalischen Elementen) bestehen. An diesem kleinen Programm erkennt man schon einige der oben genannten Bausteine der C++-Syntax: • Kommentare sollen das Verstehen und Nachvollziehen des Programms vereinfachen. Der Compiler ignoriert Programmteile von // bis zum Zeilenende. Alternativ können auch Kommentare durch /* */ umschlossen werden, die dann auch über mehrere Zeilen gehen können ( Blockkommentare). Benutzt Kommentare reichhaltig, denn unkommentierte Programme sind später häufig nur noch schwer bzw. gar nicht zu verstehen! • Schlüselwörter sind in C++ vordefinierte bzw. reservierte Wörter mit bestimmten Bedeutungen. Diese dürfen vom Programmierer nicht als Bezeichner für z.B. Variablen benutzt werden. Wichtige Beispiele für C++Schlüsselwörter sind – if-else, switch-case-default, – while, do-while, for, – bool, char, short, int, long, float, double, void, signed, unsigned, – const, sizeof – struct, class, union, enum, typedef – private, public, protected, – return, continue, break, goto, try-catch – new, delete, true, false. In unserem Programm findet sich mit int ein Schlüsselwort. • Wichtige Interpunktionszeichen sind – ‘,‘ , ‘;‘ , ‘:‘ , – ‘(‘ , ‘)‘ , ‘‘ , ‘‘ , ‘<‘ , ‘>‘. 3.2 Hello, world! 51 mit denen z.B. Anweisungen abgeschlossen oder in Blöcken zusammengefaßt werden. In diesem Programm sind die (geschweiften) Klammern zu finden. • Operatoren führen verschiedene Operationen auf ihren Argumenten (Operanden) aus. C++ besitzt u.a. folgende Operatoren: – ‘+‘ , ‘-‘ , ‘*‘ , ‘/‘ , ‘%‘ , – ‘=‘ , ‘ !=‘ , ‘>‘ , ‘<‘ , ‘<=‘ , ‘>=‘ , ‘«‘, ‘»‘, – ‘&&‘ , ‘||‘ , ! (Negation). • Trenner sind z.B. Leerzeichen, Tabulatoren, Zeilenendezeichen, Seitenvorschübe und Kommentare. Sie dienen dazu, die übrigen lexikalischen Elemente voneinander zu trennen, d.h. auf aufeinanderfolgenden Bezeichnern oder Schlüsselwörtern muss immer ein Trenner folgen. • Bezeichner sind Namen, die man an Variablen, Konstanten, Funktionen oder Strukturen (bzw. Klassen) im Programm vergibt. Sie müssen mit einem Buchstaben oder Unterstrich beginnen, während sich daran eine Folge von Groß- und Kleinbuchstaben (die unterschieden werden) sowie Ziffern von 0 bis 9 und Unterstriche anschließen können. Bezeichner können beliebig lang sein. • C++-Programme bestehen u.a. aus Funktionen (hier int main()), die sich gegenseitig aufrufen können. Jede Funktion löst eine bestimmte Aufgabe. Funktionen sind entweder selbst erstellt oder bereits geschrieben und Teil der Standard-C++-Bibliothek oder einer anderen Bibliothek. Funktionen liefern i.A. Rückgabewerte (hier vom Typ int; Ausnahme void) und ihnen können Argumente übergeben werden (hier nicht). • In jedem C++-Programm gibt es die Funktion main, bei der das Programm beginnt. Diese Funktion bildet das steuernde Element des Programms. Sie wird automatisch vom System aufgerufen, wenn das Programm startet. Ein Programm kann nur genau eine main-Funktion besitzen. • Zusammengehörige Programmblöcke werden durch { fasst. } zusammenge- • Das Programm endet, wenn man aus der Funktion main zurückkehrt bzw. wenn die Funktion main beendet ist. Der Rückgabewert von main (int), der an das Betriebssystem zurückgegeben wird, ist 0 („success“, d.h. fehlerfreier Programmlauf ), wenn nichts anderes angegeben ist. 3.2 Hello, world! Als Erstes sollen die Worte „Hello, world!“ auf dem Bildschirm erzeugt werden. 52 Kapitel 3: Einführung in C/C++ hello.cpp: Hello world in C++, erster Versuch 1 2 3 4 5 # include < iostream > int main () { std :: cout << Hello , world !; } • Für die Textausgabe auf der Standard-Ausgabe ( standard output, d.h. dem Bildschirm), gibt es den Standardausgabestream -bzw. strom std::cout, der von C++ zusammen mit anderen Standard- Funktionen zur Ein-/Ausgabe in dem Modul iostream zur Verfügung gestellt. • Mit der Präprozessoranweisung5 #include <iostream> wird das Modul in den Programmtext textuell vom Compiler bzw. Präprozessor eingefügt. • Der Operator („nach“) schreibt das, was rechts steht, in das was links steht, d.h. in unserem Fall wird der Text nach std::cout geschrieben. • (Fast) Jeder C++-Befehl bzw. jede C++-Funktion muss durch ein Semikolon abgeschlossen werden. • C++ unterscheidet zwischen Groß- und Kleinschreibung. Alle Befehle werden prinzipiell klein geschrieben. Wir versuchen das Programm zu übersetzen: $ g++ hello.cpp hello.cpp: In function ‘int main()‘: hello.cpp:4: error: ‘Hello‘ was not declared in this scope hello.cpp:4: error: ‘world‘ was not declared in this scope hello.cpp:4: error: expected ‘;‘ before ‘!‘ token Wo liegt der Fehler? Wir wollen als Argument von std::cout eine StringKonstante übergeben, die aber durch Anführungszeichen gekennzeichnet sein muss. hello1.cpp: Hello world in C++, zweiter Anlauf 1 2 3 4 5 # include < iostream > int main () { std :: cout << " Hello , ␣ world ! " ; } $ g++ hello1.cpp $ a.out Hello, world!$ 5 Präprozessoranweisungne beginnen immer mit Doppelkreuz und stehen am Zeilenanfang. Ohne Semikolon. Siehe 3.9. 3.3 Fundamentale Datentypen 53 Warum erscheint das Prompt $ direkt hinter dem ausgegebenen Text? In dem String, der std::cout übergeben wird, muss noch ein Befehl zum Zeilenumbruch eingebaut werden. Dies geschieht mit dem Steuerbefehl std::endl. (Bem: Steuerbefehle dienen der Steuerung von Ausgabegeräten, wie z.B. Bildschirm oder Drucker.) hello2.cpp: Ein fast perfektes Hello world 1 2 3 4 5 # include < iostream > int main () { std :: cout << " Hello , ␣ world ! " << std :: endl ; } $ g++ hello2.cpp $ a.out Hello, world! $ 3.3 Fundamentale Datentypen Wie schon im ersten Kapitel erläutert wurde, ist der Arbeitsspeicher in Form von Speicherzellen organisiert und sequentiell durchnumeriert (jede Speicherzelle repräsentiert 1 Bit). Da es aber nicht sehr komfortabel ist, die Speicherplätze selber zu verwalten und über ihre eindeutige Nummer anzusprechen, werden ihnen vom Compiler „Namen gegeben“ (etwas vereinfacht und blumig gesprochen). Diese benannten Speicherplätze werden als Variablen bezeichnet (wobei diese immer ein Vielfaches von 8 Bit (=1 Byte) belegen). Falls eine neue Variable definiert wird, so vergibt der Compiler automatisch den nächsten freien Platz (abhängig von der Größe bzw. Datentyp der Variablen). Der Typ einer Variablen gibt an, was in ihr gespeichert wird. Damit ist auch festgelegt, wieviel Speicherplatz für die Variable reserviert wird (in Byte = 8 Bit) und welche Operationen sie erlaubt. C++ ist streng typisiert, d.h. alle Variablen und Funktionen haben einen vom Programmierer genau festgelegten Typ, der bestimmt, welchen Wert die Variable annehmen darf und welche Operationen auf ihnen erlaubt sind. Die fundamentalen Datentypen von C++ lauten: • Ganzzahlige bzw. Integer-Typen • Fließkomma- bzw. Float-Typen • void-Datentyp (können Variablen nicht annehmen) 54 Kapitel 3: Einführung in C/C++ Integer- und Float-Typen können die unendlichen Mengen Z und R nicht vollständig darstellen. In ?? werden wir aus diesen fundamentalen bzw. elementaren Typen komplexere Datentypen aufbauen. 3.3.1 Deklaration Variablen müssen vor ihrer Benutzung deklariert werden. Dadurch verbindet der Compiler den verwendeten Namen mit dem zugewiesenen Datentyp. Die Deklaration einer Variablen muss erfolgen, bevor sie zum ersten Mal benutzt wird (in der Regel wird dieses am Anfang einer Funktion oder eines Blocks erfolgen - muss aber nicht). Eine Deklaration hat die Form: Typbezeichnung Variablenname ; Hier wird dem Compiler weiterhin gleichzeitig angewiesen, dass für diese Variable auch Speicherplatz reserviert werden soll und dieser Speicherplatz soll an die Variable geknüpft sein (Definition der Variablen). Bei der Deklaration kann die Variable auch initialisiert werden. Der Wert, der ihr dabei zugewiesen wird, wird mit dem Zuweisungsoperator = angegeben: Typbezeichnung Variablenname = Wert ; Solange Variablen nicht initialisiert wurden, haben sie einen undefinierten und unvorhersagbaren Wert. (Vorsicht! Werden uninitialisierte Variablen benutzt, so kann das zu merkwürdigem Programmverhalten führen !) Erfordert der Ausdruck auf der rechten Seite des Zuweisungsoperators eine Berechnung, so wird dieser zunächst ausgewertet und erst dann der Variablen zugewiesen. Vor dem Zuweisungsoperator darf nur ein Variablenname stehen. declare.cpp: Deklaration von Variablen 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # include < iostream > int main () { int a ; int b ; int c ; a = 12; b = 88; c = a + b; std :: cout << " Die ␣ Zahlen ␣ lauten ␣ " << a << " ,␣ " << b << " ␣ und ␣ " << c << " . " << std :: endl ; } 3.3 Fundamentale Datentypen 55 $ g++ declare.cpp $ a.out Die Zahlen lauten 12, 88 und 100. $ declare2.cpp: kürzere Deklaration 1 2 3 4 5 6 7 8 3.3.2 # include < iostream > int main () { int a =12 , b =88 , c = a + b ; std :: cout << " Die ␣ Zahlen ␣ lauten ␣ " << a << " ,␣ " << b << " ␣ und ␣ " << c << " . " << std :: endl ; } Ganzzahltypen In C++ gibt es mehrere Datentypen, die verschiedene endliche Teilmengen von Z repräsentieren. Der Speicherplatz für Variablen wird in Blöcken von 8 Bits (1 Byte) reserviert. Mit 1 Byte können genau 28 = 256 Zustände dargestellt werden, z.B. die Zahlen {0, 1, . . . , 255}. Standardmäßig verwendet C++ jedoch die vorzeichenbehafteten Zahlen {−128, −127, . . . , 127}. Die folgenden Typen stehen in C++ zur Verfügung: Typ char unsigned (signed) unsigned (signed) unsigned (signed) unsigned char short short int int long long Bytes 1 1 2 2 4 4 4 4 Zahlenbereich −128 . . . 127 0 . . . 255 −32768 . . . 32767 0 . . . 65535 −2147483648 . . . 2147483647 0 . . . 4294967295 −2147483648 . . . 2147483647 0 . . . 4294967295 Bei char ist nicht festgelegt, ob er mit oder ohne Vorzeichen implementiert ist, d.h. dies ist vom Compiler abhängig. Durch Angabe von unsigned und dem optionalen Schlüsselwort signed wird zwischen vorzeichenlosen und vorzeichenbehafteten Zahlen gewählt. Der Speicherbedarf der Typen ist nicht durch einen Standard festgelegt, sondern abhängig vom Compiler. Meist entspricht er aber dieser Tabelle. Es muss aber folgende Beziehung immer gelten: 1 = size(char) ≤ size(short int) ≤ size(int) ≤ size(long int), 56 Kapitel 3: Einführung in C/C++ wobei size die Größe des Datentyps in Vielfachem vom Typ char angibt. 3.3.3 Grundrechenarten Die Symbole +,-,*,/ werden für die Grundrechenarten benutzt, wobei * und / stärker binden als + und - (Punkt vor Strich-Regel). Durch Gruppierung mit runden Klammern ( ) lässt sich die Reihenfolge der Auswertung explizit festlegen. Bei Integer-Variablen entspricht / der Ganzzahldivision, also 3/2=1 oder -9/2=-4. rechnen.cpp: Rechnen mit ganzen Zahlen 1 2 # include < iostream > 3 4 5 6 7 8 int main () { int a ,b ,c , d ; 9 10 11 12 13 14 a b c d a = = = = = 3*2; c = a * 2; c * 2; c / (a -1); b * d + c; std :: cout << " Das ␣ Ergebnis ␣ ist ␣ " << a << " . " << std :: endl ; } Wie kommt das Ergebnis zustande? Anweisung a = 3*2; b = c = a * 2; c = c * 2; d = c / (a-1); a = b * d + c; a 6 6 6 6 72 b c d 12 12 12 12 12 24 24 24 4 4 Den Rest der Ganzzahldivision erhält man mit dem Modulo-Operator %, z.B. int a =13 , b =4; int quot = a / b ; int mod = a % b ; // quot = 3 // mod = 1 An jeder Programmstelle, an der eine Variable stehen kann, darf auch ein berechenbarer Ausdruck oder eine Konstante stehen, ausgenommen links von einer Zuweisung (d.h. kein a+b=c). Die Zeilen 3.3 Fundamentale Datentypen 57 c = a + b; std :: cout << a <<" ␣ plus ␣ " << b << " ␣ gleich ␣ " << c ; lassen sich auch kürzer schreiben, falls c nicht weiter benötigt wird: std :: cout << a <<" ␣ plus ␣ " << b << " ␣ gleich ␣ " << a + b ; 3.3.4 Bereichsüberschreitung Was passiert bei Überschreitung des Zahlbereichs bei ganzzahligen Datentypen (overflow)? Dies ist im Standard nicht festgelegt, in der Regel wird modulo weitergerechnet. Sind z.B. a und b vom Typ unsigned short, d.h. 0 ≤ a,b < 65536 = 216 , dann unsigned short a = 60000; unsigned short b = 2* a ; // b =120000 mod 2^16 = 54464 Manchmal wird aber auch eine Fehlermeldung ausgegeben. Aber am besten beim jeweiligen Compiler ausprobieren! 3.3.5 Zuweisungsoperatoren Die Zuweisungsoperatoren =, +=, -=, *=, /=, %= dienen der verkürzten Schreibweise. Beispielsweise wird bei i += 5; der Ausdruck rechts von += ausgewertet und zu i hinzuaddiert. Dabei muss die Variable i aber nur einmal ausgewertet werden. Das Endergebnis ist das gleiche wie bei i = i + 5; Weitere Beispiele: w *= 3 + v ; z += w / v ; z %= w - v ; // w = w * (3 + v ); // z = z + ( w / v ); // z = z % ( w - v ); 58 3.3.6 Kapitel 3: Einführung in C/C++ Inkrement- und Dekrementoperatoren Eine weitere Kurzschreibweise bieten die Operatoren zum Inkrement (erhöhe Wert um Eins) und Dekrement (erniedrige Wert um Eins): z ++; z - -; ++ z ; --z ; // // // // z z z z = = = = z z z z + + - 1; 1; 1; 1; Die Operatoren in der Schreibweise z++, z-- sind Postfix-Operatoren, d.h. die Variable wird erst benutzt, dann in- bzw. dekrementiert. Operatoren in ++z, --z sind dagegen Präfix-Operatoren: die Variable wird erst in- bzw. dekrementiert, dann benutzt. Ihre Verwendung liefert unterschiedliche Ergebnisse, z.B. int x =42 , y =42; std :: cout << " x ++ ␣ " << x ++ << std :: endl ; std :: cout << " ++ y ␣ " << ++ y << std :: endl ; x++ = 42 ++y = 43 Diese Operatoren sollten nur für Zähler eingesetzt werden (z.B. für die In- bzw. Dekrementierung be der for-Schleife), nicht für Addition oder Subtraktion innerhalb einer Berechnung. Das erhöht die Lesbarkeit und hilft, Fehler zu vermeiden. 3.3.7 Literale Integerzahlen können auch direkt angegeben werden (4711). Der Compiler stellt dafür einen Speicherplatz zur Verfügung, der zur Größe des ganzzahligen Literals (bzw. ganzzahliger Konstante) passt. Durch die Endung l oder L können Zahlen als long Zahlen definiert werden. Durch u oder U als unsigned int oder unsigned long int, je nach Größe des Literals. 4711L wird also bereits als long angelegt. Hexadezimalzahlen muss man nicht erst in Dezimalzalhen umrechnen, um sie im Programm zu verwenden. Steht 0x vor einer Zahl, wird sie automatisch im 16ersystem interpretiert, z.B. 0x2a (4210 ). Entsprechend werden mit 0 Oktalzahlen eingeleitet, z.B. 060 (4810 ). 3.3.8 Bezeichner In C++ müssen Variablennamen (bzw. allgemein alle Bezeichner) folgenden Regeln genügen: 3.3 Fundamentale Datentypen 59 • Sie müssen aus Buchstaben (a-z, A-Z), Ziffern (0-9) und Unterstrichen (_) bestehen. • Sie dürfen nicht mit einer Ziffer beginnen. • Sie dürfen sich nicht mit Schlüsselwörtern decken. • Groß- und Kleinschreibung wird unterschieden. • Verboten sind Bezeichner, die mit einem Unterstrich anfangen und dann von einem Großbuchstaben gefolgt werden. Achtung: Viele Bezeichner mit Unterstrichen am Anfang werden von Compilern bereits genutzt. Z.B. bezeichnet __LINE__ die aktuelle Programmzeile im Quelltext. Verboten sind auch Bezeichner, die mit einem Unterstrich anfangen und dann von einem Großbuchstaben gefolgt werden, da solche Bezeichner für andere Dinge genutzt werden. Zur besseren Lesbarkeit werden Bezeichner aus mehreren Wörtern in der Regel mit Unterstrich getrennt oder die Anfangsbuchstaben der Wörter groß geschrieben: int die_erste_Zahl ; int dieErsteZahl ; 3.3.9 Fliesskommatypen Die interne Darstellung reeller Zahlen erfolgt auf dem Computer in der Form ±0.z1 z2 . . . zt · B e mit der Basis B = 2, Ziffern zi ∈ {0, . . . , B − 1} mit z1 6= 0 und einem beschränkten Exponenten e ∈ Z. Wieviele Bits zur Verfügung gestellt werden, um das Vorzeichen, die Ziffern und den Exponenten abzuspeichern, ist implementationsabhängig. Der Standard schreibt vor, dass die Bereiche [−1038 , −10−38 ] ∪ {0} ∪ [10−38 , 1038 ] abgedeckt sind. Man beachte aber, dass immer nur endliche Teilmengen von R dargestellt werden können, d.h. die Mengen der Fließkommazahlen auf dem Computer sind nach oben und unten beschränkt und außerdem von endlicher Genauigkeit. Reelle Zahlen können deshalb (praktisch) nie exakt als Fließkommazahlen auf dem Computer dargestellt werden, sondern es sind Rundungen erforderlich. Bei Berechnungen mit Fließkommazahlen hat man mit den entstehenden Rundungsfehlern zu kämpfen. genauigkeit.cpp: Rundungsfehler 1 # include < iostream > 60 2 3 4 5 6 7 8 9 10 11 12 Kapitel 3: Einführung in C/C++ int main (){ float a = 1000012000 , c =2.0 e -06; double b = 1000012000 , d =2.0 e -06; std :: cout . precision (10); std :: cout << a << " ␣ " << b << " ␣ " std :: cout << c << " ␣ " << d << std :: endl ; } 1000012032 1000012000 1.999999995e-06 2e-07 In C++ gibt es drei Datentypen für Fließkommavariablen, wobei die folgenden Werte abhängig vom Compiler sein können (hier wurde der g++ benutzt): Typ float double long double Bytes 4 8 12 Zahlenbereich ±3.40282 · 10±38 ±1.79769 · 10±308 ±1.18973 · 10±4932 Anzahl der Stellen 6 15 18 Die Anzahl der Stellen bedeutet hier, dass z.B. die ersten 6 Stellen der Zahl signifikant bzw. exakt sind, nicht aber, dass die ersten 6 Nachkommastellen exakt sind. Fließkommaliterale werden durch einen Dezimalpunkt gekennzeichnet, z.B. 3.1415, .120, -12.. Für Zahlen anderer Größenordnungen kann nach einem e die Zehnerpotenz angegeben werden (wissenschaftliche Notation): double alter_universum = 13.77 e9 ; double radius_atomkern = 1.0 e -14; // 13.77*10^9 // 10^ -14 Standardmäßig wird der Speicherplatz für ein double reserviert. Durch das Suffix f oder F wird ein float angelegt, durch l bzw. L ein long double. Die Grundrechenarten erfolgen in natürlicher Art und Weise durch die Operatoren +,-,*,/. Die Zuweisungsoperatoren +=,-=,*=,/= können auch benutzt werden. 3.3.10 Typumwandlungen Bei den Grundrechenarten können auch Variablen unterschiedlicher Datentypen verknüpft werden, das Ergebnis ist dann vom „höherwertigen“ Typ. Auch bei Zuweisungen können Typen vermischt werden. Dies ist möglich, weil implizit eine automatische Typumwandlung vorgenommen wird (wobei der Compiler evtl. 3.3 Fundamentale Datentypen 61 eine Warnung liefert, die aber ignoriert werden kann (im Gegensatz zur richtigen Fehlermeldung), wenn diese Typumwandlung gewollt ist). int pi = 3.1415; double a = 1/100.; // pi = 3 // a = 0.01 Die implizite Typumwandlung (engl.: implicit cast)birgt aber auch Gefahren: double a , b ; b = 1/2* a ; Unabhängig vom Wert von a gilt wegen der Ganzzahldivision 1/2=0 immer b=0. double a , b ; b = 1./2* a ; // jetzt ist b = a /2. Nun ist der Typ vom Divisionsausdruck auch double. Zur expliziten Typumwandlung kann (und sollte) man die cast-Operatoren benutzen. Der neue Ausdruck erhält den angegebenen Datentyp, aber die ursprüngliche Variable behält ihren Typ. int a =3 , b =2; double x = ( double ) a / ( double ) b ; // 3.0/2.0=1.5 double y = ( double )( a / b ); // 3/2=1 , d . h . 1.0 Im obigen Beispiel: double a , b ; b = ( double )1/2* a ; // b = a /2. 3.3.11 Konstanten Variablen kann man jederzeit im Programm neue Werte zuweisen. Ist bekannt, dass sich der Wert einer Variable im Verlauf des Programms nicht ändern wird, deklariert man sie als Konstante. Dann darf dieser Variablen nur in der Definition ein Wert zugewiesen werden. Wird dieses im Laufe des Programms wiederum versucht, so bricht der Compiler den Übersetzungsprozeß mit einer Fehlermeldung ab. const int MAX_ZEILEN = 256; const double EulerZahl = 2.718281828459; const double PI = 3 . 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3 2 3 8 4 6 ; π ist übrigens bereits als M_PI in <cmath> definiert. 62 Kapitel 3: Einführung in C/C++ 3.4 Ein- und Ausgabe C++ selbst verfügt über keine direkte Möglichkeit der Ein- und Ausgabe. Stattdessen bietet die C++-Standardbibliothek einfachen Zugriff auf Kommunikation über die Konsole oder über Dateien. Dabei wird die Ein- und Ausgabe als Datenstrom (Stream) interpretiert: Die Zeichenfolgen fließen also auf die Konsole oder aus einer Datei. Die Standardbibliothek legt alle ihre Elemente im Namensraum std ab. Um darauf zuzugreifen, wird der Sichtbarkeitsoperator :: verwendet: std::cout heißt, dass die Standardausgabe cout im Namensraum std, also in der Standardbibliothek liegt. Alternativ kann mit using namespace Namensraum ; auch angegeben werden, dass Ausdrücke automatisch in einem bestimmten Namensraum gesucht werden. 3.4.1 Standardausgabe Mit std::cout werden Daten auf der Standardausgabe ausgegeben. Normalerweise ist das die Konsole bzw. der Bildschirm. Durch die Linux-Operatoren > lässt sich die Ausgabe auch direkt in eine Datei umlenken. $ a.out > hello.txt $ cat hello.txt Hello, world! $ Im Programm hello.cpp deutet der Ausgabeoperator << dabei die Richtung an, in der die Informationen fließen: von rechts nach links in das std::cout (wobei cout console output bedeutet). Auf diese Weise können alle fundamentalen Typen ausgegeben werden. Auch mehrere Ausgaben lassen sich aneinanderreihen. cout << " Das ␣ Ergebnis ␣ von ␣ " << k << " ␣ plus ␣ " << d << " ␣ lautet ␣ " << k + d ; Zusätzlich stehen diverse Formatierungsanweisungen zur Verfügung: Eine neue Zeile wird mit std::endl begonnen. Dabei wird gleichzeitig der Ausgabepuffer geleert, und die Daten werden auch wirklich abgeschickt. Nur den Puffer leert man mit std::flush. In 3.10 stehen weitere Formatierungsanweisungen. 3.4 Ein- und Ausgabe 3.4.2 63 Standardeingabe Die Standardeingabe std::cin wird wie std::cout im Modul <iostream> erklärt. Sie nimmt Eingaben von der Tastatur entgegen und speichert sie in den entsprechenden Variablen ab. eingabe.cpp: Eingabe mit std::cin 1 2 3 4 5 6 7 8 9 10 11 12 # include < iostream > using namespace std ; // Erspart uns das std :: int main () { int year ; double size ; char letter ; const double ft = 3.2808; cout << " Bitte ␣ gib ␣ dein ␣ Geburtsjahr ␣ ein : ␣ " ; cin >> year ; 13 14 15 16 17 18 19 20 21 22 cout << " Bitte ␣ gib ␣ deine ␣ Körpergröße ␣ in ␣ Metern " << " ␣ und ␣ den ␣ A nfangsbu chstaben ␣ deines " << " ␣ Namens ␣ ein : ␣ " ; cin >> size >> letter ; cout << " Hallo , ␣ " << letter << " ! " << endl << " Du ␣ bist ␣ " << 2007 - year << " ␣ Jahre ␣ alt ␣ und ␣ " << size * ft << " ft ␣ groß . " << endl ; } Der Operator >> deutet wieder die Richtung des Datenstroms an, nämlich von cin in die Variablen. Bitte gib dein Geburtsjahr ein: 1777 Bitte gib deine Körpergrö"se [...] ein: 1.87 M Hallo, J! Du bist 229 Jahre alt und 6.1679ft gro"s. Falsche Eingaben (Text, wo eine Zahl erwartet wird) führen zu unterwarteten Ergebnissen! Der Programmierer muss Eingaben nach Korrektheit prüfen (siehe if-Anweisungen). Unter Linux kann man den Eingabestrom auch mit < von einer Datei holen: $ cat person.txt 1956 1.60 A $ eingabe < person.txt Bitte gib dein Geburtsjahr ein: Bitte gib [...] ein: Hallo, A! Du bist 50 Jahre alt und 5.24928ft gro"s. 64 Kapitel 3: Einführung in C/C++ Genauso lässt sich jede andere Ausgabe mit | in das Programm weiterleiten: $ cat person.txt | eingabe Bitte gib dein Geburtsjahr ein: Bitte gib [...] ein: Hallo, A! Du bist 50 Jahre alt und 5.24928ft gro"s. 3.4.3 Standardfehlerausgabe Ein weiterer Stream steht für die Ausgabe von Fehlern zur Verfügung. Wenn die normale Ausgabe in eine Datei umgelenkt wird, erscheinen die Fehler immer noch auf dem Bildschirm (oder umgekehrt; hier nicht demonstriert). fehler.cpp: Fehlerausgabe mit std::cerr 1 2 3 # include < iostream > using namespace std ; 4 5 6 7 int main () { cout << " Hello , ␣ world ! " << endl ; cerr << " No ␣ response ! " << endl ; } $ fehler Hello, world! No response! $ fehler > out.txt No response! %$ fehler 2> out.txt %Hello, world! # Hello, world! steht in out.txt # No response! steht in out.txt 3.5 Anweisungen zur Auswahl 3.5.1 Boolesche Werte Mit Booleschen Werten steht in C++ ein weiterer Datentyp für logische Aussagen zur Verfügung. Sie haben den Typ bool und können nur zwei Werte annehmen: true und false. 3.5 Anweisungen zur Auswahl 65 Dabei ist false als 0 definiert. Alle anderen Werte (also insbesondere 1) gelten als true. Die explizite Benutzung von true und false ist jedoch besser zu verstehen. bool a = false ; // direkt angeben bool b = 42; // 42 wird umgewandelt in true , // da nicht null . 3.5.2 Logische Operatoren Boolesche Werte (und nach einem cast auch alle anderen Typen) können durch die logischen Verknüpfungen Konjunktion (AND) und Disjunktion (OR) miteinander kombiniert werden. Die Negation kehrt den Wahrheitswert um. Die Operatoren stehen jeweils auch als ausgeschriebnes Schlüsselwort zur Verfügung. Operator a && b Schlüsselwort a and b a || b a or b !a not a Ergebnis Wert ist genau dann true, wenn beide Operanden true sind. Wert ist genau dann true, wenn mindestens ein Operand true ist. Wert ist genau dann true, wenn der Operand false ist. Die Wertetabelle listet die Ergebnisse der möglichen Kombinationen auf: a false false true true 3.5.3 b false true false true a && b false false false true a || b false true true true !a true false Relationale Operatoren Relationale Operatoren vergleichen zwei Operanden. Das Ergebnis ist 1, falls der 66 Kapitel 3: Einführung in C/C++ Vergleich wahr ist, sonst 0. Operator a < b a <= b a > b a >= b a == b a != b wahr, wahr, wahr, wahr, wahr, wahr, wenn wenn wenn wenn wenn wenn Ergebnis a kleiner b a kleiner oder gleich b a grösser b a grösser oder gleich b a gleich b a nicht gleich b Beispiel: Ist eine Zahl in einem Intervall enthalten? x ∈ [20, 40] ⇐⇒ (20 <= x) && (x <= 40) x 6∈ [20, 40] ⇐⇒ (x < 20) || (40 < x) logik.cpp: Intervalltest 1 2 3 4 5 6 7 8 9 int main () { int x = 25; bool a = (20 <= x && x <=40); bool b = (x <20 || 40 < x ); cout << x << " ␣ ist ␣ in ␣ [20 ,40]? ␣ " << a << endl ; cout << x << " ␣ ist ␣ in ␣ ].. ,20[ ␣ U ␣ ]40 ,..[? ␣ " << b << endl ; } 25 ist in [20,40]? 1 25 ist in ]..,20[ U ]40,..[? 0 Logische Operatoren werden von links nach rechts abgearbeitet. Die Auswertung endet, wenn Wert des Ausdrucks feststeht. Z.B. für x = 17: Im letzten Beispiel wird dann nur (x<20) ausgewertet, da der Gesamtausdruck wahr ist und es auch bleibt. Achtung!! Der Vergleichsoperator == kann leicht mit dem Zuweisungsoperator = verwechselt werden. Letzterer ist fast immer „wahr“, da eine Zuweisung den zugewiesenen Wert zurückgibt (d.h. eine Zuweisung ist dann „falsch“, wenn Null zugewiesen wurde). Falls man also nur ein statt zweier Gleichheitszeichen verwendet, so wird zum einen der Ausdruck nicht richtig ausgewertet, zum anderen können unerwünschte Nebenwirkungen auftreten. 3.5 Anweisungen zur Auswahl 3.5.4 67 if-Anweisungen Die in Kapitel 2 eingeführte Anweisung zur Auswahl kann direkt in C++-Syntax übersetzt werden: if ( Bedingung ) { ... } else { ... } // FALLS Bedingung // DANN ... // SONST ... Zur Formulierung von Bedingungen in den Auswahlanweisungen kann man logische und relationale Operatoren verwenden. maximum.cpp: Maximums-Bestimmung 1 2 3 4 5 6 int main () { int x ,y ,z , m ; cout << " Bitte ␣ 3 ␣ Zahlen ␣ eingeben ! ␣ " ; cin >> x >> y >> z ; if (x > y ) { if (x > z ) { m = x; } else { m = z; } } else { if (y > z ) { m=y; } else { m=z; } } 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 cout << " max ( " << x << " ,␣ " << y << " ,␣ " << z << " ) ␣ = ␣ " << m << endl ; } Bei if-Anweisungen kann der else-Zweig auch weggelassen werden, sofern keine Alternative verlangt wird. Ebenso gilt dieses für die geschweiften Klammern, wenn nur eine einzelne Anweisung folgt. Dies kann aber zu Zweideutigkeiten führen, z.B. 68 Kapitel 3: Einführung in C/C++ if ( a > b ) if ( a > 2 * cout << << else cout << b) " a ␣ ist ␣ mehr ␣ als ␣ doppelt ␣ " " so ␣ gross ␣ wie ␣ b . " << endl ; " a ␣ ist ␣ groesser ␣ als ␣ b . " << endl ; Zu welchem if gehört else? In C++ zum letzten if. Deshalb sollte man immer geschweifte Klammern benutzen, außer in völlig eindeutigen Situationen wie if ( a > b ) cout << " a ␣ ist ␣ groesser ␣ als ␣ b . " << endl ; Weil Wahrheitswerte wie ganze Zahlen behandelt werden, kann man die Abfragen x 6= 0 und x = 0 auch kurz schreiben als if ( x ) { ... } if (! x ) { ... } // x !=0? // x ==0? Besser zu verstehen ist jedoch die explizite Angabe, so wie sie die Kommentare zeigen. Wir können uns nun auch ansehen, wie die Standardeingabe std::cin (d.h. die Eingabe von Daten über die Tastatur) geprüft werden kann. "std::cin >> a" (also die Anweisung Daten in die Variable a einzulesen) nimmt den Wahrheitswert true an, wenn die Anweisung erfolgreich war. Sie nimmt den Wert false an, wenn es ein Problem gab. Mit der Anweisung "std::cin.clear()" kann man in diesem Fall den auftretenden Fehlerzustand zurücksetzen und das Einlesen in anderer Form noch einmal versuchen. eingabe–test.cpp: Test der Eingabe 1 2 3 4 int main () { double d_num ; char letter ; cout << " Bitte ␣ gib ␣ Zahl ␣ ein : ␣ " ; if ( cin >> d_num ) { // Eine Zahl konnte gelesen werden cout << " Das ␣ ist ␣ die ␣ Zahl : ␣ " << d_num << endl ; } else { // Es gab ein Prolem . cin . clear (); // Wir versuchen die Eingabe noch einmal // ( als Zeichen ) zu lesen . cin >> letter ; cout << " Das ␣ war ␣ ein : ␣ " << letter << endl ; } 5 6 7 8 9 10 11 12 13 14 15 16 17 18 } 3.5 Anweisungen zur Auswahl 3.5.5 69 Bedingte Ausdrücke mit ?: Die Schreibweise von if-else kann in einigen Fällen mit dem bedingten Ausdrucksoperator ?: verkürzt werden. Dieser Operator mit drei Operanden wird benutzt, um eine Entscheidung innerhalb eines berechneten Ausdrucks zu fällen. Im Gegensatz zur if-Anweisung ist dies ein Ausdruck, der einen Wert zurück liefert, während if ein Befehl ist. var = ( Bedingung ) ? IF - Ausdruck : ELSE - Ausdruck Häufig benutzt man ihn in einfachen mathematischen Ausdrücken: max = a >= b ? a : b ; betrag = a <0 ? -a : a ; 3.5.6 switch-Anweisungen Mit geschachtelten if-Anweisungen lassen sich grundsätzlich alle Entscheidungsfolgen realisieren. Die Programme werden dadurch aber sehr unübersichtlich. Statt dessen nutzt man häufig folgendes Konstrukt: switch ( Ausdruck ) { case Vergleichswert : ... break ; case Vergleichswert : ... break ; default : ... break ; } Die Anweisung break ist nötig, weil sonst alle Anweisungen nach dem zugeordneten case nicht übersprungen sondern zusätzlich ausgeführt werden (unabhängig davon, ob die Bedingung für diese case-Teile zutrifft; sog. Durchfallen). Man sollte immer eine default-Anweisung einbauen, die ausgeführt wird, wenn keiner der anderen Fälle eingetreten ist. switch.cpp: Switch 1 2 # include < iostream > using namespace std ; 3 4 5 int main () { int z ; 70 6 7 8 9 10 cout << " Bitte ␣ geben ␣ Sie ␣ eine ␣ Zahl ␣ ein : " ; cin >> z ; cout << endl ; switch ( z ) { case 0: cout <<" Die ␣ Zahl ␣ ist ␣ 0. " << endl ; break ; case 1: case 2: case 3: cout << " Die ␣ Zahl ␣ ist ␣ " << z << " . " << endl ; break ; default : cout << " Zahl ␣ ist ␣ negativ ␣ oder ␣ groesser ␣ 3. " << endl ; break ; } 11 12 13 14 15 16 17 18 19 20 21 22 23 Kapitel 3: Einführung in C/C++ } Der Auswahlausdruck (hier z) muss von ganzzahligem Typ sein. Außerdem müssen für die Verzweigungen konstante (und ganzzahlige) Ausdrücke verwendet werden. Die Reihenfolge der case-Anweisungen ist dagegen egal. 3.6 Funktionen Funktionen dienen der strukturierten Programmierung. Wiederholt genutzte Programmteile werden aus dem Hauptprogramm ausgelagert und in sinnvoll benannte Unterprogramme aufgeteilt. Funktionen können Parameter entgegennehmen, diese unter Umständen verändern und auch einen Wert zurückgeben. 3.6.1 Definition Eine Funktion wird folgendermaßen definiert: Rückgabetyp Funktionsname ( Typ Parametername , ...) { ... [ return Wert ;] } Gültige Namen für Funktionen werden wie Variablennamen aus den Zeichen a-z, A-Z, 0-9 und _ gebildet. Auch sie dürfen nicht mit einer Ziffer beginnen. 3.6 Funktionen 71 Vor dem ersten Aufruf einer Funktion, muss diese bereits dem Compiler bekannt sein (Deklaration). Dies geschieht entweder durch ihre vollständige Definition oder vorläufig durch den Prototyp. Es genügt, im Prototyp die Datentypen der Parameter anzugeben: // Prototyp für Funktionsname Rückgabetyp Funktionsname ( Typ [ Paremtername ] , ...); quadrat.cpp: Ein einfacher Funktionsaufruf 1 2 3 4 5 6 7 8 9 10 11 int quadrat ( int a ) { int b ; b = a*a; return b ; } int main () { int z = 11; int zz = quadrat ( z ); cout << z << " ^2 ␣ = ␣ " << zz << endl ; } quadrat1.cpp: Falsch - Die Funktion ist unbekannt 1 2 3 4 5 6 7 8 9 int main () { int z = 11; int zz = quadrat ( z ); // quadrat unbekannt ! cout << z << " ^2 ␣ = ␣ " << zz << endl ; } int quadrat ( int a ) { return a * a ; } quadrat2.cpp: Richtig - Prototyp deklariert die Funktion 1 2 3 4 5 6 7 8 9 10 11 int quadrat ( int a ); // Prototyp für quadrat int main () { int z = 11; int zz = quadrat ( z ); cout << z << " ^2 ␣ = ␣ " << zz << endl ; } int quadrat ( int a ) { return a * a ; } // Kompakter Folgt der Funktionsdeklaration (im Funktionskopf ) die Funktionsdefinition (im Funktionsrumpf ), also ein Anweisungsblock, so wird sie nicht mit Semikolon 72 Kapitel 3: Einführung in C/C++ beendet. Steht die Funktionsdeklaration allein als Prototyp, dann wird sie mit Semikolon beendet. Der Funktionsaufruf selbst wird mit einem Semikolon abgeschlossen. 3.6.2 Rückgabetyp Eine Funktion kann einen, aber nur höchstens einen Wert mit dem returnBefehl zurückgeben. Will man mehrere Werte zurückgeben, muss man die Adresse eines Speicherbereichs zurückgeben, oder die Eingabeparameter modifizieren. Der Typ des Rückgabewerts wird als Typ der Funktion angegeben und steht vor dem Funktionsnamen. Will man keinen Wert zurückgeben, verwendet man den Datentyp void und den return-Befehl ohne Wert. Ein solches return am Ende einer Funktion kann auch entfallen. Dem return-Befehl kann auch ein berechenbarer Ausdruck übergeben werden. In einem sauberen Programmierstil verwendet man nur einen return-Befehl am Ende einer Funktion. Prinzipiell können weitere return-Befehle aber auch zum vorzeitigen Verlassen der Funktion benutzt werden. Auch mehrfache returnKommandos dürfen in einer Funktion benutzt werden. return.cpp: Rückgabetypen 1 2 3 4 5 6 7 8 9 10 int test ( int a ) { if (a >10) return a /10; if (a >0) return a ; } int main () { cout << test (1) << " ␣ " << test (20) << " ␣ " << test ( -2) << endl ; } Das Programm lässt sich kompilieren, führt aber zu unschönen Ergebnissen: $ ./return 1 2 1076098592 Erst die Kompilierung mit -Wall gibt eine Warnung aus: $ g++ -Wall return.cpp -o return return.cpp: In function ‘int test(int)’: return.cpp:7: warning: control reaches end of non-void function Diese Fehlermeldung bedeutet, dass der Programmfluß das Ende der Funktion erreicht, aber „nicht alle Fälle durch return abgedeckt sind“. 3.6 Funktionen 3.6.3 73 Funktionsparameter Man kann einer Funktion einen oder mehrere Parameter als Argument übergeben. Die Parameter werden in der Funktionsdefinition festgelegt und stehen innerhalb der Funktion als Variable zur Verfügung. Ein Funktionsaufruf wird immer durch Klammern gekennzeichnet. In diesen können Variablen (oder berechenbare Ausdrücke) als Argumente übergeben werden. Für einzelne Parameter können in der Funktionsdeklaration auch mit einem = Default-Argumente angegeben werden. Parameter mit Default-Werten müssen als letzte in der Parameterliste stehen und werden von links nach rechts aufgefüllt. default.cpp: Parabel mit Default-Argumenten 1 2 3 4 5 6 7 8 9 10 11 double parabel ( double x , double a =1 , double b =0 , double c =0) { return a * x * x + b * x + c ; } int main () { cout << parabel (1) << endl ; cout << parabel (1 ,2) << endl ; cout << parabel (1 ,2 ,3) << endl ; cout << parabel (1 ,2 ,3 ,4) << endl ; } Falls im Funktionsaufruf an der Stelle, an denen in der Funktionsdeklaration Default-Werte stehen, Argumente sind, dann wird der Funktionsaufruf mit diesen Werten durchgeführt. 3.6.4 Call by value Beim Aufruf der Funktion werden die aktuellen Werte dieser Variablen übergeben, wobei die Parameter erst im Speicher erzeugt und dann mit den Werten der Argumente belegt bzw. diese in die entsprechenden Speicherplätze kopiert werden. C++ übergibt Funktionsargumente grundsätzlich (bis auf 2 Ausnahmen) als Wert. Dies hat zur Folge, dass Änderungen an den Variablen, die in der Funktion vorgenommen werden, bei der Rückkehr zum Hauptprogramm (bzw. zur aufrufenden Funktion) nicht mehr wirksam sind. Die Variablen haben dort ihre alten Werte. callvalue.cpp: call by value 1 2 void vertauschen ( int a , int b ) { int help = a ; 74 3 4 5 6 7 8 9 10 11 12 13 Kapitel 3: Einführung in C/C++ a = b; b = help ; cout << " a ␣ = ␣ " << a << " ␣ b ␣ = ␣ " << b << endl ; } int main () { int a =47 , b =11; vertauschen (a , b ); cout << " a ␣ = ␣ " << a << " ␣ b ␣ = ␣ " << b << endl ; } a = 11 b = 47 a = 47 b = 11 3.6.5 Call by reference Eine Referenz kann man sich als alternativen Namen (ein Alias) für eine Variable, Konstante oder Funktion vorstellen. Die Referenz hat die gleiche Speicheradresse wie das Original (und daher auch immer den gleichen Wert). Referenzen werden mit dem Kaufmanns-Und (&) gekennzeichnet und folgendermaßen deklariert: Typbezeichnung & Referenzname = Originalname ; Die “Initialisierung“ mit dem Originalnamen ist immer erforderlich. Hier einige Beispiele: int i =10; int & m = i ; i =42; m =2006; int & j ; int & k =12; int & n = i + i // Richtige Deklaration der Referenz , // Addresse von m und i ist gleich , // m ist jetzt 10. // i ist 42 , m aber auch ! // m ist 2006 , i aber auch ! // Falsch : Referenz muss initialisiert // werden . // Falsch : Referenz darf nicht mit // konstantem Wert initialisiert werden . // Falsche Initialisierung mit // temporärem Wert i + i Referenzen auf Variablen sind eine Möglichkeit um Parameter nicht nur mit ihrem Wert zu übergeben. Sind Funktionsparameter als Referenzen deklariert, so wird beim Aufruf dieser Funktion direkt dieser Speicherplatz übergeben und 3.6 Funktionen 75 es werden keine Kopien der Variablen angelegt. Werden die Parameter in der Unterfunktion geändert, so ändern sie sich auch in der aufrufenden Umgebung. callref.cpp: call by reference 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void vertauschen ( int &a , int & b ) { int help = a ; a = b; b = help ; cout << " a ␣ = ␣ " << a << " ␣ b ␣ = ␣ " << b << endl ; } int main () { int a =47 , b =11; vertauschen (a , b ); cout << " a ␣ = ␣ " << a << " ␣ b ␣ = ␣ " << b << endl ; } a = 11 b = 47 a = 11 b = 47 3.6.6 Gültigkeitsbereich von Variablen Eine Variable oder Funktion ist nicht nur durch ihren Namen bestimmt, sondern auch dadurch, wo sie definiert wurde. Man unterscheidet allgemein zwischen lokalen und globalen Variablen. Lokale Variablen werden in Funktionen und Blöcken definiert und sind auch nur dort sichtbar. Beim Verlassen der Funktion bzw. des Blocks wird ihr Wert gelöscht. lokal.cpp: Lokale Variablen 1 2 3 4 5 6 7 8 9 void aendern () { int x =10; cout << " x ␣ = ␣ " << x << endl ; } int main () { int x =4; cout << " x ␣ = ␣ " << x << endl ; aendern (); 76 10 11 Kapitel 3: Einführung in C/C++ cout << " x ␣ = ␣ " << x << endl ; } x = 4 x = 10 x = 4 Die Änderung des Wertes von x in der Funktion aendern() ist nur dort wirksam. Wird die Funktion verlassen, dann wird ihr Wert gelöscht. Genauer: Die Variable x in aendern() ist eine andere als die Variable x im Hauptprogramm. Trotzdem kann der gleiche Name benutzt werden. In diesem Fall wird über ihn immer die lokalste der Variablen angesprochen, der Inhalt anderer Variablen gleichen Namens ist dann in der Funktion nicht zugänglich. Es ist also wichtig, den Gültigkeitsbereich bzw. die Lebensdauer jeder Variablen zu kennen. Variablen können aber nicht nur bzgl. einer Funktion lokal sein, sondern in jedem Anweisungsblock kann eine neue Variable mit schon verwendetem Namen definiert werden. Der Bezugsrahmen ist immer der Anweisungsblock, in dem eine Variable definiert wurde. Globale Variablen werden außerhalb aller Funktionen definiert. Sie können in jeder Funktion benutzt und verändert werden. global.cpp: Globale Variablen 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int a , b ; void aendern ( void ) { int a ; a = b =0; if ( a ==0) { int a = 20; b = 20; cout < < " a ␣ = ␣ " <<a < < " ␣ und ␣ " <<" b ␣ = ␣ " <<b < < endl ; } cout < < " a ␣ = ␣ " <<a < < " ␣ und ␣ " <<" b ␣ = ␣ " <<b < < endl ; } int main () { a = b =10; cout < < " a ␣ = ␣ " <<a < < " ␣ und ␣ " <<" b ␣ = ␣ " <<b < < endl ; aendern (); cout < < " a ␣ = ␣ " <<a < < " ␣ und ␣ " <<" b ␣ = ␣ " <<b < < endl ; } a a a a = = = = 10 und b = 10 20 und b = 20 0 und b = 20 10 und b = 20 3.6 Funktionen 77 Je enger eine Information begrenzt ist, d.h. je kleiner der Gültigkeitsbereich einer Variablen ist, desto weniger Fehler kann man z.B. durch unbeabsichtigte Änderungen an der Variablen machen. Eine Variable, die nur innerhalb einer Funktion benutzt wird, sollte immer lokale Variable der Funktion sein (z.B. Zählervariablen für for-Schleifen). Kurz gesagt: So lokal wie möglich, so global wie nötig. Wenn der Inhalt von in Funktionen lokal definierten Varibalen doch behalten werden soll, so kann man statische Variablen benutzen. statisch.cpp: Statische Variablen 1 2 3 4 5 6 7 8 9 10 11 void count () { static int a = 1; cout << " a ␣ = ␣ " << a << endl ; a ++; } int main () { count (); count (); count (); } a = 1 a = 2 a = 3 Wenn a nicht statisch wäre, würde der angegebene Wert immer 1 sein. Wird a als statische Variable definiert, dann behält sie ihren aktuellen Wert beim Verlassen der Funktion count(). Trotzdem ist a hier eine lokale Variable, z.B. könnte man im Hauptprogramm nicht auf sie zugreifen. Die Lebensdauer einer statischen Variable erstreckt sich (unabhängig, ob sie global oder lokal ist), vom Zeitpunkt ihrer Definition, d.h. wenn der Programmfluß ihre Definiton erreicht hat, bis zum Programmende. Beachte: Statische Variablen (dazu gehören auch globale Variablen) können bei ihrer Definition initialisiert werden. Falls dies nicht geschieht, wird dieses automatisch zur Laufzeit gemacht (mit dem Nullwert des entsprechenden Datentyps). Dies geschieht im Unterschied zu automatischen bzw. lokalen Variablen. 3.6.7 Rekursive Funktionen Funktionen dürfen sich auch selbst wieder aufrufen. Ebenso können sich auch mehrere Funktionen wechselseitig aufrufen. Die rekursive Lösung ist in der Regel kürzer und eleganter formuliert ist, aber ein iterativer Algorithmus löst die Aufgabe oft deutlich schneller (sofern es denn einen äquivalenten iterativen Algorithmus gibt). 78 Kapitel 3: Einführung in C/C++ Die Fakultät einer Zahl n ist definiert als n! = 1 · 2 · 3 · 4 · · · n rekursiv.cpp: Fakultät als rekursive Funktion 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int fakultaet ( int n ) { if ( n ==1) { return 1; } else { return n * fakultaet (n -1); } } int main () { cout << fakultaet (4) << endl ; cout << fakultaet (6) << endl ; cout << fakultaet (12) << endl ; } 24 720 479001600 rekursiv2.cpp: Fakultät als kurze rekursive Funktion 1 2 3 int fakultaet ( int n ) { return ( n ==1)? 1 : n * fakultaet (n -1); } Endloses rekursives Aufrufen muss vermieden werden. Dies kann man dadurch erreichen, dass die fortschreitenden rekursiven Aufrufe „einfacher“ werden (oben n -> n-1 -> n-2 -> ...) und ein begrenzender Fall existiert (hier n=1), bei dem kein rekursiver Aufruf zur Berechnung des Funktionswertes mehr nötig ist. Aber was passiert bei dem Aufruf von fakultaet(-1)? Ausprobieren ! Wie kann man das entstehende Problem lösen ? 3.6.8 Mathematische Funktionen Von C++ werden in dem Modul <cmath> die Prototypen für eine Reihe mathematischer Standardfunktionen zur Verfügung gestellt. Eine Übersicht über die Funktionen ist im Anhang A.3 zu finden. wurzel.cpp: Die Mathe-Bibliothek 1 2 3 # include < iostream > # include < cmath > using namespace std ; 3.7 Schleifen 4 5 6 7 8 9 10 11 79 int main () { double x ; cout << " Zahl ␣ eingeben : ␣ " ; cin >> x ; cout << " Wurzel ␣ von ␣ " << x << " ␣ = ␣ " << sqrt ( x ) << endl ; } Die Winkelfunktionen erwarten Argumente im Bogenmaß. Für die Umrechnung zwischen Grad- und Bogenmaß kann man folgende Funktionen benutzen. degrad.cpp: Bogen- und Gradmaß 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # include < iostream > # include < cmath > using namespace std ; double radtodeg ( double a ) { return a * 180. / M_PI ; } double degtorad ( double a ) { return a / 180. * M_PI ; } int main () { double x ; cout << " Zahl ␣ im ␣ Bogenmass ␣ eingeben : ␣ " ; cin >> x ; cout << x << " ␣ im ␣ Bogenmass ␣ = ␣ " << radtodeg ( x ) << " ␣ im ␣ Gradmass " << endl ; cout << " Zahl ␣ im ␣ Gradmass ␣ eingeben : ␣ " ; cin >> x ; cout << x << " ␣ im ␣ Gradmass ␣ = ␣ " << degtorad ( x ) << " ␣ im ␣ Bogenmass ␣ " << endl ; } 3.7 Schleifen 80 3.7.1 Kapitel 3: Einführung in C/C++ while-Schleifen Auch die in Kapitel 2 eingeführten Algorithmen-Bausteine zur Wiederholung von Anweisungen werden direkt in die C++-Syntax übersetzt: while ( Bedingung ) { ... } // SOLANGE Bedingung ERFÜLLT IST // FÜHRE AUS // ... do { // WIEDERHOLE // ... ... } while ( Bedingung ); // SOLANGE Bedingung ERFÜLLT IST . Man beachte, dass hier die -while-Anweisung mit Semikolon abgeschlossen wird. while.cpp: Eingabe mit Überprüfung als while-Schleife 1 2 3 4 5 6 7 8 9 10 11 12 int main () { int year , a =2007; cout << " Bitte ␣ gib ␣ dein ␣ Geburtsjahr ␣ ein : ␣ " ; cin >> year ; while ( year > a || year <1900) { cin >> year ; } cout << " Du ␣ bist ␣ " << a - year << " ␣ Jahre ␣ alt . " << endl ; } dowhile.cpp: Eingabe mit Überprüfung als do-while-Schleife 1 2 3 4 int main () { int year , a =2007; cout << " Bitte ␣ gib ␣ dein ␣ Geburtsjahr ␣ ein : ␣ " ; 5 6 7 8 9 10 11 12 do { cin >> year ; } while ( year > a || year <1900); cout << " Du ␣ bist ␣ " << a - year << " ␣ Jahre ␣ alt . " << endl ; } 3.7 Schleifen 81 In diesem Beispiel ist die do-Schleife angebrachter als die while-Schleife, weil die Eingabeaufforderung in jedem Fall mindestens einmal erfolgen soll. Prinzipiell sollte man aber while-Schleifen, wenn immer möglich, vorziehen. Denn durch Vorabprüfung der Schleifenbedingung kann sichergestellt werden, dass der Schleifenrumpf sinnvoll bearbeitet werden kann. while2.cpp: while ohne Anweisungsblock 1 2 3 4 5 6 7 8 9 10 11 bool geburtsjahr () { int year ; cout << " Bitte ␣ gib ␣ dein ␣ Geburtsjahr ␣ ein : ␣ " ; cin >> year ; return ( year >2006 || year <1900); } int main () { while ( geburtsjahr ()) ; } Die Abfrage wird erst beendet, wenn ein gültiges Geburtsjahr eingegeben wurde. Mit einer while-Schleife kann man z.B. auch eine iterative Version der Fakultätsberechunung angeben. Man beachte, dass hierbei die Variable i als Zähler verwendet wird, der die Werte 1 bis n druchläuft. iterativ2.cpp: Iterative Version von Fakultät mit while 1 2 3 4 5 6 7 8 9 3.7.2 int fakultaet ( int n ) { int produkt = 1; int i =1; while (i <= n ) { produkt *= i ; i ++; } return produkt ; } for-Schleifen Für Schleifen, die wie in dem letzen Beispiel im wesentlichen durch einen “Zähler“ kontrolliert werden, sollte man eine for-Schleife benutzen. Diese hebt den “Zähler“ deutlich hervor. Allgemein haben for-Schleifen folgende Syntax: for ( Initialisierung ; Bedingung ; Reinitia lisierun g ) { ... 82 Kapitel 3: Einführung in C/C++ } Eine solche Schleife wird folgendermaßen abgearbeitet: @ @ - Initialisierung @ falsch Bedingung @ @ @ @ @ wahr 6 ? Reinitialisierung Anweisungsblock In allen 3 Bereichen des Schleifenkopfs muss ein Ausdruck stehen, wobei der Ausdruck bei der Bedingung einen Bool-Wert zurückliefern muss. Zum Beispiel bei der Berechnung von n!: iterativ.cpp: Iterative Version von Fakultät mit for 1 2 3 4 5 6 7 int fakultaet ( int n ) { int produkt = 1; for ( int i =1; i <= n ; i ++) { produkt *= i ; } return produkt ; } Anmerkung: In dem Beispiel findet sich in der Initialisierung eine Deklaration. In diesem Fall wird i als lokale Integer Variable erklärt (quasi just-in-time). Eine Deklaration vor der for-Anweisung ist nicht erforderlich. Die Variable ist aber nur für die for-Anweisung gültig. Sie kann nach dem Anweisung nicht mehr verwendet werden. In den weiteren Beispielen werden wir sehen, dass eine Initialisierung die Deklaration nicht enthalten muss. Die verwendete Variable muss dann aber schon vorher deklariert sein. Alle drei Ausdrücke bzw. Bedingungen im Schleifenkopf der for-Anweisung können auch weggelassen werden. Meist sind aber mindestens die Bedingung (sonst entsteht eine Endlosschleife) und Reinitialisierung vorhanden. Der nachfolgende Programmausschnitt ist z.B. nur sinnvoll, wenn j im vorangegangenen Teil des Programms auf einen positiven Wert gesetzt wurde: for (; j >0; j - -) { cout << " j ␣ = ␣ " << j << endl ; } Die for-Anweisung kann auch berechenbare Ausdrücke oder zusätzliche Anweisungen enthalten, die durch Komma getrennt werden. 3.7 Schleifen 83 for ( int x =1 , y =100; x <= y ; x ++ , y - -) { cout << " x ␣ = ␣ " << x << " ␣ y ␣ = ␣ " << y << endl ; } 3.7.3 Sprungbefehle break und continue Mit dem break-Kommando (vgl. switch-Anweisung) kann die Abarbeitung der kompletten Schleife vorzeitig abgebrochen werden: Die Programmausführung wird hinter dem aktuellen Anweisungsblock fortgesetzt. Im Unterschied dazu beendet der Befehl continue nur die Ausführung des aktuellen Schleifendurchlaufs: Bei einer do-Schleife wird zum Ende der Schleife gesprungen, die Bedingung erneut überprüft und gegebenefalls ein neuer Durchlauf gestartet. Bei einer while-Schleife wird erneut die Bedingung überprüft und gegebenenfalls ein neuer Durchlauf gestartet. Bei einer for-Schleife wird zum Schleifenkopf zurück gesprungen und zunächst die Reinitialisierung ausgeführt, dann wird erneut die Bedingung überprüft und gegebenenfalls ein neuer Durchlauf gestartet. Beide Befehle beziehen sich jeweils auf die innerste Schleife. Befinden sie sich im Inneren geschachtelter Schleifen, dann wird nur die innerste verlassen, während die umgebenden Schleifen weiter abgearbeitet werden. continue.cpp: Die Schleife läuft mit continue weiter 1 2 3 4 5 6 7 8 9 10 int main () { int i ; for ( i =1; i <10; i ++) { if (i >5) { continue ; } cout << " Aktueller ␣ Wert : ␣ " << i << endl ; } cout << " Schleifenende : ␣ i ␣ = ␣ " << i << endl ; } Aktueller Wert: 1 Aktueller Wert: 2 Aktueller Wert: 3 Aktueller Wert: 4 Aktueller Wert: 5 Schleifenende: i = 10 84 Kapitel 3: Einführung in C/C++ break.cpp: Die Schleife endet mit break sofort 1 2 3 4 5 6 7 8 9 10 int main () { int i ; for ( i =1; i <10; i ++) { if (i >5) { break ; } cout << " Aktueller ␣ Wert : ␣ " << i << endl ; } cout << " Schleifenende : ␣ i ␣ = ␣ " << i << endl ; } Aktueller Wert: 1 Aktueller Wert: 2 Aktueller Wert: 3 Aktueller Wert: 4 Aktueller Wert: 5 Schleifenende: i = 6 3.7.4 Zusammenfassung Schleifen Das folgende Beispiel verdeutlicht nicht nur nochmals die Benutzung der in diesem Abschnitt vorgestellten Befehle, sondern insbesondere die Wirkung von break bei Benutzung innerhalb geschachtelter Schleifen. Das Programm fordert zur Eingabe einer Zahl auf und bestimmt die größte Primzahl, die kleiner oder gleich dieser Eingabe ist. Dabei ist int isprim(int) eine Funktion, die Eins zurückgibt, wenn der eingegebene Wert eine Primzahl ist, und Null sonst. prim.cpp: Primzahltest 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 bool isprim ( int a ); int main () { int p ; do { int z ; cout << " Bitte ␣ ganze ␣ Zahl ␣ in ␣ [2 , ␣ 1000] ␣ " << " eingeben : ␣ " ; cin >> z ; if (( z < 2) || ( z > 1000)) continue ; for ( p = z ; p >1; p - -) { if ( isprim ( p )) { cout << " Die ␣ naechste ␣ Primzahl ␣ zu ␣ " 3.7 Schleifen << z << " ␣ ist ␣ " << p << endl ; break ; 16 17 18 19 20 21 22 23 24 25 26 85 } } cout << " Nochmal ? ␣ ( ja ␣ = ␣ 1 , ␣ nein ␣ = ␣ 0) ␣ " ; cin >> p ; } while ( p ); return 0; } $ ./prim Bitte ganze Zahl in [2, 1000] eingeben: 100 Die naechste Primzahl zu 100 ist 97. Nochmal? (ja = 1, nein = 0) 0 Die drei Schleifenkonstrukte sind äquivalent, d.h. man kann jede for-Schleife durch eine while-Schleife (oder genauso durch eine do-Schleife) ersetzen und umgekehrt. Und jede while-Schleife kann man durch eine do-Schleife ersetzen und umgekehrt. for ( Init ; Bedingung ; Reinit ) { Anweisungen } while ( Bedingung ) { Anweisungen } ⇐⇒ Init ; while ( Bedingung ) { Anweisungen Reinit } ⇐⇒ for ( ; Bedingung ; ) { Anweisungen } Sofern die Schleifen eine “continue“ Anweisung enthalten, muss dieses dieses ggf. durch eine geeignete “goto“ Anweisung ersetzt werden (siehe folgend). Man §berlege, warum dieses notwendig ist! 3.7.5 Der Sprungbefehl goto Der goto-Befehl erlaubt direkt zu einer mit einer Sprungmarke (Label) versehenen Anweisung zu springen. Weil das aber zu unstrukturiertem Code führen kann, ist dieser Sprungbefehl verpönt. Label sind Bezeichner, die einer Anweisung vorangestellt sind und von dieser durch einen Doppelpunkt abgesetzt sind. Siehe folgendes Beispiel: goto.cpp: goto höchstens zur Fehlerbehandlung 86 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Kapitel 3: Einführung in C/C++ # include < iostream > # include < cmath > using namespace std ; int main () { double x ; cout << " Positive ␣ Zahl ␣ eingeben ? ␣ " ; cin >> x ; if (x <=0) goto ERROR_NUMBER ; cout << " Die ␣ Wurzel ␣ von ␣ " << x << " ␣ ist ␣ " << sqrt ( x ) << endl ; return 0; ERROR_NUMBER : cout << " Keine ␣ gültige ␣ Zahl : ␣ " << x << endl ; return 1; } $ ./goto Positive Zahl eingeben? 14 Die Wurzel von 14 ist 3.74166 $ ./goto Positive Zahl eingeben? -5 Keine gültige Zahl: -5 goto sollte man höchstens in der Form des obigen Beispiels verwenden oder bei Sprüngen aus einer tiefgeschachtelten Schleife. Die Anweisung break beendet stets nur die Schleife, in der sie sich unmittelbar befindet. Mit einem geeigneten goto kann man mit einem “Satz“ an das Ende der äußeren Schleife springen. Dort muss sich ein entsprechendes Label befinden. 3.8 Felder Felder oder Arrays sind die geeignete Methode, um Variablen oder Daten des gleichen Typs zusammenzufassen und somit eine zusammengehörige Speicherung und effiziente Zugriffe (über Schleifen o.ä.) zu ermöglichen. Vektoren oder Matrizen sind die bekanntesten Beispiele für Felder. Man bestimmt die Anzahl der zu speichernden Objekte und ihren Typ und kann dann über einen Index auf jedes einzelne Objekt zugreifen. Datentyp Name [ Dimension ]; Datentyp Name [ Dimension ] = { Initialierung }; 3.8 Felder 87 Zum Beispiel int x [4]; int y [4] = {4 , 7 , 1 , 1}; Felder dürfen sich auch über mehrere Dimensionen erstrecken: Datentyp Name [ Dim1 ][ Dim2 ]; int matrix [3][3]; int z [2][3] = {{1 ,2 ,3} , {4 ,5 ,6}}; Auf die Einträge wird über den Index (bzw. die Indizes) zugegriffen, z.B. x[2] oder z[0][0]. Dabei muss man beachten, dass der Index zwischen 0(!) und Dimension-1 läuft. Intern werden mehrdimensionale Felder zeilenweise als eindimensionale Felder abgespeichert, z.B. z={1,2,3,4,5,6}. Bei mehrdimensionalen Feldern erfolgt auch der interne Zugriff über die Berechnung des Index in der Art index = index1*Dim2+index2 ∈ { 0, ... , Dim1*Dim2-1 }. Beispiel: z[0][1]=2 ist der zweite, z[1][2]=6 ist der sechste Eintrag. Mit Ausnahme der Initialisierung sind aber Operationen mit Feldern (Zuweisung, Vergleich, Addition, ...) nur elementweise möglich, z.B. double x [3] , y [3] , z [3]; ... Initialisierung von x , y ... for ( int j =0; j <3; j ++) { z [ j ] = x [ j ] + y [ j ]; } 3.8.1 Felder als Funktionsparameter Der Name eines Feldes (ohne eckige Klammern) ist eine Speicheradresse, nämlich die Adresse unter der der erste Feldeintrag abgespeichert ist. Davon ausgehend werden die Adressen der anderen Einträge berechnet. Wird ein Feld als Parameter an eine Funktion übergeben, dann ist dieser Funktion die Speicheradresse bekannt und folglich sind Änderungen im Feld, die in der Funktion vorgenommen werden, auch außerhalb der Funktion gültig. feld.cpp: Felder sind keine lokalen Variablen 1 2 3 int sum ( int v [] , int n ) { /* Berechnung der Summe der Eintraege eines Feldes */ /* Eingabe : v [] = Feld , n = Feldlaenge */ 88 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Kapitel 3: Einführung in C/C++ /* Ausgabe : Summe int s =0; for ( int i =0; i < n ; i ++) { s += v [ i ]; } v [0] = 42; n = 42; return s ; } */ int main () { int v [10]={1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10}; int n =10; cout << " Summe ␣ ist ␣ " << sum (v , n ) << endl ; cout << " Das ␣ Feld ␣ hat ␣ " << n << " ␣ Einträge . " << endl ; cout << " Der ␣ erste ␣ Feldeintrag ␣ ist ␣ " << v [0] << " . " << endl ; } Summe ist 55 Das Feld hat 10 Einträge. Der erste Feldeintrag ist 42. Der Parameter n wird per call by value übergeben, er ist eine lokale Variable der Funktion sum. Also hat die Änderung n=42 keinen Einfluss auf den Wert im Hauptprogramm. Die Änderung v[0]=42 des ersten Eintrags des Feldes v ist dagegen auch im Hauptprogramm gültig, weil v – genauer die Speicheradresse von v – an sum übergeben wurde (call by reference). Man beachte, dass in dem Beispiel in der Funktionsdefinition die Feldübergabe mit “v[]“ erklärt wurde. Bei eindimensionalen Feldern ist in der Klammer keine Angabe notwendig. Bei mehrdimensionalen Feldern darf aber nur in der ersten Klammer die entsprechende Angabe fehlen. Die weiteren Klammern müssen genau die Größe enthalten, die der Deklaration des beim Aufruf verwendeten Feldes entspricht. Variable Angaben sind nicht zulässig. Zumindest wird dieses von vielen C++ Compilern verweigert. Die Verwendung von sogenannten “Variable Length Arrays“ ist im C-Standard seit 1999 enthalten, aber noch nicht in C++. Will man aber Funktionen für Standardoperationen mit Feldern definieren, so sollten die Dimensionen sinnvollerweise variabel sein. Die direkte Verwendung von mehrdimensionale Felder als Parameter von Funktionen ist hierbei zu unflexibel. Wir werden später erklären, wie man dieses Problem lösen kann. Beispiel: Eine Funktion zur Skalarmultiplikation von Matrizen (A → s·A) könnte man unter Verwendung mehrdimensionaler Felder nur für feste Dimensionen von A erklären, z.B. smult(double A[10][10], double s); 3.9 Präprozessoranweisungen 89 3.9 Präprozessoranweisungen Vor der Kompilierung wird der C++-Quelltext vom sogenannten Präprozessor verarbeitet. Dieser nimmt temporäre Änderungen am Programmtext vor, die für die Kompilierung benötigt werden. Jede Präprozessoranweisung beginnt mit einem Doppelkreuz (#) als erstes Zeichen einer Zeile. 3.9.1 #define Mit dem Kommando # define NAME Ersatztext werden sämtliche Vorkommen von NAME durch den Ersatztext ersetzt. # define BUFFER_SIZE 256 char buf [ BUFFER_SIZE ]; // also buf [256] Man kann auch deutlich komplexere Ersetzungen vornehmen lassen, da #define auch Argumente verarbeitet: # define HUNDRED ( a ) a *100 int z = 5; int z1 = HUNDRED ( z ); // z1 = 5*100 = 500 Leider liefert dieses #define nicht immer das Erwartete: int z2 = HUNDRED ( z +1); int z3 = ++ HUNDRED ( z ); // z2 = z +1*100 = 105 // z3 = ++ z *100 = 6*100 Nach der Textersetzung ist die Reihenfolge, in der die Operationen durchgeführt werden, anders, als es die scheinbare Funktion HUNDRED() suggeriert, denn der Präprozessor führt eine reine Textersetzung durch, wobei mathematische Vorrangregeln nicht berücksichtigt werden. Sicherer ist es, #define nur unter großzügiger Verwendung von Klammern zu verwenden: # define HUNDRED ( a ) (( a )*100) Meist sind Funktionen aber die bessere Alternative. 90 3.9.2 Kapitel 3: Einführung in C/C++ #include Trifft der Präprozessor auf den #include Befehl, liest er die entsprechende Header-Datei ein. Alle in ihr definierten Funktionen und Variablen sind für den Rest des Quelltext bekannt. # include < syscall .h > # include < cmath > # include " matrix . h " // Systemaufrufe // sin , cos , ... // Eigene Matrixfunktionen Wird der Dateiname in spitze Klammern gesetzt, sucht der Präprozessor zuerst im include-Pfad des Systems (z.B. /usr/include/). Dateien in Anführungszeichen werden zuerst im aktuellen Verzeichnis gesucht (lokal). Headerdateien enden auf .h. Bei den Headerdateien der C++Standardbibliothek lässt man diese Endung weg, um sie von den alten C-Headern zu unterscheiden. 3.10 Formatierte Ausgabe Mit std::cout haben wir Zahlen und Texte bisher nur unformatiert ausgegeben. Oft wünscht man jedoch eine Struktur in der Ausgabe, etwa bei Tabellen oder Matrizen. Die Formatierung der Ausgabe wird über Formatflags verwaltet. Auf diese kann direkt über Elementfunktionen zugegriffen werden oder über Manipulatoren. Die Formatflags sind Integer-Zahlen, bei denen einzelne Bits durch die folgenden Bit-Operatoren gesetzt und gelöscht werden können. 3.10.1 Bit-Operatoren Mit den Operatoren &, |, ^ und ~ lassen sich Zahlen bitweise verändern. Dabei wird die Operation auf die einzelnen Stellen der Zahl in der Binärdarstellung angewendet. Die Operatoren << und >> verschieben die Stellen der Zahl nach links oder rechts. 3.10 Formatierte Ausgabe Operator & | ^ ~ << >> Erläuterung Beispiel Bitweises UND Bitweises ODER Bitweises XOR Bitweises NOT Schieben nach links Schieben nach rechts char a = 14; char b = 19; a & b a | b a ^ b ~a a << 2 a >> 2 91 Ergebnis binär dezimal 00001110 14 00010011 19 00000010 2 00011111 31 00011101 29 11110001 -15 00111000 56 00000011 3 3.10.2 Formatierung über Elementfunktionen Die Formatierung kann mit Hilfe sogenannter Elementfunktionen gesteuert werden. Elementfunktionen werden wir im Rahmen der Diskussion von Klassen und Strukturen genauer kennenlernen. Mit der Elementfunktionen des Ausgabeoperators cout . width ( int n ); lässt sich eine minimale Feldbreite für die unmittelbar nächste Ausgabe definieren. Dieser Wert wird danach wieder auf 0 zurückgesetzt. Die Ausgabe von Buchstaben bzw. Zeichen ist davon nicht betroffen. Die Anzahl der gültigen Nachkommastellen bei der Ausgabe einer Fließkommazahl kann mit cout . precision ( int n ); geändert werden (in Festpunkt- und wissenschaftlicher Darstellungsform). In der normalen Form bezieht sich dieser Wert auf die Anzahl der Vor- und Nachkommastellen. Im Ist das Flag ios::fixed oder ios::scientific gesetzt, gibt setprecision() die Anzahl der Nachkommastellen an. Mit cout . fill ( char c ) lässt sich das Standard-Füllzeichen ’ ’ durch ein beliebiges anderes Zeichen ersetzen. Die Formatflags werden mit cout . setf ( long flag ); cout . unsetf ( long flag ); 92 Kapitel 3: Einführung in C/C++ gesetzt oder gelöscht. Die möglichen Flags stehen dabei als Formatierungsanweisungen zur Verfügung: Bitfeld ios::adjustfield (Ausrichtung) ios::basefield (Zahlensystem) ios::floatfield (Gleitkomma) (Sonstiges) Flags ios::left ios::right ios::oct ios::dec ios::hex ios::showbase ios::fixed ios::scientific ios::showpoint ios::uppercase ios::showpos Bedeutung linksbündig rechtsbündig oktal dezimal (standard) hexadezimal Zahlenbasis anzeigen Dezimaldarstellung (dddd.dd) Exponentialdarstellung (.dddddd Edd) führende Nullen ausgeben Großbuchstaben verwenden, d.h. ’E’,’X’ statt ’e’,’x’ explizite Ausgabe eines positiven Vorzeichens Mehrere Flags können dabei durch die Bit-Operation | kombiniert werden: cout . setf ( ios :: fixed | ios :: right ); Leider löscht cout.setf(ios::left) nicht automatisch das ios:right-Flag. Deshalb sollte man die Funktion cout . setf ( long flag , long gruppe ); nutzen, die zuerst die Bitfelder von gruppe löscht und anschließen gemäß flag neu setzt. Beispiel: Alle Bits der Gruppe ios::adjustfield löschen und danach ios::fixed und ios::right setzen: cout . setf ( ios :: fixed | ios :: right , ios :: adjustfield ); formatint.cpp: Formatierung von Ganzzahlen 1 2 3 4 5 6 7 8 9 10 11 12 void writenumbers ( int d [] , int m , int width ) { for ( int i =0; i < m ; i ++) { cout << " ( " ; cout . width ( width ); cout << d [ i ]; cout << " ) " << endl ; } cout << endl ; } int main () { int a []={24 , 28203 , -120 ,1000000}; 3.10 Formatierte Ausgabe cout . fill ( ’_ ’ ); 13 14 15 16 17 18 19 20 21 93 writenumbers (a , 4 , 0); writenumbers (a , 4 , 10); cout . setf ( ios :: left ); writenumbers (a , 4 , 10); } (24) (28203) (-120) (1000000) (________24) (_____28203) (______-120) (___1000000) (24________) (28203_____) (-120______) (1000000___) formatflt.cpp: Formatierung von Fließkommazahlen 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void writenumbers ( double d [] , int m , int width , int p ) { cout . precision ( p ); for ( int i =0; i < m ; i ++) { cout << " ( " ; cout . width ( width ); cout << d [ i ] << " ) " << endl ; } cout << endl ; } int main () { double b [] = {123. , 3.141529 , 123456789. , 0.000012}; cout << " normale ␣ Ausgabe " << endl ; writenumbers (b , 4 , 10 , 4); writenumbers (b , 4 , 10 , 6); cout << " feste ␣ Ausgabe " << endl ; cout . setf ( ios :: fixed , ios :: floatfield ); writenumbers (b , 4 , 10 , 3); writenumbers (b , 4 , 10 , 6); cout << " wissensc haftlich e ␣ Ausgabe " << endl ; cout . setf ( ios :: scientific , ios :: floatfield ); writenumbers (b , 4 , 10 , 3); writenumbers (b , 4 , 10 , 6); } 94 Kapitel 3: Einführung in C/C++ normale Ausgabe ( 123) ( 3.142) ( 1.235e+08) ( 1.2e-05) ( 123) ( 3.14153) (1.23457e+08) ( 1.2e-05) feste Ausgabe ( 123.000) ( 3.142) (123456789.000) ( 0.000) (123.000000) ( 3.141529) (123456789.000000) ( 0.000012) wissenschaftliche Ausgabe ( 1.230e+02) ( 3.142e+00) ( 1.235e+08) ( 1.200e-05) (1.230000e+02) (3.141529e+00) (1.234568e+08) (1.200000e-05) 3.10.3 Manipulatoren Die Verwendung von Manipulatoren zur Formatierung der Ausgabe kann durch die header-Datei <iomanip> eingebunden werden. Manipulatoren werden dem Ausgabestream einfach mit dem Operator << übergeben. 3.10 Formatierte Ausgabe Manipulator cout << setw(8) cout << setfill(’#’) cout << setprecision(3) cout << right cout << left cout << fixed cout << scientific cout << oct cout << dec cout << hex 95 entsprechende Elementfunktion cout.width(8) cout.fill(’#’) cout.precision(3) cout.setf(ios::right, ios::adjustfield) cout.setf(ios::left, ios::adjustfield) cout.setf(ios::fixed, ios::floatfield) cout.setf(ios::scientific, ios::floatfield) cout.setf(ios::oct, ios::basefield) cout.setf(ios::dec, ios::basefield) cout.setf(ios::hex, ios::basefield) Der Vorteil der Verwendung von Manipulatoren ist, dass Manipulatoren einfacher zu handhaben sind und für mehr Übersicht sorgen (bei gleicher Funktionalität im Vergleich zu den Elementfunktionen). 3.10.4 Beispiel: Berechnung von π nach Leibniz Von Leibniz stammt die Reihenentwicklung ∞ X 1 1 1 1 (−1)n π = 1 − + − + ∓ ... = , 4 3 5 7 9 2n + 1 n=0 die wir zur approximativen Berechnung von π benutzen wollen (wobei es dazu deutlich bessere Algorithmen gibt): leibniz.cpp: Approximation von π mit Leibniz-Verfahren 1 2 3 4 5 6 7 int main () { long max , n ; double pi = 0; cout << " Bis ␣ zu ␣ welchem ␣ n ␣ soll ␣ gerechnet ␣ werden ? ␣ " ; cin >> max ; 8 9 10 11 12 13 for ( n =0; n <= max ; n ++) { if ( n %2) pi -= 1./(2* n +1); else pi += 1./(2* n +1); } 14 15 16 pi *=4; cout << " n ␣ = ␣ " << left << setw (10) << --n ; 96 17 18 19 20 Kapitel 3: Einführung in C/C++ cout . setf ( ios :: fixed ); cout << " ␣ ␣ pi ␣ = ␣ " << right << setw (14) << setprecision (10) << pi << endl ; } $ leibniz Bis zu welchem n soll gerechnet werden? 200 n = 200 pi = 3.1465677472 $ leibniz Bis zu welchem n soll gerechnet werden? 20000 n = 20000 pi = 3.1416426511 $ leibniz Bis zu welchem n soll gerechnet werden? 2000000 n = 2000000 pi = 3.1415931536 3.11 Zeiger Felder haben einen großen Nachteil: Man muss bereits zur Übersetzungszeit wissen, wieviel Speicher man für sie reservieren muss. Häufig will man mit komplexen Daten umgehen, die in unterschiedlichen Ausprägungen auftreten können, z.B. Vektoren v ∈ Rn und Matrizen A ∈ Rm×n für verschiedene m, n ∈ N. Eine Möglichkeit ist die Definition über Felder, die so groß dimensioniert sind, dass alle möglichen Ausprägungen dort Platz finden: double v [1000] , A [1000][1000]; Dabei werden aber enorme Mengen Speicher verschwendet, wenn man den bereitgestellten Platz mit Vektoren und Matrizen viel kleinerer Dimension belegt. 3.11.1 Zeigervariablen C++ bietet die Möglichkeit, Speicherbereiche zur Laufzeit dynamisch zu allozieren (bzw. reservieren) und wieder freizugeben. Die Adressen von Speicherbereichen werden in C++ in Zeigern (engl.: Pointern) verwaltet. Während z.B. eine Integer-Variable eine ganze Zahl als Wert (bzw. Inhalt) hat, hat eine Zeigervariable eine Speicheradresse als Inhalt, und diese Adresse ist dann sogar veränderbar. Datentyp * name ; Beispiel: 3.11 Zeiger 97 double * v ; Durch diese Definition wird eine Variable name vom Typ Zeiger auf Datentyp vereinbart. Durch diese Definition sind Zeiger und der entsprechende Datentyp untrennbar miteinander verbunden. Bei dieser Definiton wird kein Speicherplatz für ein Objekt vom entsprechenden Datentyp reserviert, sondern ein Speicherbereich, der groß genug ist, um die Adresse zu speichern (i.d.R. auf heutigen Systemen 4 Bytes). Analog zur normalen Variablendefinition, ist auch der Specherbereich nach der Erzeugung der Variablen undefiniert, und dieser erhält erst dann einen sinnvoll interpretierbaren Inhalt, falls er initialisiert wird. Aber damit sind Zugriffe der Art v[0]=0.0, v[1]=1.0, . . . noch nicht sinnvoll, denn der Compiler weist v erstmal eine beliebige Adresse zu. Sauberer ist es, Zeiger mit 0 (oder auch NULL) zu initialisieren. 0 ist eine spezielle Adresse, und bedeutet, dass der Zeiger auf nichts zeigt. double * v = 0; Jetzt kann geprüft werden, ob v bereits auf reservierten Speicher zeigt: if ( v != 0) ... // nein , noch nicht if ( v != NULL ) ... // das gleiche if ( v ) ... // und nochmal das gleiche ... 3.11.2 Speicher reservieren mit new Um nun tatsächlich den Speicher zu reservieren (bzw. allozieren) auf den ein Zeiger weisen soll, verwendet man die Operatoren new Datentyp ; new Datentyp ( Initialisierung ); Beispiel: double * v = 0; v = new double ; * v = 2.718; Kompakter lässt sich das auch schreiben als 98 Kapitel 3: Einführung in C/C++ double * v = new double (2.718); Was passiert hier ? new erzeugt im Speicher eine Variable vom Typ double, initialisiert sie mit dem Wert 2.718 und liefert als Wert des Ausdrucks deren Speicheradresse zurück, die dann dem Zeiger v zugewiesen wird. Der Zeiger v speichert also nun die Adresse der neu erzeugten Variable. Mit *v wird auf diesen Wert zugegriffen. Der direkte Zugriff über einen Namen ist nicht möglich, da mittels new erzeugte Variablen „namenlos“ sind. Auf den Wert der neuen Variablen kann übrigens auch mit Hilfe des Ausdrucks v[0] zugegriffen werden, also quasi wie auf ein Feld mit einem Element. Für den Umgang mit Vektoren und Matrizen benötigt man jedoch nicht nur einen Speicherplatz, sondern große Speicherbereiche in Form eines entsprechend dimensionierten Feldes. new Datentyp [ Groesse ]; Auf die einzelnen allozierten Elemente des dynamischen Arrays kann man dann z.B. mit dem Operator [ ] wie im folgenden Beispiel zugreifen: kehrwert.cpp: Beliebige viele Kehrwerte 1 2 3 4 5 int main () { int m ; cout << " Wieviele ␣ Kehrwerte ? ␣ " ; cin >> m ; // double v [ m ]; -- nicht erlaubt . double * v = new double [ m ]; 6 7 8 9 10 11 12 13 14 15 16 for ( int i =0; i < m ; i ++) { v [ i ] = 1./( i +1); } for ( int i =0; i < m ; i ++) { cout << v [ i ] << endl ; } } Wichtig! Bei der bisherigen Form der Deklaration eines Arrays, muss der die Größe bestimmende Ausdruck konstant sein, während das in diesem Fall nicht sein muss. 3.11.3 Speicher freigeben mit delete Mit new allozierter Speicher wird mit dem Programmende wieder freigegeben. Dennoch sollte man nicht mehr benötigten Speicher in seinem Programm selber 3.11 Zeiger 99 freigeben, da C++ dieses nicht automatisch macht (auch wenn gar keine Zeiger mehr existieren, die auf diesen Bereich „zeigen“): delete Name ; delete [] Name ; // ein Wert mit new alloziert . // ein Array mit new [] alloziert . Danach zeigt Name weiterhin auf die alte Speicheradresse, auf die sie aber nicht mehr sinnvoll zugreifen kann, da das Objekt, auf das dieser Zeiger zeigt, zerstört worden ist. Dem Zeiger kann nun mit einem neuen new wieder Speicher zugewiesen werden. Eine saubere Lösung, um den Zeiger noch zu „deaktivieren“ ist es, ihn auf 0 zu setzen. 3.11.4 Zugriff auf Zeigerwerte In Zeigern werden Speicheradressen gespeichert. new gibt dafür eine neue Speicheradresse zurück. Aber ein Zeiger kann auch auf bereits bestehende Variablen zeigen, wenn dem Zeiger die Adresse der Variablen mit dem Referenzierungsoder Adressoperator & übergeben wird. Um auch dieser Adresse einen konkreten Inhalt zuzuweisen, benutzt man den Dereferenzierungsoperator *: pointer.cpp: Zeiger auf Variablen 1 2 3 4 5 6 7 8 int main () { int x , y =42 , * z ; z = &x; *z = y; cout << " Auf ␣ welche ␣ Adresse ␣ zeigt ␣ z ? ␣ " << z << endl ; cout << " Was ␣ ist ␣ dort ␣ gespeichert ? ␣ " << * z << endl ; cout << " Was ␣ ist ␣ dort ␣ gespeichert ? ␣ " << x << endl ; } Auf welche Adresse zeigt z? 0xbfffd844 Was ist dort gespeichert? 42 Was ist dort gespeichert? 42 Man beachte, dass * verschiedene Bedeutungen hat: Bei der Variablendeklaration zeigt er an, dass eine Zeigervariable definiert wird. Sonst wird damit gekennzeichnet, dass auf den Wert zugegriffen wird, der unter der Adresse gespeichert ist, auf die der Zeiger zeigt. Wie schon erwähnt, kann man auch mit dem Operator [ ] dereferenziern, d.h. *z und z[0] ist das Gleiche. 100 Kapitel 3: Einführung in C/C++ 3.11.5 Benutzung des Referenzierungs- und Dereferenzierungsoperators reference.cpp: (De-)Referenzierung von Zeigern 1 2 3 4 5 6 int main () { int x , * z ; x = 10; cout << " Der ␣ Wert ␣ von ␣ x ␣ ist ␣ x ␣ = ␣ " << x << endl ; cout << " Die ␣ Adresse ␣ von ␣ x ␣ ist ␣ " << & x << endl ; 7 8 9 10 11 12 13 14 15 16 17 18 z = &x; cout << " Der ␣ Wert ␣ von ␣ z ␣ ist ␣ die ␣ Adresse ␣ von ␣ x : ␣ " << " z ␣ = ␣ " << z << endl ; * z = 20 ; cout << " x ␣ = ␣ " << x << endl ; x = 30; cout << " x ␣ = ␣ " << x << " ,␣ dereferenziertes ␣ z ␣ = ␣ " << * z << endl ; } Der Die Der x = x = Wert von x ist x = 10 Adresse von x ist 0xbfffe640 Wert von z ist die Adresse von x: z = 0xbfffe640 20 30, dereferenziertes z = 30 Dem Zeiger z wird die Adresse der Variablen x zugewiesen, wodurch die beiden miteinander verbunden sind (“z zeigt auf x”). Dementsprechend wirkt sich die Anweisung *z=20 auf den Wert von x aus, und ebenso die Wertänderung von x auf das derefenzierte *z. 3.11.6 Zeiger als Funktionsparameter In dem Beispiel werden zwei Vektoren x, y und ein Skalar α übergeben, zurückgegeben werden z = x + αy und die euklidische Norm kzk2 . pointerret.cpp: Rückgabe mehrerer Werte 1 2 3 # define n 3 101 3.11 Zeiger 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 double vektornorm ( double * , double * , double * , double ); int main () { double x [3] = {4.0 ,2.0 ,2.0}; double y [3] = { -0.5 ,0.5 ,2.0} , z [3]; double alpha = 4.0 , norm ; int i ; norm = vektornorm (x ,y ,z , alpha ); cout << " z ␣ = ␣ " << endl ; for ( i =0; i < n ; i ++) { cout << z [ i ] << endl ; } cout << " || ␣ z ␣ || ␣ = ␣ " << norm << endl ; } double vektornorm ( double *X , double *Y , double *Z , double s ) { int i ; double sum = 0.0; for ( i =0; i < n ; i ++) { Z [ i ] = X [ i ] + s * Y [ i ]; sum += Z [ i ]* Z [ i ]; } return sqrt ( sum ); } z = 2 4 10 || z || = 10.9545 An diesem Beispiel erkennt man: • Von der Notation her ist ein Feld identisch mit einem Zeiger auf eine Variable (siehe Prototyp von vektornorm). Bei Feldern zeigt der Zeiger auf den ersten Feldeintrag bzw. der Name des Arrays wird in C++ per Standardumwandlung (d.h. automatisch) in einen Zeiger auf die erste Feldkomponente umgewandelt. • Die Definition der Funktion bzw. des Prototyps ist mit x[] oder *x möglich, dies macht keinen Unterschied in der Funktionsweise. Man beachte, dass man in beiden Fällen x in der Funktion eine neue Adresse zuweisen kann. Dieses hat natürlich gravierende Auswirkungen, die in der Regel nicht beabsichtigt sind. Ein großer Unterschied besteht aber zur Verwendung von x[N] in der Funktionsdefinition (wobei N eine Konstante ist). In diesem Fall kann die Funktion nur mit einem Feld genau dieser Größe aufgerufen werden. • Der Funktionsaufruf erfolgt nicht mit &x,&y,... x,y,..., da der Feldname für die Adresse steht. sondern mit 102 Kapitel 3: Einführung in C/C++ • Bei Benutzung des Feldindexes (vgl. vektornorm) wird der Dereferenzierungsoperator nicht benötigt, da sich bei Zugriffen auf Felder Feldzeiger und Feldname nicht unterscheiden (beide beinhalten die Adresse des Feldes). 3.11.7 Weitere Zeiger-Typen 1. Zeiger auf Zeiger Datentyp ** Name ; Matrizen von dynamischer Größe kann man durch Zeiger auf Zeiger realisieren. Dabei muss der Speicherbereich in zwei Schritten alloziert werden: Zuerst wird ein Feld von Zeigern auf Zeiger auf den Datentyp alloziert, anschließend wird für jeden Zeiger des Feldes ein Feld vom Datentyp allociert und dem Zeiger zugeweisen. Im Vergleich zur direkten Deklaration einer Matrix mit N mal M Elementen, wird also nicht nur der Speicherplatz für die N mal M Elemente angelegt, sondern zusätzlich ein Feld von N Elementen, das die Zeiger auf jeweils M Elemente (die Zeilen der Matrix) enthält. Das Freigeben das Speichers erfolgt ebenfalls in zwei Schritten, aber in anderer Reihenfolge. matrix.cpp: Matrixoperationen 1 2 3 4 5 6 7 8 9 10 11 void initMatrix ( double ** m , int size ); void writeMatrix ( double ** m , int size ); int main () { double ** v =0; int size = 10; v = new double *[ size ]; for ( int i =0; i < size ; i ++) { v [ i ] = new double [ size ]; } initMatrix (v , size ); writeMatrix (v , size ); 12 13 14 15 16 17 18 19 20 for ( int i =0; i < size ; i ++) { delete [] v [ i ]; } delete [] v ; } 3.11 Zeiger 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 103 void initMatrix ( double ** m , int size ) { for ( int i =0; i < size ; i ++) { for ( int j =0; j < size ; j ++) { m [ i ][ j ] = i == j ?1:0; } } } void writeMatrix ( double ** m , int size ) { for ( int i =0; i < size ; i ++) { for ( int j =0; j < size ; j ++) { cout << m [ i ][ j ] << " ␣ " ; } cout << endl ; } } 2. Felder von Zeigern Datentyp * Name [ n ]; int a =10 , b =20 , * c [10]; c [0] = & a ; c [1] = & b ; * c [0] = 30; c [2] = c [1]; * c [2] = 40; 3. Zeiger als Funktionswerte Rückgabetyp * Name ( ptyp1 , ptyp2 , ...) Der Rückgabewert einer Funktion darf auch ein Zeiger sein. In der Funktion allozierter Speicher bleibt auch außerhalb bestehen. matrix2.cpp: Speicher für Matrix reservieren 1 2 3 4 5 6 7 8 9 10 11 12 double ** newMatrix ( int size ); void initMatrix ( double ** m , int size ); void writeMatrix ( double ** m , int size ); void deleteMatrix ( double ** m , int size ); int main () { double ** v =0; int size = 10; v = newMatrix ( size ); initMatrix (v , size ); writeMatrix (v , size ); 104 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Kapitel 3: Einführung in C/C++ deleteMatrix (v , size ); } double ** newMatrix ( int size ) { double ** m = new double *[ size ]; for ( int i =0; i < size ; i ++) { m [ i ] = new double [ size ]; } return m ; } void deleteMatrix ( double ** m , int size ) { for ( int i =0; i < size ; i ++) { delete [] m [ i ]; } delete [] m ; } 4. Zeiger auf Funktionen Rückgabetyp (* Name )( ptyp1 , ptyp2 , ...) ( Beispiel: Zusammengesetzte Funktion f (x) = f1 (x) ; f2 (x) ; x>0 x ≤ 0, zeigerfkt.cpp: Zeiger auf Funktionen 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 double f1 ( double x ) { return x * x ;} double f2 ( double x ) { return 0;} int main () { double x ; double (* fkt )( double ); // wie f1 und f2 ! cout << " Funktionswert ␣ x ␣ eingeben : ␣ " ; cin >> x ; if (x >0) fkt = f1 ; else fkt = f2 ; cout << " f ( x ) ␣ = ␣ " << ( fkt )( x ) << endl ; } Bei der Definition muss *name eingeklammert sein (vgl. 3), beim Aufruf könnten diese Klammern weggelassen werden. Man sollte trotzdem Klammern setzen, um die Benutzung eines Funktionszeigers zu symbolisieren. Der Name eines Funktionszeigers ohne alle Klammern (auch ohne Funktionsaufrufklammern) repräsentiert dagegen die Adresse und wird an den entsprechenden Stellen benutzt (analog zu Feldern). 3.11 Zeiger 105 Beachte: Ein Funktionszeiger kann nur Adressen von Funktionen aufnehmen, die gleichen Rückgabetyp und gleiche Parameterliste besitzen. 3.11.8 Zeigerarithmetik Mit Zeigern kann man auch „rechnen“. Die Standardoperationen sind dafür sinnvoll neubelegt: Beispielsweise in- bzw. dekrementieren die Operatoren ++ und -einen Zeiger um die Größe des Variablentyps, auf den der Zeiger zeigt. pointerar.cpp: Zeigerarithmetik 1 2 3 4 5 6 7 8 9 int main () { int x =10 , * y ; y = &x; cout << " ␣ y ␣ = ␣ " cout << " * y ␣ = ␣ " y ++; cout << " ␣ y ␣ = ␣ " cout << " * y ␣ = ␣ " } y *y y *y = = = = << y << endl ; << * y << endl ; << y << endl ; << * y << endl ; 0xbfffebc0 10 0xbfffebc4 1076098592 Aber jetzt zeigt y nicht mehr auf x und das Resultat von *y ist undefiniert. Allgemein kann man ganze Zahlen mit Zeigern addieren bzw. subtrahieren (dann wird der Zeiger um entsprechend viele Adressen verschoben), Zeiger miteinander vergleichen oder Differenzen von Zeigern berechnen. Insbesonder ist *(z+n) das Gleiche wie z[n]. pointerar2.cpp: Vergleich von Zeigern 1 2 3 4 5 6 7 8 9 10 int main () { int v [10]={1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10}; int n =10 , * z ; int sum = 0; for ( z = v ; z < v + n ; z ++) { sum += * z ; } cout << " Summe ␣ ist ␣ " << sum << endl ; } Ein weiteres Beispiel soll anhand des Beispielprogramms pointerret.cpp diese äquivalente Formulierung weiter verdeutlichen: 106 Kapitel 3: Einführung in C/C++ pointer3.cpp: Vergleich von Zeigern 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # define n 3 double vektornorm ( double * , double * , double * , double ); int main () { double x [3] = {4.0 ,2.0 ,2.0}; double y [3] = { -0.5 ,0.5 ,2.0} , z [3]; double alpha = 4.0 , norm ; int i ; norm = vektornorm (x ,y ,z , alpha ); cout << " z ␣ = ␣ " << endl ; for ( i =0; i < n ; i ++) { cout << z [ i ] << endl ; } cout << " || ␣ z ␣ || ␣ = ␣ " << norm << endl ; } double vektornorm ( double *X , double *Y , double *Z , double s ) { int i ; double sum = 0.0; for ( i =0; i < n ; i ++) { *( Z + i ) = *( X + i ) + s *(*( Y + i )); sum += *( Z + i )*(*( Z + i )); } return sqrt ( sum ); } 3.11.9 Sichere Allozierung Was passiert, wenn new den gewünschten Speicherbereich nicht allozieren kann? In C++ wird ein spezieller Fehlerbehandlungsmechanismus eingeleitet: Dazu muss der Fehler aber in einem überwachten try-Block ausgelöst werden, um im anschließenden catch-Block wieder aufgefangen und behandelt zu werden. Der Fehler (besser Ausnahme oder Exception) für Speicherplatzprobleme heißt bad_alloc: memory2.cpp: Sichere Allozierung mit Exceptions 1 2 3 4 int main () { double * v ; try { for ( int i =0;; i ++) { 3.12 Zeichenketten v = new double [100000]; cout << i << " ␣ " << v << endl ; 5 6 7 8 9 10 11 12 107 } } catch ( bad_alloc ) { cerr << " Kein ␣ Speicher ␣ mehr ␣ frei ! " << endl ; } } Eine weitere Lösung wäre, den Compiler mit dem Befehl new(nothrow) anzuweisen, den Nullzeiger zurückzugeben, wenn nicht mehr ausreichend Speicher reserviert werden konnte. memory.cpp: Sichere Allozierung mit Nullzeigern 1 2 3 4 5 6 7 8 9 10 11 int main () { double * v ; for ( int i =0;; i ++) { v = new ( nothrow ) double [100000]; cout << i << " ␣ " << v << endl ; if (! v ) { cerr << " Kein ␣ Speicher ␣ mehr ␣ frei ! " << endl ; break ; } } } 3.12 Zeichenketten 3.12.1 Der Datentyp char Zwar können in char auch Zahlen abgespeichert werden, aber eigentlich wird dieser Datentyp als Zeichen (Ziffern, Buchstaben, Sonderzeichen, Steuerzeichen) interpretiert. Zeichenliterale werden durch einfache Anführungszeichen gekennzeichnet: char zeichen = ’d ’; zeichen += ’A ’ - ’a ’; // Nun ist zeichen ein ’D ’. Der Datentyp char spielt eine Doppelrolle: Ein Variable des Typs belegt 1 Byte = 8 Bits Speicherplatz, entsprechend ist jeder mögliche char-Wert c 108 Kapitel 3: Einführung in C/C++ eindeutig durch eine Zahl −128 ≤ c ≤ 127 beschrieben. Der Compiler unterscheidet nicht zwischen Zeichen und Zahl, man kann deshalb mit Zeichen auch rechnen: zeichen = ’a ’ + 5; zeichen += ’A ’ - ’a ’; // zeichen ist ’f ’. // Nun ist zeichen ein ’F ’. ascii.cpp: Erzeugen einer (lokalen) ASCII-Tabelle 1 2 3 4 5 int main () { for ( char c =32; c <=126; c ++) cout << " Zeichen ␣ " << c << " ␣ hat ␣ ASCII ␣ Code ␣ " << ( int ) c << endl ; } Die Doppelrolle kann man auch ausnutzen um char-Variablen für Fallunterscheidungen bei switch-case-Konstrukten zu benutzen: switch ( c ) { case ’a ’: case ’! ’: } ... ... 3.12.2 Die Funktionen von ctype.h Nützliche Funktionen zur Behandlung von char-Variablen und -Konstanten findet man in dem Modul <ctype.h> (siehe Anhang A.1). ctype.cpp: Beispielprogramm zu ctype.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # include < iostream > # include < ctype .h > using namespace std ; int main () { char z , dummy ; do { cout << " Bitte ␣ ein ␣ Zeichen ␣ eingeben : ␣ " ; cin . get ( z ); cin . get ( dummy ); cout << " Der ␣ ASCII - Code ␣ des ␣ Zeichens ␣ ist ␣ " << ( int ) z << endl ; cout << " Zeichen ␣ als ␣ Grossbuchstabe : ␣ ’" << ( char ) toupper ( z ) << " ’" << endl ; cout << " ... ␣ und ␣ als ␣ Kleinbuchstabe : ␣ ’" 3.12 Zeichenketten 17 18 19 20 109 << ( char ) tolower ( z ) << " ’" << endl ; } while ( isgraph ( z )); } Bitte ein Zeichen eingeben: Der ASCII-Code des Zeichens Zeichen als Grossbuchstabe: ... und als Kleinbuchstabe: Bitte ein Zeichen eingeben: Der ASCII-Code des Zeichens Zeichen als Grossbuchstabe: ... und als Kleinbuchstabe: Bitte ein Zeichen eingeben: Der ASCII-Code des Zeichens Zeichen als Grossbuchstabe: ... und als Kleinbuchstabe: a ist 97 ’A’ ’a’ ! ist 33 ’!’ ’!’ ist 32 ’ ’ ’ ’ 3.12.3 Steuerzeichen Einige Zeichen sind für Steuerzeichen reserviert. Steuerzeichen \a \b \f \n \r \t \v \" \’ \? \\ Name BEL (bell) BS (backspace) FF (formfeed) NL (newline) CR (carriage return) HT (horizontal tab) VT (vertical tab) Ausgabe akustisches Warnsignal Rückschritt Seitenvorschub Zeilenwechsel Wagenrücklauf horizontaler Tabulator vertikaler Tabulator doppelte Anführungszeichen " einfache Anführungszeichen ’ Fragezeichen ? Backslash \ 3.12.4 Zeichenketten Durch Zusammenfassung von char-Konstanten oder -Variablen bildet man Zeichenketten. Zumindest ist dies die Methode, die C anbietet. In C++ hat man 110 Kapitel 3: Einführung in C/C++ die Möglichkeit die Zeichenketten (Strings) der Standardbibliothek zu verwenden. Die damit verbundenen Vereinfachungen undVerbesserungen werden wir im nachfolgenden Kapitel ”Strings” betrachten. Literale Zeichenketten werden durch doppelte Anführungszeichen gekennzeichnet. char * s = " xyz " ; char s [4] = { ’x ’ , ’y ’ , ’z ’ }; Mit Ausnahme der Initialisierung muss die Verarbeitung von Zeichenketten ansonsten elementweise erfolgen (vgl. Felder), d.h. Zuweisungen der Art str = "abcd" sind nicht erlaubt. Jede Zeichenkette besitzt eine Endekennung (intern als Wert 0 gespeichert), um den Umgang mit flexiblen Textlängen zu erlauben. Damit ist die Textlänge = Anzahl Zeichen + 1 ≤ Feldlänge . Bei der Ausgabe einer Zeichenkette werden dann nur die Zeichen bis zur Endekennung ausgegeben, auch wenn die Zeichenkette als größeres Feld definiert ist. zeichen.cpp: Zeichen einzeln ausgegeben 1 2 3 4 5 6 7 8 9 int main () { char s [20] = " Sommer ␣ 2006 " ; int i = 0; while ( s [ i ]) { cout << s [ i ]; i ++; } cout << endl ; } Der Text s wird zeichenweise ausgegeben, bei Erreichen der Endekennung ist s[i]==0 und die while-Schleife bricht ab. Beim Umgang mit Zeichenketten muss man darauf achten, dass genügend Speicherplatz zur Verfügung steht: Bei durch char s[n] definierten Zeichenketten dürfen nur (inkl. Endekennung) n Zeichen benutzt werden, bei der Definition char *s muss man sogar noch hinreichend viel Speicherplatz beschaffen. Ansonsten werden undefinierte Speicheradressen benutzt. Felder von Zeichenketten werden durch char name[n1][n2] oder char *name[n1] definiert. Beispiel: Speicherung der Wochentage a) char tage[7][11] = {"Montag", ..., "Sonntag"} Die Zeilenlänge ist durch den längsten Wochentag ("Donnerstag") festgelegt. b) char *tage[7] = {"Montag", ... } Ein Feld von Zeigern, jeder Eintrag zeigt auf eine Zeichenkette von variabler Länge. Diese Variante verbraucht weniger Speicherplatz. 3.12 Zeichenketten 111 3.12.5 Ein- und Ausgabe von Zeichenketten Für die Ausgabe von Zeichenketten benutzt man std::cout, die Eingabe ist mit std::cin möglich: cin >> s ; std::cin kann mehr als eine Eingabe gleichzeitig verarbeiten, bei der Eingabe über die Tastatur müssen die Werte dann durch Leerzeichen getrennt werden: int i ,j , k ; cout << " 2 ␣ Zahlen ␣ eingeben : ␣ " ; cin >> j >> k ; Was passiert bei cin >> s, wenn z.B. die Zeichenkette "Hello World" eingelesen werden soll? Es wird nur "Hello" eingelesen, weil danach das Leerzeichen als StringEnde interpretiert und das Einlesen beendet wird. Abhilfe schafft die Funktion getline(), die eine Zeile bis zum Zeilenendezeichen (oder einem anderen Zeichen) einliest: cin . getline ( char * str , int n , char ende = ’\ n ’ ); zeichen2.cpp: Einlesen einer ganzen Zeile 1 2 3 4 5 6 int main () { char s [20]; cout << " String ␣ eingeben : ␣ " ; cin . getline (s ,20); cout << s << endl ; } String eingeben: hello world hello world 3.12.6 Die Funktionen von string.h Weitere nützliche Funktionen zum Umgang mit Strings liefert <string.h> (Anhang A.2). Einige Beispiele zur Anwendung: • strlen ermittelt die Länge eines Strings ohne Endekennung. Syntax: size_t strlen(const char *s) Dabei ist size_t ein spezieller Datentyp, der alle möglichen Speicherbereichsgrößen enthält, und const ist ein Attribut zur Kennzeichnung von 112 Kapitel 3: Einführung in C/C++ Konstanten, mit dem man klarstellt, dass der Wert von s in der Funktion nicht verändert werden kann. • strcpy dient zum Kopieren von Strings. Syntax: char *strcpy(char *ziel, const char *quelle) Weil die Zuweisung t = "Mathematik" so nicht möglich ist, benutzt man diese Funktion, z.B. zeichen3.cpp: Zeichenlänge 1 2 3 4 5 6 7 8 int main () { char s [20] = " Mathematik " ; int n = strlen ( s ); cout << " n = ␣ " << n << endl ; char t [20]; strcpy (t , s ); } • strcmp benutzt man zum Vergleichen von Strings. Syntax: int strcmp(const char *s1, const char *s2) Die Funktion liefert genau dann “0”, wenn die Strings identisch sind. Ist das erste unterschiedliche Zeichen bei s1 größer (im ASCII Code) als bei s2, dann wird ein positiver, sonst ein negativer Wert ausgegeben. Weitere nützliche Funktionen zur Umwandlung von Strings in Integer- bzw. Double-Werte finden sich im Modul stdlib.h: int double atoi(const char *s) atof(const char *s) convert.cpp: Umwandlung von Zeichenkette in Zahl 1 2 3 4 5 6 7 int main () { char * s = " 3.1415 " ; int j = atoi ( s ); cout << " j = ␣ " << j << endl ; double x = atof ( s ); cout << " x = ␣ " << x << endl ; } 3.12.7 Argumente für die Funktion main Linux-Programme können mit Argumenten aufgerufen werden, z.B. ls -l *.cpp . Um solche Argumentlisten C++-Programmen zu übergeben, muss die Funktion main folgendermaßen deklariert werden: 3.13 Strings 113 int main ( int argc , char * argv []) { /* ... */ } Dabei gibt argc an, wieviele Argumente bereitstehen. Die einzelnen Argumente stehen als C-String in argv. arg.cpp: Argumente für das Programm 1 2 3 4 5 6 7 int main ( int argc , char * argv []) { for ( int i =0; i < argc ; i ++) { cout << setw (4) << i << " : ␣ " << ’" ’ << argv [ i ] << ’" ’ << endl ; } } $ arg -s 120 datei.dat 0: "arg" 1: "-s" 2: "120" 3: "datei.dat" $ ./arg *.cpp 0: "./arg" 1: "arg.cpp" 2: "ascii.cpp" 3: "bitweise.cpp" 4: "break.cpp" 5: "callref.cpp" ... In argv[0] steht dabei immer der Programmname, mit dem das Programm aufgerufen wurde. Danach folgen die Argumente, so wie die Shell sie übergibt (also z.B. mit Wildcard-Ersetzung). 3.13 Strings Das Arbeiten mit Zeichenketten der Form char s[] ist sehr mühsam und fehleranfällig. Im Modul <string> wird eine Klasse std::string zum eleganten Umgang mit Zeichenketten definiert. 114 Kapitel 3: Einführung in C/C++ 3.13.1 Konstruktoren für Strings Erzeugen eines leeren Strings. string (); Erstellen einer Kopie eines anderen Strings oder Erstellen eines Teilstrings der Länge n ab Position pos. string ( const string & str ); string ( const string & str , size_type pos , size_type n = npos ); size_type steht in diesem Kapitel immer für eine Position in einem String, also eine Integerzahl. Die Zahl 0 markiert immer den Anfang, und npos das Ende des Strings. Erstellen eines Strings aus einer Zeichenkette (mit der maximalen Länge n). string ( const char * s ); string ( const char *s , size_type n ); Erstellen eines Strings in dem n mal das Zeichen ch vorkommt. string ( size_type n , const char & ch ); str_konstr.cpp: Erstellen von Strings 1 2 3 4 5 6 7 8 9 10 11 int main () string string string string cout cout cout cout << << << << { s1 ( " Vorgang " ); // s2 ( s1 , 1 , 5); // s3 ( s1 , 3); // s4 (5 , ’1 ’ ); // s1 s2 s3 s4 << << << << endl ; endl ; endl ; endl ; // // // // String aus Zeichenkette Teilstring , 5 Zeichen lang Teilstring bis zum Ende fünfmal das Zeichen ’1 ’ Vorgang organ gang 11111 } 3.13.2 Verändern von Strings Strings können den Methoden in diesem Kapitel als Parameter in diesen Formen übergeben werden: • const string &str 115 3.13 Strings • const string &str, size_type pos, size_type n • const char* s • const char* s, size_type n • size_type n, char ch Die Bedeutung der Parameter entspricht der in den jeweiligen Konstruktoren. Die Methoden werden hier aber nur für die erste Variante vorgestellt. Anfügen eines Strings. string & append ( const string & str ); Außerdem fügt der Operator + strings mit strings, char* oder char zusammen. Fügt str an der Stelle index ein. string & insert ( size_type index , const string & str ); Ersetzt m Zeichen ab index durch str. string & replace ( size_type index , size_type m , const string & str ); Entfernt n Zeichen ab index. string & erase ( size_type index = 0 , size_type n = npos ); Entfernt alle Zeichen. void clear (); Zuweisen an einen String. string & assign ( const string & str ); Der Operator = weist ebenfalls strings, char* oder char an einen String zu. str_manip.cpp: Verändern von Strings 1 2 3 4 5 6 7 8 9 10 11 12 13 int main () { string s1 , s2 ; s1 = string ( " Hallo " ); s2 = " Welt " ; cout << s1 + ’␣ ’+ s2 << endl ; // " Hallo Welt " s1 . append ( s2 ); s1 . insert (5 , " ␣ liebe ␣ " ); s1 . replace (12 ,4 , " Sonne " ); s1 . erase (5); cout << s1 << endl ; } // // // // " HalloWelt " " Hallo liebe Welt " " Hallo liebe Sonne " " Hallo " 116 Kapitel 3: Einführung in C/C++ 3.13.3 Informationen über Strings Aktuelle Länge des Strings. size_type length () const ; size_type size () const ; Derzeitige Größe des Strings im Speicher. size_type capacity () const ; Maximale Größe eine Strings. size_type max_size () const ; str_info.cpp: Informationen über String-Längen 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void info ( const string & s ) { cout << setw (12) << s << setw (12) << s . length () << setw (12) << s . size () << setw (12) << s . capacity () << setw (12) << s . max_size () << endl ; } int main () { string s1 ( " Hallo ␣ Welt " ); info ( s1 ); s1 . erase (2 ,6); info ( s1 ); } Hallo Welt Halt 10 4 10 4 10 10 1073741820 1073741820 3.13.4 Vergleiche und Tests Vergleich mit einem anderen String. int compare ( const string & str ); int compare ( const char * str ); Vergleich von n Zeichen ab Position index mit einem anderen String, oder einem Teil davon. 3.13 Strings 117 int compare ( size_type index , size_type n , const string & str ); int compare ( size_type index , size_type n , const string & str , size_type index2 , size_type n2 ); Der Rückgabewert von compare() bestimmt sich nach dieser Tabelle: Fall Rückgabewert this<str negativer Wert this==str 0 this>str positiver Wert Die Operatoren == != < > <= >= sind ebenfalls gültig. Prüft, ob der String leer ist. bool empty () const ; str_cmp.cpp: Vergleichen von Strings 1 2 3 int main () { string s1 ( " Hallo ␣ Welt " ); string s2 ( " Welt " ); 4 5 6 7 8 9 cout << s1 . compare ( s2 ) << endl ; // " H " < " W " : -1 cout << s1 . compare (6 ,4 , s2 ) << endl ; // " Welt "== " Welt " : 0 10 11 12 13 14 15 16 cout << s1 . compare (6 ,4 , s2 ,0 ,3) << endl ; // " Welt " > " Wel " : 1 cout << ( s1 == s2 ) << endl ; // 0 } 3.13.5 Umwandeln in Zeichen Umwandeln in eine Zeichenkette (die mit 0 beendet wird). const char * c_str (); Rückgabe des Zeichens an Position n. char at ( size_type n ); 118 Kapitel 3: Einführung in C/C++ Der Operator [] liefert ebenfalls ein Zeichen zurück. str_char.cpp: Zeichenumwandlung von Strings 1 2 3 4 5 6 7 8 9 10 int main () { string s ( __FILE__ ); ifstream file ( s . c_str ()); string t ; file >> t ; cout << t << endl ; cout << t . at (3) << endl ; cout << t . c_str () << endl ; } 3.13.6 Suchen in Strings Sucht nach dem ersten Vorkommen von str ab der Position index und gibt die Position zurück, oder npos, falls nichts gefunden wurde. size_type find ( const string & str , size_type index ); Sucht rückwärts nach dem Suchstring. size_type rfind ( const string & str , size_type index ); Sucht nach erstem Vorkommen eines Zeichens aus str. size_type find_first_of ( const string & str , size_type index = 0); Analog: • find_first_not_of sucht nach erstem Nicht-Vorkommen eines Zeichens. • find_last_of sucht nach letztem Vorkommen eines Zeichens. • find_last_not_of sucht nach letztem Nicht-Vorkommen eines Zeichens. 3.13.7 Sonstiges Rückgabe eines Teilstrings. string substr ( size_type index , size_type n = npos ); 3.14 Umgang mit Dateien 119 Anfügen eines Zeichens ans Ende. void push_back ( const char val ); 3.14 Umgang mit Dateien Sind komplexe Daten zu verarbeiten, dann ist die Eingabe über Tastatur mühsam und langwierig. Effizienter ist das Einlesen aus einer Datei. Umgekehrt ist es bei umfangreicheren Ergebnissen angebrachter, sie in eine Datei zu schreiben als auf dem Bildschirm auszugeben. Dafür unterscheidet man zwei Arten von Dateien: – Textdateien: editierbarer „Klartext“, geordnete Zeichenfolgen in ASCII, zeilenweise strukturiert. – Binärdateien: Byteweise Speicherung der Daten (so, wie sie im Computer intern abgelegt werden), geordnete, aber unstrukturierte Zeichenfolge. Wir wollen hier nur Textdateien bearbeiten. Die Standardbibliothek stellt in fstream Stream-Klassen6 für den Dateizugriff zur Verfügung: std::ofstream für die Dateiausgabe und std::ifstream für das Lesen einer Datei. Mit den Elementfunktionen open() und close() kann die Datei geöffnet und geschlossen werden. Dazwischen kann mit dem Operator << bzw. >> in die Datei geschrieben werden, bzw. aus ihr gelesen werden. file.cpp: Datei zum Schreiben öffnen 1 2 3 4 5 6 7 8 9 # include < fstream > using namespace std ; int main () { ofstream output ; output . open ( " test . dat " ); output << " Hallo ␣ Welt ! " << endl ; output . close (); } Wenn ein Objekt einer Klasse erzeugt wird, können automatisch bestimmte Aktionen ausgeführt werden. Erzeugt man ein ofstream-Objekt, kann beispielsweise gleich eine Datei angegeben werden, die geöffnet werden soll. Wird der Gültigkeitsbereich des Objektes verlassen, können ebenfalls automatisch Aktionen durchgeführt werden. Ein ofstream-Objekt schließt dann selbständig die Datei7 . 6 später... 7 Konstruktor, Destruktor 120 Kapitel 3: Einführung in C/C++ file2.cpp: Schnell eine Datei öffnen 1 2 3 4 5 6 7 8 9 10 11 int main () { char c ; cout << " Ausgabe ? ␣ " ; cin >> c ; if ( c == ’j ’) { ofstream output ( " test . dat " ); // test . dat wurde schon geöffnet ! output << " Hallo ␣ Welt ! " << endl ; } // Ende Gü lt igk ei ts ber ei ch output , also close () } Genauso leicht lassen sich Dateien zum Lesen öffnen. Hier ist es noch sinnvoll zu überprüfen, ob die Datei auch geöffnet werden konnte. Die Funktion good() prüft, ob die Datei zum Lesen oder Schreiben bereitsteht. filecheck.cpp: Datei lesen, wenn sie existiert 1 2 int main () { ifstream input ( " test . dat " ); 3 4 5 6 7 8 9 10 11 12 13 if ( input . good ()) { char buf [30]; input >> buf ; cout << buf << endl ; } else { cerr << " Fehler ! " << endl ; // Öffnen ist gescheitert = > Fehlerbehandlung } } Das Programm liest aber nur das erste Wort. Um eine ganze Datei auszulesen, lesen wir in einer Schleife solange, bis wir das Dateiende erreicht haben. Auch das lässt sich mit good() überprüfen. Alternativ kann auch der !-Operator verwendet werden, der das Stream-Objekt bewertet und true zurückgibt, wenn ein Fehler auftrat. filecheck2.cpp: Datei komplett auslesen 1 2 3 4 5 6 7 8 9 10 int main () { ifstream input ( " test . dat " ); if (! input ) { cerr << " File ␣ not ␣ found ! " << endl ; return 1; } while ( true ) { char buf [30]; input >> buf ; 3.14 Umgang mit Dateien 11 12 13 14 if (! input ) break ; cout << buf << endl ; } } 121 122 Anhang A: Header A Header A.1 Die Funktionen von ctype.h Nützliche Funktionen zur Behandlung von char-Variablen und -Konstanten findet man in dem Modul <ctype.h>. Prototyp Funktionsweise int isalnum(int) wahr für einen Buchstaben oder eine Zahl, also a–z, A–Z, 0–9 oder umgebungsspezifische Buchstaben (in Deutschland ä, Ä, ö, Ö, ü, Ü, oder ß). int isalpha(int) wahr für einen Buchstaben, also a–z, A–Z, oder umgebungsspezifische Buchstaben. int iscntrl(int) wahr für Steuerzeichen FF, NL, CR, HT, VT, BEL, BS. int isdigit(int) wahr für Dezimalziffern 0–9. int isgraph(int) wahr für darstellbare Zeichen außer Leerzeichen. int islower(int) wahr für Kleinbuchstaben. int isupper(int) wahr für Großbuchstaben. int isprint(int) wahr für darstellbare Zeichen inklusive Leerzeichen. int isxdigit(int) wahr für hexadezimale Ziffern, also 0–9,a–f,A–F. int tolower(int) Umwandlung in Kleinbuchstaben, x = tolower(a) wandelt a in den entsprechenden Kleinbuchstaben, ansonsten wird a zurückgegeben. int toupper(int) Umwandlung in Großbuchstaben. A.2 Die Funktionen von string.h 123 A.2 Die Funktionen von string.h Weitere nützliche Funktionen zum Umgang mit C-Strings liefert <string.h>. Prototyp Funktionsweise memchr Zeichen im Speicherblock suchen memcmp Speicherblöcke vergleichen memcpy Speicherblöcke kopieren memmove Sicheres Kopieren von Speicherblöcken memset Speicherblock initalisieren strcat String an einen anderen hängen strchr Zeichen im String suchen strcmp Zwei Strings vergleichen strcoll Zwei Strings umgebungsabhängig vergleichen strcpy String kopieren strerror Umwandlung eines Fehlers in verbale Form strlen Länge des Strings ermitteln strncat Teil eines Strings an einen anderen hängen strncmp Teile von zwei Strings vergleichen strncpy Teile eines Strings kopieren strrchr Zeichen vom Stringende aus suchen strstr prüft, ob String Teil eines anderen ist 124 Anhang A: Header A.3 Mathematische Funktionen Von C++ werden in dem Modul <cmath> die Prototypen für eine Reihe mathematischer Standardfunktionen zur Verfügung gestellt, die man mit dem #include-Kommando in das Programm einfügen kann. Prototyp Funktionsweise double acos(double) Arcus Kosinus: Umkehrfunktion des Kosinus im Bogenmaß im Intervall [0, π]. w=acos(a); ⇐⇒ w = arccos(a) double asin(double) Arcus Sinus: Umkehrfunktion des Sinus im Bogenmaß im Intervall [−π/2, π/2]. w=asin(a); ⇐⇒ w = arcsin(a) double atan(double) Arcus Tangens: Umkehrfunktion des Tangens im Bogenmaß. w=atan(a); ⇔ w = arctan(a), w ∈ [−π/2, π/2] double atan2(double, double) Umkehrfunktion des Tangens im Bogenmaß. w=atan2(a,b); ⇔ w = arctan( ab ), w ∈ [−π, π] double ceil(double) Obere Gaußklammer: Nächstgrößere Ganzzahl w=ceil(a); ⇐⇒ w = dae double cos(double) Kosinus: Berechnung des Kosinus mit Argument im Bogenmaß w=cos(a); ⇐⇒ w = cos(a) double cosh(double) Kosinus Hyperbolicus: Berechnung des Kosinus Hyperbolicus mit Argument im Bogenmaß w=cosh(a); ⇐⇒ w = cosh(a) double exp(double) Exponentialfunktion: w=exp(a); ⇐⇒ w = ea double fabs(double) Absolutwert: w=fabs(a); ⇐⇒ w = |a| double floor(double) Untere Gaußklammer: Nächstkleinere Ganzzahl w=floor(a); ⇐⇒ w = bac double fmod(double, double) Modulofunktion: analog zu a%b double frexp(double, int *) Aufspaltung von a in a = f · 2i , f ∈ [0.5, 1) wird zurückgegeben, i in b gespeichert f=frexp(a,&b); ⇐⇒ a = f · 2b w=fmod(a); ⇐⇒ w = a mod b A.3 Mathematische Funktionen 125 Prototyp Funktionsweise double ldexp(double, int) Umkehrfunktion zu frexp. w=ldexp(a,b); ⇐⇒ w = a · 2b double log(double) Logarithmus Naturalis: Logarithmus zur Basis e w=log(a); ⇐⇒ w = ln(a) = loge (a) double log10(double) Dekadischer Logarithmus: Logarithmus zur Basis 10 w=log10(a); ⇐⇒ w = log10 (a) double modf(double, double *) Aufspaltung einer Zahl a in Ganzahlanteil i und Rest. f = a − i, f ∈ [0, 1) wird zurückgegeben, i ∈ Z in b gespeichert. f=modf(a,&b); ⇐⇒ a = b + f double pow(double, double) Potenzfunktion: double sin(double) Sinus: Berechnung des Sinus mit Argument im Bogenmaß w=pow(a,b); ⇐⇒ w = ab w=sin(a); ⇐⇒ w = sin(a) double sinh(double) Sinus Hyperbolicus: Berechnung des Sinus Hyperbolicus mit Argument im Bogenmaß w=sinh(a); ⇐⇒ w = sinh(a) double sqrt(double) Quadratwurzel: w=sqrt(a); ⇐⇒ w = double tan(double) √ a Tangens: Berechnung des Tangens mit Argument im Bogenmaß w=tan(a); ⇐⇒ w = tan(a) double tanh(double) Tangens Hyperbolicus: Berechnung des Tangens Hyperbolicus mit Argument im Bogenmaß w=tanh(a); ⇐⇒ w = tanh(a) 126 Anhang B: Kompatibilitäten zu C B Kompatibilitäten zu C B.1 Ein-/Ausgabe Unter C wird die Kommunikation mit der Konsole im Modul <stdio.h> erklärt. Die Funktion printf() dient zur formatierten Ausgabe und scanf() zur formatierten Eingabe. Ein Formatierungsstring gibt jeweils an, wie die Ein- oder Ausgabe erfolgen soll: int printf ( const char * format , Typ Name , ...); int scanf ( const char * format , Typ & Name , ...); # include < stdio .h > int main () { int i ; printf ( " Bitte ␣ Zahl ␣ eingeben : ␣ " ); scanf ( " % d " , & i ); double k = 1./ i ; printf ( " Der ␣ Kehrwert ␣ von ␣ % d ␣ ist ␣ % f \ n " , i , k ); } Der Formatierungsstring kann neben normalem Text Formatierungsanweisungen enthalten, die mit % eingeleitet werden. Für jede dieser Formatierungsanweisungen wird eines der weiteren Argumente vom Aufruf von printf bzw. scanf verarbeitet. Eine Formatierungsanweisung besteht aus %[ Flags ][ Feldbreite ][. Genauigkeit ] Typspezifizierer B.1 Ein-/Ausgabe Flag Bedeutung 0 Auffüllen mit Nullen - Linksbündig + Vorzeichen (+ oder -) immer mit ausgeben ’ ’ Platzhalter für Vorzeichen Typspezifizierer erlaubter Typ Ausgabe d, i int Ganzzahlen u unsigned int Ganzzahlen f double Fließkommazahlen in normaler Darstellung e double Fließkommazahlen in wiss. Darstellung c unsigned char Zeichen s const char * C-String o int Oktalzahlen x, X int Hexadezimalzahlen 127 B.1.1 Formatierungsanweisungen für Ganzzahlen Rechtsbündige Ausgabe mit 6 (Leer-)Stellen printf ( " Wert ␣ 1 ␣ = ␣ %6 i \ n " , 42); printf ( " Wert ␣ 2 ␣ = ␣ %6 i \ n " , 42424); Wert 1 = Wert 2 = 42 42424 Benötigt die Variable mehr Platz als im Format angegeben ist, werden trotzdem alle Ziffern angezeigt, wobei aber das Format nicht mehr berücksichtigt wird. printf ( " Wert ␣ 2 ␣ = ␣ %1 i \ n " , 42424); Wert 2 = 42424 128 Anhang B: Kompatibilitäten zu C Linksbündige Ausgabe mit 6 (Leer-)Stellen printf ( " Wert ␣ 1 ␣ = ␣ % -6 i \ n " , 42); printf ( " Wert ␣ 2 ␣ = ␣ % -6 i \ n " , 42424); Wert 1 = 42 Wert 2 = 42424 Rechtsbündige Ausgabe mit 6 Stellen, Auffüllen mit Nullen printf ( " Wert ␣ 1 ␣ = ␣ %06 i \ n " , 42); printf ( " Wert ␣ 2 ␣ = ␣ %06 i \ n " , 42424); Wert 1 = 000042 Wert 2 = 042424 Immer das Vorzeichen mit ausgeben printf ( " Wert ␣ 1 ␣ = ␣ %+ i \ n " , 42); printf ( " Wert ␣ 1 ␣ = ␣ %+ i \ n " , -42); Wert 1 = +42 Wert 2 = -42 Ausgabe von „%“ printf ( " Wert ␣ 1 ␣ = ␣ % i %%\ n " , 42); Wert 1 = 42% Ausgabe von Zahlen im Oktal- oder Hexadezimalformat printf ( " Wert ␣ dezimal ␣ = ␣ % i \ n " , 2748); printf ( " Wert ␣ oktal ␣ = ␣ % o \ n " , 2748); printf ( " Wert ␣ hexadezimal ␣ = ␣ % X \ n " , 2748); Wert dezimal = 2748 Wert oktal = 5274 Wert hexadezimal = ABC B.1 Ein-/Ausgabe 129 B.1.2 Formatierungsanweisungen für Fließkommazahlen Die Ausgabe von Fließkommazahlen kann in Fixpunktnotation (z.B. x=123.456) oder in Exponentialschreibweise (z.B. x=0.123456e-2) erfolgen, wobei angegeben werden kann, wieviele Ziffern vor bzw. nach dem Dezimalpunkt verwendet werden sollen. printf.cpp: Fließkommazahlen mit printf 1 # include < stdio .h > 2 3 4 5 6 7 char format [][10] = { " % f " ," %15.2 f " , " %7.2 f " , " %9.4 f " , " %6.4 f " , " %.3 f " , " % -15.2 f " , " % -+15.2 f " , " %+15.2 f " , " ␣ " ," % e " ," %7.3 e " ," %9.3 e " ," %17.3 e " ," %17.8 e " , " -%9.3 e " ," -%9.4 e " ," % -+15.2 e " ," %+15.2 e " , 0}; 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 int main () { float x =2367.12864; printf ( " ␣ ␣ Format ␣ ␣ | ␣ ␣ Ausgabe \ n " ); printf ( " - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\ n " ); for ( int i =0; format [ i ][0]; i ++) { printf ( " ␣ % -8 s ␣ | ␣ " , format [ i ]); printf ( format [ i ] , x ); printf ( " \ n " ); // // Das gleiche , nur ausf " uhrlicher ... // printf ( " ␣ % -8 s ␣ | ␣ " ," % f " ); printf ( " % f " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," %15.2 f " ); printf ( " %15.2 f " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," %7.2 f " ); printf ( " %7.2 f " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," %9.4 f " ); printf ( " %9.4 f " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," %6.4 f " ); printf ( " %6.4 f " ,x ); printf ( " \ n " ); 130 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 Anhang B: Kompatibilitäten zu C printf ( " ␣ % -8 s ␣ | ␣ " ," %.3 f " ); printf ( " %.3 f " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," % -15.2 f " ); printf ( " % -15.2 f " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," % -+15.2 f " ); printf ( " % -+15.2 f " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," %+15.2 f " ); printf ( " %+15.2 f " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," ␣ " ); printf ( " ␣ " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," % e " ); printf ( " % e " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," %7.3 e " ); printf ( " %7.3 e " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," %9.3 e " ); printf ( " %9.3 e " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," %17.3 e " ); printf ( " %17.3 e " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," %17.8 e " ); printf ( " %17.8 e " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," -%9.3 e " ); printf ( " -%9.3 e " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," -%9.4 e " ); printf ( " -%9.4 e " ,x ); printf ( " \ n " ); printf ( " ␣ % -8 s ␣ | ␣ " ," % -+15.2 e " ); printf ( " % -+15.2 e " ,x ); B.1 Ein-/Ausgabe printf ( " \ n " ); 91 92 93 94 95 96 97 98 131 printf ( " ␣ % -8 s ␣ | ␣ " ," %15.2 e " ); printf ( " %+15.2 e " ,x ); printf ( " \ n " ); } Format | Ausgabe -----------------------------%f | 2367.128662 %15.2f | 2367.13 %7.2f | 2367.13 %9.4f | 2367.1287 %6.4f | 2367.1287 %.3f | 2367.129 %-15.2f | 2367.13 %-+15.2f | +2367.13 %+15.2f | +2367.13 | %e | 2.367129e+03 %7.3e | 2.367e+03 %9.3e | 2.367e+03 %17.3e | 2.367e+03 %17.8e | 2.36712866e+03 -%9.3e | -2.367e+03 -%9.4e | -2.3671e+03 %-+15.2e | +2.37e+03 %+15.2e | +2.37e+03 B.1.3 Eingabe mit scanf Mit der Funktion scanf können Werte von der Tastatur eingelesen werden. Die Funktion erzeugt aber keine Ausgabe und muss zum sinnvollen Einlesen mit printf kombiniert werden. Während printf der Wert einer Variablen übergeben wird, der mit dem Formatierungsstring ausgegeben werden soll, verarbeitet scanf die Adresse der Variablen, also die Stelle im Speicher, wo die Variable abgelegt ist. Deshalb muss beim Argument der Adress-Operator & benutzt werden. Falsche Eingaben (z.B. falscher Typ oder overflow) erzeugen keinen Programmabbruch, sondern es wird mit einem fehlinterpretierten also falschen Wert weitergerechnet. 132 Anhang B: Kompatibilitäten zu C B.1.4 Ein-/Ausgabe in C-Strings Mit sprintf und sscanf kann man in C-String schreiben oder aus C-Strings lesen (statt auf den Bildschirm bzw. von der Tastatur): char s [20]; sprintf (s , " % i ␣ % f " , 42 , 3.1415); // s == "42 3.1415" sscanf (s , " % i ␣ % f " , &j ,& p ); // j == 42 , p == 3.1415 B.1.5 Einzelne Zeichen Auch einzelne Zeichen können verarbeitet werden: int getchar () // Ein Zeichen einlesen int putchar ( int c ) // Ein Zeichen ausgeben B.2 Speicherverwaltung Die Operatoren new und delete sind nur unter C++ verfügbar. Echte CProgramme müssen deshalb auf Funktionen der <stdlib.h> zugreifen, um dynamisch Speicher zu allozieren oder freizugeben. void void void void * malloc ( size_t groesse ); * calloc ( size_t anzahl , size_t groesse ); free ( void * ptr ); * realloc ( void * ptr , size_t groesse ); Die Funktion calloc() stellt einen Zeiger auf den Anfang eines Feldes mit anzahl Komponenten der Größe groesse bereit und initialisiert alle Feldkomponenten mit binären Nullen; malloc stellt einen Zeiger auf einen Speicherbereich der angegebenen Größe bereit (ohne Initialisierung). Beide liefern den Nullzeiger zurück, wenn der angeforderte Speicherplatz nicht zur Verfügung steht. Sinnvollerweise sollte man nicht benötigten Speicher mit der Funktion free() wieder freigeben. Der angegebene Zeiger muss von einem vorherigen Aufruf der drei anderen Funktionen geliefert und noch nicht wieder freigegeben worden sein. realloc() kann man sich als Kombination von free() und malloc() vorstellen: Wenn ptr der Nullzeiger ist, wirkt die Funktion wie malloc(). Wenn ptr nicht der Nullzeiger, dafür aber groesse=0 ist, wirkt realloc() wie free(). B.2 Speicherverwaltung 133 Andernfalls wird der durch ptr angesprochene Speicherbereich in Abhängigkeit von groesse verkleinert oder vergrößert, wobei in nicht freigegebenen Bereichen der Inhalt unverändert bleibt. Zu realloc() gibt es in C++ übrigens keine direkte Entsprechung. Mit new angelegter Speicherplatz kann in der Größe nicht verändert werden. int * feld ; feld = ( int *) ( malloc ( n * sizeof ( int ) )); feld = ( int *) ( calloc ( n , sizeof ( int ) )); feld = ( int *) ( realloc ( feld , 2* n * sizeof ( int ) )); free ( feld ); Der in den Prototypen angegebene, typenlose Zeiger auf void muss mithilfe eines cast-Operators in den Typ umgewandelt werden, für den Speicher belegt werden soll. 134 Anhang B: Programmverzeichnis Programmverzeichnis zero.cpp Das nullte und kürzeste C++-Programm . . . . hello.cpp Hello world in C++, erster Versuch . . . . . . hello1.cpp Hello world in C++, zweiter Anlauf . . . . . . hello2.cpp Ein fast perfektes Hello world . . . . . . . . . . declare.cpp Deklaration von Variablen . . . . . . . . . . . declare2.cpp kürzere Deklaration . . . . . . . . . . . . . . . rechnen.cpp Rechnen mit ganzen Zahlen . . . . . . . . . . . genauigkeit.cpp Rundungsfehler . . . . . . . . . . . . . . . . . eingabe.cpp Eingabe mit std::cin . . . . . . . . . . . . . . fehler.cpp Fehlerausgabe mit std::cerr . . . . . . . . . . logik.cpp Intervalltest . . . . . . . . . . . . . . . . . . . maximum.cpp Maximums-Bestimmung . . . . . . . . . . . . . eingabe–test.cppTest der Eingabe . . . . . . . . . . . . . . . . . switch.cpp Switch . . . . . . . . . . . . . . . . . . . . . . quadrat.cpp Ein einfacher Funktionsaufruf . . . . . . . . . quadrat1.cpp Falsch - Die Funktion ist unbekannt . . . . . . quadrat2.cpp Richtig - Prototyp deklariert die Funktion . . . return.cpp Rückgabetypen . . . . . . . . . . . . . . . . . . default.cpp Parabel mit Default-Argumenten . . . . . . . . callvalue.cpp call by value . . . . . . . . . . . . . . . . . . . callref.cpp call by reference . . . . . . . . . . . . . . . . . lokal.cpp Lokale Variablen . . . . . . . . . . . . . . . . . global.cpp Globale Variablen . . . . . . . . . . . . . . . . statisch.cpp Statische Variablen . . . . . . . . . . . . . . . rekursiv.cpp Fakultät als rekursive Funktion . . . . . . . . . rekursiv2.cpp Fakultät als kurze rekursive Funktion . . . . . . wurzel.cpp Die Mathe-Bibliothek . . . . . . . . . . . . . . degrad.cpp Bogen- und Gradmaß . . . . . . . . . . . . . . while.cpp Eingabe mit Überprüfung als while-Schleife . . dowhile.cpp Eingabe mit Überprüfung als do-while-Schleife while2.cpp while ohne Anweisungsblock . . . . . . . . . . . iterativ2.cpp Iterative Version von Fakultät mit while . . . iterativ.cpp Iterative Version von Fakultät mit for . . . . . continue.cpp Die Schleife läuft mit continue weiter . . . . . break.cpp Die Schleife endet mit break sofort . . . . . . prim.cpp Primzahltest . . . . . . . . . . . . . . . . . . . goto.cpp goto höchstens zur Fehlerbehandlung . . . . . . feld.cpp Felder sind keine lokalen Variablen . . . . . . . formatint.cpp Formatierung von Ganzzahlen . . . . . . . . . formatflt.cpp Formatierung von Fließkommazahlen . . . . . . leibniz.cpp Approximation von π mit Leibniz-Verfahren . . kehrwert.cpp Beliebige viele Kehrwerte . . . . . . . . . . . . pointer.cpp Zeiger auf Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 52 52 53 54 55 56 59 63 64 66 67 68 69 71 71 71 72 73 73 75 75 76 77 78 78 78 79 80 80 81 81 82 83 84 84 85 87 92 93 95 98 99 135 reference.cpp pointerret.cpp matrix.cpp matrix2.cpp zeigerfkt.cpp pointerar.cpp pointerar2.cpp pointer3.cpp memory2.cpp memory.cpp ascii.cpp ctype.cpp zeichen.cpp zeichen2.cpp zeichen3.cpp convert.cpp arg.cpp str_konstr.cpp str_manip.cpp str_info.cpp str_cmp.cpp str_char.cpp file.cpp file2.cpp filecheck.cpp filecheck2.cpp printf.cpp (De-)Referenzierung von Zeigern . . . . Rückgabe mehrerer Werte . . . . . . . . Matrixoperationen . . . . . . . . . . . . Speicher für Matrix reservieren . . . . . Zeiger auf Funktionen . . . . . . . . . . Zeigerarithmetik . . . . . . . . . . . . . Vergleich von Zeigern . . . . . . . . . . Vergleich von Zeigern . . . . . . . . . . Sichere Allozierung mit Exceptions . . . Sichere Allozierung mit Nullzeigern . . Erzeugen einer (lokalen) ASCII-Tabelle Beispielprogramm zu ctype.h . . . . . Zeichen einzeln ausgegeben . . . . . . . Einlesen einer ganzen Zeile . . . . . . . Zeichenlänge . . . . . . . . . . . . . . . Umwandlung von Zeichenkette in Zahl . Argumente für das Programm . . . . . . Erstellen von Strings . . . . . . . . . . Verändern von Strings . . . . . . . . . Informationen über String-Längen . . . Vergleichen von Strings . . . . . . . . . Zeichenumwandlung von Strings . . . . Datei zum Schreiben öffnen . . . . . . . Schnell eine Datei öffnen . . . . . . . . Datei lesen, wenn sie existiert . . . . . Datei komplett auslesen . . . . . . . . . Fließkommazahlen mit printf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 100 102 103 104 105 105 106 106 107 108 108 110 111 112 112 113 114 115 116 117 118 119 120 120 120 129