1 Standard-Datentypen

1.1 Variablen-Deklaration - wieso denn das?
1.2 Bisher bekannte Datentypen
1.3 Der kleinste mögliche Datentyp: Boolean
1.4 Typumwandlungen

Zum Inhaltsverzeichnis Zum nächsten Kapitel


1.1 Variablen-Deklaration - wieso denn das?

Pascal, Java, C, all das sind "typ-strenge" Sprachen, d.h.: jede Variable muss vor ihrer ersten Verwendung im Programm "deklariert" werden. Dies ist durchaus nicht in allen Programmiersprachen so, und manche Hobby-Programmierer sehnen sich beim ersten Kontakt mit einer typ-strengen Sprache nach dem Komfort vieler "Basic"-Dialekte zurück, bei denen man eine Variable im Programm verwenden kann, ohne dies in irgend einer Weise vorbereitet zu haben. Wozu ist dieser Aufwand in den "ernsthaften" Programmiersprachen eigentlich nötig?

Wenn Variablen im Speicher abgelegt werden, geschieht dies so, dass an die zugehörige Speicherstelle ein bestimmtes Bitmuster geschrieben wird, welches den Variableninhalt darstellt. Der Typ der Variablen gibt Auskunft über das Format, d.h. darüber, wie dieses Bitmuster zu interpretieren ist, was es also "bedeutet"! Programmiersprachen, wie auf Deklarationen verzichten, machen zur Laufzeit eigene Annahmen über den Typ der verwendeten Variablen. Und die werden nicht immer genau mit den Absichten des Programmierers übereinstimmen, was zu schwierig zu erkennenden Fehlern führen kann. In den typ-strengen Sprachen hingegen muss der Programmierer genau spezifizieren, was er will - und Klarheit ist immer ein Vorteil.

Betrachten wir ein konkretes Beispiel: die Deklaration
in Delphi:
     var n : Integer;
in Java:
     int n;
erledigt zwei Aufgaben:
  1. sie weist den Compiler an, in dem Programm dafür zu sorgen, dass zur Laufzeit für eine im Programmtext "n" genannte Variable Speicherplatz reserviert wird;
  2. die Angabe des Typs der Variablen (in Delphi "Integer" und in Java "int") liefert die Information, in welchem Format die Variable im Speicher steht, also wieviele Bytes Speicherplatz sie benötigt und was die einzelnen Bits dabei bedeuten.

Liest ein Delphi- oder Java-Compiler die obige Deklaration, dann wird er 4 Byte Speicherplatz für die Variable n reservieren und dafür sorgen, dass die Bytes in umgekehrter Reihenfolge(!) ihrer Wertigkeit in den Speicher geschrieben werden. Nach der Zuweisung
in Delphi:
     n := 137682; 
in Java:
     n = 137682; 
stehen also (wegen 137682 = 219D2h) die Bytes
     D2h 19h 02h 00h
hintereinander an der für n reservierten Stelle des Speichers.

Da es eine ganze Menge elementarer Datentypen gibt, ist der Platzbedarf einer Variablen im Speicher alleine noch kein eindeutiger Hinweis auf den Typ der Variablen:
In Delphi benötigen die Datentypen "Byte" und "AnsiChar" beide jeweils 1 Byte. Es ist aber durchaus ein Unterschied, ob der Speicherinhalt "4Dh" die (Byte-)Zahl "77" oder den Buchstaben "M" bedeuten soll! In Java benötigen die Datentypen "int" und "float" jeweils 4 Byte. Interpretiert man die hexadezimale Byte-Folge "41 7D D4 78h" als Integerzahl, dann kodiert sie die ziemlich große Zahl "1 098 765 432", also etwa 1,1 Milliarden; interpretiert man aber dieselbe Byte-Folge als "float"-Zahl, dann kodiert sie die Kommazahl "15,864372", also knapp 16!





1.2 Bisher bekannte Datentypen

In Delphi und Java sind schon eine ganze Menge von numerischen Standard-Datentypen "von Haus aus" verfügbar. Die wichtigsten davon sind glücklicherweise in nahezu allen wichtigen Programmiersprachen auf dieselbe Art und Weise implementiert. Lediglich die Bezeichnungen unterscheiden sich:

