12-3-나.확장 함수 작성

C는 다양한 문자열 관리 함수들을 제공하기는 하지만 현실의 특수한 문제들을 모두 풀 수 있을만큼 충분하지는 않다. 사실 현실의 문제들은 너무 특수하기 때문에 해결 방법들을 미리 만들어 놓을 수가 없다. 결국 표준이 제공하지 않는 기능은 직접 만들어 쓰는 수밖에 없다. 단, 처음부터 모든 코드를 새로 만들 필요는 없고 기존의 존재하는 함수들을 최대한 활용해서 만들면 된다.

여기서는 C 표준 라이브러리에는 없지만 만들어 두면 재활용이 가능한 유용한 몇 가지 함수를 작성해 보되 소스를 바로 보지 말고 먼저 풀어보고 잘 안될 때만 예제를 보기 바란다.  먼저 문자열 끝에 문자 하나를 추가하는 함수를 작성해 보자. 문자열끼리 연결하는 strcat 함수는 존재하지만 문자열과 문자를 연결하는 함수는 따로 제공되지 않는데 이 정도는 직접 만들어도 어렵지 않다. 함수의 이름은 stradd로 붙이도록 하자.

 

: stradd

#include <Turboc.h>

#include <string.h>

 

void stradd(char *str,int c)

{

     int len=strlen(str);

     str[len]=c;

     str[len+1]=0;

}

 

void main()

{

     char str[128]="";

 

     stradd(str,'a');puts(str);

     stradd(str,'b');puts(str);

     stradd(str,'c');puts(str);

     stradd(str,'d');puts(str);

}

 

stradd는 문자열 str의 끝에 문자 c를 덧붙이는데 다른 문자열 함수들과 마찬가지로 배열 경계에 대한 점검은 할 수 없으므로 항상 충분한 길이의 버퍼를 제공해야 한다. 문자열 끝에 문자를 덧붙여야 하므로 문자열의 길이 len을 strlen 함수로 조사했다. 문자열의 길이란 문자열 선두에서부터 문자열 끝을 나타내는 NULL 문자의 상대적 위치(offset)라고 할수 있다. 이 위치에 새로 추가될 문자 c를 대입하면 문자는 일단 추가된다.

문자가 원래 NULL문자가 있던 자리에 대입되었으므로 문자열 끝을 나타내는 NULL은 바로 그 다음 위치에 새로 추가되어야 한다. 그렇지 않으면 뒤에 있던 쓰레기값이 계속 남아있게 되므로 완전한 문자열이 되지 못하며 이 문자열을 그대로 출력하면 뒤쪽의 쓰레기값까지 문자열의 일부로 간주되어 같이 출력될 것이다. 극단적으로 재수없는 경우 NULL문자가 한참 뒤의 허가되지 않은 영역에 있다면 죽을 수도 있다. 이 함수를 만드는데 있어 지역변수 하나를 줄이기 위해 다음과 같은 오류를 범해서는 안된다.

 

str[strlen(str)]=c;

str[strlen(str)+1]=0;

 

원래 len이 strlen(str)로 초기화되었으므로 이 변수가 쓰이는 곳에 바로 strlen(str)을 쓰면 될 것 같지만 이렇게 수정하면 항상 정확하게 동작한다고 보장할 수 없다. 테스트 코드의 지역변수 str을 ""로 초기화하지 말고 선언만 한 후 str[0]=NULL;로 빈 문자열을 만든 후 테스트해 보면 제대로 동작하지 않을 것이다. c를 대입하는 시점에서 str의 길이는 제대로 조사되지만 c에 의해 NULL 문자가 파괴된 직후에는 그 길이가 제대로 조사되지 않으므로 엉뚱한 메모리 영역을 덮어쓸 위험이 있다. 만약 정 len 지역변수를 없애려면 순서를 바꿔서 대입해야 한다.

 

str[strlen(str)+1]=0;

str[strlen(str)]=c;

 

이렇게 하면 일단 c가 추가될 뒤쪽을 NULL 문자로 막아 놓고 c를 대입하므로 안전상의 문제가 없다. 그러나 문자열 길이를 두 번 조사함으로써 쓸데없이 실행 시간을 낭비하고 있으므로 결코 좋은 코드라고 할 수는 없다. 문자열 길이를 조사하는 작업은 생각보다 시간이 오래 걸리므로 차라리 지역변수를 사용하여 길이를 한 번만 조사하는 것이 훨씬 이득이다.

