UTF-8
UTF-8 문자열은 8bit 기반으로 다음과 같은 4가지 패턴으로 표현된다.
0xxxxxxx // 000000-00007F
110xxxxx 10xxxxxx // 000080-0007FF
1110xxxx 10xxxxxx 10xxxxxx // 000800-00FFFF
11110zzz 10zzxxxx 10xxxxxx 10xxxxxx // 010000-10FFFF
0xxxxxxx
ASCII와 동일한 범위.
맨 앞 바이트가 0으로 시작하면 한 바이트짜리 ASCII문자이다.
110xxxxx 10xxxxxx
첫 바이트는 110으로 시작하고, 10으로 시작하는 한 바이트가 따라온다.
1110xxxx 10xxxxxx 10xxxxxx
첫 바이트는 1110으로 시작하고, 10으로 시작하는 두 바이트가 따라온다.
11110zzz 10zzxxxx 10xxxxxx 10xxxxxx
첫 바이트는 11110으로 시작하고, 뒤에 10으로 시작하는 세 바이트가 따라온다.
아래는 utf-8 문자인지 유효성 체크하는 코드이다.
// 0b10xxxxxx
bool isTrailing(unsigned char b) {
return (b & 0xC0) == 0x80; // 0xC0=0b11000000, 0x80=0b10000000
}
// 0b0xxxxxxx
bool isLeading1(unsigned char b) {
return (b & 0x80) == 0;
}
// 0b110xxxxx
bool isLeading2(unsigned char b) {
return (b & 0xE0) == 0xC0; // 0xE0=0b11100000
}
// 0b1110xxxx
bool isLeading3(unsigned char b) {
return (b & 0xF0) == 0xE0; // 0xF0=0b11110000
}
// 0b11110xxx
bool isLeading4(unsigned char b) {
return (b & 0xF8) == 0xF0; // 0xF8=0b11111000
}
bool validateUTF8(const unsigned char* buffer, size_t len) {
int expected = 0;
for (size_t i = 0; i < len; ++i) {
unsigned char b = buffer[i];
if (isTrailing(b)) {
if (expected-- > 0)
continue;
return false;
} else if (expected > 0) {
return false;
}
if (isLeading1(b)) {
expected = 0;
} else if (isLeading2(b)) {
expected = 1;
} else if (isLeading3(b)) {
expected = 2;
} else if (isLeading4(b)) {
expected = 3;
} else {
return false;
}
return (expected == 0);
}
}
UTF-16
아래는 UTF-16BE 기준으로 이진수로 표현한 것이다.
UTF-8은 8bit 기반으로 UTF-16의 경우 16bit 기반이다.
// UTF-8과 코드포인트에 따른 비교
00000000 0xxxxxxx // 000000-00007F
00000xxx xxxxxxxx // 000080-0007FF
xxxxxxxx xxxxxxxx // 000800-00FFFF
110110zz zzxxxxxx 110111xx xxxxxxxx // 010000-10FFFF
U' = yyyyyyyyyyxxxxxxxxxx // U - 0x10000
W1 = 110110yyyyyyyyyy // 0xD800 + yyyyyyyyyy
W2 = 110111xxxxxxxxxx // 0xDC00 + xxxxxxxxxx
U+0000~U+FFFF
BMP(Basic Multilingual Plane), 가장 많이 쓰는 문자들이 이 영역에 있다.
단, U+D800~U+DFFF surrogate 영역은 사용되지 않는다. (해당 영역은 서러게이트 문자 영역이다.)
00000000|000zzzzz|xxxxxxyy|yyyyyyyy
16bit로 표현할 수 없는 문자들은 서러게이트 문자 영역에 해당하는 두 개의 16bit 문자로 변환되어 32bit 한쌍으로 010000-10FFFF를 코드 포이트를 표현할 수 있다.
High-Surrogate (U+D800 … U+DBFF)
110110ZZ|ZZxxxxxx
상위 서러게이트는 U+D800 에서 U+DBFF 까지의 값을 갖는다. 즉 최상위비트 6개의 값이 위를 보듯이 110110 으로 일정하다.
Low-Surrogate (U+DC00 … U+DFFF)
110111yy|yyyyyyyy
하위 서러게이트는 U+DC00 에서 U+DFFF 까지의 값을 가지며 최상위비트 6개의 값은 110111 이 된다.
각 서러게이트 문자는 하위 10비트씩의 자유도를 갖는다. 따라서 주어진 문자를 10비트씩 두조각을 내서 상위 서러게이트와 하위 서러게이트에 배정한 것이다.
이 두 개의 서러게이트 문자는 상위 서러게이트, 하위 서러게이트로서 전송이 된다. 이 방법으로 U+10FFFF 까지의 문자를 인코딩 할 수 있다.
Byte Order
시스템에 따라 바이트 오더가 다르게 저장되므로 읽을 때 Byte Order를 확인해야한다 Byte Order에는 Big-Endian, Little-Endian 이 있다.
- LSB(Least-Significant Byte) 최하위 바이트
- MSB(Most-Significant Byte) 최상위 바이트
- 0x5A6C 에서 6C가 LSB, 5A가 MSB이다.
- 빅 엔디언 머신에서는 MSB가 가장 낮은 주소를 차지하고, 리틀 엔디언 머신에서는 LSB가 가장 낮은 주소를 차지한다.
코드로 현재 시스템의 바이트 오더를 확인하려면 두 가지 방법이 있을 수 있다.
// 첫 번째, 매크로를 확인하여 바이트 오더를 확인할 수 있다.
#include <endian.h>
#if __BYTE_ORDER == __BIG_ENDIAN
// 빅 엔디안
#else
// 리틀 엔디안
#endif
// 두 번째, 시스템 헤더에 __BYTE_ORDER가 정의되어 있지 않을
// 경우도 있을 수 있겠다. 함수를 구현하여 확인할 수 있다.
// union 형의 기능을 활용한다. 모든 멤버가 메모리의 같은
// 위치에서 시작하여 할당된다.
// 4 byte인 int형으로 1을 설정한 뒤 char형 1 byte로
// 읽어들여 1이 나오면 little-endian 0이 나오면 bit-endian이 된다.
bool isLittleEndian() {
union {
int theInteger;
char signleByte;
} endianTest;
endianTest.theInteger = 1;
return endianTest.signleByte;
}
𐐷(U+10437)를 표현할 때 Byte Order에 따라 아래와 같이 표현된다.
-
UTF-16BE
2진수 : 1101 1000 0000 0001 1101 1100 0011 0111
16진수 : D8 01 DC 37 -
UTF-16LE
2진수 : 0000 0001 1101 1000 0011 0111 1101 1100
16진수 : 01 D8 37 DC -
BOM(Byte Order Mark)
문서 앞에 Big-Endian인지 Little-Endian인지 체크할 수 있도록 BOM을 추가할 수 있습니다.
U+FEFF문자를 맨 앞에 붙혀서 순서를 짐작할 수 있도록 한다.
0xFE 0xFF : Big-Endian
0xFF 0xFE : Little-Endian
C/C++
utf-8은 char로 표현가능하고 utf-16의 경우 wchar_t로 표현가능하다. 하지만 wchar_t의 경우 리눅스와 윈도우즈의 크기가 다르다. 리눅스계열에서는 wchar_t 크기가 32bit이지만 윈도우즈에서는 16bit이다. 그래서 서로 호환되는 소스에서는 리눅스에서 -fshort-wchar 옵션을 이용하여 wchar_t를 16bit로 빌드하면 서로 쉽게 호환된다. 그래도 디버깅의 어려움 등등의 단점은 존재한다.
추가적으로 c++11에서 char16_t char32_t 타입이 추가되었습니다. 그리고 u8 u U prefix를 붙여 리터럴 문자를 선언할 수 있게 되었다.
const char* nomalChar = "hello"; // const char*
const char* utf8 = u8"hello"; // const char*, encoded as UTF-8
const wchar_t* wideChar = L"hello"; // const wchar_t*
const char16_t* utf16 = u"hello"; // const char16_t*, encoded as UTF-16
const char32_t* utf32 = U"hello"; // const char32_t*, encoded as UTF-32
결론
UTF-8, UTF-16 용량을 생각해야한다면 둘 중에 선택을 해야 한다. 문서에 영문자로 작성된 문서라면 1 byte로 표현가능한 UTF-8로 표현하는게 좋을 것이고, 한글로 작성된 문서라면 UTF-8의 경우 3 bytes, UTF-16의 경우에는 2 bytes로 표현하므로 UTF-16으로 표현하는게 좋을 것이다.
References
https://ko.wikipedia.org/wiki/UTF-8
http://en.wikipedia.org/wiki/UTF-16
Programming Interviews Exposed: Coding Your Way Through the Interview
https://docs.microsoft.com/ko-kr/cpp/cpp/string-and-character-literals-cpp?view=vs-2019