고오급 printf 사용법
C언어로 printf를 배운 후에 다른 언어를 배우면 printf는 개 똥같은 함수라는 생각이 들게 마련입니다. 하지만 막상 write()함수밖에 쓸 수 없는 상황을 겪는다면 생각이 많이 바뀔 것입니다.
(대충 다시보니 선녀다 짤방)
printf를 못쓰게 하는 부조리를 겪는 사람이 있다구요??
printf의 기초는 다들 잘 아시리라 생각하고, 본론으로 들어가 보겠습니다.
자세한 설명은 리눅스에서 "man 3 printf"로 보실 수 있습니다(영어주의)
"%"와 서식지정자 사이에는 여러가지 옵션들이 들어올 수 있는데, 그것들을 어떻게 쓰는지 알아봅시다.
%[Flag characters][Field width][.Precision][Length modifier]<Conversion specifiers>
"[]"는 생략해도 되는 것이고, <>는 반드시 써야 되는 것입니다.
우선 간단하게 요약해보자면
- Flag characters(이하 플래그)에는 [+][-][ ][#][0] 같은 것들이 있습니다. 이 중 하나는 공백입니다. 플래그는 정렬 방법, 부호표시 같은 것들과 관련있습니다. 겹쳐서 쓸 수 있기 때문에 이해하기 까다롭고 서식지정자에 따라 다르게 작동하는 것도 있어서 또 이해하기 까다롭습니다.
- Field width(이하 폭)은 출력할 문자열의 전체 길이를 정해줍니다.
- Precision(이하 정밀도)는 유효숫자와 관련 있는데, 정수형/부동소수/문자열 모두 다르게 작동해서 뒤에서 자세히 알아봅시다.
- Lenth modifier(이하 길이변환자)는 데이터의 길이와 관련있습니다. 서식지정자와 함께 엮여서 몇 바이트를 읽어들일지 결정해 줍니다.
- Conversion specifier(이하 서식지정자)는 데이터를 어떻게 표현할지를 결정합니다. 같은 int형 데이터라도 10진수 또는 16진수로 다르게 표현할 수 있습니다.
사용례는 다음과 같습니다.
#include <stdio.h>
int main(int argc, char **argv)
{
long long a = 0x123456abcdef;
printf("|%#-20.14llx|\n", a);
}
코드를 컴파일 하면 다음과 같습니다.
$ gcc test.c && ./a.out
|0x00123456abcdef |
설명 순서는 서식지정자 > 길이변환자 > 정밀도 > 플래그 > 폭 순 입니다.
1. 서식지정자
- c : 문자 하나를 출력합니다. 만약 데이터의 값이 97이라면, 97에 해당하는 아스키코드가 'a'이므로 a를 출력합니다.
- s : 문자열을 출력합니다. 문자열의 시작부터 '/0'까지 출력합니다. 출력하려는 곳의 주솟값이 0이면 "(null)"이 출력됩니다.
- d : 데이터를 부호있는 10진수로 출력합니다.
- i : d하고 같습니다.
- u : 부호없는 10진수로 출력합니다.
int a = 0xffffffff;
printf("%u\n", a); // 출력: 4294967295
printf("%d\n", a); // 출력: -1
- o : 8진수로 출력합니다.
- x : 16진수로 출력합니다. 9보다 큰 수는 abcdef로 나타냅니다.
- X : x와 같은데, 9보다 큰 수를 ABCDEF로 나타냅니다.
- p : 주소를 16진수로 출력합니다. 주솟값이 0이면 시스템마다 다르게 나옵니다. "(nil)" 또는 "0x0" 등등
- f : 부동소수를 출력합니다. 기본값은 소수 6째 자리까지 출력합니다.
- e : 부동소수를 출력합니다. 지수표기법으로 출력합니다. 가수는 소수점 이하 6째 자리까지 출력합니다.
float b = 1234.1234
printf("%e\n", b); // 1.234123e+03
- g : f와 e의 출력 중에서 좀 더 '간단한' 형태로 출력합니다.
float a = 123456; printf("a: %g\n", a); //a: 123456
float b = 1234567; printf("b: %g\n", b); //b: 1.23457e+06
float c = 0.0001234; printf("c: %g\n", c); //c: 0.0001234
float d = 0.0001234567; printf("d: %g\n", d); //d: 0.000123457
float e = 0.000012; printf("e: %g\n", e); //e: 1.2e-05
여기서 간단하다는 말은 보시다시피 더 짧은 것을 의미하지는 않습니다.
변수 a의 경우, 6자리 숫자이므로 숫자6자리가 그대로 출력됩니다.
변수 b의 경우, 6자리보다 큰 7자리 숫자이므로 지수표기법으로 표현됩니다.
변수 c의 경우, 유효숫자가 4개이므로 그대로 출력됩니다.
변수 d의 경우, 유효숫자가 7개로, 6보다 큽니다. 맨 끝을 반올림 해서 6자리로 표현합니다.
변수 e의 경우, 유효숫자는 2개밖에 안되는데, 0이 너무 많습니다. 그래서 이렇게 지수표기법으로 표현됩니다.
여기서 변환의 기준이 되는 "6"이라는 값은 나중에 다룰 정밀도와 관련있습니다. 그 정밀도의 기본값이 6이어서 따로 정밀도를 지정하지 않았을 때는 6을 기준으로 변환 여부를 정합니다.
f와 e는 출력 값이 충분히 예상 가능한데, g는 다소 어렵습니다ㅠㅠ
- n : 지금까지 출력한 문자열의 길이(int)를 인자에 전달합니다.
int len;
printf("written: %n", &len); // written:
printf("%d\n", len); // 9\n
당연히 포인터로 받아야 len의 값을 변경할 수 있겠죠ㅎㅎ
int len;
printf("written: %n%d", &len, len); // wrtten: 32766
주의할 점은, 방금 변환한 값을 바로 쓰려고 하면 예상치 못한 동작을 합니다.
- % : %를 출력하려면 "\%"가 아니라 "%%"를 써야 합니다.
2. 길이변환자
길이변환자는 서식지정자에 따라 다양하게 변합니다.
2.1) hh, h, l, ll이 서식지정자 d, i, u, o, x, X와 결합할 때
int i = 0xabcdef01;
long j = 0xabcdef0123456789;
long long k = 0xabcdef0123456789;
printf("hh: %hhx\n", i); // hh: 1
printf("h : %hx\n", i); // h : ef01
printf(" : %x\n", i); // : abcdef01
printf("l : %lx\n", j); // l : abcdef0123456789
printf("ll: %llx\n", k); // ll: abcdef0123456789
이해를 쉽게 하기 위해 16진수로 출력해서 알아봅니다. 숫자 자체는 10진수에 비해 생소하지만, 메모리 공간을 직관적으로 이해하기에는 훌륭한 표현방법입니다.
- hh는 가장 짧은 것이라고 보시면 되겠습니다. 원래 int형은 32비트이지만, 길이변환자로 hh를 붙였더니 앞의 8비트만 읽어들인 것을 알 수 있습니다. (출력되는 문자열 기준으로는 맨 뒤의 "01"이고, 실제로는 메모리 공간에 거꾸로 저장되어 있으므로 맨 앞의 8비트를 가져온 것입니다.)
- h는 16비트를 읽어옵니다. 마찬가지로 앞의 16비트(출력 기준으로는 뒤의 2바이트)를 읽어옵니다.
- 아무것도 없을 때는 32비트를 읽어옵니다.
- l은 long int형을 기준으로 합니다. 제 컴퓨터에서는 long형이 64비트이므로 64비트(8바이트)의 데이터를 읽어왔습니다.
- ll형은 long long int 형을 기준으로 합니다. 그러므로 64비트를 읽어들였습니다.
시스템마다 차이가 있을 수 있습니다. h를 여러개 붙이면 데이터를 짧게 읽어들이고, l을 여러개 붙이면 길게 읽어들인다는 식으로 이해하시면 편하겠습니다.
2.2) l, L이 f, e, g와 결합할 때
float a = 3.14159265358979323846264338327950288419;
double b = 3.14159265358979323846264338327950288419;
long double c = 3.14159265358979323846264338327950288419;
long double d = 3.14159265358979323846264338327950288419L;
printf("1: %.70f\n", a);
printf("2: %.70f\n", b);
printf("3: %.70lf\n", b);
printf("4: %.70Lf\n", c);
printf("5: %.70Lf\n", d);
----------------------------------------------------------------------------
1: 3.1415927410125732421875000000000000000000000000000000000000000000000000
2: 3.1415926535897931159979634685441851615905761718750000000000000000000000
3: 3.1415926535897931159979634685441851615905761718750000000000000000000000
4: 3.1415926535897931159979634685441851615905761718750000000000000000000000
5: 3.1415926535897932385128089594061862044327426701784133911132812500000000
정밀도 옵션은 뒤에서 다루겠지만, 차이를 명확하게 표현하기 위해 한번 써 보았습니다.
- 1행은 단순한 float형을 출력하고 있습니다. 잘 보시면 소숫점 이하 3.141592까지는 잘 나오다가 다음 숫자인 6이 그 다음 숫자인 5의 반올림으로 표현되고 있습니다. 정밀도 기본값이 6인 이유가 대강 짐작이 가는 부분입니다. (참고로 부동소수 리터럴은 기본이 배정도(64비트) 입니다. 32비트를 원하시면 뒤에 f를 붙여주면 됩니다.)
- 2행은 double형 입니다. double형은 64비트 자료형이라서 더 정밀합니다. 3.141592653589793 까지 정확히 표현하고 있습니다.
- 3행은 길이변환자 l을 붙여본 것입니다. 그런데 출력값이 2번과 차이가 없습니다. 그 이유는 서식변환자 f가 기본적으로 double형을 출력하기 때문입니다.
- 1번의 경우 코드상으로는 3.141592 어쩌고 저쩌고 하는 데이터가 들어가지만, float형은 32비트 자료형이므로 딱 수용할 수 있을 만큼만 들어갑니다. 그런데 printf로 출력될 때에는 double형으로 뻥튀기 되고, 그 상태로 출력된다고 보시면 되겠습니다.
- 4행은 long double 형이고, 길이변환자 L을 붙였지만 3번과 출력값이 같습니다. 변수 c에 값을 넣을 때 맨 뒤에 L을 붙여서 long double형을 명시하지 않았기 때문입니다.
- 5행은 변수 d에 값을 넣을 때 맨 뒤에 L을 붙여서 정상적으로 long double형 데이터가 입력되었습니다. 3.141592653589793238 까지 잘 나오고 뒤의 46에서 6이 반올림 되어 5로 출력된 것을 볼 수 있습니다.
3. 정밀도
요약하자면, 문자열에서는 몇 글자를 출력할지, 정수형 자료형에서는 0을 몇개를 채워넣을지, 부동소수 자료형에서는 유효숫자를 몇개로 할 지 정하는 옵션입니다.
3.1) 문자열
문자열에서의 정밀도는 앞에서 몇 글자를 출력할지를 정해줍니다.
printf("%.4s\n", "123456"); // 1234
printf("%.10s\n", "123456"); // 123456
인자로 받은 문자열의 길이가 정밀도보다 길면 정밀도 만큼만 출력합니다.
인자로 받은 문자열의 길이가 정밀도보다 짧으면 그냥 전부 출력합니다.
char *a = 0;
printf("|%s|\n", a); // |(null)|
printf("|%.2s|\n", a); // ||
printf("|%.6s|\n", a); // |(null)|
주솟값이 0인 곳을 출력하게 하면 "(null)"이 나옵니다.
제 컴퓨터에서는 이런 경우에 정밀도를 주면, 정밀도가 작으면 아무것도 안나오고, 6 이상이면 "(null)"을 출력합니다.
시스템마다 다른데, 출력할 문자열을 (null)이라고 치고 정밀도를 지정하기도 합니다. 예를들어 %.2s이면 "(n"이 출력되기도 합니다.
3.2) 정수형 d, i, u, o, x, X
정수형에서는 앞에 0을 채워줍니다.
int a = 123456;
printf("%.4d\n", a); // 123456
printf("%.8d\n", a); // 00123456
int b = -123456;
printf("%.4d\n", b); // -123456
printf("%.8d\n", b); // -00123456
int c = 0xabcdef;
printf("%.4x\n", c); // abcdef
printf("%.8x\n", c); // 00abcdef
출력할 숫자의 길이보다 정밀도가 더 길면 전체 길이가 정밀도와 같아지도록 0을 채웁니다.
특이한 점은 정밀도가 0이고 출력할 값이 0이면 아무것도 출력하지 않습니다.
printf("|%.d|\n", 0); // ||
printf("|%.d|\n", 1); // |1|
'.' 뒤에 아무 것도 쓰지 않으면 정밀도가 0입니다.
3.3) f
f에서는 항상 소수점 이하 자릿수를 정밀도만큼 출력합니다. 0을 채워서라도 그 숫자를 맞춰줍니다.
float a = 3.14159265358979;
printf("%.2f\n", a); // 3.14
printf("%.4f\n", a); // 3.1416
printf("%.8f\n", a); // 3.14159274
printf("%.30f\n", a); // 3.141592741012573242187500000000
float b = 314;
printf("%.4f\n", b); // 314.0000
3.4) e
e에서도 마찬가지로 항상 소수점 이하 자릿수를 정밀도만큼 출력합니다. 역시 0을 채워서라도 그 숫자를 맞춰줍니다.
float a = 3.14159265358979;
printf("%.2e\n", a); // 3.14e+00
printf("%.4e\n", a); // 3.1416e+00
printf("%.8e\n", a); // 3.14159274e+00
printf("%.30e\n", a); // 3.141592741012573242187500000000e+00
float b = 314;
printf("%.4e\n", b); // 3.1400+e02
3.5) g
g는 다소 복잡합니다. 원칙은 이러합니다.
- 되도록 있는 그대로 출력한다. 소수점 이하 맨 뒤의 0은 생략할 수 있으면 생략한다.
- 출력하려는 숫자의 개수가 정밀도보다 많아지면 지수표기법으로 출력한다.
- 0.0000xxxxxx 인 숫자(0.0001 미만인 숫자)는 정밀도에 관계 없이 지수표기법으로 표현된다.
printf("1: %.6g\n" , 314159.); // 1: 314159
printf("2: %.6g\n" , 3141592.); // 2: 3.14159e+06
printf("3: %.6g\n" , 3.141592); // 3: 3.14159
printf("4: %.6g\n" , 3.14); // 4: 3.14
printf("5: %.6g\n" , 0.0003); // 5: 0.0003
printf("6: %.6g\n" , 0.00003); // 6: 3e-05
printf("7: %.16g\n" , 0.00003); // 7: 3e-05
printf("8: %.6g\n" , 0.0003141592); // 8: 0.000314159
printf("9: %.6g\n" , 0.0000314159); // 9: 3.14159e-05
printf("a: %.15g\n" , 0.00031415926535897932384626); // a: 0.000314159265358979
printf("b: %.15g\n" , 0.000031415926535897932384626); // b: 3.14159265358979e-05
- 1: 314159가 6자리라서 그대로 출력합니다.
- 2: 3141592가 7자리라서(6자리보다 커서) 지수표기법으로 출력합니다.
- 3: 3.141592가 7개의 숫자이지만 뒷자리를 반올림 하면 지수표기법 없이 출력할 수 있습니다.
- 4: 정밀도가 6이라서 3.14는 여유있게 3.14로 표현할 수 있습니다.
- 5: 0.0003은 지수표기법 없이 표현됩니다.
- 6: 0.00003이 되자 지수표기법으로 바뀝니다.
- 7: 정밀도를 높여도 소용없습니다.
- 8: 0.0003141592는 문제없이 표현됩니다.
- 9: 예상했던대로 0.00003.14...는 지수표기법으로 바뀝니다. 그런데 잘 보시면 8행과 9행의 출력값의 길이가 같습니다.
- a: 이번에는 정밀도를 15로 놓고 출력해 봅니다.
- b: 마찬가지로 정밀도를 15로 놓고 출력해 봅니다. b행과 c행의 출력값의 길이가 같습니다.
즉 0.0000은 그냥 e-00으로 표현하는 게 더 짧고, 보기에 편해서 지수표기법으로 변환되는 것 같습니다.
3.6) 나머지
c와 p는 정밀도 옵션이 들어올 경우 퉤 하고 뱉어냅니다.
4. 폭
폭은 출력할 문자열의 길이와 관련있습니다.
폭은 앞서 설명한 다른 옵션들과 다음에 설명한 플래그까지 모두 적용된 뒤에 최종적으로 적용됩니다. 폭을 플래그보다 먼저 설명하는 이유는 플래그 중 몇몇 옵션은 폭과 같이 적용되기 때문입니다.
원칙은 다음과 같습니다.
-
문자열의 길이보다 폭이 크면 빈 공간을 모두 공백으로 채웁니다.
-
문자열의 길이보다 폭이 작으면 원래 문자열만 출력합니다.
printf("|%3s|\n", "123456"); // |123456|
printf("|%10s|\n", "123456"); // | 123456|
printf("|%3d|\n", 123456); // |123456|
printf("|%10d|\n", 123456); // | 123456|
원래 출력할 문자열의 길이는 6입니다.
폭으로 주어진 값이 6 이하이면 그대로 출력됩니다.
폭으로 주어진 값이 6보다 크면 공백을 출력합니다. 예를 들어 폭이 10으로 주어지면, 총 10글자를 출력하되, 원래 출력할 6글자를 뺀 나머지 4글자는 공백으로 해서 왼쪽 끝에 채웁니다.
서식지정자와 관계 없이 모든 출력에 대해 논리가 일관적입니다 ㅎㅎ
5. 플래그
플래그에는 +- #0이 있습니다. 플래그는 여러개를 같이 쓸 수 있는데, 경우에 따라서 무시되기도 하고, 경고나 오류를 내뱉기도 하기때문에 다소 머리가 아픕니다ㅠㅠ
5.1 +
+는 부호를 반드시 출력하게 합니다. 특이한 점은 0도 양수로 취급한다는 점입니다. 예시를 보시면 별로 어렵지는 않을 것 같습니다.
printf("%+d\n", 123); // +123
printf("%+d\n", -123); // -123
printf("%+d\n", 0); // +0
printf("%+f\n", 123.); // +123.000000
printf("%+f\n", -123.); // -123.000000
printf("%+f\n", 0.); // +0.000000
부호와 상관 없는 서식지정자(c, s, p, u, x)에서는 경고나 오류를 냅니다.
5.2 ' '
그냥 공백입니다.
음수인 경우에는 부호를 출력합니다.
양수인 경우에는 부호 대신에 공백이 들어갑니다. +와 마찬가지로, 0을 양수로 취급합니다.
printf("|% d|\n", 123); // | 123|
printf("|% d|\n", -123); // |-123|
printf("|% d|\n", 0); // | 0|
printf("|% f|\n", 123.); // | 123.000000|
printf("|% f|\n", -123.); // |-123.000000|
printf("|% f|\n", 0.); // | 0.000000|
+와 마찬가지로 부호와 상관 없는 서식지정자(c, s, p, u, x)에서는 경고나 오류를 냅니다.
+와 같이 쓰이면 경고나 오류를 냅니다. 다만 흥미로운 점은, 단지 경고만 내고 출력은 잘 하는 경우에는 공백 플래그를 무시합니다.
5.3 -
-는 왼쪽 정렬하는 옵션입니다. 폭이 주어져야 동작을 확인해볼 수 있습니다.
printf("|%-10d|\n", 123456); // |123456 |
printf("|%10d|\n", 123456); // | 123456|
printf("|%-3d|\n", 123456); // |123456|
printf("|%-d|\n", 123456); // |123456|
5.4 0
0은 공백 대신 0을 채우는 옵션입니다.
printf("|%03d|\n", 123456); // |123456|
printf("|%010d|\n", 123456); // |0000123456|
printf("|%010x|\n", 0xabcdef); // |0000abcdef|
printf("|%0d|\n", 123456); // |123456|
printf("|%010g|\n", 123.456); // |0000000000000123.456|
printf("|%020e|\n", 123.456); // |000000001.234560e+02|
printf("|%020f|\n", 123.456); // |0000000000123.456000|
printf("|%020.8g|\n", 123.456); // |0000000000000123.456|
printf("|%020.8e|\n", 123.456); // |0000001.23456000e+02|
printf("|%020.8f|\n", 123.456); // |00000000123.45600000|
- 서식지정자가 c, s, p인 경우에는 컴파일시 경고나 오류가 발생합니다. (경고인 경우에 예상한대로 출력되기는 합니다)
- 0플래그와 -플래그가 함께 쓰이면 경고나 오류가 발생합니다.
- 곰곰히 생각해 보자면, 왼쪽 정렬인 경우에는 오른쪽에 0을 채우면 아예 다른 숫자가 됩니다. 예를 들어 원래 출력할 숫자가 12일 때, 00을 붙이는 상황을 가정해 봅시다. 0012는 12와 같은 수라고 볼 수 있지만, 1200은 아예 다른 숫자입니다.
- 한편, 소수점 형태인 경우에는 어떨까요. 원래 출력할 값이 12.00이고, 00을 붙이는 상황을 가정해 봅시다. 0012.00이나 12.0000이나 같은 12라고 볼 수 있겠지만, 역시 이 경우에도 컴파일러는 경고나 오류를 냅니다.
- 정밀도와 함께 쓰이는 경우에는 정수형(d, i, o, u, x, X)과 부동소수형에서 다르게 나타납니다.
- 정수형에서는 경고나 오류가 발생합니다. 경고가 나고 출력은 되는 경우에는 0플래그를 무시합니다. 그 이유는 정밀도와 0플래그 모두 출력값 앞에 0을 채우는 옵션이라서 충돌하기 때문입니다.
- 부동소수형에서는 문제없이 출력됩니다. 정수형과는 달리 충돌할 일이 없기 때문입니다.
5.5 #
해시태그는 정말 일관성 없습니다.
일단 s, c, d, i, u, p에 #을 붙여주면 퉤 하고 뱉어냅니다.
그럼 나머지 x, e, f, g에 어떻게 반응하는지 한번 보겠습니다.
- x, X
printf("|%x|\n", 0x123abc); // |123abc|
printf("|%#x|\n", 0x123abc); // |0x123abc|
printf("|%X|\n", 0x123abc); // |123ABC|
printf("|%#X|\n", 0x123abc); // |0X123ABC|
x에 #를 붙여주면 "0x"가 생깁니다.
X에 #를 붙여주면 "0X"가 생깁니다.
- e, f, g
printf("e |%.e|\n", 3.1415); // e |3e+00|
printf("e |%#.e|\n", 3.1415); // e |3.e+00|
printf("f |%.f|\n", 3.1415); // f |3|
printf("f |%#.f|\n", 3.1415); // f |3.|
printf("g |%.0g|\n", 3.1415); // g |3|
printf("g |%#.0g|\n", 3.1415); // g |3.|
이 깨알같은 변화가 느껴지십니까ㅎㅎ
정밀도 값으로 0을 넣었고, 그러므로 소수점 이하는 생략되면서 마치 정수형처럼 표시될 것입니다.
#가 있고 없고의 차이는 .을 생략하느냐 마느냐의 차이입니다. #을 붙이면 어느 경우라도 소수점의 .을 표시합니다.
- g 추가
printf("g |%.10g|\n", 3.1415); // g |3.1415|
printf("g |%#.10g|\n", 3.1415); // g |3.141500000|
g의 경우에는 좀 더 많은 변화가 돋보입니다.
g에 #를 붙이면 원래는 생략되었을 0을 반드시 표시합니다.