Search
Duplicate
🖨️

ft_printf

Holy Graph
1Circle
간략한 내용
C언어에서 사용하던 printf를 구현
적정 기간
1 week
제작에 참여한 사람
진행 중인 사람
최종 편집일
May 27
통과한 사람
1 more property

Subject

들어가기 앞서...

프엪 무섭다 ㅜㅜ
Jose is IU → 산호세 가고 싶다
하기 너무 귀찮다...

0. 프로젝트 개요

이번 프로젝트는 stdio.h 기본 라이브러리에 포함되어 있는 printf 함수의 일부 기능들을 직접 구현하는 것이다.
printffformatted의 약자이며, 서식화된 출력을 지원한다는 의미이다!
printf의 많은 필드 중에서 flag 필드의 -, 0, ., * 그리고 width.precision 필드, type 필드의 c, s, p, d, i, u, x, X, %만 구현 하면된다.
실제 printf의 함수와 비교하면서 구현한다.

1. Printf Format Placeholder

printf 함수의 인자로 주어지는 일반적인 문자열은 출력 스트림에 그대로 전달되어 출력을 하게 되지만, %로 시작하는 Format Tag(형식 태그)는 추가 인자를 받아 출력 스트림에 어떻게 출력해야 할지 가이드를 제공한다. 형식 태그%Format Specifier (서식 지정자)로 구성되어 서식 지정자에 따라 다양한 해석 방식을 나타낸다.
아래 예시를 살펴보면, %를 제외한 문자열들은 그대로 출력된다.
pritnf("Color %s, Number %d, Float %4.2f", "red", 123456, 3.14);
C
복사

1) Printf Format Placeholder Syntax

printf 함수의 Format Placeholder Syntax(서식 표기 구문)%로 시작하는 형식 태그로 표기 되며, 그 구문은 아래와 같다.
%[parameter][flags][width][.precision][length]type
C
복사
% 뒤에 나타나는 서식 지정자는 parameter, flags 등 필드에 대한 다양한 옵션을 받고 type을 받는 형태이다. 아래에 필드에 대한 옵션들의 설명을 확인하자.

2) parameter (optional)

parameter 필드는 POSIX에서의 C 언어에서만 이용 가능하고, C99에서의 C 언어에서는 이용이 불가능한 필드이다. 필드에 주어진 옵션 값을 통해 함수 호출에 넘겨 받은 인자들 중 문자열을 제외한 매개 변수를 조종할 수 있다. parameter의 값은 1부터 이용할 수 있다. 0부터 이용하려고 하면 아래와 같은 오류가 발생한다.
Search
[option]
Character
Description
nn은 숫자를 의미하며, 대체할 매개 변수의 번호를 의미한다. 제공된 매개 변수를 n$꼴의 형태로 나타내어, 매개 변수를 다른 순서로 여러 번 출력할 수 있다.
아래의 n$를 사용한 예시를 보고 이해해보자.
#include <stdio.h> int main(void) { printf("%2$d %2$#x; %1$d %1$#x", 16, 17); return (0); }
C
복사
코드를 실행시키면 위와 같은 결과가 나온다.
우리는 매개 변수를 2개만 받았음에도 4개의 출력을 만들어 낼 수 있다. 위의 코드를 살펴보면 앞 부분의 %2$d에서 $라는 옵션을 이용해서 2번째 매개 변수를 받아오겠다고 표시한 것이다. 그래서 우리는 매개 변수를 16, 17의 형태로 받았지만, 나중에 받은 매개 변수를 먼저 사용하듯 순서에 상관없이 이 변수들을 사용할 수 있는 것이다. 또한 할당된 매개 변수에 대해 중복사용도 가능하다. n$ 옵션을 이용하여 출력 할 때 사용하지 않은 매개 변수가 있어서 안 된다는 점을 유의해야 한다.

3) flags (optional)

다양한 필드 중 가장 앞에 나오는 flags(parameter 필드는 POSIX에서만 동작하므로 대체적으로 flags 필드가 가장 앞에 나온다고 본다.) 출력의 전체 방향을 설정한다. 서식 지정자flags 단독으로 사용되는 경우도 있고 다른 필드와 함께 사용되는 경우도 있는데, 다른 필드와 함께 사용되는 경우 flags의 옵션이 영향을 받는 경우가 있다. 또한 flags 필드에는 여러 옵션을 동시에 사용할 수 있다.
Search
Character
Description
Default
좌측 정렬을 수행한다.
우측 정렬
양수일 떄는 +, 음수일 때는 - 표시한다.
음수만 부호표시
양수일 때는 부호를 생략하고 공백으로 표시한다. 단, 음수일 때는 -를 표시한다.
width 필드에 주어진 옵션자리에 맞춰 빈 자리에 0을 추가하고, - (minus)와 사용될 경우 좌측 정렬을 하기 때문에 무시하게 된다. type 필드의 수를 표기하는 d, i, o, u, x, X를 사용할 때 .preicison 필드도 함께 이용되면, 주어진 옵션만큼 빈 자리를 0으로 채운다.
정수와 지수에 천 단위 구분자를 표시한다. ex) 1,000
각 진법과 형식에 맞게 0, 0x, 0X를 추가한다. .precision 필드의 e, E, f, a, A 옵션과 사용될 경우 소수점에 영향을 준다. .preicison 필드의 g, G 옵션도 마찬가지로 소수점에 영향을 주고, 0이 잘리는 현상을 방지한다. type 필드의 c, s, d, i, u옵션과 사용되면 #를 무시한다.

4) width (optional)

