끝나지 않는 프로그래밍 일기

[시스템 프로그래밍]


문자 집합

(Character Sets)




이 문자 집합(Character Sets) 편에서는 SBCS(Single Byte Character Set), MBCS(Multi Byte Character Set), WBCS(Wide Byte Character Set)와 이를 기반한 함수를 간단히 살펴볼 생각입니다.


우리가 대표적으로 알고있는 문자 집합(Character Sets)엔 무엇이 있을까요? 예를 들어보면, 아스키코드(ASCII), 유니코드(Unicode)가 있습니다. 이 두 집합을 자세히 알아보자면, 아스키코드(American Standard Code for Information Interchange)는 미국에서 정의하고 있는 정보 교환 표준이며 7-bit(128개) 인코딩 방식입니다. 33개의 제어 문자와 95 개의 출력 문자로 이루어져 있으며, 1바이트 만으로도 영어, 숫자, 특수기호, 제어문자 등을 모두 나타낼수 있습니다. 

<아스키코드 표(ASCII Table)>


위에 있는 테이블이, 아스키코드 테이블인데 33개의 제어 문자와, 95개의 출력 문자를 합쳐보면 128개의 문자인데 1바이트로 나타낼 수 있는 문자가 256개의 문자이므로 1바이트만으로 위의 문자들을 모두 표현이 가능하기에 아스키코드는 1바이트로 표현이 된다고 말합니다. 이번에는 유니코드(Unicode)를 봐볼까요? 유니코드는 전 세계의 문자를 사용하기 위해 한 문자를 2바이트로 사용하고 있는 녀석입니다. 2바이트론 65536개(2의 16승)의 문자를 나타낼수 있죠. 일본어든, 한국어든, 영어든 모두 2바이트로 표현합니다.


이제 더 자세히 들어가, 문자 집합(Character Sets)에 대해 알아봅시다. 문자 집합은 크게 4가지로 분류할수 있으나, 여기선 3가지만을 설명하도록 하겠습니다.


 1-1. SBCS(Single Byte Character Set)  1바이트만을 사용한다!

SBCS(Single Byte Character Set)은 Single Byte(0~255) 그대로 1바이트만을 사용하는 문자 집합입니다. 대표적으로 아스키코드가 이에 해당합니다.


 1-2. MBCS(Multi Byte Character Set)  한글은 2바이트, 영문은 1바이트!

MBCS(Multi Byte Character Set)은 Multi Byte(멀티 바이트) 그대로 다양한 바이트 수를 통해 문자를 표현합니다. 이 MBCS는 1바이트로 표현 가능한 문자는 1바이트로 표현하고, 2바이트로 표현 가능한 문자는 2바이트로 표현하며 대표적으로 EUC-KR, CP949 등이 있습니다.


 1-3. WBCS(Wide Byte Character Set)  2바이트만을 사용한다!

WBCS(Wide Byte Character Set)은 SBCS와는 달리 모든 문자를 2바이트로 표현합니다. 한글도 2바이트, 영문도 2바이트, 일본어도 2바이트이며 대표적으로 앞서 말한 유니코드(Unicode)가 이에 해당합니다.


위의 문자 집합들을 살펴보면, MBCS가 WBCS 보다 효율적으로 문자를 표현합니다. MBCS는 영문은 1바이트로, 한글은 2바이트로 나타내지만 WBCS는 영문 혹은 한글 등과 상관 없이 모두 2바이트로 표현하기 때문입니다. 언뜻 보면 MBCS가 좋은것 같으나 단점도 역시 존재합니다. 한번 보도록 하죠.

#include <stdio.h>
#include <string.h>

int main()
{
	char str[] = "ABCD가나다라";
	printf("Array Size: %d\n", sizeof(str));
	printf("String Length: %d\n", strlen(str));

	return 0;
}

결과:

Array Size: 13
String Length: 12
계속하려면 아무 키나 누르십시오 . . .


