2.3.2 Standardtypen


Praktisch jeder Bezeichner, so z. B. jede Variable, muß in beiden Sprachen mit einem Typ assoziiert werden. Datentypen können durch die Attribute Name, Gültigkeits- und Sichtbarkeitsbereich, Konstruktionsregel, Wertebereich und zulässige Operationen charakterisiert werden.


Elementare Datentypen (simple types) bilden die Grundlage für die Erzeugung weiterer, benutzerdefinierter, abgeleiteter Typen. Integer, Real, Char und Boolean sind Beispiele für elementare Typen. Die wichtigsten in Visual C++ und Object Pascal sind:


Ganzzahl-Typen:


VC++

Object Pascal

Länge in Win32 (Intel)

darstellbarer Bereich

char

Char = AnsiChar

1 Byte

ASCII-Code 0 . . 255

wchar_t

WideChar

2 Byte

Uni-Code 0 . . 65535

signed char

ShortInt

1 Byte

-128 . . 127

unsigned char

Byte

1 Byte

0 . . 255

short

SmallInt

2 Byte

- 32768 . . 32767

unsigned short

Word

2 Byte

0 . . 65535

int = long

Integer = LongInt

4 Byte

-2147483648 . . 2147483647

-

Cardinal

4 Byte

0 . . 2147483647

unsigned int = unsigned long

-

4 Byte

0 . . 4294967296

__int64

Comp

8 Byte

-263 + 1 . . 263 - 1

CURRENCY

Currency

8 Byte

-9,22337 · 1014 . . 9,22337 · 1014


SizeOf(Cardinal) liefert 4 Byte als Ergebnis. Dieser Typ benutzt aber eigentlich nur 31 Bit; das Vorzeichen Bit wird von Delphi 2 einfach ignoriert.

Currency stellt Dezimalzahlen immer mit 4 Dezimalstellen dar. Er wird intern als Integertyp realisiert, so daß das Intervall zwischen zwei darstellbaren Zahlen im gesamten darstellbaren Zahlenbereich konstant 0,0001 beträgt. Er eignet sich gut für die Repräsentation von Geldbeträgen.


Dezimalzahl-Typen:


VC++

Object Pascal

Länge in Win32 (Intel)

float

Single

4 Byte

double

Double

8 Byte

long double

Extended

10 Byte


Boolsche Typen:


VC++

Object Pascal

Länge in Win32 (Intel)

boolean

Booleans

1 Byte

BOOL

BOOL = LongBool

4 Byte


Alle anderen, abgeleiteten Datentypen werden unter Verwendung der elementaren Datentypen gebildet.


Datentypen




Mögliche Einteilung der Datentypen nach [11]


Eingeschränkte Datentypen sind dadurch gekennzeichnet, daß der Wertebereich eines vorhandenen Basisdatentyps eingeschränkt wird. Dies kann durch die Angabe eines Intervalls (Teilbereichstyp) oder die explizite Aufzählung der zulässigen Werte (Aufzählungstyp) geschehen.


Ein eingeschränkter Datentyp in Objekt Pascal ist der Teilbereichsdatentyp (subrange type). Die Definition eines Teilbereichstyps spezifiziert den kleinsten und den größten Wert im entsprechenden Teilbereich: type_name = wert_min .. wert_max;

type
  DoppelZiffern = 10..99;

var Zahl: DoppelZiffern;
Zahl:= 55;
Zahl:= 123;  // Fehler, wird vom Compiler abgewiesen


Die Nutzung des Teilbereichsdatentyps erspart Prüfungen der Art:

var Zahl1: Integer;
    Zahl2: Integer;
    ···
if (Zahl2 >= 10) and (Zahl2 <= 99) then
  Zahl1:= Zahl2
else
  Fehlerbehandlung;

Falls einer Teilbereichstyp-Variablen zur Laufzeit ein Wert außerhalb des gültigen Bereichs zugewiesen wird, wird eine Exception der Art ERangeError ausgelöst, sofern das Programm mit aktivierter Bereichsüberprüfung übersetzt wurde. Exceptions werden im Abschnitt "2.3.4 Anweisungen" besprochen.

Den Teilbereichstypen wird ungerechtfertigter Weise allgemein sehr wenig Beachtung geschenkt. Immerhin tragen sie durch ihre aktive Bereichsüberprüfung während der Übersetzungs- und der Laufzeit dazu bei, mögliche programminterne Inkonsistenzen zu vermeiden bzw. helfen dabei, solche aufzudecken. Visual C++ besitzt keinen Teilbereichsdatentyp.


Dagegen kennen beide Sprachen Aufzählungen (enumeration type). Aufzählungen werden durch eine Reihe symbolischer Bezeichner gebildet, die alle Integerkonstanten repräsentieren. Der erste Bezeichner bekommt automatisch den Wert 0, der zweite den Wert 1 usw. Durch Angabe konstanter Ausdrücke und Zuweisungen an die Bezeichner können in C++ auch andere Werte für die Integer-Konstanten definiert werden.


C++

Pascal

enum Farbe {Kreuz, Karo,
            Herz, Pik};  

···
Farbe Karte;
Karte = Herz;
printf("%d", Karte);

Ausgabe: 2

type
  Farbe = (Kreuz, Karo, 
           Herz, Pik);
···
var Karte: Farbe;
Karte:= Herz;
writeln(Karte);

Ausgabe: 2

enum Antwort
{
  Nein,
  Ja,
  KannSein = -1
};  


Die Werte aggregierter Datentypen (komplexer Datentypen, composite types) bestehen meist aus verschiedenen Komponenten, evtl. mit jeweils eigenem Datentyp. Die Komponenten dieses Typs sind sichtbar, alle zulässigen Operationen sind uneingeschränkt auf sie anwendbar. Zu dieser Art Typen zählen Vektoren, Records, das Set und Varianten.

Vektoren, häufig auch Arrays oder Felder genannt, besitzen eine bestimmte Anzahl von Elementen eines bestimmten Typs. Sie können ein- oder mehrdimensional sein. Ihre Elemente müssen in C++ mit den Werten 0 bis (Anzahl-1) indiziert sein. In Pascal hingegen können obere und untere Indexgrenze frei gewählt werden.

Visual C++ und Object Pascal verwenden den Begriff "Feld" auch im Zusammenhang mit Objekt-Membern. Um Verwechslungen vorzubeugen, sollten hier bevorzugt die Begriffe Vektor oder Array verwandt werden.



C++

Pascal

ein-

dimen-

sional

float v[3];
// Vektor von 3 reellen
// Zahlen v[0], v[1], v[2]

var v: Array[6..8]of Single;
// Vektor von 3 reellen
// Zahlen v[6], v[7], v[8]



mehr-

dimen-

sional

int a[2][3];

// belegter Speicherplatz
// ist 2*3*sizeof(int) Byte

var a: Array[0..1, 0..2] 
       of Integer;
// belegter Speicherplatz
// ist 2*3*SizeOf(Integer)

oder alternativ

var a: Array[0..1] of
       Array[0..2]
       of Integer;



Die Elemente der Arrays können in C++ problemlos auch über Zeiger angesprochen werden. Object Pascal unterstützt diese Art des Zugriffs nur bei Arrays vom Typ Char. C++ gestattet die Initialisierung mehrerer Elemente des Arrays in einem Ausdruck durch eine kommaseparierte Liste:

int z[4] = {9, 8, 7}  // z[0]=9  z[1]=8  z[2]=7


Elemente, die in der Initialisierungsliste keinen Anfangswert zugewiesen bekommen (hier z[4]), werden vom Compiler zu Null initialisiert.

Direkt-Zuweisungen der Inhalte von Array-Variablen gleicher Struktur sind in Pascal möglich, in C++ durch das Umkopieren des Array-Speichers realisierbar:


C++

Pascal

#include <memory.h>

int x[8], y[8];

x[0] = 8;
x[1] = 14;
x[2] = 3;

y = x; // Fehler
memcpy(y, x, sizeof(x));



var x, y: Array[1..8] of Integer;

x[1]:= 8;
x[2]:= 14;
x[3]:= 3;

y:= x;



Ein Problem stellen Überschreitungen der Indexgrenzen in C++ dar. In einem 4 Elemente umfassenden Array wird ein Zugriff auf ein fünftes, nicht gültiges Element, in der Art

   z[4]:= 10;


anstandslos und ohne Warnhinweise von C++ übersetzt, obwohl ein Zugriff auf einen Speicherbereich außerhalb des Arrays erfolgt! Das ist ein Programmierfehler, der einem Programmierer leicht unterläuft, der aber schwerwiegende Folgen nach sich ziehen kann, die im ungünstigsten Fall zunächst nicht einmal bemerkt werden. Die Fehlersuche gestaltet sich entsprechend schwierig. Object Pascal unterbindet die Überschreitung von Array-Grenzen zur Übersetzungs- und zur Laufzeit. Letzteres jedoch nur dann, wenn das Programm mit gesetzter Projektoption "Bereichsüberprüfung" compiliert wurde. Wird zur Laufzeit die Array-Grenze überschritten, so wird wiederum eine Exception der Art ERangeError ausgelöst.

Am 2. April 1981 äußerte Brian W. Kernighan, einer der Entwickler von C, seine Meinung über Standard-Pascal in einem Aufsatz mit dem Titel "Warum Pascal nicht meine bevorzugte Programmiersprache ist" [12]. Der gravierendste sprachliche Mangel von Pascal ist seiner Meinung nach die Tatsache, daß Funktionen und Prozeduren als Parameter nur Arrays mit fester Größe akzeptieren. Durch diese Einschränkung ist es in Standard-Pascal nicht möglich Funktionen zu schreiben, die verschieden große Arrays als Parameter akzeptieren. Object Pascal erweitert Standard-Pascal deshalb an dieser Stelle und führt "Offene Array-Parameter" ein (Open Array). C++ kennt derartige Typen unter dem Begriff der "Incomplete Types".


VC++

Object Pascal

void DoIt(int Arr[])
{
  printf("%d\n", Arr[1]);
}



void main()
{  
  int z2[2] = {1, 2};
  int z2[3] = {1, 2, 3};
  DoIt(z2);
  DoIt(z3);
}
procedure DoIt(Arr: Array of
                        Integer);
begin
  writeln(Arr[1]);
end;

var z2: Array[0..1] of Integer;
    z3: Array[0..2] of Integer; 
begin
  z2[0]:= 1; z2[1]:= 2;
  z3[0]:= 1; z3[1]:= 2; z3[2]:= 3;
  DoIt(z2);
  DoIt(z3);
end.



Die Anzahl der Elemente eines offenen Arrays können in Object Pascal über die Funktion High(OpenArray) abgefragt werden.


Eine Eigenheit von C++ bei der Parameterübergabe an Funktionen wird deutlich, wenn man die Ausgabe der folgenden Programmstücke vergleicht:



VC++

Object Pascal

void DoIt(int a, int Arr[])
{
  printf("%d %d\n", a, Arr[0]);
  a = 0; 
  Arr[0] = 0;
}