다음은 문자열 비교 함수 strstr의 확장 함수를 작성해 보자. 대소문자 구분을 무시하고 문자열을 비교하는 stricmp 함수는 있는데 대소문자 구분없이 문자열을 검색하는 함수는 없다. 표준 strstr 함수는 정확하게 일치하는 부분 문자열만 찾으므로 strstr("KoreaSeoul","eas")는 검색하지 못한다. 현실에서는 대소 구분없이 문자열을 검색해야 하는 경우도 아주 많으므로 stristr 함수를 작성해 보았다.

 

: stristr

#include <Turboc.h>

#include <string.h>

 

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

{

     const char *s,*sub;

 

     for (;*string;string++) {

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

              if (tolower(*s) != tolower(*sub)) break;

          }

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

     }

     return NULL;

}

 

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

{

     char *scopy,*srchcopy,*p;

 

     scopy=(char *)malloc(strlen(string)+1);

     strcpy(scopy,string);

     strlwr(scopy);

     srchcopy=(char *)malloc(strlen(strSearch)+1);

     strcpy(srchcopy,strSearch);

     strlwr(srchcopy);

 

     p=strstr(scopy,srchcopy);

     free(scopy);

     free(srchcopy);

 

     if (p==NULL) {

          return NULL;

     } else {

          return (char *)string+(p-scopy);

     }

}

 

void main()

{

     if (stristr2("madeINkorea","inko")) {

          puts("찾는 문자열이 있습니다.");

     } else {

          puts("찾는 문자열이 없습니다.");

     }

}

 

예제에는 똑같은 동작을 하는 함수가 두 벌 작성되어 있다. stristr 함수는 앞 항에서 작성했던 my_strstr 함수를 약간 변형한 것인데 전체 문자열과 부분 문자열의 대응되는 문자를 비교할 때 소문자로 바꾼 후 비교하는 방식을 사용했다. 비교할 때만 대소문자를 무시하도록 했고 비교하는 절차는 동일하다.

stristr2는 표준 strstr 함수를 사용하되 비교하기 전에 두 문자열의 사본을 작성한 후 모두 소문자로 바꾼 후 검색했다. 다 소문자로 바뀐 상태에서 검색되므로 대소문자 구성이 검색 결과에 영향을 미치지 않게 된다. 부분 문자열이 검색되었다면 검색된 위치를 돌려 주어야 하는데 strstr이 찾은 위치는 사본의 위치이므로 이 위치를 오프셋으로 바꾼 후 원본 문자열의 기준 번지에 더해야 한다.

stristr2 함수는 초보자들이 흔히 쉽게 생각할 수 있는 알고리즘이고 쉬워 보이는 것도 사실이다. 또한 고도로 최적화된 표준 strstr 함수의 덕을 좀 보자는 속셈도 나름대로 효과가 있기는 하다. 하지만 사본을 작성하고 해제하는 과정을 거쳐야 하기 때문에 효율은 결코 좋다고 할 수 없다. 문제를 해결하는 두 가지 함수를 작성해 봤는데 제대로 만들려면 훨씬 더 복잡한 알고리즘이 동원되어야 한다.

다음 예제는 문자열 내에서 부분 문자열을 찾아 검색된 모든 문자열을 다른 문자열로 대체한다. 스크립트 언어나 다른 고급 언어들은 문자열 대체 함수가 기본 제공되는데 C 표준 라이브러리에는 이 함수가 빠져 있다. 문자열 대체는 무척 실용적인데 예를 들어 어떤 파일에서 특정 단어를 찾아 다른 단어로 일괄 치환할 때나 오타를 자동 수정할 때 이런 기능이 필요하다.

 

: strreplace

#include <Turboc.h>

#include <string.h>

 

void insertstr(char *str,const char *insert)

{

     int len;

 

     len=strlen(insert);

     memmove(str+len,str,strlen(str)+1);

     memcpy(str,insert,len);

}

 

void deletestr(char *str,int count)

{

     memmove(str,str+count,strlen(str+count)+1);

}

 

void strreplace(char *str,const char *a,const char *b)

