Organisatorisches

Sie haben 2 Wochen Zeit, eine Übung abzugeben. Der genaue Termin steht am Angabezettel der Übung. Für einen positiven Abschlu\xa7 des Praktikums müssen Sie

Pro Übung sind maximal 24 Punkte zu erreichen. Auch das Programmierprojekt wird mit maximal 24 Punkten bewertet; es hat daher gleich viel Gewicht wie der Durchschnitt der fünf besten abgegebenen Übungen. Am Ende des Semesters findet ein Kolloquium statt.

Abgabe der Übung mit Deckblatt (= Angabe), vollständigem Programmcode (Header-Files und Implementation-Files) sowie Bildschirmdialog bis zum Ende des Praktikums am jeweiligen Rückgabetag. Auf sinnvolle Testfälle achten! Keine Disketten abgeben. Eine Übung gilt dann als abgegeben, wenn zumindest der Kern einer Aufgabe gelöst wurde.

Zur Beurteilung dient folgender Punkteschlüssel:

Tabelle 1: Punkte und Noten
Punkte Gesamtpunkte Note
SGT 124 - 22:(x 2 = 48-44)
GUT 221 - 19:(x 2 = 43- 38)
BEF 318 - 16:(x 2 = 37- 32)
GEN 415 - 12: (x 2 = 31- 24)
NGD 5< 12:(x 2 = < 24)
Die Termine des PK C++ im SS 96:

18. April Ü1, 25. April Ü2, 2. Mai Ü3, 9. Mai Ü4, 23. Mai Ü5, 30. Mai Ü6 und Programmierprojekt

Namenskonventionen

Grundsätzlich sollten englische Bezeichnungen verwendet werden.

Variablennamen beginnen mit einem Kleinbuchstaben. Zur semantischen Trennung innerhalb eines Namens werden Großbuchstaben verwendet z.B.

    int noOfDays;
Globale Variablen beginnen mit einem g, z.B.

    extern System gSystem;
Enumerationstypen werden wie folgt deklariert

    enum Signal {
           eSigBus,
           eSigSegmentationViolation,
           eSigSystem,
           eSigPipe,
           eSigIllegalInstruction,
           eSigQuit,
         };
Funktions-/Methodennamen beginnen mit einem Großbuchstaben, z.B.

    GetNumberOfDays();
