Programmieren
 
 
 
 
 
C-Grundkurs

 
 
 
 
.. Arrays -> Strings (eindimensional)
 
 

 
 
 
 


Nullterminierte Strings (Zeichenfolgen) sind grundsätzlich eindimensionale Arrays. Diese bestehen aus aneinandergereihten Speichern des Typs char, die normalerweise Bytes darstellen. Den Bytes lassen sich Zahlen zwischen 0-255 einschreiben und wenn diese Zahlen dem ASCII-Code entsprechen, ergibt sich nach menschlicher Interpretation eine Zeichenfolge. Werden nämlich die Zahlen durch eine elektrische Schaltung in Bildchen aus Punkten umgesetzt, so erkennen Menschen Buchstaben, Ziffern und Sonderzeichen. Zusammengenommen ergeben diese eine Zeichenfolge, die ein Wort, einen Satz oder eine Zahl bilden können. Im folgenden Bild ist dies am String www.GoBlack.de dargestellt.




Das Bild zeigt einen Ausschnitt des RAM Hauptspeichers eines PCs. Es beginnt willkürlich mit einem Speicherbyte dessen Nummer (Adresse) 0FFEh lautet und endet mit dem Byte 100Fh. Der Inhalt dieser Speicherbytes sind irgend welche Zahlen im Bereich zwischen 0 und 255. Nur durch die in der ASCII-Code-Tabelle hinterlegte Vereinbarung lassen sich diese Zahlen in Punktrasterbildchen wandeln, die dann wie Buchstaben aussehen. Die Wandlung der Zahlen zu Buchstaben übernimmt eine elektronische Schaltung, die als Grafikkarte bezeichnet wird.


 


 

 

 

 


Das Bild offenbart zudem eine besondere Eigenschaft des Variablentyps char*. Während Variable der Typen char, int, unsigned, float oder double den Inhalt von Speicherzellen kennzeichnen, geschieht dies mit char* nicht.
Im Beispiel oben enthält die Variable string vom Typ char* nicht den Speicherzelleninhalt 77, sondern die Zahl 1000. Das ist die Nummer (Adresse) dieser Speicherzelle im Hauptspeicher des PC und in ihr befindet sich die Zahl 77. In der Sprache C werden solche Variable als Zeiger bezeichnet. Die Variable string ist also ein Zeiger.

Aus dieser Erkenntnis heraus wird unmittelbar klar, warum die nachfolgende Zuweisung nicht funktionieren kann ..


 

 

 


char* string;
...
string = "www.GoBlack.de";              // das geht nicht

// hier wird während der Laufzeit eines Programms versucht, dem
// Zeiger string, der wie oben den Wert 1000 haben möge, die
// Zahlenfolge 77, 77, 77, 2E .. usw. zuzuweisen. Das funktioniert
// auch in C nicht.


// Dagegen ist es natürlich weiterhin möglich einem zweiten Zeiger
// den Wert eines vorhergehenden Zeigers zu übergeben

char* string = "www.GoBlack.de";
char* zeiger;
...
zeiger = string;                        // das geht

// nach dieser Zuordnung während der Laufzeit zeigen beide Zeiger
// auf den Wert 1000, also auf das erste Byte des Strings. 


 

 

 


Deklaration, Initialisation und Definition von Strings

Wie das letzte Beispiel zeigt, stellt der Augenblick der Deklaration von Strings eine Ausnahme dar. Zu diesem Zeitpunkt kann der Compiler mithelfen, Zeigervariable und Stringliterale miteinander zu verbinden. Dies funktioniert, weil der Compiler ja erst den Maschinencode erzeugt, der zur Laufzeit ausgeführt werden soll. Der Compiler kann also ein Maschinenprogramm erzeugen, welches zunächst Speicherbytes in genügender Menge für das Stringliteral anfordert (definiert), diesen mit den ASCII-Codezahlen füllt (initialisiert), um dann die Adresse des ersten dieser Bytes in die Zeigervariable zu schreiben. Die nachfolgenden Beispiele zeigen, welche Möglichkeiten es in C hierfür gibt ..


 

 

 


char string1[25] = "alle meine Entchen";    // a)
char string2[] = "schwimmen auf dem";       // b)
char* string = "See";                       // c)


 

 

 


a) Hier organisiert der Compiler einen Speicherraum von 25 aufeinander folgenden Bytes im Hauptspeicher und kopiert in diese den ASCII-Codes der 18 Buchstaben. In das 19. Byte wird der Wert 0 , die Nullterminierung übertragen. Die restlichen sechs Speicherbytes bleiben für eine spätere Benutzung frei.

