12-3.문자열 연습

12-3-가.표준 함수 구현

C 문법은 문자열 타입을 제공하지 않는 대신 풍부한 문자열 관리 함수를 제공하므로 문자열을 사용하는데 별 불편함은 없다. 문자열은 배열 형태로 표현되며 배열은 곧 포인터라고 할 수 있으므로 C 표준 문자열 함수들은 거의 포인터로 구현되어 있다. 그래서 표준 함수들의 모방작을 제작해 보면 문자열을 좀 더 잘 이해할 수 있을 뿐만 아니라 C 포인터에 대한 생생한 실습을 해 볼 수 있을 것이다.

다음 예제는 문자열 관련 C 표준 함수를 사용자 정의 함수로 다시 작성해 본 것이다. C 표준 함수의 구현은 컴파일러 제작사마다 다르고 고도로 최적화되어 있어서 이와 완전히 같지는 않겠지만 겉으로 드러나는 동작은 동일하다. 이 실습을 통해 이미 제공되는 표준 함수들의 내부 구현이 어떻게 되어 있는지 상상해 볼 수 있음은 물론이고 표준 함수가 제공하지 않는 기능을 직접 만들어 쓸 수 있는 응용력을 키울 수 있다.

 

: strfunc

#include <Turboc.h>

 

void my_puts(const char *str)

{

     while (*str) putch(*str++);

     putch('\n');

}

 

void my_puts2(const char *str)

{

     int i;

 

     for (i=0;str[i];i++) {

          putch(str[i]);

     }

     putch('\n');

}

 

char *my_strcpy(char *dest, const char *src)

{

     char *d=dest;

     while (*dest++ = *src++) {;}

     return d;

}

 

size_t my_strlen(const char *str)

{

     const char *p;

 

     for (p=str;*p;p++) {;}

     return p-str;

}

 

size_t my_strlen2(const char *str)

{

     int i;

 

     for (i=0;str[i];i++) {;}

     return i;

}

 

char *my_strcat(char *dest, const char *src)

{

     my_strcpy(dest+my_strlen(dest),src);

     return dest;

}

 

char *my_strchr(const char *string,int c)

{

     while (*string) {

          if (*string == c) {

              return (char *)string;

          }

          string++;

     }

     return NULL;

}

 

char *my_strstr(const char *string,const char *strSearch)

{

     const char *s,*sub;

 

     for (;*string;string++) {

          for (sub=strSearch,s=string;*sub && *s && *s == *sub;sub++,s++) {;}

          if (*sub == 0) return (char *)string;

     }

     return NULL;

}

 

void main()

{

     char dest[256];

 

     my_puts("Korea");

     my_puts2("한글도 잘 된다.");

 

     my_puts(my_strcpy(dest,"my string copy function test"));

 

     printf("문자열의 길이는 %d입니다.\n",my_strlen("1234"));

     printf("문자열의 길이는 %d입니다.\n",my_strlen2("123456789"));

    

     char str[128]="abcd";

     my_puts(my_strcat(str,"efgh"));

 

     printf("o가 %s습니다.\n",my_strchr("notebook",'o')==NULL ? "없":"있");

     printf("z가 %s습니다.\n",my_strchr("notebook",'z')==NULL ? "없":"있");

     printf("under가 %s습니다.\n",my_strstr("misunderstand","under")==NULL ? "없":"있");

     printf("above가 %s습니다.\n",my_strstr("misunderstand","above")==NULL ? "없":"있");

}

 

사용자 정의 함수들은 표준 함수와 비슷한 이름을 사용하되 구분은 되어야 하므로 모두 my_로 시작하도록 했다. main에서는 이 함수들이 제대로 동작하는지를 테스트하는 간단한 코드가 작성되어 있다. 실행 결과는 다음과 같다.

 

Korea

한글도 잘 된다.

my string copy function test

문자열의 길이는 4입니다.

문자열의 길이는 9입니다.

abcdefgh

o가 있습니다.

z가 없습니다.

under가 있습니다.

above가 없습니다.

 

my_puts

표준 함수의 puts를 그대로 다시 만들어 본 것이다. 이 함수는 문자열이 저장된 시작 번지를 전달받아 NULL 문자를 만날 때까지 즉, 문자열 끝까지 모두 출력한 후 덤으로 개행 코드까지 출력하도록 되어 있다. 문자열 출력 코드는 while문 하나로 구성된다. *str이 NULL이 아닌 동안 *str을 읽어 putch로 출력하기를 반복하면서 str++로 계속 뒤로 이동하면 모든 문자들이 순서대로 출력될 것이다. putch의 인수가 *str++로 되어 있어 먼저 출력한 후 다음 번지로 이동하도록 되어 있는데 *++str이나 (*str)++ 따위로 적어서는 안된다.

*str이 NULL일 때 while 루프를 탈출하며 마지막으로 리턴하기 전에 '\n'을 출력하여 개행한다. 예제에는 동일한 동작을 하는 my_puts2 함수도 작성되어 있는데 두 함수를 비교해 보자. my_puts 함수는 인수로 전달된 str을 포인터로 인식한 것이고 my_puts2는 배열로 인식하여 첨자 연산을 통해 문자열을 출력하는데 출력 과정은 비슷한다. 첨자 i를 0에서 시작하여 계속 증가시키며 str[i]를 출력하기를 str[i]가 NULL이 아닌 동안 반복하면 결국 str 배열의 모든 문자들이 출력된다.

