Get Next Line

김지수·2023년 5월 3일
0

42

목록 보기
1/4

과제 목표

해당 프로젝트는 작업물에 편한 함수를 더하는 것 뿐만 아니라 static variable에 대한 이해도 또한 기를 수 있다.


해당 함수 설명

Function name : get_next_line

Prototype : char *get_next_line(int fd);

Turn in files : 
	get_next_line.c, 
	get_next_line_utils.c,
	get_next_line.h

Parameters fd : The file descriptor to read from

Return value :
	Read line: correct behavior
	NULL: there is nothing else to read, or an error occurred

External functs.: read, malloc, free

Description : 
	Write a function that returns a line read from a file descriptor

get_next_line() 함수를 반복 호출(예: 루프를 사용하여)하면 파일 디스크립터로 가리키는 텍스트 파일을 한 번에 한 줄씩 읽을 수 있어야 한다.
• 함수는 읽은 라인을 반환해야 한다.
읽을 것이 더 이상 없거나 오류가 발생한 경우 NULL을 반환해야 한다.
• 함수는 파일에서 읽는 경우와 표준 입력에서 읽는 경우 모두 예상대로 작동하도록 해야 한다.
• 반환된 라인은 파일 끝이 \n 문자로 끝나지 않는 경우를 제외하고 종료하는 \n 문자를 포함해야 한다.
• get_next_line.h 헤더 파일은 get_next_line() 함수의 프로토타입을 포함해야 한다.
• get_next_line_utils.c 파일에 필요한 모든 헬퍼 함수를 추가해야 한다.
• static variable을 알고 시작하는 것이 유리하다!
• get_next_line()으로 파일을 읽기 때문에, 컴파일러가 해당 옵션을 추가해야하기 때문이다.(-D BUFFER_SIZE=n)
해당 옵션은 read()로부터 버퍼 사이즈가 결정된다.
작성한 코드를 시험하기 위해 버퍼 사이즈를 변경해볼 것이다.
• 평소 사용하는 flags에 더해서 -D BUFFER_SIZE가 있든 없든 해당 프로젝트는 컴파일 되어야 한다. 기본값은 본인이 설정할 수 있다.
• 코드는 다음 규칙으로 컴파일 된다.(cc -Wall -Wextra -Werror -D BUFFER_SIZE=42 <'files'>.c (여기서''기호는 생략, 여기서 '42'는 임의의 수)
• 만약 파일 디스크립터가 가리키는 파일이 마지막 호출 이후 변경되었지만 read()가 파일의 끝에 도달하지 않은 경우, get_next_line() 함수의 동작은 정의하지 않았다고 간주한다.
• 또한 이진 파일을 읽는 경우 get_next_line() 함수의 동작이 정의되지 않았다고 간주한다. 그러나 당신이 하고 싶다면 이러한 동작을 처리하기 위한 논리적인 방법을 구현할 수 있다.
• 만약 BUFFER_SIZE가 999, 1, 1000000일 때도 작동하는지, 그 이유는 무엇인지 알아야 한다.
• get_next_line()이 불려질 때마다 가능한 적은 시간을 사용해야한다. 만약 새로운 라인을 읽는다면, 당신은 현재의 라인을 반환해야한다.
전체 파일을 읽고, 각각의 줄을 처리하지 마라.

금지사항
• 해당 과제에선 libft를 사용해선 안된다.
• lseek()은 금지된다.
• Global variable은 금지된다.

과제 풀이내용

함수에 대한 이해

끝내 정의는 "fd를 매개변수로 입력받고, fd에서 한 줄씩 읽어 출력하는 함수이다. 만약 읽을 것이 없거나, 오류가 발생하면 NULL을 반환해야 한다."는 것이다.
static variable을 사용하라 했는데 읽자마자 생각한건 한줄 씩 읽기 때문에 새로운 것이 올 경우 뒤에 붙여써야 하기 때문이라 생각했다.

fd란 무엇인가?

file descriptor라 하며 유닉스 계열의 시스템에서 프로세스가 파일을 다룰 때 사용하는 개념이다. 유닉스는 모든걸 파일로 생각을 한다. 내가 새로운 파일에 접근(만들던가, 쓰던가, 읽던가)하기 위해 해당 개념을 사용할텐데, 이때 커널은 사용하지 않은 숫자 중 가장 작은 값을 할당한다. 일반적으로 0이 아닌 정수 값을 갖는다.
여기서 0(입력), 1(출력), 2(에러)는 이미 저장되어 있다.
최댓값은 platform마다 다르다. 해당 멕북은 하드웨어에선 200~300 사이, 소프트 웨어서는 open limit이지만 실제로 돌려보면 49152에서 멈춘다.

ulimit -a

static이란?

data 영역에 저장된다. 즉, life span은 프로그램을 실행하고 끝났을 때까지 이다.
특이한 것은 static 전역 변수, static 함수는 외부에서 볼 수 없다는 것이다. 운영체제 시간에 각 thread는 stack을 제외한 data, heap, code영역은 공유할 수 있다 배웠기 때문이다.
이유는 static 전역 변수는 정적 데이터 영역(static data area)에 저장되고, 전역 변수는 프로그램의 데이터 섹션(data section)에 저장되기 때문이다.
이하는 chatGPT의 답변이고 신비성이 있어 첨부한다.(data section은 컴파일러가 컴파일 타임에 초기화된 전역 변수와 정적 변수를 저장하는 섹션입니다. 이 섹션에 저장된 변수는 프로그램의 수명주기동안 유지됩니다. 이 섹션은 실행 파일의 데이터 섹션에 저장되며, 프로그램 실행 중에 메모리에 로드됩니다.
반면 static data area는 전역 변수와 정적 변수를 저장하는 공간 중 하나입니다. 이 공간은 BSS(Block Started by Symbol) 섹션에 저장되며, 프로그램 실행 중에 해당 변수들은 0으로 초기화됩니다.
즉, data section은 초기화된 전역 변수와 정적 변수를 저장하는 섹션이며, static data area는 초기화되지 않은 전역 변수와 정적 변수를 저장하는 공간입니다. 이 둘은 비슷한 개념이지만 약간의 차이가 있습니다.)

read함수란?

ssize_t read(int fd, void *buf, size_t count)

fd를 count만큼 읽고 buf에 저장
정상적이면 읽은 바이트수, 문장 끝(EOF)이면 0, 에러시 -1 반환
ssize_t는 부호 있는 정수형 타입으로, I/O 함수의 반환값과 같은 부호 있는 값을 저장하는 데 사용된다.(size_t는 부호 없는 정수형 타입 반환)

-D BUFFER_SIZE=42

따로 변수를 정의하지 않아도 외부에서 정의해주면 된다.

#include "head.h"

int main(void)
{
    printf("%d", i);
    return 0;
}
gcc -Wall -Werror -Wextra -D BUFFER_SIZE=xx get_next_line.c get_next_line_utils.c && ./a.out

fd에 대한 추가 이해

#include <stdio.h>
#include <fcntl.h>

int main()
{
	char	*p;

	p = get_next_line(0);
	printf("%s", p);
	while (p)
	{
		free(p);
		p = 0;
		p = get_next_line(0);
		printf("%s", p);
	}
}

해당 main문을 밑에다 작성하고 돌리면 내가 입력한 내용을 그대로 받는다. fd가 0일 때는 표준 입력 상태이기 때문이다.

구현

소스 코드

#include "get_next_line.h"

char	*ft_strdup(const char *s1)
{
	size_t		count_s1;
	char		*return_value;

	count_s1 = 0;
	while (s1[count_s1])
		count_s1++;
	count_s1++;
	return_value = (char *)malloc(sizeof(char) * count_s1);
	if (!return_value)
		return (0);
	count_s1 = 0;
	while (s1[count_s1])
	{
		return_value[count_s1] = s1[count_s1];
		count_s1++;
	}
	return_value[count_s1] = 0;
	return (return_value);
}

static int	sub1(char **temp, char **previous, char **buffer, int value_fd)
{
	(*buffer)[value_fd] = '\0';
	*temp = *previous;
	*previous = ft_strjoin(*temp, *buffer);
	free(*temp);
	if (ft_strchr(*buffer, '\n'))
		return (1);
	return (0);
}

char	*find_newline(char *buffer, char *previous, int fd)
{
	ssize_t	value_fd;
	char	*temp;

	value_fd = read(fd, buffer, BUFFER_SIZE);
	while (value_fd != 0)
	{
		if (value_fd == -1)
			return (NULL);
		else if (value_fd == 0)
			break ;
		if (!previous)
		{
			previous = ft_strdup("");
			if (!previous)
				return (NULL);
		}
		if (sub1(&temp, &previous, &buffer, value_fd))
			break ;
		value_fd = read(fd, buffer, BUFFER_SIZE);
	}
	return (previous);
}

char	*make_previous(char *return_value)
{
	char	*temp;
	int		index;

	index = 0;
	while (return_value[index] != '\n' && return_value[index] != '\0')
		index++;
	if (return_value[index] == '\0' || return_value[1] == '\0')
		return (NULL);
	temp = ft_substr(return_value, index + 1, ft_strlen(return_value) - index);
	if (temp == NULL || *temp == '\0')
	{
		if (temp == NULL)
		{
			free(return_value);
			return (NULL);
		}
		free(temp);
		temp = NULL;
	}
	return_value[index + 1] = '\0';
	return (temp);
}

char	*get_next_line(int fd)
{
	char		*buffer;
	char		*return_value;
	static char	*previous;

	if (fd < 0 || BUFFER_SIZE <= 0)
		return (NULL);
	buffer = (char *)malloc(sizeof(char) * (BUFFER_SIZE + 1));
	if (!buffer)
		return (NULL);
	return_value = find_newline(buffer, previous, fd);
	free(buffer);
	if (!return_value)
	{
		if (previous)
		{
			free(previous);
			previous = NULL;
		}
	}
	if (!return_value)
		return (NULL);
	previous = make_previous(return_value);
	return (return_value);
}
#include "get_next_line.h"

static char	*join_str(char *return_value, char const *s1, char const *s2)
{
	size_t	len_temp;
	size_t	len;

	len_temp = 0;
	while (s1[len_temp])
	{
		return_value[len_temp] = s1[len_temp];
		len_temp++;
	}
	len = len_temp;
	len_temp = 0;
	while (s2[len_temp])
	{
		return_value[len + len_temp] = s2[len_temp];
		len_temp++;
	}
	return_value[len + len_temp] = 0;
	return (return_value);
}

char	*ft_strjoin(char const *s1, char const *s2)
{
	char	*return_value;
	size_t	len_s1;
	size_t	len_s2;
	size_t	len;

	len_s1 = 0;
	while (s1[len_s1])
		len_s1++;
	len_s2 = 0;
	while (s2[len_s2])
	len_s2++;
	len = len_s1 + len_s2 + 1;
	return_value = (char *)(malloc(sizeof(char) * len));
	if (!return_value)
		return (0);
	return (join_str(return_value, s1, s2));
}

char	*ft_strchr(const char *s, int c)
{
	while (*s != '\0')
	{
		if (*s == (unsigned char)c)
			return ((char *)s);
		s++;
	}
	if ((char)c == '\0')
		return ((char *)s);
	return (NULL);
}

char	*ft_substr(char const *s, unsigned int start, size_t len)
{
	char	*return_value;
	size_t	i;

	if (ft_strlen(s) < start)
	{
		return_value = (char *)malloc(sizeof(char) * 1);
		if (!return_value)
			return (0);
		return_value[0] = 0;
		return (return_value);
	}
	if (ft_strlen(s) < len)
		len = ft_strlen(s);
	return_value = (char *)malloc(sizeof(char) * (len + 1));
	if (!return_value)
		return (0);
	i = 0;
	while (i < len && s[start] != 0)
		return_value[i++] = s[start++];
	return_value[i] = 0;
	return (return_value);
}

size_t	ft_strlen(char const *str)
{
	size_t	i;

	i = 0;
	while (str[i])
		i++;
	return (i);
}
#ifndef GET_NEXT_LINE_H
# define GET_NEXT_LINE_H

# ifndef BUFFER_SIZE
#  define BUFFER_SIZE 1
# endif

# include <stdlib.h>
# include <unistd.h>

char	*get_next_line(int fd);
char	*make_previous(char *return_value);
char	*find_newline(char *buffer, char *previous, int fd);
char	*ft_strdup(const char *s1);
char	*ft_strjoin(char const *s1, char const *s2);
char	*ft_strchr(const char *s, int c);
char	*ft_substr(char const *s, unsigned int start, size_t len);
size_t	ft_strlen(char const *str);

#endif

삽질 모음

ft_substr

나에게 가장 어려웠던 항목은 바로 이 부분이다.

	if (temp == NULL || *temp == '\0')
	{
		if (temp == NULL)
		{
			free(return_value);
			return (NULL);
		}
		free(temp);
		temp = NULL;
	}

42에서는 옵션 3개를 사용하는데

*temp == "" // 틀린 표현
*temp == '\0' // 알맞은 표현

이게 굉장히 헷갈렸다.
ft_substr에서 보면 반환 값이 세분화하면 3가지가 나온다.
NULL, return_value(1바이트(\0으로 채워짐)), return_value(다른 값들로 채워짐)
2번 째의 경우를 생각하고 고려하기가 굉장히 어려웠다. NULL은 터져서 나온 경우이고, 2번째의 경우는 틀린 경우는 아니기 때문이다. 따라서 따로 예외처리를 해줘야 한다.

해당 코드에 추가 커멘트

사실 *temp == '\0'이 되는 경우는 없다. 이유는 len - index를 하는 것인데. index가 len보다 클 수가 없기 때문이다.

read

생각해보면 왜 헷갈릴까 싶은데 하다보면 헷갈린다.
위에서 나는 읽으면 읽은 크기를 반환하고, 다 읽으면 0을 반환하다고 하였다. 그렇다면 만약 버퍼가 굉장히 커서 문장 끝까지 읽을 경우는 어떻게 되는가? 읽은 글자의 수를 반환하는가 아님 끝까지 읽었기에 0을 반환하는가? 정답은 당연히 전자이다. 읽었기 때문이다. 그렇기 때문에 그 이후에는 0을 반환한다고 생각해야한다.

연계된 문제

value_fd = 1;
	while (value_fd != 0)
	{
		value_fd = read(fd, buffer, BUFFER_SIZE);

buffer의 크기가 100이고 들어온게 a\n\0 이라면 2가 읽힌다. 처음엔 0을 반환 할 것이라 생각하여 저렇게 계획했는데 수정했다.

value_fd = read(fd, buffer, BUFFER_SIZE;
	while (value_fd != 0)
	{
    	...
		value_fd = read(fd, buffer, BUFFER_SIZE);

return에 대한 free의 문제

이게 get_next_line자체에서 할당의 문제 생겼다면 간단히 NULL을 return해주면 된다. 다만 헷갈리는 부분은 내가 call한 함수에서 터진경우를 고려해야한다는 것이다.터졌을 때, 반환값은 NULL을 할당한 공간인가 아님 NULL을 가리키는 포인트 그 자체인가? 답은 후자이다. 터졌기 때문에 따로 공간을 할당하지 않았고, 그렇기에 따로 free하면 안된다.

잘못봤다.

temp = ft_substr(return_value, index + 1, ft_strlen(return_value) - index);

실수인지 실력인지 처음에 ft_strlen(return_value - index)라 쳤다. 개인적으로 이게 왜 가능한건지 의문이 너무 든다. 왜 컴파일러가 왜 안잡았을까?


참고 사이트
https://gamguma.dev/post/2022/02/get_next_line
profile
노는게 제일 좋아

0개의 댓글