ft_printf?

int	ft_printf(const char *str, ...)

ft_printf, 이하 fp는 c의 printf함수를 구현해보는 과제이다.

ft_printf 과제에 필요한 개념들

  1. 가변인자
  2. 매크로 함수 / 인라인 함수
  3. Printf - Format
  4. Printf - Option
  5. Escape squence
  6. Parsing

가변인자(Variable Argument)

ft_printf의 매개변수 부분을 보면 명확한 변수 대신 ...이 들어가 있는 것을 볼 수 있다.
이는 변수의 개수와 타입이 정해져있지 않다는 의미이며, 이렇게 함수에 들어가는 인수가 변하는 것을 가변인자라고 한다.
C에는 가변 인자를 사용하기 위한 매크로들이 stdarg.h 헤더에 정의되어있으며, 아래와 같은 매크로들을 통해 가변인자를 호출 및 처리할 수 있다.
참고한 블로그

  • va_list
    typedef __builtin_va_list va_list;
    

    우선 가변 인자 사용에 기본이 되는 자료형인 va_list를 알아두어야한다.
    va_list가 내부적으로 char *형으로 선언되어있다는 글들도 보이지만, gcc 컴파일러 상에서의 va_list__builtin_va_list형으로 정의되어있다. 이처럼 C표준에 정확히 명시되어있는 자료형은 아니고 운영체제나 컴파일러에 따라 구현이 다를 수 있다.
    어쨌든 가변 인자를 사용할때는 va_list로 선언한 포인터 자료형으로 다루게 된다.

  • va_start
    #define va_start(ap, param) __builtin_va_start(ap, param)
    

    va_list형으로 선언된 변수 ap를 초기화하는 매크로 함수이다.
    이 매크로 함수 역시 gcc 컴파일러 내부적으로 정의되어있으나, 위 링크한 글을 참고하면

    #define __builtin_va_start(ap, param) ( ap = (va_list)_ADDRESSOF(param) + _INTSIZEOF(param) )
    

    과 유사하게 되어있을 것이라는걸 유추할 수 있다.
    _ADDRESSOF 매크로는 연산자 &와 같고, _INTSIZEOF 매크로는 받아오는 인자에 해당하는 자료형의 크기를 4의 배수로 올림해주는 역할을 한다. 즉 크기가 4바이트보다 작은 자료형(char, short 등)을 최소 4바이트로 맞추어주는데, 이는 스택 하나의 크기가 4바이트이기 때문에 이에 맞추모가 동시에 16비트, 32비트 등의 모든 환경에서도 동일하게 맞추어주기 위함이라고 한다.
    결국 ap는 고정인수 param의 시작주소에 해당 인수의 크기만큼 더해진 위치로 초기화되고, 이는 가변인수의 시작 주소가 된다.

  • va_arg
    #define va_arg(ap, type) __builtin_va_arg(ap, type)
    #define __builtin_va_arg(ap, type) ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )
    

    가변인수를 하나씩 넣어주는 매크로 함수이다.
    먼저 현재 포인터 ap의 값의 주소에 받고자 하는 인수의 크기만큼 더해주어 다음 인수를 받을 위치로 옮겨놓는다.
    이후 인수의 크기만큼 뺀 값을 인수의 자료형에 해당하는 포인터로 캐스팅한 값을 반환하여 사용한다.

  • va_copy
    #define va_copy(dest, src)  __builtin_va_copy(dest, src)
    

    복사하는 시점의 포인터 src에 저장되어있는 주소값을 dest에 복사한다.
    destsrc는 독립적으로 사용된다.

  • va_end
    #define va_end(ap) __builtin_va_end(ap)
    #define __builtin_va_end(ap) ( ap = (va_list)0 )
    

    포인터 ap0, 즉 NULL을 대입하여 안전하게 가변인수 사용과정을 종료한다.


매크로 / 인라인 함수

매크로 함수

매크로 함수란 #define으로 정의한 함수를 뜻한다.
#으로 시작하는 구문은 전처리기가 처리하기 때문에, 매크로 함수 역시 전처리기가 처리하게 된다.
전처리기는 컴파일러와는 달리 우선순위를 고려하지 않기 때문에 매크로 함수 사용시 괄호를 적절하게 사용하지 않으면 사용자가 원하는 결과가 나오지 않을 수 있다. 간단한 예시는 다음과 같다.