Variablen-Art Typ-Name
Delphi / Java
Werte-Bereich Belegter
Speicher
Ganze Zahlen Byte / byte 0 .. 255 1 Byte
Integer / int 2 147 483 648 .. 2 147 483 647 4 Byte
Fließkomma-Zahlen Single / float 1.5 x 10^45 .. 3.4 x 10^38,
7-8 Dez.-Stellen genau
4 Byte
Double / double 5.0 x 10^324 .. 1.7 x 10^308,
15-16 Dez.-Stellen genau
8 Byte


Schwieriger wird es bei der Kodierung von (Schrift-)Zeichen und Zeichenketten:

In Delphi gibt für die Kodierung von Buchstaben den 1 Byte großen Typ "AnsiChar". Da 1 Byte 256 verschiedene Werte annehmen kann, ist die Anzahl der damit kodierbaren Zeichen also auf 256 begrenzt. Eine Zeichenkette aus solchen AnsiChar-Buchstaben bildet dann einen "ShortString"; dieser kann maximal 255 Zeichen enthalten, was somit die maximale Länge einer Zeile ist.

Variablen-
Art
Typ-
Name
Werte-
Bereich
Belegter
Speicher
Zeichen AnsiChar {256
ASCII-
Zeichen}
1 Byte
Zeichen-
 ketten
ShortString {n Zeichen,
n ≤ 255}
(n+1) Byte
In Java gibt für die Kodierung von Buchstaben den 2 Byte großen Typ "char". Da 2 Byte 65536 verschiedene Werte annehmen kann, ist die Anzahl der damit kodierbaren Zeichen also auf 65536 begrenzt. Eine Zeichenkette aus solchen AnsiChar-Buchstaben bildet dann einen "String"; dieser kann maximal 2 147 483 647 Zeichen enthalten, was somit die maximale Länge einer Zeile ist.

Variblen-
Art
Typ-
Name
Werte-
Bereich
Belegter
Speicher
Zeichen char {65536
UniCode-
Zeichen}
2 Byte
Zeichen-
 ketten
String {n Zeichen,
n ≤ 2147483647}
≥ 2*n+4 Byte

Diese Tabellen sind bei weitem nicht vollständig; es sind nur die für uns bisher wichtigsten Datentypen aufgeführt. Die Angaben zum belegten Speicher können bei den verschiedenen Delphi-Versionen von den obigen Angaben abweichen, speziell bei den Zeichenketten, deren Implementierung in der Vergangenheit mehrfach überarbeitet wurde. Und natürlich gibt es auch in Delphi "Multibyte-Zeichen" und die dazugehörigen "langen Strings". Wer's genauer wissen will, kann Details dazu in der Online-Hilfe zu Delphi finden (Stichwort "Typen").




1.3 Der kleinste mögliche Datentyp: Boolean

In der obigen Tabelle fehlt noch ein elementarer Datentyp, der bisher noch nicht explizit aufgetaucht ist, den wir aber sehr wohl schon verwendet haben. Es handelt sich dabei um den Datentyp Boolean, der nur die beiden "Wahrheitswerte" true und false (also wahr und falsch) annehmen kann. Obwohl die damit transportierte Informationsmenge naturgemäß nur 1 Bit groß ist, belegt eine Variable von diesem Typ im Speicher ein ganzes Byte! Dies liegt daran, dass aus technischen Gründen auf den Speicher stets (mindestens) byte-weise zugegriffen wird.

Und wo sollen diese seltsamen Variablen in unseren bisherigen Programmen schon vorgekommen sein? Indirekt überall da, wo eine Bedingung programmiert wurde! Also zum Beispiel in der Entscheidungsbedingung jeder IF-ELSE-Konstruktion und in der Durchführungsbedingung jeder WHILE-Schleife! Allerdings haben wir an diesen Stellen bisher nie einzelne Variablen benutzt, sondern stets mit Hilfe von Termen formulierte Aussagen, die sich dann zur Laufzeit als wahr oder falsch erweisen konnten - in Abhängigkeit von den Werten der in den Termen verwendeten Variablen. Die Bedingungen waren also stets "boolsche Ausdrücke"; wenn sie aber zur Laufzeit "ausgerechnet" wurden, dann konnte es stets nur 2 mögliche Ergebnisse geben: wahr oder falsch!