width 필드는 단어 뜻 그대로 출력 너비를 설정한다. .precision 필드의 옵션과 사용되면 옵션 값을 고려하여 너비 값을 적용한다. (.precision에서 자세히 살펴보자.)
Search
Character
Description
nn은 숫자를 의미하며, 양수만 사용 가능하다. 음수를 사용하면 부호 -flags 필드의 - (minus)로 인식한다.
너비 값을 인자로 받아서 사용한다. * (asterisk)에 할당될 인자로 받은 값보다 서식 지정자에 따라 대치될 인자로 더 긴 값이 들어오면 원래 길이만큼 출력한다.
옵션으로 입력한 숫자만큼 너비를 가진다. * 기호를 사용하면 인자로 너비를 주어야 한다. 아래의 두 코드는 같은 출력을 나타낸다.
printf("%*d", 5, 42); printf("%5d", 42);
C
복사

너비 값 < 주어진 값의 길이

#include <stdio.h> int main(void) { printf("%*d", 2, 100); return (0); }
C
복사
위 코드는 주어진 너비 값 2를 무시하고 원래 값 100을 출력한다.

너비 값 ≥ 주어진 값의 길이

#include <stdio.h> int main(void) { printf("%*d", 2, 1); return (0); }
C
복사
반면 위 코드는 너비 값에 맞춰 우측 정렬을 수행한다.

5) .precision (optional)

.precision은 출력하는 값의 정확도 표기를 위한 필드이다. 너비를 나타내는 width 필드 다음으로 나타나는 필드이기 때문에 구분을 위해서 .은 필수적으로 요구된다. 단독으로 . 이 이용되기도 하며 .숫자를 조합하여 사용되기도 하는데, 각 경우에는 바로 뒤에 이어지는 type 필드의 옵션에 따라서 출력이 바뀐다. (위에서 밝힌 것과 같이 width 필드의 값에도 영향을 받기 때문에, width로 주어진 값과 .precision에 주어진 값을 유의해야하고, .precision 뒤에 주어진 type 필드의 옵션이 무엇인지에도 유의해야 한다.)
Search
구분
Character
Description
type
단독으로 .만 사용된 경우를 말한다. 아래는 단독 사용된 . 이후에 이어지는 type 옵션에 따른 출력 설명이다.
c, p
정확도를 무시하고, type 옵션에 따른 값을 출력한다.
d, i, o, u, x, X
flags 필드로 0이 주어지면, 옵션 0을 무시하고 처리한다.
f, e, E, g, G, a, A
소수점 아래를 출력하지 않는다. 마지막 숫자는 반올림하여 처리한다. (실수 출력을 위한 type 필드 옵션 값의 정확도를 주지 않은 Default 역할은 소수점 이하 6자리까지 출력이다.)
s
문자열을 출력하지 않는다.
*s
출력할 최대 길이를 인자로 넘겨 받아, 해당 길이만큼 문자열을 출력한다.
type
nn은 숫자를 의미하며, 양수만 사용할 수 있다. (음수를 사용하면 width의 옵션 값을 무시하고, .precision의 값으로 들어온 음수 nn만큼을 width의 옵션 값으로 인식한다. width에서의 음수 처리 방식을 따르므로 좌측 정렬을 수행한다.)
c
정확도를 무시하고, type 옵션에 띠른 값을 출력한다.
d, i, o, u, x, X
출력할 최대 자릿수를 지정하게 된다. 자릿수에서 출력할 값의 길이를 뺀 남은 공간은 기본적으로 0으로 출력하게 된다. 정수 값이 0으로 들어왔는데 nn0이라면 출력 값이 없다.
f, e, E, g, G, a, A
출력할 소수점 자릿수를 지정하게 된다. 출력할 자리 직후의 숫자를 반올림하여 자릿수 내에 출력할 수 있도록 한다. 실수 값으로 0.0이 들어왔는데 nn0이라면 0을 출력한다.
s
문자열에서 출력할 최대 길이를 지정하게 된다. 설정한 길이가 문자열보다 크다면 원래 길이의 문자열을 출력한다.
p
정확도를 무시하고, type 옵션에 따른 값을 출력한다.
COUNT12

.이 단독으로 사용되어 type을 받은 예시

c, p
d, i, o, u, x, X
f, e, E, g, G, a, A
s, *s

.에 숫자를 명시하여 type을 받은 예시

c, p
d, i, o, u, x, X
f, e, E, g, G, a, A
s

6) length (optional)

length 필드의 경우 type 필드 앞에 붙어 서식에 대한 자료의 형을 지정하도록 하고, 지정한 자료의 형을 통해 인자에 대한 적절한 형 변환을 수행할 수 있도록 해준다.
Search
Character
d i
u o x X
f F e E g G a A
c
s
p
n
int
unsigned int
double
int
char *
void *
int *
signed char
unsigned char
signed char *
short int
unsigned short int
short int *
long int
unsigned long int
wint_t
wchar_t *
long int *
long long int
unsigned long long int
long long int *
intmax_t
uintmax_t
intmax_t *
size_t
size_t
size_t *
ptrdiff_t
ptrdiff_t
ptrdiff_t *
long double
COUNT9
위의 표에 주어진 것과 같이 type 필드 앞에 사용된 옵션에 따라서 받을 수 있는 인자 타입 (해석되는 타입)이 달라진다. 아래의 예를 살펴보자.
type 필드의 d 옵션 앞에 hh라는 length 옵션이 붙으면 signed char로 인식하게 된다. 하지만 signed char 범위 내에 있는 값을 printf가변 인자에 넣어주면 아래와 같이 경고가 발생하는 것을 알 수 있다. (컴파일 시 경고 허용을 하지 않으면 컴파일이 되지 않는다.)
이는 오류가 발생한 부분의 리터럴컴파일러가 자동으로 타입을 추측하여 int로 해석하고, intsigned char에 할당하려 해서 생기는 경고이다. 따라서 아래 코드를 이용하면 정상적으로 출력 값을 확인할 수 있다.
#include <stdio.h> int main(void) { char ch; ch = 99; printf("%3.2hhd", ch); return (0); }
C
복사
length의 나머지 옵션들 역시 위와 같이 작용하게 된다. 주어진 각 타입에 대해 이해가 안 가는 것이 있다면 별도로 찾아보도록 하자. (intmax_t, uintmax_t, ptrdiff_t 등)