void main()
{  
  int z = 2;  
  int zArr[2] = {2};
  
  printf("%d %d\n",z, zArr[0]);
  DoIt(z, zArr);
  printf("%d %d\n",z, zArr[0]);
}

procedure DoIt(a: Integer; Arr:
              Array of Integer);
begin
  writeln(a, Arr[0]);
  a:= 0;
  Arr[0]:= 0;
end;

var z: Integer;
   zArr: Array[0..1] of Integer;
begin
  z:= 2; 
  zArr[0]:= 2;
  writeln(z, zArr[0]);
  DoIt(z, zArr);
  writeln(z, zArr[0]);
end;
Ausgabe:

2 2

2 2

2 0

Ausgabe:

2 2

2 2

2 2


Der Wert des Arrays wurde in C++ durch den Aufruf der Prozedur DoIt global verändert, obwohl der Funktionsparameter syntaktisch als Call-By-Value - Parameter angegeben wurde. C++ reserviert generell bei Funktionsaufrufen für alle Arrays keinen Stack-Speicher, sondern übergibt nur einen Zeiger an die Funktion. Object Pascal verhält sich entsprechend, wenn der Funktionsparameter als VAR-Parameter deklariert wird: var Arr: Array of Integer. Der Array-Parameter wird in diesem Fall auch als Call-By-Reference Wert übergeben; eine lokale Kopie auf dem Stack wird dann nicht angelegt, sondern ein Zeiger verweist auf den Speicher an der Aufrufstelle.


In Pascal wurde ein weiterer aggregierter Datentyp implementiert, das Set. Es definiert eine Sammlung von Objekten desselben Ordinaltyps mit maximal 256 möglichen Werten. Die eckigen Klammern bezeichnen in Pascal den Mengenkonstruktor und haben nichts mit dem Vektoroperator in C++ zu tun.


var
  KleinbuchstabenMenge: Set of 'a'..'z';
begin
  KleinbuchstabenMenge:= ['b', 'm', 'l'];
  // jetzt wird 'm' aus der Menge entfernt
  KleinbuchstabenMenge:= KleinbuchstabenMenge - ['m'];
  // die entstandene Menge ist ['b', 'l']
  KleinbuchstabenMenge:= [];   //leere Menge
end;

Der Operator in gestattet Tests der Art


if (c = Blank) or (c = Tab) or (c = NewLine) then ...


in der leichter lesbaren Form


if c in [Blank, Tab, NewLine] then ...


zu schreiben. Ein vordefinierter Typ, der dem Set entspricht, existiert in C++ nicht.



Ein anderer aggregierter Datentyp ist der Verbundtyp. In C++ wird er Struct und in Pascal Record genannt. Er stellt ein Aggregat von Elementen beliebigen Typs dar und gruppiert sie unter einem einzigen Namen. Die Elemente werden in C++ "Member", in Pascal "Felder" genannt.


C++

Pascal

struct WohntIn
{
  int PLZ;
  char* Stadt;
};

WohntIn HeimatOrt;
···
type 
  WohntIn = record
    PLZ: Integer;
    Stadt: PChar;
  end;

var HeimatOrt: WohntIn;
···


Der Zugriff auf die Member / Felder erfolgt bei statischen Variablen über den Punktoperator, bei dynamischen Variablen über den Pfeiloperator (siehe Beispiel in der Tabelle der Operatoren weiter vorn). Verkettete Listen (Linked Lists) werden häufig mit Hilfe dynamischer Variablen dieses Datentyps realisiert.

Visual C++ und Delphi vergrößern den Verbunddatentyp beim Übersetzen standardmäßig so, daß den Membern des Structs / den Feldern des Records solche Adressen zugewiesen werden können, die "natürlich" für die Member- / Feldtypen sind und die das beste Laufzeitverhalten bei modernen Prozessoren bewirken. Bekannt unter dem Begriff des "Structure Alignment" wird so z. B. ein Struct / Record, der eigentlich nur 5 Byte belegen würde, auf 8 Byte vergrößert. Beide Sprachen bieten jedoch die Möglichkeit, dieses Verhalten zu unterbinden.


VC++

Object Pascal

struct WohntIn
{
  int PLZ;          // 4 Byte
  char Landkennung; // 1 Byte
};
···
printf("%d", sizeof(WohntIn));
// 8 Byte

type 
  WohntIn =  record
    PLZ: Integer;      // 4 Byte
    Landkennung: Char; // 1 Byte
  end;
  ···
writeln(sizeof(WohntIn)); 
// 8 Byte

#pragma pack(1)
struct WohntIn
{
  int PLZ;          // 4 Byte
  char Landkennung; // 1 Byte
};
#pragma pack(8)
···
printf("%d", sizeof(WohntIn));
// 5 Byte

type 
  WohntIn = packed record
    PLZ: Integer;      // 4 Byte
    Landkennung: Char; // 1 Byte
  end;
  ···

writeln(sizeof(WohntIn)); 
// 5 Byte


Structs können in C++ neben Daten auch Funktionen aufnehmen. Man zählt sie dann zu abstrakten Datentypen (ADT).


Ein Spezialfall des Struct- / Record- Typs stellt der Typ Union / varianter Record dar. Ein Union deklariert in C++ einen struct, in dem jedes Member dieselbe Adresse besitzt. Man kann sagen: ein Union gestattet die Aufnahme verschiedener, vordefinierter Typen. Ein varianter Record deklariert in Pascal einen Record, in dem unterschiedliche Typen in unterschiedlicher Anzahl gespeichert werden können. Anders gesagt: variante Records gestatten die Aufnahme verschiedener, vordefinierter Typen und neu zu bildender Record-Typen. Auch hier besitzt jedes Feld bzw. jeder Subrecord dieselbe Adresse.

Der Union- / Record- Typ ist so groß, daß er gerade das größte Member / die größte Felderstruktur aufnehmen kann.



C++

Pascal

union FreiZahl
{ 
  int i; 
  double d;
  char* s;
};

FreiZahl z;
// z wird als Integer-
// Typ behandelt
z.i = 5;
// z wird als Double-
// Typ behandelt
z.d = 5.0;
// z wird als String-
// Typ behandelt
z.s = '5,0';
printf("%s", z.s); 
// Ausgabe:  5,0  
printf("%d", z.i); 
// Fehl-Ausgabe
// zur Laufzeit,weil z
// inzwischen nicht mehr
// als Integer behandelt 
// wird

type FreiZahl = record
       Case Integer Of
         0: (i: Integer);
         1: (d: Double);
         2: (s: PChar);
     end;

var z: FreiZahl;
// z wird als Integer-
// Typ behandelt
z.i:= 5;
// z wird als Double-
// Typ behandelt
z.d:= 5.0;
// z wird als String-
// Typ behandelt
z.s:= '5,0';
writeln(z.s);  
// Ausgabe:  5,0
writeln(z.i); 
// Fehl-Ausgabe
// zur Laufzeit,weil z 
// inzwischen nicht mehr
// als Integer behandelt 
// wird


Pascal gestattet die Aufnahme varianter und nichtvarianter Teile in einem Record. Einen entsprechenden Typ erhält man in C++, indem man unbenannte (anonyme) Unions innerhalb von Structs deklariert.


C++

Pascal

struct Person
{
  unsigned short Nummer;
  union
  {
    struct
    { char Geburtsort[41];
      int Geburtsjahr;};
    struct
    { char Land[26];
      char Stadt[31];
      int PLZ;};
  };
};
Person = record
  Nummer: Word;
  Case Integer Of
    0: (Geburtsort: String[40];
        Geburtsjahr: Integer);
    1: (Land: String[25];
        Stadt: String[30];
        PLZ: Integer);
end;


Der Ausdruck Case Integer Of in Pascals varianten Records kann leicht mißverstanden werden. Man kann ihn sich zum Verständnis des varianten Records aber einfach wegdenken. Er stellt nur einen syntaktischen Ausdruck dar, der es dem Compiler erlaubt zu verstehen, was hier deklariert wird: bestimmte Felder (oder Feldergruppen) im Record, die denselben Speicherplatz belegen. Der Compiler versucht niemals herauszufinden, welcher Case-Zweig benutzt werden soll. Das liegt vollkommen in der Hand des Entwicklers. Wenn man in einer Variablen vom Typ Person dem Feld Geburtsort einen Wert zugewiesen hat, dann ist man im Programm selbst dafür verantwortlich, dieses Feld nicht als Land zu interpretieren.

Aus diesem Grund müssen Unions und variante Records stets mit Vorsicht eingesetzt werden. Eine Typprüfung durch den Compiler ist nicht möglich.


Unions und variante Records bilden die Grundlage für den Typ Variant. Mit dem Typ Variant können Werte dargestellt werden, die ihren Typ dynamisch ändern. Varianten werden auch dann verwendet, wenn der tatsächliche Typ, mit dem gearbeitet wird, zur Übersetzungszeit unbekannt ist. Man bezeichnet Varianten auch als sichere "untypisierte" Typen (safe "non-typed" types) oder als "spät gebundene Typen" (late-bound types).

Intern bestehen sie aus einem Datenfeld für den zu speichernden Wert und einem weiteren Feld, das den Typ angibt, der im Datenfeld enthalten ist. Varianten können ganze oder reelle Zahlen, Stringwerte, boolesche Werte, Datum-Uhrzeit-Werte sowie OLE-Automatisierungsobjekte enthalten. Object Pascal unterstützt explizit Varianten, deren Werte Arrays aufnehmen können, deren Größe und Dimensionen wechseln und die Elemente eines der oben genannten Typen enthalten können. Mit dem speziellen Variantenwert VT_EMPTY in Visual C++ und varEmpty in Object Pascal wird angezeigt, daß einer Variante bislang noch keine Daten zugewiesen wurden. Der grundlegende Varianten-Typ wird in Visual C++ nur sehr schlecht unterstützt. Der Entwickler ist für Initialisierungen und die Zuweisung an das Feld, das die Typinformation angibt, selbst verantwortlich. Eine spezielle Klasse ColeVariant kapselt den Variant-Typ ein und bietet eine bessere Unterstützung für Varianten (speziell für OLE-Verfahren).


VC++

Object Pascal

#include <oaidl.h>

VARIANT v1, v2;
  
VariantInit(&v1);
VariantInit(&v2);
  
// Typ short setzen
v1.vt = VT_I2; 
v1.iVal = 23;
  
// Typ Double setzen
v1.vt = VT_R8; 
v1.dblVal = 23.75;

VariantCopy(&v2, &v1); 
// v2 = v1
printf("Wert=%g  Typ=%d\n",
       v2.dblVal, v2.vt);


var v1, v2: Variant;




v1:= 23;
// varInteger
writeln(VarType(v1)); 

v1:= 23.75;
// varCurrency
writeln(VarType(v1)); 

v2:= v1;

writeln(v2 + ' ' + IntToStr(
        VarType(v2)));


Variablen vom Typ Variant belegen in beiden Sprachen 16 Byte; die Strukturen sind identisch.

