Search
Duplicate
🍋

get_next_line() 뽀개기 by suhshin

간단소개
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
42seoul
Scrap
태그
파일디스크립터
더블포인터
9 more properties

Subject

0. Get Next Line 문제 이해하기

Get Next Line 문제를 보지 않고 제목만 봤을 때에는 이 함수가 어떤 역할을 하는 지 바로 떠오르지 않았다. 이전에 libft 에서는 strmap, strcmp 와 같이 기존에 자주 쓰던 함수들을 다시 재구성하는 프로젝트여서 함수 이름만 보면 그 역할을 대부분 인지하고 있었다.
하지만 이 함수는 어떤 목적을 가지고 있는지 return 값의 의미가 무엇인지 매개변수로 받는 인자들에는 어떤 이유가 있는지 감이 오질 않았다. 이후 과제를 완전히 풀고나서야 모든 퍼즐이 맞추어지는 느낌이였다.
간단하게 요약하면 이번 프로젝트는 특정 파일을 받아서 해당 파일을 읽어오는데, 개행을 만나기 전까지만 출력을 하기위한 함수이다. 아래 에 나오는 필수 파트들을 살펴보자.
Search
Mandatory Part
함수 이름
get_next_line
get_next_line.cget_next_line_utils.cget_next_line.h
#1. 프로그램이 읽을 파일 디스크럽터(file descriptor for reading) #2. 읽어왔던 값(The value of what has been read)
1 : 한 라인이 읽혔을 때(A line has been read) 0 : EOF에 도달했을 때(EOF has been reached) -1 : 에러가 발생했을 때(An error happened)
readmallocfree
파일 디스크럽터로부터 읽어 온 하나의 라인(newline 없이)을 반환하는 함수 작성 Write a function which returns a line read from a file descriptor, without the newline
get_next_line함수는 fd**line 을 매개변수로 받는다. fd는 프로그램이 읽을 file descriptor, line은 읽어온 값을 넣는 변수이다.
리턴 값은 위의 목적에 맞게 1, 0, -1 을 반환하고 사용가능한 외부함수는 read(), malloc() free()이다.

1. read()에 대한 이해

이 프로젝트를 구현하기 위해서는 우선 메모리와 파일에 대한 공부와 사용 가능한 외부 함수들에 대한 이해가 필요했다. 이 부분은 여기에서 모두 다룰 수 있다. 해당 문서에서 작성한 마지막 read() 테스트 코드만 가지고 와보았다. 이제 이 기반 코드를 가지고 아래의 과정들을 이어나갈 예정이다.
#include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <string.h> #define BUFFER_SIZE 1 int main(void) { char buf[BUFFER_SIZE + 1]; char *save; int fd; int read_size = 0; fd = open("./text.txt", O_RDONLY); if (fd == -1) printf("file open error"); else { while ((read_size = read(fd, buf, BUFFER_SIZE)) > 0) { buf[read_size] = '\0'; if(save == NULL) save = strdup(buf); else strcat(save, buf); } printf("%s", save); close(fd); } return(0); }
C
복사

2. Main Function

get_next_line()은 두개의 인자를 받는데 우리는 이 변수들을 직접 설정하는 것이 아니라 다른 함수의 변수를 받아서 쓰게 된다. 그래서 gnl과 관련된 함수 이외에 이 함수를 실행하는 환경, 즉 main function에 대한 고려가 필요했다. gnl 함수에 필요한 두 매개변수는 어떻게 전달되는 것일까?
#include "get_next_line.h" #include <stdio.h> int main(void) { char *line; int ret; int fd = open("./test.txt", O_RDONLY); while((ret = get_next_line(fd, &line)) > 0) { printf("return : %d\n",ret); printf("line : %s\n",line); } return (0); }
C
복사
여기서 우리는 지역 변수에 대한 이해가 필요하다. 왜 char* 로 선언된 line을 그대로 변수로 넘기는게 아니라 &line 으로 넘기는 것일까?

함수의 인수 및 반환에 대한 이해