7) type (required)

형식 문자열에는 일반 문자열을 포함하여 여러 형식 태그들로 이뤄져 있으며, 형식 태그%서식 지정자로 구성된다. %를 제외한 나머지 필드들을 모두 포함하여 서식 지정자라고 하는데, type 필드는 %와 더불어 형식 태그에 없어서는 안 되는 필드이므로 type 필드를 서식 지정자로 보기도 한다.
printf로 출력할 어떤 값이 주어졌을 때, type 필드에 주어진 옵션에 따라서 그 값을 어떻게 해석할지 달라진다.
정수를 출력할 때는 %d로, 실수를 출력할 때는 %f로 출력을 했던 기억 덕에 type 필드는 자료의 타입과 관련이 있다고 생각하는 경우가 많다. 하지만 length 필드에 대한 설명을 통해서 알 수 있듯이 자료의 타입length 필드와 관련이 있다. length 필드를 비워두고 %d, %f를 이용했음에도 정수, 실수를 타입에 따라 제대로 출력할 수 있는 이유는 type 필드 앞에 length 옵션이 주어지지 않을 경우에 각 type 옵션에 대한 기본 설정 값으로 그 형식을 해석하기 때문이다. 이를 통해 알 수 있는 것은 type 필드는 자료의 타입에 대한 해석이 아니라 출력 형식에 대한 해석을 수행한다는 것이다.
예를 들어, 10이라는 값을 10진수로 해석하면 10, 8진수로 해석하면 012, 16진수로 해석하면 0xA와 같은데 이와 같이 해석하여 출력하는 것을 printftype 필드가 도와준다.
Search
Character
Description
Abbreviation
부호 있는 10진 정수로 해석
Signed Decimal Integer
부호 있는 10진 정수로 해석
Signed Decimal Integer
부호 없는 10진 정수로 해석
Signed Decimal Integer
부호 없는 8진 정수로 해석
Unsigned Octal
부호 없는 16진 정수로 해석 (소문자)
Unsigned Hexadecimal Integer (Lowercase)
부호 없는 16진 정수로 해석(대문자)
Unsigned Hexadecimal Integer (Uppercase)
실수를 10진수 표기법을 사용하여 해석 (소문자)
Decimal Floating Point (Lowercase)
실수를 10진수 표기법을 사용하여 해석 (대문자)
Decimal Floating Point (Uppercase)
실수를 과학적 기수법을 사용하여 해석 (소문자)
Scientific Notation (Lowercase)
실수를 과학적 기수법을 사용하여 해석 (대문자)
Scientific Notation (Uppercase)
f와 e 중에서 짧은 것을 사용 (소문자)
F와 E 중에서 짧은 것을 사용 (대문자)
실수를 16진법으로 표기 (소문자)
Hexadecimal Floating Point (Lowercase)
실수를 16진법으로 표기 (대문자)
Hexadecimal Floating Point (Uppercase)
정수를 문자로 해석
Character
정수를 문자열로 해석
String of Characters
포인터참조하고 있는 메모리의 주소 값 (8자리의 16진수)
Pointer Address
해당 옵션 전까지 printf를 이용하여 출력한 문자의 수를 인자로 받은 변수에 할당
Nothing Printed
%에 뒤이은 %를 이용하여 출력 스트림% 기호를 씀
%

d 옵션과 i 옵션의 차이

d 옵션과 i 옵션은 모두 Signed Decimal Integer로 부호있는 10진 정수로 해석한다. 두 옵션에 대해선 출력에서는 차이가 없지만 입력에서 차이가 있다.
d 옵션은 부호가 있는 10진 정수를 입력으로 받지만, i 옵션은 부호가 있는 8진, 10진, 16진 정수를 입력으로 받을 수 있다. 단, i 옵션으로 입력을 받을 때는 특정 형식을 맞춰야만 printf 함수 내에서 이를 해석할 수 있다. 아래 코드를 살펴보자.
#include <stdio.h> int main(void) { printf("%d\n", -42); // 10진수 printf("%i\n", -052); // 8진수 printf("%i\n", -0x2a); // 16진수 printf("%i\n", -0x2A); // 16진수 return (0); }
C
복사

f 옵션과 F 옵션의 차이

