dev.log2004. 2. 27. 12:28
C로 프로그래밍을 처음 배울때 누구나 만들어본 프로그램이 있다. 모두들 짐작할 것이다. 바로 그 유명한 "Hello, world" 프로그램이다. 이 프로그램은 원래 커니건 & 리치가 쓴 The C Programming Language에 등장하는 첫번째 예제로 그후 거의 모든 C입문서의 첫번째 예제로 쓰이고 있다. Hello, world 프로그램은 다음과 같은 간단한 코드이다.

#inlcude <stdio.h>

void main()
{
    printf( "Hello, world\n" );
}

이 예제에서는 Hello, world를 출력하기 위해 printf()를 사용하는데, 아는 사람은 알겠지만, printf는 C의 표준 라이브러리 함수중 가장 복잡한 함수이다. printf()의 모든 기능을 완전히 이해하기 위해서는 variable argument와 호출규약 등 언어의 지저분한(?) 하위레벨 기능까지를 알아야 하는 까닭이다. C에서 가장 처음 배우는 예제가 C에서 가장 복잡한 함수를 사용한다는 것은 일종의 아이러니가 아닐까.
사실, 위의 예제에서 문자열을 출력한다는 용도로는 puts()함수를 쓰는 것이 더 적합할 것이다. 그러나 아마도 커니건 & 리치는 출력에 관한한 거의 만능에 가까운 printf()함수를, 사용자에게 친숙하게 만들고자 일부러 썼을 것이다.
C라는 언어안에서만 이야기 한다면, printf()의 가장 큰 약점은 수행속도이다. printf()는 첫번째 인자로 주어지는 형식지정문자열(format specifier)을 파싱하여 그 뒤에 주어지는 인자의 출력형식을 결정하는데, 여기에 걸리는 오버헤드가 만만치 않다. 간단한 문자열 출력이라면 puts등을 사용하는 것이 적절하다. 물론 문자열, 정수, 부동소수 등이 복잡하게 얽힌 형식으로 출력한다면 이야기가 달라지긴 하지만.

그러면 이제 논의를 C++의 세계로 옮겨 보자.
C++의 표준 출력기능은 이제 printf[함수]가 아닌 cout[객체]로 옮겨졌다. 왜 옮겼을까?
printf()가 가지는 가장 큰 약점은 형안정성을 보증하지 못한다는 것이다.

#inlcude <stdio.h>

void main()
{
    printf( "%f", 1 );
}

위 와 같은 코드는 C환경에서 정상적으로 컴파일되며 실행도 된다. 물론 현대적인 컴파일러나, lint같은 툴을 쓴다면 printf()에 주어진 형식지정자와 실제인자의 타입이 일치하지 않는다는 워닝을 내줄 수 있겠지만, 위의 코드가 작동하며, 또한 언뜻 보기에 의미 없는 값을 출력한다는 점은 변하지 않는다.
이는 printf()함수가 실매개변수의 타입과 출력 형식을 별도의 정보로 지정하기 때문이다. 대개의 출력형식은 실매개변수의 타입에 따라 결정된다. int형의 실매개변수는 거의 90%이상 %d형식으로 출력할 것이며, 절대 %f형식으로 출력하려고 하는 사람은 없을 것이다. 이는 타입정보와 형식정보가 실제로는 별개의 정보가 아니며 둘 사이에 상관관계가 있다는 것을 의미한다. 물론 고의적으로 플로팅포인트 타입의 비트패턴을 보기 위해 float타입을 16진수 정수형으로 출력하는 경우도 있으나 대개의 경우 타입에 따라 출력 형식이 정해짐을 주지해야 한다. 그러나 printf()는 밀접한 상관관계가 있는 두개의 정보를 별개의 정보로 지정함으로써 둘 사이에 미스매치가 발생할 경우 그에 대해 어떠한 식별도 할 수 없다는 맹점을 지니고 있다.
C++에서 가장 강화된 것 중의 하나가 타입시스템이라고 말한바 있다. 따라서 C++의 설계당시부터 표준출력을 담당하는 printf()함수의 위와같은 맹점때문에 C++에서 표준출력을 담당할 객체가 cout으로 정해지게 된다.
#inlcude <iostream>

int main()
{
    std::cout << "Hello, world" << std::endl;
    return 0;
}

첫번째 Hello, world와 같은 일을 하는 프로그램의 C++버전이다.
cout 은 printf()와는 달리 형 안정성을 보장한다. cout객체가 작동하는 방식은 연산자 오버로딩에 기초를 두고 있기 때문이다. 무슨 말인고 하니, cout객체가 출력할 수 있는 모든 타입에 대해 <<연산자가 오버로딩 되어 있다는 것이다. 이제 컴파일러가 cout객체와 << 연산자를 통해 출력하라는 문장을 만나면 출력할 대상의 타입에 따라 오버로딩된 <<연산자 중에서 적절한 연산자를 자동으로 골라서 출력해 준다는 의미가 된다. 즉, 타입에 맞는 출력 형식을 컴파일러가 선택하므로 형 안정성이 보장되는 것이다.


#inlcude <iostream>

int main()
{
    std::cout << 1 << 1. << std::endl;
    return 0;
}


위 프로그램은 오해의 여지가 없이 의미있는 결과만을 출력해 낸다. 플로팅포인트 타입을 엉뚱하게 int타입으로 출력한다든가 하는 일은 발생하지 않는다. C++의 표준출력객체 cou은 형식정보와 실제 타입정보를 하나로 묶어 관리함으로써 의도치 않은 동작이 발생하는 것을 미연에 방지할 수 있다.

여기에 더하여 printf()가 가지는 또하나의 맹점은 사용자정의 타입에 대한 지원을 전혀 할 수 없다는 것이다. 어떤 클래스 A의 객체 a가 있을 때, printf( "%a", a )와 같은 식으로 일관성 있는 조작을 할 방법이 전혀 없다. 하지만, cout객체로는 ostream 클래스에 대해 <<연산자만 오버로딩 하면 어떠한 사용자정의 타입이건 내장 타입과 같은 방식으로, 일관성 있게 사용할 수 있다는 점은 매우 중요한 강점이다.


이로써 C++에서 되도록이면 printf()를 쓰지 말아야할 몇가지 이유가 분명해진 셈이다. 물론 몇가지 특수한 상황에서는 printf()류의 함수가 유용할 때가 있으나, 일반적인 출력기능은 이제 cout에게 맡기는 편이 현명하지 않을까.

Posted by uhm