메아리 저널

printf와 친구들 (1)

사실 알고 보면 C를 안다는 사람들 중에 printfscanf를 정확히 알고 있는 사람은 그렇게 많지 않다. (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만을 넘어 버렸다.

전체 글 목록은 다음과 같다.

(2010-03-25)

이 글은 본래 http://tokigun.net/blog/entry.php?blogid=60에 썼던 것을 옮겨 온 것입니다.


(rev 1d46270eb038)