f 옵션과 F 옵션은 할당된 값을 10진수 표기법을 사용한 실수로 해석한다. 이에 대해 어차피 숫자로만 해석하게 되어 있는데 옵션에 대문자 소문자 구분을 둘 필요가 있는지 의문이 들 수 있다.
#include <stdio.h> int main(void) { printf("val : %f\n", -123.456); printf("val : %F\n", -123.456); return (0); }
C
복사
위 코드를 살펴보면 대문자와 소문자 옵션 구분 없이 동일한 값이 출력되는 것을 볼 수 있는데, 그렇다면 실수에 대한 10진수 표기를 어째서 fF로 구분을 해두었을까?
Linux Manual에 따르면 아래와 같이 두 옵션에 대한 차이를 설명해둔 것을 볼 수 있다.
SUSv2 does not know about F and says that character string representations for infinity and NaN may be made available. SUSv3 adds a specification for F. The C99 standard specifies "[-]inf" or "[-]infinity" for infinity, and a string starting with "nan" for NaN, in the case of f conversion, and "[-]INF" or "[-]INFINITY" or "NAN" in the case of F conversion.
즉, 표현 가능한 범위 밖의 수들을 표현 가능한 수들로 정의하여 해당 값을 출력하려 할 때 알파벳으로 출력하기 때문에 옵션에 대소문자 차이를 둔 것이라고 볼 수 있다. 각 옵션을 이용했을 때는 아래와 같이 출력되며, 대괄호의 의미는 Optional하다는 의미이다.
f 옵션 : []inf[-] inf, []infinity[-] infinity, nannan
F 옵션 : []INF[-]INF, []INFINITY[-]INFINITY, NANNAN
하지만 비주얼 스튜디오와 Dev C++에서는 type 필드의 F 옵션을 허용하지 않는다고 한다.
표현 가능 범위 밖의 수들 표현 가능 범위 밖의 수들은 3가지가 있다. 1. Denormalized Number 2. Infinity 3. NaN (Not a Number) → NaN 체크에 대해 자세히 알고싶다면 여기에서 확인하자. 각 수들이 왜 존재하는지, 어떻게 지수부와 가수부를 구성하여 메모리에 올라가는지 궁금하다면 아래 링크의 3번 항목에서 확인할 수 있다. → float과 double의 소수점 표현
과학적 기수법 (Scientific Notation)이란? 과학적 기수법이라고 하는 Scientific Notation지수 표기법 (Exponent Notation), 가수 표기법 (Mantissa Notation)과 동의어이다. 10의 배수로 많은 0을 표기 해야하는 경우, 그 표기가 까다롭기 때문에 지수 (e)를 사용하여 표기하는 방식을 말한다. ex) 100,000=1×105=1.0e+05100,000 = 1 \times 10^5 = 1.0e+05 → 과학적 기수법에 대해 자세히 알고 싶다면 여기를 눌러보자.

n 옵션

n 옵션의 경우 여러 옵션 들 중에 가장 특이하게 동작한다. n 옵션은 n 옵션이 적용되기 전까지의 출력된 문자 길이를 측정하여 가변 인자에 주어진 변수에 할당하게 된다. 즉, 일반적으로 출력에 사용되는 옵션이 아니기 때문에 다른 옵션들과 차이가 있음을 알 수 있다. 아래의 코드를 살펴보자.
n 옵션의 기본 할당 타입은 int *이므로 int 변수의 주소를 넘긴 것을 확인할 수 있다. 만일 다른 타입의 변수의 주소를 넘겨 할당 받고 싶다면, 해석시키고 싶은 length의 옵션을 n 옵션 앞에 표기하도록 한다.
#include <stdio.h> int main(void) { char len; printf("hello init 6!%hhn\n", &len); printf("length : %hhd", len); return (0); }
C
복사

% 기호 출력

형식 문자열을 이용할 때는 % 기호를 이용하여 형식 태그의 시작임을 알리게 된다. 따라서 %는 일반적인 문자가 아니기 때문에, 단순히 printf를 이용하여 기호를 출력하려 하면 아래와 같이 경고 문구를 띄우면서 컴파일이 되지 않는 것을 볼 수 있다.
일반적으로 특수한 문자들은 Escape Sequence라고 하여 \를 이용하여 출력하곤 하는데, % 기호도 \를 이용하여 출력할 수 있는지 확인해보자. 아래 그림을 보면 여전히 경고 문구를 띄우면서 컴파일을 막는 모습을 확인할 수 있다.
% 기호를 출력하기 위해선 \로는 출력할 수 없고 오로지 % 기호를 이용해서만 출력할수 있다. % 기호 출력을 위해 % 기호를 이용한다는 것은 % 기호 앞에도 여러 필드들의 다양한 옵션을 사용할 수 있다는 것을 의미한다.
아래와 같이 flag의 옵션으로 0을 주고 width의 옵션으로 5라는 값을 주면 5자리의 너비로 %를 출력하고 빈 자리는 0을 채우는 것을 확인할 수 있다.
#include <stdio.h> int main(void) { printf("%05%"); return (0); }
C
복사
그렇다면 잠깐 소개되었던 \로 출력할 수 있는 문자들은 어떤 문자들이 있을까? 다음 항목에서 Escape Sequence에 대해 알아보자.

8) Escape sequence (Not a Field, String of Characters)

일반적으로 특수한 문자들은 Escape Sequence라고 하여 \를 이용하여 출력하게 된다. \를 포함한 문자는 printf 함수를 이용하여 출력할 때 특정 문자를 지칭하거나 특정 기능을 수행하게 된다.
Search
Character
Description
Representation
작은 따옴표
'
큰 따옴표
"
물음표
?
백 슬래시
\
경고음 발생
Beep Sound
백 스페이스 (공백 문자)
Backspace
줄 바꿈 (공백 문자)
Newline
캐리지 리턴 (공백 문자)
Carriage Return
수평 탭 (공백 문자)
Horizontal Tab
수직 탭 (공백 문자)
Vertical Tab
폼 피드 (공백 문자)
Form Feed

2. Variable Argument

1) Variable Argument (가변 인자)란?

