- [42경산/C언어] Get Next Line(GNL) 완벽 가이드: 버퍼 크기별 동작 원리와 정적 변수 메모리 관리2025년 03월 16일 18시 17분 44초에 업로드 된 글입니다.작성자: kugorang
TL;DR — 3줄 핵심 요약
• 문제: read()는 한 줄 단위로 읽지 않는다. 요청한 바이트 수보다 적게 돌려줄 수 있고, 개행이 어디 있는지도 알려주지 않는다. 그래서 "아직 반환하지 못한 나머지"를 함수 밖에 저장해야 한다.
• 기술/원인: 시그니처가 char *get_next_line(int fd)로 고정되어 상태를 인자로 넘길 수 없다. 그래서 static 변수를 fd별로 두고, read로 채운 덩어리와 합쳐 개행 앞까지를 반환하고 뒤는 남긴다.
• 해결/인사이트: 흐름은 네 단계다. ① 저장된 문자열에서 개행을 찾는다 ② 없으면 read로 이어붙인다 ③ 개행 앞부분을 복사해 반환한다 ④ 나머지를 static에 다시 저장하고 이전 메모리를 free한다. EOF와 오류 경로에서도 static이 가리키던 메모리를 반드시 해제한다.
read()가 실제로 하는 일

줄여서 gnl (겟넥라) ssize_t read(int fd, void *buf, size_t count);- 반환값이 양수: 그 바이트 수만큼 buf가 채워졌다. count보다 적을 수 있다.
- 반환값 0: 파일 끝(EOF). 더 읽을 데이터가 없다.
- 반환값 -1: 오류.
- buf에는 '\0'이 자동으로 붙지 않는다. 읽은 뒤 직접 넣어야 문자열이 된다.
- 파일 오프셋은 커널이 관리하므로 다음 호출은 이어서 읽는다.
즉 read는 줄 단위가 아니라 바이트 덩어리 단위다. 덩어리에서 줄을 잘라내는 일은 호출한 쪽이 해야 한다.
왜 static 변수가 필요한가
함수가 끝나면 지역 변수는 사라진다. 그런데 한 번 읽은 덩어리에는 두 줄 이상이 들어 있을 수 있다. 이번에 반환하지 않은 뒷부분을 다음 호출에서 써야 하므로 함수 밖에 살아 있는 저장소가 필요하다. static 지역 변수는 수명이 프로그램 전체이고 접근 범위는 함수 안이다.
static char *saved[OPEN_MAX];fd를 인덱스로 쓰면 파일마다 남은 문자열을 따로 보관한다. fd1과 fd2를 번갈아 읽어도 섞이지 않는다. OPEN_MAX(limits.h)는 한 프로세스가 열 수 있는 fd 수다.
본체 흐름
char *get_next_line(int fd) { static char *saved[OPEN_MAX]; char buf[BUFFER_SIZE + 1]; ssize_t n; char *nl; if (fd < 0 || fd >= OPEN_MAX || BUFFER_SIZE <= 0) return (NULL); nl = saved[fd] ? ft_strchr(saved[fd], '\n') : NULL; while (!nl) { n = read(fd, buf, BUFFER_SIZE); if (n <= 0) break ; buf[n] = '\0'; saved[fd] = ft_strjoin_free(saved[fd], buf); nl = ft_strchr(saved[fd], '\n'); } if (n < 0) return (free(saved[fd]), saved[fd] = NULL, NULL); return (extract_line(&saved[fd], nl)); }extract_line은 개행 위치까지를 새 문자열로 복사해 반환하고, 그 뒤를 새로 할당해 saved[fd]에 넣은 다음 이전 메모리를 해제한다. 남은 것이 없으면 saved[fd]는 NULL이 된다.
메모리 누수가 생기는 지점
- strjoin으로 새 문자열을 만들고 이전 saved[fd]를 free하지 않아 호출마다 누수
- 줄을 잘라 반환한 뒤 남은 부분을 저장하지 않아 다음 호출에서 데이터 손실
- EOF에서 saved[fd]를 free하지 않아 still reachable 발생
- read가 -1을 반환했을 때 정리 없이 NULL 반환
검증은 valgrind로 한다. still reachable이 남지 않아야 통과하는 경우가 많다.
BUFFER_SIZE를 어떻게 잡을까
- 평가는 -D BUFFER_SIZE=1, 42, 9999 등 여러 값으로 컴파일한다. 어떤 값에서도 동작해야 한다.
- 1바이트면 read 호출이 잦아 느리지만 결과는 같아야 한다.
- 매우 큰 값이면 char buf[BUFFER_SIZE + 1]이 스택을 넘길 수 있다. 스택 크기는 보통 8MB다.
- BUFFER_SIZE가 0 이하이면 read가 0을 반환해 무한 루프가 되므로 진입 시 검사한다.
긴 줄에서의 성능
strjoin을 반복하면 매번 새로 할당하고 복사한다. 한 줄이 아주 길면 복사량이 제곱으로 늘어난다. 대안은 누적 버퍼(단순, 대부분 충분)와 동적 확장(용량을 두 배로 늘려 재할당 감소)이다. realloc이 금지되어 있어 동적 확장은 직접 구현해야 한다. 평가는 정확성과 메모리 안전성을 먼저 본다.
평가에서 보는 것
- 빈 파일, 개행만 있는 파일, 마지막 줄에 개행이 없는 파일
- 여러 fd 교차 호출
- 잘못된 fd, 읽기 오류
- 메모리 누수 없음(leaks, valgrind)
- 허용 함수만 사용(read, malloc, free). 디버그 출력 금지
- Norm 규칙: 함수 25줄 이하, 파일 분리(get_next_line.c, get_next_line_utils.c)
자주 묻는 질문 (FAQ)
Q1. static 변수와 전역 변수는 무엇이 다른가요?
A. 저장 위치는 같고 접근 범위가 다르다. 전역 변수는 다른 파일에서도 extern으로 접근할 수 있지만, 함수 안의 static은 그 함수 안에서만 보인다. 과제에서는 은닉이 목적이므로 static을 쓴다.
Q2. BUFFER_SIZE가 1일 때도 정상 동작해야 하나요?
A. 그렇다. 평가는 여러 값으로 컴파일한다. 1바이트씩 읽어도 개행을 만날 때까지 이어붙이고, 반환 후 남은 1바이트를 정확히 저장해야 한다. 이 경우를 먼저 통과시키면 대부분의 버그가 드러난다.
Q3. EOF 이후에 다시 호출하면 무엇을 반환하나요?
A. NULL이다. 남은 데이터가 없으면 saved[fd]를 free하고 NULL로 만든 뒤 NULL을 반환한다. 이후 같은 fd로 호출해도 read를 반복하지 않고 바로 NULL이 나와야 한다.
728x90728x90'42경산 & 알고리즘 > 42 과제 회고' 카테고리의 다른 글
[42경산 FDF] 와이어프레임 모델 구현 가이드: 등각 투영 좌표 계산부터 DDA·Bresenham 선 그리기까지 (0) 2026.04.12 [42경산 push_swap] 11개 명령어로 정렬하기: 그리디의 함정과 이진 기수 정렬 해법 (0) 2025.09.21 [42경산 Born2beRoot] Rocky Linux 가상 서버 구축: LVM·UFW·SSH·sudo·SELinux 핵심 정리 (5) 2025.08.05 [42경산 ft_printf] 가변 인자(va_list)로 직접 구현하는 printf: 형식 지정자 처리와 버퍼 최적화 (0) 2025.07.20 [42경산/C언어] Libft 라이브러리 구현 가이드: 표준 C 라이브러리 함수 재구현과 Makefile 설계 (0) 2025.03.12 다음글이 없습니다.이전글이 없습니다.댓글