ft_putnbr_fd 구현하기

윤효준·2024년 8월 9일
0

42 Libft 복습

목록 보기
21/28

42서울의 ft_putnbr_fd를 구현해보자!

우리가 쓸 수 있는 함수는 #unistd.hwrite 함수이다!

일단 write 함수에 대해 알아보자!!

Write 함수란???

ssize_t	write(int fd, const void *buf, size_t count);
  • fd파일 디스크립터이다.
  • buf는 쓰려는 데이터가 저장된 버퍼의 포인터이다.
  • count는 버퍼에서 쓰려는 데이터의 바이트 수이다.

write 함수의 사용 예시를 들어 더 자세히 설명하겠다.

#include <unistd.h>

int main(void)
{
	const char *msg = "Hello, hyoyoon"; //내 42서울 별명(?)이다.
    write(1, msg, 14); // 표준 출력으로 msg를 14바이트 쓴다.
    return (0);
} //해당 main을 실행하면 콘솔창에 Hello, hyoyoon이 나온다.

여기서 잠깐!!! 왜 가운데 인자를 메모리가 아니라 버퍼라고 할까??

위의 예시만 보아도 const char *msg는 메모리에 저장되는 것을 알 수 있다. 근데 왜 버퍼라고 하는 것일까??

  • 메모리 : 컴퓨터에서 데이터를 저장하고 처리하기 위한 공간 전체, 프로그램이 실행되는 동안 모든 종류의 데이터를 저장하는 데 사용되며 매우 포괄적인 개념이다.

  • 버퍼 : 메모리의 특정 부분을 가리키며 데이터를 임시로 저장하고 전송하기 위해 사용되는 공간을 의미한다.

여기서 먼저 버퍼의 필요성에 대해 알아보자!

버퍼는 주로 입출력 작업의 효율성과 성능을 개선하기 위해서이다.

입출력 작업은 일반적으로 CPU가 메모리에서 데이터를 처리하는 속도보다 훨씬 느리다.
예를 들어, 디스크에 데이터를 쓰거나 네트워크를 통해 데이터를 전송하는 속도는 메모리에서 데이터를 읽고 쓰는 속도보다 훨씬 느리다.

이로 인해 다음과 같은 문제들이 발생할 수 있다!

  • 속도 불일치: CPU와 메모리의 처리 속도는 매우 빠른 반면 디스크나 네트워크 장치는 상대적으로 느리다. 만약 CPU가 데이터를 디스크로 바로 쓴다면 디스크가 데이터를 처리할 때까지 CPU는 대기해야 한다. 이는 시스템의 전반적인 성능 저하를 초래한다.

  • 시스템 호출 오버헤드: 파일이나 소켓에 데이터를 쓸 때마다 시스템 호출(write)을 수행해야 한다. 시스템 호출은 사용자 모드에서 커널 모드로의 전환이 필요하기 때문에 비용이 많이 드는 작업이다. 따라서 데이터를 조금씩 자주 전송하는 대신 데이터를 모아서 한 번에 전송하는 것이 효율적이다.

이 데이터를 모으는 공간을 버퍼라고 한다. CPU는 데이터를 빠르게 버퍼에 쓰고 나서 다른 작업을 계속 수행할 수 있으며 디스크나 네트워크 장치는 자신의 속도에 맞춰 버퍼에 저장된 데이터를 처리한다.

사실 우리는 일상생활에서 버퍼와 관련된 단어를 자주 사용하고 있다.
바로 버퍼링이다. 버퍼링은 우리가 인터넷에서 영상을 볼 때 일시적으로 데이터를 저장해서 네트워크 속도의 변동이나 일시적인 지연에도 영상을 끊김없이 볼 수 있도록 한다. 물론 해당 상황의 버퍼는 사용자 경험을 최적화하기 위한 것이긴 하다.

이렇듯 버퍼는 데이터 전송을 위한 공간이라는 의미가 있으므로 역할에 대한 의미를 뜻하기 위해 가운데 인자를 buf라고 칭하는 것이다.

그럼 이제 본론으로 돌아 와서 ft_putnbr_fd를 구현해보자!

해당 과제를 처음 접했을 때 내가 처음 write를 사용할 때는 int는 4바이트이니 인자 count 값을 4로 해서 출력하면 되겠지 했다.
아래가 그 예시다!

#include <unistd.h>

int main(void)
{
	int a = 10;
    write(1, &a, 4);
    return (0);
}

