CString Management Drucken
Benutzerbewertung: / 9
SchwachPerfekt 
Geschrieben von: StarShaper   
Dienstag, den 20. September 2005 um 18:30 Uhr

header

CStrings sind nützliche Datentypen. Sie vereinfachen eine Reihe von Arbeiten mit den MFC, indem sie die string Manipulation auf sehr viel angenehmere Art und Weise ermöglichen. Wie auch immer, es gibt eine Reihe von Techniken die es beim Umgang mit CStrings zu erlernen gibt. Diese können oft besonders schwer von Leuten mit reinen C-Hintergrund nachvollzogen werden, da in C strings immer nur als reine char Array's behandelt werden und der Umgang mit diesen etwas anders aussieht. Aber auch C++ Programmierer die bisher nur die string-Klasse kennen werden ein wenig umdenken müssen. Dieses Tutorial soll versuchen einige der Techniken im Umgang mit CStrings zu beschreiben.

Ein Teil von dem Tutorial richtet sich bereits an fortgeschrittene Programmierer. Dies ist zudem keine komplette Anleitung für die Arbeit mit CStrings, liefert dennoch Antworten zu den wesentlichen und grundlegenden Fragen.

  • String Verkettungen
  • Formatierung (inklusive Integer-nach-CString)
  • Konvertierung von CStrings in Integer
  • Konvertierung zwischen char * und CStrings
  • char * in CString
  • CString in char * I: Cast in LPCTSTR
  • CString in char * II: Unter Benutzung von GetBuffer
  • CString in char * III: Schnittstelle zu einer Control
  • CString in BSTR
  • BSTR in CString
  • VARIANT in CString
  • Laden von STRINGTABLE Werten
  • CStrings und temporäre Objekte
  • Konvertierungen zwischen std::string und CString
  • CString Effizienz

Allgemeines

Die MFC-Klasse CString stellt einige nützliche Methoden zur Verfügung. Eine vollständige Übersicht der CString Klassen Member findet man wie zu erwarten auf den MSDN Seiten unter CString Class Members. Das folgende Beispiel zeigt wie sich mit Hilfe der Methoden der Dateiname aus einer URI extrahieren lässt.

CString s (_T("C:\\Programme\\XYZ\\file.jpg"));
// Nun enthält s den string "file.jpg"
s = s.Right(s.GetLength() - (s.ReverseFind('\\') + 1 ));

String Verkettungen

Eines der angenehmen Eigenschaften von CStrings ist die Fähigkeit zwei strings auf einfache Art und Weise miteinander zu verknüpfen. Zum Beispiel auf die folgende Art und Weise:

CString gray("Gray");
CString cat("Cat");
CString graycat = gray + cat;

In C müsste man zuerst jeweils zwei char Array's definieren, anschließend Speicher für ein größeres char Pointer Array allokieren und letztenendes über die C Funktionen strcpy(); diesen in das neue array hineinkopieren. Wie das folgende Beispiel zeigt ist soetwas natürlich im Gegensatz zum obigen Code sehr umständlich und bietet zudem einige Fehlermöglichkeiten, wie z.B. das Weglassen des notwendigen Speichers für das string-Ende Zeichen.

char gray[] = "Gray";
char cat[] = "Cat";
char *graycat = malloc(strlen(gray) + strlen(cat) + 1);
strcpy(graycat, gray);
strcat(graycat, cat);

Formatierung (inklusive Integer-nach-CString)

Besser als die Benutzung von sprintf oder wsprintf ist die Nutzung der Methode Format zur Formatierung der CStrings.

CString s;
s.Format(_T("The total is %d"), total);

Der Vorteil hier liegt darin, das man sich nicht um die Größe des zur Speicherung der Daten notwendigen Buffers Sorgen machen muss. Dies wird automatisch für Sie von den Formatierungs-Routinen gemanaged.

Der Gebrauch der Formatierung ist der gebräuchlichste Weg nicht-string Datentypen in einen CString zu konvertieren. Zum Beispiel die Konvertierung von einem Integer in ein CString:

CString s;
s.Format(_T("%d"), total);

Ich benutze immer das _T( ) Makro, weil ich meine Programme so gestalte das diese zumindest Unicode "wissend" sind. Aber das ist für sich ein Thema und bietet genug Stoff für ein anderes Tutorial. Der Sinn des _T ( ) Makros besteht darin den string für eine 8-Bit-Character Anwendung folgendermaßen zu kompilieren,

#define _T(x) x // non-Unicode version

während für Unicode Anwendungen dieser hingegen so kompiliert wird.

#define _T(x) L##x // Unicode version

So hat das den Effekt als hätte ich für eine Unicode-Version den Code geschrieben:

s.Format(L"%d", total);

Wenn Sie der Meinung sind eventuell Codeteile mit Unicode zu benutzen, sollten Sie in einer Unicode erkennende Weise ihre Programme schreiben. Zum Beispiel sollten sie niemals den Operator sizeof() benutzen um die Größe eines Character (Zeichens) festzustellen. Dies würde in einer Unicode Anwendung um den Faktor 2 von der tatsächlichen Größe abweichen. Wenn ich die Größe eines Objektes erfahren möchte, muss ich ein Makro namens DIM aufrufen, welches in der Header-Datei dim.h definiert wurde. Dieses inkludiere ich entsprechend vorher.

#define DIM(x) ( sizeof((x)) / sizeof((x)[0]) )

Das ist nicht nur für den Umgang mit Unicode Buffern deren Größe zur Komilierungszeit noch nicht bekannt ist nützlich, sondern auch für jede in der Tabelle definierte Kompilierungszeit.

class Whatever { ... };
Whatever data[] = {
   { ... },
    ...
   { ... },
};
 
for(int i = 0; i < DIM(data); i++) // durchsuche die Tabelle nach einem Treffer 

Hüten sie sich vor diesen nachfolgend dargestellten API Aufrufen welche nach echten Bytezahlen verlangen. Die Benutzung eines Character Zählers wird nicht funktionieren.

TCHAR data[20];
lstrcpyn(data, longstring, sizeof(data) - 1); // FALSCH!
lstrcpyn(data, longstring, DIM(data) - 1); // RICHTIG
WriteFile(f, data, DIM(data), &bytesWritten, NULL); // FALSCH!
WriteFile(f, data, sizeof(data), &bytesWritten, NULL); // RICHTIG

Dies liegt daran, das lstrcpyn nach der Character Anzahl verlangt, aber WriteFile einen Byte Anzahl möchte.

Die Benutzung von _T( ) erzeugt noch keine Unicode Anwendung! Es generiert nur eine Unicode erkennende Anwendung. Wenn sie den Code im 8-Bit Modus kompilieren, erhalten sie ein "normales" 8-Bit-Character Programm. Wenn sie es im Unicode Modus kompilieren erhalten sie hingegen ein 16-Bit-Character Programm. Dieser Modus kann in ihrem Compiler unter Chracter Set definiert werden. Beachten sie, das ein CString in einer Unicode Anwendung ein string ist, welcher 16-Bit Character enthält.

Konvertierung von CStrings in Integer

Der einfachste Weg einen CString in einen Integer Wert zu konvertieren, ist eines der Standard string-nach-integer Umwandlungsroutinen zu verwenden.

Während sie normalerweise erwarten würden das _atoi eine gute Wahl wäre, ist es selten die richtige Wahl. Wenn sie Unicode gerechte Anwendungen programmieren wollen sollten sie die Funktion _ttoi aufrufen, welche im MultiByte Character Modus mit _atoi und im Unicode Modus mit _wtoi kompiliert werden. Sie sollten ebenfalls in Betracht ziehen _tcstoul (für unsigned Umwandlungen zu jeder 2-er Potenz, wie 2, 4, 8, 16...) oder _tcstol (für signed Umwandlungen zu jeder 2-er Potenz) zu benutzen. Hier ein paar Beispiele dazu:

