get_next_line 구성

get_next_line 과제에서는 총 세가지 파일을 제출해야한다. get_next_line.c, get_next_line_utils.c, get_next_line.h.
libft를 사용할 수 없는 대신 _utils.c가 허용되어 총 10개의 함수를 사용할 수 있다.
연결 리스트를 이용하여 기능을 구현하였으며, 사용한 함수들은 다음과 같다.

char	*get_next_line(int fd)
t_list	*add_fd_remain(t_list *remain, int fd)
static char	*check_free(t_list *rm, t_list **head)
static char	*check_remain(int fd, t_list *remain)
static char	*make_next_line(char *remain)
static char	*update_remain(char *remain)
size_t	ft_strlen(const char *str);
char	*ft_strchr(const char *str, int c);
char	*ft_strdup(const char *src);
char	*ft_strjoin(char const *str1, char const *str2);

코드 분석

리스트의 노드로 사용될 구조체의 구성 요소는 다음과 같다.

typedef struct s_list
{
	char			*str;
	int				fd;
	int				flag;
	struct s_list	*next;
}	t_list;
  • 파일에서 읽어들일 문자열
  • fd
  • EOF에 도달했는지 여부를 판단할 flag
  • 다음 리스트로 연결할 구조체 포인터 next

get_next_line 함수

char	*get_next_line(int fd)
{
	static t_list	*head;
	t_list			*remain;
	char			*next_line;

	if (fd < 0 || BUFFER_SIZE <= 0)
		return (NULL);
	if (!head)
	{	
		head = (t_list *)malloc(sizeof(t_list));
		head->next = NULL;
		head->fd = -1;
	}
	remain = add_fd_remain(head, fd);
	if (!check_remain(remain, fd))
		return (NULL);
	else
		next_line = make_next_line(ft_strdup(remain->str));
	remain->str = update_remain(remain->str);
	if (!remain->str)
		check_free(head, &head);
	return (next_line);
}

문자열을 정적 변수를 선언했던 것과 달리, 이번엔 리스트의 head 노드를 정적 변수로 선언해주었다.
따라서 get_next_line 함수가 실행될 때마다 head 노드를 참조하여 원하는 기능을 수행하게 된다.
get_next_line함수의 작동과정은 다음과 같다.

  1. 인자로 받아온 fd값과, 컴파일시 -D 옵션으로 지정한 변수 BUFFER_SIZE의 값을 체크한다. fd < 0이거나 BUFFER_SIZE <= 0 이라면 정상적인 입력이 아니므로 NULL을 리턴한다.
  2. head노드가 NULL이라면 새롭게 할당한다. 이 때 head노드의 fd값은 head라는 것을 지정해주기 위해 -1로 설정한다.
  3. add_fd_remain 함수를 통해 fd값에 해당하는 remain 노드를 결정하고, remain 노드에 담길 변수들의 값을 설정한다.
  4. check_remain 함수를 통해 파일에서 데이터를 읽어 remain->str에 저장한다. 만약 저장할 데이터가 없다면 NULL을 반환한다.
  5. remain->str이 있다면 ft_strdup함수로 메모리를 새롭게 할당 후 remain->str을 복사하고, make_next_line함수를 통해 복사된 문자열에서 첫 \n까지의 한 줄을 잘라내어 next_line에 저장한다.
  6. next_lineNULL이 아니라면 ft_strdup 함수를 이용해 새롭게 메모리를 할당한다. 이는 추후 remain의 메모리가 해제될 경우를 대비하기 위함이다.
  7. update_remain 함수를 통해 remain->str에서 ` next_line`으로 저장할 한 줄을 빼고 나머지 문자열을 다시 저장한다.
  8. 갱신 후 remain->str이 비어있다면 check_free함수를 통해 필요없어진 메모리들을 해제하는 작업을 수행한다.
  9. 만들어놓았던 next_line을 반환한다.

add_fd_remain 함수

t_list	*add_fd_remain(t_list *remain, int fd)
{
	t_list	*new;

	new = (t_list *)malloc(sizeof(t_list));
	new->str = NULL;
	new->fd = fd;
	new->next = NULL;
	new->flag = 1;
	while (remain)
	{
		if (remain->fd == fd)
		{
			free(new);
			break ;
		}
		if (!remain->next)
		{
			remain->next = new;
			return (new);
		}
		remain = remain->next;
	}
	return (remain);
}