Das Ergebnis der Berechnung eines boolschen Ausdrucks kann nun auch einer boolschen Variablen zugewiesen werden - genau wie das Ergebnis einer numerischen Berechnung in einer Double-Variablen gespeichert werden kann. Man wird boolsche Variablen vor allem in solchen Fällen verwenden, wo der berechnete "Wahrheitswert" an mehreren Stellen des Programms benötigt wird. Das prinzipielle Vorgehen zeigt das folgende

Beispiel:

Boolsche Variablen tauchen aber auch in den Komponenten auf, die wir in unsere Programme einbauen: das klassische Beispiel dafür ist die "Checkbox", die zur Laufzeit ein Häkchen tragen kann oder nicht. Dies wird in einer boolschen Variablen vermerkt und kann dort jederzeit vom Programm überprüft werden, z.B.:
In Delphi:
     If CheckBox1.Checked then
       ......
     else
       ......;
   
Anfänger neigen bei der Formulierung der Bedingung häufig dazu, statt der boolschen Variablen "CheckBox1.Checked" ganz ausführlich den boolschen Ausdruck "CheckBox1.Checked = True" zu schreiben. Dies liefert zwar stets ebenfalls das richtige Ergebnis und ist daher nicht falsch. Eleganter ist jedoch die Kurzform; machen Sie sich daher klar, dass diese tatsächlich schon das jeweilige Ergebnis enthält!
In Java:
     if (checkBox1.State) {
       .........
     } else {
       .........
     };
   
Anfänger neigen bei der Formulierung der Bedingung häufig dazu, statt der boolschen Variablen "CheckBox1.State" ganz ausführlich den boolschen Ausdruck "CheckBox1.State == true" zu schreiben. Dies liefert zwar ebenfalls stets den korrekten Wahrheitswert und ist daher nicht falsch. Eleganter ist jedoch die Kurzform; machen Sie sich daher klar, dass diese tatsächlich schon das jeweils korrekte Ergebnis enthält!


Man kann mit boolschen Werten auch rechnen: für die Verknüpfung boolscher Variablen stellen viele Programmiersprachen die logischen Operatoren NOT, AND und OR zur Verfügung. Für die Priorität dieser Operatoren gilt üblicherweise die Regel "NOT vor AND vor OR" (analog zu "Hoch vor Punkt vor Strich" bei den üblichen mathematischen Operatoren).

In Delphi werden NOT, AND und OR mit genau diesen Wörtern als Bezeichner bereitgehalten. Damit lassen sich auch kompliziertere zusammengesetzte Bedingungen formulieren, wie z.B.:
 If Checkbox1.Checked AND NOT Checkbox2.Checked then
   ........;

 oder:

 If Checkbox1.Checked OR ((s >= 3) AND (s <= 7)) then
   .........;
Die äußere Klammer um die beiden Teilbedingungen im letzten Beipiel ist eigentlich überflüssig, da AND ja ohnehin stärker bindet als OR. Häufig setzt man sie trotzdem, damit die Bedingung leichter lesbar wird. Beachten Sie aber, dass in Delphi die logischen Operatoren (also NOT, AND und OR) stärker binden als die Vergleichsoperatoren (<, > usw.). Die inneren Klammern um die Vergleiche sind also nötig!

In Java werden die logischen Operatoren NOT, AND und OR durch jeweils eigene Symbole ausgedrückt: NOT wird durch ein "!" dargestellt, AND durch das "&&" und ODER durch "||" (natürlich in Termen jeweils ohne die Anführungszeichen). Damit lassen sich auch kompliziertere zusammengesetzte Bedingungen formulieren, wie z.B.:
 if (Checkbox1.State && !Checkbox2.State) {
   ....... }

 oder:

 if ( Checkbox1.Checked || ((s >= 3) && (s <= 7)) ) {
   ....... }
Die mittlere und die inneren Klammern um die beiden Teilbedingungen im letzten Beipiel sind eigentlich überflüssig, da in Java > und < stärker binden als AND, und AND wiederum stärker bindet als OR. Häufig setzt man diese Klamern aber trotzdem, damit die Bedingung leichter lesbar wird.





