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 1 | 24 - 22: | (x 2 = 48-44) |
GUT 2 | 21 - 19: | (x 2 = 43- 38) |
BEF 3 | 18 - 16: | (x 2 = 37- 32) |
GEN 4 | 15 - 12: | (x 2 = 31- 24) |
NGD 5 | < 12: | (x 2 = < 24) |
18. April Ü1, 25. April Ü2, 2. Mai Ü3, 9. Mai Ü4, 23. Mai Ü5, 30. Mai Ü6 und Programmierprojekt
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);
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"
#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".// 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
#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 {
...
};
#endifIn 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.14und
const double PI = 3.14besteht 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 MC68882Der 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
double func (double a, double b);
func
. Mit
double func (double a, double b)
{
return a * b;
}
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).
char
,
int
, double
, float
und
void
.
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'
\
)
angeführt
\n // newline
\t // Tabulator
\b // backspace
\' // einfaches Hochkomma
//normal, octal und hexadezimal
char ch= `a';
cout << "\nch = " << ch;
ch= `\141'; //octal
cout << "\nch = " << ch;
ch= `\x061'; //hexadezimal
cout << "\nch = " << ch;
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)
enum boolean {false, true};
enum figur {bube=2, dame, koenig, as = 11};
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;
...
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>]
float pi = 3.14;
double d = -200.4711E+47;
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);
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.
*
' wird ein `normaler' Datentyp zum
Zeigerdatentyp. Durch den Adreßoperator `&
' wird
einem Zeiger die Adresse einer Variable zugewiesen.
int *iPtr, i = 4711;
iPtr = &i;
*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;
i = 4711, und iPtr = 4711
&
' 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.
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
int &&rr = ... // nicht möglich
<Typ> <Name> {"["<Elementzahl>"]"}
int matrix [2][3];
double field [10];
int vector [3];
int
-Vektor deklariert, dessen
Elemente mit 0, 1, 2 angesprochen werden, also
vector[2] = 3;
vector[0] = 1;
int dim = 2;
int vector[3][2*dim];
...
extern int v[][]; //Deklaration
double ex[3] = {1. 0. 0};
double toShort[4] = {0, 1,};
double undefinedEx[] = {0, 2, 4};
toShort
werden hierbei mit Nullen aufgefüllt. Im Falle
von undefinedEx
ermittelt der Compiler selbstständig die
Größe des Feldes.
\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" "++";
s
entspricht also
&s[0]
. Durch sogenannte Zeigerarithmetik kann dann auf
andere Elemente zugegriffen werden
char plus = *(s+1); // plus erhält den Wert `+'
ss = s;
ss
ein konstanter Zeiger ist
und Konstanten nicht geändert werden dürfen. Zeichenketten
werden daher in C++ immer elementeweise kopiert.
int m [2][3] = {{1, 2, 3}, {4, 5, 6}};
int m [2][3] = {1, 2, 3, 4, 5, 6};
-5
bis +5
.
struct
mit der Definition
eines eigenen Typs verbunden. Die grundlegendes Syntax dafür lautet
"typedef" "struct" "{" {<Datentyp Name>} "}" {<Strukturname>}
typedef
typedef struct tnode{
char *word;
int occurrences;
struct tnode *left;
struct tnode *right;
} Treenode, *TreeNodePtr;
tnode
kann unter mehreren Namen
angesprochen werden. Einerseits als Zeiger (*TreeNodePtr
),
andererseits aber auch direkt als struct
.
Treenode t;
TreeNodePtr ptr, ptr2;
->
'.
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;
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;
typdef
verwenden? Weil Programme
dadurch
struct
steht bei union
die Art der
Belegung der Komponenten noch nicht fest.
typedef struct {
char *name;
char *address;
} PrivatAddress;
typedef struct {
char *name;
char *department;
char *address;
} BusinessAddress;
union AddressType { // type of union
PrivatAddress privatA;
BusinessAddress businessA;
} Address; // name of union
union
AddressType a;
cout << a.privatA.name;
cout << a.businessA.department;
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 |
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
Folgende Operatoren zur Bitmanipulation werden in C++ angeboten:
>>
Shift um die Anzahl des rechten Operanden nach
rechts. Auffüllen mit Nullen
<<
Shift um den Wert des rechten Operanden nach
links. Auffüllen der `freiwerdenden' Bits mit Nullen
&
Und-Verknüpfung
|
Oder-Verknüpfung
^
Ausschließende Oder-Verknüpfung
~
Bitweise Negation (genaues Gegenteil)
char ch, ch2;
ch2 = ch ^ key; // XOR
ch2 = ch << 1; // shift um eins nach linksAbbildung 3 "Bitmanipulationen" zeigt wie sich die Bits bei Ausführung von obigem Beispiel verschieben:
BitmanipulationenTypumwandlung
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
char
, short
, Enumerationstypen und Bitfelder
können immer an der Stelle von int
verwendet werden.
Wenn alle Werte durch int
dargestellt werden können wird
int
verwendet, ansonsten verwendet der Compiler automatisch
unsigned int
.
long
double
wird auch der andere in einen long
double
konvertiert.
double
wird auch
der zweite in einen double
konvertiert.
float
wird auch der
andere in den Typ float
konvertiert.
int
-Konvertierungen) versucht.
unsigned
long
wird der andere in einen unsigned
long
konvertiert.
long
int
und der andere vom Typ unsigned
int
ist, so wird letzterer -- falls keine Probleme mit dem
Wertebereich auftreten -- ebenfalls in einen long
int
konvertiert. Andernfalls werden beide auf
unsigned
long
int
konvertiert.
long
, werden beide in den
Typ long
konvertiert.
unsigned
wird auch der
andere in einen unsigned
umgewandelt.
void*
umgewandelt
werden, sofern er nicht auf const
oder
volatile
-Objekte zeigt.
long l;
float f=4.5555;
l = (long)f;
cout << "\nl = " << l;
l = long(f);
cout << "\nl = " << l;
long()
mit einem float
-Parameter gibt, die einen
long
zurückliefert.
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.
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.
auto int autoVar; //sutoVar steht außerhalb des
main
-Blocks
int main(int argc, char *argv[])
{ return 0;}
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;}
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).
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;}
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;
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.
volatile auto int just4Fun = 4711;
const volatile auto shortVersion = 4712;
const
ohne Datentyp implizit Integer
angenommen wird.
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.
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.
cin
,
cout
, cerr
)
extern istream cin;
extern ostream cout, cerr;
cout
und cerr
darin liegt, daß cout
meist auf Betriebssystemebene in
eine andere Datei umgeleitet werden kann, cerr
hingegen nicht.
cout.flags(cout.flags() | ios::showpos);
+
' erzwungen werden.
cout.flags(cout.flags() | ios::showpos);
cout.width(20);
cout << x;
width
(= Füllzeichen) auf 20.
void ofstream::open(const char* name, int mode);
void ofstream::close();
mode
kann dabei folgende Werte aufweisen
(definiert in streambuf.h
):
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.
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;
}
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
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.
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>
{}
'. 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 werdenWird 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)
const
kann verwendet werden, damit nicht versehentlich
die übergebene Adresse geändert wird, sondern nur deren Wert.
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
-Funktioneninline
-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.
inline
Funktionen durch das
Schlüsselwort inline
:
inline
-Funktion
inline int max(int a, int b)
{ return (a > b ? a : b);}
void Time(int hours, int minutes=10, int seconds=20);
void PrintHello(ostream &os = cout);
PrintHello();
PrintHello(cerr);
...
Time (10, 3); // 10, 3, 20
Time (3); // 3, 10, 20
Time (1, 2, 3); // 1, 2, 3
void Time(int hours, int minutes, int seconds);
void Time(int hours, int minutes, int seconds, int millis=0);
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.
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!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
.
', `.*
', '::
',
`? :
', 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.
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(); }
Für den zweiten Teil des Skriptums klicken Sie bitte hier.