^(코딩캣)^ = @"코딩"하는 고양이;

libc 문자열 조작 함수 정리 (part 10 - strxfrm, strcoll)

Language/C & C++
2018. 8. 21. 23:41

libc 문자열 조작 함수 정리


C 언어에서 문자열 처리는 복잡하다. 언어 수준에서 문자열이라는 데이터 형 자체를 지원하지도 않으니, 덧셈 기호(+)나 비교연산자(==)와 같은 기호를 사용하는 직관적인 문자열 연산을 사용할 수 없기 때문이다. C 언어가 문자열 데이터 형을 지원하지 않고, 문자열을 다루는 연산자도 없으니 모든 문자열 연산은 문자열 함수를 통해 이루어진다. C 표준 라이브러리(일명 'libc')에서 str...로 시작하는 함수들이 그것이며, 모두 string.h 헤더(C++은 cstring 헤더)에 정의되어 있으며 본 시리즈를 통해 이들 함수의 사용법을 정리해보고자 한다. 본 시리즈는 cplusplus(http://www.cplusplus.com) 및 MSDN에 나와있는 레퍼런스를 기준으로 하여 작성되었다.

  1. libc 문자열 조작 함수 정리 (part 01 - strcpy, strncpy)
  2. libc 문자열 조작 함수 정리 (part 02 - strcat, strncat)
  3. libc 문자열 조작 함수 정리 (part 03 - strcmp, strncmp)
  4. libc 문자열 조작 함수 정리 (part 04 - strchr, strrchr)
  5. libc 문자열 조작 함수 정리 (part 05 - strstr)
  6. libc 문자열 조작 함수 정리 (part 06 - strtok)
  7. libc 문자열 조작 함수 정리 (part 07 - strspn, strcspn)
  8. libc 문자열 조작 함수 정리 (part 08 - strlen)
  9. libc 문자열 조작 함수 정리 (part 09 - strpbrk)
  10. libc 문자열 조작 함수 정리 (part 10 - strxfrm, strcoll)
  11. libc 문자열 조작 함수 정리 (part 11 - strerror)

Part X. strxfrm, strcoll


이번 포스팅에서는 시스템 로케일 설정에 따라 문자열을 변환하고 비교할 목적으로 사용되는 함수인 strxfrmstrcoll 함수에 대해 정리한다.

1. strxfrm


strxfrm 함수는 시스템 로케일 설정에 따라 문자열을 변환transform하여 버퍼에 복사하고 변환된 문자열의 길이를 반환하는 함수이다. 여기서 변환이란 시스템 로케일에서 정의한 문자열 변환작업을 의미하는데 구체적으로 무엇을 어떻게 변환하는지에 대해서는 명확하게 정의된 것이 없다. 다만, 변환된 문자열은 일종의 해시hash로서 취급되며 strcmp 함수에 의한 일치 또는 순서가 본래의 문자열과 일치함은 보장한다.

strxfrm 함수의 원형은 다음과 같다.

size_t strxfrm(char * destination, const char * source, size_t num);
destination
변환된 문자열이 복사될 문자열 버퍼이다.
source
변환할 문자열이 보관된 상수 또는 문자열 버퍼이다.
num
destination 버퍼가 보관 가능한 최대 문자 수이다.

함수의 수행 결과 현재 시스템 로케일 조건에서 원본 문자열로부터 변환된 문자열이 destination으로 지정한 문자열 버퍼에 복사되고 이 버퍼에 복사된 문자 수가 반환된다.

다음은 strxfrm 함수의 사용 예이다. macOS(10.13.6 High Sierra 기준) 및 FreeBSD에서는 로케일 기능을 구현하는 부분에서 버그가 존재하기 때문에 아래 소스 코드에 의한 결과가 다를 수 있다. 그 결과는 신뢰할 수 없으므로 가급적 macOSFreeBSD를 제외한 운영체제에서 실행해보기를 권장한다.

/* strxfrm.c */
#include <stdio.h>
#include <string.h>
#include <locale.h>

int main(int argc, char * argv[])
{
    char str1[512] = "hlava";
    char str2[512] = "číšník";
    char xfm1[512] = { '\0', };
    char xfm2[512] = { '\0', };
    char * result = NULL;
    size_t lxfm1 = 0;
    size_t lxfm2 = 0;

    lxfm1 = strxfrm(xfm1, str1, sizeof xfm1);
    lxfm2 = strxfrm(xfm2, str2, sizeof xfm2);
    if ((lxfm1 > 0) && (lxfm2 > 0))
    {
        printf("<Locale Unset>\n");
        printf("setlocale = \"%s\"\n", (result == NULL) ? "NULL" : result);
        printf("str1: \"%s\" --> \"%s\"\n", str1, xfm1);
        printf("str2: \"%s\" --> \"%s\"\n", str2, xfm2);
        printf("strcmp(str1, str2) = %d\n", strcmp(str1, str2));
        printf("strcmp(xfm1, xfm2) = %d\n", strcmp(xfm1, xfm2));
        printf("returns of strxfrm: %zu / %zu\n", lxfm1, lxfm2);
    }

    result = setlocale(LC_ALL, "cs_CZ.UTF-8");
    lxfm1 = strxfrm(xfm1, str1, sizeof xfm1);
    lxfm2 = strxfrm(xfm2, str2, sizeof xfm2);
    if ((lxfm1 > 0) && (lxfm2 > 0))
    {
        printf("<cs-CZ.UTF-8>\n");
        printf("setlocale = \"%s\"\n", result);
        printf("str1: \"%s\" --> \"%s\"\n", str1, xfm1);
        printf("str2: \"%s\" --> \"%s\"\n", str2, xfm2);
        printf("strcmp(str1, str2) = %d\n", strcmp(str1, str2));
        printf("strcmp(xfm1, xfm2) = %d\n", strcmp(xfm1, xfm2));
        printf("returns of strxfrm: %zu / %zu\n", lxfm1, lxfm2);
    }

    return 0;
}
[그림 1] strxfrm.c 예제 소스 코드
[그림 2] strxfrm.c 예제 소스 코드의 실행 결과

위 코드는 체코어 단어인 "hlava""číšník"의 순서를 비교하는 예이다. 두 단어는 각각 str1str2에 UTF-8 인코딩으로 보관되어 있다. (단, unix 일때에 한함.) Microsoft Windows 등의 운영체제를 고려하여 확실하게 UTF-8 인코딩으로 문자열 상수를 보관하기 위해 str1str2의 상수 할당을 다음과 같이 지정해도 좋다.

/* strxfrm.c: alternate */
char str1[] = { 0x68, 0x6C, 0x61, 0x76, 0x61, 0x00 }; // hlava
char str2[] = { 0xC4, 0x8D, 0xC3, 0xAD, 0xC5, 0xA1, 0x6E, 0xC3, 0xAD, x6B, 0x00 }; // číšník

보통의 소문자 'c'의 유니코드는 U+0063이고, 카론caron이 붙은 소문자 'č'의 유니코드는 U+010D이다. 또한 보통의 소문자 'h'의 유니코드는 U+0068이다. 유니코드에 의한 단순 정렬 시,

'c' (U+0063) - 'h' (U+0068) - 'č' (U+010D)

가 되겠지만 체코어 알파벳의 순서대로 문자를 정렬할 경우,

'c' (U+0063) - 'č' (U+010D) - 'h' (U+0068)

의 순서로 정렬된다. strxfrm은 특정 언어로 적힌 문자열을 strcmp로 순서 비교할 때 이러한 문화권(로케일) 차이를 반영하여 해당 언어의 사전 순서대로 정렬할 수 있도록 특별한 패턴의 문자열을 생성하는 역할을 한다. 다시 말하면, strcmp로 순서 비교하고자 할 때 여기에 들어갈 문자열은 "hlava""číšník" 등의 원본 문자열이 아니고, strxfrm을 통해 변환된 문자열(일종의 해시hash)이어야 한다는 것이다.

첫 번째 실험인 <Locale Unset>항목을 본다.

아직 로케일을 명시하지 않은 상태이기 때문에 모든 문자열은 단순히 유니코드에 등재된 순서대로 비교 연산을 수행한다. 그렇기 때문에 strxfrm 함수는 문자열 버퍼에 원본 문자열 그대로를 복사하고 strcmp 함수는 원본 문자열 그대로 비교 연산을 수행한다. 앞서 설명한 대로 유니코드에 의한 단순 정렬 시 보통의 라틴문자인 'h'가 확장 라틴문자인 'č'에 선행하기 때문에(우선하기 때문에) strcmp("hlava", "číšník");의 결과 음수가 반환된다. strcmp의 반환값에 대한 설명은 [libc 문자열 조작 함수 정리 (part 03 - strcmp, strncmp)]를 참고한다.

두 번째 실험인 <cs_CZ.UTF-8>항목을 본다.

setlocale 함수를 사용하여 로케일이 설정된 상태이므로 체코어 알파벳 순서에 따라 문자열의 비교가 가능하다. 로케일이 설정된 상태에서 strxfrm 함수는 문자열 버퍼에 변환된 문자열을 만든다. 각 문자열은 "hlava""číšník"로부터 얻어진 일종의 해시이기 때문에 읽을 수 있는 문자열은 아니지만 해당 문자열을 대신하여 strcmp 함수에 의한 우선순위 또는 일치 여부를 확인하는데 사용될 수 있다.

위와 같이 얻어진 문자열을 strcmp 함수에 적용해 보자. 로케일을 설정하기 전에는 'h''č'에 선행한다고 보아 음수를 반환하였는데, 로케일을 설정한 후에는 'č''h'에 선행한다고 보아서 양수를 반환하는 것을 볼 수 있다.

좀 더 확인하기 위하여 여러 종류의 로케일에 대해 문자열을 비교해보도록 한다. 위의 코드 중 setlocale에 전달되는 "cz_CS.UTF-8" 부분을 "문자열이 UTF-8로 인코드된 미국 영어 로케일"(en_US.UTF-8)과 "문자열이 UTF-8로 인코드된 한국어 로케일"(ko_KR.UTF-8)로 설정하였을 때 결과는 각각 다음과 같이 나올 것이다. (Ubuntu 기준)

########## terminal ##########
<ko_KR.UTF-8>
setlocale = "ko_KR.UTF-8"
str1: "hlava" --> "hlava"
str2: "číšník" --> "číšník"
strcmp(str1, str2) = -92
strcmp(xfm1, xfm2) = -92
returns of strxfrm: 5 / 10

<en_US.UTF-8>
setlocale = "en_US.UTF-8"
str1: "hlava" --> (garbage)
str2: "číšník" --> (garbage)
strcmp(str1, str2) = -92
strcmp(xfm1, xfm2) = 5
returns of strxfrm: 17 / 20

미국 영어 로케일(en_US)도 체코어와 같은 로마자를 사용하므로 문자 'č''c' 계열의 문자로 간주하여 'h'보다는 앞 순서로 판별하도록 strxfrm에서 문자열 변환이 이루어진다. 그러므로 strcmp(strxfrm("hlava"), strxfrm("číšník"));의 결과 양수가 반환된다.

한국어 로케일(ko_KR) 조건에서는 로마자 문화권이 아닌 상태가 되므로 'č' 문자를 인식하지 않아 단순 유니코드 값대로 순서가 판단되도록 strxfrm에서 문자열 변환이 일어나지 않는다. 그러므로 strcmp(strxfrm("hlava"), strxfrm("číšník"));의 결과 음수가 반환됨을 확인할 수 있다.

참고로 유닉스(리눅스)에서 현재 시스템이 지원 가능한 로케일의 목록을 확인하는 방법은 다음과 같다.

$ locale -a

로케일 관련된 파일들은 대체로 /usr/share/locale 디렉터리에 정의되어 있다. Debian(Ubuntu) 계열의 운영체제에서 특정 로케일을 설치하고자 할 경우 다음과 같이 실행한다.

예를 들어 한국어 로케일(ko-KR)을 생성하고자 할 경우,

$ sudo locale-gen ko_KR

그리고 EUC-KR 인코딩을 지원하는 한국어 로케일을 추가하고자 할 경우,

$ sudo locale-gen ko_KR.EUC-KR

1-1. Wide Character 확장 함수 - wcsxfrm


상기 strxfrm는 ASCII 문자열 또는 UTF-8 인코딩의 Unicode 문자열에 대해 사용 가능하다. UTF-16/UTF-32와 같은 Wide Character 문자열의 복사는 아래의 함수를 사용 가능하며, wchar.h, C++에서는 cwchar 헤더를 include한다.

size_t wcsxfrm(wchar_t * destination, const wchar_t * source, size_t num);

2. strcoll


함수의 원형은 다음과 같이 정의되어 있다.

int strcoll(const char * str1, const char * str2);
str1
현지 언어로 적힌 첫번째 문자열이다.
str2
현지 언어로 적힌 두번째 문자열이다.

strcoll 함수는 strxfrm 함수와 strcmp 함수를 합친 함수로서, 현재 설정된 로케일에 의한 문자열 비교 연산을 수행한다. 즉 변환 문자열을 담기 위한 버퍼의 선언과 변환 문자열의 복사는 함수 내부적으로 알아서 수행되므로 strcmp 함수를 사용할 때와 같은 방식으로 원본 문자열만 직접 전달해주면 비교 연산 결과를 반환한다. 문자열 비교 연산의 결과에 대한 정리는 [libc 문자열 조작 함수 정리 (part 03 - strcmp, strncmp)]를 참고한다.

앞서 예시로 적은 소스 코드를 strcoll 함수를 사용할 경우 다음과 같이 코드 분량을 줄이면서 현지 언어 문자열 비교를 할 수 있다.

/* strcoll.c */
#include <stdio.h>
#include <string.h>
#include <locale.h>

int main(int argc, char * argv[])
{
    char str1[512] = "hlava";
    char str2[512] = "číšník";
    char * result = NULL;

    printf("<Locale Unset>\n");
    printf("setlocale = \"%s\"\n", (result == NULL) ? "NULL" : result);
    printf("strcoll(\"%s\", \"%s\") = %d\n", str1, str2, strcoll(str1, str2));

    printf("\n");

    result = setlocale(LC_ALL, "cs_CZ.UTF-8");
    printf("<cs-CZ.UTF-8>\n");
    printf("setlocale = \"%s\"\n", (result == NULL) ? "NULL" : result);
    printf("strcoll(\"%s\", \"%s\") = %d\n", str1, str2, strcoll(str1, str2));

    return 0;
}
[그림 3] strcoll.c 예제 소스 코드
[그림 4] strcoll.c 예제 소스 코드의 실행 결과

마찬가지로 두 문자열에 대해 로케일을 적용하기 전의 문자열 비교 결과와 로케일 적용 후의 문자열 비교 결과가 서로 다름을 알 수 있다. 로케일을 적용하기 전에는 문자열을 단순 유니코드 순으로 정렬하므로 "hlava""číšník"에 선행한다고 보아 음수를 반환하지만, 로케일을 적용한 후에는 확장 라틴 문자 또한 해당 언어의 알파벳 순서대로 비교하므로 "číšník""hlava"에 선행한다고 보아 strcoll("hlava", "číšník");는 양수를 반환한다.

2-1. Wide Character 확장 함수 - wcscoll


상기 strcoll은 ASCII 문자열 또는 UTF-8 인코딩의 Unicode 문자열에 대해 사용 가능하다. UTF-16/UTF-32와 같은 Wide Character 문자열의 복사는 아래의 함수를 사용 가능하며, wchar.h, C++에서는 cwchar 헤더를 include한다.

int wcscoll(const wchar_t * wcs1, const wchar_t * wcs2);

<Epilogue>


본 포스팅을 통해 문자열 비교 함수에 대해 정리해 보았다. 다음 포스팅[libc 문자열 조작 함수 정리 (part 11 - strerror)]에서는 가장 마지막에 발생한 오류의 내용을 구할 수 있는 strerror 함수에 대해 정리한다.

카테고리 “Language/C & C++”
more...