#define square(x) x * x

int	main()
{
	printf("%d, %d", square(5), square(5 + 1));
	return (0);
}

해당 코드의 실행결과는 25, 11, 0이다.
두번째 값이 36으로 출력되지 않은 이유는, 5 + 1이 우선순위 고려 없이 그대로 대입되었기 때문이다. 즉 5 + 1 * 5 + 1으로 대입되어 계산된 결과이다.

인라인 함수

참고한 글
매크로 함수에는 앞서 말한 단점이 있기 때문에, 함수를 전처리기가 아닌 컴파일러가 처리하도록 만든 것이 인라인 함수이다.
아래와 같이 함수 앞에 inline을 표기하면 인라인 함수가 된다.

inline int min(int x, int y)
{
	return x > y ? y : x;
}

인라인 함수와 일반 함수와의 차이점은 호출에 있다.
일반 함수는 호출될 때마다 해당 함수가 저장되어있는 메모리에 접근하고, 해당 메모리에 인자를 전달하여 return값을 함수를 호출했던 곳의 메모리로 가지고 오게 된다. 반면 인라인 함수는 호출과정 없이 해당 함수를 함수를 호출했던 곳으로 그대로 복사해온 후 그 내용을 수행하여 더 향상된 성능을 발휘한다.
따라서 함수 호출에 필요한 시간이 실제로 함수 코드를 실행하는데 필요한 시간보다 많은 작은 함수의 경우 인라인 함수로 사용하는 것이 유리하며, 이미 최신 컴파일러에서는 인라인으로 만들시 성능이 향상될 것 같은 함수들을 자동으로 인라인화하여 사용한다.
다만, 인라인 함수가 여러번 호출된다는 것은 함수가 여러번 복사된다는 것과 같은 의미이고, 따라서 메모리적으로 불리해질 수 있기에 모든 경우에서 유리한 것은 아니라고 한다. 그래서 보통은 단순한 함수에만 써먹는 듯 하다.

Printf - Format

% 뒤에 오는 서식 지정자는 입력되는 가변 인자를 어떻게 출력할 것인가를 결정한다.
자료형을 결정하는 것이 아니다. 자료형은 length 옵션에서 지정되지만, 따로 length를 설정하지 않았을 경우 각 서식 지정자에 설정된 기본 자료형으로 해석되기 때문에 그렇게 보일 뿐이라고 한다.

참고자료 링크

  설명  
d, i 부호 있는 10진 정수, 단 입력서식으로 사용할경우 사용법이 달라짐에 주의 Signed Decimal / Integer
u 부호 없는 10진 정수 Unsigned Decimal Integer
o 부호 없는 8진 정수 Unsigned Octal
x, X 부호 없는 16진수 정수(소문자 / 대문자) Unsigned Hexadeciaml Integer
f, F 소수점으로 표기된 실수(소문자 / 대문자) Decimal Floating Point
e, E 과학적 기수법으로 표기된 실수(소문자 / 대문자) Scientific Notation
g, G 숫자의 절대치가 precision의 자릿수를 넘는 경우 + 숫자의 절대값이 0.0001보다 작은 경우 e/E, 그 외의 경우 f/F형식 출력  
a, A 16진수로 표기된 실수(소문자 / 대문자) Hexadecimal Floating Point
c 문자 Character
s 문자열 String of Characters
p 포인터의 메모리 주소 Pointer Address
n %n 이전에 출력한 문자의 개수 Nothing printed
% % %

Decimal / Integer 차이

출력에서는 양쪽이 차이가 없지만 scanf에서 차이가 생긴다.

  • %d는 입력받는 숫자를 10진수로 인식한다.
  • %i는 입력받는 숫자의 형식에 따라 10진수(숫자 그대로), 8진수(숫자 앞에 0을 붙임), 16진수(숫자 앞에 0x를 붙임)으로 인식한다.

서식 n

변수의 포인터를 인자로 받아 서식이 나오기 전까지의 출력된 문자 개수를 변수의 값으로 넣어준다. 예시는 아래와 같다.

