POCU C언어 정주행 18회차 - 멀티바이트, wchar_t

2023. 1. 9. 20:33C언어 복습

1. 멀티바이트

C언어는 기본적으로 문자열 처리가 다른 언어에 비해서 굉장히 구리다... 그래서 이전 글에서 C99의 기능들의 리스트를 쭉 나열하고 대충 요약하고 넘어갔는데 이 부분은 굉장히 중요한 부분으로 보여서 그럴 수 없었다. 또한 이 글부터는 C99를 기준으로 글을 쓸 것이며 예제 코드 역시 그럴 것이다.

 

C언어는 어떤 방식으로 다국어를 지원할까? 아마 다들 머릿속으로 유니코드를 떠올릴 것인데 C99부터 유니코드를 지원하게 되면서 변수 이름(...왜?)과 문자열로 유니코드에 포함된 문자를 사용할 수 있게 되었다. 단, 여기서 말하는 것은 소스 파일을 특정 유니코드 인코딩으로 저장하는 것이 아니라 소스 코드 안에 직접 유니코드 코드 포인트를 작성하는 방식이라고 한다. 이걸 유니버셜 문자 이름(Universal Character Name)이라고 한다. 아래의 그림과 같이 사용할 수 있다.

출처: POCU 아카데미 C 언매니지드 강의

사실 내가 저 그림을 보자마자 들었던 생각이 있었다. "굳이 이렇게까지...?" 누가 일일히 문자 하나하나 찾아서 저런 식으로 사용하고 싶겠는가라는 생각이 들었기 때문이다. 그리고 강의에서는 바로 그 다음에 UCN이라고 하는 것을 지원하는 의의에 대해서 설명해주었다.

  • 아스키 인코딩으로 저장할 경우에 그 문자가 유지되지 않는다.
  • EUC-KR같은 특정 언어용 인코딩으로 저장할 경우에 대부분 컴파일러가 읽지 못한다.
  • UTF-8로 저장할 경우에 옛날 컴파일러는 오류를 띄울 수 있고 해당 언어에 대한 폰트가 없으면 문자가 제대로 나오지 않는다.

그렇다면 어쨋든 C언어에서 소스 코드를 작성하면서 문자열을 처리할 때, UCN을 사용하면 완벽하게 문자열을 처리할 수 있을 것 같다. 하지만 실제로는 그렇지 않다. 왜냐하면 UCN을 써도 인코딩의 종류 자체가 많아서 어떤 것을 기준으로 인코딩을 할지에 따라 결과도 다르게 나올 수 밖에 없기 때문이다. 실제로 아래의 코드의 결과를 보면 알겠지만 의도대로 나오지 않는다.

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

int main(void)
{
    const char* str = "포프";
    
    // 결과는 6이 나온다.
    printf("%d\n", strlen(str));
    
    return 0;
}

이것을 멀티바이트 문자라고 한다. C에서의 멀티바이트는 인코딩에 상관없이 1개 이상의 바이트로 된 문자를 의미한다. 이것이 기본으로 세팅되어 있다. 즉, 어떤 나라의 문자를 사용하는지에 따라 해당 문자를 표현하기 위한 바이트 수가 달라질 수 있다는 것이다.

 

C언어에서 멀티바이트가 제대로 동작하는 과정은 다음과 같다.

  • 아스키가 아닌 문자열을 받았다고 치면 그대로 char*로 들어온다.
  • 이 때, char*로 들어온 데이터를 읽기 위해 인코딩이 어떻게 되어 있는지 알아야 하는데 이것은 사용자의 컴퓨터 환경에 따른다.
  • 즉, 바이트 단위로 그대로 읽고 사용자의 인코딩 세팅에 따라 변환시켜서 사용자에게 보여주는 것이다.
  • 참고로 아스키 문자는 이런 거 상관없이 대부분 그대로 잘 동작한다.

이 과정에서 아주 큰 문제가 있다. 서로 문자열과 같은 데이터를 주고 받을 때, 내가 어떤 사람에게 무언가를 전달했으면 받는 사람의 컴퓨터의 인코딩 세팅도 나와 같아야 한다는 것이다. 그렇지 않으면 같은 바이트를 다른 방식으로 읽게 되어서 완전히 엉뚱하게 읽어버린다. C언어는 이 단점을 해결하기 위해 ICU(International Components for Unicode)라는 라이브러리를 제공한다.

 

 

2. wchar_t

지금까지 C가 얼마나 문자열 처리가 구린지 알아봤는데 사실 옛날부터 C89는 다국어 처리를 위해 여러 시도를 했었다. 그 중 하나가 바로 wchar_t이다. 이것은 기존에 알아봤던 멀티바이트와 다르게 모든 문자가 같은 크기를 차지하며 그 크기는 타겟 플랫폼에서 지원하는 인코딩 중 가장 큰 문자를 담을 수 있도록 하게 되어있다. 즉, 컴파일러마다 다르며 리눅스는 UTF-32로 4바이트이며 윈도우는 UCS-2로 2바이트를 차지한다. 

 

사용법은 아래와 같다. L을 빼면 컴파일 오류가 난다.

const wchar_t* w_str = L"포프"; // L을 빼면 컴파일 오류!
printf("%ls\n", w_str);			// %ls의 l을 빼면 컴파일 오류!

 

이렇게 wchar_t와 적절한 서식 문자를 사용하면 컴파일 중에 적절하게 인코딩에 맞게 변경된다. 또한 C89는 char*와 wchar_t*를 서로 변경할 수 있는 함수도 제공하며 wchar_t*에 대한 또 다른 기능을 가지고 있는 함수는 C95에 처음 나와서 C99에서 제대로 쓸 수 있다.

//아래의 함수를 사용하려면 이 헤더파일을 인클루드해야 한다.
#include <stdlib.h>

int mbtowc(wchar_t* pwc, const char* s, size_t n);
size_t mbstowcs(wchar_t* dst, const char* src, size_t len);

int wctomb(char* s, wchar_t wc);
size_t wcstombs(char* dst, const wchar_t* src, size_t len);

 

근데 여기에 아주 큰 문제가 있다. wchar_t에 대해서 이야기할 때, 표준에서 wchar_t의 크기를 어떻게 명시하고 있는지 기억하는가? 분명히 컴파일러마다 플랫폼마다 다르다고 이야기했었다. 즉, wchar_t을 사용한 파일을 서로 공유한다고 할 때, 서로 다른 운영체제를 사용한다면 어떻게 되겠는가? 누군가의 컴퓨터에서는 제대로 작동을 하는데 누군가의 컴퓨터에서는 제대로 작동을 하지 않는 문제가 발생하게 될 것이다. 즉, 다양한 플랫폼에서 포팅이 힘들다는 큰 문제가 생긴 것이다.

 

해결방법: ICU를 사용하거나 두 플랫폼 사이에 변환하는 라이브러리를 사용해야 한다.