CString hex = _T("FAB");
CString decimal = _T("4011");
ASSERT(_tcstoul(hex, 0, 16) == _ttoi(decimal));

Konvertierung zwischen char * und CStrings

Dies ist die am häufigsten gestellte Frage die von Anfängern zum CString Datentyp gestellt wird. Wegen den großen magischen C++ Klassen und Funktionen kommt der gewöhnliche objektorientierte C++ Programmierer nicht in Versuchung die computerinternen Mechanismen zu studieren, die hinter der Character Verwaltung stehen. Das ist oft auch einfach egal, weil die string-Klasse sich um Alles im Detail kümmert. Probleme tauchen aber spätestens dann auf wenn es um allgemeine Verständnisprobleme geht, welche auf das mangelnde Wissen über diese grundlegenden Mechanismen zurück zu führen sind. Dann stellt man sich oft die Frage warum etwas nicht funktioniert obwohl es so doch eigentlich funktionieren müsste.

Zum Beispiel warum man bei dem oben genannten Code nicht einfach dies schreiben kann:

CString graycat = "Gray" + "Cat";
oder
CString graycat("Gray" + "Cat");

Tatsächlich würde sich der Compiler bitter über diese Versuche beschweren. Warum? Weil der Operator + als ein überladener Operator zwischen verschiedenen Kombinationen der CString und LPCTSTR Datentypen definiert ist, aber nicht zwischen zwei LPCTSTR Datentypen welche zugrunde liegende Datentypen sind. Sie können nicht C++ Operatoren an Basis Typen, wie int und char oder char * überladen. Folgendes aber funktioniert:

CString graycat = CString("Gray") + CString("Cat");

oder sogar auch

CString graycat = CString("Gray") + "Cat";

Wenn sie diese Codeteile genauer betrachten stellen sie schnell fest, dass das + immer mindestens an einem CString und einem LPCSTR hängt.

char * in CString

Angenommen sie haben einen char * oder einen string. Wie machen sie daraus nun einen CString. Hier sind einige Beispiele:

char *p = "This is a test"

oder, in Unicode-erkennenden Anwendungen

TCHAR *p = _T("This is a test")

oder

LPTSTR p = _T("This is a test");

Sie können jede der folgenden Schreibweisen benutzen.

