std::string, std::u16string, std::u32string oder doch std::wstring?

Dank „Hello World!“ ist die Zeichenfolge wohl das Erste, was man als Einsteiger in eine neue Programmiersprache zu Gesicht bekommt. In den meisten Sprachen ist dies auch ganz einfach zu verstehen. Beispielsweise benutzt C# das Schlüsselwort „string“ und Visual Basic die Klasse „String“ um Zeichenfolgen abzubilden. Aber warum hat C++ so ein Durcheinander? Oder ist es gar nicht so verwirrend, wie es erst erscheint?

Um genau diese Fragen zu beantworten, müssen wir erst einmal verstehen, was denn eine Zeichenfolge ist und warum die Arbeit damit so kompliziert sein kann. Okay, der Name verrät es schon ein wenig... „Zeichen“ und „Folge“. Nun haben wir zwei Wörter, wobei wir uns denke ich alle einig sind, was die Bedeutung von „Folge“ ist. Aber wenn wir schon dabei sind, dann können wir ja auch das anmerken. Laut Duden ist eine Folge „das Aufeinanderfolgen von etwas“. Gut, wir wollen also etwas aufeinanderfolgen lassen, nur genau da ist das Problem.

„Zeichen“ gibt es viele und nicht alle können ohne Hilfe gleichzeitig von einem Computer verarbeitet werden. Das Datentyp „char“ kann mit 8-Bit beispielsweise nur 256 unterschiedliche Zeichen darstellen, sodass man sich eine andere Lösung überlegen musste, um noch mehr Zeichen abzubilden. Dies wurde dann durch sogenannte „CodePages“ (Zeichentabelle) gelöst. Zusätzlich zum jeweiligen „char“ teilt man dem Computer mit, in welcher Zeichentabelle man sich aktuell befindet und anhand dieser wird dann der Wert hinter dem char zum richtigen Zeichen aufgelöst.

Beispielsweise können wir die Zeichen 200 bis 204 durch Anpassen der Codepage in 874 (thailändisch), 1256 (arabisch) oder 1257 (baltisch) in unterschiedlichen Schriftzeichen wiedergeben.

CPlusPlus_Zeichenfolgen_Codepage

Nun wissen wir also was Zeichen sind und wie sie abgebildet werden, was hat das aber mit den unterschiedlichen Zeichenfolgen zu tun? Leider sehr viel. Das Problem bei einer Programmiersprache wie C++ ist, dass diese nicht vollständig genormt ist, sondern Spielraum nach oben lässt. Die Genauigkeit, die ein Datentyp haben muss, ist gleich oder besser als der vorherige.


Der Datentyp char hat eine festgeschriebene Größe von einem „Byte“, was mindestens 8-Bit entspricht. Ja, du hast richtig gelesen – mindestens. Ein „Byte“ kann innerhalb von C++ auch mehr als 8-Bit entsprechen, das kommt immer auf die Implementierung an. Normalerweise bleibt es jedoch bei den 8-Bit.

Danach haben wir den Datentyp char16_t, er darf nicht kleiner sein als ein char, muss aber mindestens 16-Bit groß sein. Dann haben wir noch char32_t, der wiederum nicht kleiner sein darf als sein Vorgänger, aber mindestens 32-Bit groß sein muss.

Besides the minimal bit counts, the C++ Standard guarantees that 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long).

Note: this allows the extreme case in which bytes are sized 64 bits, all types (including char) are 64 bits wide, and sizeof returns 1 for every type.
cppreference.com


Wenn wir nun aber einen char als 64-Bit abbilden (was wir dürften) dann müsste der char16_t auch mindestens 64-Bit groß sein und ebenso der char32_t. Und alle hätten gleichzeitig die Größe von einem „Byte“. Nun haben wir noch wchar_t als weiteren Datentyp. wchar_t soll nun den größtmöglichen Codepoint (Zahlenwert hinter einem Zeichen) abbilden können, sprich 32-Bit für beispielsweise UTF-32, wenn das System UTF-32 unterstützen kann.

Leider halten sich da nicht alle dran, denn das „soll“ ist kein „muss“, sodass Microsoft sich entschied seinem wchat_t keine 32-Bit zuzuweisen, sondern nur 16-Bit. Das hat den Grund, dass Windows auf UTF16-LE aufbaut und dies auch in den meisten APIs verwendet. Wenn man beispielsweise mit einer Windows-API kommunizieren möchte, dann muss man auch einen wchar_t verwenden.

