[gnl] get_next_line - 연결 리스트
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
함수의 작동과정은 다음과 같다.
- 인자로 받아온
fd
값과, 컴파일시-D
옵션으로 지정한 변수BUFFER_SIZE
의 값을 체크한다.fd < 0
이거나BUFFER_SIZE <= 0
이라면 정상적인 입력이 아니므로NULL
을 리턴한다. head
노드가NULL
이라면 새롭게 할당한다. 이 때head
노드의fd
값은head
라는 것을 지정해주기 위해 -1로 설정한다.add_fd_remain
함수를 통해fd
값에 해당하는remain
노드를 결정하고,remain
노드에 담길 변수들의 값을 설정한다.check_remain
함수를 통해 파일에서 데이터를 읽어remain->str
에 저장한다. 만약 저장할 데이터가 없다면NULL
을 반환한다.remain->str
이 있다면ft_strdup
함수로 메모리를 새롭게 할당 후remain->str
을 복사하고,make_next_line
함수를 통해 복사된 문자열에서 첫\n
까지의 한 줄을 잘라내어next_line
에 저장한다.next_line
이NULL
이 아니라면ft_strdup
함수를 이용해 새롭게 메모리를 할당한다. 이는 추후remain
의 메모리가 해제될 경우를 대비하기 위함이다.update_remain
함수를 통해remain->str
에서 ` next_line`으로 저장할 한 줄을 빼고 나머지 문자열을 다시 저장한다.- 갱신 후
remain->str
이 비어있다면check_free
함수를 통해 필요없어진 메모리들을 해제하는 작업을 수행한다. - 만들어놓았던
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
함수의 작동과정은 다음과 같다.
- 새로운
new
노드를 할당하고,new->fd
값을 인자로 받은fd
값으로 저장한다. - 인자로 받아온
remain
은head
노드부터 시작되는 리스트이다. 노드를 이동시켜가면서 반복문을 돌린다. remain->fd
가 인자로 받은fd
값과 일치한다면 이미fd
값에 해당하는 노드가 존재하는 것이므로 할당했던new
노드를 해제하고 반복문을 빠져나온 후 해당하는 노드의 위치를 반환한다.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
함수의 작동과정은 다음과 같다.
- 변수
buff
를BUFFER_SIZE
+ 1(\0
문자가 들어갈 공간)만큼 할당한다. remain->str
이 없거나,remain->str
에 남아있는 문자열에\n
이 없을 경우 반복문으로 들어간다.read
함수로fd
에서BUFFER_SIZE
바이트만큼 데이터를 읽어와buff
에 저장한다.
이 때read
함수의 반환값은 정상적으로 읽어들인 바이트의 크기가 되므로 이를read_idx
변수에 저장해 0보다 작으면fd
에 해당하는 파일을 끝까지 읽은것이다.
따라서remain->flag
값을 -1로 바꿈으로써 이를 반영한 후 반복문을 빠져나온다.- 그렇지 않으면 읽어낸 데이터를 다루기 위해
buff[read_idx]
, 즉buff
의 마지막에\0
을 넣어준다. - 만약
remain->str
이 빈 상태였다면ft_strdup
함수를 이용해remain
에 새롭게 메모리를 할당하며buff
를 복사해준다. remain->str
에 남아있는 데이터가 있었다면ft_strjoin
함수를 이용해remain->str
뒤에buff
를 붙인 새로운 문자열을 할당한다.- 이 때
ft_strjoin
함수로 반환되는 문자열은 기존의remain->str
과 다른 메모리를 가지고 있으므로, 메모리 누수를 피하기 위해서 기존remain->str
의 메모리를 해제한 후 새롭게 만든 메모리로 연결시켜주어야 한다.
- 이 때
remain->str
에서\n
을 찾을 수 있을 때까지 위 과정을 반복한다. 물론 중간의 탈출조건으로 반복문을 빠져나올 수도 있다.- 역할이 끝난
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
함수의 작동과정은 다음과 같다.
str
을 읽으면서next_line
에 할당할 길이를 잰다.\n
가 있다면 거기서 멈추고, 없다면str
의 끝까지 잰다.\n
가 없는 경우는 파일을 끝까지 읽은 것이다.- 잰 길이로
next_line
에 메모리를 할당한다. next_line
에str
의 문자열을 길이만큼 복사하고 마지막에\0
을 넣는다.- 기존
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
함수의 작동과정은 다음과 같다.
ft_strchr
함수를 이용해remain->str
에서\n
의 위치를 찾아fix_remain_str
변수에 저장한다.- 찾을 수 없다면(fix_remain ==
NULL
) 파일을 끝까지 읽은 경우이다. 또는,fix_remain_str
의 길이가 1인 경우\n
뒤에 문자열이 없는 경우이다. 두 경우 모두 더이상remain->str
을 남길 이유가 없으니 메모리를 해제한 후NULL
을 반환한다.- 배열으로 함수를 짰을 때와는 달리 두번째 조건이 추가된 이유는, 함수 갯수의 제한으로 인해
ft_strlcpy
함수를 사용하지 않기 때문이다.
- 배열으로 함수를 짰을 때와는 달리 두번째 조건이 추가된 이유는, 함수 갯수의 제한으로 인해
- 찾은 경우엔 남은 부분을 저장할 메모리를 변수
new
에 새롭게 할당한다. 이 때fix_remain_str
을 한칸 뒤로 옮겨\n
다음의 문자열부터 시작되도록 하고, 그 길이를 변수len
에 저장한 후 이를 할당할 메모리 크기로 사용한다. new
에fix_remain_str
의 문자열을 복사하고, 마지막에는\0
을 넣어준다.ft_strlcpy
함수를 사용한 것과 같다.- 기존의
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
함수의 작동과정은 다음과 같다.
head
노드부터 시작해 노드가 있다면 노드를 넘겨가면서 반복문을 돈다.- 만약 다음 노드가 없다면 반복문을 빠져나온다.
- 다음 노드가 존재하지만 문자열이 없고,
flag
값이EOF
에 도달했다는 의미인 -1인 경우 두가지 경우로 나뉜다.- 다다음 노드가 없는 경우 다음 노드가 마지막 노드이므로 다음 노드를 해제한 후 현재 노드의
next
를NULL
로 지정한다. - 다다음 노드가 존재하는 경우 다음 노드는 중간에 낀 노드이므로, 현재 노드와 다다음 노드를 연결시킨 후 다음 노드를 해제한다.
- 다다음 노드가 없는 경우 다음 노드가 마지막 노드이므로 다음 노드를 해제한 후 현재 노드의
- 반복문에서 빠져나온 후, 만약
head
노드의 다음 노드가 없다면 모든fd
에 해당하는 노드들이 해제된 것이므로 추가로get_next_line
함수가 호출되지 않는 한head
노드 또한 유지될 필요가 없다. 따라서head
노드의 메모리를 해제한 후NULL
을 반환한다.- 이 때
head
노드 포인터에NULL
을 넣어주지 않으면 다음번get_next_line
이 호출되었을 시head
노드 포인터는 여전히 해제된 메모리 주소를 가리키고있어 문제가 발생한다.
- 이 때
flag
값을 사용하지 않고, check_remain
함수에서 사용한 read_idx
을 인자로 주고받으며 사용하는 방법도 가능할 것 같다.