Klassennamen beginnen mit einem Großbuchstaben, Instanzennamen mit einem Kleinbuchstaben, z. B.

    class PrintManager: public Object {
         protected:
           OrderedCollection *printers;
           Printer *currentPrinter;
         public:
           PrintManager();
           ~PrintManager();
           void InstallPrinter (char *name);
           virtual void Print (char *file, int from, int to);

Literatur

Bücher

Zeitschriften

Internet

In folgenden news-groups wird unter anderem über C++ diskutiert:

Für Fragen über sämtliche news-Gruppen können Sie den Server www.dejanews.com verwenden. Dort sind news-Artikel des letzten halben Jahres gespeichert und abfragbar.

Das Beispiel "Hello World"

Hello World

#include <iostream.h> //cout
#include <stdlib.h> //atoi
#define usageString "Usage:\n"
 
void PrintHello(ostream &os=cout);
void PrintEnvironment(char **);
 
int main(int argc, char *argv[], char *envp[])
{
        int i, numberOfHellos = (argv[1] != 0 ? atoi(argv[1]) : 0);
        while (!numberOfHellos) {
          cout << usageString;
          cout << argv[0] << " numberOfHellos" << 
endl;
          cout << "\nEnter number of hellos: ";
          cin >> numberOfHellos;
        }
        for (i=0; i < numberOfHellos; i++){
          int j;
          for (j=0; j<i; j++){
            cout << " ";
          }
          PrintHello();
          //cout << "\na second PrintHello on cerr\n";
          //PrintHello(cerr);
        }
        for (i=0; i < numberOfHellos; i++){
          int j;
          for (j = numberOfHellos; j > i; j--){
            cout << " ";
          }
          cout << "hello world! " << endl;
        }
        PrintEnvironment(envp);
        return 0;
};
 
void PrintHello(ostream &os)
{
        os << "hello world! " << endl;
}
 
void PrintEnvironment(char **envp)
{
        int i = 0;
        while (envp[i] != `\0') {
          cout << endl << "envp[" << i << 
"] = " << envp[i];
          i++;
        }
}


Das Beispiel "Hello World" in C++

Eine mögliche Implementierung eines objektorientierten "Hello-World".

Hello World in C++

hello.h

// sample class hello
#include "../object/object.h"
class Hello : public Object {

private:
  int number;

public:

  Hello(int numberOfHellosToPrint);
  ~Hello(); //destructor of object is virtual
  void PrintHellos();
};
hello.c

#include <iostream.h>
#include "hello.h"

Hello::Hello(int no) : Object(sizeof(this))
{
  number= no;
}

Hello::~Hello()
{
}

void Hello::PrintHellos()
{

  cout << "\nPrinting " << number << " 
`Hellos"';
  for (int i=0; i < number; i++) {
     cout << "\n";
            for (int j=0; j < i; j++) {
                cout << " ";
            }
            cout << "hello";
        }
}
main.cc:

//test of class hello
Hello *helloPointer  = new Hello(atoi(argv[1]));
Hello helloStatic(7);

helloStatic.PrintHellos();
helloPointer->PrintHellos();

...
delete helloPointer;

Der Präprozessor

Der Präprozessor ist ein eigenes Programm, das vor dem eigentlichen Übersetzungslauf gestartet wird und den Sourcecode gemäß den #-Anweisungen ändert. Im wesentlichen übernimmt der Präprozessor

Besonders wichtig ist somit die Dateiinklusion, da C++ (sowie C) vom Sprachkonzept her die Bildung von Modulen nicht unterstützt. Mit

#include <iostream.h> 
bzw.

#include "myFile.h"
werden die jeweiligen Dateien inkludiert (d.h. der Sourcecode wird geändert). Spitze Klammern bedeuten dabei Dateien, die im Standard-Include-Pfad stehen; werden die zu inkludierenden Dateien unter Hochkomma gestellt, so wird zuerst das lokale Verzeichnis durchsucht und erst dann im Standard-Include-Pfad nachgeschaut.

Die zweite wesentliche Verwendung des Präprozessors besteht in der Definition von Makros, die durch das Schlüsselwort #define angezeigt wird, also etwa

#define MC68882
#define PI 3.141592
#define sqrt(a) (a*a)
Es wird also ein Makroname mit einem Text verknüpft, im ersten Fall daher mit einem leeren Text. Solche Anweisungen sind aber dennoch sinnvoll: z.B. bei der Bildung von Modulen:

#ifndef Person_H
#define Person_H
class Person {
...
};
#endif
In diesem Fall werden vom Präprozessor die Anweisungen zwischen class und }; nur dann (textuell) in den Sourcecode eingefügt, falls das Makro Person_H noch nicht definiert ist. Auf diese Weise werden in C++ Module `simuliert'.

Makros sollten nicht für die Definition von Konstanten verwendet werden (z.B. PI). C++ bietet dafür das Konstrukt const. Der wesentliche Unterschied zwischen

#define PI 3.14
und

const double PI = 3.14
besteht darin, daß die Makrodefinition nur eine textuelle Ersetzung der Zeichenkette PI mit 3.14 vornimmt, die zweite Definition aber eine konstante double-Variable mit Initialwert 3.14 erzeugt.

Makros können auch parametrisiert werden, wie z. B. in

#define sqr(a) (a*a)
wobei man allerdings auf folgendes achten muß

a = sqr(r)*PI;
wird expandiert zu

a=r*r*Pi;
wenn man allerdings etwa sqr(a-b) haben will, so wird dieser ausdruck in (a-b*a-b) expandiert, was ja wahrscheinlich nicht gewünscht ist. Ausweg: Parameter klammern, also

#define sqr(a) ((a)*(a))
Im allgemeinen sollte man aber mit solchen Makros sparsam umgehen.

Makros werden mit dem Zeilenende abgeschlossen. Will man mehrzeilige Makros, so muß man vor dem Ende der Zeile den `Backslash' (\) einfügen. Bereits bestehende Makrodefinitionen können auch wieder `wegdefiniert' werden

#undef MC68882
Der Aufruf des Präprozessors sowie der darauf folgenden Compile- und Link-Läufe werden unter Unix bwz. DOS durch ein sogenanntes Makefile gesteuert. Untenstehende Abbildung gibt ein Beispiel für ein Makefile für SunOS 4.1.3 und den GNU C++ 2.6.3 Compiler. Programmierumgebungen erlauben im allgemeinen das Einstellen der für die Übersetzung wichtigen Parameter über eigene Bildschirmdialoge. Der Benutzer merkt daher vom Makefile nichts.

Makefile

CCFLAGS =
LDOPTIONS = -g -Ur
LIBDIR  = /usr/local/lib
#LIBS = /usr/lib/libm.a
#/usr/lib/libma.a is library for mathematical functions

PROGRAM = hello
.SUFFIXES: .cc
CC      =   g++ $(CCFLAGS)
MAKE    =   make
LDFLAGS =   $(LDOPTIONS) -L$(LIBDIR)
.cc.o:

$(CC) $(CCFLAGS) -c $<
OFILES  =   \
             main.o

all:		$(PROGRAM)

$(PROGRAM):     $(OFILES)
$(CC) $(LDFLAGS) -o $@ $(OFILES) $(LIBS)

clean:
    rm -f core *.o *..c

clobber:    clean
    rm -f $(PROGRAM) makefile.bak .MAP/*.map

Datentypen

Deklarationen und Definitionen

Die Unterscheidung zwischen Deklarationen und Definitionen ist aus folgendem Grund wesentlich: Deklarationen geben dem Compiler Name und Typ von Variablen und Funktionen bekannt, definieren aber keinen Programmcode. Dies wird durch Definitionen gemacht. Beispiel
double func (double a, double b);
ist eine Deklarationen der Funktion func. Mit
double func (double a, double b)
{
return a * b;
}
wird die Funktion func definiert. Bei der Deklaration wird also der Funktionsrumpf {...} durch einen Strichpunkt ersetzt. Wichtig ist diese Unterscheidung für den Compiler: gibt es eine Deklaration für eine Funktion so nimmt der Compiler an, daß irgendwann später auch eine Definition erfolgt. Bis zur eigentlichen Definition kann die Funktion dann aber `verwendet' werden, d.h. man kann sie aufrufen, usw. Bei der Verwendung von externen Variablen ist es teilweise unumgänglich Deklarationen und nicht Definitionen zu verwenden, da Definitionen zu einer Neudefinition führen würden, was ja im allgemeinen nicht gewünscht ist (siehe auch "Speicherklassenattribute" auf Seite 27).

Fundamentale Datentypen

C++ kennt die fundamentalen Datentypen char, int, double, float und void.
Der Datentyp char wird für einzelne Zeichen verwendet. Er verbraucht genau ein Byte Speicherplatz (siehe auch Tabelle "Datentyp und Speicherplatz (UNIX)" auf Seite 22). Konstante Zeichen werden unter einfachen Hochkommas angeführt, z. B.
`a', `1'
Steuerzeichen werden von einem `Backslash' (\) angeführt
\n    // newline
\t    // Tabulator
\b    // backspace
\'    // einfaches Hochkomma
Zeichen können auch numerisch angegeben werden und zwar als Oktalzahl oder als Hexadezimalzahl
//normal, octal und hexadezimal
char ch= `a';
cout << "\nch = " << ch;
ch= `\141';   //octal
cout << "\nch = " << ch;
ch= `\x061';  //hexadezimal
cout << "\nch = " << ch;
führt zur Ausgabe von
a = a
a = a
a = a
int ist der grundlegende ganzzahlige Datentyp. Durch Voranstellen der verschiedenen Speicherklassenattribute, kann der Wertebereich von Integern beeinflußt werden (siehe Tabelle "Datentyp und Speicherplatz (UNIX)" auf Seite 22). Dieser Wertebereich ist in C++ maschinenabhängig, immer gilt jedoch
1 = sizeof(char) <= sizeof(short) <= sizeof(int) <= 
sizeof(long)
Auch Aufzähldatentypen (Enumerationstypen) sind in C++ möglich:
enum boolean {false, true};
Dabei wird den einzelnen Werten ein Integerwert beginnend bei Null zugeordnet. Dies kann aber auch umgangen werden
enum figur {bube=2, dame, koenig, as = 11};
In diesem Fall werden den nicht explizit zugeordneten Konstanten der Reihe nach aufsteigende Integerwerte zugeordnet (im Beispiel also 3 und 4). Die Reihe muß aber nicht unbedingt aufsteigend sein, auch folgendes ist möglich
enum figur {bube=2, dame, koenig=dame, as=1};
figur karte = bube;
...
Die Gleitkommadatentypen in C++ heißen float und double. float besteht aus einem ganzzahligen Teil, dem Dezimalpunkt, dem Fließkommateil und einem optionalen Exponenten (zum Wertebereich siehe "Datentyp und Speicherplatz (UNIX)" auf Seite 22).
<Ganzzahlteil> "." [<Bruchteil>] [["e"|"E"] 
["+"|"-"] <Exponent>]
Datentypen

float pi = 3.14;
double d = -200.4711E+47;
Der Datentyp void bezeichnet in C++ den leeren Wertebereich. void wird auch als der Antidatentyp bezeichnet. void zeigt an, daß kein Wert vorhanden ist. Verwendet wird void bei parameterlosen Funktionen bzw. zum Verwerfen von Rückgabewerten. Beispiele
void main();
void func(void);

Höhere Datentypen

Zu den höheren (auch abgeleiteten) Datentypen in C++ zählen Konstanten, Zeiger, Referenzen, Felder, Strukturen, Variantenstrukturen und natürlich selbstdefinierte Datentypen und Klassen.
Konstante werden durch das Schlüsselwort const deklariert. Wie der Name bereits sagt, soll eine Konstante genau einmal mit einem Wert belegt werden und ist ab dann unveränderbar. Globale Konstante (= Konstante, die außerhalb eines Blockes definiert werden) gelten implizit als statisch deklariert. Zu Konstanten siehe auch "Speicherklassenattribute" auf Seite 27.
Zeigertypen werden zur Manipulation von Adressen von Variablen verwendet. Durch `*' wird ein `normaler' Datentyp zum Zeigerdatentyp. Durch den Adreßoperator `&' wird einem Zeiger die Adresse einer Variable zugewiesen.
int *iPtr, i = 4711;
iPtr = &i;
Hiebei werden zwei Variablen angelegt. Ein Zeiger auf einen Integer (*iPtr) sowie ein Integer i mit Initialwert 4711. Man beachte, daß sich `*' nur auf den jeweils direkt folgenden Variablenbezeichner bezieht. Im zweiten Statement wird dem Integerzeiger iPtr die Adresse des Integers i zugewiesen. iPtr zeigt also jetzt auf den gleichen Speicherplatz, in dem der Wert 4711 steht. Durch `*' kann der Wert eines Zeigers (= das, wo der Zeiger hinzeigt) geholt werden. Die Zeile
cout << endl <<"i = " << i << ", und iPtr 
= " << *iPtr << endl;
führt also zur Ausgabe von
i = 4711, und iPtr = 4711
Abbildung 1 soll diesen Zusammenhang nochmals veranschaulichen.

Integer und Referenzen

Zur Verwendung von Zeigern siehe auch das Beispiel "Swap-Integer" auf Seite 37.
Referenzen werden mit `&' gekennzeichnet. Eine Initialisierung ist notwendig. Man kann sich Referenzen als konstante Zeiger bzw. als alias auf Objekte vorstellen. Sie werden bei ihrer Verwendung automatisch (also ohne `*') dereferenziert.
Referenzen

int i = 3;
int &r = i;

cout << "\ni = " << i << " und r = " << r 
<< "\n";
// i = 3 und r = 3
r = 44;
cout << "\ni = " << i << " und r = " << r 
<< "\n";
// i = 44 und r = 44
Im Unterschied zu Zeigern stellen Referenzen keine eigenen Datentypen dar, eine Referenz auf eine Referenz ist also nicht möglich
int &&rr = ... // nicht möglich
Zur Verwendung von Referenzen siehe "Funktionen und Übergabe von Parametern" auf Seite 36.
Felder bezeichnen Zusammenfassungen von Objekten gleichen Typs zu einer Sequenz. Sie werden durch
<Typ> <Name> {"["<Elementzahl>"]"}
deklariert.
Matrixdeklaration

int matrix [2][3];
double field [10];
Die einzelnen Elemente werden dabei von Null weg durchnumeriert, d.h. daß das Statement
int vector [3];
einen dreielementigen int-Vektor deklariert, dessen Elemente mit 0, 1, 2 angesprochen werden, also
vector[2] = 3;
vector[0] = 1;
Arithmetische Ausdrücke bei der Deklaration sind erlaubt; bei reinen Deklarationen kann die Ausdehnungsangabe auch entfallen
int dim = 2;
int vector[3][2*dim];
...
extern int v[][]; //Deklaration
Durch Angabe von Werten in geschwungenen Klammern, können Felder initialisiert werden
double ex[3] = {1. 0. 0};
double toShort[4] = {0, 1,};
double undefinedEx[] = {0, 2, 4};
Die fehlenden Initialisierungswerte für das Feld toShort werden hierbei mit Nullen aufgefüllt. Im Falle von undefinedEx ermittelt der Compiler selbstständig die Größe des Feldes.
Zeichenkettenfelder werden durch ein Nullzeichen (`\0') terminiert; die physische Länge ist daher um eins größer als die Länge des eigentlichen Inhalts.
char s[] = "C++"; // sizeof(s) = 4
char ss[] = {`C', `+', `+', `\0'};
char sss[] = "C" "++";
Der Name eines Feldes alleine entspricht einem konstanten Zeiger auf das erste Element des Feldes. s entspricht also &s[0]. Durch sogenannte Zeigerarithmetik kann dann auf andere Elemente zugegriffen werden
char plus = *(s+1); // plus erhält den Wert `+'
Die wohl wesentlichste Konsequenz dieses Sachverhalts ist, daß Zeichenketten in C++ nicht einfach aufeinander zugewiesen werden können
ss = s;
geht daher nicht, weil ja ss ein konstanter Zeiger ist und Konstanten nicht geändert werden dürfen. Zeichenketten werden daher in C++ immer elementeweise kopiert.
Im Speicher wird eine Matrix immer linear dargestellt. Die Matrix
int m [2][3] = {{1, 2, 3}, {4, 5, 6}};
wird also im Hauptspeicher wie in Abbildung 2 dargestellt.

Darstellung einer Matrix im Hauptspeicher

Daher könnte obige Matrix auch so initialisiert werden
int m [2][3] = {1, 2, 3, 4, 5, 6};
Die Schwächen des Feldkonzeptes in C++ liegen in
typedef

typedef struct tnode{
  char *word;
  int occurrences;
  struct tnode *left;
  struct tnode *right;
} Treenode, *TreeNodePtr;

Die Struktur tnode kann unter mehreren Namen angesprochen werden. Einerseits als Zeiger (*TreeNodePtr), andererseits aber auch direkt als struct.
Treenode t;
TreeNodePtr ptr, ptr2;

Der Zugriff auf Strukturen (oder Teile darauf) erfolgt durch Namensselektion verbunden mit dem Punktoperator bzw. mit einem Zeiger und dem Operator `->'.
t.occurrences = 12;
ptr = (TreePtr)malloc(sizeof(tnode));
ptr2 = (TreePtr)malloc(sizeof(Treenode));
ptr->occurrences = 1;
(*ptr2).occurrences = 2;

cout << "\n occurrences of t= " << t.occurrences;
cout << "\n occurrences of ptr= " << ptr->occurrences;
cout << "\n occurrences of ptr2= " << 
ptr2->occurrences;
cout << endl;

Das Schlüsselwort typedef erlaubt dabei die Definition eigener Datentypen. typedef führt allerdings keine neuen Datentypen ein, sondern definiert gewissermaßen ein Synonym für einen bestehenden Datentyp.
typedef int Number;
typedef enum {false, true} Boolean;
...
Number aNumber;
Boolean b;
Warum sollte man typdef verwenden? Weil Programme dadurch
Verwendung von union

AddressType a;
cout << a.privatA.name;
cout << a.businessA.department;
Bei Varianten kann zu einem Zeitpunkt nur eine der beiden möglichen Strukturen sinnvolle Werte haben. Sie werden im Speicher `übereinander' verwaltet. Der verbrauchte Speicherplatz ist jener der größeren Komponente.
Tabelle 2: Datentyp und Speicherplatz (UNIX)
Typ Wertebereich von bis belegter Speicher
unsigned char 0 -> 255 8 Bits
char -128 -> 127 8 Bits
unsigned short 0 -> 65535 16 Bits
short -32768 -> 32767 16 Bits
unsigned int 0 -> 4294967295 32 Bits
int -2147483648 -> 2147483647 32 Bits
unsigned long 0 -> 4294967295 32 Bits
long -2147483648 -> 2147483647 32 Bits
float 3.40282e+38 -> 3.40282e+38 32 Bits
double 1.79769e+308 -> 1.79769e+308 64 Bits
pointer 32 Bits
Tabelle 2 zeigt die eingebauten Datentypen in C++, deren Speicherplatzverbrauch sowie mögliche Wertebereiche (32-Bit Unix System). Die möglichen Wertebereiche sind im allgemeinen in der Datei limits.h als Präprozessorkonstante definiert. Ein typischer Eintrag in dieser Datei lautet etwa
/* Maximum value an 'unsigned char' can hold.  (Minimum is 0).  */
#undef UCHAR_MAX
#define UCHAR_MAX 255
Man beachte, daß es sich bei den Speicherplatzangaben um "typische" Werte handelt, d.h. je nach Implementierung können auch andere Speicherplatzwerte gelten. Ein typischer DOS-Integer hat etwa 16-Bit, ein typischer UNIX-Integer 32-Bit.

Bitmanipulationen

Die Bitoperatoren in C++ erlauben das Manipulieren auf Bitebene. Somit ist auch hardwarenahe Programmierung möglich.

Folgende Operatoren zur Bitmanipulation werden in C++ angeboten:

Bitmanipulationen

char ch, ch2;
ch2 = ch ^ key; // XOR
ch2 = ch << 1;  // shift um eins nach links
Abbildung 3 "Bitmanipulationen" zeigt wie sich die Bits bei Ausführung von obigem Beispiel verschieben:


Bitmanipulationen

Typumwandlung

In streng typgebundenen Sprachen wie C++ kommt der Typumwandlung naturgemäß besondere Bedeutung zu. Zwei Arten werden Unterschieden: die implizite Umwandlung durch den Compiler sowie die explizite Umwandlung durch den Benutzer.

Implizite Typumwandlung

Folgende wesentliche Regeln liegen der automatischen Konversion durch den Compiler zu Grunde:

Explizite Typumwandlung

Speziell bei der Definition von eigenen Klassen wird es teilweise notwendig sein, explizite casts (=Typkonversionen) durchzuführen.
Die alte Schreibweise (aus C und den Anfängen von C++) lautet dabei beispielsweise folgendermaßen
	long l;
float f=4.5555;
l = (long)f;
cout << "\nl = " << l;
Der neue Datentyp wird also in Klammern vor die zu konvertierende Variable gestellt. Die neue Schreibweise ähnelt eher einer Funktion und sieht so aus
l = long(f);
cout << "\nl = " << l;
Der neue Datentyp wird vor die Variable gestellt, die Variable selbst wird geklammert. Das sieht dann so aus, als ob es eine Funktion long() mit einem float-Parameter gibt, die einen long zurückliefert.

Konstruktor

Zur Verwendung dieser (dritten) Möglichkeit der Konvertierung siehe "Copy-Konstruktoren" auf Seite 49.

Attribute für Variablen

Speicherklassenattribute

auto-Variablen existieren grundsätzlich solange der Block, in dem sie definiert sind, gültig ist. Sie werden daher auch bei jedem Durchlaufen des Blocks neu angelegt (und mit einem Zufallswert belegt). Durch das Schlüsselwort register kann dem Compiler gesagt werden, daß er eine auto-Variable optimieren darf, weil man sie z. B. sehr häufig verwenden will. Sie wird dann in einem Register abgelegt. Durch intensive Verwendung von Registervariablen kann aber auch ein gegenteiliger Effekt -- nämlich eine Verlangsamung des Programmes -- bewirkt werden.
Das Speicherklassenattribut static definiert, daß die Variable auch nach Verlassen des Blocks noch gültig bleibt. Alle Variablen, die außerhalb von Blöcken stehen, sind automatisch static; alle jene Variablen, die vor der main-Funktion stehen sind also immer static. Statische Variablen haben immer 0 als Default-Initialisierungswert. Noch ein Unterschied besteht bei statischen Variablen: sie können nicht exportiert werden.
Speicherklassenattribute

auto int autoVar; //sutoVar steht außerhalb des 
main-Blocks
int main(int argc, char *argv[])
{ return 0;}
Obige Anweisung läßt sich nicht übersetzen, weil die Variable autoVar in keinem Block steht und daher automatisch static ist -- und static und auto können nicht kombiniert werden.
void f()
{
static int statInt;
}

int main(int argc, char *argv[])
{ return 0;}
In diesem Beispiel bleibt statInt für die Dauer des Programmlaufes erhalten. Es kann aber nicht von aussen, d.h. von der main-Funktion darauf zugegriffen werden, da die Gültigkeit von statInt der Block ist, in dem diese Variable definiert wurde (siehe "Gültigkeitsbereich" auf Seite 32).

Andere Attribute

Das Schlüsselwort extern zeigt dem Übersetzer, daß eine Variable woanders, d.h. in einem getrennt zu übersetzenden Programmstück, bereits definiert wurde und daher davon ausgegangen werden kann, daß eine entsprechende Variable beim Linken gefunden wird. Falls nicht, ergibt sich ein Fehler beim Linken. Also etwa
extern System gSystem;
int main(int argc, char *argv[])
{ if (gSystem.Name() == "Macintosh") ...
  return 0;}
Das Schlüsselwort const besagt, daß eine Variable konstant ist. Sie muß daher genau einmal initialisiert werden und darf danach nicht mehr geändert werden. Globale Konstanten werden implizit als static vereinbart.
const double pi = 3.4;
Ebenso wie const kann auch das Schlüsselwort volatile vor Typspezifikationen gestellt werden. Es sagt dem Compiler, daß er von einer Optimierung dieser Variable Abstand nehmen soll, weil sie unter Umständen durch dem Compiler unbekannte Einflüsse verändert werden kann (z.B durch Interrupts). Das Schlüsselwort volatile wird sehr selten verwendet.
Zum Abschluß sei noch bemerkt, daß auch Kombinationen von Attributen möglich sind:
volatile auto int just4Fun = 4711;
const volatile auto shortVersion = 4712;
wobei im zweiten Fall zu bemerken ist, daß bei Angabe des Schlüsselwortes const ohne Datentyp implizit Integer angenommen wird.

Ein-/Ausgabe in C++

Allgemeines

Die streambuf (streambuf.h) Klassen dienen dazu, unformatierte Ein- und Ausgabe über Datenströme zu realisieren. strstreambuf ist eine Spezialisierung für Ströme, die im Hauptspeicher gehalten werden, filebuf wird für Datenströme auf Dateien verwendet.

Klassen für die Ein- und Ausgabe in C++

Die ios Klassen bieten für den Benutzer eine Schnittstelle zur formatierten Ein- und Ausgabe. Deshalb sind für den Benutzer eigentlich auch nur die ios Klassen wichtig \xa1 -- auf die notwendige Funktionalität greift er ja über die Schnittstelle zu.
Es gibt drei vordefinierte Variablen (cin, cout, cerr)
extern istream cin;
extern ostream cout, cerr; 
Der Unterschied zwischen cout und cerr darin liegt, daß cout meist auf Betriebssystemebene in eine andere Datei umgeleitet werden kann, cerr hingegen nicht.
Zur Formatierung werden sogenannte Flags verwendet, beispielsweise kann mit
cout.flags(cout.flags() | ios::showpos);
ein führendes `+' erzwungen werden.
Formatierte Ausgabe

cout.flags(cout.flags() | ios::showpos);
cout.width(20);
cout << x;
Setzt width (= Füllzeichen) auf 20.

Ein- und Ausgabe auf Dateien

void ofstream::open(const char* name, int mode);
void ofstream::close();
Der Integer mode kann dabei folgende Werte aufweisen (definiert in streambuf.h):
Öffnungsmodus für Dateien

ios::out // Öffnen, wenn Datei existiert wird ihr Inhalt 
überschrieben
ios::ate // Öffnen und hinten anhängen
ios::binary // im Binärmodus öffnen (`\n' wird auf DOS 
Maschinen 
nicht durch CR + LF ersetzt sondern bleibt ein Zeichen);
ios::ate bedeutet `append at end' und ist eine Komponente von ios (in streambuf.h), die public deklariert wurde. Diese Komponente entspricht dem Wert 4.
Dateiverarbeitung: Verschlüsselung durch XOR-Bit

int encrypt(char *inFilename, char *outFilename, char key)
{
    ifstream inFile;
    ofstream outFile;
    char ch, ch2;
    int i;

    inFile.open(inFilename, ios::in);
    if(!inFile) return 1;
    outFile.open(outFilename, ios::out);
    if(!outFile) return 1;

    ch = inFile.get(); // get first byte
    do {
       ch2 = ch ^ key; // XOR change
       outFile << ch2;
       ch = inFile.get();
    } while(!(ch == EOF));

    outFile.close();
    inFile.close();
    return 0;
}

Gültigkeitsbereich

Der Gültigkeitsbereich wird auch als Sichtbarkeit (oder auf englisch scope) bezeichnet. Grundsätzlich kann man in C++ zwischen lokalem, globalem, funktionslokalem und klassenlokalem Gültigkeitsbereich unterscheiden.

Lokale Variablen gelten innerhalb des Blocks, in dem sie definiert wurden.

Globale Variablen sind außerhalb jedes Blocks und auß erhalb von Klassen definiert. Sie behalten ihre Gültigkeit solange das Programm läuft bzw. sie durch andere Definitionen (z.B innerhalb eines Blocks) überlagert werden. Zugreifen kann man auf globale Variablen entweder direkt oder über den Bereichsoperator (`::').