CString s = "This is a test";     // Nur 8-bit
CString s = _T("This is a test"); // Unicode-erkennend
CString s("This is a test");      // Nur 8-bit
CSTring s(_T("This is a test");   // Unicode-erkennend
CString s = p;
CString s(p);

Jede Schreibweise konvertiert bequem den konstanten string oder den Pointer in einen CString Wert. Achten sie darauf, das die zugehörigen Character immer in den CString kopiert werden, so dass sie auch sowetwas anschließend machen können:

TCHAR *p = _T("Gray");
CString s(p);
p = _T("Cat");
s += p;

Und sie können sicher sein, dass das Ergebnis der string "GrayCat" sein wird.

Es gibt zahlreiche andere Methoden für CString Konstruktoren, aber wir werden hier auf die meisten nicht näher eingehen. Sie können sich näher darüber z.B. in der MSDN informieren.

CString in char * I: Cast in LPCTSTR

Dies ist ein bißchen schwerer in der Durchführung, und es gibt eine Menge Verwirrung über den richtigen Weg es zu tun. Es gibt einige richtige Wege, aber wahrscheinlich genauso viele falsche Wege.

Die erste Sache die sie bei CStrings verstehen müssen ist das es sich um ein C++ Objekt handelt, welches drei Werte beinhaltet: ein Pointer auf ein Buffer, eine Zahl an gültigen Charactern in diesem Buffer und eine Bufferlänge. Die Zahl der vorhandenen Character kann jeden Wert angefangen bei 0 bis hin zu der maximalen Länge des Buffers minus 1 sein. Das letzte ist für das Nullbyte reserviert. Die Character Anzahl und die Buffer Länge sind elegant versteckt.

Solange sie nicht irgend etwas spezielles anstellen, wissen sie nichts über die Größe des Buffers, welcher mit dem CString assoziert ist. solange sie die Adresse des Buffers nicht kennen, können sie nicht dessen Inhalt verändern. Sie können den Inhalt nicht kürzen und sie dürfen die Länge des Inhalts nicht verändern. Dies führt zu einigen sehr merkwürdigen Workarounds.

Der Operator LPCTSTR (oder genauer gesagt, der Operator const TCHAR *), ist für CString überladen. Die Definition des Operators ist die Adresse des Buffers zurück zu geben. Infolgedessen, wenn sie einen string Pointer zum CString brauchen können sie etwas in dieser Art machen

CString s("GrayCat");
LPCTSTR p =  s;

und es wird korrekt funktionieren. Dies ist begründet in dem Vorgang wie in C Cast's (Typumwandlungen) vollzogen werden. Wenn ein Cast benötigt wird, erlaubt es C++ den Cast auszuwählen. Zum Beispiel können sie (float) als Cast an einer komplexen Zahl (in unserem Fall sind Real- als auch Imaginärteil vom Typ float) definieren und es so definieren das es nur die erste Zahl, den Real-Teil, dieser komplexen Zahl zurückgibt.

Complex c(1.2f, 4.8f);
float realpart = c;

Wenn der explizite Cast (float) richtig angewandt wurde, ist nun der Wert des realpart 1.2.

Dies funktioniert in jedem Fall. Zum Beispiel wird jede Funktion welche ein LPCTSTR Parameter annimmt, dieser Operation folgen, so dass sie eine Funktion haben können (vielleicht in einer DLL):

BOOL DoSomethingCool(LPCTSTR s);

und diese wie folgt aufrufen

CString file("c:\\myfiles\\coolstuff")
BOOL result = DoSomethingCool(file);

Das funktioniert einwandfrei, weil die Funktion BOOL DoSomethingCool() einen LPCTSTR als Parameter definiert hat und der LPCTSTR Operator auf das Argument angewendet wird, was in MFC bedeutet das die Adresse des Strings zurückgegeben wird.

Aber was ist zu tun wenn sie den CString formatieren wollen?

CString graycat("GrayCat");
CString s;
s.Format("Mew! I love %s", graycat);

Aufgrunddessen das der Wert in der Variablen-Argument Liste erscheint (die Argumenten-Liste dient neben dem Compiler zu Konstenzprüfung der Typen unter anderem der impliziten und Benutzer definierten Typumwandlung), gibt es keinen Operator der die Formatierung implizit erzwingt. Was also nun?

Tja, sie werden überrascht sein, aber wir erhalten den string

"Mew! I love GrayCat"

weil die MFC Implementierer den CString Datentypen sehr überlegt konstruiert haben, so dass ein Ausdruck des CString Typs auf den Pointer zum string zeigt. Wurde kein Cast angegeben bzw. fehlt dieser, so wie in einer Formatierung oder sprintf, werden sie trotzdem das richtige Ergebnis erhalten. Die zusätzlichen Daten die einen CString beschreiben, existieren in den Adressen unterhalb der nominalen CString Adresse.

Sie können aber nicht den string modifizieren. Wenn sie zum Beispiel "." durch "," (sie sollten aber die National Language Support Features für dezimale Umwandlungen benutzen, wenn sie auf internationale Standards achten wollen) ersetzen möchten, wie in diesem Beispiel:

CString v("1.00");  // 2 dezimal Stellen
LPCTSTR p = v;
p[lstrlen(p) - 3] = ',';

Wenn sie das versuchen, werden sie vom Compiler die Fehlermeldung erhalten das sie an einem konstanten string eine Zuweisung durchführen wollen. Der Compiler wird auch bei folgenden Versuch eine Fehlermeldung produzieren:

strcat(p, "each");

Die Funktion strcat() verlangt nach einem LPTSTR als erstem Argument aber sie haben dieser ein LPCTSTR gegeben.

Versuchen sie nicht diese Fehlermeldungen irgendwie zu umgehen. Sie werden sich nur in Schwierigkeiten bringen.

Der Grund liegt darin, das der Buffer einen Zähler hat, auf welchen sie nicht zugegreifen können (er befindet sich in einem verstecktem Bereich welcher unterhalb der CString Adresse sitzt) und wenn sie nun den string verändern, werden sie sehen das sich der Zähler des Buffers nicht verändert. Mehr noch! Wenn der string so lang ist, wie die phisikalische Buffer Grenze (mehr dazu später) wird jeder Versuch den String zu erweitern darin enden, das sie irgendwelche Daten im Speicher überschreiben, welche sich hinter dem Buffer befinden. Natürlich haben sie dazu keine Rechte, den diese Daten werden von anderen Programmen benutzt. Der sichere Tod einer jeden Anwendung!


CString in char * II: Unter Benutzung von GetBuffer

Es gibt für CStrings eine spezielle Methode um diese zu modifizieren. Es handelt sich um die Operation GetBuffer. Diese gibt ihnen den Pointer zum Buffer, welcher als beschreibbar angesehen wird, zurück. Wenn sie nur die Character ändern wollen oder den string verkürzen möchten, können sie das nun tun.

CString s(_T("File.ext"));
LPTSTR p = s.GetBuffer();
LPTSTR dot = strchr(p, '.'); // OK, sollte s.Find verwenden...
if(p != NULL)
    *p = _T('\0');
s.ReleaseBuffer();

Das ist der erste und einfachste Weg GetBuffer zu benutzen. Sie liefern kein Argument, so dass der Default Wert 0 verwendet wird, was soviel bedeutet wie "gib mir einen Pointer zum string, ich verspreche dir den string nicht zu erweitern". Sobald sie ReleaseBuffer aufrufen wird die aktuelle Länge des strings berechnet und im CString gespeichert. Innerhalb des Scopes (Sichtbarkeitsbereichs) einer GetBuffer/ReleaseBuffer Sequenz betone ich hier ausdrücklich folgendes: Sie dürfen niemals irgend eine Methode von CString an einem CString anwenden wessen Buffer sie haben! Der Grund ist, das die Integrität eines CString Objektes nicht garantiert ist solange ReleaseBuffer nicht aufgerufen wurde. Schauen sie sich den Code unten etwas genauer an:

CString s(...);
LPTSTR p = s.GetBuffer();
//...zahlreiche Dinge passieren mit dem Pointer p
int n = s.GetLength(); // Schlecht!!! Gibt wahrscheinlich einen falschen Wert zurück
s.TrimRight();         // Schlecht!!! Keine Garantie das es funktioniert
s.ReleaseBuffer();     // OK
int m = s.GetLength(); // Garantiert richtig
s.TrimRight();         // Auch korrekt

Stellen sie sich vor, sie möchten den string vergrößern. In diesem Fall müssen sie wissen wie groß der string werden wird. Das ist so wie bei der Deklaration eines char Array's von dem sie wissen das die Bufferlänge niemals die reservierte Anzahl überschreiten wird.

char buffer[1024];

Dazu äquivalent in der CString Welt ist:

LPTSTR p = s.GetBuffer(1024);

Dieser Aufruf liefert nicht nur den Pointer zum Buffer, sondern garantiert auch das die Bufferlänge (mindestens) 1024 byte groß ist.

Beachten sie auch, das der Wert einen strings selbst im Read-Only Speicher liegt, wenn sie den Pointer zu diesem konstanten string besitzen. Ein Versuch in diesem zu speichern, unabhänig davon ob sie GetBuffer ausgeführt haben, wird in einem Zugriffsfehler enden. Ich habe das für CStrings nicht verifiziert, aber ich habe C Programmierer gesehen die diesen Fehler öfters begehen.

Ein üblicher, schlechter Ratschlag, der noch von C Programmierern stammt ist zuerst einen Buffer fester Größe zu allokieren, sprintf zu benutzen und diesen anschließend einem CString zuzuordnen.

char buffer[256];
sprintf(buffer, "%......", args, ...); // ...die Daten
CString s = buffer;

Die bessere Lösung ist:

CString s;
s.Format(_T("%....", args, ...);

Beachten sie, dass das immer funktioniert. Wenn ihr string länger als 256 byte ausfällt zerschlagen sie außerdem nicht gleich den Stack!

Ein anderer oft begangener Fehler ist den offensichtlich klevereren Weg zu gehen und statt einer fixen Buffergröße diesen dynamisch zu allokieren. Das ist aber sogar noch dämlicher.

int len = lstrlen(parm1) + 13 + lstrlen(parm2) + 10 + 100;
char *buffer = new char[len];
sprintf(buffer, "%s is equal to %s, valid data", parm1, parm2);
CString s = buffer;
....
delete [] buffer;

Schließlich kann das Ganze sehr leicht mit zwei kurzen Zeilen realisiert werden:

CString s;
s.Format(_T("%s is equal to %s, valid data"), parm1, parm2);

Beachten sie außerdem das die sprintf Beispiele nicht Unicode kompatibel sind (obwohl sie natürlich tsprintf benutzen und _T( ) um den zu formatierenden string schreiben können), aber die Grundidee ist das sie sich mit dem obigen Code sehr viel leichter tun. Denn der andere ist zum einen sehr viel länger und deshalb auch sehr viel anfälliger für Fehler.

CString in char * III: Schnittstelle zu einer Control

Eine sehr gängige Methode ist ein CString Wert an ein Control zu übergeben, z.B. ein CTreeCtrl. Während MFC eine Reihe an angenhemen Überladungen für diesen Vorgang zur Verfügung stellt, wird gewöhlich in den meisten Situationen die "raw" Form des Updates benutzt. Und daher müssen sie den auf einen string zeigenden Pointer im TVITEM speichern, welcher im TVINSERTITEMSTRUCT beinhaltet ist.

TVINSERTITEMSTRUCT tvi;
CString s;
// ...irgend etwas s zuordnen
tvi.item.pszText = s; // Compiler beschwert sich
// ...andere Dinge
HTREEITEM ti = c_MyTree.InsertItem(&tvi);

Nun, warum beschwert sich der Compiler? Es sieht nach einer perfekten Zuweisung aus! Aber wenn sie tatsächlich auf die Struktur schauen, werden sie sehen dass das Memberobjekt in der TVITEM Struktur deklariert ist, so wie unten gezeigt.

LPTSTR pszText;
int cchTextMax;

Daher ist die Zuweisung keinem LPCTSTR zugeordnet und der Compiler weiß nicht wie er den rechten Teil der Zuweisung in einen LPTSTR casten soll.

OK, sie denken sie können das beheben indem sie einen expliziten Cast durchführen.

tvi.item.pszText = (LPCTSTR)s; // Compiler beschwert sich aber immer noch!

Nein, es funktioniert immer noch nicht! Der Grund liegt darin, dass sie versuchen einen LPCTSTR einem LPTSTR zuzuweisen. Eine Operation die in C/C++ verboten ist. Sie können diese Methode nicht versehentlich verwenden um einen konstanten Pointer in einen nicht konstanten umzuwandeln. Das würde den Sinn von Konstanten ad absurdum führen. Wenn es ihnen gelingt, verwirren sie eventuell den Optimizer, welcher ihren Anweisungen blind vertraut wenn es um die Entscheidung geht das Programm zu optimieren. Zum Beispiel, wenn sie das hier machen:

const int i = ...;
//...führe eine Reihe von Operationen durch
     ... = a[i];  // erste Benutzung
// ...weitere Dinge
     ... = a[i];  // zweite Benutzung 

Der Compiler kann in diesem Beispiel darauf vertrauen das der Wert von i sowohl bei der ersten als auch bei der zweiten Verwendung gleich ist, weil die Variable i von ihnen mit const initialisiert wurde. Darüberhinaus kann der Compiler sogar die Adresse von a[] bei der ersten Benutzung vorherberechnen und diesen Wert für nachfolgende Verwendungen, wie z.B. bei der zweiten Benutzung wiederverwenden. Dies ist natürlich besser als das jedesmal neu zu berechnen. Wenn sie in der Lage wären folgenden Code zu schreiben (in der Form würde der Compiler natürlich einen Fehler wegen inkompatibler Typumwandlung melden aber das soll auch nur als Beispiel dienen),

const int i = ...;
int *p = &i;
//...führe eine Reihe von Operationen durch
     ... = a[i];  // erste Benutzung
// ...mache irgendetwas
     (*p)++;      // den Compiler in die Irre führen
// ...weiterer Code
     ... = a[i];  // zweite Benutzung

würde der Compiler auf die Konstanz von i vertrauen und konsequent auf die Konstanz des Ziels von i. Aber wir haben den Compiler in die Irre geführt indem wir zunächst einen Pointer vom Typ int auf i haben zeigen lassen und anschließend den Inhalt von p, und somit den von i, inkrementiert haben. In so einem Fall würde das Programm im Debug Modus (ohne Optimierungen) ein anderes Verhalten an den Tag legen als im Release Modus mit voller Optimierung. Das ist nicht gut. Deshalb wird der Versuch den Pointer auf i, also auf eine veränderbare Referenz zeigen zu lassen vom Compiler als falsch diagnostiziert. Das ist der Grund warum der weiter oben angesprochene Fall mit dem expliziten (LPCTSTR) Cast nicht helfen würde.

Warum nicht einfach den Member als einen LPCTSTR deklarieren? Weil die Struktur sowohl für das Lesen als auch das Schreiben zu dem Control verwendet wird. Wenn sie zum Control schreiben wird der Text Pointer tatsächlich wie ein LPCTSTR behandelt, aber wenn sie vom Control lesen benötigen sie einen beschreibbaren string. Die Struktur kann nicht zwischen ihrem Gebrauch für Inputs und ihrem Gebrauch für Outputs unterscheiden.

Deshalb werden sie oft etwas in meinem Code finden was so aussieht wie das hier:

tvi.item.pszText = (LPTSTR)(LPCTSTR)s;

Dies castet den CString in einen LPCTSTR, welches mir auf diese Weise die Adresse des strings liefert, welchen ich dann in einen LPTSTR umwandeln kann. Anschließend bin ich in der Lage diesen zuzuweisen. Beachten sie dass das nur gültig ist wenn sie den Wert als Datum im Stile für Set- oder Insert-Methoden verwenden. Sie können das nicht machen wenn sie versuchen Daten zu holen!

Sie benötigen eine etwas veränderte Methode wenn sie versuchen Daten, so wie den Wert der in einem Control gespeichert ist, zu bekommen. Zum Beispiel für einen CTreeCtrl mit Benutzung der GetItem Methode. Hier möchte ich nun den Text eines Elementes erhalten. Ich weiß das der Text nicht größer ist als MY_LIMIT. Deshalb kann ich folgendes schreiben:

TVITEM tvi;
// ...passende Initialisierung von andern Feldern von tvi
tvi.pszText = s.GetBuffer(MY_LIMIT);
tvi.cchTextMax = MY_LIMIT;
c_MyTree.GetItem(&tvi);
s.ReleaseBuffer();

Der oben gepostete Code arbeitet auch für jede Art von Set-Methoden, ist aber nicht unbedingt erforderlich, weil sie bei einer Set-/Insert-Methode keinen string schreiben. Wenn sie einen CString schreiben, müssen sie sich vergewissern das der Buffer beschreibbar ist. Das ist was GetBuffer macht. Noch einmal, wenn sie erst einmal den Aufruf von GetBuffer getätigt haben, dürfen sie nichts anderes am CString machen solange sie nicht ReleaseBuffer aufrufen.

CString in BSTR

Wenn sie mit ActiveX programmieren, werden sie manchmal einen Wert benötigen welcher als BSTR Typ dargestellt wird. Ein BSTR ist ein abgezähler string, ein wide-character (Unicode) string auf Intel Plattformen und kann eingebettete Nullbyte Character (auch NUL Character genannt) enthalten.

Sie können einen CString in einen BSTR konvertieren indem sie die CString Methode AllocSysString aufrufen.

CString s;
s = ... ; // irgendwas
BSTR b = s.AllocSysString()

Der Pointer b zeigt auf ein neu allokiertes BSTR Objekt welches eine Kopie des CString inklusive des abschließenden Nullbytes ist. Das kann nun an irgend eine Schnittstelle übergeben werden welche beim Aufruf nach einem BSTR verlangt. Normalerweise wird ein BSTR von der Komponente welches es empfängt gelöst. Wenn sie dies manuell machen müssen, müssen sie den Aufruf

::SysFreeString(b);

verwenden um den string wieder freizugeben.

Hintergrund ist, das die Entscheidung wie man strings darstellen sollte die zu ActiveX Steuerelementen gesendet werden, in einem Microsoft internen Kleinkrieg endete. Die Visual Basic Leute gewannen und der string Typ BSTR (akronym für "Basic String") war das Ergebnis.

BSTR in CString

Infolgedessen das ein BSTR ein abgezählter Unicode string ist, können sie die Standard-Umwandlungen benutzen um einen 8-Bit CString daraus zu machen. Tatsächlich ist das eine "In-Box" Lösung, da es spezielle Konstruktoren gibt welche ANSI strings in Unicode konvertieren und vice versa. Sie können auch BSTRs als Ergebnisse in einem VARIANT Typ erhalten, welcher der Rückgabetyp bei verschiedenen COM und automatisierten Aufrufen ist.

Zum Beispiel wenn sie in einer ANSI Anwendung

BSTR b;
b = ...; // irgendwas
CString s(b == NULL ? L"" : s)

schreiben funktioniert das gut für einen einzelnen BSTR string, weil ein spezieller Konstruktor existiert welcher einen LPCWSTR (was eigentlich ein BSTR ist) aufnimmt und in einen ANSI string konvertiert. Der spezielle Test wird benötigt, weil ein BSTR NULL sein kann und die Konstruktoren nicht besonders gut mit NULL Inputs arbeiten (Danke an Brian Ross für den Hinweis!). Das funktioniert also nur für einen BSTR welcher aus einem einzelnen string mit abschließendem Nullbyte besteht. Sie müssen etwas mehr tun um strings welche mehrfache Nullbytes enthalten zu konvertieren. Beachten sie das eingebettete Nullbytes generell nicht besonders gut in CStrings funktionieren und deshalb vermieden werden sollten.

Erinnern sie sich an die Regel in C/C++ bezüglich LPWSTR Typen? Wenn sie einen LPWSTR haben passt dieser zum Parameter Typ von LPCWSTR (das funktionert aber nicht umgekehrt!).

Im Unicode Modus ist das nur der Konstruktor:

CString::CString(LPCTSTR);

Wie bereits oben angedeutet, gibt es im ANSI Modus (Multi-Byte) einen speziellen Konstruktor dafür.

CString::CString(LPCWSTR);

Dies ruft eine interne Funktion auf um den Unicode string in einen ANSI string zu konvertieren. (Im Unicode Modus gibt es einen speziellen Konstruktor der einen LPCSTR aufnimmt, einen 8-Bit ANSI string und diesen zu einem Unicode string erweitert). Wie gesagt, beachten sie die Einschränkungen die durch den Test für einen BSTR Wert welcher NULL ist entstehen.

Es gibt zudem ein zusätzliches Problem welches auch oben bereits angesprochen wurde. BSTRs können eingebettete Nullbytes enthalten. CStrings können aber nur mit einem Nullbyte pro string umgehen. Das bedeutet das CStrings die falsche Länge für einen string berechnen werden welcher mehrere eingebettete Nullbytes enthält. Sie müssen das Problem selbst lösen. Wenn sie auf die Konstruktoren in strcore.cpp schauen, werden sie sehen das dort Alle lstrlen oder vergleichbare Algorithmen benutzen um die Länge zu berechnen.

Beachten sie außerdem das die Konvertierung von Unicode in ANSI die ::WideCharToMultiByte Umwandlung mit bestimmten Argumenten benutzt welche sie vielleicht nicht gerne haben. Wenn sie eine andere Umwandlung möchten als im Standard, müssen sie diese selbst schreiben.

Wenn sie den Code als Unicode kompilieren, benötigen sie nur eine einfache Zuweisung:

CString convert(BSTR b)
{
    if(b == NULL)
        return CString(_T(""));
    CString s(b); // im UNICODE Modus
    return s;
}

Wenn sie sich aber im ANSI Modus befinden, benötigen sie eine etwas komplexere Lösung um den string zu konvertieren. Der nachfolgende Code ist eine solche Lösung. Beachten sie das dieser Code die gleichen Argumentenwerte in ::WideCharToMultiByte benutzt, wie die impliziten Konstruktoren für CString benutzen, so dass sie diese Technik nur dann verwenden würden wenn sie diese Parameter ändern möchten um die Konvertierung auf irgend eine andere Art und Weise zu vollziehen. Zum Beispiel in dem sie einen anderen Default Character, anderer Flag's, etc. definieren.

CString convert(BSTR b)
{
    CString s;
    if(b == NULL)
       return s; // Leer für NULL BSTR
#ifdef UNICODE
    s = b;
#else
    LPSTR p = s.GetBuffer(SysStringLen(b) + 1); 
    ::WideCharToMultiByte(CP_ACP,            // ANSI Code Page
                          0,                 // keine Flags
                          b,                 // Source widechar string
                          -1,                // Annahme NUL-terminiert
                          p,                 // Ziel Buffer
                          SysStringLen(b)+1, // Länge des Ziel Buffers
                          NULL,              // Benutze default char des Systems
                          NULL);             // egal wenn default benutzt wird
    s.ReleaseBuffer();
#endif
    return s;
}

Achten sie darauf das ich keine Rücksicht darauf nehme was passiert wenn der BSTR Unicode Character beinhaltet die nicht dem 8-Bit Zeichensatz entsprechen. Deshalb habe ich NULL für die beiden letzten Parameter angegeben. Eventuell möchten sie dies zum Beispiel ändern.


VARIANT in CString

Tatsächlich habe ich dies bisher noch nicht gemacht. Ich arbeite nicht in COM/OLE/ActiveX wo das ein Thema ist. Aber ich habe ein Posting von Robert Quirk in der microsoft.public.vc.mfc Newsgroup gelesen wie es funktioniert. Zudem wäre es blöd dies nicht in das Tutorial zu schreiben. Deshalb hier nun die Anleitung mit ausführlicher Erklärung. Jegliche Fehler im Vergleich zum Original nehme ich auf meine Kappe.

Ein VARIANT ist ein generischer Parameter/Rückgabewert in der COM Programmierung. Sie können Methoden schreiben welche einen VARIANT Typ zurückgeben. Welchen Typ die Funktion zurückgibt kann (und das tut sie oft) von den Input Parametern der Methode abhängen (z.B. in der Automatisation, anhängig davon welche Methode aufgerufen wird, gibt IDispatch::Invoke (via einer ihrer Parameters) einen VARIANT welcher ein BYTE enthält, ein WORD, ein float, ein double, ein Datum, ein BSTR und ungefähr drei Dutzend weitere Typen zurück. Schauen sie sich dazu die Spezifikationen eines VARIANT Typs in der MSDN an. In dem unteren Beispiel wird angenommen das der Typ als VARIANT vom Typ BSTR bekannt ist, was bedeutet das der Wert im string welcher durch bstrVal referenziert wird gefunden werden kann. So wird der Vorteil ausgenutzt das die Tatsache das es einen Konstruktor gibt welcher in einer ANSI Anwendung einen Wert, mit einer Referenz eines LPCWCHAR auf einen CString, konvertiert. Im Unicode Modus ist das der nomale CString Konstruktor. Schauen sie sich die Vorbahalte zu der Standard ::WideCharToMultibyte Konvertierung an und machen sie es für sich aus ob dies für sie akzeptabel ist oder nicht. Meistens, ist es ok.

VARIANT vaData;
 
vaData = m_com.YourMethodHere();
ASSERT(vaData.vt == VT_BSTR);
 
CString strData(vaData.bstrVal);

Beachten sei das sie auch eine generischere (vielfältigarer wiederverwendbar) Umwandlungs-Routine, welche das vt Feld fallspezifisch mit einbezieht, schreiben können. In diesem Fall ist der folgende Code vielleicht etwas für sie:

CString VariantToString(VARIANT *va)
{
    CString s;
    switch(va->vt)
      { /* vt */
       case VT_BSTR:
          return CString(vaData->bstrVal);
       case VT_BSTR | VT_BYREF:
          return CString(*vaData->pbstrVal);
       case VT_I4:
          s.Format(_T("%d"), va->lVal);
          return s;
       case VT_I4 | VT_BYREF:
          s.Format(_T("%d"), *va->plVal);
       case VT_R8:
          s.Format(_T("%f"), va->dblVal);
          return s;
       ... die übrigen cases wurden als kleine Übung für sie weggelassen
       default:
          ASSERT(FALSE); // unbekannter VARIANT Typ (dieses ASSERT ist optional)
          return CString("");
      } /* vt */
}

Laden von STRINGTABLE Werten

Wenn sie ein Programm schreiben möchten welches leicht in andere Sprachen zu konvertieren sein soll, dürfen sie nicht native Sprach-Strings in ihrem Quellcode benutzen. Zum Beispiel wäre das in meinem Fall Englisch, weil das meine Muttersprache ist - aber ich kann auch ein wenig Deutsch sprechen. Wie auch immer. Es ist nicht gut diese strings an den Quellcode zu binden, so wie in dem folgenden Beispiel.

CString s = "There is an error";

Stattdessen sollten sie alle sprachspezifischen strings in eine seperate Resourcen Datei packen (außer vielleicht die strings zum Debuggen. Aber diese sollten in einem fertigen Programm sowieso nicht mehr enthalten sein). Deshalb solle man dies so ähnlich in ein Programm schreiben:

s.Format(_T("%d - %s"), code, text);

Der nachfolgende literale string ist nicht sprachsensitiv. Wie auch immer. Sie sollten sehr vorsichtig sein und strings wie diesen nicht benutzen.

// fmt ist "Error in %s file %s"
// readorwrite ist "reading" oder "writing"
s.Format(fmt, readorwrite, filename);

Ich spreche hier aus eigener Erfahrung. Ich beging diesen Fehler in meiner ersten internationalisierten Anwendung. Trotz der Tatsache das ich Deutsch kann und weiß das ein Verb im Deutschen im Gegensatz zum Englischen an das Ende eines Satzes gestellt wird. Unser deutscher Verleger beschwerte sich lautstark darüber das er verwirrende deutsche Fehlernachrichten schreiben müsse damit der formatierte Code das machte was er sollte.

Es ist viel besser (und das mache ich heute so) zwei strings zu haben. Einen für das Lesen und einen für das Schreiben und anschließend den entsprechenden zu laden. Dies erreicht man indem man sie string-parameter-insensitiv gestaltet, was bedeutet das ganze Format anstatt die einzelnen strings "reading" oder "writing" zu laden.

// fmt is "Error in reading file %s"
// "Error in writing file %s"
s.Format(fmt, filename);

Beachten sie das wenn sie mehr als eine Ersetzung haben, sie darauf achten müssen das die Reihenfolge der Wörter, also Nomen - Verb oder Verb - Nomen usw. keine Rolle spielt.

Ich werde vorerst nicht über FormatMessage sprechen, welches derzeit besser ist als sprintf/Format, aber schlecht in die CString Klasse implementiert wurde. FormatMessage löst das obige Problem indem es die Parameter anhand der Position in der Parameterliste erkennt und es erlaubt diese im Output string neu anzuordnen.

Aber wie wollen wir das nun erreichen? Indem wir wie oben schon einmal kurz erwähnt die strings in der Resourcen Datei, auch bekannt als STRINGTABLE in den Resourcen Segmenten, speichern. Um das zu tun, müssen sie zuerst einmal einen string mit dem Visual Studio Resourcen Editor erzeugen. Dem string wird eine string ID zugeordnet, normalerweise beginnend mit IDS_. Somit haben sie eine Nachricht, einen string wessen ID IDS_READING_FILE ist und einen anderen string mit der ID IDS_WRITING_FILE. Diese erscheinen in ihrer .rc Datei als:

STRINGTABLE
    IDS_READING_FILE "Reading file %s"
    IDS_WRITING_FILE "Writing file %s"
END

Achtung: Diese Resourcen werden immer als Unicode strings gespeichert, unabhängig davon wie ihr Programm kompiliert wird. Sie sind sogar auf Win9x Plattformen Unicode strings, obwohl dort normalerweise Unicode strings nicht unterstützt werden. Das gilt aber eben nicht für die Resourcen! Gehen sie nun zu der Stelle wo die strings von ihnen gespeichert wurden

// vorheriger code
CString fmt;
   if(...)
      fmt = "Reading file %s";
   else
      fmt = "Writing file %s";
...
// viel später
CString s;
s.Format(fmt, filename);

und schreiben stattdessen das

// überarbeiteter code
CString fmt;
   if(...)
      fmt.LoadString(IDS_READING_FILE);
   else
      fmt.LoadString(IDS_WRITING_FILE);
...
// viel später
CString s;
s.Format(fmt, filename);

Nun können sie ihren Code in jede Sprache konvertieren. Die LoadString Methode nimmt eine string ID auf und erhält den für diese ID stellvertretenden STRINGTABLE Wert. Anschließend wird dieser dem CString zugeordnet.

Es gibt ein kleveres Feature des CString Konstruktors welcher die Benutzung von STRINGTABLE Einträgen vereinfacht. Es ist nicht explizit in den CString::CString Spezifikationen dokumentiert, aber das Beispiel zur Benutzung des Konstruktors deutet es vage an. Warum es nicht Teil der offizellen Dokumentation ist weiß ich nicht. Das Feature ist, dass wenn sie eine STRINGTABLE ID in ein LPCTSTR casten implizit ein LoadString ausgeführt wird. Daher erreicht man mit den folgenden zwei Beispielen einen string zu generieren denselben Effekt und das ASSERT wird bei der Kompilierung im Debug Modus nicht auslösen.

CString s;
s.LoadString(IDS_WHATEVER);
CString t( (LPCTSTR)IDS_WHATEVER);
ASSERT(s == t);

Sie fragen sich nun vielleicht wie das überhaupt funktionieren kann?! Wie kann es einen gültigen Pointer von einer STRINGTABLE ID bekommen? Ganz einfach. Alle string ID's liegen in einem Bereich zwischen 1 und 65536. Das bedeutet das die höherwertigen Bits des Pointers 0 sind. Zumindest auf gängigen 32-Bit Systemen auf denen ein Pointer 4 Byte groß ist. Klingt gut, aber was wenn ich Daten in einer sehr kleinen Adresse im RAM habe? Nun, die Antwort lautet schlicht und ergreifend. Das gibt es nicht! Derart niedrige verwendete Adressen in diesem Bereich existieren einfach nicht. Jeder Versuch auf Daten in diesem Adressbereich zwischen 0x00000001 bis 0x0000FFFF (1...65535) zuzugreifen wird immer in einem Zugriffsfehler enden. Diese Adressen sind in keinem Fall, zu keiner Zeit gültige Adressen. Deshalb muss ein Wert in diesem Bereich eine STRINGTABLE ID sein.

Ich tendiere dazu das MAKEINTRESOURCE Makro für Castings zu benutzen. Ich denke das lässt den Code klarer erscheinen. Es handelt sich um ein Standard-Makro welches ansonsten in der MFC nicht viel Anwendung findet. Sie haben vielleicht bemerkt das viele Methoden entweder einen UINT oder einen LPCTSTR als Parameter annehmen, indem die C++ Überladungen genutzt werden. Das macht es unnötig, wie in C explizite Casts bei überladenen Methoden (in C sowieso nicht wirklich überladen) benutzen zu müssen. Das ist auch nützlich bei der Zuweisung von Resourcen Namen an verschiedene andere Strukturen.

CString s;
s.LoadString(IDS_WHATEVER);
CString t( MAKEINTRESOURCE(IDS_WHATEVER));
ASSERT(s == t);

Ich sage es ihnen nebenbei hier noch einmal. Ich praktiziere das was ich ihnen hier erzähle in meinen eigenen Projekten. Sie werden selten, wenn überhaupt, einen literalen string in meinen Programmen finden mit Ausnahme von gelegentlichen Debug Meldungen und natürlich jeglichen sprachunabhängigen strings.

CStrings und temporäre Objekte

Hier ein kleines Problem welches vor einer Weile in der microsoft.public.vc.mfc Newsgroup auftauchte. Ich werde es ein wenig vereinfachen. Das grundlegende Problem bestand darin das der Programmierer einen string in die Registry schreiben wollte. Nachfolgend möchte ich ihnen zwei Beiträge aus diesem Thread zeigen. Der erste stammt vom Fragesteller selbst und der zweite Beitrag von einem User welcher eine Lösung für das Problem aufzeigt:

Hallo,

ich versuche einen Registry Wert einzutragen indem ich RegSetValueEx() benutze und es ist gleichzeitig der Wert der mir auch Probleme bereitet. Wenn ich eine Variable vom Typ char[] deklariere funktioniert es ohne Probleme. Wie auch immer, ich versuche von einem CString zu konvertieren und ich erhalte nur Müll. Um genau zu sein "ÝÝÝÝ...ÝÝÝÝÝÝ". Ich habe es auch mit GetBuffer, Typumwandlung in char* und LPCSTR versucht. Die Rückgabe von GetBuffer (Debuginfo) ist der korrekte string aber wenn ich es einem char* zuweisen will (oder einem LPCSTR) ist es Datenmüll. Nachfolgend ein Teil meines Codes:

    CString Name = GetName();
    RegSetValueEx(hKey, _T("Name"), 0, REG_SZ, 
                        (CONST BYTE *) (LPCTSTR)Name,
                        (Name.GetLength() + 1) * sizeof(TCHAR));

Der Name des strings ist kleiner als 20 chars, so dass ich nicht denke das der GetBuffer Parameter schuld ist. Es ist sehr frustrierend! Ich freue mich über jede Hilfe.


Lieber Frustrierte,

sie haben sich in eine sehr subtile Fehlersituation hineinmanövriert, indem sie versucht haben etwas zu klever zu sein. Sie wissen einfach zu viel und das ist ihnen zum Verhängnis geworden. Der korrekte Code steht unten:

    CString Name = GetName();
    RegSetValueEx(hKey, _T("Name"), 0, REG_SZ, 
                        (CONST BYTE *) (LPCTSTR)Name,
                        (Name.GetLength() + 1) * sizeof(TCHAR));

Hier nun warum mein Code funktioniert und ihrer nicht. Als ihre Funktion GetName einen CString zurückgab, gab sie ein "temporäres Objekt" zurück. Mehr dazu im C++ Reference Manual §12.2.

...In einigen Situationen kann es notwendig oder von Vorteil sein das der Compiler ein temporäres Objekt erzeugt. So eine Einführung in Temporäres ist abhängig von der Implementierung. Wenn ein Compiler ein temporäres Objekt von einer Klasse die einen Konstruktor besitzt einführt, muss es sicher stellen das ein Konstrukt für das temporäre Objekt aufgerufen wird. Genauso muss für ein temporäres Objekt von einer Klasse in welcher ein Destruktor deklariert wurde ein Destrukt aufgerufen werden.

Der Compiler muss sicher stellen dass das temporäre Objekt zerstört wird. Der exakte Punkt der Zerstörung ist abhängig von der Implementierung. Diese Zerstörung muss noch vor dem Ende des Scopes, in welchem das temporäre Objekt erzeugt wurde, stattfinden...

Die meisten Compiler implementieren den impliziten Destruktor für ein temporäres Objekt an dem nächsten Programm-Sequenz-Punkt welcher seiner Erzeugung folgt, das ist normalerweise das nächste Semikolon. Daher existierte der CString als der GetBuffer Aufruf erfolgte, aber wurde mit dem darauffolgenden Semikolon zerstört. (Nebenbei gibt es keinen Grund ein Argument an GetBuffer zu übergeben und außerdem ist der Code so wie er steht nicht richtig, weil kein ReleaseBuffer durchgeführt wurde). So gab GetBuffer einen Pointer für den Text des CStrings zurück. Als der Destruktor dann beim Semikolon aufgerufen wurde, wurde das eigentliche CString Objekt freigegeben zusammen mit dem allokierten Speicher. Der MFC Debug Speicher Allokator beschreibt diesen anschließend wieder mit 0xDD, was das Symbol für "Ý" ist. Zu dem Zeitpunkt als sie zur Registry geschrieben haben wurde der string Inhalt zerstört.

Es gibt keinen besonderen Grund das Ergebnis sofort in einen char* zu casten. Es als CString zu speichern bedeutet das eine Kopie des Ergebnisses gemacht wird, so dass nachdem der temporäre CString zerstört wurde, der string trotzdem in der CString Variable existiert. Das Casting zum Zeitpunkt an dem der Registry Aufruf erfolgt ist ausreichend um den Wert des strings welcher ohnehin schon existiert zu erhalten.

Mein Code ist zusätzlich Unicode unterstützend. Der Registry Aufruf verlangt eine Byteanzahl. Beachten sie außerdem das der Aufruf lstrlen(Name+1) einen Wert zurückgibt welcher um den Faktor 2 kleiner ist als ein ANSI string. Was sie wahrscheinlich versucht haben zu schreiben war lstrlen(Name) + 1 (OK, ich gebe es zu, ich habe auch denselben Fehler gemacht). Wie auch immer, im Unicode, in welchem alle Character 2 Byte groß sind, müssen wir damit zurechtkommen. Die Microsoft Dokumentation hüllt sich dazu überaschenderweise in Schweigen: Ist der Wert der an ein REG_SZ übergeben wird eine Byteanzahl oder eine Zeicheannzahl? Ich nehme an das deren Formulierung von "Byteanzahl" genau das meint und sie das kompensieren müssen.


Konvertierungen zwischen std::string und CString

Der Datentyp std::string aus der C++ STL Bibliothek unterscheidet sich grundlegend vom Datentypen CString. CString ist eine MFC Klasse und nur für Windows Programme gedacht die statisch oder dynamisch mit den MFC gelinkt werden. Die std::string Klasse ist Teil der C++ Standard Bibliothek (STL) und daher fester Bestandteil der C++ Programmiersprache. Als solche ist sie plattformunabhängig! Die std::string Klasse ist eine sehr ausgereifte und mächtige Template Klasse und kann problemlos auch in Windows Programmen verwendet werden. Allerdings sollte man sich genau überlegen ob man std::string in seinen MFC Projekten zusammen mit CString verwendet, da es hier unter anderem einige Sachen zu beachten gibt. Außerdem kann es sein das die zur MFC gehörende CString Klasse in der Performance dort ein wenig besser abschneidet als die Standard C++ string Klasse. Der Grund hierfür liegt u.a. darin das CString Funktionsparameteraufrufe mit Call-by-Reference durchführt. Dies erfordert ledeglich einen Pointer, während std::string immer eine volle Kopie durchführt.

Die folgenden Code-Samples zeigen die Konvertierung von std::string in CString.

std::string s("Ein std::string");
CString cs(s.c_str());

und umgekehrt:

CString cs("Ein CString");
std::string s((LPCTSTR)cs);

Nachfolgend noch ein Beispiel dafür wie man einen std::string mit Escape-Sequenzen in mehrere CStrings aufteilt:

CString cs[10];    // 10 CStrings
std::string s = "1\n22\n333";   // STL string
 
int i = 0;
char *token;
char seps[]   = "\n";
CString tmp = s.c_str();
char* buffer = tmp.GetBuffer(tmp.GetLength()+1);
 
token = strtok( buffer, seps );
 
while( token != NULL )
{
    if ( i < 10 )
        cs[i++] = token;
 
    token = strtok( NULL, seps );
}
 
tmp.ReleaseBuffer();
 
for ( int j = 0; j < i; j++ )
    MessageBox(cs[j]);

CString Effizienz

Ein Problem von CString ist das es bestimmte Ineffizienzen vor ihnen versteckt. Andererseits bedeutet das auch, dass es bestimmte Effizienzen implementieren kann. Sie werden vielleicht bei dem folgenden Beispiel dazu verleitet werden zu sagen das er furchbar ineffizient ist.

CString s = SomeCString1;
s += SomeCString2;
s += SomeCString3;
s += ",";
s += SomeCString4;

Zumindest im Vergleich zu diesem:

char s[1024];
lstrcpy(s, SomeString1);
lstrcat(s, SomeString2);
lstrcat(s, SomeString 3);
lstrcat(s, ",");
lstrcat(s, SomeString4);

Nach allem denken sie sich nun vielleicht, das im ersten Beispiel der Code zuerst einen Buffer allokiert um den SomeCString1 aufzunehmen, dann kopiert es diesen string darein, dann merkt es das es eine Verknüpfung macht, allokiert einen neuen Buffer der groß genug ist um den aktuellen string und den SomeCString2 aufzunehmen, kopiert den Inhalt in den Buffer und verknüpft den SomeCString2 anschließend mit diesem. Dann löscht es den ersten Buffer und ersetzt den Pointer durch einen Pointer der auf den neuen Buffer zeigt und wiederholt das Ganze mit jedem anderen string während es dabei aufgrund der vielen Kopien furchtbar ineffizient ist.

Die Wahrheit ist das es in den meisten Fällen wahrscheinlich nie die Quellstrings (die linke Seite von +=) kopiert.

In VC++ 6.0 werden im Release Modus alle CString Buffer in vorgegebenen Portionen (Quanten) allokiert. Diese sind als Vielfache definiert. Nämlich 64, 128, 256 und 512 Bytes. Das bedeutet das solange die strings nicht sehr lang sind, es sich bei der Erzeugung des verknüpften strings um eine optimierte Version der strcat() Operation handelt (solange der Ort vom Stringende bekannt ist, muss nicht danach gesucht werden, so wie strcat() es machen müsste. Es führt einfach nur ein memcpy an der entsprechenden Stelle durch) und um eine Neuberechnung der Stringlänge. So ist es ungefähr genauso effizient wie der umständliche reine C Code, aber viel einfacher zu schreiben, zu verwalten und zu verstehen.

Diejenigen von ihnen die nicht sicher sind was da wirklich passiert sollten einen Blick in den Quellcode von CString werfen. Der entsprechende Code befindet sich in strcore.cpp im vc7/atlmfc/src/mfc Unterverzeichnis ihrer Visual Studio Installation. Suchen sie nach der ConcatInPlace Methode welche von allen += Operatoren aufgerufen wird.

Aha! Dann ist CString also nicht sehr "effizient". Zumindest wenn ich das hier schreibe:

CString cat("Mew!");

Da der Speicher ja in Vielfachen allokiert wird, werden in diesem Beispiel nicht nur die erforderlichen 5 Byte reserviert. Stattdessen beansprucht das System mindestens 64 Byte. Es werden also 59 Byte einfach verschwendet, oder?

Wenn sie sich das nun denken, sollten sie bereit sein das Ganze aus einem völlig anderem Blickwinkel neu zu lernen. Irgendwann in ihrer Karriere hat ihnen jemand mal beigebracht das sie immer so wenig Platz wie möglich reservieren sollten und dass das eine gute Sache ist.

Allerdings ist das so nicht richtig. Es ignoriert einige ernsthafte und wichtige Aspekte in der Realität.

Wenn sie eingebettete Anwendungen für 16K EPROMs programmieren müssen haben sie einen triftigen Grund das so zu machen. In diesem Bereich ist dies unter anderem auch aufgrund der begrenzten Ressourcen notwendig. Aber wenn sie für Windows auf 1000MHz/256MB RAM Maschinen Anwendungen schreiben, verschwenden sie mit solchen Versuchen ihren Code zu optimieren zum einen nur ihre Zeit und zum anderen wird ihre Anwendung auf diese Art im Gegensatz zu einer nicht manuell "optimierten" Version wahrscheinlich sogar noch schlechter abschneiden.

Oft wird die Größe von strings als der erste Ansatzpunkt angesehen, wo man "optimieren" sollte. Es ist gut strings klein zu halten und schlecht wenn diese größer als erforderlich ausfallen. Das ist Nonsens. Der Effekt ist nämlich, dass bei der Speicherallokierung von lauter kleinen strings nach ein paar Stunden der Heap mit lauter kleinen Speicherstücken vollgestopft ist, welche gänzlich nutzlos sind aber den Speicherbedarf ihrer Anwendung in die Höhe treiben, das Paging erhöhen und zudem unter Umständen den Speicherallokator verlangsamen und somit den gesamten RAM Stück für Stück vereinnahmen. Die Speicherfragmentierung, ein zweitrangiger Effekt, hat auch starken Einfluß auf die System Performance. Eventuell gefährdet es sogar die Systemstabilität was einfach nicht zu akzeptieren ist.

Beachten sie das bei der Kompilierung im Debug Modus die Allokierung immer gleich ist. Dies ist bei der Fehlersuche von Bedeutung.

Nehmen sie mal an das ihre Anwendung mehrere Monate laufen soll. Zum Beispiel wenn ich VC++, Word, PowerPoint, FrontPage, Outlook Express, Firefox und ein paar andere Anwendungen starte und nicht mehr schließe. Ich habe PowerPoint Tage lang ohne Probleme benutzt. Auf der anderen Seite hat man ziemliches Pech wenn man das mit Anwendungen wie dem Adobe FrameMaker versucht. Ich war nur selten in der Lage diese Anwendung zu benutzen ohne sie vier bis sechs mal pro Tag zum Absturz zu bringen. Und immer, weil der Speicher aufgebraucht wurde indem die Auslagerungsdatei in astronomische Höhen Anstieg. Zu genaue Allokation ist eines der Tücken welche zur Gefährdung der Zuverlässigkeit und Stabilität ihrer Anwendungen führen wird.

Aufgrunddessen das CStrings aus Vielfachen von 64 Byte Portionen bestehen, wird der Speicherallokator mit Speicherblöcken vollgepumpt, welche dann fast immer sofort von anderen CStrings wiederverwendet werden können. Auf diese Weise nimmt die Fragmentierung ab, die Speicherallokator Performance zu, der RAM Verbrauch bleibt so gering wie möglich und ihre Anwendung läuft ohne Probleme Monate lang durch.

Nebenbemerkung: Vor vielen Jahren an der CMU, haben wir ein interaktives System geschrieben. Einige Studien über den Speicherallokator zeigten, daß dieser eine Tendenz dazu hatte Speicher schlecht zu fragmentieren. Jim Mitchell, der heute bei Sun Microsystems arbeitet, kreierte einen Speicherallokator welcher Laufzeitstatistiken über die Allokierungsgröße, sowie Abweichungen vom Durchschnitt und vom Standard bei allen Allokierungen anfertigte. Wenn ein Block des Speichers in eine Größe kleiner als der Durchschnitt abzüglich einem s als die vorherrschende Allokation aufgeteilt werden sollte, teilte er sie nicht vollkommen und verhinderte damit das der Allokator mit zu kleinen und unbrauchbaren Teilen vollgestopft wurde. Er benutzte Floating Points innerhalb des Allokators. Seine Beobachtung war das die langfristige Speicherung in Instruktionen, ohne die unbrauchbaren kleinen Speicherblöcke zu ignorieren, bei weitem die Zusatzkosten durch ein paar an der Allokation angewandte Floating Point Operationen überstieg. Er lag richtig.

Denken sie niemals über "Optimierungen" auf Basis von auf einzelnen Codezeilen analysierten schnellen und kleinen Ausdrücken. Optimierungen sollten schnell und klein auf dem gesamten Level ihrer Anwendung sein.

Wenn sie der Meinung sind Optimierungen würde man auf dem Niveau einzelner Codezeilen machen, sollten sie noch einmal nachdenken. Optimierungen auf diesem Level sind von geringer Bedeutung. Wenn sie wirklich mehr über das Thema Optimierungen wissen möchten empfehle ich ihnen sich diesbezüglich seperat im Internet zu informieren. Sie werden wahrscheinlich einige Überraschungen erleben.

Beachten sie das der += Operator ein Speziall-Fall ist. Wenn sie stattdessen den unteren Code schreiben würden, würde jede Anwendung des + Operators einen neuen string erzeugen und eine Kopie ausführen (obwohl es eine optimierte Version ist, da die Länge des strings bekannt ist und die Ineffizienzen von strcat() nicht zum Tragen kommen).

CString s = SomeCString1 + SomeCString2 + SomeCString3 + "," + SomeCString4;

Zusammenfassung

Das sind nur einige Techniken in der Verwendung von CString. Ich benutze diese jeden Tag in meiner Programmierung. Der Umgang mit der CString Klasse ist nicht soo kompliziert, aber die MFC offenbart einiges nicht unmittelbar. So müssen sie vieles davon selbst in Erfahrung bringen. Und mit diesem Tutorial konnten sie ihren Erfahrungsschatz bezüglich CStrings hoffentlich ein kleines Stückchen erweitern.

Danksagungen

Zuletzt noch ein paar Danksagungen an Lynn Wallace für die Entdeckung eines Syntaxfehlers in einem der Beispiele, Brian Ross für seine Kommentare zu BSTR Konvertierungen und Robert Quirk für seine Beispiele zu VARIANT-in-BSTR Umwandlungen.

Dieses Tutorial wurde zum Teil von Joseph M. Newcomer verfasst und freundlicherweise zur Verfügung gestellt. Die hier bereitgestellte deutsche Fassung inklusive aller Erweiterungen steht allein im Verantwortungsbereich des neuen Autors!

Zuletzt aktualisiert am Mittwoch, den 22. August 2007 um 17:22 Uhr
 
AUSWAHLMENÜ