b) Hier geschieht alles so wie im vorherigen Beispiel, jedoch werden nur so viele Speicherbytes vom Compiler reserviert, wie sie der nachfolgende String benötigt. Also 18 Bytes ... 17 Buchstaben und die Nullterminierung.

c) Dieses Beispiel offenbart den Hintergrund von Strings. Bei den Variablennamen von Strings, im obigen Beispiel string1, string2 und string, handelt es sich grundsätzlich um Zeiger. Der Compiler verbindet mit der Variablen also nicht den Inhalt einer Speicherzelle, sondern deren Ablageort oder wie man im Computerdeutsch sagt, deren Adresse. Das letzte Beispiel c) ist also identisch mit dem Beispiel b). Wenn man so will, eine alternative Schreibweise. Dem genannten String entsprechend werden 4 Bytes belegt, 3 für die Buchstaben und 1Byte für die Nullterminierung ..

Aber Achtung es gibt auch Unterschiede zwischen den obigen Deklarationen. Diese liegen einmal in dem Ort an dem der Speicherplatz für die Stringliterale im Hauptspeicher reserviert wird, dem soll hier noch nicht nachgegangen werden .. im Besonderen aber in der Verfügbarkeit der Speicherzellen (des Buffers) für die Literale.

Wird nur deklariert und keine Zuweisung zu einem Literal angegeben, so stehen nach char string1[25]; tatsächlich 25 Speicherbytes bereit, die gefüllt werden können. Mit char* string; wird nur ein Zeiger für Strings erzeugt aber kein Speicherraum bereitgestellt und char string2[]; ohne weitere Angaben, würde zu einer Fehlermeldung bei der Übersetzung des Programms führen.


 

 

 


Zeiger oder indirekte Adressierung
Die obigen Variablen string1, string2 bzw. string enthalten, wie beschrieben, nicht den String selber, sondern enthalten nur die Adresse des ersten Bytes, in dem der String beginnt. Das lässt sich schnell feststellen, wenn man sich den Inhalt der Variablen ausdrucken lässt, was beispielsweise durch ..
         printf("%04X - %04X - %04X", string1, string2, string);

.. geschehen kann. Es erscheinen auf dem Bildschirm drei hexadezimale Zahlen, welche die augenblickliche Ablageadresse des jeweiligen Strings im Hauptspeicher darstellen. Die Strings selber können erst sichtbar gemacht werden, wenn der Programmierer nicht verlangt, dass die Adresse ausgegeben wird, sondern der Inhalt des Speichers, auf den diese Adresse zeigt. Hierzu gibt es den Indirektionsoperator * (wobei * bedeutet .. zeige den Inhalt der Speicherzelle an, auf den die angegebene Adresse zeigt). Auch dass lässt sich an einem Beispiel verdeutlichen ..
         printf("%d - %d - %d", *string1, *string2, *string);

hier werden die Zahlen angezeigt, die in den Bytes abgelegt sind, auf welche die Adressen string1, string2 und string zeigen. Da es sich bei diesen um ASCII-Codes handelt, kann man sie auch in Punktbildchen wandelt lassen, was dann zu Buchstaben führt ..
         printf("%c - %c - %c", *string1, *string2, *string);

Bisher konnten nur die Inhalte der Speicherzellen angezeigt werden, auf welche die Anfangsadressen der Stringvariablen zeigen. Aber auch die nachfolgenden Speicherzellen können erreicht und dargestellt werden. Man muss ja nur die Adressen entsprechend erhöhen. C bietet hierzu zwei gleichbedeutende Schreibweisen an. Doch zunächst die Klärung der Frage, wie die nächste Adresse erreicht werden kann.
Wenn die Variable string eine Adresse darstellt, so muss string+1 die nachfolgende Adresse sein. Dem entsprechend muss *(string+1) einen Zeiger auf das zweite Byte darstellen usw. Probe ..
  printf("%c - %c - %c", *(string1+1), *(string2+1), *(string+1));

Wie es zu erwarten war, werden durch diese Zeile die Inhalte der jeweiligen zweiten Bytes als ASCII -Zeichen dargestellt. Alternativ hierzu hätte man auch schreiben können ..
  printf("%c - %c - %c", string1[1]), *string2[1], *string[1]);

.. denn *(string+1) = string[1], wobei der zweite Ausdruck kürzer und wahrscheinlich übersichtlicher ist. Mit der Angabe eines Offsets (Versatzes) kann man also den Inhalt beliebiger Bytes eines Strings anzeigen lassen. So wird *(string+5) oder string[5] das sechste Zeichen des Strings ausgeben, denn das erste Zeichen ist ja *(string+0) oder string[0] was gleichbedeutend mit *string ist.