코드엔 아무런 이상이 없어보입니다. 하지만 결과를 한번 보도록 하죠. 7행의 sizeof 연산자는 str 배열이 차지하고 있는 메모리 크기를 바이트 단위로 반환합니다. 여기서 영문은 1바이트, 한글은 2바이트(MBCS 기반) 이므로 ABCD는 4바이트, 가나다라는 8바이트를 차지합니다. 그리고 문자열의 끝을 의미하는 NULL 문자까지 합쳐서 13바이트를 차지하는 것입니다. 배열의 크기가 13인건 정상적인 출력 결과입니다. 문제인건 그 다음 행이죠. 여기서 strlen 함수는 SBCS 기반의 함수입니다. 영문은 1바이트로 계산해도, 한글은 2바이트를 차지합니다. (NULL 문자의 크기는 제외함) 그렇기 때문에 문자열의 크기가 12라고 나온겁니다.


위의 문제를 해결하려면 WBCS 기반(여기서 말하는건 유니코드 기반)의 프로그래밍을 하여야 합니다. WBCS 기반의 프로그래밍에선 1바이트 만큼의 메모리 공간을 할당하는 char형 변수와는 달리 2바이트 메모리 공간이 할당되는 wchar_t형 변수를 사용합니다. 그럼 위의 예제에서 char만 wchar_t로 바꾸어 보도록 할까요?

wchar_t str[] = "ABCD가나다라";

char형을 wchar_t형으로 바꾼 뒤에 컴파일을 하려 하자 아래와 같은 에러가 발생합니다.

1    IntelliSense: "const char [13]" 형식의 값을 사용하여 "wchar_t []" 형식의 엔터티를 초기화할 수 없습니다.    c:\Users\su6net-work\Documents\Visual Studio 2012\Projects\ConsoleApplication2\ConsoleApplication2\소스.cpp    6    18    ConsoleApplication2


왜 위와 같은 에러가 발생할까요? 오른쪽은 유니코드를 기반으로 한 문자 자료형이지만, 왼쪽은 MBCS 기반의 문자열이기 때문입니다. MBCS 기반의 문자열을 wchar_t형 배열에 넣으려니 에러가 발생하는 것이죠. 이와 같은 에러는 오른쪽 문자열 앞에 L을 붙여줌으로써 해결할 수 있습니다. 여기 적히는 L은 다음과 같은 의미를 가집니다. "L 뒤에 등장하는 문자열은 WBCS 기반으로 표현한다" 라고 말이죠. 앞에다 L을 달아주면 아래와 같은 형태를 가질 것입니다. (아래의 wchar_t는 typedef를 통하여 unsigned short를 줄인 것입니다.)

wchar_t str[] = L"ABCD가나다라";

이렇게 MBCS 기반의 문자열 앞에다가 L을 달아주면 WBCS 기반의 문자열로 표현하게 되며, 영문도 2바이트 한글도 2바이트 심지어 NULL 문자까지도 2바이트로 표현하게 됩니다. 하지만 아직 끝이 아닙니다. 예제 아래에 있는 strlen 함수는 SBCS 기반의 함수이기 때문입니다. strlen의 함수 원형을 보시면 char형을 인수로 받고 있음을 보실 수 있습니다. 이를 해결하기 위해선 SBCS 기반의 함수가 아닌 WBSC 기반의 문자열 함수를 이용해야만 합니다.


SBCS 기반의 함수

WBCS 기반의 문자열 관련 함수

strlen

size_t __cdecl wcslen(const wchar_t* _Str);

strcpy

wchar_t* wcscpy(wchar_t* Dest, const wchar_t* Source);

strcat

wchar_t* wcscat(wchar_t* Dest, const wchar_t* Source);

strcmp

int wcscmp(const wchar_t* Str1, const wchar_t* Str2);


여기서 SBCS 기반의 함수와 WBCS 기반의 함수의 전달되는 인자는 거의 똑같으므로 크게 신경쓰지 않으셔도 되고, 위의 SBCS 기반의 함수들을 WBCS 기반의 문자열 관련 함수를 통해 예제를 바꾸어보도록 합시다.