int	num;
printf("abcdefg%n\n", &num);
printf("%d", num);

출력결과
abcdefg
7

Printf - Option

flag

|플래그|설명| |:—|:—| | - | 왼쪽 정렬 | | + | 부호 출력 | | ` (공백) | 양수일 경우 부호를 공백으로 표시 | | # | 숫자 앞에 8진법일 경우 0, 16진법일 경우 0x 또는 0X를 붙임 </br> a A e E f F g G 서식과 같이 사용될경우 소수점 아래 수가 없어도 강제로 소수점을 붙임 | | 0` | 출력되는 전체 폭의 남는 공간을 0으로 채움 |

Width

printf("%5d\n", 123);
printf("%*", 8, 123);

출력 결과
  123
     123

출력할 문자열(다른 모든 옵션을 적용한 후)의 전체 폭을 설정하여 출력한다. 단, 폭이 문자열의 길이보다 클 경우에만 작동한다. 전체 폭에서 출력할 문자열을 제외한 나머지 부분은 0 플래그가 없는 경우 공백으로 채워진다.
*와 함께 사용될 경우 폭으로 사용할 수치를 인자로 받아서 사용한다.

Precision

printf("%.3s\n", "12345");
printf("%.7d\n", 12345);
printf("%.5f\n", 123.45);

출력결과
123
0012345
123.45000

출력할 문자열의 자릿수를 결정한다.

  • s : 정밀도가 출력할 문자열의 길이보다 작은 경우, 문자열의 앞에서부터 정밀도만큼만 출력한다.
  • d i o u x X : 정밀도가 출력할 숫자의 자릿수보다 큰 경우, 앞에 0을 붙여 숫자의 자릿수를 맞춰준다.
  • a A f F e E g G : 출력할 소수점 아래 자릿수를 지정한다. 반올림을 적용한다.

Length

자료의 형을 지정한다.

  d i u o x X a A f F e E g G c s p n
none int unsigned int double int char * void * int *
hh signed char unsigned char         signed char *
h short int unsigned short int         short int *
l long int unsigned long int   wint_t wchar_t *   long int *
ll long long int unsigned long long int         long long int *
j intmax_t uintmax_t         intmax_t *
z size_t size_t         size_t *
t ptrdiff_t ptrdiff_t         ptrdiff_t *
L     long double        

사용 예시는 아래와 같다.

char ch = 99;
printf("%3hhd", ch);

출력결과
 99

c -> int?

%c에서는 왜 char형이 아닌 int로 지정되어있을까?
결론은 성능 때문이라고 한다. 구조체의 바이트 패딩과 유사한 이유가 아닐까 싶다.


Escape Sequence(확장열)

특정 문자들은 특수하게 사용되기 때문에 일반적으로는 출력할 수 없고, Escape Character를 사용하여 출력해야한다.
C에서의 Escape Character\(백슬래시)이고, 이를 특정 문자들 앞에 붙여 Escape Sequence를 만들어서 쓰게 된다. Escape Sequence는 단일 문자또는 특정 기능으로 간주된다.

   
` 작은 따옴표
" 큰 따옴표
\? 물음표
\ 백슬래시
\a 경고음 발생
\b 백스페이스(공백)
\n 줄바꿈(개행)
\r 캐리지 리턴(커서의 위치를 앞으로 이동)
\t 수평 탭
\v 수직 탭
\f 폼 피드

Parsing(파싱)

파싱 = 구문 분석이라고 한다.
쉽게 말해, 주어진 데이터에 대해 내가 사용하고자 하는 방식을 적용하기 위한 형태로 분해하여 해석하는 행동을 의미하는 듯 하다. 즉,

  1. 파싱하는 데이터가 특정 문법에 알맞게 정리된 데이터인지 확인.
  2. 파싱하는 데이터를 목적에 맞게 이용하기 쉬운 형태로 구성.
    참고한 자료

printf에서도 주어진 문자열(데이터)를 출력하기 위해 문자열을 쪼개어가며 서식을 읽고, 플래그를 읽고, 해당 서식과 플래그에 맞추어 출력하는 과정을 거쳤기 때문에 기초적인 수준의 파싱을 연습해봤다고 보면 될 것 같다.