본문 바로가기
42Seoul/NetPractice

42서울 ft_printf 시작 전 공부 정리(printf직접 구현하기)

by 뜨거운 개발자 2023. 1. 9.
728x90
SMALL

구조체 값을 초기화 하는 방법 : ft_memset

printf함수가 write보다 늦게 나오는 이유

printf함수는 라인 버퍼링을 사용하기 때문이다.

line buffering 이란?

버퍼에 개행 문자가 입력될 때 마다 출력한다.

즉 개행문자가 들어오지 않는다면 버퍼에 계속 쌓아두다가 더이상 실행 할 명령이 없을 때 해당 버퍼의 내용을 출력한다.(과제에서는 실제 printf처럼 버퍼관리를 수행해서는 안된다고 한다.)

 

가변인자 목록(코딩도장)

42서울 ft_printf 시작 전 공부 정(printf직접 구현하기)

구조체 값을 초기화 하는 방법 : ft_memset

printf함수가 write보다 늦게 나오는 이유

printf함수는 라인 버퍼링을 사용하기 때문이다.

line buffering 이란?

버퍼에 개행 문자가 입력될 때 마다 출력한다.
즉 개행문자가 들어오지 않는다면 버퍼에 계속 쌓아두다가 더이상 실행 할 명령이 없을 때 해당 버퍼의 내용을 출력한다.(과제에서는 실제 printf처럼 버퍼관리를 수행해서는 안된다고 한다.)

가변인자 목록(코딩도장)

printf 또는 scanf같은 함수에서는 매개변수의 갯수가 정해지지 않은 함수이다.
함수에 들어가는 인자의 갯수가 변하는 것을 가변인자(가변인수)라고 한다.

  • 함수에서 가변인자 정의! 참조 코드
    #include <stdio.h>
    
    // args는 고정 매개변수
    void printNumbers(int args, ...)
    {
        printf("%d ", args);
    }
    
    int main()
    {
        printNumbers(1, 10);
        printNumbers(2, 10, 20);
        printNumbers(3, 10, 20, 30);
        printNumbers(4, 10, 20, 30, 40);
    
        return 0;
    }//출력값 1 2 3 4 args에 첫번째 인자가 들어갑니다.
    
    #include <stdio.h>
    #include <stdarg.h>    // va_list, va_start, va_arg, va_end가 정의된 헤더 파일
    
    void printNumbers(int args, ...)    // 가변 인자의 개수를 받음, ...로 가변 인자 설정
    {
        va_list ap;    // 가변 인자 목록 포인터
    
        va_start(ap, args);    // 가변 인자 목록 포인터 설정
        for (int i = 0; i < args; i++)    // 가변 인자 개수만큼 반복
        {
            int num = va_arg(ap, int);    // int 크기만큼 가변 인자 목록 포인터에서 값을 가져옴
                                          // ap를 int 크기만큼 순방향으로 이동
            printf("%d ", num);           // 가변 인자 값 출력
        }
        va_end(ap);    // 가변 인자 목록 포인터를 NULL로 초기화
    
        printf("\\n");    // 줄바꿈
    }
    
    int main()
    {
        printNumbers(1, 10);                // 인수 개수 1개
        printNumbers(2, 10, 20);            // 인수 개수 2개
        printNumbers(3, 10, 20, 30);        // 인수 개수 3개
        printNumbers(4, 10, 20, 30, 40);    // 인수 개수 4개
    
        return 0;
    }
    
    #include <stdarg.h>
  • 반환값자료형 함수이름(자료형 고정매개변수, ...) { }
  • C의 표준 라이브러리 헤더로 인자 수를 제한 없이 할 수 있도록 하는 함수를 허용하도록 한다.
  • 즉 가변인자함수를 가지고 자료형으로 va_list를 가지며 ,함수로 va_start, va_arg, va_end 등의 함수를 포함하고 있다.
  • 함수를 호출 할 때 맨처음에 인자 개수를 넣어자구 인자 개수에 맞게 인수를 콤마로 구분해 넣어준다.
  • 각각 자료형이 다른 가변인자 처리
  • void 함수명 (char *types, …) // 가변인자의 자료형을 받고 …으로 가변 인자를 설정한다.
    • va_start(가변인자 포인터, types); 가변인자 포인터 열에서 문자 갯수를 구해서 가변인자 포인터 설정.
  • switch와 가변인자를 함께 사용하면된다.(코드)
