16. Unicode and UTF-8
Unicode and UTF-8#
Computers store bytes, not letters. If we want a byte sequence to represent text, we need an agreement which maps between numbers and the characters a human intends to read. That agreement is a character encoding.
Three ideas must be kept separate:
| Idea | Example for € |
|---|---|
| character | the abstract euro sign |
| code point | U+20AC |
| UTF-8 encoding | bytes E2 82 AC |
A code point is not "the bytes". The same code point can be represented using UTF-8, UTF-16 or UTF-32, producing different code units in memory or a file. Keep the fixed-width representations from Integers in mind: an encoding supplies the context which gives each bit pattern meaning.
ASCII#
ASCII is a fixed-width 7-bit encoding with 128 values, 0x00 through 0x7F. It includes:
- control characters such as NUL (
'\0') and line feed ('\n'); - punctuation;
- digits
'0'through'9'; - uppercase
'A'through'Z'; - lowercase
'a'through'z'.
The useful blocks are sequential:
int digit = c - '0'; // if c is known to be '0' ... '9'
char next = letter + 1; // 'a' becomes 'b'The digit trick works because the code points for the ten digits are consecutive. Do not infer that arbitrary character arithmetic is meaningful: punctuation also occupies gaps between the alphabetic blocks.
| Hex Range | Category | Key Characters / Range | Bitwise Pattern / Notes |
|---|---|---|---|
0x00 – 0x1F |
Control characters | 0x00 (NUL), 0x0A (LF / \n), 0x0D (CR / \r), 0x09 (TAB) |
Non-printable commands |
0x20 – 0x2F |
Punctuation & Space | 0x20 (Space ' '), ! " # $ % & ' ( ) * + , - . / |
First printable character is space (0x20) |
0x30 – 0x39 |
Digits ('0'–'9') |
'0' (0x30) through '9' (0x39) |
High nibble is 0x3; '5' - '0' == 5 |
0x41 – 0x5A |
Uppercase letters | 'A' (0x41 = 0100 0001₂) through 'Z' (0x5A) |
Bit 5 is 0 |
0x61 – 0x7A |
Lowercase letters | 'a' (0x61 = 0110 0001₂) through 'z' (0x7A) |
Bit 5 is 1 (differs from uppercase by 0x20) |
0x7F |
Control character | DEL (Delete) |
0111 1111₂ (all 7 bits set) |
Tip
Notice that uppercase 'A' (0x41 = 01000001₂) and lowercase 'a' (0x61 = 01100001₂) differ only at bit 5 (1 << 5 = 0x20). Toggling bit 5 with XOR (c ^ 0x20) flips the case of an ASCII alphabetic character.
Unicode#
Unicode provides a common coded character set for writing systems, symbols and control characters. Its codespace runs from U+0000 through U+10FFFF, giving 1,114,112 possible code points. Not every code point is assigned to a character, and the surrogate range is not made of Unicode scalar values.
Code points are conventionally written as U+ followed by at least four hexadecimal digits:
| Character | Code point |
|---|---|
A |
U+0041 |
€ |
U+20AC |
字 |
U+5B57 |
😀 |
U+1F600 |
Unicode is not merely "one giant font". It standardises character identities and properties. The font and rendering system later decide what glyphs to draw.
Note
- Characters are more complicated than code points
A user-perceived character can contain multiple code points. For example, an accented letter may be encoded as one precomposed code point or as a base letter followed by a combining mark. Emoji can join several code points into one displayed grapheme. COMP1521 mainly asks you to count and decode UTF-8 code points, but real text editors often need the higher-level idea of a grapheme cluster.
UTF-32#
UTF-32 uses a 32-bit code unit for each Unicode scalar value. Conceptually, the value stored is the code point:
A U+0041 -> 00000041
€ U+20AC -> 000020AC
字 U+5B57 -> 00005B57
😀 U+1F600 -> 0001F600
The fixed width makes indexing by code point simple, but wastes space for ASCII-heavy text. It also still does not guarantee that one code unit equals one user-perceived character.
UTF-8#
UTF-8 is a variable-width encoding which represents one Unicode scalar value using one to four bytes:
| Bytes | Payload bits | Pattern |
|---|---|---|
| 1 | 7 | 0xxxxxxx |
| 2 | 11 | 110xxxxx 10xxxxxx |
| 3 | 16 | 1110xxxx 10xxxxxx 10xxxxxx |
| 4 | 21 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
The x bits carry the code point. Prefixes make the structure self-identifying:
0xxxxxxxis an ASCII byte and a complete one-byte character;110xxxxx,1110xxxxand11110xxxbegin two-, three- and four-byte sequences;10xxxxxxis a continuation byte.
flowchart TD
B["Inspect leading byte"] --> A{"highest bit is 0?"}
A -->|yes| ONE["1 byte total"]
A -->|no| P{"prefix"}
P -->|110| TWO["2 bytes total"]
P -->|1110| THREE["3 bytes total"]
P -->|11110| FOUR["4 bytes total"]
P -->|10 or invalid prefix| BAD["not a valid leading byte"]
ASCII is therefore a byte-for-byte subset of UTF-8. Bytes 0x00 through 0x7F never appear inside the multibyte encoding of another code point. In particular, a continuation byte cannot be zero or /, which lets UTF-8 fit safely into the existing Unix conventions for NUL-terminated pathnames.
Encoding a Code Point#
Take €, code point U+20AC:
- Write the value in binary:
10 000010 101100. - Split it from the right into six-bit groups.
- It needs three groups, so choose the three-byte template.
- Insert the payload bits and zero-fill unused leading payload positions.
template: 1110xxxx 10xxxxxx 10xxxxxx
payload: 10 000010 101100
result: 11100010 10000010 10101100
hex: E2 82 AC
More examples:
| Character | Code point | UTF-8 bytes |
|---|---|---|
A |
U+0041 |
41 |
€ |
U+20AC |
E2 82 AC |
字 |
U+5B57 |
E5 AD 97 |
😀 |
U+1F600 |
F0 9F 98 80 |
Decoding UTF-8#
To recover the code point:
- determine the length from the leading-byte prefix;
- verify that the required following bytes begin with
10; - remove all structural prefix bits;
- concatenate the payload bits.
For E2 82 AC:
11100010 10000010 10101100
0010 000010 101100
10 000010 101100
0x20AC
Validating real UTF-8 requires more than checking prefixes. A decoder must reject:
- truncated sequences;
- a continuation byte where a leading byte is expected;
- a non-continuation byte inside a multibyte sequence;
- overlong encodings, where a value uses more bytes than necessary;
- surrogate code points
U+D800throughU+DFFF; - values above
U+10FFFF.
Warning
- A common exam shortcut
If a question only asks how many structurally indicated UTF-8 characters occur, prefix patterns may be sufficient. If it asks whether the sequence is valid UTF-8, check the entire sequence and the decoded value.
Working with UTF-8 in C#
A C string is still a zero-terminated array of char. The library sees bytes; it does not automatically see Unicode code points.
char message[] = "A€字😀";
printf("%zu bytes\n", strlen(message));In UTF-8 this contains 1 + 3 + 3 + 4 = 11 bytes before the terminating zero. strlen(message) therefore returns 11, not 4.
A simple count of well-formed code-point starts can ignore continuation bytes:
#include <stddef.h>
size_t utf8_code_points(const unsigned char *s) {
size_t count = 0;
for (size_t i = 0; s[i] != '\0'; i++) {
if ((s[i] & 0xC0) != 0x80) {
count++;
}
}
return count;
}0xC0 is 11000000. Masking with it preserves the first two bits. Continuation bytes have the pattern 10xxxxxx, so they produce 0x80 and are not counted. Bitwise Operations develops the mask-and-compare technique in detail.
This is not a validator. Invalid leading bytes would also be counted. A safer routine validates and advances according to the leading byte:
int utf8_sequence_length(unsigned char byte) {
if ((byte & 0x80) == 0x00) return 1; // 0xxxxxxx
if ((byte & 0xE0) == 0xC0) return 2; // 110xxxxx
if ((byte & 0xF0) == 0xE0) return 3; // 1110xxxx
if ((byte & 0xF8) == 0xF0) return 4; // 11110xxx
return -1;
}The full decoder must then check each continuation byte and reject illegal values.
Bytes, Code Points and Display Width#
These counts answer different questions:
| Question | Relevant unit |
|---|---|
| How much memory does this UTF-8 string occupy? | bytes |
| How many encoded Unicode scalar values are present? | code points |
| How many characters does a user perceive? | grapheme clusters |
| How many terminal columns will it occupy? | display width |
For plain ASCII, all four often coincide. Unicode makes the distinction visible.
Common Mistakes#
- Calling UTF-8 "Unicode" as though the code-point assignment and byte encoding were the same thing.
- Assuming one
charstores one Unicode character. - Using
strlen()to count characters. - Starting in the middle of a UTF-8 sequence and treating a continuation byte as a complete character.
- Checking only the number of bytes without checking continuation prefixes.
Practice#
- How many bytes does UTF-8 use to encode code point
U+0041('A'), and what are its bits? - What bit pattern identifies a UTF-8 continuation byte?
- Why does
strlen()fail to return the number of characters in a non-ASCII UTF-8 string? - What range of values are Unicode scalar values?
- How can you determine the length of a UTF-8 sequence just by inspecting the first byte?
Note
- Answers
- 1 byte:
0x41(0100 0001₂). - The prefix
10xxxxxx(i.e.(byte & 0xC0) == 0x80). strlen()counts the number of bytes until the terminating'\0', not the number of Unicode code points or graphemes. Non-ASCII UTF-8 characters use 2 to 4 bytes each.U+0000throughU+10FFFF, excluding the surrogate rangeU+D800–U+DFFF. The broader term code point includes surrogate code points; Unicode scalar value deliberately excludes them.0xxxxxxx= 1 byte;110xxxxx= 2 bytes;1110xxxx= 3 bytes;11110xxx= 4 bytes.
Note
- Further reading
The course's official Unicode topic notes include the historical path from telegraphs to variable-width encodings. The Unicode Consortium's technical introduction gives the larger model, while the current standard's encoding-forms chapter defines UTF-8, UTF-16 and UTF-32 precisely.