#include <stdio.h>
#include <string.h>

int main()
{
	wchar_t str[] = L"ABCD가나다라";
	printf("Array Size: %d\n", sizeof(str));
	printf("String Length: %d\n", wcslen(str));

	return 0;
}

결과:

Array Size: 18
String Length: 8
계속하려면 아무 키나 누르십시오 . . .


결과를 보니 정상적인 결과가 출력됨을 보실 수 있습니다. 8행의 sizeof 연산자는 str가 차지하고 있는 메모리 공간을 바이트 단위로 보여주며, 영문, 한글 상관없이 모두 2바이트로 표현되므로 16바이트를 차지하게 되고 마지막에 있는 NULL 마저도 2바이트로 표현되어 총 18바이트가 되는 것입니다. 그리고 SBCS 기반의 함수를 WBCS 기반의 함수로 바꾸어주니 문자열의 길이가 제대로 표현됨을 알 수 있습니다. 이참에 문자열 입출력 함수를 통해 SBCS 함수인 printf, scanf를 WBCS 기반의 함수로 바꾸어 str의 내용을 출력해보도록 합시다.


SBCS 기반의 함수

WBCS 기반의 문자열 입출력 함수

printf

int wprintf(const wchar_t *format [,argument]...);

scanf

int wscanf(const wchar_t *format [,argument]...);


WBCS 기반의 문자열 입출력 함수도 SBCS 기반의 함수에 전달해야하는 인자와 별반 다를게 없습니다. 예제에 있는 printf를 wprintf로 바꾸고 str의 내용을 출력해보도록 합시다. 유니코드로 표현하려는 문자열 앞에다 L을 붙여야 한다는 것을 꼭 기억하세요.

#include <stdio.h>
#include <string.h>
#include <locale.h> // _wsetlocale 함수를 쓰려면 locale.h를 포함

int main()
{
	wchar_t str[] = L"ABCD가나다라";

	// 유니코드 기반으로 한글을 출력하기 위함
	_wsetlocale(LC_ALL, L"korean");
	wprintf(L"Array Size: %d\n", sizeof(str));
	wprintf(L"String Length: %d\n", wcslen(str));
	wprintf(L"str[] = %s\n", str);

	return 0;
}

결과:

Array Size: 18
String Length: 8
str[] = ABCD가나다라
계속하려면 아무 키나 누르십시오 . . .


위의 예제의 10행을 보시면 _wsetlocale란 함수가 쓰였는데, 유니코드 기반으로 한글을 출력할 때 이 함수를 호출해 주어야 합니다. 이 함수를 통해서 자신이 해당하는 나라의 언어로 표현을 할 수 있게 되는 것입니다. 나머지는 설명을 드리지 않아도 아시겠죠? 여기까진 WBCS 기반과 MBCS 기반의 프로그래밍 예제를 보았습니다. 이번에는 WBCS와 MBCS를 동시에 지원하려면 어떻게 해야하는지에 대해 알아보도록 하겠습니다.


만약에, 우리가 MBCS 기반의 프로그램을 만들다가, 문제가 생겨 WBCS 기반의 프로그램으로 바꾸려면 어떻게 해야 할까요? 일일히 문자열 앞에는 L을, 그리고 SBCS 기반의 함수는 WBCS 기반의 함수로 바꾸어 주어야 합니다. 규모가 작은 프로젝트라면 크게 문제될 것은 없겠지만, 큰 프로젝트는 어떨까요? 유지보수에 불편함이 있고, 번거롭다는 등의 문제가 존재합니다. 그렇다면 어떻게 해야 할까요? 바로 TCHAR란 녀석을 이용하면 쉽게 해결할 수 있습니다. 아래는 windows.h와 tchar.h의 내용을 일부만 뽑아내어 정리된 것입니다.

typedef char    CHAR;
typedef wchar_t WCHAR;
 
