본문 바로가기

프로그래밍/C++

스트림에서 이진 파일 다루기

이 글을 읽기 전에 다음 글들을 읽기를 권장합니다.

  1. 한 스트림을 다른 스트림으로 연결하기

스트림의 두 가지 용법

스트림은 C의 파일 디스크립터와는 달리 타입에 따른 다른 입력을 제공합니다. 마치 자바에서 스트림으로 객체를 넘기면 toString()함수를 호출해 주듯이, int 형의 정수를 넣으면 자동으로 문자열로 변환하여 넣어주고 클래스와 스트림의 변환(Shift) 연산을 구현해주면 클래스 자체도 스트림으로 넘기고 받을 수 있게 해주죠.

위와 같은 기능을 서식화 입출력 기능( formatted I/O operation )이라고 합니다. 사용자의 타입을 항상 확인한 뒤에 그 타입을 스트림에 걸맞는 타입으로 변환하는 작업을 항상 진행해줍니다.

하지만 가끔은 문자의 그 자체를 이용하고 싶을 때가 있습니다. 예를들면 각 단어마다 간단한 암호화를 걸고 싶을 때. 문자 하나는 다른 의미없이 무조건 문자 하나라고 취급하고 싶을 때. 이런 경우에 서식화 입출력 기능은 원하는 내용도 아닐 뿐더러 비효율적입니다. 이 때 이용하는 것이 바로 비서식화 입출력 기능( unformatted I/O operation )입니다.

스트림 클래스는 위의 두 가지를 모두 제공하고 있습니다. 다만 별다른 제한 사항이 없기 때문에 두 가지를 혼용해서 쓰는 경우가 허다합니다. 스트림으로부터 값을 넣고 빼는 모든 함수들은 모두 서식화 용이거나 비서식화 용으로 구분되어 있습니다. 한 스트림의 두 가지 용법을 번갈아 쓰는 것은 매우 위험한 일이죠. 따라서 스트림을 이용할 때에는 자신이 어떤 용법을 이용할 것인지 꼭 생각하고 그 용법만 사용하는 것이 옳은 방향입니다.

이 글에서는 일반적으로 잘 알려져있지 않은 비서식화 입출력 기능을 위주로 말씀드리겠습니다.

비서식화 입출력 기능 이용법

  1. 생성시 플래그에 ios::binary를 꼭 추가한다.

    ios::binary는 해당 파일이 서식화된 스트림이 아님을 컴파일러에게 알립니다. 일반적으로 서식화된 스트림의 경우 개행문자나 널문자는 문자로 읽어오지 않는 경우가 허다합니다. 각 문자의 모든 내용을 검토하고 싶다면 ios::binary 플래그는 필수로 넣어주셔야 합니다.

  2. istream_iterator, ostream_iterator 대신 istreambuf_iterator, ostreambuf_iterator를 이용한다.

    스트림 반복자를 모르시는 사람들을 위해 간단히 설명하자면, 스트림 반복자는 스트림을 마치 컨테이너처럼 취급함으로써 몇몇 STL 알고리즘을 이용할 수 있게 해주는 단방향 반복자입니다. ( 단방향 반복자이기에 쓸 수 있는 STL 알고리즘은 꽤 한정되어있고, 이용 방법도 약간씩 다릅니다. 이에 대한 내용은 밑에서 다루도록 하겠습니다. )

    스트림 반복자는 스트림이 하나의 문자정보( character trait )로만 이루어져있다고 가정합니다. 이 스트림은 char로만 이루어져 있다고 가정하여 보겠습니다. 그 때에는 여느 반복자처럼 istream_iterator<char> 처럼 사용합니다. 그러면 마치 컨테이너를 돌듯이 스트림의 한 문자씩 빼내와 해당 코드를 시행합니다.

    여기서 스트림 반복자도 서식화, 비서식화의 두 가지 용법을 제공합니다. istreambuf_iterator는 비서식화, istream_iterator는 서식화 반복자입니다. 위에서 잠깐 언급한 적이 있는데 서식화된 문자는 특정 문자( 널문자 혹은 개행문자 )는 취급하지 않습니다. 문자열의 경우 개행문자는 토큰의 구분자로 사용되고 널문자는 문자열의 끝을 나타내죠. 내가 서식화 반복자를 이용한다면 이런 문자를 만나면 무시하거나 반복을 중단합니다. 이것은 문자열의 안전한 처리를 위한 것인데 오히려 그것이 사용자가 의도한 바를 못 나타내는 수가 있게 되죠.

    어떤 문자건 차별없이 행해주기 위해서는 비서식화 용법을 행해주어야 합니다. 이진 파일 플래그(ios::binary)를 올렸을 때와 istreambuf_iterator를 이용할 때에 각각 어떤 정확한 차이를 보이는 지는 확인하지 못했습니다. 참고로 istream_iterator를 이용할 떄에도 ios::skipsw 플래그를 켜 두면 istreambuf_iterator와 똑같이 동작한다고 합니다. 그래도 역시 가장 좋은 방법은 서식화 용법과 비서식화 용법을 따로 떼어주는 것입니다.

    아래는 사용법 예시입니다.

    void foo( istream &is, ostream &os )
    {
    	transform( istreambuf_iterator< char >( is ), 
    		istreambuf_iterator< char >(),
    		ostreambuf_iterator< char >( os ), Decode() );
    }
    	
  3. MSVC 7.0에서 istreambuf_iterator< unsigned char >는 구현되어 있지 않다.

    이유는 모르겠습니다만, MSVC 7.0에서 비서식화 스트림 반복자는 char만이 구현되어 있습니다. unsigned char로 컴파일을 했다가는 어떠한 암시적 변환을 해야되는지 모르겠다는 모호성 오류가 뜹니다. 이 외에도 많은 경우에 char는 구현이 되어있어도 unsigned char는 구현이 되어있지 않습니다. ( 일례로 unsigned char 간의 뺼셈도 정의되어있지 않습니다. ) 가능하면 char을 위주로 사용하는 것이 호환성이나 편의성에서 더욱 좋을 듯 합니다.

마치며

기존에 썼던 내용인데 이번에 스트림을 좀 더 다뤄보게 되면서 보강하게 되었습니다. 스트림은 쓰면 쓸수록 매력있으면서도 위험하네요. 잘 알고 쓰면 정말 빠르고 안전한 코드가 나오는 반면 모르고 쓰면 치명적인 독이 될 수 있는 코드가 바로 스트림이 아닌가 싶습니다. 그래서 요새 MFC나 DirectX를 쓸 때마다 괴롭습니다. 마소는 마소 나름의 코드 표현 방식을 지니고 있기 때문에 안 그래도 STL과는 정말 연계가 되지 않는데 스트림은 더욱더 심각하기 때문입니다.

하지만 스트림은 분명 모든 C++ 간의 호환을 지원하며 그 개념도 꽤 놀랍도록 정립되어 있는 편입니다. 개인적인 바램이 있다면 국내에서도 스트림을 자주 쓰는 사람들이 나왔으면 하는 바램이네요. 구글에서 스트림으로 검색해도 별 글이 나오지 않는다는 것은 상당히 안타깝습니다. 여러분들도 한 번 스트림을 쓰도록 노력해보는 건 어떻습니까 ?

참고

  1. Effective STL - Scott Meyers. Section 29 - 문자 단위의 입력에는 istreambuf_iterator의 사용도 적절하다.