Der funktionslokale Gültigkeitsbereich betrifft vor allem Sprungmarken (labels), die z.B. durch ein goto-Statement angesprungen werden.

void f()
{
if (deviceNotFound) goto error;
...
error: exit(4711); //label error
}
Der Gültigkeitsbereich von Namen von Komponenten in Klassen wird als klassenlokal bezeichnet. Somit kann auf Komponenten nur durch den Bereichsoperator `::', `.' oder `->' zugegriffen werden. Beispiel

Operatingsystem *os;
WindowSystem *wSys;
...
os->GetName();
wSys->GetName();
Nur durch explizites Referenzieren der Methode GetName() kann der Compiler `herausfinden' welches GetName() nun gemeint ist; das bedeutet, daß der Aufruf durch

GetName();
alleine vom Compiler nicht aufgelöst werden kann. Die Anführung einer Instanz einer Klasse für die eine Methode GetName() definiert ist, erlaubt dem Compiler die entsprechende Methode zu finden.

Gültigkeitsbereich

double x = 1.23;
void f();

int main(int argc, char *argv[], char *envp[])
{
        cout << "\n" << x;
        int x = 10;
        cout << "\n" << x;
        {
          float x = 47.11;
          cout << "\n" << x;
          ::x = 3.14; //Bereichsoperator; geht ganz nach aussen
        }
        cout << "\n" << x;
        f();
};

void f ()
{ cout << "\n" << x;}

Führt zur Ausgabe von

1.23
10
47.11
10
3.14

Verzweigungen

C++ unterstützt Verzweigungen durch if-Anweisungen (Zweiwegverzweigung) sowie durch switch-Anweisungen (case-Statement, Mehrwegverzweigung).

"if" "("<ausdruck>")" <statement> |
"if" "("<ausdruck>")" <statement1> "else" 
<statement2>
Der zu evaluierende Ausdruck muß also immer geklammert werden; der then-Zweig wird dann ausgeführt, wenn der zu evaluierende Ausdruck einen Wert ungleich Null ergibt.

Die Mehrwegverzweigung evaluiert einen Ausdruck und verzweigt dann zur ersten Stelle auf die der Ausdruck zutrifft. Von da an wird sequentiell die Reihe der case-Statements ausgeführt. Im allgemeinen will man jedoch, daß nach ausführen des passenden Statements der Block verlassen wird: man fügt daher ein break-Statement ein, das zum Verlassen des switch-Blockes führt.

"switch" "("<ausdruck>")" "{"
"case" konst-1":" <statement-1> [break;]
"case" konst-2":" <statement-2> [break;]
...
"case" konst-n":" <statement-n> [break;]
["default:" <default-Statement>]
"}"
In das optionale default-Statement wird dann verzweigt, wenn kein anderes Statement paßt.

switch-Anweisung

char input = "...";

switch (input) {
  case `H': case `h': help(); break;
  case `Q': case `q': return;
  case `D': case `d': doSomething(); break;
  default: cerr << "\nUnknown command " << input 
