Text: Zeichen und Zeichenketten
Im Anfang war das Wort.
– Johannes 1,1
Zeichenkodierung
Rechner (engl. computer) dienen nicht nur als Zahlenfresser (number cruncher). Als Datenverarbeitungsanlagen, so die ältere deutsche Bezeichnung, verarbeiten sie auch Text. Eine Kodierung ordnet jedem Schriftzeichen eindeutig ein Bitmuster zu. Neben Kodierungen zum Telegraphieren (Baudot-Code für Fernschreibgeräte, Lochstreifen) und in IBM-Großrechnern (EBCDIC, Lochkarten) erlangte ASCII (American Standard Code for Information Interchange 1963 bis 1968, ISO 646) Verbreitung für den Datenaustausch. Deutsche Umlaute und Zeichen anderer Sprachen sucht darin man jedoch vergebens — 128 Zeichen sind nicht genug für die Welt. ISO 8859-1 (ISO-Latin-1) kodiert Zeichen westeuropäischer Sprachen. Ein Wechsel zu anderen Kodierungen für Griechisch, Kyrillisch, Hebräisch oder Arabisch führte zu Zeichensalat. Asiatische Schriften blieben außen vor. Der Universal Coded Character Set UCS (Unicode ab 1991, ISO 10646) erlaubt über eine Million Zeichen. Bislang sind über 170 Schriftsysteme mit nahezu 160000 Symbolen erfasst. UTF_8 definiert die Darstellung jedes Unicode-Symbols mit variablem Speicherbedarf von ein bis vier Byte. Diese Kodierung hat sich bis heute fast vollständig durchgesetzt.
ASCII
7-Bit-ASCII-Code umfasst das englische Alphabet, Ziffern und häufig vorkommende Sonderzeichen in einigermaßen übersichtlicher Weise. Den Großbuchstaben wird 64 plus ihrer Nummer im Alphabet zugeordnet, Kleinbuchstaben haben eine um 32 höhere Nummer. Der ASCII-Code einer Dezimalziffer entspricht ihrem Zahlenwert plus 48:
32 !"#$%&'()*+,-./
48 0123456789:;<=>?
64 @ABCDEFGHIJKLMNO
80 PQRSTUVWXYZ[\]^_
96 `abcdefghijklmno
112 pqrstuvwxyz{|}~
Die Codenummern 0 bis 31 enthalten Steuerzeichen wie Zeilenumbruch '\n' (ASCII-Wert 13) und Tabulator '\t' (ASCII-Wert 8). Das folgende Programm erzeugt die obige Ausgabe:
//> cpp_basics/ascii.cpp
import std;
int main()
{
for (int i = 32; i < 128; ++i)
{
if (i % 16 == 0) std::cout << '\n' << i << '\t';
std::cout << char(i);
}
std::cout << '\n';
}UTF-8
Die bis zu 21 Bits der erlaubten ca. 1,1 Millionen Codepoints U+0000 bis U+10FFFF von Unicode werden auf ein bis vier Byte verteilt. Erlaubt ist dabei jeweils nur die kürzeste mögliche Darstellung. Die führenden Bits des ersten Bytes zeigen, wie viele mit 10 beginnende Folgebytes zu diesem Zeichen gehören:
0.......
110..... 10......
1110.... 10...... 10......
11110... 10...... 10...... 10......
Zeichen, Zeichenketten und -Literale in C++
Der Datentyp char ist die kleinste adressierbare Speichereinheit in C++, typischerweise1 ein Byte mit 8 Bit. Einzelne Zeichenwerte werden in einfache Anführungsstriche (Apostrophen) eingeschlossen. Alles, was über den 7-Bit-ASCII-Zeichensatz hinausgeht, benötigt in UTF-8 Folgen von char-Werten mit bis zu vier Byte, also eine Zeichenkette. Für die Verarbeitung von Zeichenketten gibt es den Datentyp std::string.
char zeichen = 'A';
std::string umlaut = "ä":Eine Folge aus beliebig vielen Zeichen im Quelltext wird von doppelten Anführungsstrichen (Gänsefüßchen) umschlossen. Solche null-terminierten Zeichenketten-Literale werden im Speicher mit einem char(0) abgeschlossen:
+---+---+---+---+---+---+
| H | a | l | l | o | \O|
+---+---+---+---+---+---+
Die Zeichenkette "Hallo" belegt also 6 Byte Speicherplatz:
//> cpp_basics/char_string.cpp
import std;
int main()
{
for (auto c : "Hallo")
{
auto bits = std::bitset<8>(c);
auto u = bits.to_ulong();
std::cout << c << " : " << bits << " = " << u << '\n';
}
std::cout << '\n';
}Zeichenkettenliterale sind konstante Speicherbereiche, die nicht verändert werden dürfen. Bei der Zuweisung zu bzw. Initialisierung einer std::string-Variable wird deren Inhalt in deren Speicherbereich kopiert und darf anschließend auch verändert werden.
Ein an die Gänsefüßchen angehängtes s macht kenntlich, dass ein std::string erstellt werden soll. "Hallo"s ist gleichbedeutend mit std::string("Hallo"):
using namespace std::literals;
auto nachricht = "Hallo"s;Aufgabe
- Ändere im obigen Programm
"Hallo"zu"Hallo"s. Wie viele Zeichen werden ausgegeben? Warum?
Zeichenketten und Unicode
Unicode ist komplex. UTF-8-kodierter Text wird dennoch korrekt verarbeitet, wenn einige Kleinigkeiten beachtet werden. Nehmen Unicode-Zeichen mehr als ein Byte ein, ergeben einzelne Bytes keine sinnvollen Zeichen. UTF-8 wird mit std::print() unter allen Betriebssystemen korrekt auf die Konsole ausgegeben. Die Windows-Konsole erzeugt mit std::cout 8-Bit-ASCII-Kauderwelsch.
Das folgenden Programms zeigt auch unter Linux, dass der Quelltext UTF-8 kodiert wurde:
//> cpp_basics/utf8_zeichenkette.cpp
import std;
void zeige_utf8kodierung(std::string s)
{
std::cout << s << " : " << s.size() << " Zeichen\n";
for (char c : s)
{
auto bits = std::bitset<8>(c);
auto u = bits.to_ulong();
std::cout << c << " : " << bits << " = " << u << '\n';
}
std::cout << "-------\n";
}
int main()
{
for (auto s : {"Hallo", "Gruß", "καλημέρα", "привет"})
{
zeige_utf8kodierung(s);
}
}Sowohl das griechische als auch das kyrillische Wort belegen je zwei Byte pro Buchstabe, die für sich genommen Zeichensalat darstellen, der mit dem Ersatzzeichen � kenntlich gemacht wird. Deutsch ist ein Mix von englischen Buchstaben und Umlauten aus dem erweiterten Zeichensatz. Die Abfrage s.size() liefert die Anzahl der Byte, nicht die Anzahl sichtbarer Zeichen:
Hallo : 5 Zeichen
H : 01001000 = 72
a : 01100001 = 97
l : 01101100 = 108
l : 01101100 = 108
o : 01101111 = 111
-------
Gruß : 5 Zeichen
G : 01000111 = 71
r : 01110010 = 114
u : 01110101 = 117
� : 11000011 = 195
� : 10011111 = 159
-------
καλημέρα : 16 Zeichen
� : 11001110 = 206
� : 10111010 = 186
� : 11001110 = 206
� : 10110001 = 177
� : 11001110 = 206
� : 10111011 = 187
� : 11001110 = 206
� : 10110111 = 183
� : 11001110 = 206
� : 10111100 = 188
� : 11001110 = 206
� : 10101101 = 173
� : 11001111 = 207
� : 10000001 = 129
� : 11001110 = 206
� : 10110001 = 177
-------
привет : 12 Zeichen
� : 11010000 = 208
� : 10111111 = 191
� : 11010001 = 209
� : 10000000 = 128
� : 11010000 = 208
� : 10111000 = 184
� : 11010000 = 208
� : 10110010 = 178
� : 11010000 = 208
� : 10110101 = 181
� : 11010001 = 209
� : 10000010 = 130
-------
Ausgewählte Operationen mit Zeichenketten
Das folgende Programm enthält einige wichtige Operationen mit std::string:
//> cpp_basics/die_welt_ist_nicht_genug.cpp
import std;
int main()
{
using namespace std::literals;
std::string s = "Hallo";
auto s2 = "Welt"s;
s = s + ", " + s2;
s += "!";
if (s == "Hallo, Welt!") std::println("{}", s);
if (s.contains(s2)) s.replace(s.find(s2), size(s2), "ко́смос");
std::println("{}", s);
}Aufgaben
- Analysiere das obige Programm. Was bewirkt jede Zeile? Informiere dich über die dir unbekannten Methoden und deren Parameter, falls sie nicht selbsterklärend sind.
- Nutze im Programm andere Vergleichsoperatoren. Was bewirken sie?
- Vergleiche mit
<und>sortieren asciibetisch, nicht in lexikalischer Reihenfolge. Welche Folgen hat das für nicht-englische Sprachen?- Schreibe je eine Funktion, die deutsche Texte in Großbuchstaben bzw. Kleinbuchstaben umwandelt. Teste sie. Wie ist ß zu behandeln?
Anmerkungen
-
Es gibt Ausnahmen: Digitale Klangprozessoren (DSPs) adressieren 16 bis 32 Bit als kleinste Einheit. ↩