내가 선언한 변수를 특정 함수에 넣고, 함수 내부에서 해당 변수의 값을 조정하면 함수가 끝난 후 다시 원위치에서 해당 변수의 값이 바뀌어 있을까?
#include <stdio.h> void sum(int a) { a = a + 9; printf("in func a : %d\n", a); } int main(void) { int a = 0; printf("out func a : %d\n", a); sum(a); printf("out func a : %d\n", a); }
C
복사
아래의 코드를 살펴보자
위와 같은 현상이 발생하는 이유는
함수가 호출될 때 전달인자는 매개변수에 복사되기 때문이다.
즉, 우리가 선언한 a를 함수 인자로 넣으면 해당 함수는 그 값을 복사해서 매개변수로 가지게 되는 것이다. 그리고 매개변수는 지역변수이기 때문에 해당 함수가 종료되는 } 를 만나면 사라지게 된다. 따라서 복사된 값을 가지고 아무리 난리를 쳐도 원래의 값이 바뀌진 않는 것이다.
그래서 우리는 주소를 참조할 필요가 있다.
위의 코드에서 전달인자와 매개변수를 해당 변수의 주소로만 바꿔준다면 우리는 우리가 원하는 결과를 얻을 수 있다.
#include <stdio.h> void sum(int *a) { *a = *a + 9; printf("in func a : %d\n", *a); } int main(void) { int a = 0; printf("out func a : %d\n", a); sum(&a); printf("out func a : %d\n", a); }
C
복사
우리는 char *line을 get_next_line() 함수의 인자로 전달해서 함수 내부에서 line에 문자열을 할당하려고 한다. 따라서 함수에 문자열을 인자로 넘기는 것이 아니라 해당 문자열을 가리키는 포인터를 넘겨주어야 한다. 그래서 우리는 아래와 같은 함수 원형을 사용해야한다.
int get_next_line(int fd, char **line);
C
복사
여기에 다른 함수에서 인자를 전달할 때에는 아래와 같은 형태로 전달한다.
int ret; ret = get_next_line(fd, &line);
C
복사

3. GNL Pseudo Code

Main function에 대한 고려가 끝나고 get_next_line()의 매개변수들의 역할을 파악했으니 이제 핵심역할에 대한 고려가 필요했다. read()함수를 공부하고 나니 어떤방식으로 read()함수를 사용해야하는지 감이 왔다.
read()함수는 지정된 BUFFER_SIZE만큼 무조건 읽어오기 때문에 읽어올 때 개행이 있는지 알아야 했다. 그래서 위의 read()함수를 기준으로 아래와 같이 pseudo code 를 작성해 보았다.
#include "get_next_line.h" int get_next_line(int fd, char **line) { char *buf; // read함수로 버퍼사이즈만큼 읽어올 때 저장되는 스트림 static char *backup; // 개행을 만나기 전까지의 문자열을 합치면서 저장할 포인터 int fd; // 파일디스크립터 테이블의 인덱스 값 int read_size; // read 함수로 읽어온 바이트의 양 if ( fd 불가능 조건 || line이 들어오지 않았을 떄 || 버퍼사이즈 불가능 조건 || 버퍼 저장할 문자열 malloc 에러처리 ) return (-1); while ( read_size = read(fd, 버퍼, 버퍼사이즈)) { // 읽어온 buffer의 마지막에 '\0' 추가하기! // backup = 문자열 합치기 (backup , buf) if ( backup 에 개행이 있으면? ) { //개행이 발견된 인덱스의 값을 '\0'으로 바꾸어준다. // -> strlen, strdup등 문자열 관련 함수를 사용할때 해당 위치가 // '\0' 이면 문자열의 끝으로 인식할 수 있기 때문 /* function strslice() 1. 에러가 발생한 경우 return -1 2. 이전단계에서 합쳐진 문자열 backup에서 개행이 발견된 경우 - 개행의 앞부분은 line에 할당 - 개행의 뒷부분은 backup에 저장 return 0 */ } return (문자열 처리 및 return value 처리) } /* function return_check() 에서 고려할 내용 read 로 읽어온 사이즈가 0으로 나왔을 경우 처리할 조건들 1. read_size = -1 오류가 발생했으므로 return -1 2. read_size = 0, backup = NULL 라인에 빈 문자열을 할당해주고 return 0 3. read_size = 0, backup 이 남아있을 때 backup에 저장한 문자열을 line에 넣고 return 0 */ return ( return_check() ) }
C
복사
이제 위의 pseudo code를 기반으로 코드를 구현해보자.

