<아스키코드 vs 유니코드>
[Windows에서의 유니코드]
[문자셋(Character Set)의 종류와 특성]
[대표적인 문자셋]
1) 아스키(ASCII) 코드 (1Byte)
미국에서 정의한 표준.
알파벳의 갯수는 26개이며, 대소문자를 포함하면 52개.
그리고 몇 가지 확장 문자를 포함하더라도 256개를 넘지 않으므로 1바이트($2^8$)으로 충분히 표현 가능하다.
2) 유니코드(Unicode) (2Byte)
영어뿐만이 아닌 다른 국가에서 사용하는 문자들도 표현하기 위해 사용.
유니코드는 문자를 표현하는데 모두 동일하게 2바이트를 사용.
2바이트($2^16)면 최대 65536개의 문자를 표현할 수 있다.
그래서 영어나 한글 외에도 전세계의 문자와 다양한 종류의 기호를 표현하는 것이 가능하다.
[문자셋(Character Set)]
문자들의 집합, 바꿔서 말하면 "문자를 어떻게 표현할 것인지에 대한 방법을 약속해둔 것"을 문자셋이라고 한다.
문자셋은 종류에 따라서 크게 세 가지로 분류가 된다.
1) SBCS(Single Byte Character Set)
이름 그대로 1바이트만을 사용하여 문자를 표현하는 문자셋이다.
아스키 코드가 대표적인 SBCS에 해당한다.
2) MBCS(Multi Byte Character Set)
이름에서 말하는 것처럼 다양한 바이트 수를 사용해서 문자를 표현하는 문자셋이다.
어떤 문자는 1바이트로, 어떤 문자는 2바이트로 표현한다.
주의할 점은 위에 있는 유니코드는 2바이트니까 MBCS라고 착각할 수 있는데, 유니코드는 MBCS에 속하지 않는다.
그리고 MBCS는 SBCS를 포함한다.
쉽게 말하면, SBCS는 MBCS의 부분집합이라고 보면 된다.
그래서 아스키코드를 정의하는 문자를 표현할 때에는 1바이트, 그게 아니라면 2바이트로 처리를 한다.
예를 들면 알파벳은 1바이트로 처리하지만 한글은 2바이트로 처리하는 것을 예로 들 수 있다.
3) WBCS(Wide Byte Character Set)
위에서 유니코드는 MBCS가 아니라고 했다.
실제로 유니코드는 WBCS에 속한다.
WBCS는 모든 문자를 2바이트로 문자를 표현하는 문자셋이다.
[MBCS 기반의 문자열]
[MBCS1.cpp]
/*
* Windows System Programming - 문자 셋(Character Set)
* 파일명: MBCS1.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-01
* 이전 버전 작성 일자:
* 버전 내용: MBCS와 WBCS의 기본적인 이해
* 이전 버전 내용:
*/
#include <iostream>
#include <cstring>
using std::cout;
int main()
{
char str[] = "ABC한글";
int size = sizeof(str);
int len = strlen(str);
cout << "배열의 크기: " << size << '\n';
cout << "문자열 길이: " << len << '\n';
return 0;
}
MBCS와 WBCS에 대한 기본적인 이해를 돕기 위한 예제.
현재 Windows10 기반의 환경에서 해당 파일을 빌드하고 결과를 확인하면 8과 7이 나온다.
ABC는 3바이트, 한글은 4바이트, 널문자는 1바이트로 처리되어 배열은 8바이트가 나오고, 문자열의 길이는 7로 나온다.
내가 사용하는 운영체제에서는 기본적으로 MBCS를 기반 문자열을 처리하고 있음을 보이는 결과다.
그런데 우리가 입력한 문자열의 길이는 실제로 5가 되어야 맞다.
이것이 MBCS가 가지고 있는 문제점이다.
[MBCS2.cpp]
/*
* Windows System Programming - 문자 셋(Character Set)
* 파일명: MBCS2.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-01
* 이전 버전 작성 일자:
* 버전 내용: MBCS와 WBCS의 기본적인 이해
* 이전 버전 내용:
*/
#include <iostream>
using std::cout;
int main()
{
char str[] = "한글입니다";
int i;
for (i = 0; i < 5; i++)
fputc(str[i], stdout);
// c++로 쓴다면
// cout << str[i];
fputs("\n", stdout);
for (i = 0; i < 10; i++)
fputc(str[i], stdout);
// c++로 쓴다면
// cout << str[i];
fputs("\n", stdout);
return 0;
}
MBCS의 문제점을 재차 확인 가능한 두 번째 예제다.
보다시피 배열에는 5글자가 들어가있으므로 길이는 5라는 기대를 하게 된다.
하지만 위의 반복문에서는 '한글'까지만 출력되고, 뒤의 반복문에서는 '한글입니다'가 모두 출력된다.
다시 말해서 문자열의 길이는 5지만, 실제로 문자열에 할당된 크기는 10바이트이기 때문에 위와 같은 문제가 생긴다.
그래서 문자열에 대한 바이트 수를 처리하는 것은 MBCS에서는 꽤 까다로워지게 된다.
그리고 이를 해결하기 위한 방식이 WBCS 방식을 사용하는 것이다.
[WBCS 기반의 프로그래밍]
WBCS 기반 (유니코드 기반)의 프로그래밍을 하면서는 다음의 사항들을 신경써야 한다.
1) char형을 대신하는 wchar_t
typedef unsigned short wchar_t;
문자를 표현하는데 사용되는 자료형으로 wchar_t를 사용해야 한다.
char형과 달리 wchar_t는 2바이트 메모리 공간이 할당된다.
그래서 유니코드를 기반으로 한 문자를 표현하는 것이 가능하다.
2) "ABC"를 대신하는 L"ABC"
위에서 wchar_t를 배웠으니 이제 문자열을 그대로 넣으면 될 것이라는 생각을 할 수 있다.
wchar_t str[] = "ABC";
그런데 이와 같이 문자열을 선언하면 컴파일 에러가 난다.
배열 str을 유니코드 문자열을 저장해야하는데, 오른쪽에 있는 문자열은 MBCS 기반의 문자열이기 때문이다.
그래서 다음과 같이 문자열에 추가적인 명시를 해야한다.
wchar_t str[] = L"ABC";
앞에 L을 붙이게 되면 "뒤에 이어서 나오는 문자열은 WBCS 기반으로 표현하라"라는 의미가 된다.
그래서 위의 L"ABC"는 널문자를 포함해 총 8바이트로 표현된다.
참고로 유니코드에서는 널 문자('\0')도 2바이트로 처리된다.
3) strlen을 대신하는 wcslen
[WBCS1.cpp]
/*
* Windows System Programming - 문자 셋(Character Set)
* 파일명: WBCS1.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-01
* 이전 버전 작성 일자:
* 버전 내용: MBCS와 WBCS의 기본적인 이해
* 이전 버전 내용:
*/
#include <iostream>
#include <cstring>
using std::cout;
int main()
{
wchar_t str_w[] = L"ABC"; // WBCS (Wide Byte Character Set)
char str_s[] = "ABC"; // SBCS (Single Byte Character Set)
int size_w = sizeof(str_w);
int size_s = sizeof(str_s);
int len_w = wcslen(str_w); // strlen이 아니라 wcslen
int len_s = strlen(str_s);
cout << "WBCS 배열의 크기: " << size_w << '\n';
cout << "WBCS 문자열 길이: " << len_w << '\n';
cout << "SBCS 배열의 크기: " << size_s << '\n';
cout << "WBCS 문자열 길이: " << len_s << '\n';
return 0;
}
위에서도 주석으로 추가해둔 부분이지만, wcslen을 strlen으로 하면 해당 문장에서 컴파일 에러가 발생한다.
strlen 함수는 SBCS 기반의 문자열을 처리하기 위한 함수다.
str_w는 WBCS 기반의 문자열인데 해당 길이를 구하라고 하면 데이터 형이 일치하지 않아서 문제가 생기는 것이다.
그래서 WBCS 기반의 문자열 길이를 구하기 위해 사용하는 함수가 wcslen 함수다.
wcslen 함수 외에도 우리가 기존에 알고 있던 문자열 처리 함수인 strcpy, strcmp, strcat 등이 있다.
여기서 str을 wcs로 바꿔주면 WBCS 기반의 문자열 조작 함수가 된다.
4) 완전한 유니코드 기반으로의 전환
[WBCS2.cpp]
/*
* Windows System Programming - 문자 셋(Character Set)
* 파일명: WBCS2.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-01
* 이전 버전 작성 일자:
* 버전 내용: MBCS와 WBCS의 기본적인 이해
* 이전 버전 내용:
*/
#include <iostream>
#include <cstring>
int main()
{
wchar_t str[] = L"ABC";
int size = sizeof(str);
int len = wcslen(str);
wprintf(L"Array Size: %d\n", size);
wprintf(L"String Length: %d\n", len);
return 0;
}
현재 사용하는 Windows2000 이상의 운영체제에서는 문자열을 내부적으로 2바이트 유니코드 형식으로 변환한다.
모든 문자열을 유니코드 기반으로 처리하기 때문이다.
printf 함수는 SBCS 기반의 문자열을 처리하는 함수다.
그래서 printf와 같은 SBCS 기반의 함수가 호출되면 문자열을 내부적으로 변경하게 된다.
문자열을 변경하는 과정이 생기면서 프로그램 성능에 다소 영향을 미칠 수 있다.
하지만 유니코드 기반으로 프로그램을 작성한다면 성능에 영향을 미치지 않게 된다.
위의 예제에서 보인 wprintf를 사용한 것처럼 wprintf는 printf 함수 호출의 유니코드 버전이다.
앞서 문자열 조작 함수에서 str을 wcs로 바꾼 것처럼 여기서도 w를 추가하면 된다.
printf - wprintf / scanf - wscanf / fgets - fgetws / fputs - fputws
[WBCS3.cpp]
/*
* Windows System Programming - 문자 셋(Character Set)
* 파일명: WBCS3.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-01
* 이전 버전 작성 일자:
* 버전 내용: MBCS와 WBCS의 기본적인 이해
* 이전 버전 내용:
*/
#include <stdio.h>
int main(int argc, char* argv[])
{
int i;
for (i = 0; i < argc; i++)
fputws(argv[i], stdout); // 여기에서 컴파일 에러 발생, 데이터 타입이 일치하지 않음
return 0;
}
이 예제는 컴파일 자체가 되지 않는다.
왜냐하면 입력받는 argv를 통해 전달받는 문자열은 SBCS 기반의 문자열이다.
하지만 fputws를 통해서 WBCS 기반의 문자열을 출력하려고 하기 때문에 컴파일 에러가 난다.
물론 char*를 wchar_t*로 바꾸면 컴파일 에러는 해결된다.
그렇다고 해서 해결이 되지는 않는다.
아까도 봤겠지만 운영체제에서는 기본적으로 MBCS 기반으로 처리한다.
main 함수에 전달되는 문자열은 MBCS로 처리하기 때문에 결국 근본적인 문제는 해결되지 않는다.
[WBCS4.cpp]
/*
* Windows System Programming - 문자 셋(Character Set)
* 파일명: WBCS4.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-01
* 이전 버전 작성 일자:
* 버전 내용: MBCS와 WBCS의 기본적인 이해
* 이전 버전 내용:
*/
#include <iostream>
// main이 아니라 wmain
// 전달되는 문자열을 유니코드 기반으로 구성하겠다는 의미
// char* argv[] -> wchar_t* argv[]로 변경
int wmain(int argc, wchar_t* argv[])
{
// 1부터 시작하므로 실행파일 명은 제외
for (int i = 1; i < argc; i++)
{
fputws(argv[i], stdout);
fputws(L"\n", stdout);
}
return 0;
}
그래서 main이 아닌 wmain을 함수 이름으로 하면 해결이 된다.
나도 처음봤을 때는 생소했는데, 전달되는 문자열을 유니코드 기반으로 구성하겠다는 의미에서 wmain이라고 한 것이다.
그래서 프로그램을 실행하면 전달되는 문자열은 유니코드 기반으로 처리가 된다.
[MBCS와 WBCS의 동시지원]
프로그램을 구현하는 데 있어서 WBCS로 구현하는 것도 사실은 문제가 될 수 있다.
지금은 잘 모르겠지만, 현존하는 시스템 모두가 유니코드를 완벽하게 지원하는 것이 아닐 수 있기 때문이다.
그렇다고 하나의 프로그램을 MBCS 기반, WBCS 기반으로 나눠서 구현하는 것도 번거로운 일이 된다.
그래서 프로그램을 한 번만 구현하고 MBCS 기반으로도, WBCS 기반으로도 돌아하는 형태로 컴파일을 할 수 있어야 한다.
Windows에서는 그 방법을 지원한다.
[#include <windows.h>]
windows.h (또는 Windows.h)는 Windows 기반의 프로그래밍을 하는데 있어서 기본적으로 항상 포함되는 헤더파일이다.
windows.h에는 Windows 프로그래밍에 필요한 다양한 종류의 헤더 파일을 더불어 포함하고 있다.
그래서 어지간하면 이 헤더파일 하나로 충분히 해결되는 경우가 많다.
[Windows에서 정의하고 있는 자료형]
Windows에서는 typedef 키워드를 통해 몇몇 기본 자료형에 Windows 스타일의 새로운 이름을 정의해놓았다.
typedef char CHAR;
typedef wchar_t WCHAR;
#define CONST const;
typedef CHAR* LPSTR; // long pointer string
typedef CONST CHAR* LPCSTR; // long pointer constant string
typedef WCHAR* LPWSTR; // long pointer wide string
typedef CONST WCHAR* LPCWSTR; // long pointer constant wide string
자잘한 이야기이긴 한데, 여기서 LP는 long pointer를 의미한다.
과거 16비트 주소값을 이용하던 시절, Windows 3.1 때의 레거시 코드(옛날 코드)를 그대로 남긴 것.
지금은 32비트 운영체제면 32비트, 64비트 운영체제면 64비트의 주소값을 갖게 된다.
말이 길어졌는데, 쉽게 생각하면 그냥 포인터라고 생각하면 된다.
[WinString.cpp]
/*
* Windows System Programming - 문자 셋(Character Set)
* 파일명: WinString.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-01
* 이전 버전 작성 일자:
* 버전 내용: MBCS와 WBCS의 기본적인 이해
* 이전 버전 내용:
*/
#include <stdio.h>
#include <windows.h>
int wmain(int argc, wchar_t* argv[])
{
// 예전에 쓰인 책이라 현재의 C++에서 아래의 코드는 불가능
//
// LPSTR str1 = "SBCS Style String 1";
// LPWSTR str2 = L"WBCS Styple String 1";
//
// 현재 C++은 문자열 리터럴을 char*가 아닌 const char*로 처리하기 때문
LPCSTR str1 = "SBCS Style String 1";
LPCWSTR str2 = L"WBCS Styple String 1";
CHAR arr1[] = "SBCS Style String 2";
WCHAR arr2[] = L"WBCS Style String 2";
LPCSTR cStr = arr1;
LPCWSTR wStr = arr2;
printf("%s\n", str1);
printf("%s\n", arr1);
wprintf(L"%s\n", str2);
wprintf(L"%s\n", arr2);
return 0;
}
위에서 설명한 예시들을 사용한 예제다.
[MBCS와 WBCS를 동시에 지원하기 위한 매크로]
// tchar.h 에 있는 내용 중 일부
#ifdef UNICODE // UNICODE 매크로가 정의되어 있다면
typedef WCHAR TCHAR;
typedef LPWSTR LPTSTR;
typedef LPCWSTR LPCTSTR;
#else // 정의되어 있지 않다면
typedef CHAR TCHAR;
typedef LPSTR LPTSTR;
typedef LPCSTR LPCTSTR;
#endif
#ifdef _UNICODE // _UNICODE 매크로가 정의되어 있다면
#define __T(x) L ## x // ##은 토큰을 연결하는 것을 의미, 해석하면 Lx가 됨
#else // 정의되어 있지 않다면
#define __T(x) x
#define _T(x) __T(x)
#define _TEXT(x) __T(x)
Windows에서는 MBCS와 WBCS를 동시에 수용하는 형태의 프로그램 구현을 위해 위와 같은 매크로를 정의한다.
해당 매크로를 사용하기 위해서는 tchar.h라는 헤더파일을 포함하면 된다.
그래서 UNICODE와 _UNICODE라는 매크로를 정의하면 WBCS, 유니코드 기반의 프로그램이 구현된다.
반대로 정의하지 않을 경우에는 MBCS 기반의 프로그램이 구현된다.
[MBCS_WBCS1.cpp]
/*
* Windows System Programming - 문자 셋(Character Set)
* 파일명: MBCS_WBCS1.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-01
* 이전 버전 작성 일자:
* 버전 내용: MBCS와 WBCS의 기본적인 이해
* 이전 버전 내용:
*/
// warning 메시지가 나온다
// 기본적으로 .NET 컴파일러에서는 해당 매크로를 기본으로 삽입해주기 때문.
#define UNICODE
#define _UNICODE
// MBCS 기반으로 컴파일하겠다면 아래의 지시자로 정의된 매크로를 무효화하면 된다.
// #undef UNICODE
// #undef _UNICODE
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int wmain()
{
TCHAR str[] = _T("1234567");
int size = sizeof(str);
printf("string length: %d\n", size);
return 0;
}
위의 예제를 그대로 실행하면 빌드는 잘 된다.
그런데 warning 메시지가 뜨면서 매크로를 재정의했다는 말이 나온다.
아마 엄청 예전 컴파일러를 쓰시는 분이 아니라면 이 메시지는 다들 보셨을 것이라 생각이 든다.
최근의 컴파일러는 기본적으로 UNICODE와 _UNICODE 매크로를 자동으로 정의를 해준다.
추가적으로 WIN32, _DEBUG, _CONSOLE이라는 매크로도 정의를 해준다.
책이 있는 분들은 책을 참고해서 확인해보시면 되고, 그게 아니라면 구글링을 해서 확인해보셔도 된다.
그리고 주석으로 달아둔 것처럼 MBCS 기반으로 빌드하여 확인하고 싶으신 분들은 매크로를 해제하면 된다.
[MBCS와 WBCS를 동시에 지원하기 위한 함수들]
위에서는 자료형만을 예시로 들었는데 함수도 매크로로 정의가 되어있다.
마찬가지로 tchar.h 헤더 파일에 선언되어 있는 내용이므로 이를 활용하면 된다.
// tchar.h에 있는 일부 내용
#ifdef _UNICODE // 유니코드 매크로가 정의되었다면
define _tmain wmain
define _tcslen wcslen
define _tcscpy wcscpy
define _tcsncpy wcsncpy
define _tcscmp wcscmp
define _tcsncmp wcsncmp
define _tprintf wprintf
define _tscanf wscanf
define _fgetts fgetws
define _fputts fputws
#else // 아니라면 MBCS 기반
define _tmain main
define _tcslen strlen
define _tcscpy strcpy
define _tcsncpy strncpy
define _tcscmp strcmp
define _tcsncmp strncmp
define _tprintf printf
define _tscanf scanf
define _fgetts fgets
define _fputts fputs
#endif
[MBCS_WBCS2.cpp]
/*
* Windows System Programming - 문자 셋(Character Set)
* 파일명: MBCS_WBCS2.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-01
* 이전 버전 작성 일자:
* 버전 내용: MBCS와 WBCS의 기본적인 이해
* 이전 버전 내용:
*/
#define _CRT_SECURE_NO_WARNINGS
// 사실 정의 안해도 됨
#define UNICODE
#define _UNICODE
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int _tmain(int argc, TCHAR* argv[])
{
// 마찬가지 이유로 문자열 리터럴은 현재 C++에서 안되는 문장.
// MBCS라면 const char*, WBCS라면 const wchar_t*가 된다.
// LPTSTR str1 = _T("MBCS or WBCS 1");
LPCTSTR str1 = _T("MBCS or WBCS 1");
TCHAR str2[] = _T("MBCS or WBCS 2");
TCHAR str3[100];
TCHAR str4[50];
LPCTSTR pStr = str1;
_tprintf(_T("string size: %d \n", sizeof(str2)));
_tprintf(_T("string length: %d \n", sizeof(str2)));
_fputts(_T("Input String 1: "), stdout);
_tscanf(_T("%s"), str3);
_fputts(_T("Input String 2: "), stdout);
_tscanf(_T("%s"), str4);
_tcscat(str3, str4);
_tprintf(_T("String1 + String2: %s \n"), str3);
return 0;
}
이 예제가 어떻게 흘러가는지 이해가 됐다면 이번 챕터에서 설명했던 내용을 전반적으로 잘 정리한 것이 된다.
실제로 이해하는 데에는 어려운 부분은 없었다.
다만 다른 언어들을 새로 배우는 것처럼 자료형이나 함수들을 새로 배우는 느낌이다.
실제로는 새로운 것들이 아니라 원래 있던 것들의 이름이 바뀐 것을 알아가는 것이지만.
아무래도 초반부라서 이런 것부터 알려주는 것이라고 생각한다.