{

     char *p;

 

     for (;;) {

          p=strstr(str,a);

          if (p == NULL) return;

          deletestr(p,strlen(a));

          insertstr(p,b);

     }

}

 

void main()

{

     char str[128]="welcome to korea";

     char str2[512]="내가 그린 기린 그림은 암 기린을 그린 기린 그림이고 "

          "네가 그린 기린 그림은 숫 기린을 그린 기린 그림이다.";

     char *p;

 

     puts(str);

     p=strstr(str,"korea");

     insertstr(p,"beautiful ");

     puts(str);

     p=strstr(str,"to");

     deletestr(p,3);

     puts(str);

 

     puts(str2);

     strreplace(str2,"기린","오랑우탄");

     puts(str2);

}

 

strreplace 함수는 세 가지 작업을 순서대로 실행한다. 우선 대상 문자열을 찾고 그 문자열을 지운 후 대체 문자열을 다시 삽입하는 것이다. 이 과정을 더 이상 검색되지 않을 때까지 반복하면 모든 문자열을 완전히 대체할 수 있다. 대체 작업은 문자열 a를 찾아 문자열 b로 바꾸는 것인데 a와 b의 길이가 항상 똑같다면 a만 찾아 b로 바꾸면 될 것이다. 그러나 길이가 다르다면 삭제 후에 삽입해야 한다.

strreplace의 도우미 함수 insertstr은 바로 앞절에서 논리를 소개한 바 있고 deletestr도 비슷한 논리로 이해하면 된다. 삽입할 때는 삽입되는 위치 이후를 삽입되는 문자열의 길이만큼 뒤로 이동시켜야 하고 삭제할 때는 삭제되는 길이만큼 메모리를 앞쪽으로 이동시켜야 한다. 이때 이동 대상은 NULL 종료 문자로 끝나는 문자열이 아니라 단순한 메모리 덩어리이므로 메모리 관리 함수를 사용한다.

다음은 strcat 함수와 유사한 반대 방향 문자열 연결 함수를 생각해 보자. strcat는 문자열을 뒤쪽에 덧붙이는데 앞쪽에 연결하는 strcatfront 함수를 생각해 볼 수 있다. "Kim"이라는 dest 문자열 앞에 "Mr. "이라는 문자열을 더 추가하고 싶을 때 strcatfront(dest, "Mr. ")을 호출하는 식인데 종종 이런 함수가 필요하다. 아마 이런 함수를 직접 만들라고 하면 사본을 뜬 후 strcat를 호출하는 방식을 사용하겠지만 더 간단한 방법이 있다. 이런 동작을 하는 함수는 이미 앞에서 작성한 바 있는데 바로 insertstr 함수이다. 이 함수가 똑같은 동작을 한다는 것을 알 수 있을 것이다.

 

 strrevcase

문자열 전체를 대문자로, 또는 소문자로 바꾸는 strupr, strlwr 함수는 존재하지만 반대로 뒤집는 함수는 없다. 주어진 문자열의 대소문자를 반대로 바꾸는 함수 strrevcase를 작성하라. 예를 들어 "Case" 문자열을 주면 "cASE"로 바뀌어 돌아와야 하며 영문자 이외의 문자는 건드리지 말아야 한다.

 strtrim

문자열의 앞쪽과 뒤쪽에 있는 공백은 보통 큰 의미가 없는 빈 문자인 경우가 많다. 주어진 문자열의 왼쪽 공백을 잘라내는 strltrim 함수와 오른쪽 공백을 잘라내는 strrtrim 함수를 작성하라. 잘라낼 공백에는 스페이스 문자(0x20) 뿐만 아니라 탭('\t')도 포함된다.

 hstrrev

표준 함수 중에 문자열을 반대로 뒤집는 strrev 함수가 있는데 이 함수는 바이트 단위로 문자를 직접 교체하기 때문에 2바이트 문자인 한글에 대해서는 동작하지 않는다. 한글에 대해서도 뒤집기를 할 수 있는 함수 hstrrev 함수를 작성하라. 예를 들어 "피노키오" 문자열을 주면 "오키노피"가 리턴되어야 한다. 한글 코드는 0x80 이상의 값을 가지므로 특정 문자 c가 한글인지는 ((c & 0x80) != 0)조건문으로 간단하게 조사할 수 있다.