Startseite
Impressum
Skills
Referenzen
pfeil Produkte
Links
pfeil Tips
Benutzerverwaltung 1
Benutzerverwaltung 2
Das ideale Paßwort
Datum und SQL-DB
DSL-Probleme
Dr. Watson Teil 1
Dr. Watson Teil 2
DTS Automatisierung
Installation autom.
Kalender und Zeit
Mail Automatisierung
Office Automation
Stack Frame
SQL Server
Strings + Unicode
VB 6 Tips & Tricks
Windows-Uhrzeit

 

Drucker - freundliche Darstellung


  zalando.de - Schuhe und Fashion online

 

Letzte Aktualisierung: 04.01.2023



Strings in C++, VB und ATL

Strings werden in verschiedenen Sprachen unterschiedlich behandelt. Diese Unterschiede muß man kennen, wenn man "gemischte" Projekte erstellen will, in denen Strings übergeben werden.
Es kommt noch hinzu, daß zunehmend sogenannte "Unicode" - Strings Verwendung finden, die die Repräsentation von verschiedenen Zeichensätzen und von Sonderzeichen erleichtern. Auch hierbei gibt es einiges zu beachten.

Der folgende Artikel erklärt die technischen Grundlagen zur Stringbehandlung in C und C++, Visual Basic und ATL, und zeigt, wie man mit Unicode - Strings arbeiten kann.

Wie wird ein String intern dargestellt ?

Was ist Unicode ?

Wie kann man mit Unicode programmieren ?

Buchtips

Wie wird ein String intern dargestellt ?

Ein String ist, einfach gesagt, erstmal nur eine Kette von einzelnen Zeichen oder Buchstaben. Der Einfachheit halber gehen wir zunächst mal davon aus, daß jedes Zeichen im Speicher genau ein Byte Platz benötigt, denn das war früher immer so, und ist auch heute noch sehr weit verbreitet. Dann sieht ein typischer String im Speicher etwa so aus (ein Kästchen soll ein Byte / Speicherstelle darstellen) :


Der Pfeil steht dabei für einen Zeiger auf den String-Anfang, denn in dieser Zeiger-Form werden Strings in allen hier genannten Sprachen intern verwaltet.
Jetzt muß es für Programme noch eine Möglichkeit geben, zu Wissen, wo der String zu Ende ist; schließlich ist der Speicher nicht in solche Fünferblöcke eingeteilt, wie oben dargestellt, sondern er ist eigentlich eine Art lineares Band. Es muß also irgendwie erkennbar werden, wo der String zu Ende ist.

Und hier unterscheiden sich jetzt einige Sprachen. In C++ ist es so, wie es in C schon immer war: Ein String wird hinten durch ein spezielles Zeichen begrenzt, nämlich durch ein Null-Byte. Dieses nennt man auch Terminator. In C ist ein String immer ein Array von Charactern, und die Variable selbst ist ein Zeiger auf den Anfang.


Nur durch das Null-Byte am Ende (das hier nicht mit einer "0" gekennzeichnet ist, weil es sonst mit der Text-"0" verwechselt wird) wird das Ende erkannt, und so wird auch die Länge des Strings ermittelt: Es wird vom Anfang an so lange gesucht und gezählt, bis das Null-Byte gefunden wird.

In Visual Basic geht es anders: Dort heißt der Variablentyp "String", und er hat eine Besonderheit. Anstatt eines Terminator - Bytes steht dort die Länge des Strings VOR dem String selbst, in einer long - Variable (4 Bytes lang). Somit ist eindeutig definiert, wie lang der String ist:


Die eigentliche String-Variable zeigt dabei weiterhin auf den Start der Zeichenkette. Die Zahlen davor sind die vier Bytes, die die String-Länge angeben. Auf Intel - Systemen wird immer das niederwertigste Byte zuerst gespeichert, so daß die dargestellte Zahl hexadezimal 0x000A lauten würde, in Dezimalform entspricht das der Zahl 10. Warum 10 ? Der String ist doch nur 5 Zeichen lang ? Hier zeigt sich eine Besonderheit: Die Längenangabe im BSTR gibt nicht die Länge in Zeichen an, sondern die Länge in Bytes. Wie im nächsten Abschnitt (Unicode) gezeigt wird, haben viele Strings aber zwei Bytes pro Zeichen, und das ist auch beim BSTR der Fall, deshalb stehen hier 10 Bytes für 5 Zeichen.
(Randbemerkung: Wer sich mit C++ auskennt, und sich von letzterem überzeugen möchte, kann dieses dreizeilige Testprojekt einmal anschauen, damit kann man den Inhalt der Längenangabe prüfen).