#define CONST  const
typedef CHAR*   LPSTR;
typedef CONST CHAR*     LPCSTR;
typedef CONST WCHAR*    LPCWSTR;
typedef WCHAR*  LPWSTR;
 
#ifdef UNICODE
    typedef WCHAR   TCHAR;
    typedef LPWSTR  LPTSTR;
    typedef LPCWSTR LPCTSTR;
#else
    typedef CHAR    TCHAR;
    typedef LPSTR   LPTSTR;
    typedef LPCSTR  LPCTSTR;
#endif
 
 
#ifdef _UNICODE
    #define __T(x)      L ## x
    #define _tmain      wmain
    #define _tcslen     wcslen
    #define _tcscat     wcscat
    #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
    #define __T(x)      x
    #define _tmain      main
    #define _tcslen     strlen
    #define _tcscat     strcat
    #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
 
#define _T(x)       __T(x)
#define _TEXT(x)    __T(x)

위 내용을 살펴보면 typedef 키워드를 통해 char가 CHAR로, wchar_t가 WCHAR라는 또다른 이름이 붙었습니다. 그 아래를 보시면 const는 CONST라는 또다른 이름이 붙고, LPSTR(Long Pointer String, CHAR*), LPCSTR(Long Pointer Constant String, CONST CHAR*), LPWSTR(Long Pointer Wide String, WCHAR*), LPCWSTR(Long Pointer Constant Wide String, CONST WCHAR*) 처럼 포인터와 CONST 포인터의 또다른 이름이 붙었습니다. 그 아래를 보시면 UNICODE가 정의되었냐, 정의되지 않았느냐에 따라 TCHAR, LPTSTR, LPCTSTR의 의미가 달라지는데 만약 UNICODE가 정의되었다면(WBCS 기반의 프로그래밍) TCHAR가 의미하는건 WCHAR, LPTSTR가 의미하는건 LPWSTR, LPCTSTR가 의미하는건 LPCTSTR가 되며 만약 반대일 경우 CHAR, LPSTR, LPCSTR를 의미하겠죠?


그리고 더 아래에는 _UNICODE가 정의되었냐, 정의되지 않았냐에 따라 함수와 문자열에 L을 표기하느냐 마느냐가 달라지게 됩니다. 마지막에 있는 _T(x), _TEXT(x)는 __T(x)와 동일하다는 것입니다. TCHAR를 사용하려면 tchar.h를 포함하여야 한다는 것을 명심하세요. 이는 windows.h에 정의되어 있지 않습니다. 그럼, MBCS, WBCS를 둘다 지원하는 매크로를 통해 예제를 작성해보도록 합시다.

#include <stdio.h>
#include <string.h>
#include <Windows.h>
#include <tchar.h>

int main()
{
    TCHAR str[] = _T("ABCD1234");

    _tprintf(_T("Array Size: %d\n"), sizeof(str));
    _tprintf(_T("String Length: %d\n"), _tcslen(str));
    _tprintf(_T("str[] = %s\n"), str);
 
    return 0;
}

결과(멀티바이트 문자 집합을 사용할 경우):

Array Size: 9
String Length: 8
str[] = ABCD1234
계속하려면 아무 키나 누르십시오 . . .

결과(유니코드 문자 집합을 사용할 경우):

Array Size: 18
String Length: 8
str[] = ABCD1234
계속하려면 아무 키나 누르십시오 . . .


멀티바이트 문자 집합을 사용하였을 경우와, 유니코드 문자 집합을 사용하였을 경우의 결과가 서로 다르죠? 이렇게 설정 하나만 바꾸어도 손쉽게 MBCS에서 WBCS로, WBCS에서 MBCS로 바꿀수 있습니다. Visual Studio 2012 기준으로 "프로젝트 우클릭 -> 속성 -> 구성 속성 -> 일반 -> 프로젝트 기본값 -> 문자 집합 수정" 에서 수정하실 수 있습니다. 문자 집합은 여기까지 알아보도록 하겠습니다. 모두 수고하셨습니다.