근데 이를 실행하면 줄바꿈 문자 하나만 나온다...
왜 그런가에 대해 알아보니 이는 이진 데이터가 터미널에 전달되면서 터미널은 해당 데이터를 문자로 해석하려고 하기 때문이다.

우선 10의 경우 little-endian 시스템에서는 메모리에 다음과 같이 저장된다.

0x0A 00 00 00 (16진수 표현)

이를 2진수로 표현하면:

00001010 00000000 00000000 00000000

여기서 잠깐!!! little-endian과 big-endian에 대해 알고가자!

  • little-endian : 데이터의 가장 낮은 바이트(least significant byte, LSB)를 메모리의 가장 낮은 주소부터 저장하는 방식이다. 예를 들어 32비트 정수 0x12345678을 little-endian 방식으로 메모리에 저장하면 메모리 배치는 다음과 같다.
메모리 주소:  [0x00] [0x01] [0x02] [0x03](16진수):  0x78   0x56   0x34   0x12
  • big-endian : 데이터의 가장 높은 바이트(most significant byte, MSB)를 메모리의 가장 낮은 주소부터 저장하는 방식이다. 같은 32비트 정수 0x12345678을 big-endian 방식으로 메모리에 저장하면 메모리 배치는 다음과 같다.
메모리 주소:  [0x00] [0x01] [0x02] [0x03](16진수):  0x12   0x34   0x56   0x78

이 두 방식의 장점과 단점, 사용 사례 및 적용은 아래 표에 정리해 두었다.

구분Little-endianBig-endian
장점- 편리한 데이터 접근: 낮은 주소부터 하위 바이트를 저장, 작은 크기의 데이터 타입 처리에 유리- 직관성: 메모리에서 숫자를 사람이 읽는 방식과 동일하게 볼 수 있어, 디버깅과 데이터 분석에서 직관적
- 산술 연산의 효율성: 작은 단위의 데이터를 더 자주 처리할 때 효율적, 하위 비트부터 연산하는 프로세서 구조와 잘 맞음- 네트워크 표준과의 일관성: 네트워크 바이트 순서가 big-endian을 사용, 데이터 전송 시 변환 작업이 필요 없음
단점- 호환성 문제: 네트워크 표준인 big-endian 방식과의 호환성 문제로 바이트 순서 변환 작업이 필요- 산술 연산의 비효율성: 하위 바이트부터 처리하는 연산에서 메모리의 높은 주소에서 시작, 추가적인 주소 계산이 필요할 수 있음
- 덜 직관적: 사람이 읽기에 덜 직관적, 메모리 덤프에서 데이터를 읽을 때 바이트 순서를 뒤집어야 해 해석이 어렵다- 편의성 부족: 하위 비트를 자주 사용하는 프로세서 구조에서 직관적이지 않고, 하위 비트를 읽거나 쓸 때 번거로울 수 있음
사용 사례 및 적용- 대부분의 현대적인 CPU 아키텍처, 특히 x86 계열(인텔, AMD 등)이 사용- IBM 메인프레임, 일부 네트워크 장비, RISC 아키텍처(예: PowerPC, SPARC 등)에서 사용

아무튼 다시 돌아와서

00001010 00000000 00000000 00000000

이 값은 write 함수가 출력하려고 하는 데이터이다.
그러면 각각의 ASCII 코드로 변환하면 줄바꿈, NULL, NULL, NULL이기에 줄바꿈만 나오는 것이다.

그러면 10을 출력하기 위해서는 어떻게 해야하는가????
문자로 '1'을 출력하고 '0'을 출력하면 된다...!

#include <unistd.h>

int main(void)
{
    char	tens_digit = '1';
    char	units_digit = '0';

    write(1, &tens_digit, 1);
    write(1, &units_digit, 1);
    return (0);
}

이를 활용해서 정수를 표현할 때는 재귀함수를 활용하기로 했다. 재귀함수의 가장 중요한 점은 종료 조건을 잘 주어야 한다! 이를 기반으로 아래와 같은 코드를 작성했다!
추가로 코드 밑에 예시를 들어 설명을 추가했으므로 같이 보면 이해가 더 잘 될 거 같다!

static void	positive(int n, int fd)
{
	char	d; //숫자 n의 1의 자리를 저장할 변수

	if (0 <= n && n < 10) //만약 n이 0이상 9이하이면
	{
		d = n + '0'; //n을 문자로 바꾸고
		write(fd, &d, 1); //출력하고
		return ; //함수를 종료한다.
	}
	positive(n / 10, fd); //위의 if 조건을 만족하지 않았을 경우에는 재귀적으로 호출한다.
	d = n % 10 + '0';
	write(fd, &d, 1);
}