C 언어에서 함수를 사용하다보면 매개 변수의 개수가 정해져있지 않은 함수가 있다. 위에서 배운 printf도 상황에 따라 원하는 만큼의 인자를 받아서 사용할 수 있다. 이렇게 상황에 따라 함수에 인자의 개수가 다르게 할당되어도 처리할 수 있게 해주는 것이 가변 인자이다.
매크로 함수란? 일반 함수와는 달리 단순 텍스트 치환만을 해준다. 따라서 여러 명령문을 동시에 포함할 수 있다. 함수 호출 구문과 비슷하게 작성하더라도 실제로는 함수 호출을 하지 않으므로 함수 호출 구문과 비교했을 때는 성능이 조금 더 좋은 편이다. 하지만 매크로 함수에서는 인자의 타입을 고려하지 않기 때문에 의도하지 않은 여러 오류가 발생할 수도 있어 주의하여 사용해야 한다. → 매크로 함수에 대해 자세히 알고 싶다면 여기를 눌러 확인해보자. → 매크로 함수 vs 인라인 함수에 대해 궁금하다면 여기를 눌러 확인해보자.

2) Variadic Function Format

간단한 예로 가변 인자를 가지는 함수 원형과 호출 방법은 아래와 같다.
#include <stdarg.h> void function(int args, ...) { //fuction body } int main(void) { function(42, /* 원하는 만큼 인자 입력 */); return (0); }
C
복사
가변 인자를 포함한 함수에는 두 종류의 인자가 요구된다.
필수 인자
선택적 인자
여기서 말한 선택적 인자가변 인자인데, 가변 인자를 받기 위해선 사전에 필수 인자가 무조건 요구된다. 선택적 인자가변 인자는 말 그대로 인자의 수가 정해져 있지 않기 때문에 선택적 인자를 먼저 받게 되면 함수의 원형에서 어느 인자를 필수 인자로 받은 것인지 알 수 없기 때문에, 필수 인자를 먼저 받은 다음 선택적 인자를 받는 것을 원칙으로 한다.
선택적 인자...으료 표시하며 위에서 밝힌 것처럼 필수 인자선택적 인자의 순서가 중요하기 때문에 선택적 인자 뒤에는 다른 매개 변수를 둘 수 없다. 또한 필수 인자를 반드시 필요로 한다. 아래 함수의 원형에서 arg가 필수 인자를 의미한다.
T function(T args, ...);
C
복사
pritnf 함수의 경우를 살펴보자.
printf("i'm %d years old.\n you are %d years old.\n", age1, age2);
C
복사
이 경우에 ""(Double Qoute)로 묶여진 문자열 부분이 필수 인자가 되는 것이고 age1 이후 age2까지가 바로 선택적 인자가 되는 것이다. 일반적으로 필수 인자로 주어지는 인자들 중 하나는 선택적 인자를 몇 개 받았는지 명시하여 선택적 인자들에서 정지 신호를 찾을 수 있게 해준다.
가변 인자를 포함한 함수를 이용하기 전에 매크로에 대해 먼저 알아보자.

3) C Variadic Macros

C 언어에서 가변 인자를 가지는 함수를 사용하기 위해서 아래의 매크로를 활용한다.
va_list // 가변 인자 목록 va_start // 가변 인자를 가져올 수 있도록 가변 인자 시작 주소 참조하는 포인터 설정 va_arg // 가변 인자를 참조하는 포인터를 통해 역참조 후, 해당 데이터의 크기만큼 밀어 다음 인자를 참조 va_end // 가변 인자를 모두 처리 후, 가변인자를 가리키는 포인터를 NULL로 초기화 va_copy // 가변 인자 목록을 복사
C
복사
위의 매크로들은 stdarg.h에 정의되어 있기 때문에 헤더를 추가해줘야 이용할 수 있다.
이전에는 varargs.h가 사용되었지만 더이상 사용을 권장하지 않는다. stdarg.hC89 표준부터 추가된 헤더 파일이다. C++에서는 cstdarg 헤더 파일이 사용된다.
#include <stdarg.h>
C
복사
매크로를 활용한 함수를 보면서 가변 인자를 처리하는 과정을 알아보자! 아래에 작성된 sumAll이라는 함수는 각 인자로 들어온 정수를 모두 더해서 결과를 출력하는 함수이다.
#include <stdio.h> #include <stdarg.h> void sumAll(int args, ...) { int result; int tmp; // Part 1 va_list ap; result = 0; // Part 2 va_start(ap, args); for (int i = 0 ; i < args ; i++) { // Part 3 tmp = va_arg(ap, int); result += tmp; printf("%d", tmp); if (i == args - 1) break; printf(" + "); } printf(" = %d\n", result); // Part 4 va_end(ap); } int main(void) { sumAll(1, 10); sumAll(2, 10, 20); sumAll(3, 10, 20, 30); sumAll(4, 10, 20, 30, 40); sumAll(5, 10, 20, 30, 40, 50); return (0); }
C
복사

Part 1: va_list

va_list ap;
C
복사
가변 인자 목록를 의미하며, 가변 인자의 주소를 담을 수 있는 포인터 변수이다. 관습적으로 ap라는 이름을 사용하한다. va_list의 타입은 typedef를 통해 내부적으로 char *로 정의되어 있다.

Part 2: va_start