#include <stdio.h>
#include <stdarg.h>    // va_list, va_start, va_arg, va_end가 정의된 헤더 파일

void printValues(char *types, ...)    // 가변 인자의 자료형을 받음, ...로 가변 인자 설정
{
    va_list ap;    // 가변 인자 목록
    int i = 0;

    va_start(ap, types);        // types 문자열에서 문자 개수를 구해서 가변 인자 포인터 설정
    while (types[i] != '\\0')    // 가변 인자 자료형이 없을 때까지 반복
    {
        switch (types[i])       // 가변 인자 자료형으로 분기
        {
        case 'i':                           // int형일 때
            printf("%d ", va_arg(ap, int)); // int 크기만큼 값을 가져옴
                                           // ap를 int 크기만큼 순방향으로 이동
            break;
        case 'd':                                // double형일 때
            printf("%f ", va_arg(ap, double)); // double 크기만큼 값을 가져옴
                                       // ap를 double 크기만큼 순방향으로 이동
            break;
        case 'c':                                // char형 문자일 때
            printf("%c ", va_arg(ap, char));     // char 크기만큼 값을 가져옴
                                          // ap를 char 크기만큼 순방향으로 이동
            break;
        case 's':                                // char *형 문자열일 때
            printf("%s ", va_arg(ap, char *)); // char * 크기만큼 값을 가져옴
                                        // ap를 char * 크기만큼 순방향으로 이동
            break;
        default:
            break;
        }
        i++;
    }
    va_end(ap);    // 가변 인자 포인터를 NULL로 초기화

    printf("\\n");    // 줄바꿈
}

int main()
{
    printValues("i", 10);                               // 정수
    printValues("ci", 'a', 10);                         // 문자, 정수
    printValues("dci", 1.234567, 'a', 10);              // 실수, 문자, 정수
    printValues("sicd", "Hello, world!", 10, 'a', 1.234567);
// 문자열, 정수, 문자, 실수

    return 0;
}

매크로 함수란

#deifne 선행 처리 시지문에 인수로 함수의 정의를 전달 함으로써 함수처럼 동작하는 메크로이다.
#define SUB(X,Y) X-Y
#define PRT(X) printf("계산 결과는 %d입니다.\n", X)
이런식이다.

  1. 매크로 함수의 전체를 괄호(())로 감싸야 합니다.
  2. 매크로 함수의 인수들도 각각 괄호로 감싸야 합니다.
  3. 매크로 함수를 호출할 때에는 증감 연산자(++, --)나 복합 대입 연산자 등은 사용하지 않는 것이 좋습니다.

매크로 함수의 장점은 다음과 같습니다.

  1. 매크로 함수는 단순 치환만을 해주므로, 인수의 타입을 신경 쓰지 않습니다.
  2. 매크로 함수를 사용하면 여러 개의 명령문을 동시에 포함할 수 있습니다.
  3. 함수 호출에 의한 성능 저하가 일어나지 않으므로, 프로그램의 실행속도가 향상됩니다.

매크로 함수의 단점은 다음과 같습니다.

  1. 원하는 결과를 얻는 정확한 매크로 함수의 구현은 어려우며, 따라서 디버깅 또한 매우 어렵습니다.
  2. 매크로 함수의 크기가 증가하면 증가할수록 사용되는 괄호 또한 매우 많아져서 가독성이 떨어집니다.

Makefile 공부

  • make - C dir
  • Makefile을 계속 읽지 말고 우선은 dir로 이동하라는 것
  • 순환 make에 사용된다.

printf함수

1. format

printf는 첫번째 인수로 format을 받아서 뒤의 인수를 출력한다.
이런 format은 3가지 종류가 있다.

  • 표준 출력에 출력되는 일반 문자
  • 표준 출력에 변환되어 복사되는 문자 이스케이프 시퀀스(\b, \t 이런 애들…)
  • 다음 연속 인수의 인쇄를 일으키는 형식 지정.(%d, %s, %c 등으로 받는 인자, 가변인자를 받아야함.)