<< endl;
}
In obigem Beispiel gibt es drei leere Anweisungen, die sich die Funktionsweise des switch-Statements in C++ zu Nutze machen, nämlich case `H', case `Q' und case `D'. In diesen Fällen fährt das Programm sequentiell fort, d.h. die ausgeführte Option ist beispielsweise die gleiche für `H' und `h'. Falls keines der Statements paß t, wird in das default-Statement verzweigt und ein Fehler ausgegeben.

switch-Statements können auch geschachtelt werden, die case- und default-Statements beziehen sich dann auf die unmittelbar vorhergehende switch-Anweisung.

Schleifen

C++ unterstützt die Schleifenkonstrukte for, while und do-while.

Die for-Schleife ist folgendermaßen aufgebaut

"for" "("[init-statement>]";" [expression]";" 
[re-init-statement]")" 
<statement>
Vor dem ersten Schleifendurchlauf wird init-statement ausgewertet, d.h. eine etwaige Laufvariable initialisiert. Vor jedem (auch vor dem ersten) Durchlauf wird Ausdruck zwei (expression) ausgewertet; soferne er ungleich Null ist, wird das Anweisungsstatement und dann das re-init-statement ausgewertet.

for-Schleife

for (int i=start; i < 100; i++, j--)
{
...
}
Endlosschleife