va_start(ap, args);
C
복사
va_start는 위에서 선언한 va_list 타입의 ap를 초기화 해주는 매크로이다. va_start를 통해 첫 번째 가변 인자의 주소를 ap참조할 수 있도록 만들어준다. 정의 되어 있는 형태는 Microsoft Visual Studio 기준으로 아래와 같다.
#define va_start(ap, v) ( (ap) = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
C
복사
_ADDRESSOF(v) => &(v)
_INTSIZEOF(v) => ( (sizeof(v) + sizeof(v) - 1) & ~(sizeof(int) - 1) )
va_start는 마지막 필수 인자를 매개 변수로 넣기만 하면 자동으로 가변 인자의 시작 주소를 계산하여 ap에 할당해주는 역할을 수행한다.
주어진 필수 인자들 중 마지막 필수 인자v의 크기를 재고, v의 시작 주소에서 v의 크기만큼을 더하여 가변 인자의 시작 주소를 구할 수 있게 해준다. 그리고 가변 인자의 시작 주소를 ap에 할당해주는데, 이 모든 것들이 va_start를 통해 이뤄진다.
v의 주소에서 v의 크기를 더한 것이 가변 인자의 시작 주소가 되는 이유는, 함수의 호출을 통해 넘어온 인자들은 일련의 메모리 공간을 차지하는 형태를 갖기 떄문이다.

Part 3: va_arg

tmp = va_arg(ap, int);
C
복사
va_argva_list참조하고 있는 특정 가변 인자역참조하고, va_list내의 다음 가변 인자참조하도록 해주는 매크로이다. 다음 가변 인자참조하도록 만들 때는 sizeof(type)만큼 다음에 있는 값을 참조하도록 만든다. 위의 경우는 sizeof(int)만큼 다음에 있는 값을 참조하도록 만든다고 보면된다.
위 과정은 아래와 같은 매크로를 통해 이뤄진다.
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
C
복사
va_list가변 인자역참조하고 다음 가변 인자를 가리키도록 만드는 연산은 자세히 보면, 대입 연산자를 통해 특정 타입의 크기 만큼 먼저 밀어서 다음 가변 인자를 가리키도록 만들어 놓은 후 다시 그 특정 타입의 크기만큼을 빼서 현재 참조하고 가변 인자역참조 할 수 있도록 만들어 둔 것을 볼 수 있다.
_INTSIZEOF(t)만큼 더해놓고 _INTSIZEOF(t)만큼 다시 뺀 값을 t * 타입의 포인터 변수를 역참조한 것을 볼 수 있다.
for 구문으로 작성한 반복문을 그림을 통해 살펴보자.
char, short 의 경우에는 int로 대신 쓰고, float의 경우에는 double로 대신 쓴 이후 형 변환을 해주어야 한다. 이처럼 수행하는 이유는 메모리에 특정 값이 쓰일 때는 주소 체계 단위로 기록을 하기 때문이다. 이를 통해 Byte Padding 효과가 일어나서 32 비트 주소 체계를 기준으로 4 바이트 아래의 타입들은 4 바이트int로 처리 후, 각자의 타입으로 형 변환 하여 값을 읽어내는 것이다.
char ch = (char)va_arg(ap, int);
C
복사

Part 4: va_end

va_end(ap);
C
복사
가변 인자 목록을 지칭하는 va_list 타입의 포인터 변수를 NULL을 할당하면서 가변 인자 사용의 끝낼 때 사용한다. 아래 매크로를 보면 va_list 타입으로 형 변환 후 0을 넣어주는 것을 볼 수있다. 여기서의 0NULL을 의미하는데, va_arg를 사용하여 특정 메모리 공간을 참조하고 있는 포인터NULL을 할당하여 참조하는 공간이 없도록 만든다.
#define va_end(ap) ( ap = (va_list)0 )
C
복사
매크로는 실제로 없어도 프로그램에 지장이 없다. 인텔 계열의 CPU에서는 va_end가 아무 일도 하지 않는다고 한다. 하지만 다른 플랫폼과의 호환성에서 중요한 역할을 할 수 있으므로 관례적으로 넣어준다. 또한 프로그래밍을 하면서 이미 사용이 끝난 va_list를 사용하여도 참조하는 것이 없도록 안전성에 기여하기도 한다.

Bonus: va_copy

va_copy(va_list dest, va_list src);
C
복사
va_copy는 현재 위치를 저장해야 하는 상황에서 사용한다. 예를들어 루프를 돌면서 포인터가 계속 전진하고 있는 상황에서 해당 위치를 저장해야 할 경우가 생기면 va_copy를 활용해서 해당 위치를 저장해둔다.

Bonus: Without Macros

매크로전처리기에 의해 컴파일 시점 직전에 텍스트 치환으로 처리된다. 그렇다면 가변 인자를 위해 구현된 매크로들을 사용하지 않고도 가변 인자를 활용한 함수처럼 만들 수 있지 않을까?
위에서 가변 인자와 관련된 모든 매크로들의 형태를 살펴보았다. 이에 따라 매크로를 이용하지 않고 아래와 같이 코드를 작성할 수 있다.
#include <stdio.h> #include <stdarg.h> void sumAll(int args, ...) { int result; int tmp; //va_list ap; char *ap; result = 0; //va_start(ap, args); ap = (char *)&args + sizeof(args); for (int i = 0 ; i < args ; i++) { //tmp = va_arg(ap, int); tmp = *(int *)ap; ap += sizeof(int); result += tmp; printf("%d", tmp); if (i == args - 1) break; printf(" + "); } printf(" = %d\n", result); //va_end(ap); ap = 0; } int main(void) { sumAll(1, 10); sumAll(2, 10, 20); sumAll(3, 10, 20, 30); sumAll(4, 10, 20, 30, 40); sumAll(5, 10, 20, 30, 40, 50); return (0); }
C
복사
위 코드를 컴파일 하고 실행하면, 올바르지 않은 결과가 나타나는 것을 볼 수 있다. 분명 올바른 값이 나와야 하는데 메모리의 값이 올바르게 역참조 되지 않는 것을 유추할 수 있다. 일반적으로 32 비트 주소 체계에서는 문제 없이 수행될 코드지만, 현재 실습을 수행하는 컴퓨터는 64 비트 주소 체계를 이용함에 따라 발생한 문제이다.
64 비트 주소 체계를 이용했을 때, 매크로를 사용하지 않고 구현한 코드가 올바른 결과가 나오지 않는 이유는 아래 블로그에 자세히 설명되어 있다.

