[Hive Helsinki / Piscine] C03
Exercise 00: ft_strcmp
Reproduce the behavior of the function strcmp (man strcmp).
Allowed Function: None
C03의 경우 C02에서와 비슷하게 string에 관한 내용이 이어진다. 첫 번째 문제부터 C02와 유사하게 string에 관한 함수를 구현하는 문제가 나왔다. strcmp라는 함수는 string compare을 수행하는 함수이다. 자세한 내용은 함수의 매뉴얼을 통해 확인할 수 있는데, 다음과 같다.
위 매뉴얼과 같이 string 두 개를 인자로 받아, 두 개를 비교하고 같으면 0을 다르면 음수나 양수를 리턴한다. 음수/양수를 판단하는 기준은 string이 다른 그 지점에서 어떤 문자의 아스키코드가 더 작냐로 판단하게 된다. 다른 첫 지점에서 s1(첫 번째 string)의 아스키코드가 더 작을 경우 음수를, s2(두 번째 string)의 아스키코드가 더 작을 경우 양수를 리턴하게 된다. 매뉴얼 자체에서 양수/음수라고만 명시하고 정확한 값은 명시하지 않았기 때문에 어떤 양수/음수이던지 상관없지만, 실제 strcmp 함수와 똑같은 값을 출력하고 싶다면, 해당 위치의 s1에서 s2를 뺀 값을 리턴하면 된다.
이 문제에서 우리 클러스터 내에 꽤 논란이 된 사안이 있었는데, Description 두 번째 줄 마지막에 나온 "The comparison is done using unsigned character"이라는 부분에서 비롯된 논란이었다. 그러니까 이 언급 때문에 해당 함수가 아스키 테이블 밖의 값도 handle 가능해야 하며, 심지어는 음수의 아스키코드를 가진 값도 처리가 가능해야 한다는 것이다. (내가 하필 핀란드에서 피신을 하고 있었고, 핀란드 문자가 음수 아스키코드로 처리되더라...) 일리가 있는 말이었고, 나도 수긍했다. 하지만 나는 속도가 빠른 편이라 이 의견이 제기되었을 때는 이미 C03를 100%로 통과한 이후였고, unsigned로 처리하지 않았음에도 통과할 수 있었다. (즉, 시스템은 unsgined인지까지는 체크하지 않는다. 테스트 케이스에는 일반 아스키 테이블 내에 있는 문자들만 있는 것으로 보인다.) 한국에서도 이렇게 깐깐하게 기준을 제시할 평가자가 있을지는 모르겠지만, 적용하기 어렵지 않기도 하고, 사실 날카롭고 정확한 지적이기도 하기 때문에 왠만해서는 적용하는 편을 추천한다. (실제로 스탭들도 이렇게 기계가 채점하지 못하는 부분을 발견하는 게 동료 평가의 의의라고 하면서 굉장히 좋아했다) 나는 이 내용을 듣고 난 이후에도 이 내용 때문에 fail을 주지는 않았고, 적용 안 한 동료들에게는 이런 기준으로 평가하는 평가자가 있으니 혹시 재도전하게 된다면 수정하는 것을 추천한다 정도만 언급해 줬다.
서론이 길었는데, 아무튼 내가 통과한 코드는 다음과 같다. 앞에서 언급한 바와 같이 unsigned는 적용하지 않았다. 또한, 두 문자열이 다를 경우 s1과 s2의 차를 리턴하지 않고, 1과 -1로 처리했다. s1과 s2, 둘 모두가 존재할 때까지 문자열을 순회하면서 체크하고, 중간에 다른 문자에서 바로 1이나 -1을 리턴한다. 만약 쭉 같아서 리턴하지 않고 while문이 종료될 경우, 둘 중 더 긴 문자열이 있는지 체크하고, 있다면 더 긴 문자열 쪽이 큰 것으로 판단한다. (더 짧은 문자열은 null character라서 다음이 0일 테니, 더 긴 문자열 쪽 아스키코드가 당연히 더 클 것이다.) 이 모든 과정을 거쳐도 리턴되지 않은 경우는 문자열이 완전히 같다는 뜻이므로, 0을 리턴한다.
int ft_strcmp(char *s1, char *s2)
{
while (*s1 && *s2)
{
if (*s1 > *s2)
return (1);
if (*s1 < *s2)
return (-1);
s1++;
s2++;
}
if (*s1 != 0)
return (1);
if (*s2 != 0)
return (-1);
return (0);
}
하지만, 평가하러 다니면서 더 간단한 방법을 발견하기도 했고, unsigned도 적용하고 싶어서 따로 코드를 다시 적어보았다. 아래 코드는 피신에서 통과한 코드가 아니고, 또 피신의 norm에도 맞지 않는다. 오류가 있을 수 있으니 참고만 하길 바란다.
int ft_strcmp(char *s1, char *s2)
{
unsigned char *str1 = (unsigned char *) s1;
unsigned char *str2 = (unsigned char *) s2;
while (*str1 && *str1 == *str2)
{
str1++;
str2++;
}
return (*str1 - *str2);
}
Exercise 01: ft_strncmp
Reproduce the behavior of the function strncmp (man strncmp).
Allowed Function: None
해당 문제는 Exercise0과 완전히 같지만, n까지만 비교한다는 점만 다르다. 따라서 s1과 s2 문자열이 둘 다 존재할 때까지 서로 같은지 확인해주고, 만약 s1과 s2 둘 다 존재할 때까지 비교했는데 n에 도달하지 못한 경우 남은 문자가 있는 쪽이 더 크다고 판단해 주면 된다. 위 과정을 모두 거쳐도 다른 문자가 나오지 않은 경우는 0을 리턴해주면 된다.
int ft_strncmp(char *s1, char *s2, unsigned int n)
{
unsigned int i;
i = 0;
while (s1[i] && s2[i] && (i < n))
{
if (s1[i] > s2[i])
return (1);
if (s1[i] < s2[i])
return (-1);
i++;
}
if (i < n)
{
if (s1[i])
return (1);
if (s2[i])
return (-1);
}
return (0);
}
하지만 해당 문제도 Exercise0처럼 보다 간단하고 정확하게 처리하고 싶어서 코드를 다시 작성했다. 앞서 언급한대로 해당 코드는 시스템에서 통과한 코드가 아니라, 피신이 끝난 이후 따로 작성해 본 코드이므로 오류가 있을 수 있다.
int ft_strncmp(char *s1, char *s2, unsigned int n)
{
unsigned int i;
unsigned char *str1;
unsigned char *str2;
str1 = (unsigned char *) s1;
str2 = (unsigned char *) s2;
i = 0;
while (str1[i] && (str1[i] == str2[i]) && (i < n))
i++;
if (i < n)
return (str1[i] - str2[i]);
return (0);
}
Exercise 02: ft_strcat
Reproduce the behavior of the function strcat (man strcat).
Allowed Function: None
해당 문제는 strcat 함수를 reproduce하는 문제이다. strcat은 source string을 destination에 append하는 함수이다. strcat의 매뉴얼을 확인해 보면 다음과 같다.
메뉴얼을 보면 strcat 함수는 dest 문자열의 null character부터 시작하여 src 문자열을 dest에 붙여 넣는 함수이다. dest 배열의 크기가 충분하지 않으면 프로그램의 행동이 예측 불가능하다고 언급한 것으로 봐서, dest 배열 크기에 따른 오류는 따로 처리하지 않아도 되는 것으로 보인다. terminating null byte이므로, 마지막에 null character까지 추가해줘야 한다는 것을 알 수 있다. 아랫부분은 뒤에 나올 strncat 함수에 대한 설명이며, 사진에는 나와있지 않지만 dest가 다시 리턴되어야 한다.
따라서 아래 코드에서 변수 i로 dest 문자열의 끝을 찾고, index i부터 src 문자열을 하나씩 붙여넣은 후, 마지막에 null character를 추가해서 마무리한다.
char *ft_strcat(char *dest, char *src)
{
int i;
int j;
i = 0;
while (dest[i])
i++;
j = 0;
while (src[j])
{
dest[i] = src[j];
i++;
j++;
}
dest[i] = 0;
return (dest);
}
Exercise 03: ft_strncat
Reproduce the behavior of the function strncat (man strncat).
Allowed Function: None
해당 문제는 strncat 함수를 reproduce하는 문제인데, strcat과 완전히 동일하지만 src 문자열에서 nb개까지만 append해야 한다. 따라서 코드의 다른 부분은 모두 같지만 while문의 조건에 j가 주어진 nb보다 작을 때까지만 붙여 넣는 조건을 하나 추가했다.
char *ft_strncat(char *dest, char *src, unsigned int nb)
{
unsigned int i;
unsigned int j;
i = 0;
while (dest[i])
i++;
j = 0;
while (src[j] && (j < nb))
{
dest[i] = src[j];
i++;
j++;
}
dest[i] = 0;
return (dest);
}
Exercise 04: ft_strstr
Reproduce the behavior of the function strstr (man strstr).
Allowed Function: None
해당 문제는 strstr 함수를 reproduce하는 문제이다. strstr 함수는 아래 매뉴얼에서 확인할 수 있듯, haystack 문자열에서 needle 문자열을 찾아서 없으면 NULL을 리턴하고, 있으면 해당 문자열이 처음 시작하는 부분의 주소값을 리턴한다. 예를 들어, haystack 문자열이 "abcabdabd"이고 needle 문자열이 "abd"라면 index 3의 주소값을 리턴하게 된다. 리턴값을 출력해 보면 "abdabd"로 나오는 것을 확인할 수 있을 것이다. (즉 printf("%s", strstr( "abcabdabd", "abd"));를 하면, 터미널에 abdabd가 출력된다.)
코드는 아래와 같이 짰다. 위 매뉴얼에서는 인자들을 haystack과 needle로 표현했지만, 문제의 프로토타입에서는 str과 to_find로 표현해서 나도 그대로 사용했다. 만약 to_find가 빈 문자열이라면 str을 그대로 리턴한다. (매뉴얼에 따름)
str을 순회하면서 만약 현재의 문자가 to_find의 첫번째 문자랑 같다면 str과 to_find를 변수 i를 이용해 같이 순회하면서 to_find의 끝까지 같은지를 확인한다. 만약 끝까지 같다면, 두 문자열이 동이하게 진행되기 시작한 위치인 str을 리턴하고 아니면 다시 str + 1의 위치로 가서 탐색을 시작한다.
왜 if문 안에서 굳이 변수 i를 써서 순회하는지에 대해서 의문을 제기한 평가자가 있었는데, 세 가지 이유가 있다. 첫 번째는 만약 이번 시도에서 같지 않다면, 다음번에 또다시 첫 글자가 같을 때 to_find 문자열을 처음부터 순회해야 하기 때문이다. 두 번째는 리턴값이 같기 시작한 위치여야 하기 때문이다. 세 번째는 바깥 while문의 다음 순회가 같기 시작한 위치의 바로 다음 위치가 되어야 하기 때문이다. 다시 "abcabdabd"를 str, "abd"를 to_find로 예를 들어보자. str의 첫 문자부터 to_find의 첫문자와 같으므로 바로 if문으로 진입하게 될 것이다. 하지만 순회하는 과정에서 to_find와 완전히 동일하지 않다는 것을 알게 될 것이다. 그것을 알게 되는 위치는 문자 'c'가 있는 인덱스 2이다. 하지만 그렇다고 해서 c나 그다음 a부터 시작해서는 안되고, if문에 진입했던 a의 다음 문자인 b부터 다시 시작해야 한다. (이렇게 하지 않으면 놓치는 경우가 생길 수 있다.) 그렇게 순회를 하다가, 다음 a(인덱스 3)을 만나서 다시 if문에 진입했다고 해보자. 이번에는 to_find의 끝까지 같다는 것을 확인했다. 이 사실이 확인되는 시점은 d에서, 즉 인덱스 5에서이다. 하지만 우리는 시작점인 인덱스 3의 주소값을 리턴해야 한다. 이러한 이유들 때문에 변수 i를 따로 정의하여 사용하는 것이다.
char *ft_strstr(char *str, char *to_find)
{
int i;
if (!(*to_find))
return (str);
while (*str)
{
if (*str == *to_find)
{
i = 0;
while (to_find[i] && (str[i] == to_find[i]))
i++;
if (!(to_find[i]))
return (str);
}
str++;
}
return (0);
}
Exercise 05: ft_strlcat
Reproduce the behavior of the function strlcat (man strlcat).
Allowed Function: None
해당 문제는 strlcat 함수를 reproduce하는 문제이다. 앞서 만들었던 strcat, strncat과 유사하지만 좀 더 복잡한 조건을 요구한다. 특히 return value가 헷갈렸는데, 그냥 매뉴얼 및 검색결과가 시키는 대로 했지만 아직도 리턴값이 왜 저렇게 되는지 잘 모르겠다. strlcat 역시 붙여 넣는 숫자에 제한이 있다는 점에서 strncat과 유사한데, 다른 점은 strncat에서 n이 source 문자열에서 몇 개를 가져올지에 관한 숫자였다면, strlcat의 l은 붙여 넣고 난 후 destination의 총길이를 나타낸다는 것이다. 이전에 포스팅한 C02 내에 strncpy와 strlcpy의 차이에 대해서 읽어봤다면 조금 더 쉽게 이해할 수 있을 것이다. 즉 strncat을 실행하고 난 후의 destination 문자열의 길이는 (원래 destination 문자열의 길이 + n + 1)이 될 것이다. 하지만 strlcat을 실행하고 난 후의 destination 문자열의 길이는 l이 된다. 즉 (l - 원래 destination 문자열의 길이 - 1)만큼만 source 문자열에서 가져와서 붙여 넣는 것이다.
코드를 보면 나는 check_size()라는 helper function을 만들어서, dest 문자열과 src 문자열의 크기를 파악하는데 사용했다. 그리고 이것을 주어진 l에 해당하는 인자 size와 비교했을 때, size가 0이거나 dest 문자열의 크기가 이미 size 값과 같거나 그보다 큰 경우 우리는 append를 수행할 수 없으므로 함수를 종료시킨다. 그리고 while문을 넣어서 dest의 끝부터 시작해서 총길이가 size-1이 될 때까지 append를 수행해 준다. 이때 size에 -1을 해주는 이유는 null character를 위한 자리를 남겨둬야 하기 때문이다. 마지막으로 null character까지 추가하면 함수가 마무리된다.
unsigned int check_size(char *str)
{
unsigned int size;
size = 0;
while (str[size])
size++;
return (size);
}
unsigned int ft_strlcat(char *dest, char *src, unsigned int size)
{
unsigned int dsize;
unsigned int ssize;
unsigned int i;
dsize = check_size(dest);
ssize = check_size(src);
if ((size == 0) || (dsize >= size))
return (size + ssize);
i = dsize;
while (*src && (i < (size - 1)))
{
dest[i] = *src;
src++;
i++;
}
dest[i] = 0;
return (dsize + ssize);
}