2. 인수의 규칙

첫 번째 이후의 인수는 대응하는 형식이 c,b s인 경우 문자열로 처리되며, 그렇지 않은 경우 아래 확장자를 가진 C의 상수로 간주된다.

  • 앞에 선행 플러스 기호 또는 마이너스 기호를 사용 할 수 있다. -숫자를 인자로 전달해야 할 때, 문자열로 전달한다. (itoa)
  • 선두 문자가 단일 따옴표 또는 이중 따옴표 인 경우 값은 다음 문자의 문자코드이다. 값은 다음 문자의 문자코드이다. “asdf”가 인자일 경우 a가 가리키는 포인터이고, c이면 c의 포인터를 전달한다.

3. 리턴 값

출력한 문자열의 총 길이를 리턴

4. 변환자.

printf에서 구현해야 하는 플래그는 cspdiuxX%이다.

  • %c : 인자의 맨 앞 1바이트가 출력된다.
  • %s : 문자열을 출력한다.
  • %p : 포인터의 주소를 출력한다.
  • %d : 10진수의 정수를 출력한다.
  • %i : 정수를 10진수로 출력한다.(%d와 같음)
  • %u : 부호없는 10진수 정수를 출력한다.
  • %x : 16진수로 변환해 출력한다.(10이상 숫자는 소문자)
  • %X :16진수로 변환해 출력한다. (10이상 숫자는 대문자.)
  • %% : %를 출력한다.

5. 허용함수

stdlib : malloc free wirte
stdarg : va_start, va_arg, va_copy, va_end

6. stdarg헤더에 대해

  • 가변인자 처리 메크로 모음
    • va_list : 가변 인자 목록. 가변 인자의 메모리 주소를 저장하는 포인터이다.
    • va_start : 가변 인자를 가져올 수 있도록 포인터를 설정한다.
    • va_arg :가변 인자 포인터에서 특정 자료형 크기만큼 값을 초기화 한다.
    • va_end : 가변 인자 처리가 끝났을 때 포인터를 NULL로 초기화 한다.
  • va_list 변수명; : 가변인자 목록 포인터를 선언.
  • va_start(가변인자 포인터, 가변인자 갯수) : 가변인자 목록 포인터를 설장하는 것.
  • va_arg(가변인자 포인터 ,자료형) : 가변인자 포인터에서 자료형의 크기만큼 역참조 해서 값을 가져오고 포인터를 자료형 만큼 순방향 이동시킨다.
  • va_end(가변인자 포인터) : 가변인자 포인터를 널로 초기화 해준다.

va_start

void va_start(va_list ap, last);

#define va_start(ap, pN)	\\
	((ap) = ((va_list) (&pN) + __va_argsiz(pN)))

va_arg ,va_end를 나중에 사용하기 위해 가장 먼저 호출해야만 한다.
ap인자를 초기 설정해주는 역활이다.(Argument pointer)
last 인자는 가변인자 목록 앞의 마지막인자의 이름이다.(—왜 마지막인지 모르겠지만, 목록앞의 마지막 인자의 이름이라고 한다. 즉 호출된 함수가 타임을 알고있는 마지막 인자라는데 printf의 첫번째 인자로 생각하면 될듯.)
이 인자의 주소는 va_start메크로에서 사용되므로 함수,배열 등으로 선언하면 안된다.
last의 역활 : 첫번째 가변인자의 주소를 알기위해 고정인수가 필요한데, va_list 주소값에다가 고정인수의 크기를 더한 위치로 ap를 초기화한다.
마지막 고정인수가 char형 포인텅인 경우 fmt의 시작포인터로부터 +8byte한 곳에 ap가 위치한다.
왜 이게 last라고 불리는지 궁금하다면 여러가지 고정인자가 있고 가변인자가 있는 경우를 생각해보면 된다.
만약 함수가 (int a, int b ,int c, …) 이렇게 정의가 됐다면 우리는 이 고정인자의 어디부터 가변인자 리스트에 넣을 것인지를 찾아보는 것이다.

int test(int *ar, int a, ...)
{
va_list ap;
va_start(ap, ar);
}

위의 코드를 돌려보려면 컴파일러가 오류를 뿜는다 가변인자 전의 마지막 인자를 꼭 해달라고 한다.