1.4 Typumwandlungen

Gelegentlich muss in einem Programm der Inhalt einer Variablen in verschiedenen "Typ-Ansichten" interpretiert werden. Als Beispiel dafür betrachten wir eine einfache Methode zur Verschlüsselung von Texten, bei der der verschlüsselte Text aus dem Originaltext erzeugt wird, indem die Buchstaben des Alphabets einfach permutiert werden. Die einfachste Variante davon stellt die "Caesar-Verschlüsselung" dar:
Beispielsweise wird also für s=1 jeder Buchstabe durch seinen Nachfolger im Alphabet ersetzt. Damit das Verfahren am hinteren Ende des Alphabets nicht schief geht, denkt man sich das Alphabet zyklisch geschlossen: nach "Z" kommt wieder "A", dann "B" usw.

Wie wird nun diese Verschlüsselung in einem Programm realisiert? Aus naheliegenden Gründen wollen wir uns auf Texte beschränken, die nur aus Großbuchstaben bestehen. Selbst die Leerzeichen zwischen den einzelnen Worten wollen wir unberücksichtigt lassen, im Vertrauen darauf, dass sich diese nach der Entschlüsselung durch einen hinreichend cleveren Leser aus dem Kontext restaurieren lassen werden.

Nun ordnen wir jedem Buchstaben des Alphabets eine Nummer zu. Solch eine Zuordnung ist ja schon in der ASCII-Tabelle gegeben, wo jedes Zeichen durch seine ASCII-Nummer eindeutig identifiziert ist.

In Delphi können wir die ASCII-Nummer eines Zeichens durch eine Typ-Umwandlung erhalten: sind u und v CHAR-Variablen und a, n, m und b vom Typ INTEGER, dann wird mit
  a := Byte(u);
    { Typ-Umwandlung Zeichen -> Zahl }
die ASCII-Nummer des in u gespeicherten Zeichens in der Variablen a abgelegt. Wenn z.B. u das Zeichen "K" enthält, dann liefert Byte(u) den Wert 75, was die ASCII-Nummer von "K" ist. Wir wollen allerdings (aus später nachvollziehbaren Gründen) für unsere Numerierung bei "A" mit der "0" beginnen. "B" erhält dann die "1", "C" die "2" usw usf. Dies läßt sich erreichen, ohne dass wir die ASCII-Nummer von "A" nachschlagen:
  n := a - Byte('A')
    { Verschiebung ASCII-Nr von 'A' -> 0  }
liefert genau die gewünschte Zuordnung. Die verschlüsselnde Verschiebung um s Zeichen wird nun durch eine schlichte Addition erreicht. Damit wir dabei aber nicht über das Ende des Alphabets hinausrutschen, muss das Ergebnis noch MOD 26 genommen werden:
  m := (n + s) MOD 26
    { Verschiebung um s Zeichen  }
Um nun das zu u passende verschlüsselte Zeichen v zu erhalten, muss dieser Wert für g dann wieder in ein Zeichen umgewandelt werden:
  b := m + Byte('A');
    { Verschiebung  0 -> ASCII-Nr von 'A' }
  v := Char(b);
    { Typ-Umwandlung Zahl -> Zeichen }
In Java können wir die ASCII-Nummer eines Zeichens durch eine Typ-Umwandlung erhalten: sind u und v CHAR-Variablen und a, n, m und b vom Typ INTEGER, dann wird mit
  a = (int) u;
    // Typ-Umwandlung Zeichen -> Zahl
die ASCII-Nummer des in u gespeicherten Zeichens in der Variablen a abgelegt. Wenn z.B. u das Zeichen "K" enthält, dann liefert Byte(u) den Wert 75, was die ASCII-Nummer von "K" ist. Wir wollen allerdings (aus später nachvollziehbaren Gründen) für unsere Numerierung bei "A" mit der "0" beginnen. "B" erhält dann die "1", "C" die "2" usw usf. Dies läßt sich erreichen, ohne dass wir die ASCII-Nummer von "A" nachschlagen:
  n = a - (int) 'A';
    // Verschiebung  ASCII-Nr von 'A' -> 0 