Aber Achtung. Der Compiler kontrolliert keinen der beiden Ausdrücke darauf hin, ob er wirklich noch im Bereich des anfänglich deklarierten Strings liegt. Bei der Ausgabe ist dies nicht gefährlich. Man kann sich ja mal die Inhalte der Bytes im Hauptspeicher des Computers anzeigen lassen. Beim Beschreiben von Bytes ist es mit beiden Ausdrücken möglich den Speicherraum eines Computers zu überschreiben. Nach dem bisherigen Kenntnisstand sind das zwar nur 64kByte, aber es sollte reichen um den Rechner zum Absturz zu bringen. Bleibt also die Frage, wie kann das Ende eines Strings erkannt werden.


 

 

 


Die Endekennung (Nullterminierung)
Zur Kennzeichnung des Endes eines Strings gibt es mehrere Wege. Bei der Sprache C einigte man sich darauf, das letzte Byte eines Strings mit dem Wert 0 zu füllen und spricht von einer Nullterminierung. Ist also *(string+n) = string[n] = 0 so ist das letzte Byte des Strings erreicht. Wer möchte, kann weitere Bytes lesen und ausgeben lassen, was bei Druckern häufig zu einem erweiterten Papierbedarf führt und auf Bildschirmen zur Ausgabe von lustigen aber sinnlosen Zeichen. Es macht also durchaus Sinn, diese Endekennung zu berücksichtigen.

Ausgabe vollständiger Strings

Nachdem bisher einzelne Zeichen ausgegeben wurden, vermittelt die nachfolgende Funktion einen Eindruck davon, wie printf("%s",string); funktionieren könnte, bei der es ja durchaus verwunderlich ist, dass kein Indirektionsoperator vor der Variablen string verlangt wird. Das Geheimnis ist ganz einfach gelöst. Das Ersatzzeichen %s gibt der Funktion printf den Hinweis darauf, dass es sich bei der zugeordneten Variable um einen Zeiger handeln muss. Ist das nicht der Fall, behandelt printf() die Variable trotzdem als Zeiger, was zu unvorhersehbaren Bildschirmdarstellungen führen kann.


 

 

 


// Funktion zur Ausgabe eines Strings zum Bildschirm. Benutzt wird
// das BIOS Unterprogramm putch(), welches einzelne ASCII-Codes
// auf dem Monitor als Zeichen darstellen kann. 

#include <conio.h>                // für die BIOS-Funktion putch()
void zeigeString (char* string)
{ 
 unsigned n=0;
 for(n=0; string[n]!=0x00; n++){  // läuft bis Byte n =0 ist
     putch(string[n]);            // n-tes Zeichen ausgeben
 }
}

// Testprogramm, gibt einen String zum Bildschirm aus
void main (void)
{
  char *zeichenfolge = "Alle meine Entchen";
  zeigeString(zeichenfolge);
}


 

 

 

 

 

 


Zusammenfassung
Strings sind nullterminierte, eindimensionale Arrays vom Typ char*. Stringvariable enthalten also nicht den String selber, sondern die Anfangsadresse seines Buffers.



1. Deklaration
Zum Zeitpunkt der Festlegung (Deklaration) von Stringvariablen ist es möglich die angeforderten Speicherbytes im Speicher zu reservieren (definieren) und mit Werten zu füllen (initialisieren). Die drei Möglichkeiten sind nachfolgend genannt.



char string1[25] = "alle meine Entchen";
char string2[] = "schwimmen auf dem";
char* string = "See";



Es ist natürlich auch möglich, die Stringvariablen nur festzulegen, also zu deklarieren. Dabei wird im ersten Beispiel neben dem Zeiger auch ein Buffer bestehend aus 25 Bytes erzeugt, während im zweiten Fall ausschliesslich ein Zeiger erzeugt wird.



char string1[25];
char* string;


 

 

 


2. Zuweisung
Da Stringvariable Zeiger sind, also Adressen beinhalten, macht es keinen Sinn, ihnen die ASCII-Codes eines Buffers zuordnen zu wollen. Die Anweisung ..

string1 = "alle meine Entchen";             // das geht nicht

.. funktioniert also nicht. Man kann aber durch eine Funktion der statischen Bibliothek den Buffer auf den die Stringvariable zeigt mit einem neuen String belegen.

strcpy (string1, "alle meine Entchen");     // das geht