va_arg

type va_arg(va_list ap, type);

#define va_arg(ap, t)					\\
	 (((ap) = (ap) + __va_argsiz(t)),		\\
	  *((t*) (void*) ((ap) - __va_argsiz(t))))

인수 ap 는 va_start로 초기화가 된 변수이다.
va_arg를 호출할때마다 다음 인수가를 반환하도록 한다. 즉 read함수와 비슷하게 뒤로 밀어주는 것이다.
인수 type는 type의 크기만큼 포인터를 전진시킨다. 그리고 받은 인자 타입의 포인터를 리턴해준다.

!!!! 중요 !!!

  • 다음 인수가 없거나 타입이 다음인수의 타입과 호환되지 않으면 랜덤한 오류가 발생한다.
  • va_arg를 사용하는 함수에 ap가 전달되면 해당 함수가 반환된 후의 ap값은 정의되지 않는다.

va_arg 함수 주의 할 특징

visual stdio의 경우 va_arg(ap ,char)처럼 사용이 가능하다.
단, gcc에서는 char 형 문자일 때 va_arg메크로에 char형 대신에 int를 사용해야만 한다.
char, bool,short → int로
float →double로 바뀐다.

  • 주의할 점 : va로 float을 넘기게 되면 넘어갈 때 double형태로 넘어가게 된다. (short, char 등도 int로 넘어간다.)
  • 따라서 va_arg(va_list list, type_t) 에서 type는 float이 될 수 없다.
  • 다만 function_call 시 float을 인자로 넘긴다면, va_arg(list,double)double로 지정하더라도 할당 받는 변수의 형태에 따라서 자동으로 float으로 변환이 된다. 할당 받는 변수가 float이면 float으로 double이면 double로 처리된다.
  • int 미만의 데이터 형은 int로 승격된다
  • va_end
  • void va_end(va_list ap);
#undef va_end
void va_end (__gnuc_va_list);		/* Defined in libgcc.a */
# define va_end(AP)	((void)0)

va_list 가 무엇인가?

일단 va_list의 정의를 찾아보면

#ifndef _VA_LIST
typedef __builtin_va_list va_list;
#define _VA_LIST

이렇게 되어있다.
그래서 __builtin_va_list를 검색해 봤는데 일단 코드에 추가된 파일이 아니라 gcc내부에서 알아서 정의해주고 컴파일러의 종류에 따라서 다르게 구현이 되어있다고 한다.
va_list형 변수가 va_arg를 사용해서 이동을 하면 정말 주소값이 이동을 하는건지 궁금했지만 그 구현이 어떻게 되어있는지 알 수가 없었다.
그래서 va_list형 변수를 찍어보면서 va_arg작동 방식이 주소값을 이동시키면서 보는건지 봤는데 그 테스트 결과

va_list ap;
va_start(ap, ar);
for (int i =0; i < 12; i++)
{
	printf("%p\\\\n",ap);
	//printf("ar:::::: %d,%p\\\\n",*ar ,ar);
	printf("%d\\\\n", va_arg(ap,int));
	//ap+= sizeof(int);
}
va_end(ap);

이렇게 코드를 짜보면서 테스트를 해봤는데 ap주소값의 변화는 없었다.
그래서 따로 주소값이 이동하는게 아니라 피벗값을 줘서 그 피벗값을 이동시키는 것이라는걸 유추해 볼 수 있다.
나는 그래서 ap의 주소값을 이동시키는 방법을 사용해서 과연 이동 후 에는 va_arg를 사용했을 때 만약 한칸 건너 뛴 값이 나온다면 나는 테스트가 나의 유추와 맞다고 생각할 수 있었다.
하지만 va_list자체적으로 이 주소값은 const형으로 선언됐는지 변경이 불가능했고 그래서 뒤로 밀어볼 수가 없었다. 하지만 제법 많은걸 알게됐다. 일단 여기까지가 내가 찾아본 부분이다.

이상한 테스트 :

int max값보다 큰 길이의 문자열이 들어올때

참고 링크

코딩도장 가변인자목록
stdarg헤더에 대해 (블로그 정리)
팔만 코딩경 참조

728x90
BIG