Manchmal wird in VB aber auch eine Terminator - Null hinten dran gehangen, ich habe nur noch nicht herausgefunden, wann genau. Der Hintergrund ist folgender: Die Win32 - API - Funktionen sind alle im "C - Stil" geschrieben, das heißt das die Funktionen, die einen String als Parameter erwarten, einen String in Form eines Character - Arrays wie in C erwarten. Das würde mit den VB - Strings dann problemlos gehen, wenn eine Terminator - Null zusätzlich hinten dran wäre. Und es funktioniert auch, man kann aus VB heraus die API - Funktionen aufrufen, und als Strings kann man dabei reine VB - Strings einsetzen, sofern diese nicht von der API - Funktion verändert werden.

Wenn die API - Funktion den String auch noch verändern will, dann gibt es immer einen zusätzlichen Parameter in der API - Funktion, über den man dazusagen kann, wieviele Zeichen man dafür reserviert hat. Auch solche Funktionen kann man aus VB heraus aufrufen. Man muß nur vorher über den Befehl "MeinString = Space(128)" dafür sorgen, daß in der übergebenen String - Variablen auch wirklich soviele Zeichen reserviert werden, wie man es der API-Funktion mitteilt, in diesem Fall also z.B. 128 Zeichen. Dabei muß man nur noch eines beachten: MeinString ist an dieser Stelle mit 128 Leerzeichen (Spaces) gefüllt worden, und in den 4 Längen-Bytes vornedran steht dann auch, daß der String 128 Zeichen lang ist. Die API - Funktion kennt diese Längenangabe nicht; sie füllt den String von der ersten Stelle an so lange, bis sie fertig ist, und macht hinten den Null - Terminator dran. Das könnte zum Beispiel ein 5 Byte langer String sein, mit der Null an Stelle 6. Visual Basic sieht den String jedoch nach wie vor als 128 Zeichen langen String an, weil die Längenangabe vorne für VB entscheidend ist. Daher muß man nach einer solchen API - Funktion in der Regel noch den VB - Befehl "Trim" auf einen solchen String anwenden, also etwa "MeinString = Trim(MeinString)". Dadurch werden die Spaces und das Null-Byte hinten "abgeschnitten", und die Längenangabe wird anschließend aktualisiert.

Was ist Unicode ?

Bisher wurde stets davon ausgegangen, daß ein Zeichen in ein Byte hineinpaßt, und dies hat auch einige Jahre problemlos funktioniert. Nun lassen sich in einem Byte aber maximal 256 verschiedene Zeichen darstellen. Unser Alphabet hat zwar nur 26 Zeichen, aber die übrigen Zeichen waren ebenfalls schnell reserviert, sei es für Steuerzeichen, Leerzeichen, Symbole und so weiter. Die ersten 127 Zeichen sind als "ASCII" - Tabelle bekannt und enthalten alle Klein- und Großbuchstaben des lateinischen Alphabets. Häufige Verwendung findet auch die "ANSI" - Tabelle, die in den ersten 127 Zeichen mit ASCII übereinstimmt, und in den hinteren 128 Zeichen weitere Zeichen enthält, unter anderem auch die deutschen Umlaute.

Es hat sich jedoch gezeigt, daß ein Byte einfach zuwenig ist, um alle möglichen Zeichen aus allen Welt - Alphabeten einheitlich darzustellen. Also wurde Unicode als Standard erfunden. Unicode ist eine Zeichentabelle, die 32767 verschiedene Zeichen ermöglicht, weil jedes Zeichen 2 Byte (= 16 Bit) groß ist. Dort passen locker alle bekannten Alphabete hinein, inklusive China und Japan, die ja sehr viele Schriftzeichen besitzen. Auch Symbole wie Euro, Dollar und britisches Pfund haben dort ihren eindeutigen Platz. Der Nachteil ist jedoch, daß jedes Zeichen nun 16 Bit Platz braucht, und daß dies natürlich Texte und Strings auf die doppelte Größe aufbläht.