Wir sehen also, dass ein einfaches Zeichen dann doch sehr kompliziert sein kann und wir uns überlegen müssen, mit was wir arbeiten. Zu jedem Zeichen-Typ gibt es dann eine passende Klasse, um eine Zeichenfolge abzubilden.

  • char = std::string
  • char16_t = std::u16string
  • char32_t = std::u32string
  • wchar_t = std::wstring

Welche wir davon benutzen hängt also von dem grundlegenden Datentyp ab, welchen wir abbilden möchten und was unser Programm machen soll.


Gut, und wann benutze ich nun was?

Sollte das Programm vollständig mit der ASCII-Zeichentabelle auskommen, dann können wir char und std::string benutzen. Reicht das nicht aus und wir benötigen beispielsweise Zeichen aus UTF-16, dann ist char16_t und std::u16string die richtige Wahl. Wenn auch das nicht ausreicht und wir unsere Zeichen im UTF-32 Format angeben müssen, dann brauchen wir eben char32_t und std::u32string.


Und wozu nun wchar_t?

Grundlegend würde ich wchar_t nicht verwenden und meine Daten notfalls konvertieren, damit ich Kompatibilität zu fremdem Code und Schnittstellen sicherstellen kann. Ich erreiche damit aber einen Code, welcher OS/Compiler-unabhängig ist und mir somit eine doppelte Entwicklung spart. Das ist aber natürlich abhängig vom Ziel meines Programmes, sodass man dies hier nicht als allgemeingültige Aussage werten sollte.

Wichtig ist eigentlich nur, dass man selbst weiß mit welcher Kodierung man arbeitet und den Code auch daran orientiert. Sowohl für die Ausgabe als auch die Eingabe.

Wenn ich innerhalb meines Programmcodes Zeichen verwenden möchte, welche nicht dem ASCII-Code entsprechen, dann muss ich zu meinen Zeichen-Literalen noch den passenden Datentyp angeben. Für einen wchat_t setzen wir einfach ein großes „L“ vor das Literal. Bei char16_T ist es ein kleines „u“ und bei char32_t ein großes „U“. Somit interpretiert der Compiler das Zeichen oder aber auch die Zeichenkette als den Datentyp, um den es sich tatsächlich handelt.


Und warum wird mir immer etwas von „const char[]“ angezeigt?

Hierbei handelt es sich um eine C-Array, welche bei einer Zuweisung zum passenden string konvertiert wird. C-Arrays – wie der Name schon verrät – kommen aus C und wurden somit auch in C++ übernommen. In C gab es keine Klasse die eine Zeichenfolge abbilden konnte, deshalb musste man mit Arrays vom Datentyp char arbeiten. Und da C-Code auch mit C++ kompatibel sein soll, werden auch heute noch String-Literale als C-Array abgebildet.


Aber warum ist die Größe der Array höher als die Zeichenanzahl?

Zeichenketten werden in C immer mit dem ASCII-Zeichen 0 beziehungsweise „\0“ abgeschlossen. Man spricht hierbei von einer Nullterminierung der Zeichenfolge. Hinter unsere eigentliche Zeichenfolge wird also immer noch ein „NUL“ gehängt. Anhand der Position dieses „NUL“ wird dann der String behandelt beziehungsweise beendet.

#include <iostream>
int main()
{
	char zeichenfolge[8] = "abcdefg";

	std::cout << "Zeichenfolge mit sieben Buchstaben" << std::endl;
	std::cout << "Zeichenfolge: " << zeichenfolge << std::endl << std::endl;
	std::cout << "Laenge:       " << *(&zeichenfolge + 1) - zeichenfolge << std::endl;
	std::cout << "              0 1 2 3 4 5 6 7" << std::endl;
	std::cout << "Zeichenfolge: ";
	for (size_t i = 0; i < *(&zeichenfolge + 1) - zeichenfolge; i++)
	{
		std::cout << zeichenfolge[i] << " ";
	}
	std::cout << "\n\n";
	std::cout << "Nullterminierung an vierter Stelle" << std::endl;
	zeichenfolge[3] = '\0';
	std::cout << "Zeichenfolge: " << zeichenfolge << std::endl;
}

CPlusPlus_Zeichenfolgen_Nullterminierung

Kommentare sind geschlossen