3. Format Specifier Rules

1) Rule 정리 형식

정리를 위해 아래와 같이 축약해서 표현했다는 것을 참고하자.
printf("%3.2c", 'a');
C
복사
→ "%3.2c", 'a'
→ %{3.2}c
→ %{w.p}c
w.p

2) Rule of Only Width (with Flag)

width에 대해선 기본적으로 인자의 길이 값이 width의 옵션으로 주어진 값을 넘어갔을 때, width 필드를 무시한다.

[1] w

너비가 w가 되도록 빈 자리에 대해 공백을 왼쪽에 추가
#include <stdio.h> int main(void) { printf("%5d", 0); return (0); }
C
복사

[2] -w

너비가 w가 되도록 빈 자리에 대해 공백을 오른쪽에 추가
#include <stdio.h> int main(void) { printf("%-5d", 0); return (0); }
C
복사

[3] 0w

너비가 w가 되도록 빈 자리에 대해 0을 왼쪽에 추가
#include <stdio.h> int main(void) { printf("%05d\n", 1); return (0); }
C
복사

[4] -0w

너비가 w가 되도록 빈 자리에 대해 0을 오른쪽에 추가해야할 것 같지만, flag 필드의 -0은 함께 사용할 수 없다. 기본적으로 아래 그림과 같이 컴파일 경고를 띄운다.
#include <stdio.h> int main(void) { printf("%-05d\n", 1); return (0); }
C
복사
만일 이 경고를 무시하고 강제로 컴파일 하면 -w와 같은 결과로 출력되는 것을 볼 수 있다.

[5] *

0 < * : 인자로 받은 *에 대해 너비가 *이 되도록 빈 자리에 대해 공백을 왼쪽에 추가
0 > * : 인자로 받은 *에 대해 너비가 *이 되도록 빈 자리에 대해 공백을 오른쪽에 추가
#include <stdio.h> int main(void) { printf("%*d\n", 5, 0); printf("%*d\n", -5, 0); return (0); }
C
복사

[6] -*

인자로 받은 *에 대해 너비가 *이 되도록 빈 자리에 대해 공백을 오른쪽에 추가
#include <stdio.h> int main(void) { printf("%-*d\n", 5, 0); printf("%-*d\n", -5, 0); return (0); }
C
복사

[7] 0*

0 < * : 인자로 받은 *에 대해 너비가 *이 되도록 빈 자리에 대해 0을 왼쪽에 추가
0 > * : 인자로 받은 *에 대해 너비가 *이 되도록 빈 자리에 대해 0을 오른쪽에 추가해야할 것 같지만, 인자로 받은 *이 음수라면 flag의 -옵션이 적용된다. flag의 -0은 함께 사용할 수 없으므로 -w와 같은 결과로 출력된다. -0w처럼 컴파일 경고를 주지 않는 이유는 코드 상에 w를 명시해놓은 것이 아니고 런 타임에 사용자로부터 입력을 받도록 만들었기 때문이다. 런 타임에 발생한 케이스는 컴파일 타임에서 찾아낼 수 없다.
#include <stdio.h> int main(void) { printf("%0*d\n", 5, 1); printf("%0*d\n", -5, 1); return (0); }
C
복사

[8] -0*

flag 필드의 -0은 함께 사용할 수 없다는 동일한 이유로 컴파일 경고를 나타낸다.
#include <stdio.h> int main(void) { printf("%-0*d\n", 5, 1); printf("%-0*d\n", -5, 1); return (0); }
C
복사
강제로 컴파일을 수행하면, 아래에 보이는 것처럼 -*과 같은 결과로 출력되는 것을 볼 수 있다.

3) Rule of Only Precision (with Flag)

.precision은 기본적으로 자릿수를 채울 때 0을 채워주는 성질이 있으며 .precision의 값으로는 음수가 아닌 값만 올 수 있다. (음수가 들어오면 .precision의 값을 처리하는 방식이 있다. 자세한 것은 printf 설명 부에 있는 .precision 필드의 설명을 참고하자.)

[1] .p

정수에 대해선 총 자릿수가 p가 되도록 0을 왼쪽 빈 공간에 채워 넣는다.
실수에 대해선 소수점 아래 자릿수가 p가 되도록 0을 빈 공간에 채워 넣는다. 만일 자릿수가 넘어가면 자릿수를 맞추기 위해 반올림을 수행한다.
#include <stdio.h> int main(void) { printf("%.5d\n", 1); printf("%.5f\n", 0.1); printf("%.5f", 0.000015); return (0); }
C
복사

[2] -.p

width 값이 주어진 형태가 아니기 때문에 그 결과가 .p와 동일하게 나타난다.
#include <stdio.h> int main(void) { printf("%-.5d\n", 1); printf("%-.5f\n", 0.1); printf("%-.5f", 0.000015); return (0); }
C
복사

[3] 0.p