Microsoft war hier sehr weit vorausschauend und hat in den frühen neunziger Jahren das Betriebssystem "Windows NT" von den Wurzelspitzen her auf Unicode ausgelegt. Dieses Betriebssystem war der Vorgänger von Windows 2000, und dieses wiederum von Windows XP und Windows Server 2003. Alle internen Stringverarbeitungsoperationen werden mit Unicode - Zeichen durchgeführt, und für Anwendungen hat man die Wahl, ob man mit Unicode oder 8-Bit-ANSI-Zeichen arbeiten möchte. Fast alle API - Funktionen, die einen String als Parameter erhalten oder zurückgeben, gibt es daher in zwei Varianten, einmal als 16-Bit und einmal als 8-Bit-Variante. Wenn eine Funktion hinten ein großes "A" wie ANSI hat, ist es die 8-Bit-Variante, hat sie hinten ein großes "W" (für wide string), so ist es die 16-Bit-Variante. Es ist sogar so, daß Anwendungen, die konsequent auf 16-Bit-Zeichen hin programmiert sind, etwas schneller laufen als solche, die teilweise oder überwiegend mit 8-Bit-Zeichen arbeiten, weil das Betriebssystem auf die Unicode - Strings optimiert ist, und weniger hin- und her konvertiert werden muß.

Woher weiß jetzt eine Textverarbeitung wie Word oder Notepad, ob in einer Textdatei, die geöffnet werden soll, Unicode oder 1-Byte-Zeichen verwendet werden ? Es hat sich durchgesetzt, daß Unicode - Textdateien stets mit den beiden Bytes "FF FE" beginnen, daran wird diese Erkennung festgemacht. Daher sollte man diesen Code auch selbst verwenden, wenn man Unicode-Dateien erzeugt.

Mehr über Unicode: www.unicode.org

Wie kann man mit Unicode programmieren ?

Für Visual Basic ist diese Frage sehr schnell beantwortet: Bis zur Version 3.0 wurde automatisch mit ANSI gearbeitet, aber der Version 4.0 wird Unicode verwendet. Daran kann man nichts ändern durch irgendwelche Einstellungen. Es ist dabei so, daß für API - Aufrufe (s.o.) automatisch von Unicode in ANSI konvertiert wird, ohne daß man etwas dazutun muß. Das heißt aber auch, daß man in VB immer diejenigen API - Funktionen verwenden muß, die hinten ein großes "A" haben, sofern man die Auswahl hat. Auch beim Schreiben von Strings in eine Datei konvertiert VB automatisch von Unicode in ANSI.

In C und C++ ist die Sache anders. Dort kann man durch Definition einer Compiler-Anweisung angeben, wie man standardmäßig mit Strings umgehen will. Das hindert einen aber nicht daran, bewußt auch "gemischt" zu programmieren, es ist eben dann kein guter Stil. Die Compiler-Anweisungen sind:

  • MBCS, _MBCS für ANSI - Zeichen (steht für "Multi Byte Character Set")

  • UNICODE, _UNICODE für Unicode - Zeichen

Die erste Variante ist standardmäßig eingestellt, wenn man ein neues Projekt in C++ 6.0 beginnt. Ich habe jeweils zwei Konstanten genannt, und verwende auch stets alle beide, weil es von Microsoft - Komponenten nicht durchgängig mit einer einzigen Definition verstanden wird, was man will. Diese Konstanten sorgen dann automatisch dafür, daß hinter API - Funktionen ein "A" oder "W" hinten drangehangen wird. Außerdem sorgen sie dafür, daß sogenannte "Template" - Makros und Funktionen, auf die ich gleich noch komme, richtig übersetzt werden. Und wenn die MFC verwendet werden, dann werden auch diese in der Unicode - Variante benutzt.

Während früher nur der Datentyp für 8-Bit-Zeichen, "char", zum Einsatz kam, gibt es seit einiger Zeit auch einen Datentyp für 16-Bit-Zeichen, nämlich "wchar_t". Damit kann man analog zu den bisher gewohnten String - Operationen arbeiten, nur heißen die Funktionen dann alle etwas anders, statt "strlen()" nimmt man dann "wcslen()" zur Berechnung der Stringlänge. Diese anderen Namen sind aber alle sehr gut dokumentiert; in jeder Doku zu einer alten Funktion steht auch gleich daneben, wie der entsprechende Name für 16-Bit-Strings lautet. Worüber man als Anfänger häufig stolpert, ist, daß jetzt die Stringlänge und die Bytelänge nicht mehr identisch sind. Aber man gewöhnt sich dran. Auch der Terminator ist jetzt eine Doppel-Null.

Warum hat dieser Datentyp einen so bescheuerten Namen, "wchar" hätte es doch auch getan ? Nun, ich denke diese Abkürzung steht für "wide character type", und diese Abkürzung hat sich dann historisch irgendwie eingebürgert.

Manchmal möchte man eine Zeichenkette als Konstante hinterlegen, zum Beispiel bei der Zuweisung an einen konstanten String:
const wchar_t * lpszUeberschrift = L"Fehlermeldung";
Das einzig neue, was zu beachten ist, ist das große "L". Es sagt dem Compiler, daß der konstante String aus 16-Bit-Zeichen bestehen soll. Andernfalls bekommt man auch eine Fehlermeldung beim compilieren.