static void	negative(int n, int fd)
{
	char	d;

	if (-10 < n && n <= 0)
	{
		d = -1 * n + '0';
		write(fd, &d, 1);
		return ;
	}
	negative(n / 10, fd);
	d = -1 * (n % 10) + '0';
	write(fd, &d, 1);
}

void	ft_putnbr_fd(int n, int fd)
{
	write(fd, "-", n < 0); //n이 음수이면 '-'을 출력 아니면 count가 0이므로 출력 안함
	if (n < 0)
		negative(n, fd); //음수일 때는 negative helper func 실행
	else
		positive(n, fd); //양수일 때는 positive helper func 실행
}

ft_putnbr_fd 함수 설명 및 실행 과정

주어진 코드는 정수를 파일 디스크립터(fd)에 출력하는 ft_putnbr_fd 함수와 이를 보조하는 positivenegative라는 두 개의 재귀 함수로 구성되어 있다.

아래에 123-123을 예로 들어 각 값이 어떻게 출력되는지 단계별로 설명하겠다!

1. 123ft_putnbr_fd에 전달될 때

ft_putnbr_fd(123, fd) 실행 흐름

  • Step 1: write(fd, "-", n < 0);
    • n < 0false(0)이므로 "-" 문자열은 출력되지 않는다.
  • Step 2: positive(123, fd) 호출
    • n이 양수이므로 positive(123, fd)를 호출한다.

positive(123, fd) 실행 흐름

  • Step 3: positive(123, fd)

    • 1230 <= n < 10 조건에 맞지 않으므로, 재귀적으로 positive(12, fd)를 호출한다.
  • Step 4: positive(12, fd)

    • 120 <= n < 10 조건에 맞지 않으므로, 재귀적으로 positive(1, fd)를 호출한다.
  • Step 5: positive(1, fd)

    • 10 <= n < 10 조건을 만족하므로, d = '1'으로 설정하고 write(fd, &d, 1);을 실행하여 1을 출력한다.
    • 함수는 여기서 종료되지 않고, 호출된 이전 스택 프레임으로 돌아가서 계속 실행된다.
  • Step 6: 돌아가서 positive(12, fd) 실행

    • d = n % 10 + '0';에서 d = '2'로 설정되고, write(fd, &d, 1);을 실행하여 2를 출력한다.
  • Step 7: 돌아가서 positive(123, fd) 실행

    • d = n % 10 + '0';에서 d = '3'로 설정되고, write(fd, &d, 1);을 실행하여 3을 출력한다.
  • 결과: 123이 순서대로 출력된다.

2. -123ft_putnbr_fd에 전달될 때

ft_putnbr_fd(-123, fd) 실행 흐름

  • Step 1: write(fd, "-", n < 0);
    • n < 0true(1)이므로 "-" 문자열이 출력된다.
  • Step 2: negative(-123, fd) 호출
    • n이 음수이므로 negative(-123, fd)를 호출한다.

negative(-123, fd) 실행 흐름

  • Step 3: negative(-123, fd)

    • -123-10 < n <= 0 조건에 맞지 않으므로, 재귀적으로 negative(-12, fd)를 호출한다.
  • Step 4: negative(-12, fd)

    • -12-10 < n <= 0 조건에 맞지 않으므로, 재귀적으로 negative(-1, fd)를 호출한다.
  • Step 5: negative(-1, fd)

    • -1-10 < n <= 0 조건을 만족하므로, d = '1'으로 설정하고 write(fd, &d, 1);을 실행하여 1을 출력한다.
    • 함수는 여기서 종료되지 않고, 호출된 이전 스택 프레임으로 돌아가서 계속 실행된다.
  • Step 6: 돌아가서 negative(-12, fd) 실행

    • d = -1 * (n % 10) + '0';에서 d = '2'로 설정되고, write(fd, &d, 1);을 실행하여 2를 출력한다.
  • Step 7: 돌아가서 negative(-123, fd) 실행

    • d = -1 * (n % 10) + '0';에서 d = '3'로 설정되고, write(fd, &d, 1);을 실행하여 3을 출력한다.
  • 결과: -123이 순서대로 출력된다.

profile
작은 문제를 하나하나 해결하며, 누군가의 하루에 선물이 되는 코드를 작성해 갑니다.

0개의 댓글