for (;;)
{
...
}

Eine while-Schleife wird in C++ folgendermaßen definiert

"while" <expression> <statement>
Hierbei wird der Ausdruck in expression jeweils zu Beginn des Schleifendurchlaufs ausgewertet, d.h. der Ausdruck in statement kann auch Null-mal durchlaufen werden. Die Variante mit do-while wird mindestens einmal durchlaufen. Sie besitzt folgendes Aussehen

"do" <statement> "while" <expression>

Funktionen und Übergabe von Parametern

Die Definition einer Funktion erfolgt durch Angabe des Ergebnistyps, des Funktionsnamens, einer (evtl. leeren) Parameterliste und des Funktionsrumpfs `{}'. Bei der Deklaration wird der Rumpf durch einen Strichpunkt `;'ersetzt.

Definition und Deklaration

double hyp(double a, double b) //Definition
{
  extern double sqrt(double);  //Deklaration
  return sqrt(a*a + b*b);
}
Jede Funktion wird automatisch als extern deklariert. Durch das Schlüsselwort static kann ihr export verhindert werden.

static double hyp();
// kann von anderen nicht importiert werden
Wird kein Ergbnistyp angegeben nimmt der Compiler implizit int an. Will man keinen Rückgabewert, muß man das Schlüsselwort void angeben. Funktionen ohne Rückgabewert werden als Prozeduren bezeichnet.