Lieber Leser, Sie müssen nun recht tapfer sein, denn im Folgenden werden sehr viele Kürzel erklärt, die einen erst einmal "erschlagen", die aber alle eine gewisse Systematik haben, an die man sich recht schnell gewöhnt. Ich fange an mit den Abkürzungen für verschiedene Arten von Zeichenketten, so wie sie durchgehend durch die Microsoft / MSDN - Dokumentation verwendet werden:

  • LPSTR ist dasselbe wie "char *" ("long pointer to string")

  • LPWSTR ist dasselbe wie "wchar_t *" ("long pointer to wide string")

  • OLESTR ist auch dasselbe wie "wchar_t *" (historisch überholt)

  • LPCSTR ist dasselbe wie "const char *" ("long pointer to const string")

  • LPCWSTR ist dasselbe wie "const wchar_t *" ("long pointer to const wide string")

  • BSTR steht für "Basic String", ein Datentyp, der gleich noch erklärt wird.

Lassen Sie sich nicht davon verwirren, daß dies alles Großbuchstaben sind; es handelt sich nicht um Makros oder Konstanten, sondern einfach um Abkürzungen für die Datentypen, die hier beschrieben werden.

Auch Konvertierungen sind möglich. Dafür gibt es zwei API - Funktionen: MultiByteToWideChar und WideCharToMultiByte. Weil diese Funktionen aber sehr viele Parameter haben, und jedesmal spezielle Puffer für die Konvertierung bereitzustellen sind, gibt es sehr bequeme Makros, die die Konvertierungsarbeit erledigen (definiert in ATLCONV.H):

  • A2W(LPSTR) konvertiert einen 8-Bit-String (char *) in einen 16-Bit-String (wchar_t *)

  • W2A(LPSTR) konvertiert einen 16-Bit-String (wchar_t *) in einen 8-Bit-String (char *)

  • A2CW(LPSTR) konvertiert einen 8-Bit-String (char *) in einen konstanten 16-Bit-String (const wchar_t *)

  • W2CA(LPSTR) konvertiert einen 16-Bit-String (wchar_t *) in einen konstanten 8-Bit-String (const char *)

Als Eselsbrücke ist dabei gedacht, daß "2" ja "two" heißt und so klingt wie "to", so daß z.B. "A2W" steht für "ANSI to Wide". Damit diese Funktionen keine Compilerfehler erzeugen, muß in derselben Funktion irgendwo davor folgende Zeile eingetragen werden:

USES_CONVERSION;

Diese Zeile ist ein Makro, welches einen lokalen Puffer reserviert, der nachher bei der Konvertierung das Ergebnis aufnimmt. Daher ist das Ergebnis der Konvertierung auch immer physikalisch zu kopieren (z.B. mit strcpy), und nicht einfach per Zuweisung zu übertragen:

char * pErgebnis = W2A(lpszWideString); /* Fehler ! pErgebnis zeigt jetzt nur auf den Puffer, der in der nächsten Operation überschrieben wird ! */

Es wurde schonmal angedeutet, daß es auch Template - Funktionen und Makros gibt. Diese Funktionen werden vom Preprozessor entsprechend behandelt, je nachdem wie die Compiler - Anweisung (MBCS / UNICODE) eingestellt ist. Ein Beispiel:

TCHAR * lpszMeldung = _T("Datei nicht da!");

Diese Zeile wird folgendermaßen übersetzt: Wenn "MBCS" eingestellt ist, wird daraus:
char * lpszMeldung = "Datei nicht da!";

und wenn UNICODE eingestellt ist, wird daraus:
wchar_t * lpszMeldung = L"Datei nicht da!";

Somit kann man auch Code schreiben, der je nach Compilereinstellung passend zur Umgebung compiliert. Man erkennt die entsprechenden Makros und Funktionen alle daran, daß ein "T" enthalten ist. Beispielsweise ist "_tcscpy" die Template- Funktion, die je nach Compilereinstellung zu "strcpy" wird, oder eben zu "wcscpy".

Auch ein weiterer Sonderling wurde schonmal kurz genannt, der BSTR. Dieser Datentyp ist genau das, was weiter oben als String unter Visual Basic beschrieben wurde (der mit der 4-Byte-Längenangabe), daher hat er auch seinen Namen ("Basic String"). Er ist allerdings immer mit 16-Bit-Zeichen gefüllt, es gibt ihn nicht als 8-Bit-Variante. Und, auch wichtig, die Längenangabe vor dem String ist NICHT die Byte-Länge, sondern die Anzahl der Zeichen. Der BSTR hat deswegen so eine große Bedeutung, weil er der String- Datentyp ist, der über COM - Schnittstellen verwendet werden darf. Wenn man in C++ eine COM - Komponente schreibt, dann muß in der IDL - Datei für Strings dieser BSTR verwendet werden.