width 값이 주어진 형태가 아닌데다가 .precision 특성 상 0을 빈 자리에 채우기 때문에 그 결과가 .p와 동일하게 나타난다.
#include <stdio.h> int main(void) { printf("%0.5d\n", 1); printf("%0.5f\n", 0.1); printf("%0.5f", 0.000015); return (0); }
C
복사

[4] -0.p

flag-0은 함께 사용할 수 없기 때문에 컴파일 경고가 발생한다.
#include <stdio.h> int main(void) { printf("%-0.5d\n", 1); printf("%-0.5f\n", 0.1); printf("%-0.5f", 0.000015); return (0); }
C
복사
다만 강제로 컴파일을 시켜도, width 값이 주어진 형태가 아닌데다가 .precision 특성 상 0을 빈 자리에 채우기 때문에 그 결과가 -.p와 동일하게 나타난다.

[5] .*

.p와 결과가 동일하다. 하지만 p 값을 사용자에게서 인자로 받아서 사용하게 된다.
#include <stdio.h> int main(void) { printf("%.*d\n", 5, 1); printf("%.*f\n", 5, 0.1); printf("%.*f", 5, 0.000015); return (0); }
C
복사

[6] -.*

-.p와 결과가 동일하다. 하지만 p값을 사용자에게서 인자로 받아서 사용하게 된다.
#include <stdio.h> int main(void) { printf("%-.*d\n", 5, 1); printf("%-.*f\n", 5, 0.1); printf("%-.*f", 5, 0.000015); return (0); }
C
복사

[7] 0.*

0.p와 결과가 동일하다. 하지만 p값을 사용자에게서 인자로 받아서 사용하게 된다.
#include <stdio.h> int main(void) { printf("%0.*d\n", 5, 1); printf("%0.*f\n", 5, 0.1); printf("%0.*f", 5, 0.000015); return (0); }
C
복사

[8] -0.*

-0.p와 같이 flag에서 -0이 함께 사용될 수 없기 때문에 아래와 같이 컴파일 경고가 발생한다.
#include <stdio.h> int main(void) { printf("%-0.*d\n", 5, 1); printf("%-0.*f\n", 5, 0.1); printf("%-0.*f", 5, 0.000015); return (0); }
C
복사
다만 강제로 컴파일을 시켜도, width 값이 주어진 형태가 아닌데다가 .precision 특성 상 0을 빈 자리에 채우기 때문에 그 결과가 -.*와 동일하게 나타난다.

4) Rules of Width + Precision (with Flag)

[1] w.p

[2] -w.p

w.p의 결과와 동일하면서 추가적으로 좌측 정렬을 수행한다.

[3] 0w.p

w.p의 결과와 동일하면서 추가적으로 빈 공간에 0으로 채워 넣는다.

[4] -0w.p

flag에는 -0을 함께 사용할 수 없기 때문에 컴파일 경고를 띄운다.
강제로 컴파일 시킬 경우 -w.p와 동일한 결과를 나타낸다.

[5] *.* / *.p / w.*

w.p의 결과와 동일하면서 *에 해당하는 인자를 사용자에게 직접 받는다.

[6] -*.* / -*.p / -w.*

-w.p의 결과와 동일하면서 *에 해당하는 인자를 사용자에게 직접 받는다.

[7] 0*.* / 0*.p / 0w.*

0w.p의 결과와 동일하면서 *에 해당하는 인자를 사용자에게 직접 받는다.

[8] -0*.* / -0*.p / -0w.*

flag에는 -0을 함께 사용할 수 없기 때문에 컴파일 경고를 띄운다.
강제로 컴파일 시킬 경우 -*.*와 동일한 결과를 나타낸다.

5) Warnings of Each Type

[1] c

w, *, -w, -* 등에 대해 활용 가능하다.
.p, .*, 0w, 0* 등에 대해 경고가 나타난다.
다만 0w, 0*에 대해서는 아래와 같이 경고가 나타나도 강제로 컴파일 시에는 정상 적용된 값이 나와야 한다는 것을 숙지해야 한다.
#include <stdio.h> int main(void) { printf("%05c\n", 'a'); printf("%0*c\n", 5, 'a'); return (0); }
C
복사

[2] s

w, *, -w, -*, .p, .*, w., *., w.p, *.* 등에 대해 활용 가능하다.
0w, 0* 등에 대해 경고가 나타난다.
다만 0w, 0*에 대해서는 아래와 같이 경고가 나타나도 강제로 컴파일 시에는 정상 적용된 값이 나와야 한다는 것을 숙지해야 한다.
#include <stdio.h> int main(void) { printf("%05.3s\n", "abcdef"); printf("%0*.*s\n", 5, 3, "abcdef"); return (0); }
C
복사

[3] p

w, *, -w, -* 등에 대해 활용 가능하다.
.p, .*, 0w, 0* 등에 대해 경고가 나타난다.
다만 .p, .*, 0w, 0*에 대해서는 아래와 같이 경고가 나타나도 강제로 컴파일 시에는 정상 적용된 값이 나와야 한다는 것을 숙지해야 한다.
w의 경우에는 0x를 너비에 포함하고, p의 경우 0x를 정확도에서 제외하고 출력이 되는 것을 볼 수 있다.
#include <stdio.h> int main(void) { printf("%.20p\n", "abcdef"); printf("%.*p\n", 20, "abcdef"); printf("%020p\n", "abcdef"); printf("%0*p\n", 20, "abcdef"); return (0); }
C
복사

[4] d / i / u / x / X

모든 필드에 대해 활용 가능하다.

4. Reference

1) printf

2) Variable Argument