두 방식의 실행 결과는 완전히 동일하지만 내부적인 연산 과정은 약간 다르다. my_puts는 포인터를 증가시키면서 포인터 위치의 값을 바로 읽으므로 속도가 빠르다는 장점이 있지만 출력이 끝난 후 str인수의 위치가 변경된다는 특징이 있다. my_puts2는 별도의 지역변수가 필요하고 매 문자를 출력할 때마다 첨자 연산을 하므로 더 느리지만 시작 번지인 str을 그대로 유지한다는 점이 다르다. puts 함수의 경우는 시작 번지를 별도로 유지할 필요가 없으므로 my_puts가 훨씬 더 합리적인 선택이라고 할 수 있다. 다른 문자열 함수들은 연산 후에 시작 번지를 다시 읽어야 하는 경우가 있는데 이럴 때는 시작 번지를 직접 이동시키는 방법을 쓸 수 없다.

my_strcpy

문자열을 복사하는 my_strcpy 함수는 세 줄로 작성되어 있지만 실제로 복사를 하는 코드는 단 한 줄 뿐이다. 하나의 while 문으로 복사 동작을 기술할 수 있는데 *src의 문자를 *dest에 대입하는 과정을 *src가 NULL일 때까지 반복하는 것이다.

src위치의 문자를 dest위치에 대입한 후 두 포인터를 증가시키고 대입된 결과가 NULL인지 점검한다. ++ 연산자를 후위형으로 사용했으므로 NULL 문자까지도 복사 대상이다. 후위 증가 연산자와 대입 연산자의 리턴값까지 활용하여 한 문장으로 복사 동작을 압축적으로 표현하고 있다. 평이하게 풀어쓴다면 다음과 같이 풀어 쓸 수밖에 없을 것이다.

 

for (;;) {

     *dest=*src;

     if (*src == 0) {

          break;

     }

     dest++;

     src++;

}

 

일단 대입하고 널 문자일 때 탈출하며 그렇지 않을 경우 다음 번지로 이동한다. 복사와 이동 중간에 조건 점검을 해야 하므로 무한 루프를 구성하고 복사 후에 탈출해야 한다. 다음과 같이 do while문이나 while문으로 작성하면 NULL 종료 문자가 복사 대상에서 제외 되므로 올바른 복사가 아니다.

 

do {

     *dest=*src;

     dest++;

     src++;

} while (*src != 0);

while(*src != 0) {

     *dest=*src;

     dest++;

     src++;

};

 

++ 연산자와 대입 연산자의 리턴값을 활용하며 이런 코드를 한 줄로 표현할 수 있다. *ptr++이라는 표현식이 얼마나 함축적이고 편리한지 실감할 수 있을 것이다.

my_strcpy 함수는 문자 복사 코드 외에도 별도의 코드를 더 가지고 있는데 이 코드는 표준 strcpy 함수와 동작을 완전히 일치 시키기 위한 것이다. strcpy 함수는 복사 후 dest의 번지를 리턴하도록 되어 있는데 이대로 구현하자면dest의 원래 위치를 저장해 놓고 복사 후에 그 값을 리턴해야 한다. 이렇게 해야 복사한 결과를 puts 함수로 곧바로 출력할 수 있다. 리턴을 하지 않는다면 my_strcpy 함수는 한 줄로 작성할 수 있다.

표준 strcpy 함수는 dest의 길이에 대해서는 어떠한 가정도 하지 않으며 배열 범위를 넘어서는지 점검하지도 않는다. 그래서 복사할 버퍼의 크기를 호출원에서 알아서 충분하게 제공해야 한다. my_strcpy 함수도 마찬가지인데 코드를 보다시피 어디가 dest의 끝이라는 정보를 전혀 받아들이지 않기 때문이다. 배열 범위를 점검하지 않는 것은 C 언어의 특징이기도 하지만 더 근본적으로는 컴퓨터의 메모리에 별도의 끝 표식이 없어 어쩔 수가 없다.

my_strlen

문자열의 길이를 조사하는 my_strlen 함수는 다소 쉽다. str에서 시작해서 NULL 문자를 만날 때까지 포인터를 계속 증가시킨 후 p-str을 리턴하면 된다. str을 직접 이동해 버리면 최초 시작 위치를 알 수 없게 되므로 str은 그 자리에 유지한 채로 별도의 포인터 변수 p를 이동시켜야 한다. 포인터끼리는 뺄셈이 가능하고 그 결과는 정수형이므로 NULL 문자 위치의 p와 시작 위치 str의 뺄셈 연산 결과가 곧 문자열의 길이이다.