Um es nochmal deutlich zu machen: wchar_t und BSTR sind beides Zeiger auf 16-Bit Zeichenketten, beide sind Doppel-Null-terminiert, aber nur der BSTR hat vor dem eigentlichen String diese genannte Längenangabe (4-Byte-Integer). Es gibt in der Microsoft - Dokumentation mindestens eine Stelle, an der gewarnt wird, daß der BSTR nicht immer zwingend mit einer Doppel - Null terminiert wird, einziges sicheres Mittel zur Bestimmung der gültigen Zeichen ist die Längenangabe vor dem String (die 4 Bytes). In meiner wahrhaftig langjährigen Praxis bin ich aber noch nie auf einen BSTR gestoßen, der nicht mit Doppel - Null terminiert war. Wer aber ganz auf Nummer sicher gehen will, sollte diesen Hinweis trotzdem beachten.

In VB wird der BSTR also sowieso automatisch verwendet ... wie verwendet man diese Datenstruktur jetzt in C++ ? Dazu ein kleines Beispiel:

BSTR bsMeldung; bsMeldung = SysAllocString(L"Falsche Eingabe!"); pComObjekt->ZeigeMeldung(bsMeldung); SysFreeString(bsMeldung);

Zu Beachten ist, daß man nicht selbständig die Datenstruktur manipuliert, sondern immer nur die dafür vorgesehenen Funktionen aufruft:

  • SysAllocString() zum Erzeugen (allokieren) eines Strings

  • SysFreeString() zum Befreien eines allokierten Strings

  • SysStringLen() zur Abfrage der Stringlänge

Eine Besonderheit ist, daß der Wert "NULL" in einer BSTR - Objektvariablen durchaus erlaubt ist, dies repräsentiert einen leeren String. Und man muß höllisch aufpassen, daß man seinen String auch immer wieder schön befreit, sonst ballert man sich ganz schnell den Speicher zu.

Um Sicherzustellen, daß man nicht vergißt, jeden BSTR wieder freizugeben, und um das Handling zu vereinfachen, gibt es in der ATL - Bibliothek zwei spezielle Klassen, nämlich CComBSTR und _bstr_t. Diese beiden Klassen kapseln intern ein BSTR - Objekt, um den Umgang damit zu vereinfachen. Man kann für so einen CComBSTR oder auch einen _bstr_t einfach einen Konstruktor aufrufen, indem man einen 16-Bit-String mitgibt, oder man kann auch den leeren Kontruktor nutzen, und diesen Objekten dann später mit dem "=" Operator Strings zuweisen. Diese Objekte räumen selbst ihren Speicher wieder auf, das erledigt der Destruktor. Man kann die enthaltenen BSTR - Objekte dann mit einem simplen cast ("(BSTR)") verwenden.

Ein interessanter Artikel mit einem kleinen Tool zum Verarbeiten verschiedener Unicode-Dateien: http://www.codeproject.com/KB/files/EZUTF.aspx

Buchtips


cover




"Professional ATL COM" von Dr. Richard Grimes bietet einen guten Einstieg in die Programmierung mit ATL - Templates. Es wird auch sehr viel COM - Hintergrundwissen vertieft, wer also nur ein einziges Bich sucht, und die COM - Grundlagen kennt, sollte eher zu diesem Buch greifen. Die Active Template Library (ATL) ist eine Art Framework für die COM - Programmierung, welches dem Entwickler eine Menge an Arbeit abnimmt, und ihm trotzdem die volle Kontrolle läßt. Es ist aber nur etwas für C++ - Entwickler, in Visual Basic hat man keine Chance, damit zu arbeiten.

Informationen zum Buch

cover


"Visual Basic 6" von Michael Kofler ist der Klassiker unter den Lehrbüchern für Visual Basic. Das Buch ist sowohl für den Einsteiger interessant, als auch als Referenzwerk für den Profi. Es deckt im Grunde alles ab, was man über Visual Basic 6 wissen muß.

Informationen zum Buch

Support, Feedback, Anregungen

Alles verstanden ? Oder noch Fragen ? Wir freuen uns über Feedback zu den Tips, auch über Verbesserungsvorschläge. Schreiben Sie an support@a-m-i.de.