Die Anweisung string = string1; würde allerdings dazu führen, dass nun beide Variablen die gleiche Adresse enthielten und damit auf den String "alle meine Entchen" zeigen würden. Der String "See" wäre nun nicht mehr erreichbar.


 

 

 


3. Zugriff auf einzelne Speicher eines Stringbuffers

Auf einzelne Zeichen innerhalb eines Strings kann durch die Angabe eines Offsetwertes, der sich auf die Anfangsadresse des Strings bezieht, lesend und schreibend zugegriffen werden. So wird durch eine der beiden nachfolgenden Zuweisungen .. alle meine Entchen .. zu .. alle Deine Entchen ..


string1[5] = 'D';
*(string1+5) = 'D';


.. und die Variable c wird mit 0 gefüllt, wenn einer der beiden Lesevorgänge stattfindet ..


int c;
c = string1[18];                // string1[18] enthält die
c = *(string1+18);              // Nullterminierung 

 

 

 

 


 

 

 

 

 

 

 

 

Beispiele:
In der statischen C-Bibliothek gibt es etliche Funktionen zum Umgang mit Strings bzw. eindimensionalen Arrays in deren Speicherbytes ASCII-Code abgelegt wurde. Hier einige Beispiele, wie diese Funktionen programmiert sein könnten.

 

 


 

// stringcopy()
// in der C-Bibliothek strcpy(). Die Funktion überträgt (kopiert)
// die Inhalte eines Stringbuffers auf den der Stringzeiger quelle
// weist, in den Buffer eines zweiten Stringzeigers mit dem Namen
// ziel. Die Funktion gibt die Adresse zurück, auf die ziel zeigt.

char* stringcopy(char* ziel, char* quelle)
{ 
 unsigned n=0;
 for(n=0; quelle[n]!=0x00; n++){  // Speicher einzeln kopieren
     ziel[n]=quelle[n];
 }
 ziel[n]=0x00;                    // Nullterminierung von ziel
 return(ziel);                    // Startddresse von ziel
}

// main() Testprogramm zur Anwendung der Funktion stringcopy()
// Es werden die Adresse auf die der Stringzeiger zeigt, sowie der
// Stringbuffer zum Bildschirm ausgegeben

#include <stdio.h>                // für printf()

void main (void)
{
  char string[20];

  // ...
  stringcopy(string, "alle meine Entchen");
  printf("%04X - %s schwimmen auf dem See", string, string);
}

 


 

// stringlang()
// in der C-Blbliothek strlen(). Die Funktion zählt die Anzahl der
// Bytes eines Stringbuffers, ohne das Byte mit der Nullterminie-
// rung zu berücksichtigen. Es gibt die Anzahl der gefundenen Bytes
// also die Länge des Strings zurück.

unsigned stringlang(char* string)
{ 
  unsigned n=0;
  while(string[n]!=0x00)n++;       // zählen bis Nullterminierung
  return(n);                       // Zählwert zurückgeben
}

// main() Testprogramm zur Anwendung der Funktion stringlang()
// Es werden die Adresse auf die der Stringzeiger zeigt, sowie der
// Stringbuffer zum Bildschirm ausgegeben

#include <stdio.h>                // für printf()

void main (void)
{
  unsigned lang = 0;
  char string[]= "alle meine Entchen";

  // ...
  lang = stringlang(string);
  printf("Der String \"%s\" besteht aus %u Zeichen", string, lang);
}

 


 

// stringanhang()
// in der C-Bibliothek strcat(). Die Funktion hängt an einen ersten
// String ziel einen zweiten mit dem Namen quelle an. Es ist darauf
// zu achten, dass der Buffer des ersten Strings genügend gross
// ist. Die Funktion gibt die Adresse zurück, auf die ziel zeigt.

char* stringanhang(char* ziel, char* quelle)
{ 
  unsigned n=0, m=0;
  for(n=0; ziel[n]!=0x00; n++);    // Ende von ziel suchen
  for(m=0; quelle[m]!=0x00; m++,n++){
    ziel[n]=quelle[m];             // Bytes an ziel anhängen
  }
 ziel[n]=0x00;                    // Nullterminierung von ziel
 return(ziel);                    // Startddresse von ziel
}

// main() Testprogramm zur Anwendung der Funktion stringanhang()
// Es werden die Adresse auf die der Stringzeiger zeigt, sowie der
// Stringbuffer von string1 zum Bildschirm ausgegeben

#include <stdio.h>                // für printf()

void main (void)
{
  char string1[40]= "alle meine Entchen";
  char *string =  " schwimmen auf dem See";

  // ...
  stringanhang(string1, string);
  printf("%04X - %s", string1, string1);
}
www..de