fd값에 대응하는 노드를 만드는 함수다.
add_fd_remain 함수의 작동과정은 다음과 같다.

  1. 새로운 new노드를 할당하고, new->fd값을 인자로 받은 fd값으로 저장한다.
  2. 인자로 받아온 remainhead노드부터 시작되는 리스트이다. 노드를 이동시켜가면서 반복문을 돌린다.
  3. remain->fd가 인자로 받은 fd값과 일치한다면 이미 fd값에 해당하는 노드가 존재하는 것이므로 할당했던 new노드를 해제하고 반복문을 빠져나온 후 해당하는 노드의 위치를 반환한다.
  4. fd값을 가지고있는 노드를 찾지 못하고 리스트의 끝까지 돈다면 새로운 노드가 필요한 것이므로, 기존에 존재하던 리스트 맨 뒤에 new노드를 붙여주고 반환한다.

따라서 add_fd_remain으로 반환된 노드는 get_next_line 함수의 인자로 주어진 fd값을 가진 노드가 되어 여러 fd값이 주어지더라도 구분할 수 있게 된다.


check_remain 함수

static char	*check_remain(t_list *remain, int fd)
{
	char	*buff;
	char	*temp;
	int		read_idx;

	buff = (char *)malloc((BUFFER_SIZE + 1) * sizeof(char));
	while (!remain->str || !ft_strchr(remain->str, '\n'))
	{
		read_idx = read(fd, buff, BUFFER_SIZE);
		if (read_idx <= 0)
		{
			remain->flag = -1;
			break ;
		}
		buff[read_idx] = '\0';
		if (!remain->str)
			remain->str = ft_strdup(buff);
		else
		{	
			temp = ft_strjoin(remain->str, buff);
			free(remain->str);
			remain->str = temp;
		}
	}
	free(buff);
	return (remain->str);
}

fd에서 데이터를 읽어들여 노드의 str 변수에 저장하는 함수다.
check_remain 함수의 작동과정은 다음과 같다.

  1. 변수 buffBUFFER_SIZE + 1(\0문자가 들어갈 공간)만큼 할당한다.
  2. remain->str이 없거나, remain->str에 남아있는 문자열에 \n이 없을 경우 반복문으로 들어간다.
  3. read함수로 fd에서 BUFFER_SIZE바이트만큼 데이터를 읽어와 buff에 저장한다.
    이 때 read함수의 반환값은 정상적으로 읽어들인 바이트의 크기가 되므로 이를 read_idx 변수에 저장해 0보다 작으면 fd에 해당하는 파일을 끝까지 읽은것이다.
    따라서 remain->flag값을 -1로 바꿈으로써 이를 반영한 후 반복문을 빠져나온다.
  4. 그렇지 않으면 읽어낸 데이터를 다루기 위해 buff[read_idx], 즉 buff의 마지막에 \0을 넣어준다.
  5. 만약 remain->str이 빈 상태였다면 ft_strdup함수를 이용해 remain에 새롭게 메모리를 할당하며 buff를 복사해준다.
  6. remain->str에 남아있는 데이터가 있었다면 ft_strjoin함수를 이용해 remain->str뒤에 buff를 붙인 새로운 문자열을 할당한다.
    • 이 때 ft_strjoin 함수로 반환되는 문자열은 기존의 remain->str과 다른 메모리를 가지고 있으므로, 메모리 누수를 피하기 위해서 기존 remain->str의 메모리를 해제한 후 새롭게 만든 메모리로 연결시켜주어야 한다.
  7. remain->str에서 \n을 찾을 수 있을 때까지 위 과정을 반복한다. 물론 중간의 탈출조건으로 반복문을 빠져나올 수도 있다.
  8. 역할이 끝난 buff의 메모리를 해제한 후, remain->str을 반환한다.

이 결과 만들어지는 remain->str은 셋 중 하나이다.

  • \n이 반드시 하나 이상 포함되어있는 문자열
  • \n이 없고, EOF가 포함되어있는 문자열
  • NULL(처음부터 remain->str이 비어있었고, read함수로 읽어올 데이터도 남아있지 않았을 경우)

배열로 구성했을 때와 다른점은 파일을 다 읽은 경우 remain->flag값을 설정해주었다는 것 뿐이다.


make_next_line 함수

static char	*make_next_line(char *str)
{
	char	*next_line;
	int		len;

	if (!str)
		return (NULL);
	len = 0;
	while (*(str + len))
	{
		if (*(str + len++) == '\n')
			break ;
	}
	next_line = (char *)malloc((len + 1) * sizeof(char));
	*(next_line + len) = '\0';
	while (--len >= 0)
		*(next_line + len) = *(str + len);
	free(str);
	return(next_line);
}

make_next_line 함수의 작동과정은 다음과 같다.

  1. str을 읽으면서 next_line에 할당할 길이를 잰다. \n가 있다면 거기서 멈추고, 없다면 str의 끝까지 잰다. \n가 없는 경우는 파일을 끝까지 읽은 것이다.
  2. 잰 길이로 next_line에 메모리를 할당한다.
  3. next_linestr의 문자열을 길이만큼 복사하고 마지막에 \0을 넣는다.
  4. 기존 str의 메모리를 해제하고, 만들어진 next_line을 반환한다.