4. C library를 활용한 GNL

일단 위의 pseudo code에서 고려한 예외사항들을 제외하고 함수의 형태만 만들어보기위해 C library 함수들을 사용해서 만들어 보았다. 내가 딱 원하는 함수들도 있었고 조금은 고려가 필요한 함수도 있었다.
#include "get_next_line.h" int get_next_line(int fd, char **line) { char *buf; static char *backup; ssize_t read_size; ssize_t i; char *tmp; // strchr 에서 나온 포인터를 활용할 임시 변수 // 과제코드에서는 i에 인덱스를 받는 함수를 구현 할 예정이다. if (fd < 0 || fd > OPEN_MAX || BUFFER_SIZE < 1 || !line || !(buf = (char *)malloc(BUFFER_SIZE + 1))) return (-1); while ((read_size = read(fd, buf, BUFFER_SIZE)) > 0) { buf[read_size] = '\0'; if (backup == NULL) backup = strdup(buf); else strcat(backup, buf); if (tmp = strchr(backup, '\n')) { i = tmp - backup; backup[i] = '\0'; free(buf); buf = NULL; return (gnl_strslice(&backup, i, line)); // 추가적인 고려가 필요한 부분 } } free(buf); buf = NULL return (gnl_return_check(read_size, &backup, line)); // 추가적인 고려가 필요한 부분 }
C
복사
기존의 함수만을 활용해서는 모든 예외사항들을 고려한 함수를 만들기 어려웠다. 이제 어느정도 형태가 잡혔으니 필요한 함수들을 제작하여 코드를 완성해보았다.

5. GNL 완성하기

위에서 나온 기존 함수들을 이제 나만의 함수들로 만들어 볼 시간이다. 기존 함수들에서 조금씩 필요한 부분들 추가하고 에러처리를 위한 함수들을 추가하였다. 특히 리턴값을 결정하는 함수들이 가장 고려가 많이 필요했다.
#include "get_next_line.h" int get_next_line(int fd, char **line) { char *buf; static char *backup; ssize_t read_size; ssize_t i; if (fd < 0 || fd > OPEN_MAX || BUFFER_SIZE < 1 || !line || !(buf = (char *)malloc(BUFFER_SIZE + 1))) return (-1); while ((read_size = read(fd, buf, BUFFER_SIZE)) > 0) { buf[read_size] = '\0'; backup = gnl_strappend(backup, buf); if ((i = gnl_check_new_line(backup)) >= 0) { gnl_free(&buf); return (gnl_strslice(&backup, i, line)); } } gnl_free(&buf); return (gnl_return_check(read_size, &backup, line)); }
C
복사

get_next_line()을 위한 추가 함수들

// read_size가 0일때 return값과 backup, line을 처리해주는 함수 int gnl_return_check(ssize_t read_size, char **backup, char **line); // 문자열 두개를 합쳐주기 위한 함수(에러처리와 메모리 공간 추가할당을 고려함) char *gnl_strappend(char *str1, char *str2); // 문자열에 개행이 있는지 체크 후 해당 인덱스를 반환하는 함수 int gnl_check_new_line(char *line); // 개행이 나온 인덱스 i를 기준으로 backup, line문자열을 할당 및 처리 해주는 함수 int gnl_strslice(char **backup, ssize_t i, char **line); // strdup와 동일하게 작동 char *gnl_strdup(char *src); // strlcpy와 동일하게 작동 size_t gnl_strlcpy(char *dst, const char *src, size_t dstsize); // strlen과 동일하게 작동 size_t gnl_strlen(char *str); // 중복되는 free구문을 줄여주기위한 함수 void gnl_free(char **str);
C
복사

6. Bonus Part!

보너스 파트를 구현하는 것은 생각보다 어렵지 않았다.
...be continue