36-1.iostream

입출력 스트림은 C++이 라이브러리 차원에서 제공하는 기본적인 입출력 방법이며 25장에서 간단한 사용 방법을 설명한 바 있다. 이 장에서는 입출력 스트림의 상세한 사용 방법과 여러 가지 입출력 옵션, 그리고 파일 입출력 방법에 대해 알아볼 것이다. 입출력은 언어의 가장 기본적인 기능임에도 불구하고 이 책은 뒷부분에서 따로 다루고 있는데 나름대로 이유가 있다.

우선 콘솔 프로젝트의 실용성이 크게 떨어지기 때문에 입출력 스트림을 실제 프로젝트에서 쓸 일이 거의 없고 설사 있다 하더라도 cout과 << 연산자만 알면 기본적인 입출력을 할 수 있으며 대체할 수 있는 방법들도 많은 편이다. 그보다 더 중요한 이유는 C++의 입출력 방법이 생각보다 훨씬 더 복잡해서 배우기 까다롭다는 점이다. 템플릿 기반인데다 C++의 거의 모든 문법들을 총 동원해서 만들어져 있기 때문에 내부까지 속속들이 이해하기는 무척 어렵다. 게다가 표준 확립전의 마구잡이식 확장으로 인해 컴파일러마다 동작이 조금씩 달라지기도 하고 한가지 일을 하는데 여러 가지 방법들이 존재해서 간결하지도 않다.

C의 printf와 비교해 볼 때 안정적이기는 하지만 편의성면에서 훨씬 더 떨어지며 성능상의 이점도 없다. 이런 복잡한 주제는 C++을 막 배우기 시작하는 사람들에게 흥미를 잃게 만들 위험이 있어 가급적 뒤쪽에 배치했다. 당장 이 내용이 꼭 필요치 않다면 입출력 스트림에 대해서는 그리 힘을 쏟을 필요가 없다. 예를 들어 윈도우즈 환경으로 바로 넘어갈 계획이라면 장담컨데 cout을 쓸 기회는 없을 것이다. cout을 처음 만들 때와 현재의 환경은 전혀 맞지 않다. 이 절에서는 지나치게 상세한 부분은 생략하고 요약적으로 설명을 전개한다.

36-1-가.입출력 스트림의 구조

C++의 표준 입출력 스트림은 여러 가지 복잡한 상황에 대해서도 입출력을 처리할 수 있도록 확장 가능한 클래스 계층을 구성하고 있다. 다음 그림은 비주얼 C++ 7.0의 헤더 파일을 기준으로 그려 본 간단한 클래스 계층도이다. 다른 컴파일러에서는 조금씩 달라질 수도 있다.

ios_base 클래스는 입출력과 관련된 여러 가지 상수나 플래그들을 가지며 이 클래스로부터 입출력 클래스들이 파생된다. 출력 클래스인 basic_ostream의 선언문을 보면 다음과 같이 되어 있다.

 

template<class _Elem, class _Traits>

class basic_ostream : virtual public basic_ios<_Elem, _Traits>

{

