2006년 2월 25일 printf와 친구들 (1)
사실 알고 보면 C를 안다는 사람들 중에 printf
나 scanf
를 정확히 알고 있는 사람은 그렇게 많지 않다. (CN 님께서 지적하셨듯이 C를 배우는 사람들 중 대부분이 별로인 교재로 공부해서 그럴 지는 모르겠지만) 그럼에도 불구하고 Hello, world! 찍는 데조차 printf
를 쓸 정도니 확실히 편리한 함수임에는 분명하다.
printf
는 그 편리함 때문에 다른 언어에서도 비스무리한 형태로 많이들 지원한다. 최근에 찾아 볼 일이 생긴 데다가 호기심도 생겨서 printf
를 비롯한 여러 종류의 포매팅 방법을 뒤져 봤는데, 거기에 대해서 한 번 써 보려 한다.
B
1970년대 초에 C가 만들어지는 과정에서는 ALGOL 60, CPL, BCPL, B, C로 이어지는 흐름이 있었는데, 처음으로 printf
가 등장한 것이 C의 선조인 B였다. (BCPL에는 애초에 printf
같은 형태의 출력문이 없었다)
켄 톰프슨이 쓴 것을 데니스 리치가 스캔해서 올려 놓은 1972년판 B 매뉴얼 9.3장에는 printf
의 구현(!)이 쓰여져 있다. 당시 제공했던 기능을 살펴 보면 다른 옵션 없이 %d
, %o
, %c
, %s
만이 구현되어 있음을 알 수 있다. (코드를 조금 더 들여 보면 가변 인자를 모두 같은 크기로 가정했고 문자열이 널 문자 대신 EOF로 끝났다는 걸 알 수 있다. *e
는 지금의 \e
쯤에 대응한다고 생각하면 되겠다.) 아마 켄 톰프슨은 이걸 쓸 당시에만 해도 printf
가 이렇게 복잡해질 줄은 생각을 못 했을 것이다.
C
B에서 발전되어 만들어진 C는 표준화 과정을 거치면서 복잡한 언어로 성장했고, 당연히 printf
에도 별의별 기능들이 들어 가면서 복잡해졌다. 잘 알려진 건 일단 빼고 그래도 잘 알려지지 않은 것 같은 것만 골라서 몇 개 설명해 보자면, (몇몇은 나중에 들어 간 것도 있다)
%#x
와 같이#
를 붙인 것을 대체 형식(alternative form)이라 부르며, 어디에서 쓰느냐에 따라 의미가 다르다.%#x
같은 경우 앞에 0x 접두사를 붙인다.%.30s
와 같이%s
에 정확도(precision)를 지정하면 맨 처음 30글자만 출력한다.%n
라고 쓰면 아무 것도 출력하지 않는다. 대신%n
이 나올 때까지 쓰여진 문자들의 갯수를 다음 인자에 지정된 int 포인터에 넘겨 준다. (이해가 안 가시는 분을 위해,printf("%d%d%n%d", 123, 456, &n, 789);
는 123456789를 출력하고, %n이 있는 위치는 123456 바로 뒤기 때문에 n에 6을 대입한다.)- 1999년판 ANSI C에서는 길이 변경자(length modifier)로
j
(intmax_t
),z
(size_t
),t
(ptrdiff_t
)를 제공하고, 부동 소숫점 실수를 16진수로 표시하는 %a라는 형식도 제공한다. 이 모든 게 ANSI C에 별의별 형들이 다 들어 가면서 벌어진 일이다. 하지만 안타깝게도 C99에서 추가된_Complex
형을 바로 출력하는 방법은 없다. - 그리고 다들 아시겠지만
%*.*f
같이*
를 숫자 대신 쓰면 너비 등을 인자로 지정할 수 있다.
C + UNIX
POSIX 표준(옛날에는 SCS)은 유닉스 계열 운영체제의 API를 표준화한 것으로, (하긴 유닉스 계열 아니어도 쓰는 데가 꽤 되긴 하지만...) 여기에는 ANSI C 표준도 함께 들어 가 있다. 그리고 예상하셨다시피 printf
도 좀 더 복잡해졌다. -,.-
POSIX 표준의 printf
에서 가장 주목할 만한 것으로 인자를 그냥 지정하는 것이 아니라 인자 번호를 사용해서 인자를 지정하는 것이 있다. 예를 들어서,
printf("There were %1$d deer(s) and %2$d tiger(s). But %2$d tiger(s) ate %1$d deer(s).\n", 42, 3);
이런 것이 가능하다는 것이다. (%
대신에 %pos$
를, *
대신에 *pos$
를 쓰면 된다. 첫 인자가 1, 둘째 인자가 2, ... 순서대로이다.) C의 가변 인자 특성상 구현이 좀 더 난감해지긴 했지만 특정 상황에서는 편리한 기능이라고 할 수 있겠다.
C++
C++는 C로부터 나왔기 때문에 printf
를 물론 가지고 있다. (정확히는 std::printf
) 하지만 printf
의 구현 방법은 형 검사가 불가능하기 때문에 언제나 위험에 노출되어 있다. (printf("%d", "Hello, world!");
라고 쓰는 걸 생각해 보시라. 사실 웬만한 컴파일러들은 printf
같이 잘 알려진 가변 인자 함수는 어떻게든 형 검사를 하려고 하긴 하지만, 일반적인 형 검사는 거의 불가능한 게 사실이다.) 그리고 C++의 클래스와 잘 융합될 수도 없다는 문제가 있다. 그래서 C++의 iostream 라이브러리는 형 검사가 가능한 데다가 사용자가 맘대로 바꿀 수 있는 customizable한 인터페이스를 만들어 놓았다.
cout << 3 << "blah" << SomeClass(1, 2, 3) << endl;
알고 보면 상당히 "마법"같은 이 인터페이스는 std::ostream& operator <<(std::ostream&, type)
라는 연산자 오버로딩 함수에서 유래한 것이다. 이렇게만 한다면 %5.1f
같은 printf
의 풍성한 기능을 놓칠 수도 있겠지만, 이 type
에 다른 클래스를 집어 넣어서 스트림의 옵션을 바꾸는 방법으로 이를 구현하고 있다. 아주 간단한 예시로 setw의 내부 구현을 생각해 본다면,
// setw가 반환하는 구조체. 사실 무슨 형이라도 상관 없다.
struct _Xyzzy_setw { int _width; };
// setw는 구조체에 너비 정보를 담아서 반환한다.
const _Xyzzy_setw setw(int width)
{
_Xyzzy_setw blah;
blah._width = width;
return blah;
}
// operator <<를 오버로딩해서 setw가 반환한 너비 정보를 실제로 적용한다.
std::ios_base& operator <<(std::ios_base& stream, _Xyzzy_setw manip)
{
stream.width(manip._width);
return stream;
}
// 사용 예
std::cout << setw(16) << 3141592;
(2부에서 계속됨)
덤: 알게 모르게 카운터가 2만을 넘어 버렸다.
전체 글 목록은 다음과 같다.
- 2006-02-25: printf와 친구들 (1)
- 2006-02-28: printf와 친구들 (2)
- 2006-03-11: printf와 친구들 (3)
(2010-03-25)