다른 방법으로는 포인터를 이동시키는 대신 첨자 i를 증가시켜가며 str[i]가 NULL일 때까지 반복한 후에 i를 리턴하면 된다. my_strlen2 함수가 이 방식으로 작성되었다. 최종적으로 리턴할 값은 첨자 그 자체이므로 간단하게 구할 수 있지만 NULL 문자 점검에 매번 첨자 연산을 사용하므로 속도는 훨씬 더 느리다. 문자열이 긴 경우는 이 속도 차이가 무시 못할 정도이므로 첨자를 쓰는 방법보다는 포인터를 바로 사용하는 방법이 훨씬 더 유리하다.

my_strcat

문자열을 연결하는 strcat 함수는 아주 간단하다. 연결한다는 것은 문자열 끝에 새로운 문자열을 복사하는 것과 같으므로 문제를 조금 바꿔서 생각해 보면 쉽다. dest의 제일 끝 위치로 이동한 후 이 위치에 src를 복사해 넣기만 하면 된다.

앞에서 문자열 복사 함수와 길이를 구하는 함수를 이미 만들어 두었으므로 이 함수들의 조합만으로도 연결 함수를 쉽게 구현할 수 있다. my_strcat에서는 앞에서 만든 사용자 정의 함수를 써 봤는데 표준 함수를 써서 만들어도 마찬가지다.

my_strchr

이 함수는 문자열에서 문자를 검색한다. 문자 배열을 순회하면서 해당 위치의 문자가 c이면 그 포인터를 바로 리턴하고 NULL 종료 문자를 만나면 NULL을 리턴한다. 더 짧게 줄이면 다음 두 줄로 압축할 수도 있다.

 

for (;*string!=c && *string;string++) {;}

return *string ? (char *)string:NULL;

 

이 함수가 끝날 조건은 결국 문자를 찾거나 아니면 문자열 끝까지 검색했는데 문자가 발견되지 않거나 둘 중의 하나이므로 이 두 조건이 만족하지 않을 때까지 string을 이동시키면서 루프를 돌다가 루프가 끝났을 때 *string 위치가 NULL인지 아닌지만 판단하면 된다.

my_strstr

부분 문자열을 검색하는 my_strstr 함수는 조금 복잡하다. 일련의 문자들이 연속적으로 존재하는 위치를 찾아야 하므로 전체 문자열 루프 안에서 부분 문자열 루프를 또 구성해야 한다. 전체 문자열의 각 위치에 대해 s는 현재 위치에서부터 증가 이동하며 부분 문자열 sub도 같이 이동한다. 이동중에 둘 중 하나라도 NULL이거나 아니면 두 문자열의 대응되는 문자가 다르면 루프를 종료한다.

루프 종료 직후에 *sub를 점검하여 부분 문자열의 끝까지 테스트를 무사히 통과했다면 전체 문자열의 현재 위치에서 부분 문자열이 발견된 것이다. 그렇지 않다면 전체 문자열의 다음 위치로 이동하여 계속 부분 문자열을 검색한다. 전체 문자열의 끝(*string이 NULL)까지 검색해도 부분 문자열이 발견되지 않으면 이때는 NULL을 리턴한다.

이 예제의 my_strstr은 루프가 조금 복잡해 보이기는 하지만 그래도 이해하지 못할 정도로 어렵지는 않다. 구조가 간단하기 때문에 검색 속도는 그리 좋지 않을 것이다. 실제 표준 함수 strstr은 이런 이중 루프같은 간단한 방법을 쓰지 않고 좀 더 복잡하고 성능이 좋은 알고리즘을 사용하는데 다음에 기회가 되면 알고리즘에도 관심을 가져 보기 바란다.

 

이상으로 표준 함수 몇 가지를 흉내내서 똑같이(또는 최소한 비슷하게라도) 동작하는 함수를 작성해 보았다. 여기서 실습해 보지 않은 문자열 관련 함수들도 비슷한 방법으로 대부분 직접 만들 수 있을 것이다. 표준 함수들이란 사실 만들기 어려워서 제공되는 것이라기보다는 누가 만들어도 똑같기 때문에 미리 작성되어 있는 것이라고 할 수 있다. 그러나 putch같은 함수는 간단해 보여도 직접 만들기 어려운데 왜냐하면 이 함수는 운영체제와 직접 통신하는 원자적인 함수이기 때문이다. printf 도 내부가 좀 복잡하기는 하겠지만 굳이 직접 만들어 쓸려면 못할 것도 없다. 물론 표준 함수가 있는데 직접 만들 이유는 없지만 말이다.

 

 my_strcmp

두 문자열의 상등 및 대소 관계를 비교하는 strcmp 함수와 동일하게 동작하는 my_strcmp 함수를 작성하고 테스트하라. 비교 대상 문자열의 길이가 다를 수도 있다는 점을 주의해야 한다. 또한 대소문자 구분없이 문자열을 비교하는 my_stricmp 함수도 작성해 보아라.

 my_strncpy

문자열의 일부만 복사하는 my_strncpy 함수를 작성하고 테스트하라. 또한 문자열의 일부만 연결하는 my_strncat 함수도 작성해 보아라. 표준 함수의 동작을 먼저 잘 관찰하고 가급적이면 똑같이 동작하도록 작성하는 것이 중요하다.