Um die Flexibilität dieses Typs darzustellen, sei ein weiteres Beispiel angeführt. Eine Funktion Addiere übernimmt die (unbestimmten) Werte eines Varianten-Arrays, addiert sie und liefert die Summe als Ergebnis im Integer-Format.


function Addiere(Zahlen: Array of Variant): Integer;
var
  i, Sum: Integer;
begin
  Sum:= 0;
  for i:= 0 to High(Zahlen) do Sum:= Sum + Zahlen[i];
  Addiere:= Sum;
end;

Ein Aufruf könnte dann z.B. in der Form erfolgen:

writeln(Addiere([2,  8.00, ' 5,00 DM'])); 


Hier wird eine Integer-Zahl, eine Zahl vom Typ Double und ein String miteinander addiert. Die Ausgabe lautet 15.

Operationen auf Varianten laufen langsamer ab als Operationen auf statisch typisierten Werten. Um eine Vergleichsmöglichkeit zu schaffen, wurde eine zweite Funktion "AddiereStatisch" geschrieben, deren Parameter als statische Typen deklariert wurden. Eigene Messungen haben gezeigt, daß ein Aufruf der Funktion AddiereStatisch (mit statischen Parametern) 0,66 µs und ein Aufruf von Addiere (mit varianten Parametern) 40,65 µs benötigt, letzterer also ungefähr 60 mal langsamer ist. Die Messungen erfolgten auf einem Rechner mit Pentium-Prozessor (150 MHz Taktrate), 8 kByte Primär-Cache und 512 kByte Sekundär-Cache. Die Funktionen wurden jeweils 1 Mio. mal aufgerufen und die Zeiten gemessen. Es kann davon ausgegangen werden, daß die betreffenden Programmteile ausschließlich im schnellen Cache-RAM abgelaufen sind.



Prozedurale Datentypen ermöglichen es, Prozeduren und Funktionen als Einheiten zu behandeln, sie Variablen zuzuweisen und als Parameter übergeben zu können. Man unterscheidet zwei Kategorien von prozeduralen Typen: Zeiger auf globale Prozeduren und Methodenzeiger.

Zeiger auf globale Prozeduren sind Zeiger auf Prozeduren und Funktionen außerhalb von Klassen. Der Zeiger speichert die Adresse der Funktion oder Prozedur. Hier ein Beispiel, bei dem ein prozeduraler Typ als Parameter für die Funktion SetWindowsHook deklariert wird:





V

C


+

+

typedef 
  LRESULT(CALLBACK* HookProc)(int code, WPARAM wParam,
                              LPARAM lParam);


int SetWindowsHook(int aFilterType, HookProc aFilterProc);
{...

/* bei einem späteren Aufruf von SetWindowsHook erwartet der Compiler beim Parameter aFilterProc die Adresse einer Funktion, die genauso deklariert wurde wie HookProc. Die Adresse einer Funktion erhält man durch den Adressoperator &

*/

}


O

b

j.


P

a

s

c

a

l


type
  HookProc = function(code: Integer; wparam: WPARAM;
                      lparam: LPARAM): LRESULT;

function SetWindowsHook(aFilterType: Integer; 
                        aFilterProc: HookProc): Integer;
begin
  ...

{bei einem späteren Aufruf von SetWindowsHook erwartet der Compiler beim Parameter aFilterProc die Adresse einer Funktion, die genauso deklariert wurde wie HookProc. Die Adresse einer Funktion erhält man durch den Adressoperator @}

end;
  


So deklarierte Callback- (Rückruf-) Funktionen werden in Windows gewöhnlich nicht vom eigenen Programm, sondern von Windows selber aufgerufen (etliche Funktionen im Windows-API funktionieren nur in Zusammenarbeit mit vom Entwickler zu erstellenden Callback-Funktionen).


Eine Variable vom Typ Methodenzeiger kann auf eine Prozedur oder Funktionsmethode eines Objekts oder einer Klasse zeigen. In C++ stellen Methodenzeiger allerdings in Wahrheit keine Zeiger dar, sondern sind ein Adreß-Offset bezüglich eines Objektes auf ein Element dieses Objektes. Die Bezeichnung "Methodenzeiger" wurde dem eigentlich treffenderen Begriff "Methoden-Offset" vorgezogen, um die syntaktische Parallele zur Zeigersyntax und zum globalen Funktionszeiger zu unterstreichen [6, S. 387]. In Object Pascal dagegen stellen Methodenzeiger wirkliche Zeiger dar. Sie werden in der Form von zwei Zeigern realisiert: der erste Zeiger speichert die Adresse einer Methode, der zweite (versteckte Zeiger) speichert eine Referenz auf das Objekt, zu dem die Methode gehört. Prozedurale Typen werden in Object Pascal durch die Anweisung of object deklariert.

Syntax und Funktionsweise von Methodenzeigern sollen an einem Beispiel gezeigt werden. Der Methodenzeiger heißt Aufruf und soll auf sehr einfache Funktionen zeigen können, die keinen Wert als Parameter erwarten und auch keinen Wert als Ergebnis liefern. Im Konstruktor der Klasse CMyObject / TMyObject wird dem Methodenzeiger Methode1 zugewiesen, d.h. er wird mit Methode1 in der Klasse "verbunden". Ein späterer Aufruf des Methodenzeigers wird den Aufruf der Methode1 zur Folge haben. Die Implementierung der Methode1 setzt ihrerseits den Methodenzeiger auf eine andere Methode2. Ein später folgender Aufruf des Methodenzeigers wird nun den Aufruf von Methode2 zur Folge haben. Methode2 setzt den Methodenzeiger wieder auf Methode1 zurück.


VC++

Object Pascal

class CMyObject
{
  private:
    void Methode1(void);
    void Methode2(void);    
  public:    
    // Aufruf ist eine
    // Methodenzeiger- 
    // Variable
    void(CMyObject::*Aufruf)
                    (void);
    CMyObject();
};

CMyObject::CMyObject()
{
  // zuerst soll Aufruf auf
  // Methode1 zeigen
  Aufruf = Methode1;
  // identisch könnte man 
  // auch schreiben
  // Aufruf = &CMyObject::Methode1;
};