Zeiger auf Funktionen können wie folgt realisiert werden:

TYP "*" {"*"} <Name> ["("<Parameterliste>")"]
also etwa

void (*fSin)(char*);
...
double are = Integral(sin, 0, pi/2);
wobei

double sin (double);
Die Funktion Integral müßte also folgendermaßen vereinbart werden

double Integral (double (*f) (double), double low, double high)
{ ...
Die Klammern um *f sind notwendig, da ohne sie kein Zeiger auf eine Funktion, sondern eine Funktion mit einem Zeiger auf double als Ergebnistyp deklariert würde und ein Funktionsaufruf eine höhere Priorität aufweist:

double integral (double *f (double), double low, double high)

Parameterübergabe am Beispiel swap Integer

Werden keine Referenzen bzw. Zeiger als Parameter übergeben, so legt der Compiler automatisch Kopien an, die dann an Stelle der formalen Parameter übergeben werden -- und somit innerhalb einer Methode oder Funktion geändert werden können -- deren Wert allerdings nach dem Aufruf der Sub-Routine nicht geändert worden ist, weil ja eine Kopie übergeben wurde.
const kann verwendet werden, damit nicht versehentlich die übergebene Adresse geändert wird, sondern nur deren Wert.
Warum werden Referenzen verwendet (siehe auch "Referenzen" auf Seite 18): es wird nur eine Referenz (ein konstanter Pointer) übergeben und nicht eine Kopie der ganzen Datenstruktur -- bei großen C++ Objekten kann das Performanceverbesserungen bringen. Außerdem sind Referenzen eben der einzige Weg, übergebene Parameter innerhalb der Funktion zu ändern: durch das Anlegen einer Kopie am Heap wird nämlich nur die Kopie geändert und nicht das `Original'!
Swap-Integer

void SwapInt(int * const in, int * const out);
// in ist konstanter Pointer auf int
void ReSwapInt(int &in, int &out);
// Referenzen sind per default konstante Pointer

int main(int argc, char *argv[])
{
  int in = atoi(argv[1]), out = atoi(argv[2]);
  SwapInt(&in, &out);
  cout << endl << in << " and " << out;
  ReSwapInt(in, out);
  cout << endl << in << " and " << out;
};

void SwapInt(int * const inP, int * const outP)
// *inP ist ein konstanter Zeiger auf einen Integer, d.h., ich 
übergebe 
// einen Zeiger
        int tmp = *inP;
// *inP liefert den Inhalt des als konstanter Pointer deklarierten 
// Parameters *inP
        *inP = *outP;
        *outP = tmp;
}
void ReSwapInt(int &in, int &out)
// &in ist eine Referenz auf int, d.h. ich übergebe einen int
{
        int tmp = in;
        in = out;
        out = tmp;
}

inline-Funktionen

Die grundsätzliche Idee von inline-Funktionen ist, daß der Implementierungsaufwand einer Funktion so gering ist, und die Funktion so oft aufgerufen wird, daß man sie `gleich hinschreibt'. Dadurch wird dem Compiler angezeigt, daß man eine Geschwindigkeitsteigerung erreichen will. Einschränkungen entstehen dadurch, daß Compiler teilweise keine Schleifen, if oder switch Anweisungen vertragen. inline-Funktionen können auch keinem Funktionszeiger zugewiesen werden, weil von ihnen keine Adresse ermittelt werden kann.
inline-Funktionen sollten vorsichtig verwendet werden, weil sie die Programmgröße signifikant erhöhen können. Im extremsten Fall kann durch cache trashing die Laufzeit des Programmes sogar erhöht werden.
Deklariert werden inline Funktionen durch das Schlüsselwort inline:
inline-Funktion

inline int max(int a, int b)
{ return (a > b ? a : b);}

Default Argumente

Beginnend von rechts, können Parameter mit Standardwerten versehen werden, die beim Aufruf dann nicht angegeben werden müssen. Anwendung finden Default-Argumente z.B. in der Erweiterung von Schnittstellen: man fügt einen neuen Parameter hinzu, den man mit einem Default-Wert belegt. Dadurch können bestehende Funktionen ohne Änderung weiterverwendet werden.
void Time(int hours, int minutes=10, int seconds=20);
void PrintHello(ostream &os = cout);
Default Argumente

PrintHello();
PrintHello(cerr);
...
Time (10, 3);   // 10, 3, 20
Time (3);       // 3, 10, 20
Time (1, 2, 3); // 1, 2, 3
Eine bestehende Funktion mit der Schnittstelle
void Time(int hours, int minutes, int seconds);
könnte also im Falle einer Erweiterung beispielsweise um Millisekunden folgendermaßen deklariert werden:
void Time(int hours, int minutes, int seconds, int millis=0);
bestehender Programmcode muß dabei nicht geändert werden.

Variable Parameterlisten

Variable Parameterlisten sind in vielen Fällen praktisch: man weiß ja oft im vorhinein noch nicht mit wievielen Objekten man es zu tun hat. Eine Methode AddItems einer Liste beispielsweise sollte es erlauben, beliebig viele Objekte in eine Liste einzufügen. In C++ gibt es ein eigenes Konstrukt, genannt ellipsis (`...'), das angibt, daß mehr aktuelle als formale Parameter übergeben werden dürfen.

Die entsprechenden Makros sind in stdarg.h definiert. Anwendungsbeispiel: die Standardausgabefunktion printf() in C. Oder auch eine Funktion max(), die das größte von beliebig vielen int-Argumenten liefert.

Man braucht als Parameter die Anzahl der übergebenen Parameter oder aber man terminiert die Liste mit 0:

Variable Parameterlisten

PrintSomeIntegers(5,1,2,3,4,5)
// 5 ist Anzahl der Parameter
...
PrintSomeIntegers2(1,2,3,4,5,6,9,'a','A',0);
// 0 terminiert `a' und `A' werden als 97 und 65 ausgegeben
...

void PrintSomeIntegers2(int a, int b, ...)
{
  va_list argList;

  va_start(argList,b); // last argument
  int  arg = va_arg(argList, int);

  cout << endl << "1st parameter  = " << a;
  cout << endl << "2nd parameter  = " << b;

  while(arg) {
    cout << endl << "next parameter = " << arg;
    arg = va_arg(argList, int);
  }
  cout << endl;
}
Der Datentyp va_list kann dazu benützt werden, eine lokale Variable zu definieren, die die Liste der Parameter enthält. Mit va_start(args,b) wird die Liste initialisiert wobei `b' den letzten benannten Parameter darstellt. Mit arg=va_arg(argList,int) erhält man jeweils das nächste Element der Liste.

Überladen von Funktionen

In den meisten Programmiersprachen ist man gezwungen eine Funktion mit der gleichen Semantik für verschiedene Datentypen anders zu benennen, also dmax() für `double max', imax() für `int max' usw. In C++ kann man Funktionen überladen: man nennt das Bildung von Homonymen. Wenn keine passende Funktion zur Verfügung steht, wird durch Typumwandlung eine Funktion ausgewählt (siehe auch "Typumwandlung" auf Seite 25).

Überladen

int min(int a, ...);
double min(double a, ...);
...
Zweideutigkeiten (Ambiguities) sollten dabei vermieden werden!

Überladene Funktionen dürfen sich nicht nur im Rückgabewert unterscheiden, weil sonst der Compiler nicht weiß, welche Funktion er wählen soll:

int foo(int i);
void foo(int i);
Probleme gibt es auch bei gleich gut bzw. gleich schlecht passenden Alternativen einer Typumwandlung:

typedef INT int;
void foo(int i);
void foo(INT i);

Operator Overloading

Auch (fast alle) Operatoren können überladen werden (Ausnahme: `.', `.*', '::', `? :', sizeof sowie die Präprozessor Symbole `#' und `##'). Überladene Operatoren werden wie normale Funktionen definiert; das Schlüsselwort operator wird dem Funktionsnamen (z.B. `+') vorangestellt. Es dürfen keine eigenen Operatoren (z. B. `**') eingeführt werden. Default-Argumente sind nicht erlaubt. Ein unärer Operator bleibt immer unär, d.h. die Anzahl der Parameter kann nicht geändert werden.
Operator Overloading für komplexe Zahlen

typedef struct Complex {
        double re, im;}
Complex;

Complex& add(Complex &, Complex &);

ostream& operator << (ostream& s, Complex c)
{
  s << c.re << " + " << c.im << "i";
return s;
}

Complex& operator + (Complex &x, Complex &y)
{
  return add(x, y);
}

Complex&  add(Complex &a, Complex &b)
{
  Complex c;
  c.re = a.re + b.re;
  c.im = a.im + b.im;
  a=c; // sonst Rückgabe einer lokalen Referenz
  return a;
}
Operator Overloading für die Ausgabe

typedef struct IntArray { int array[10];};
...
ostream& operator << (ostream& s, const IntArray& a)
{
  for (int i=0; i < (sizeof(a.array)/sizeof(int)); i++) {
    if ((i % 5) == 0) s << endl;
    s << "[" << i << "] = " << a.array[i] 
<< " ";
    }
    return s;
}
...
main()
{
  IntArray iArray;

  int i;
  for (i=0; i < (sizeof(iArray.array)/sizeof(int)); i++){
    iArray.array[i] = i;
  }
  cout << "iArray = " << iArray << endl;
}

Eine Referenz auf einen ostream wird zurückgegeben, damit eine Verkettung von Aufrufen möglich ist (cout << b << c ...).

Weiteres Beispiel

if (!cin) ...     // äquivalent zu if (cin.fail())
weil int operator ! () überladen wurde (in streambuf.h):

int operator!() const { return fail(); }

Funktionsschablonen

Siehe "Funktionsschablonen" auf Seite 74.

Für den zweiten Teil des Skriptums klicken Sie bitte hier.


Last Modified: 11:03pm PST, November 24, 1996