liefert genau die gewünschte Zuordnung. Die verschlüsselnde Verschiebung um s Zeichen wird nun durch eine schlichte Addition erreicht. Damit wir dabei aber nicht über das Ende des Alphabets hinausrutschen, muss das Ergebnis noch modulo 26 genommen werden:
  m = (n + s) % 26;
    // Verschiebung um s Zeichen 
Um nun das zu u passende verschlüsselte Zeichen v zu erhalten, muss dieser Wert für g dann wieder in ein Zeichen umgewandelt werden:
  b = m + (int) 'A';
    // Verschiebung  0 -> ASCII-Nr von 'A'
  v = (char) b;
    // Typ-Umwandlung Zahl -> Zeichen
Damit haben Sie alle Bausteine beieinander, die Sie brauchen, um die Caesar-Verschlüsselung zu implementieren.


Aufgaben:

  1. Die Caesar-Verschlüsselung:

    Schreiben Sie ein Programm "Caesar", das die Eingabe einer kleinen Zahl s und eines einzeiligen Textes gestattet und dann auf Knopfdruck diesen Text in einem zweiten Edit-Feld Caesar-verschlüsselt mit Verschiebung s ausgibt.

    Deklarieren Sie dazu eine Funktion "encrypted", die das zu verschlüsselnde Zeichen und die Verschiebung übergeben bekommt und das verschlüsselte Zeichen zurückliefert.

    Sie können dasselbe Programm auch zum Entschlüsseln nehmen, indem Sie den verschlüsselten Text ins erste Edit-Feld eintragen und dann ein zur Verschiebung s passendes s* wählen. Wie müssen s und s* zusammenhängen?

    Bequemer wäre es allerdings, wenn man den entschlüsselten Text immer in der ersten und den verschlüsselten Text immer in der zweiten Textbox hätte und die Ver- bzw. Entschlüsselung mit jeweils einem eigenen Knopf ausgelöst würde. Schreiben Sie dazu zusätzlich eine Funktion "decrypted", die zu jedem verschlüsselten Zeichen das zugehörige unverschlüsselte zurückliefert, und benutzen Sie diese, um den Text aus der zweiten Edit-Komponente entschlüsselt in der ersten auszugeben.

    Lösungsvorschlag [Delphi] [Java]



  2. Die Vigenère-Verschlüsselung:

    Eine Weiterentwicklung des Caesar-Verfahrens ist die Vigenère-Verschlüsselung. Dabei wird jedes Zeichen des Textes mit einer durch ein Schlüsselwort bestimmten Verschiebung Caesar-verschlüsselt:

    1. Zunächst wird unter die Nachricht so oft wiederholt das Schlüsselwort geschrieben, bis unter jedem Buchstaben der Nachricht ein Buchstabe des Schlüsselwortes steht.

    2. Dann wird jeder Buchstabe der Nachricht mit derjenigen Verschiebung Caesar-verschlüsselt, die durch die Position des darunterstehenden Schlüsselbuchstabens im Alphabet bestimmt ist.
      (Beispiel: Unter dem Nachrichten-Buchstaben "D" stehe der Schlüsselbuchstabe "K". Dieser führt zu einer Verschiebung um s = 10; das "D" wird also so in ein "N" verschlüsselt.)

    Schreiben Sie ein Programm "Vigenere", das die Eingabe eines Schlüsselwortes und eines einzeiligen Textes gestattet und dann auf Knopfdruck diesen Text in einem zweiten Edit-Feld Vigenère-verschlüsselt ausgibt. Implementieren Sie auch die Entschlüsselung!

    Damit dieses Programm schön übersichtlich strukturiert wird, sollten Sie zunächst auch hier wieder (genau wie in der vorigen Aufgabe) eine Funktion "encrypted" implementieren, diesmal aber mit 2 Char-Parametern: dem zu verschlüsselnden Buchstaben der Botschaft und dem zugeordneten Schlüsselbuchstaben. Die eigentliche Ver- (und Ent-!) schlüsselung sollte dann durch Aufrufe dieser Funktion erledigt werden.

    Lösungsvorschlag [Delphi] [Java]






Zum Inhaltsverzeichnis Zum nächsten Kapitel