strlen 구현해 보기
strlen 함수
strlen은 C 언어에서 문자열의 길이를 구하는 함수다.
예를 들어 "Hello World"는 공백을 포함해 11 글자다.
#include <stdio.h>
#include <string.h>
int main(void)
{
const char *s = "Hello World";
printf("%zu\n", strlen(s)); // 11
return 0;
}
자세한 내용은 cppreference의 strlen 문서에서 확인할 수 있다.
풀이
문자열은 문자의 배열이고 문자가 연속해서 나열된 것이다.
C 언어에서 문자열의 끝에는 널 문자('\0')가 붙는다.
그래서 문자열의 처음부터 널 문자를 만나기 전까지 문자 수를 세면 문자열의 길이를 알 수 있다.
마지막 널 문자는 길이에 포함되지 않도록 주의한다.
이 과정을 여러 방법으로 구현할 수 있어, 각 방식의 차이를 비교해 보는 재미가 있다.
while 문과 배열 인덱스 방식
#include <stddef.h>
size_t my_strlen(const char *s)
{
size_t length = 0;
while (s[length] != '\0')
{
length++;
}
return length;
}
length는 지금까지 센 문자의 개수이면서, 다음에 확인할 배열 인덱스이기도 하다.
처음에는 아무 문자도 세지 않았으므로 length는 0이다.
s[0]이 널 문자가 아니면 조건식이 참이 되고, 문자가 하나 있다는 뜻이므로 length를 1 증가시킨다.
이 과정을 반복하다 s[length]가 '\0'이 되는 순간 조건식이 거짓이 되어 반복문이 종료된다.
예를 들어 문자열이 "abc"라면 다음 순서로 진행된다.
s[0]은'a'이므로 조건식은 참이고length는 0에서 1이 된다.s[1]은'b'이므로 조건식은 참이고length는 1에서 2가 된다.s[2]는'c'이므로 조건식은 참이고length는 2에서 3이 된다.s[3]은'\0'이므로 조건식은 거짓이고 반복문이 종료된다.
반복문이 끝났을 때 length는 3이다.
따라서 "abc"의 길이는 3이 된다.
for 문과 배열 인덱스 방식
같은 논리를 for 문으로도 표현할 수 있다.
#include <stddef.h>
size_t my_strlen(const char *s)
{
size_t length;
for (length = 0; s[length] != '\0'; length++)
{
}
return length;
}
포인터 이동 카운팅 방식
#include <stddef.h>
size_t my_strlen(const char *s)
{
size_t length = 0;
while (*s++ != '\0')
{
length++;
}
return length;
}
포인터에 증감 연산을 하면 주소 값은 포인터가 가리키는 자료형의 크기만큼 이동한다.
후위 증가 연산자 ++가 역참조 연산자 *보다 우선순위가 높기 때문에 *s++는 *(s++)로 해석된다.
s는 다음 주소를 가리키게 되지만, 조건식 평가에는 증가 전 주소의 문자가 사용된다.
두 포인터 뺄셈을 이용한 방식
#include <stddef.h>
size_t my_strlen(const char *s)
{
const char *p = s;
while (*p != '\0')
{
p++;
}
return (size_t)(p - s);
}
s는 문자열의 첫 문자의 주소를 가리킨다.
p에는 그 시작 주소를 대입하고, 반복문을 통해 p가 널 문자의 주소를 가리킬 때까지 반복한다.
여기서 p - s의 연산 결과를 주소끼리 뺄셈하니 두 주소 값의 차이만큼 나올 것이라고 생각할 수 있지만, 그렇지 않다.
포인터 뺄셈은 두 주소 값의 차이를 구하는 연산이 아니라, 가리키는 타입을 기준으로 두 포인터 사이의 원소 개수를 구하는 연산이다.
단, 포인터 뺄셈은 두 포인터가 같은 배열 안의 같은 타입 원소를 가리킨다는 전제에서 성립한다.
원소 크기가 1바이트보다 큰 타입으로 바꿔 보면 그 차이가 확연히 드러난다.
int arr[5];
int *p1 = &arr[0];
int *p2 = &arr[3];
ptrdiff_t diff = p2 - p1; // 결과: 3
p1과 p2의 실제 주소 값 차이는 sizeof(int) * 3, 보통 12바이트다.
그러나 p2 - p1의 결과는 12가 아니라 3이다.
다시 my_strlen을 보면, p - s는 s와 p가 가리키는 두 주소 값의 차이가 아니라 두 포인터 사이의 char 원소 개수를 구하는 것이다.
문자열이 "abc"라면 메모리에는 'a', 'b', 'c', '\0'이 차례로 놓인다.
반복문이 끝난 시점에 p에는 네 번째 원소인 '\0'의 주소가, s에는 첫 번째 원소인 'a'의 주소가 들어 있다.
두 포인터 사이에는 'a', 'b', 'c' 세 개의 char 원소가 있으므로 p - s의 결과는 3이 된다.
재귀 함수
실용적인 구현은 아니지만, 문자열을 한 글자씩 줄여 가며 길이를 구하는 방식도 생각해 볼 수 있다.
#include <stddef.h>
size_t my_strlen(const char *s)
{
if (*s == '\0')
{
return 0;
}
return 1 + my_strlen(s + 1);
}
현재 문자가 널 문자가 아니면, 다음 문자를 가리키도록 인자를 바꿔 새로운 함수 호출을 이어간다.
새 함수 호출에서도 널 문자가 아니라면 다시 인자를 바꿔 또 다음 호출로 넘어간다.
이 과정이 널 문자를 만날 때까지 반복되며 스택 프레임이 쌓인다.
입력값 "abc"에 대해 다음처럼 생각할 수 있다.
strlen("abc") = 1 + strlen("bc")
strlen("bc") = 1 + strlen("c")
strlen("c") = 1 + strlen("")
strlen("") = 0
스택 프레임을 계속 쌓다가 널 문자를 만나면 마지막 호출에서 0을 반환한다.
그다음 함수들이 하나씩 종료되며 각 함수에서 1씩 더해 최종 길이 3이 완성된다.