void CMyObject::Methode1(void)
{
printf("Methode 1
        aufgerufen");
// DYNAMISCHER Methoden-
// Wechsel Aufruf = Methode2;
};

void CMyObject::Methode2(void)
{
printf("Methode 2
        aufgerufen");
// DYNAMISCHER Methoden-
// Wechsel Aufruf = Methode1;
};

void main()
{    
  CMyObject* myObj;
  
  myObj = new CMyObject;
  // Operator Klassen-
  // Member-Zeiger  ->*
  (myObj->*myObj->Aufruf)();
  (myObj->*myObj->Aufruf)();
  (myObj->*myObj->Aufruf)();
};

Ausgabe:

Methode 1 aufgerufen

Methode 2 aufgerufen

Methode 1 aufgerufen

type
  TMyObject = class
  private
    procedure Methode1;
    procedure Methode2;
  public
    // Aufruf ist eine 
    // Variable vom Typ 
    // Methodenzeiger
    Aufruf: procedure of object;
    constructor Create;
  end;


Constructor TMyObject.Create;
begin
  // zuerst soll Aufruf auf
  // Methode1 zeigen
  Aufruf:= Methode1;
  // identisch könnte man auch
  // schreiben
  // Aufruf:= Self.Methode1;
end;


procedure TMyObject.Methode1;
begin
  writeln('Methode 1
           aufgerufen');
  // DYNAMISCHER Methoden-
  // Wechsel Aufruf:= Methode2;
end;

procedure TMyObject.Methode2;
begin
  writeln('Methode 2
           aufgerufen');
  // DYNAMISCHER Methoden-
  // Wechsel Aufruf:= Methode1;
end;

var
  myObj: TMyObject;

begin  
  myObj:= TMyObject.Create;
  // Aufruf, als ob "Aufruf" 
  // eine Methode wäre
  myObj.Aufruf; 
  myObj.Aufruf; 
  myObj.Aufruf; 
end.

Ausgabe:

Methode 1 aufgerufen

Methode 2 aufgerufen

Methode 1 aufgerufen



Durch Methodenzeiger ist es möglich, zur Laufzeit bestimmte Methoden von Objektinstanzen aufzurufen.

Durch den zweiten, versteckten Zeiger bei Object Pascals Methodenzeigern wird immer auf die Methode eines ganz konkreten Objekts verwiesen. Methodenzeiger ermöglichen dadurch das Erweitern eines Objekts durch Delegieren eines bestimmten Verhaltens an ein anderes Objekt. Das Ableiten eines neuen Objekts und Überschreiben von Methoden kann so manchmal umgangen werden.

Delphi verwendet Methodenzeiger, um Ereignisse zu implementieren. Alle Ereignismethoden, die den Objekten in der Anwendung beim visuellen Programmieren mit Hilfe des Objektinspektors zugewiesen werden, beruhen auf Methodenzeigern. Alle Dialogelemente erben beispielsweise eine dynamische Methode namens Click zur Behandlung von Mausklickereignissen. Die Implementierung von Click ruft, sofern vorhanden, die Mausklick-Ereignisbehandlungsroutine des Anwenders auf. Der Aufruf erfolgt mit Hilfe des Methodenzeigers OnClick. Ob der Anwender eine eigene Mausklick-Ereignisbehandlungsroutine definiert hat, wird dadurch erkannt, daß der Methodenzeiger auf einen gültigen Wert verweist, also nicht den Wert nil besitzt.


procedure TControl.Click;
begin
  if OnClick <> nil then OnClick(Self);
end;


Einmal erstellte Ereignisbehandlungsroutinen können einfach durch weitere Objekte benutzt werden. Diese Objekte lassen den entsprechenden Methodenzeiger (z.B. OnClick) dazu einfach auf dieselbe Ereignisbehandlungsroutine zeigen.

C++ kennt neben Methodenzeigern auch Zeiger auf Daten in Klassen und Objekten. Die Syntax ähnelt der von Methodenzeigern. Ein Beispiel wurde in der Tabelle der Operatoren angegeben.


Wie bei aggregierten Datentypen bestehen die Werte abstrakter Datentypen (abstract data type, ADT) meist aus verschiedenen Komponenten mit jeweils eigenem Datentyp. Der Zugriff auf die Komponenten ist über Operationen (Methoden) möglich, die Bestandteil der Datentypdeklaration sind.


C++ gestattet die Deklaration von Structs, die Inline-Funktionen und geschützte Bereiche zulassen. Durch die Schlüsselwörter private und public können die Zugriffsmöglichkeiten von außen auf die (inneren) Daten und Funktionen kontrolliert gesteuert werden.


struct WohntIn
{
  public:
    void reset() {PLZ = 0; Landkennung = ' '; used = FALSE;}
    boolean isUsed() {return used;}
    int PLZ;
    char Landkennung;
  private:
    boolean used;
};

    ···

WohntIn Ort;
Ort.reset();
if (!Ort.isUsed()) Ort.PLZ = 70806;
Ort.used = TRUE; // Fehler, kein Zugriff auf private Member


Member-Daten und Funktionen sind standardmäßig (wenn keine Schutzart angegeben wurde) public, also frei zugänglich.


Der wichtigste abstrakte Datentyp in der objektorientierten Programmierung ist die Klasse. In C++ wie in Object Pascal werden Klassen durch das Wort class deklariert. Die C++ Structs mit Member-Funktionen ähneln den Klassen. Klassen-Member in Object Pascal sind standardmäßig public deklariert, in C++ dagegen private. Aufgrund der Bedeutung und Komplexität der Klassen wird dieser Typ in einem eigenen Abschnitt "Klassen und Objekte" besprochen.


Die letzte Gruppe von Typen bilden die Zeiger-Datentypen. Werte von Zeiger-Datentypen sind Adressen (von Variablen, Unterprogrammen, usw.). Die Variablen eines Zeiger-Datentyps heißen Zeiger (Pointer). Prinzipiell können Zeiger eingeteilt werden in:



VC++

Object Pascal

Länge in Win32 (Intel)

untypisierter Zeiger

void *

Pointer

4 Byte

typisierter Zeiger

typ *

^Typ = PTyp

4 Byte


Generell sind typisierte und untypisierte Zeiger insofern identisch, als sie beide auf eine bestimmte Adresse zeigen. Bei typisierten Zeigern weiß der Compiler aber zusätzlich noch, daß die Adresse, auf die der Zeiger zeigt, der Adress-Anfang einer Variablen eines ihm bekannten Typs ist. Somit sind Prüfungen beim Übersetzen möglich. Untypisierte Zeiger können (aufgrund der Typunkenntnis) nicht dereferenziert werden.

Die drei wichtigen Zeiger-Operatoren Dereferenz-, Pfeil- und Adreßoperator werden in der Tabelle der Operatoren aufgeführt. Ein Zeiger kann auch auf "Nichts" zeigen; er enthält keine relevante Adresse. Um einen Zeiger auf "Nichts" zeigen zu lassen, weist man ihm in C++ den Wert NULL und in Pascal den Wert nil zu. Eine Unterteilung der Zeiger in verschiedene Größen, wie in 16-bit Betriebssystemen üblich (near, far, huge), ist in 32-bit Umgebungen überflüssig geworden. Alle Zeiger in Win32 sind 32 bit groß.

Ein sehr wichtiger, weil viel verwandter Zeigertyp ist der Zeiger auf Zeichenketten. Pascal folgt hier der Vorgehensweise von C++ und behandelt den Typ Array of Char und Zeiger darauf genauso.


VC++

Object Pascal

#include <string.h>


char KurzText[100];
char* PKurzText; // Zeiger

strcpy(KurzText, "Hallo Welt");
printf("%s\n", KurzText);
PKurzText = KurzText;
PKurzText = PKurzText + 3;
printf("%s\n", PKurzText);
*PKurzText = 'b';
PKurzText++;
*PKurzText = 'e';
printf("%s\n", KurzText);
KurzText[8] = '\0';
printf("%s\n", PKurzText-1);

Ausgabe:

Hallo Welt

lo Welt

Halbe Welt

be We

Uses SysUtils;

var
 KurzText: Array[0..99] of Char;
 PKurzText: PChar; // Zeiger

StrCopy(KurzText, 'Hallo Welt');
writeln(KurzText);
PKurzText:= KurzText;
PKurzText:= PKurzText + 3;
writeln(PKurzText);
PKurzText^:= 'b';
inc(PKurzText);
PKurzText^:= 'e';
writeln(KurzText);
KurzText[8]:= #0;
writeln(PKurzText-1);

Ausgabe:

Hallo Welt

lo Welt

Halbe Welt

be We


Als Alternativen zu den Char-Arrays und ihrer Zeiger-Arithmetik bieten beide Sprachen Ersatztypen an:



VC++

Object Pascal

Länge in Win32 (Intel)

String-Klasse

CString

-

4 Byte

Stringzeiger

-

String = AnsiString

4 Byte

String, kurz

-

ShortString[xxx]

mit 1 <= xxx <= 255

xxx Byte


Visual C++ und Object Pascal preisen ihren jeweiligen neuen Stringtyp als vollwertigen Ersatz des Char-Array-Typs an, der bei gleicher Flexibilität doch leichter handhabbar sei. Ein Vergleich folgt im Abschnitt "Erweiterte Stringtypen".


Neben den aufgeführten Typen gibt es in beiden Sprachen noch einige weitere vordefinierte Typen, wie Bitfelder in C++ und Dateitypen in Pascal, die hier aber nicht besprochen werden sollen.


Um Programme gut lesbar zu gestalten, ist es sinnvoll, abgeleitete Typen zu benennen. C++ bietet mit dem Schlüsselwort typedef, Pascal mit dem Schlüsselwort type diese Möglichkeit. Schreibt man ein Programm, in dem häufig 8x8 Integer-Matrizen vorkommen, kann man in


VC++

Object Pascal

statt immer wieder


int matrix_a[8][8];


zu schreiben, einmalig einen Typ definieren:

typedef int Matrix[8][8];


und diesen dann stets bei der Definition von Variablen verwenden:


Matrix a, b;

statt immer wieder

var matrix_a: 
    Array[0..7,0..7] of Integer;

zu schreiben, einmalig einen Typ definieren:

type Matrix = 
    Array[0..7,0..7] of Integer;

und diesen dann stets bei der Definition von Variablen verwenden:


var a, b: Matrix;


Auch um Arrays fest vorgegebener Länge in Object Pascal als Funktionsparameter übergeben zu können, ist eine vorherige, benutzerdefinierte Typ-Definition nötig. Statt

MeineProzedur(x: Array[0..7,0..7] of Integer);
begin
  ···
end;

muß geschrieben werden:

MeineProzedur(x: Matrix);
begin
  ···
end;.


Das Schlüsselwort type hat in Pascal eine größere Bedeutung als typedef in C++, weil Pascal-Typen keine "Tag"-Namen kennen. Auch Klassen-Definitionen (zur Festlegung der Klassen-Schnittstellen) müssen deswegen z.B. in einem type-Abschnitt aufgeführt werden:


VC++

Object Pascal

class Lanfahrzeug 
{
   ···
};
type 
  Landfahrzeug = class
     ···
  end;



2.3.3. Variablen und Konstanten


Variablen stellen Instanzen von Typen dar. Der Typ einer Variablen definiert die Menge der Werte, die sie annehmen kann, sowie die Operationen, die mit ihr durchgeführt werden dürfen. Eine Variablen-Definition schreibt neben der Typangabe, die Angabe eines Bezeichners, einer Speicherkategorie und eines lexikalischen Gültigkeitsbereichs vor. Durch ihre Vereinbarung im Programm erfolgt implizit eine statische Speicherreservierung durch den Compiler. Die gleichzeitige Initialisierung einer Variablen bei ihrer Definition ist in C++ möglich und in Pascal nicht möglich. Die Verwendung nicht initialisierter Variablen stellt einen schweren Fehler dar, der oft ein sporadisches Fehlverhalten des betreffenden Programms verursacht. Die Compiler beider Entwicklungssysteme erkennen Lesezugriffe auf nicht initialisierte Variablen und geben beim Übersetzen Warnungen aus: Visual C++ meldet "variable ... used without having been initialized" und Delphi teil dem Entwickler mit, daß "die Variable ... wahrscheinlich nicht initialisiert wurde".

Mögliche Geltungsbereiche von Variablen sind globale und lokale (Block-) Gültigkeit. Blöcke bestehen aus Deklarationen und Anweisungen. Der globale Gültigkeitsbereich wird in C++ durch den sogenannten externen Gültigkeitsbereich (external scope) gebildet, der alle Funktionen in einem Programm umfaßt. In Pascal wird der globale Gültigkeitsbereich durch den globalen Block gebildet.

Lokale Blöcke werden z.B. durch Prozedur-, Funktions- und Methodendeklaration gebildet. Variablen, die innerhalb solcher Blöcke definiert werden, heißen lokale Variablen und werden im Stack einer Anwendung abgelegt. Der Stack wird mitunter auch Stapel genannt. Er bezeichnet einen Speicherbereich, der für die Speicherung lokaler Variablen reserviert ist. Der Gesamtbedarf an Speicherplatz im Stack ergibt sich aus der Summe des Einzelbedarfs sämtlicher Funktionen und Prozeduren, die zu einem gegebenen Zeitpunkt gleichzeitig aktiv sein können. Der Bedarf an Stack-Speicher ist bei rekursiven Funktionsaufrufen besonders groß; die Gefahr eines Stack-Überlaufs besteht, wenn der Stack nicht genügend groß dimensioniert wurde.

Der Gültigkeitsbereich einer Variablen wird dadurch bestimmt, in welchem Block sie definiert wurde. Variablen sind im aktuellen Block und in allen, diesem Block untergeordneten Blöcken gültig, solange im untergeordneten Block nicht eine Variable mit gleichem Namen definiert wurde. In dem Fall wird sie von der neuen, gleichnamigen Variablen "verdrängt" und ist bis zum Blockende nicht mehr sichtbar.

Gemeinsam ist beiden Sprachen, daß Variablen definiert werden müssen, bevor sie benutzt werden können (declaration-before-use). Von dieser Einschränkung abgesehen, gestattet C++ die Variablen-Definition an beliebiger Stelle im Quelltext (on-the-spot variables), während Pascal zwingend vorschreibt, daß Variable für jeden Gültigkeitsbereich in einem gesonderten Abschnitt, der mit dem Schlüsselwort var eingeleitet wird, definiert werden müssen. Der Entwickler wird dadurch zu einer gewissen Disziplin gezwungen. Dagegen können die Variablen in C++ beliebig verstreut im Text auftauchen. Andererseits kann es bei größeren Blöcken vorteilhaft sein, wenn die Variablen erst dort definiert werden, wo sie tatsächlich gebraucht werden. Ihr Zweck ist dadurch in C++ sofort erkennbar und muß in Pascal durch geschickte, selbsterklärende Namenswahl verdeutlicht werden. Die Gültigkeit einer Variablen endet in beiden Sprachen mit dem Blockende, in dem sie definiert wurde.


VC++

Object Pascal

int x;     // globale Variable

void Test()
{
  int i;   // lokale Variable
  i = 3;
  for (int m=1; m <= x; m++)... 
  // lokale Variable m wird
  // erst innerhalb der 
  // for-Schleife definiert
}

void main(){    
  x = 8;
  i = 7;
  // Fehler, da i nur lokale
  // Blockgültigkeit innerhalb
  // der Funktion besitzt
}

var x: Integer;   // globale Var.

Procedure Test;
var
  i, m: Integer;  // lokale Var.
begin
  i:= 3;
  for m:= 1 to x do ...
end;




begin
  x:= 8;
  i:= 7; 
  // Fehler, da i nur lokale
  // Blockgültigkeit innerhalb
  // der Prozedur besitzt
end.


Variable können folgenden Speicherkategorien (auch Speicherklassen genannt) angehören:



VC++

Object Pascal

auto

Ja

Ja

extern

Ja

Nein

register explizit

Ja

Nein

register automatisch

Ja

Ja

static

Ja

Nein


Auch in Object Pascal kann "register" explizit verwendet werden, aber nur bei Funktionsdeklarationen. Die ersten drei Parameter solcherart deklarierter Funktionen übergeben ihre Werte von der Aufrufstelle zur aufgerufenen Funktion nicht über den Stack-Speicher, sondern über die CPU-Register EAX, EDX und ECX (in dieser Reihenfolge).


Alle Variablen in Pascal und alle Variablen in C++, bei denen keine explizite Angabe der Speicherkategorie erfolgte, zählen bei deaktivierter Code-Optimierung zur Kategorie auto. Wenn der Gültigkeitsbereich einer Variablen verlassen wird, wird der für sie reservierte Speicher automatisch frei gegeben. Beim Verlassen einer Funktion werden deswegen auch solche lokalen Variablen automatisch wieder gelöscht.

Variable, die mit extern definiert sind, weisen den Compiler an, die Variablen irgendwo anders zu suchen, in der gleichen oder in einer anderen Datei. Solche Variablen werden niemals ungültig.

Mit "register" definierte Variablen weisen den Compiler an, diese, sofern möglich, in schnellen Prozessor-Registern zu halten. Solche Variablen können z.B. als schnelle Schleifen-Zähler sinnvoll verwendet werden. Bei eingeschalteter Optimierung werden explizite register-Anweisungen durch Visual C++ ignoriert. Der Compiler erzeugt dann, ebenso wie Delphi, selbständig Register-optimierten Code für oft benutzte Variable und Ausdrücke. D.h. Variable sind oftmals Register-Variable, ohne daß man es explizit angegeben hat. Man wird darüber auch nicht informiert, kann es höchstens durch eine Analyse des erzeugten Assembler-Codes erkennen. Delphi geht hier sogar soweit, eine Lebenszeit-Analyse ("lifetime analysis”) für Variable vorzunehmen. Wenn eine bestimmte Variable I exklusiv in einem Codeabschnitt und eine andere Variable J in einem späteren Abschnitt exklusiv verwendet wird, verwendet der Compiler ein einziges Register für I und J.

Eine lokal definierte Variable, die mit static gekennzeichnet ist, behält ihren Wert über das Blockende hinaus. Wenn der Block erneut betreten wird, ist der alte Wert noch immer verfügbar. Extern- und Static- Variable, die nicht explizit initialisiert wurden, werden durch das System mit 0 initialisiert. Das trifft auch auf Variable strukturierter Typen zu: alle Elemente solcher Variablen werden zu 0 initialisiert. Variable der Kategorien auto und register werden nicht automatisch initialisiert.


Konstanten bekommen bei ihrer Definition einen unabänderlichen, festen Wert zugewiesen. Die Konstante ist schreibgeschützt, ist eine Nur-Lese-Variable. Es gibt typisierte und untypisierte Konstanten.

Bei der Definition typisierter Konstanten werden oft elementare Datentypen benutzt:


VC++

Object Pascal

const int Minimum = 144;
    ···
Minimum = 25;  
// Fehler, Zuweisung
// nicht erlaubt

const Minimum: Integer = 144; 
    ···
Minimum:= 25;  
// Fehler, Zuweisung 
// nicht erlaubt



Auch aggregierte und Zeiger - Datentypen können als typisierte Konstanten definiert werden:


VC++

Object Pascal

const int FestWerte[3] =
                  {1, 2, 24};
const FestWerte: 
      Array[0..2] of Integer =
                      (1, 2, 24);


Eine Besonderheit in C++ stellt die Tatsache dar, daß mit const definierte Ausdrücke nur innerhalb der Datei Gültigkeit besitzen. Sie können nicht direkt in einer anderen Datei importiert werden. Wenn eine Konstante aber trotzdem auch in einer anderen Datei gültig sein soll, so muß explizit eine Definition mit dem Schlüsselwort extern vorgenommen werden.


Neben den typisierten Konstanten existieren in beiden Sprachen untypisierte Konstanten. Sie können mitten im Quelltext als feststehende Zeichen und Zeichenketten auftreten oder als konstanter Ausdruck explizit definiert worden sein. Sie werden in C++, anders als in Pascal, nicht durch das Schlüsselwort const, sondern durch einen Ausdruck der Art #define Konstante (ohne abschließendes Semikolon) angegeben und durch den Präprozessor vor dem eigentlichen Übersetzungslauf an den entsprechenden Stellen im Quelltext ersetzt.




C++

Pascal


Numerische

Konstante

im Text

double Betrag;
  ···
Betrag = 35.20;

// hexadezimal
Betrag = 0x3F20;
var Betrag: Double;
  ···
Betrag:= 35.20;

// hexadezimal
Betrag:= $3F20; 

String-

Konstante

im Text


printf("Das ist mehr"\
       "zeiliger Text\n");

writeln('Das ist mehr' +
     'zeiliger Text'#13);

durch

explizite

Definition

#define Laenge = 13
  ···
printf("%d", Laenge);
const Laenge = 13;
  ···
writeln(Laenge);


String-Konstanten werden auch String-Literale genannt. Im Text auftretende ASCII-Zeichen wie '\0' oder #0 nennt man Zeichen-Literale. Beachtet werden muß, daß const-Ausdrücke Blockgültigkeit besitzen, während der Gültigkeitsbereich der #define-Anweisung erst durch eine zugehörige #undefine-Anweisung aufgehoben wird.

Konstanten werden von den Compilern im Code-Abschnitt des zu erzeugenden Programms eingefügt. Da konstante Ausdrücke schreibgeschützt sind, kann sich der Codeinhalt zur Laufzeit nicht ändern. Sich selbst modifizierender Code (veränderte Konstanten-Werte) würde die Optimierungsstrategien moderner Prozessoren, wie die des Pentium Pro von Intel, unterlaufen. Die hier angewandten Verfahren der überlappten, parallelen und vorausschauenden Befehlsausführung können nur dann beschleunigend wirken, wenn einmal geladener Code nicht verändert wird. Delphi gestattet jedoch aus Gründen der Abwärtskompatibilität, mit der Compiler-Anweisung {$WRITEABLECONST ON} Schreibzugriffe auf typisierte Konstanten zuzulassen.

Das Wort const kann außerdem in C++ und Object Pascal vor Funktionsparametern auftreten. Bei so deklarierten Parametern werden vom Compiler Schreibzugriffe auf diese unterbunden:


VC++

Object Pascal

void MyProc(char const x)
{
  x = 'm';  // Fehler
}

procedure MyProc(const x: Char);
begin
  x:= 'm'; // Fehler
end;



2.3.4 Anweisungen


Anweisungen (statements) beschreiben algorithmische Vorgänge, die vom Computer ausgeführt werden können. Sie steuern oft den Kontrollfluß von Programmen. Jeder Anweisung kann in Visual C++ und Object Pascal ein Label vorangestellt werden, auf das eine Bezugnahme mit der Sprunganweisung "goto" möglich ist.


Anweisungen




Mögliche Einteilung der Anweisungen



     

Anweisung

VC++

Object Pascal

1

Leere Anweisung

;
;

1

Zuweisung

i = m;
i := m;

1

Verbund- Anweisung

{
  ···
}
begin
  ···
end

2

If - Else- Anweisung

if (i == m)
  printf("gleich");
else
  printf("ungleich");

if i = m then
  writeln('gleich')
else
  writeln('ungleich');

2

Case- Anweisung

char Zeichen = 'x';


switch (Zeichen)
{
  case 'ä': printf("ae");
            break;
  case 'ö': printf("oe");
            break;
  case 'ü': printf("ue");
            break;
  case 'ß': printf("ss");
            break;
  default:
    printf("%c",Zeichen);
};

var Zeichen: Char;
Zeichen:= 'x';

Case Zeichen Of
  'ä': write('ae');  
  'ö': write('oe');
  'ü': write('ue');
  'ß': write('ss');
Else
  write(Zeichen);
End;

3

While- Anweisung

while (x != y) ...

while (TRUE) {}; 
//Endlosschleife!

while x <> y do ...

while True do;
//Endlosschleife!

3

For- Anweisung

for (i = 0; i < n; i++)
  a[i] = b[i+2];

for i:= 0 to n-1 do
  a[i]:= b[i+2];

3

Repeat- Anweisung

do
  x = x + 1;
while (x <= 100);
// x ist jetzt 101
repeat
  x:= x + 1;
until x > 100;
// x ist jetzt 101


Anmerkung:

Die Repeat-Anweisung garantiert die mindestens einmalige Ausführung der Anweisung in der Schleife.


4

Goto- Anweisung

void main()
{
  Label1: printf("L 1");
   ···
  goto Label1;
}

Label Label1;

begin
  Label1: write('L 1');
   ···
  goto Label1;
end.

4

Break- Anweisung

for (i=0; i < n; i++)
{
  if (a[i] < 0)
  {
    printf("Fehler");
    break;
  }
  ···
}

for i:= 1 to n do 
begin
  if a[i] < 0 then
  begin
    writeln("Fehler");
    Break;
  end;
  ···
end;


Anmerkung:

Die Break-Anweisung bewirkt das sofortige Beenden der While-, For- und Repeat- Schleifen. In C++ wird sie auch zum Sprung aus der Case-Anweisung benötigt.


4

Continue- Anweisung

for (i=0; i < n; i++)
{
  if (a[i] < 0)
  {    
    continue;
    // neagtive Elemente
    // werden ignoriert
  }
  ···
}

for i:= 1 to n do 
begin
  if a[i] < 0 then
  begin
    continue;
    // neagtive Elemente
    // werden ignoriert
  end;
  ···
end;


Anmerkung:

Die Continue-Anweisung bewirkt den sofortigen Beginn der nächsten Schleifeniteration (d.h. bewirkt den Sprung an das Ende der Schleife).


4

Return- Anweisung

// bei Prozeduren
void Hallo(int a)
{
  
  printf("Hallo ");
  if (a == 0) return;
  printf("Welt\n");
}

// bei Funktionen
int Kubik(int m)
{
  return m * m * m;
}

// bei Prozeduren
procedure Hallo(
           a: Integer);
begin
  write("Hallo ");
  if a = 0 then exit;
  writeln("Welt");
end;

// bei Funktionen
function Kubik
 (m: Integer): Integer;
begin
  Kubik := m * m * m;
  // exit-Aufruf ist
  // hier überflüssig
end;

// statt 
// Kubik:= m*m*m;  
// wäre auch möglich 
// Result:= m*m*m;

5

Ausnahme- Behandlung

(exception handling)

try
{
  <zu schützender Block>;
}
catch(<Typ1>)
{
  BehandlungsBlock1;
}
catch(<Typ2>)
{
  BehandlungsBlock2;
}

Uses SysUtils;

try 
  <zu schützender Block>;
except
  on <class1> do
    BehandlungsBlock1;
  on <class2> do
    BehandlungsBlock2;  
end;

5

Ausnahme- auslöse- Anweisung

(raise exception)

void IsInside(int Wert,
              int Min, 
              int Max)
{
  if ((Wert < Min) ||
      (Wert > Max))
   throw "Wert außerhalb";
}



 ···
int i;
i = 15;
try
{
  IsInside(i, 1, 10);
}
catch(char* str)
{
  printf("Exception %s 
       aufgetreten", str);
}

procedure IsInside(Wert,
                    Min, 
           Max: Integer);
begin
  if (Wert < Min) or
     (Wert > Max) then
    raise ERangeError.
            Create('Wert
             außerhalb');
end;

 ···
var i: Integer;
i:= 15;
try
  IsInside(i, 1, 10);
except
  on E: ERangeError do
    write('Exception '+
          E.Message +
          'aufgetreten');
end;

5

Ausnahme- Ende- Behandlung

(termination handling)

__try
{
  <zu schützender Block>;
}
__finally
{
  <Termination-Block>;
}

try
  <zu schützender Block>;
finally
  <Termination-Block>;
end;
    



char* str;

// Ressource belegen
str = new char[50]; 
__try
{
  // Laufzeitfehler
  // provozieren!
  strcpy(str, NULL);  
}
__finally
{
  printf("Termination");
  // Ressource sicher
  // freigeben
  delete [] str;
}
printf("Good Bye!\n");

var str: PChar;

// Ressource belegen
GetMem(str, 50);
try
  // Laufzeitfehler
  // provozieren!
  StrCopy(str, nil);
finally
  writeln('Termination');
  // Ressource sicher
  // freigeben
  FreeMem(str, 50);
end;
writeln('Good Bye!');



Anmerkung:

In beiden Programmstücken wird niemals die Ausgabe "Good Bye!" erscheinen. Nur der finally-Block kann trotz Laufzeitfehler noch sicher erreicht werden. Die Freigabe der belegten Ressourcen (hier des Speichers) kann so auf alle Fälle ordnungsgemäß ausgeführt werden.


6

With- Anweisung

Anweisung nicht 
definiert
TDatum = record
  Tag: Integer;
  Monat: Integer;
  Jahr: Integer;
end;
var BestellDatum: TDatum;

with BestellDatum do
  if Monat = 12 then
  begin
    Monat:= 1;
    Jahr:= Jahr + 1;
  end else
    Monat:= Monat + 1; 


Anmerkung:


Die With-Anweisung vereinfacht den Zugriff auf Felder von Records und Objekten.




Anmerkungen zur For - Schleife:


Die For Schleife hat in C++ folgende Form:

for (expression1; expression2; expression3)
  statement
next statement

Sie ist vollkommen äquivalent zur While Anweisung:

expression1;
while (expression2)
{
  statement;
  expression3;
}
next statement ,

wenn kein continue Ausdruck innerhalb der Schleife vorkommt. Zunächst wird expression1 ausgewertet, wobei typischerweise eine Laufvariable initialisiert wird. Dann wird expression2 ausgewertet. Wenn das Ergebnis TRUE ist, wird statement ausgeführt, expression3 ausgewertet und wieder an den Anfang der For-Schleife gesprungen. Allerdings wird nun die Auswertung von expression1 übersprungen. Diese Iteration wird so lange fortgesetzt, bis expression2 FALSE liefert. Es wird mit der Ausführung von next statement fortgesetzt.

In Object Pascal besitzt die For Schleife folgendes Aussehen:

for Laufvariable:= Anfangswert <to> | <downto> Endwert do
  statement;
next statement

Anders als in C++ entspricht die For Schleife in Object Pascal nicht der While Schleife, wenngleich ihre Wirkung ähnlich ist. In Object Pascal wird, je nachdem, ob das Schlüsselwort to oder downto benutzt wird, die Laufvariable jeweils um eins erhöht oder vermindert, währenddessen man in C++ im Ausdruck expression3 frei festlegen kann, was bei jedem Schleifendurchgang passieren soll. Die Ende- oder Abbruchbedingung der Schleife wird, anders als in C++, nur einmal ausgewertet. Folgendes ist zwar syntaktisch nicht falsch, aber unsinnig:

j:= 100;
for i:= 0 to j do dec(j);

Eine weitere Besonderheit bezüglich der Object Pascal Implementierung der For Schleife besteht darin, daß die verwendete Laufvariable eine lokal definierte Variable sein muß. Die Laufvariable darf innerhalb der Schleife nicht verändert werden.

var
  i: Integer;
begin
  for i:= 1 to 20 do inc(i);
end;

Delphi weist diesen Code bereits beim Compilieren ab und meldet, daß der Laufvariablen innerhalb der Schleife kein Wert zugewiesen werden darf. Allerdings ist es auch in C++ kein besonders guter Progammierstil, die Abbruchbedingung durch Setzen der Laufvariablen innerhalb der Schleife zu erwirken und ergibt undurchschaubaren Code.


Nach dem Ende der For Schleife ist der Wert der Laufvariablen in C++ definiert und in Pascal undefiniert. Das oft benutzte Beispiel für StringCopy darf in Pascal so nicht umgesetzt werden:

for (i = 0; i < 4; s[i++] = 'x');
s[i] = \0;

Die Variable i hat in C++ in der letzten Zeile den Wert 5. Falls i in Pascal nach der For Schleife ebenfalls den Wert 5 haben sollte, so ist das reiner Zufall. In der Praxis ist es aber so, daß die Laufvariable nach der Schleife tatsächlich meist den richtigen Wert besitzt und deswegen leicht unvorhergesehene Fehler im Programm auftreten können. Folgendes Programmstück

var
  i, j: Integer;
begin
  for i:= 1 to 4 do
    j:= i;
  writeln('Variable i = ', i);
  writeln('Variable j = ', j);
end;

liefert folgende Ausgabe:

Variable i = 5

Variable j = 4


Delphi erzeugt dabei (bei eingeschalteter Optimierung) für die Schleife folgenden Assembler- Code:

:D5D    mov    ebx,00000001   // for i:= 1 to 4 do
:D62    mov    esi,ebx        // j:= i;
:D64    inc    ebx            // entspricht i:= i + 1
:D65    cmp    ebx,00000005   // prüfen ist i = 5 ?    
:D68    jne    (D62)          // wenn nicht gleich, dann
                              // springe zu Adresse (:D62)
:D6A    mov    edx,DDC        // writeln('Variable i = ', i);
:D6F    mov    eax,1FC
:D74    call    @Write0LString
:D79    mov    edx,ebx
:D7B    call    @Write0Long
:D80    call    @WriteLn
:D8A    mov    edx,DF4        // writeln('Variable j = ', j);
:D8F    mov    eax,1FC
:D74    call    @Write0LString
:D79    mov    edx,ebx
:D7B    call    @Write0Long
:D80    call    @WriteLn

Man erkennt hier, daß Delphi für die Laufvariable i zur Optimierung des Laufzeitverhaltens Register verwendet (hier Register ebx). Falls nach der Schleife zwischenzeitlich zufällig Register ebx überschrieben worden wäre, dann würde writeln(i) einen "falschen" Wert ausgeben. Genau das Verhalten kann man beobachten, wenn man folgenden Code ausführt:

var
  i, j, k, l: Integer;
begin
  for i:= 1 to 4 do j:= i;
  for k:= 8 to 10 do j:= k;
  for l:= 12 to 16 do j:= l;
  writeln('Variable i = ', i);
  writeln('Variable k = ', k);
  writeln('Variable l = ', k);
  writeln('Variable j = ', j);
  readln;
end;

Ausgabe:

Variable i = 5

Variable k = 11

Variable l = 11

Variable j = 16


Anders als man erwarten könnte ist Variable l nun nicht 17 sondern 11.



Anmerkungen zu Ausnahmebehandlungen (Exceptions):


Exceptions sind ein Hilfsmittel zur Behandlung von Ausnahmesituationen in Programmen, insbesondere zur Fehlerbehandlung. Wenn eine Exception ausgelöst wird, verzweigt das Programm sofort zu einem Exception-Handler, der auf den Fehler reagieren kann. Durch die Benutzung von Exceptions wird der normale Kontrollfluß des Programms vom Kontrollfluß für Fehlerfälle getrennt, wodurch das Programm besser strukturiert wird und leichter zu warten ist. Der Einsatz von Exceptions garantiert, daß Fehler im ganzen Programm in einer konsistenten Weise behandelt und angezeigt werden. Belegte Systemressourcen können mit Sicherheit frei gegeben werden und Laufzeitfehler müssen ein Programm nun nicht mehr zwangsweise beenden.

Die Exceptions in Visual C++ und Object Pascal haben viele Gemeinsamkeiten; insbesondere sind sie in beiden Sprachen Typ-basierend. Beim Erzeugen einer Exception wird der Typ der Exception festgelegt, indem beim throw bzw. raise Aufruf ein Argument übergeben wird. Die Exception ist dann vom selben Typ wie der Typ dieses Arguments. Exceptions können in C++ von beliebigem Typ sein. In Object Pascal dagegen sind Exceptions nur vom Typ Objekt (bevorzugt abgeleitet von der Klasse Exception) zulässig.

Mehrere Exception-Handler können, einem try-Block nachfolgend, angelegt werden. Sie werden durch das Wort catch in C++ und except in Object Pascal eingeleitet. Der zuständige Exception-Handler, der für die Auswertung der aufgetretenen Exceptions verantwortlich ist, wird dadurch ausgewählt, indem geprüft wird, von welchem Typ die Exception ist und welcher Handler diese Exception verarbeiten kann.

Eine einmal erzeugte Exception bleibt so lange bestehen, bis sie behandelt oder die Anwendung beendet wird. Deshalb müssen auftretende Exceptions in irgendeiner Weise behandelt werden. Visual C++ und Object Pascal bieten vordefinierte Standardbehandlungsfunktionen (Default-Handler) für Exceptions an. Deswegen braucht man nicht alle möglicherweise auftretenden Exceptions in jedem try...catch / try...except Block zu behandeln.

Die Klassenbibliothek von Visual C++ (MFC) bietet, ebenso wie die Klassenbibliothek von Delphi (VCL), eine Vielzahl bereits vordefinierter Exception-Klassen an. Ein entscheidender Unterschied zwischen beiden Sprachen ist die Tatsache, daß eine Exception-Klasse in Visual C++ innerhalb des Exception-Handlers gelöscht werden muß und in Object Pascal nicht gelöscht werden darf.


catch(CException* e)
{
  // Behandlung der Exception hier.
  // "e" beinhaltet Informationen über die Exception.
  // Zuletzt ist das Löschen der Exception nötig!
  e->Delete();
}


Dagegen führt in Object Pascal die Behandlung einer Exception dazu, daß das Exception-Objekt automatisch entfernt wird.

Visual C++ unterstützt zwei verschiedene Exception-Modelle: "C++ Exception Handling" und "Strukturiertes Exception Handling". Microsoft empfiehlt das erste, ANSI-normierte Modell zu nutzen. Leider bietet dieses Exception-Handling keine Unterstützung für try/ finally Ressourcen-Schutzblöcke. Wenn man diese Schutzblöcke trotzdem verwenden möchte, ist man deswegen gezwungen, das Modell des "Strukturierten Exception Handlings" zu verwenden. Allerdings ergibt sich dabei das Problem, daß nicht beide Exception-Modelle in einer Funktion gleichzeitig verwendet werden dürfen.

Die Verwendung von try / finally Schutzblöcken ist bei der Windows-Programmierung besonders sinnvoll, weil hier verwendete Ressourcen-Objekte (wie Stifte, Pinsel und Zeichenflächen usw.) immer explizit vom Programm freigegeben werden müssen. Eine Verschachtelung der try / finally Anweisungen ist hier notwendig. Die Freigabe belegter Ressourcen erfolgt meist umgekehrt zur Belegungsreihenfolge.


verschachteltes Try-Final


Auch das beliebige Verschachteln von try / except - Blöcken und try / finally - Blöcken ist möglich.

Damit Standard-Exceptions in Object Pascal funktionieren muß die Unit SysUtils in's Programm eingebunden werden. Die Basisklasse aller Exceptions mit dem Namen Exception ist in dieser Unit definiert.


2.3.5 Programmstruktur


Programme sind eine Sammlung von Funktionen, Prozeduren und Deklarationen. C++ und Pascal sind block-strukturiert. Funktionen und Prozeduren können in Pascal beliebig tief verschachtelt sein, d.h. jede Funktion kann weitere Unterfunktionen definieren. C++ erlaubt für Funktionen nur eine Schachtelungstiefe von 1, d.h. Funktionen können keine weiteren Unterfunktionen definieren. Beide Sprachen erlauben rekursive Aufrufe.



VC++

Object Pascal

function (call by value)

double Quadrat(int x)
{
  return x * x;
}

function Quadrat(x: Integer):
                       Double;
begin
  Result:= x * x;
end;

procedure (call by value)

void Quadrat(int x)
{
  printf("%e", x * x);
}

procedure Quadrat(x: Integer);
begin
  writeln(x * x);
end;

leere Argumen- tenliste

void Test()
{
  printf("Hallo");
}

Aufruf: Test();
// mit Klammer

procedure Test;
begin
  writeln("Hallo");
end;

Aufruf:  Test;  
// ohne Klammer

Variablen Argu- ment (call by reference)

void Tausch
     (int& i, int& j);
{
  int t = i;
  i = j;
  j = t;
}

procedure Tausch
          (var i, j: Integer);
var t: Integer;
begin
  t:= i;
  i:= j;
  j:= t;
end;

Standard- Argu- mente

int Potenz(int a, 
           int n = 2)
{
  int Result = a;
  int i;
  for (i=1; i<n; i++)
    Result = Result*a;
  return Result;
};

mögliche Aufrufe:
   Potenz(3);
   Potenz(5,2);
   Potenz(3,4);

Object Pascal 
unterstützt keine
Standard-Funktions-
Argumente


Die Funktion Potenz zur Lösung der Aufgabe an besitzt das Standard-Argument n = 2. Die Funktion kann deswegen wahlweise auch mit nur einem Parameter aufgerufen werden. Als Ergebnis liefert Potenz in diesem Fall das Quadrat der Zahl a. Standard-Argumente müssen keine Konstanten sein; auch globale Variablen oder Funktionsausdrücke sind zulässig. Die Standard-Argumente müssen immer am Ende der Parameterliste stehen. Durch die Verwendung von Standard-Argumenten lassen sich keine Effizienz-Vorteile erzielen, da bei einem Funktionsaufruf immer alle Parameter übergeben werden (gegebenenfalls der Standardwert). Überdies bergen Standard-Argumente Risiken, da versehentlich nicht übergebene Argumente durch den Compiler nicht erkannt werden können.


C++ gestattet das Überladen von Funktionen (function overloading). Hinweis: Die Dokumentationen zu Visual C++ und Object Pascal benutzen den Begriff "Überladen" mit ganz verschiedener Bedeutung: Visual C++: Funktion mit veränderter Parameterliste deklarieren oder für einen Operator eine neue Bedeutung definieren. Obj. Pascal: als Synonym für das Überschreiben von Klassen-Methoden

Beim Überladen werden einer Funktion mehrere Bedeutungen zugeordnet. Mehrere Funktionen werden dazu mit gleichem Namen, aber unterschiedlichen Parametertypen und / oder Parameteranzahl definiert. Beim Aufruf einer so überladenen Funktion wird beim Übersetzen vom Compilier durch Typvergleich des aktuellen Parameters mit dem formalen Parameter die entsprechende (zutreffende) Funktion ausgewählt. Sind alle Typen der formalen Parameter ungleich dem Typ des aktuellen Parameters, so wird versucht, durch Typkonvertierung die zugehörige Funktion zu bestimmen. Zu überladende Funktionen müssen alle im selben Gültigkeitsbereich deklariert sein. Weiterhin muß der Ergebnistyp bei allen überladenen Funktionen gleich sein.

Object Pascal gestattet das Überladen von Funktionen nicht. Die einzige Möglichkeit, das Verhalten von C++ nachzuahmen besteht darin, Funktionen mit sehr ähnlichem Namen zu deklarieren. So könnten sich z.B. die Namen mehrerer Funktionen nur im letzten Buchstaben unterscheiden, der damit angibt, mit welchem Parameter-Typ die Funktion aufzurufen ist.


VC++

Object Pascal

void Drucke(int i)
{
  printf("%d", i);
}

void Drucke(char* s)
{
  printf("%s\n", s);
}

void Drucke(double k, int i)
{
  printf("%e %d", k, i);
}

 ···

Drucke(16);
Drucke(23.8, 12);
Drucke("Hallo Welt");

procedure DruckeI(i: Integer);
begin
  writeln(i);
end;

procedure DruckeS(s: String);
begin
  writeln(s);
end;

procedure DruckeDI(k: Double;
                   i: Integer);
begin
  writeln(k,' ',i);
end;

 ···
DruckeI(16);
DruckeDI(23.8, 12);
DruckeS('Hallo Welt');


Das Überladen von Funktionen wird mitunter auch als "ad-hoc-Polymorphismus" bezeichnet. Während man bei dem auf Klassen basierenden (echten) Polymorphismus (pure polymorphism) eine Klasse verändert, um ein anderes Verhalten zu bewirken, so ändert man bei dem auf Funktionen basierenden ad-hoc-Polymorphismus einen oder mehrere Funktions-Parameter, um ein anderes Verhalten zu erzielen.

Wenn in C++ bei überladenen Funktionen neue Argumente als Standard-Argumente deklariert werden, kann ein späterer Funktionsaufruf unter Umständen nicht eindeutig einer konkreten Funktion zugeordnet werden.

void Drucke(int i)
{
  printf("%d", i);
}

// überladene Funktion mit Standard-Argument
void Drucke(int i, int j=5)
{
  printf("%d %d", i, j);
}

void main()
{ 
  Drucke(3);
  // Fehler; welche Drucke-Funktion ist gemeint?
}

Der Compiler bricht die Übersetzung mit einer Fehlermeldung der Art "ambiguous call to overloaded function" ab. Der Funktionsaufruf ist mehrdeutig.


Aufrufkonventionen für Funktionen und Prozeduren:


Reihenfolge der Argumente

VC++

Object Pascal

von rechts nach links


__cdecl

cdecl

die ersten zwei in Registern, folgende von rechts nach links


__fastcall

-

von links nach rechts


WINAPI

pascal

die ersten drei in Registern, folgende von links nach rechts


-

register

von rechts nach links


__stdcall

stdcall


Bsp.:


void __stdcall Tausch(int& i, int& j) 
{ 
  ... 
}

procedure Tausch(var i, j: Integer); stdcall;
begin
  ...
end;


Wenn keine expliziten Angaben bei der Funktionsdeklaration erfolgen, wird in Visual C++ und Object Pascal die Standard-Aufrufkonvention benutzt. Standard-Aufrufkonvention ist in:

Visual C++:

__cdecl

Object Pascal:

register

Win32 - API:

stdcall


Funktionen können in C++ mit dem Vorsatz "Inline" versehen werden.

inline int Quadrat(int m)
{
  return m * m;
}


An jeder Stelle im Programm, an dem der Compiler Quadrat vorfindet, ersetzt dieser den Funktionsaufruf durch den Anweisungsteil des Funktionsrumpfes. Es liegt somit kein Funktionsaufruf mehr vor. Die Inline-Deklaration von Funktionen ist nur sinnvoll, wenn der Funktionsrumpf sehr klein ist.

Um den Geschwindigkeitsvorteil von Inline-Funktionen gegenüber normalen Funktionen in Visual C++ und Object Pascal einschätzen zu können, wurden eigene Messungen angestellt. Die oben angegebene Funktion Quadrat wurde dazu 90 Millionen mal aufgerufen und die benötigten Zeiten gemessen.


Laufzeit bei Integer


Weitere Messungen sollten das Zeitverhalten klären, wenn statt der Integer-Ganzzahlen Fließkomma-Zahlen vom Typ Double als Parameter und Rückgabewert der Funktion Quadrat eingesetzt werden.


Laufzeit bei Floating


Im Extremfall können also Inline-Funktionen bis zu 3-4 mal schneller als normale Funktionen sein.


Eine weitere Ersatz-Form für sehr kleine Funktionen stellen in C++ Makros dar. Diese werden durch den Präprozessor vor dem eigentlichen Übersetzen im Quelltext expandiert.


#define Quadrat(m) ((m) * (m))
#define Kubik(m)   (Quadrat(m) * (m))   
   ···
printf("%d", Kubik(2));  
// wird expandiert zu printf("%d", 2 * 2 * 2);


Bei Makros ist auf sorgsame Klammersetzung zu achten, um Expansions-Fehler, wie im folgenden Beispiel, zu vermeiden:


#define Quadrat(m) m * m
   ···
printf("%d", Quadrat(3+2));
// wird expandiert zu printf("%d", 3+2 * 3+2);
// man erhält als Ergebnis nicht 25 sondern 11


Makros bieten keine Typsicherheit. Inline-Funktionen und Makros müssen in Object Pascal durch normale Funktionen realisiert werden.


Eine wesentliche Erweiterung der Sprache C++ stellen Schablonen (Templates) dar. Sie sind in Object Pascal nicht implementiert. Funktions-Schablonen gestatten mit Hilfe des Schlüsselworts "template" die Definition einer Familie von Funktionen, die mit verschiedenen Typen arbeiten können. Sie stellen sogenannten "parametrischen Polymorphismus" (parametric polymorphism) zur Verfügung, der es erlaubt, den selben Code mit verschiedenen Typen zu nutzen, wobei der Typ als Parameter im Code angegeben wird.


C++: ohne Funktions-Schablone

C++: mit Funktions-Schablone

// Minimum - Funktion für
// Integer-Zahlen
int Min(int a, int b)
{
  if (a < b) return a;
  else return b;
};

// Minimum - Funktion für
// Double-Zahlen
double Min(double a, double b)
{
  if (a < b) return a;
  else return b;
};

// Minimum - Funktion für 
// Char-Zeichens
char Min(char a, char b)
{
  if (a < b) return a;
  else return b;
};
template <class Typ> 
Typ Min(Typ a, Typ b)
{
  if (a < b) return a;
  else return b;
};

void main()
{ 
  int i = Min(8, 3);
  double d = Min(8.2, 3.1);
  char c = Min('Ü', 'Ö');
}


Durch die Angabe von "class" im Ausdruck template <class Typ> wird in diesem Zusammenhang nur angegeben, daß Typ für einen noch zu spezifizierenden Typ steht. Durch die Definition der Schablone wird noch keine Funktion Min definiert. Eine Definition mit konkreter Typangabe wird vom Compiler erst dann angelegt, wenn im Programm ein Funktionsaufruf erfolgt (im Beispiel Min(8, 3)). Der Compiler ersetzt dann den abstrakten Typ durch einen konkreten Typ; er "instanziert" eine spezielle Version der Schablone.

Durch den Einsatz von Schablonen gelangt man zu großer Flexibilität, kann Redundanzen im Code vermeiden und muß doch trotzdem nicht auf die strikte Typprüfung durch den Compiler verzichten. Ihr Einsatz kann zudem eine Verkleinerung des vom Compiler erzeugten Programms bewirken. Die Beschreibung von Schablonen ist nicht nur für Funktionen sondern auch für Klassen möglich (vgl. "2.3.6.9 Klassen-Schablonen").


Die Funktion main() oder wmain() wird in Visual C++ als Startpunkt für die Programmausführung benutzt. Programme, die wmain() benutzten, werden Programmstart-Argumente im 16 bit breiten UNI-Zeichencode übergeben. Jedes VC++ Programm muß eine main() oder wmain() Funktion besitzen. Der Startpunkt aller Pascal-Programme wird durch einen globalen Funktionsblock gebildet, der mit begin eingeleitet und mit end. beendet wird. Ebenso wie jedes Programm, muß auch jede Unit mit dem Schlüsselwort end, gefolgt von einem Punkt enden: end.

Größere Programme sollte man nicht monolithisch in einem Block erstellen, sondern über mehrere Dateien verteilen. Um die einzelnen Programme zusammenzuführen oder auch bereits existierende Bibliotheken einem Programmtext hinzuzufügen, verwenden beide Compiler unterschiedliche Konzepte. In C++ werden Programmteile in mehreren Dateien mit der Endung CPP ( = C Plus Plus) implementiert. In getrennten Header Dateien (*.H) werden die Schnittstellen der CPP-Dateien veröffentlicht. Durch die Verwendung der "#include"- Präprozessor-Anweisung können einzelne Header-Dateien in einem CPP-Modul eingefügt werden. Dem Compiler sind dadurch externe Deklarationen mit exakten Typangaben bekannt, so daß er alle Programm-Module erfolgreich übersetzen kann. Nach dem Übersetzen werden die nun übersetzten Module (sogenannte Objekt-Dateien mit der Endung OBJ) durch den Linker zusammengebunden. Es entsteht das ausführbare Programm (Standard-Endung EXE) oder eine Bibliothek (Standard-Endung DLL).

Objekt Pascal verwendet das sogenannte Konzept der Units. Eine Hauptdatei mit der Dateiendung DPR (= Delphi PRojekt) oder PAS (= PAScal) wird durch einen Ausdruck program <ProgrammName> oder library <LibraryName> eingeleitet. Alle weiteren Programm-Module werden in Units (Dateiendung PAS) untergebracht. Sie bestehen aus zwei Abschnitten: einem mit dem Schlüsselwort Interface eingeleiteten, öffentlichen und einem mit dem Schlüsselwort Implementation eingeleiteten, privaten Implementierungsteil. Im Interface-Abschnitt von Pascal-Units werden wie bei Visual C++ die Schnittstelleninformationen in den Header-Dateien veröffentlicht. Der Implementation-Teil beinhaltet die tatsächliche Realisierung einzelner Programmteile und entspricht dem Inhalt einzelner CPP-Module. Übersetzte Units erhalten die Endung DCU (= Delphi Compiled Unit).

Durch das Schlüsselwort "uses" können einem Pascal-Programm oder einer Unit andere Pascal-Units bekannt gemacht werden. Zu einzelnen Units können sogenannte Formulare gehören, die das visuelle Gestalten von Programmoberflächen erlauben. Sie haben stets denselben Namen wie die zugehörige Unit, besitzen aber die Dateiendung DFM (=Delphi ForMular).


C++

Object Pascal

Syntax:

 #include "Datei"
 #include <Datei>


Datei darf Pfadangaben enthalten. Bei der ersten Version wird Datei zunächst im aktuellen Verzeichnis gesucht. Nur wenn sie dort nicht gefunden wurde, wird sie im Pfad gesucht. Pro Zeile darf nur eine include Anweisung erscheinen.


Wirkung:

Der ganze Inhalt von Datei wird durch den Präprozessor an der Stelle im Quelltext eingefügt, an dem die #include Anweisung auftritt.


Syntax 1:

 program | library Name;
 uses Unit1 [in 'Datei1'],
      ... , 
      UnitN [in 'DateiN']; 

Syntax 2:

 Unit Name;
 interface
 uses Unit1, ... , UnitN;
  ...
 implementation  
 uses UnitN+1, ... , UnitM;
  ...
 
 initialization
  ...
 finalization
  ...
 end.

Bei Programmen und Bibliotheken können Datei- und Pfadangaben dem Schlüsselwort in folgen.

Das Schlüsselwort uses darf in Programmen und Bibliotheken nur ein einziges Mal, in Units nur einmal pro Abschnitt auftreten. Alle einzubindenden Units müssen in einer kommaseparierten Liste aufgeführt werden.

Bsp.:


// Test Programm, 
// das nichts tut

#include <windows.h>
#include "c:\bwcc32.h"
#include <string.h>

void main()
{
}

Bsp.:


program Test;
{$APPTYPE CONSOLE}

uses
  Windows, 
  BWCC32 in 'C:\BWCC32.PAS',
  SysUtils;

{$R Test.res} // Einbinden von
              // Ressourcen   
begin
end.



Auch in Delphi kann der Inhalt ganzer Dateien durch die Compiler-Anweisung {$I Datei} oder {$INCLUDE Datei} in Quelltexte eingefügt werden.

Am letzten Beispiel erkennt man einen weiteren Unterschied: anders als in Visual C++ werden in Delphi zusätzliche Dateien, die zum endgültigen Programm dazugelinkt werden sollen, direkt im Programmtext über Compiler-Anweisungen eingebunden. Ressource-Dateien werden mittels {$R Datei.RES} oder {$RESOURCE Datei.RES}, Objektdateien aus anderen Programmiersprachen im INTEL-Object-Format durch {$L Datei.OBJ} oder {$LINK Datei.OBJ} hinzugefügt. In Delphi arbeiten, durch das Konzept der Units bedingt, Übersetzer und Linker wesentlich enger zusammen, als dies in Visual C++ der Fall ist. Nachdem eine Anwendung einmal übersetzt wurde, werden Programmteile, die sich nicht verändert haben, nicht neu übersetzt. Sie können im Speicher gehalten und direkt vom Speicher in die zu bildende EXE-Datei eingebunden werden. Nicht benutzte Funktionen und Variable werden nicht mit in das zu bildende Programm aufgenommen (smart linking).

Aber auch Visual C++ bietet ein "inkrementelles Linken", bei dem nur veränderte Programmteile in der entstehenden Anwendung neu eingebunden werden. Kürzere Linkzeiten sind die Folge. Die Turn-Around Zeiten in Visual C++ sind aber im Vergleich zu Delphis Compilier- und Link- Zeitbedarf spürbar länger.


Die zentrale Verwaltung eines jeden VC-Programms stellt die Projektdatei dar (Projektname.MDP), die man bei Microsoft "Workspace" nennt. In dieser werden, neben den Einstellungen auch für mehrere Erstellungsoptionen, intern alle zum Projekt gehörenden Dateien vermerkt. Beim Übersetzen erstellt Visual C++ eine Make-Datei, mit Hilfe derer beim Übersetzen zwei voneinander getrennte Vorgänge gesteuert werden: erstens der Compile- und im Anschluß daran der Link - Vorgang. Delphi kann dagegen nur eine Erstellungsoption pro Projekt verwalten und speichert diese in einer Datei mit dem Namen Projektname.DOF ab.


Bei der Verwendung von Bibliotheken fremder Hersteller kann es leicht passieren, daß in einer Header-Datei oder einer Delphi-Unit ein Bezeichner (Identifier) definiert wurde, der bereits in einer anderen Header-Datei bzw. Delphi-Unit verwendet wird. Natürlich kann für unterschiedliche Aufgaben nicht zweimal derselbe Name verwendet werden; der Compiler wird die Übersetzung an der entsprechenden Stelle mit einer Fehlermeldung abbrechen.



VC++

Object Pascal

// Lib1.h

char Drucke(char c);
...

// Lib2.h

void Drucke(int Len; 
            char* Text);
...
Unit Lib1;
interface
  function Drucke(c: char);
  ...

Unit Lib2;
interface
  function Drucke(Len: Integer;
                  Text: PChar);
  ...


Zur Lösung dieses Problems können in Visual C++ "Namensbereiche" (Namespaces) definiert werden. Alle in einem Namensbereich deklarierten Bezeichner bekommen einen frei definierbaren Zusatzbezeichner hinzugefügt, der es weniger wahrscheinlich macht, daß Mehrdeutigkeiten auftreten können.

Alle Bezeichner in einer Object Pascal-Unit besitzen implizit so einen Zusatz-Bezeichner, der den Namen der Unit trägt.


VC++

Object Pascal

// Lib1.h
namespace Lib1
{
  char Drucke(char c);
  ...
}

// Lib2.h
namespace Lib2
{
  void Drucke(int Len,
              char* Text);
  ...
}

Unit Lib1;
interface
  function Drucke(c: char);
  ...





Unit Lib2;
interface
  function Drucke(Len: Integer;
                  Text: PChar);
  ...


Ein Aufruf erfolgt dann in der Art:


VC++

Object Pascal

#include "Lib1.h"
#include "Lib2.h"


Lib1::Drucke('a');
Lib2::Drucke(10, "Hallo Welt");

Uses
  Lib1, Lib2;


Lib1.Drucke('a');
Lib2.Drucke(10, 'Hallo Welt');


Selbst einige Bezeichner in der Klassenbibliothek Delphis führen zu Namenskonflikten mit Funktionen des Windows-API, so daß sogar hier manchmal die Unit-Namen mit angegeben werden müssen (z.B. VCL-Klasse Graphics.TBitmap und Record Windows.TBitmap oder VCL-Methode OleAuto.RegisterClass(...) und Funktion Windows.RegisterClass(...)).


Zurück

Zurück zum Inhaltsverzeichnis

Weiter

Weiter in Kapitel 2.3.6