     ....

 

보다시피 실제 클래스가 아니라 다음과 같은 두 개의 인수를 취하는 클래스 템플릿이다.

 

_Elem

출력하는 데이터의 기본 타입이다. 통상 문자열 형태로 출력되므로 과거에는 char 타입의 문자들을 출력했으나 모든 문자를 16비트 코드를 표현하는 유니코드 환경에서는 wchar_t가 될 수도 있다. wchar_t는 unsigned short로 정의되어 있다. 만약 미래에 세상의 모든 문자를 32비트로 표현하는 코드 체계가 나온다면 _Elem은 unsigned long이 될 수도 있으며 basic_ostream 템플릿은 이런 타입에 대한 지정을 인수로 선택하도록 충분히 일반적으로 설계되어 있다.

_Traits

이 인수는 출력 문자열의 형태와 관리 방법을 정의하는 객체이다. 보편적으로 널 종료 문자열을 많이 사용하는데 시작 번지에서부터 문자가 나타나며 끝은 NULL 문자로 표현하는 방식이다. C/C++을 주로 사용하는 사람들에게는 이 문자열 형태가 아주 익숙하겠지만 이는 문자열을 표현하는 여러 가지 방법 중의 하나일 뿐이다. 베이직의 기본 문자열인 BSTR과 파스칼의 문자열 타입은 널 종료 문자를 사용하는 대신 선두에 문자열의 길이를 먼저 밝히고 문자열을 뒤에 저장한다. 다음은 "Format"이라는 문자열을 표현하는 두 가지 방법이다.

널 종료 문자열에 익숙한 상황에서 BSTR 포맷은 아주 이상하게 보이겠지만 두 방식 모두 일장 일단이 있다. BSTR은 길이 정보가 제일 앞에 있으므로 일일이 세 보지 않아도 문자열의 길이를 금방 알 수 있다는 것이 장점이고 널 종료 문자열은 시작 번지에 문자열이 바로 들어 있으므로 포인터를 통해 문자열을 다루기 쉽다. 반면 널 종료 문자열은 길이를 조사하는 속도가 무척 느린데 널 문자가 나올 때까지 메모리의 뒤쪽을 일일이 읽어보는 수밖에 없다.

이 두 가지 포맷 외에도 문자열을 표현할 수 있는 방법은 무수히 많을 것이다. 이런 방식을 지정하는 인수가 바로 _Traits인데 이 객체가 문자열의 길이를 관리하는 방법, 문자간의 순서를 정하는 방법 등을 결정한다. 디폴트인 char_Traits<_Elem> 객체는 우리에게 이미 익숙한 널 종료 문자열을 관리하지만 원한다면 다른 것으로 바꿀 수도 있다. 그래서 입출력 객체가 다루는 문자열은 반드시 NULL로 끝나지 않아도 되며 특정한 코드 체계에 종속되지도 않는다.

 

basic_ostream 클래스는 인수로 주어진 타입을 요소로 가지는 문자열을 표현하되 문자열을 다루는 방식은 _Traits객체에 따라 달라진다. 이 클래스로부터 두 개의 특수화된 클래스가 정의된다.

 

typedef basic_ostream<char, char_traits<char> > ostream;

typedef basic_ostream<wchar_t, char_traits<wchar_t> > wostream;

 

ostream은 char 타입의 문자열을 출력하는 클래스이며 char_traits<char> 객체는 C에서 보편적으로 사용되는 널 종료 문자열을 관리하는 객체이다. wostream은 wchar_t 타입의 문자열, 즉 유니코드 문자열을 출력하는 클래스이다. 과거 표준이 확립되기 전에는 ostream이 일반 클래스였지만 유니코드를 다룰 수 있도록 하기 위해 basic_ostream 부모 클래스로부터 상속받도록 수정되었다. 표준 입력도 마찬가지로 basic_istream 부모 클래스로부터 안시, 유니코드를 입력받는 두 개의 특수화된 클래스를 정의한다. 표준 라이브러리는 8개의 입출력 객체를 미리 정의하는데 iostream 헤더 파일을 보면 다음과 같은 선언문을 볼 수 있다.

 

extern istream cin;

extern ostream cout;

extern ostream cerr;

extern ostream clog;

extern wistream wcin;

extern wostream wcout;

extern wostream wcerr;

extern wostream wclog;

 

cin이 표준 입력 객체이며 cout이 표준 출력 객체이다. cerr은 표준 에러 객체이되 버퍼를 사용하지 않고 곧바로 출력을 내 보낸다는 점이 cout과 다르며 clog는 디버깅을 위한 기록 객체이며 버퍼를 사용한다. cerr과 clog로의 출력은 둘 다 모니터로 나가게 되어 있지만 재지향될 경우 다른 장치로 출력을 내 보낼 수도 있다. 아래쪽의 w가 붙은 객체들은 유니코드 입출력 객체들이다.

표준 입출력 객체들은 효율적인 입출력 관리를 위해 내부적으로 버퍼를 사용한다. 입출력할 때마다 한 문자씩 장치로 직접 입출력하면 느리기 때문에 버퍼가 필요하다. 이 버퍼는 streambuf라는 내부 클래스에 의해 자동으로 관리된다.