배열로 구성했을 때와 기능상 달라진점은 전혀 없다.
다만 함수 갯수의 제한때문에 ft_strlcpy함수를 사용하지 못하게 되어 make_next_line 함수 안에 직접 집어넣었다.


update_remain 함수

static char	*update_remain(char *remain_str)
{
	char	*new;
	char	*fix_remain_str;
	int		idx;
	int		len;

	idx = -1;
	fix_remain_str = ft_strchr(remain_str, '\n');
	if (!fix_remain_str || !(ft_strlen(fix_remain_str) - 1))
	{
		free(remain_str);
		return (NULL);
	}
	len = ft_strlen(++fix_remain_str);
	new = (char *)malloc((len + 1) * sizeof(char));
	while (len > ++idx && *(fix_remain_str + idx))
		*(new + idx) = *(fix_remain_str + idx);
	*(new + idx) = 0;
	free(remain_str);
	return (new);
}

remian->str에서 next_line으로 출력할 문자열을 빼는 함수다.
update_remain 함수의 작동과정은 다음과 같다.

  1. ft_strchr 함수를 이용해 remain->str에서 \n의 위치를 찾아 fix_remain_str 변수에 저장한다.
  2. 찾을 수 없다면(fix_remain == NULL) 파일을 끝까지 읽은 경우이다. 또는, fix_remain_str의 길이가 1인 경우 \n뒤에 문자열이 없는 경우이다. 두 경우 모두 더이상 remain->str을 남길 이유가 없으니 메모리를 해제한 후 NULL을 반환한다.
    • 배열으로 함수를 짰을 때와는 달리 두번째 조건이 추가된 이유는, 함수 갯수의 제한으로 인해 ft_strlcpy 함수를 사용하지 않기 때문이다.
  3. 찾은 경우엔 남은 부분을 저장할 메모리를 변수 new에 새롭게 할당한다. 이 때 fix_remain_str을 한칸 뒤로 옮겨 \n 다음의 문자열부터 시작되도록 하고, 그 길이를 변수 len에 저장한 후 이를 할당할 메모리 크기로 사용한다.
  4. newfix_remain_str의 문자열을 복사하고, 마지막에는 \0을 넣어준다. ft_strlcpy 함수를 사용한 것과 같다.
  5. 기존의 remain->str을 해제하고, new를 반환한다.

check_free 함수

static char	*check_free(t_list *rm, t_list **head)
{
	t_list	*temp;

	while (rm)
	{
		if (!rm->next)
			break ;
		if (!(rm->next)->str && (rm->next)->flag == -1 && !(rm->next)->next)
		{
			free(rm->next);
			rm->next = NULL;
		}
		else if (!(rm->next)->str && (rm->next)->flag == -1 && (rm->next)->next)
		{
			temp = rm->next;
			rm->next = (rm->next)->next;
			free(temp);
		}
		rm = rm->next;
	}
	if (!(*head)->next)
	{
		free(*head);
		*head = NULL;
	}
	return (NULL);
}

메모리 누수를 막기 위해 필요없어진 메모리들을 해제시켜주는 함수이다.
check_free 함수의 작동과정은 다음과 같다.

  1. head노드부터 시작해 노드가 있다면 노드를 넘겨가면서 반복문을 돈다.
  2. 만약 다음 노드가 없다면 반복문을 빠져나온다.
  3. 다음 노드가 존재하지만 문자열이 없고, flag값이 EOF에 도달했다는 의미인 -1인 경우 두가지 경우로 나뉜다.
    • 다다음 노드가 없는 경우 다음 노드가 마지막 노드이므로 다음 노드를 해제한 후 현재 노드의 nextNULL로 지정한다.
    • 다다음 노드가 존재하는 경우 다음 노드는 중간에 낀 노드이므로, 현재 노드와 다다음 노드를 연결시킨 후 다음 노드를 해제한다.
  4. 반복문에서 빠져나온 후, 만약 head노드의 다음 노드가 없다면 모든 fd에 해당하는 노드들이 해제된 것이므로 추가로 get_next_line 함수가 호출되지 않는 한 head노드 또한 유지될 필요가 없다. 따라서 head노드의 메모리를 해제한 후 NULL을 반환한다.
    • 이 때 head노드 포인터에 NULL을 넣어주지 않으면 다음번 get_next_line이 호출되었을 시 head노드 포인터는 여전히 해제된 메모리 주소를 가리키고있어 문제가 발생한다.

flag값을 사용하지 않고, check_remain함수에서 사용한 read_idx을 인자로 주고받으며 사용하는 방법도 가능할 것 같다.