제 6 장: IO-스트림 라이브러리

C++C 언어에서 잘 알려진 표준 스트림 (FILE) 접근법을 확장하여 class 개념에 기반한 입/출력 (I/O) 라이브러리를 제공한다.

모든 C++ I/O 편의기능은 std 이름공간에 정의된다. std:: 이름공간은 모호할 경우는 제외하고 앞으로 생략한다.

앞에서 C++ I/O 라이브러리를 사용하는 여러 예제를 보았다 (제 3장). 특히 삽입 연산자(<<)와 추출 연산자 (>>)를 보여주었다. 이 장은 I/O 기능을 더 자세하게 연구한다.

C++ 언어에서의 입출력 편의기능의 연구는 class 개념과 멤버 함수의 표기법을 많이 사용한다. 클래스 생성을 아직 다루지 않았다 (제 7장). 그리고 상속도 공식적으로 다루지 않았다 (제 13장). 그러나 I/O 편의기능을 미리 다루어 보고 나면 클래스 생성의 기술적 배경을 알 수 있을 것이다.

C++ I/O 클래스는 대부분 (basic_ios처럼) basic_으로 시작하는 이름이 있다. 그렇지만 이렇게 basic_ 접두사가 붙은 이름은 C++ 프로그램에서 자주 볼수는 없다. 대부분의 클래스는 typedef를 사용하여 정의되기 때문이다. 예를 들어:

        typedef basic_ios<char>       ios;
C++는 다양한 문자 유형을 지원한다 (예, char, wchar_t). I/O 편의기능은 템플릿 메커니즘을 사용하여 개발되었다. 전통적인 char 유형말고 다른 문자 유형으로 쉽게 변환할 수 있다. 제 21장에 더 정밀하게 다듬겠지만 이것으로 총칭 소프트웨어를 만들 수도 있다. 문자를 나타내는 어떠한 유형에도 사용이 가능하다. 그래서 위의 typedef와 비슷하게 다음이 존재한다.
        typedef basic_ios<wchar_t>    wios;
이렇게 하면 wchar_t 유형도 정의할 수 있다. 그래서 basic_ 접두사는 흐름이 끊기지만 않는다면 C++ 주해서에서 생략했다. C++ 주해서는 주로 표준 8-비트 char 유형에 초점을 둔다.

iostream 객체는 표준 전방 선언을 사용하여 선언할 수 없다. 예를 들어:

    class std::ostream;     // 이제는 에러가 된다.
대신에 iostream 클래스를 선언하려면 <iosfwd> 헤더를 포함해야 한다.
    #include <iosfwd>       // iostream 클래스를 선언하는 올바른 방법

C++ I/O를 사용하면 전통적으로 유형에 안전하다는 장점이 있다. 객체는 (또는 평범한 값들은) 스트림 안으로 삽입된다. 이것을 C에서 자주 맞이하는 상황에 비교해 보라. fprintf 함수는 어디에 어떤 종류의 값을 사용할지 형식화 문자열로 나타낸다. 이것을 다음의 상황과 비교해 보라. C++iostream 접근법은 값이 나타나는 곳에 객체를 다음과 같이 곧바로 사용한다.

    cout << "There were " << nMaidens << " virgins present\n";
컴파일러는 nMaidens 값의 유형을 알아채고 cout iostream 안으로 삽입되는 문장 안의 적절한 곳에 적절한 값을 삽입한다.

이것을 C에서 만나는 상황에 비교해 보라. C 컴파일러는 나날이 똑똑해지고 있다. 설계가 잘된 C 컴파일러는 printf 서술문의 인자 목록에 상응하는 위치에서 형식 지정자와 변수의 유형이 서로 일치하지 않으면 경고해 준다. 그러나 경고해 주는 것 이상의 일을 별로 하지 못한다. 유형에 안전한 C++는 유형 불일치로부터 여러분을 지켜 준다. 일치시켜 볼 유형이 전혀 없기 때문이다.

이 외에도 iostreamC에 사용된 표준 FILE 기반의 I/O와 거의 비슷하게 기능을 제공한다. 파일을 열고, 닫고, 위치를 찾고, 읽고, 쓰고 등등이 가능하다. C++에서 기본 FILE 구조는 C에 사용되는 것처럼 여전히 사용할 수 있다. 그러나 C++는 여기에 클래스 기반의 I/O가 추가되며 결과적으로 유형에 안전하고 확장성이 있으며 깔끔하게 디자인이 가능해진다.

ANSI/ISO 표준에서 그 의도는 I/O에 독립적인 골격구조를 세우는 것이었다. 이전의 iostream 라이브러리 구현이 언제나 표준을 따른 것은 아니다. 결과적으로 표준에 많은 확장이 추가되었다. 이전에 개발된 소프트웨어에서 I/O 부분은 부분적으로 재작성해야 할 가능성도 있다. 이것은 구형 소프트웨어를 수정해야 하는 이에게는 힘든 일이다. 그러나 ANSI/ISO 표준을 준수하는 I/O를 사용하여 이미 구축되어 있으므로 모든 특징과 확장을 쉽게 재구축할 수 있다. 이 모든 구현을 이 장에 다루는 것은 아니다. 많은 재구현이 상속과 다형성에 의존하기 때문이다. 이 주제는 공식적으로 제 13장14장에 다룬다. 선택된 재구현은 제 24장에 제공하고 이 장은 다른 장에 있는 적절한 항목을 참조하는 것으로 마무리한다.

Figure 3 is shown here.
그림 3: 핵심 I/O 클래스

이 장은 다음과 같이 조직되어 있다 (그림 3 참고): 스트림 객체는 제한적이지만 중요한 역할이 있다. 한편으로 입력이나 출력에 대한 객체로서 그리고 또 다른 한편으로 streambuf 객체 사이의 인터페이스로서 streambuf 객체가 접근하는 장치와의 실제 입력과 출력을 책임진다.

이 접근법으로 새로운 종류의 장치에 새로운 종류의 streambuf를 생성할 수 있다. `백전노장의' istream 클래스와 ostream클래스의 편의기능과 조합하여 streambuf를 사용할 수 있다. iostream 객체의 형식화 임무와 streambuf 객체로 구현된 외부 장치와의 버퍼링 인터페이스 사이의 차이를 이해하는 것이 중요하다. (소켓이나 파일 기술자 같은) 새 장치와의 인터페이스는 새로운 종류의 streambuf를 생성하기를 요구한다. 새로운 종류의 istream이나 ostream 객체를 요구하지 않는다. 그렇지만 istream 클래스나 ostream 클래스를 포장 클래스로 두르면 특수 장치에 쉽게 접근할 수 있다. 이 방식으로 stringstream 클래스를 만들었다.

6.1: 특별한 헤더 파일

iostream과 관련된 여러 헤더 파일을 사용할 수 있다. 당면한 상황에 따라 다음 헤더 파일을 사용해야 한다.

6.2: `ios_base' 바탕 클래스

std::ios_base 클래스는 모든 I/O 연산을 위한 토대이다. 무엇보다도 I/O 스트림의 상태를 조사하는 기능과 대부분의 출력 형식화 기능을 정의한다. I/O 라이브러리의 스트림 클래스는 모두 이 클래스부터 시작하여 ios 클래스를 통하여 파생되고 그의 모든 능력을 상속받는다. ios_base는 모든 C++ I/O 기능이 건설되는 토대이기 때문에 여기에 C++ I/O 라이브러리의 첫 클래스로 소개한다.

C에서처럼 C++의 I/O는 언어의 일부가 아니다 (물론 C++에 관한 ANSI/ISO 표준에 포함된다). 미리 정의된 모든 I/O 편의기능을 기술적으로 무시할 수는 있지만 그렇게 하는 사람은 없다. 그러므로 I/O 라이브러리는 C++를 위한 사실상의 I/O 표준이다. 앞에서 언급했듯이 iostream 클래스 자체는 I/O 사건에 책임을 지지 않고 보조 클래스에게 맡긴다. streambuf 클래스나 그로부터 파생된 클래스에 위임하는 것이다.

직접적으로 ios_base 객체를 생성하는 것은 불가능할 뿐만 아니라 필수도 아니다. 생성은 언제나 클래스 계통도를 따라 내려오다가 std::ios와 같은 한 객체를 생성하는 부작용으로 일어날 뿐이다. ios는 iostream 계통도에서 다음을 차지한다 (그림 3 참고). 모든 스트림 클래스가 차례로 ios로부터 상속을 받고 그리하여 또 ios_base로부터 상속을 받는다. ios_baseios의 구별은 실제로 중요하지 않다. 그러므로 실제로 ios_base의 편의기능은 ios가 제공하는 편의기능으로 간주된다. 특정한 편의기능이 어느 클래스에 실제로 정의되어 있는지 궁금한 독자는 적절한 헤더를 참조해야 한다 (예를 들어 ios_base.hbasic_ios.h를 참고하라).

6.3: `streambuf' 객체 인터페이스: `ios' 클래스

std::ios 클래스는 ios_base 클래스로부터 직접적으로 파생된다. 사실상 C++ I/O 라이브러리의 모든 스트림 클래스가 건설되는 토대이다.

ios 객체를 직접적으로 생성할 수는 있지만 그런 일은 거의 없다. ios 클래스의 목적은 basic_ios 클래스의 편의기능을 제공하는 것이다. 그리고 streambuf 객체와 관련된 여러 편의기능을 새로 추가한다. 이 기능들은 ios 클래스의 객체가 관리한다.

다른 모든 스트림 클래스는 직접 또는 간접적으로 ios로부터 파생된다. 이것은 제 13장에 설명했듯이 iosios_base의 모든 편의기능을 다른 스트림 클래스에서도 사용할 수 있다는 뜻이다. 이런 추가 스트림 클래스를 다루기 전에 먼저 ios 클래스가 제공하는 편의기능들을 소개한다 (묵시적으로: ios_base가 제공).

명시적으로 ios를 포함해야 할 경우도 있다. 예를 들어 형식화 깃발 자체가 소스 코드에 참조될 경우가 그렇다 (6.3.2.2목).

ios 클래스는 여러 멤버 함수를 제공한다. 대부분 형식화에 관련이 있다. 자주 사용되는 다른 멤버 함수는 다음과 같다.

6.3.1: 조건 상태

스트림 연산은 다양한 이유로 실패할 수 있다. 연산이 실패할 때마다 스트림 연산은 곧바로 정지된다. 스트림의 조건 상태를 조사해 보고 설정을 다시 하거나 지울 수도 있다. 프로그램은 포기하는 대신에 복구를 시도할 수도 있다. 스트림의 상태를 질의하고 조작하기 위한 멤버를 이 항에 기술한다.

조건은 다음 조건 깃발로 나타낸다.

ios 객체의 상태를 결정하거나 조작하기 위하여 여러 조건 멤버 함수를 사용할 수 있다. 원래는 int 값을 돌려주었는데 지금은 bool유형이 반환된다.

에러 조건을 관리하기 위해 다음 멤버들을 사용할 수 있다.

C++는 예외 상황을 처리하기 위한 예외 메커니즘을 지원한다. ANSI/ISO 표준에 따라 예외는 스트림 객체에 사용할 수 있다. 예외는 제 10장에 다룬다. 예외를 스트림 객체와 함께 사용하는 것은 10.7절에 다룬다.

6.3.2: 입력과 출력의 형식화

정보를 스트림에 쓰는 방식은 (또는 스트림으로부터 읽는 방식은) 형식화 깃발로 제어한다.

출력 필드나 입력 버퍼의 너비를 설정할 필요가 있을 때 그리고 어느 값을 화면에 보여줄지 형식을 결정할 필요가 있을 때 형식화를 사용한다 (예를 들어 radix). 형식화 특징은 대부분 ios 클래스의 영역에 속한다. 형식화는 ios 클래스에 정의된 깃발로 제어된다. 이런 깃발들은 두 가지 방식으로 조작할 수 있다. 전문 멤버 함수를 사용하거나 아니면 조작자를 사용하는 것이다. 직접적으로 스트림으로부터 추출되거나 또는 스트림 안으로 삽입된다. 둘 중에 어떤 방법을 사용해야 하는지 특별한 이유는 없다. 두 방법 모두 가능하다. 다음 개관에서 다양한 멤버 함수를 먼저 소개한다. 다음에 깃발과 조작자를 다룬다. 깃발을 어떻게 조작하고 그 효과는 어떤지 보여주는 예제를 제공한다.

매개변수가 없는 조작자가 많다. 그리고 스트림 헤더 파일을 포함하면 하면 사용할 수있다 (즉, iostream). 어떤 조작자는 인자를 요구하기도 한다. 인자를 요구하는 조작자를 사용하려면 iomanip 헤더를 포함해야 한다.

6.3.2.1: 형식을 변경하는 멤버 함수

I/O 형식 깃발을 조작하기 위하여 여러 멤버 함수를 사용할 수 있다. 아래에 나열된 멤버를 사용하는 대신에 조작자를 사용할 수 있다. 직접적으로 스트림에 삽입하거나 추출할 수 있다. 사용 가능한 멤버를 알파벳 순서로 나열했다. 그러나 실제로 가장 중요한 멤버는 setfunsetf 그리고 width이다.

6.3.2.2: 형식화 깃발

형식화 깃발은 대부분 출력 정보와 관련이 있다. 정보는 기본적으로 두 가지 방식으로 출력 스트림에 쓸 수 있다. 이진 출력을 사용하면 정보는 직접적으로 출력 스트림에 씌여진다. 미리 인간이 읽을 수 있는 형식으로 변환되지 않는다. 형식화 출력을 사용하면 메모리에 저장된 값들이 먼저 인간이 읽을 수 있는 텍스트로 변환된 다음 출력 스트림에 씌여진다. 형식화 깃발은 이런 변환이 일어나는 방식을 정의한다. 이 목에 형식화 깃발을 모두 다룬다. 형식화 깃발은 멤버 함수로 설정하거나 지울 수 있지만 조작자도 같은 효과가 있다. 각각의 깃발을 어떻게 멤버 함수 또는 (있다면) 조작자로 제어할 수 있는지 보여준다.

정보를 넓게 보여주려면:

다양한 숫자 표현을 사용하기:

값을 세밀하게 조율하여 보여주기:

부동 소수점 수 보여주기

공백 처리와 스트림 비우기

6.4: 출력

C++에서 출력은 주로 std::ostream 클래스에 기반한다. ostream 클래스는 정보를 스트림에 삽입하는 삽입 연산자 (<<)와 write와 같이 형식이 없는 정보를 스트림에 쓰는 멤버가 있다.

ostream 클래스는 여러 다른 클래스를 위한 바탕 클래스로 작동한다. ostream 클래스의 모든 기능을 제공하지만 각자 특수한 기능을 보유한다. 이 절은 다음 클래스들을 연구한다.

6.4.1: 기본 출력: `ostream' 클래스

ostream 클래스는 기본 출력 편의기능을 정의한다. coutclog 그리고 cerr 객체는 모두 ostream 객체이다. 출력에 관련하여 ios 클래스가 제공하는 모든 편의기능을 ostream 클래스에서도 사용할 수 있다.

다음 ostream 생성자와 같이 ostream 객체를 정의할 수도 있다.

ostream 클래스를 C++ 소스에 선언하려면 <ostream> 헤더를 포함해야 한다. 미리 정의된 ostream 객체 (std::cin, std::cout 등등)을 사용하려면 <iostream> 헤더를 포함해야 한다.

6.4.1.1: `ostream' 객체에 쓰기

ostream 클래스는 형식화 출력과 이진 출력을 모두 지원한다.

삽입 연산자 (<<)는 유형에 안전하게 ostream 객체에 값을 삽입한다. 이것을 형식화된 출력이라고 부른다. 컴퓨터 메모리에 저장된 이진 값들이 인간이 읽을 수 있는 ASCII 문자로 특정한 형식화 규칙에 맞게 변환되기 때문이다.

삽입 연산자는 정보를 받을 ostream 객체를 가리킨다. 보통의 << 연상 작용은 바뀌지 않는다. 그래서 다음과 같은 서술문을 만나면

    cout << "hello " << "world";
가장 왼쪽의 피연산자가 먼저 평가되고 (cout << "hello ") 그 다음에 실제로는 cout 객체인 ostream & 객체가 반환된다. 이제 서술문은 다음과 같이 줄어든다.
    cout << "world";
그리고 두 번째 문자열이 cout에 삽입된다.

<< 연산자는 (재정의된) 변형이 많이 있다. 그래서 다양한 유형의 변수를 ostream 객체에 삽입할 수 있다. 중복정의 << 연산자가 있다. intdouble 그리고 포인터 등등을 기대한다. 연산자마다 ostream 객체를 돌려준다. 그 안에 지금까지의 정보가 삽입되어 있다. 그래서 다음 삽입 때 즉시 그대로 사용할 수 있다.

스트림은 형식화 출력을 위한 편의기능이 없다. 예를 들어 Cprintfvprintf 함수 같은 기능이 없다. 이런 편의기능을 스트림의 세계에 구현하는 것은 어렵지 않다. 그러나 printf-류의 기능은 C++ 프로그램에 거의 필요하지 않다. 게다가 잠재적으로 유형에 안전하지 않다. 그래서 이 기능은 완전히 피하는 편이 더 좋다.

이진 파일을 써야 할 때 텍스트 형식화는 사용되지 않으며 필요하지도 않다. int 값은 일련의 날 바이트로 써야 하며 ASCII 숫자로 0부터 9까지 쓰면 안된다. ostream 객체의 다음 멤버들은 `이진 파일'을 쓸 수 있다.

6.4.1.2: `ostream'의 위치 변경

모든 ostream 객체가 위치 변경 능력을 지원하는 것은 아니지만 보통은 지원한다. 다시 말해 이전에 씌여진 스트림의 한 부분을 다시 쓸 수 있다는 뜻이다. 위치 변경은 데이터베이스 어플리케이션에서 자주 사용된다. 데이터 베이스에 있는 정보에 무작위로 접근할 수 있어야 하기 때문이다.

현재 위치는 다음 멤버로 얻고 변경한다.

6.4.1.3: `ostream' 비우기

ios::unitbuf 깃발이 서 있지 않는 한, ostream 객체에 씌여지는 정보는 곧바로 물리적 스트림에 씌여지지 않는다. 대신에 쓰기 연산을 하는 동안 내부 버퍼에 채워지고 가득 차면 그 때야 버퍼가 비워진다.

스트림의 내부 버퍼는 프로그램의 통제 아래에서 비울 수 있다.

6.4.2: 파일에 출력하기: `ofstream' 클래스

std::ofstream 클래스는 ostream 클래스로부터 파생된다. ostream 클래스와 능력이 같지만 쓰기 용으로 파일에 접근하거나 파일을 생성할 수 있다.

ofstream 클래스를 사용하려면 <fstream> 헤더를 포함해야 한다. fstream을 포함하더라도 자동으로 표준 스트림 cincout 그리고 cerr를 사용할 수 있는 것은 아니다. 표준 스트림을 사용하려면 iostream을 포함해야 한다.

다음 생성자들을 ofstream 객체에 사용할 수 있다.

파일 기술자를 사용하여 ofstream을 여는 것은 불가능하다. 그 이유는 파일 기술자가 널리 운영 체제마다 갖추어져 있는 것은 아니기 때문이다. 불행하게도 파일 기술자는 (간접적으로) std::streambuf 객체와 함께 사용할 수 있다 (어떤 구현에서는 std::filebuf 객체와 사용된다. 이 역시 streambuf이다). streambuf 객체는 14.8절에 다루고 filebuf 객체는 14.8.2항에 다룬다.

직접적으로 ofstream 객체를 파일에 연관짓는 대신에 먼저 그 객체를 생성한 다음에 열 수 있다.

6.4.2.1: 스트림 객체 열기 모드

ofstream(또는 istream)을 생성하거나 열 때 다음의 파일 모드 또는 파일 깃발을 사용할 수 있다 (6.5.2항). 이 값들은 유형이 ios::openmode이다. bitor 연산자를 사용하면 깃발들을 조합할 수 있다. 다음의 파일 깃발 조합은 특별한 뜻이 있다.
in | out:           스트림은 읽기와 쓰기용이다. 그렇지만,
                    파일이 반드시 존재해야 한다.
in | out | trunc:   스트림은 읽기 쓰기용이다.
                    먼저 빈 파일을 (재)생성한다.
흥미로운 점은 ifstreamofstream 그리고 fstream 클래스의 open 멤버에 ios::openmode 유형의 두 번째 매개변수가 있다는 것이다. 이와 대조적으로 두 개의 열거 값에 적용하면 bitor 연산자는 int를 돌려준다. 그럼에도 어떻게 여기에 bitor 연산자를 사용할 수 있는지는 11.11절에 해답이 있다 .

6.4.3: 메모리로 출력: `ostringstream' 클래스

stream 편의기능을 이용하여 정보를 메모리에 쓰려면 std::ostringstream 객체를 사용해야 한다. ostringstream 클래스는 ostream 클래스로부터 파생되기 때문에 ostream의 모든 편의기능을 ostringstream 객체도 사용할 수 있다. ostringstream 객체를 정의하고 사용하려면 <sstream> 헤더를 포함해야 한다. ostringstream 클래스는 다음 생성자와 멤버를 제공한다. 다음 예제는 ostringstream 클래스의 사용법을 보여준다. 여러 값들을 객체에 삽입한다. 다음, ostringstream 객체가 보유한 텍스트가 std::string으로 저장된다. 그 다음 그의 길이와 내용이 인쇄된다. 그런 ostringstream 객체들은 `유형을 문자로' 변환에, 예를 들어 int 값을 텍스트로 변환하는 데 자주 사용된다. 형식화 깃발을 ostringstreams과 함께 사용해도 된다. ostream 클래스의 일부이기 때문이다.

다음은 ostringstream 객체가 어떻게 사용되는지 보여준다.

    #include <iostream>
    #include <sstream>

    using namespace std;

    int main()
    {
        ostringstream ostr("hello ", ios::ate);

        cout << ostr.str() << '\n';

        ostr.setf(ios::showbase);
        ostr.setf(ios::hex, ios::basefield);
        ostr << 12345;

        cout << ostr.str() << '\n';

        ostr << " -- ";
        ostr.unsetf(ios::hex);
        ostr << 12;

        cout << ostr.str() << '\n';

        ostr.str("new text");
        cout << ostr.str() << '\n';

        ostr.seekp(4, ios::beg);
        ostr << "world";
        cout << ostr.str() << '\n';
    }
    /*
        이 프로그램의 출력:
    hello
    hello 0x3039
    hello 0x3039 -- 12
    new text
    new world
    */

6.5: 입력

C++에서 입력은 주로 std::istream 클래스에 기반한다. istream 클래스는 스트림으로부터 정보를 추출하는 추출 연산자 (>>)와 비형식 정보를 읽는 특수 멤버를 정의한다. 예를 들어 istream::read 멤버는 스트림으로부터 비형식 정보를 읽는다.

istream 클래스는 여러 다른 클래스들을 위한 바탕 클래스로 기여한다. 파생 클래스들은 istream 클래스의 기능을 제공하면서도 따로 자신만의 특징을 제공한다. 지금부터 다음 클래스들을 연구한다.

6.5.1: 기본 입력: `istream' 클래스

istream 클래스는 기본적인 입력 편의기능을 정의한다. cin 객체는 istream 객체이다. ios 클래스가 정의한 입력과 관련된 모든 편의기능은 istream 클래스에서도 사용할 수 있다.

다음 istream 생성자를 사용하면 istream 객체를 정의할 수 있다.

istream 클래스를 정의하려면 <istream> 헤더를 포함해야 한다. 미리 정의된 istream 객체인 cin을 사용하려면 <iostream> 헤더를 포함해야 한다.

6.5.1.1: `istream' 객체로부터 읽기

istream 클래스는 형식 그리고 비형식 이진 입력을 모두 지원한다. 추출 연산자(operator>>)istream 객체로부터 유형에 안전하게 값을 추출한다. 이것을 이른바 형식화 입력이라고 부른다. 인간이 읽을 수 있는 ASCII 문자들은 특정한 형식화 규칙에 따라 이진 값으로 변환된다.

추출 연산자는 새로운 값을 받는 객체나 변수를 가리킨다. >>의 보통의 연관성은 그대로 바뀌지 않는다. 그래서 다음과 같은 서술문을 만나면

    cin >> x >> y;
가장 왼쪽의 두 피연산자가 먼저 평가된다 (cin >> x). 실제로 cin과 같은 객체인 istream & 객체가 반환된다. 이제 서술문은 다음과 같이 줄어든다.
    cin >> y
그리고 y 변수는 cin으로부터 추출된다.

>> 연산자는 (중복정의) 변형이 많다. 그래서 다양한 유형의 변수들을 istream 객체로부터 추출할 수 있다. int, double, 문자열, 문자 배열, 포인터 등등의 추출에 중복정의 >> 버전을 사용할 수 있다. 문자열 또는 문자 배열 추출은 기본값으로 먼저 모든 공백 문자들을 건너뛴 다음, 연속되어 있는 비공백 문자들을 모두 추출한다. 추출 연산자 처리가 끝났으면 정보가 추출된 istream객체가 반환된다. 그리고 그것을 다음 표현식에 나타나는 추가 istream 연산에 즉시 사용할 수 있다.

스트림은 (Cscanfvscanf 함수 같은) 형식화 입력을 위한 편의기능이 없다. 이런 편의기능을 스트림의 세계에 추가하는 것이 어렵지는 않지만 scanf 같은 기능은 C++ 프로그램에 거의 필요가 없다. 게다가 잠재적으로 유형에 안전하지 않기 때문에 형식화 입력은 완전히 피하는 편이 더 좋다.

이진 파일을 읽어야 할 때 정보는 형식화되지 않는다. 다시 말해, int 값은 있는 그대로 일련의 바이트 연속열로 읽어야 한다. ASCII 숫치 문자 0부터 9로 읽지 않는다. istream 객체로부터 정보를 읽는 데 다음 멤버 함수를 사용할 수 있다.

6.5.1.2: `istream' 위치 변경

모든 istream 객체가 위치 변경을 지원하는 것은 아니다. 그러나 지원하는 객체가 있다. 이것은 스트림에서 같은 부분을 반복적으로 읽을 수 있다는 뜻이다. 위치 변경은 데이터베이스 어플리케이션에 자주 사용된다. 데이터 베이스에 있는 정보에 무작위로 접근할 수 있어야 하기 때문이다.

현재 위치는 다음 멤버로 열람하고 변경할 수 있다.

6.5.2: 파일로부터 입력: `ifstream' 클래스

std::ifstream 클래스는 istream 클래스로부터 파생된다. istream 클래스와 능력이 같지만 읽기용으로 파일에 접근할 수 있다.

ifstream 클래스를 사용하려면 <fstream> 헤더를 포함해야 한다. fstream을 포함하더라도 자동으로 표준 스트림 cincout 그리고 cerr를 사용할 수 있는 것은 아니다. 표준 스트림을 선언하려면 iostream 헤더를 포함하라.

ifstream 객체에 다음 생성자들을 사용할 수 있다.

직접적으로 ifstream 객체를 파일에 연관짓는 대신에, 객체를 먼저 생성하고 나중에 열 수 있다.

6.5.3: 메모리로부터 입력: `istringstream' 클래스

stream 편의기능을 사용하여 메모리로부터 정보를 읽으려면 std::istringstream 객체를 사용해야 한다. istringstream 클래스는 istream 클래스로부터 파생되었기 때문에 istream의 모든 편의기능은 istringstream 객체도 사용할 수 있다. istringstream 객체를 정의하고 사용하려면 <sstream> 헤더를 포함해야 한다. istringstream 클래스는 다음 생성자와 멤버를 제공한다. 다음 예제는 istringstream 클래스의 사용법을 보여준다. 여러 값들을 이 객체로부터 추출한다. 예를 들어 istringstream 객체는 문자열을 int 값으로 변환하는 데 자주 사용된다 (Catoi 함수와 비슷하다). 형식 깃발을 istringstreams와 함께 사용할 수도 있다. istream 클래스의 일부이기 때문이다. 예제에서 특히 seekg 멤버의 사용에 주목하라:
    #include <iostream>
    #include <sstream>
    using namespace std;

    int main()
    {
        istringstream istr("123 345");  // 텍스트를 저장한다.
        int x;

        istr.seekg(2);              // "12"를 건너뛴다.
        istr >> x;                  // int를 추출한다.
        cout << x << '\n';          // 출력한다.
        istr.seekg(0);              // 처음부터 다시 시도한다.
        istr >> x;                  // int를 추출한다.
        cout << x << '\n';          // 출력한다.
        istr.str("666");            // 또다른 텍스트를 저장한다.
        istr >> x;                  // 그것을 추출한다.
        cout << x << '\n';          // 출력한다.
    }
    /*
        이 프로그램의 출력:
    3
    123
    666
    */

6.5.4: 스트림 복사하기

파일 복사는 소스 파일을 문자 단위로 또는 줄 단위로 읽어서 처리한다. 스트림을 처리하는 기본 모형은 다음과 같다. 먼저 읽어보아야 테스트할 수 있음을 유념하라. 실제로 파일 읽기를 시도해 본 후에만 읽기에 실패했는지 아니면 성공했는지 알 수 있다. 물론 변형도 가능하다. getline(istream &, string &)istream &을 돌려준다 (6.5.1.1목). 그래서 읽기와 테스트는 하나의 표현식으로 축소할 수 있다. 그럼에도 위의 모형은 일반적 사례를 대표한다. 그래서 다음 프로그램을 사용하면 cincout으로 복사할 수 있다.
#include <iostream>
using namespace::std;

int main()
{
    while (true)
    {
        char c;

        cin.get(c);
        if (cin.fail())
            break;
        cout << c;
    }
}

더 줄일 수도 있다. getif 서술문과 조합하면 결과는 다음과 같다.

    if (!cin.get(c))
        break;
그럼에도 이것은 여전히 기본 규칙을 따를 것이다. `먼저 읽어라. 테스트는 그 다음이다'.

단순히 파일만 복사하는 일은 별로 요구되지 않는다. 자주 요구되는 상황은 파일을 특정한 지점까지 처리한 다음에 예와 같이 파일의 나머지 정보를 복사하는 것이다. 다음 프로그램은 이것을 보여준다. ignore를 사용하여 첫 줄을 건너뛰고 (예제를 위해 첫 줄은 기껏해야 길이가 80 문자라고 간주함) 두 번째 서술문은 또다른 중복정의 << 삽입 연산자 버전을 사용한다. streambuf 포인터가 스트림 안으로 삽입된다. 스트림의 streambuf *를 돌려주기 때문에 rdbuf 멤버는 스트림의 내용을 ostream 안으로 삽입할 간단한 도구이다.

    #include <iostream>
    using namespace std;

    int main()
    {
        cin.ignore(80, '\n');   // 첫 줄을 건너뛴다. 그리고...
        cout << cin.rdbuf();    // 나머지는 streambuf *를 통하여 복사한다.
    }

이런 식으로 스트림을 복사하는 것은 반드시 streambuf 객체가 존재한다고 전제하기 때문이다. 결론적으로 streambuf의 특정화된 모든 객체와 함께 사용할 수 있다.

6.5.5: 스트림 연결하기

tie 멤버 함수를 사용하면 ostream 객체를 ios 객체에 연결할 수 있다. 연결하면 ios 객체에 입력 또는 출력 연산이 수행될 때마다 ostream 객체에 묶인 버퍼가 비워진다. 기본으로 coutcin에 묶인다 (cin.tie(cout)를 사용). 이 묶기는 cin에 연산을 요구할 때마다 cout이 먼저 비워진다는 뜻이다. 결합을 깨려면 예제와 같이 ios::tie(0)을 호출하면 된다.

스트림을 결합하는 또다른 유용한 예는 cerr 스트림과 cout 스트림을 묶는 것이다. 이렇게 결합하면 표준 출력과 에러 메시지가 생성되는 순간에 동기적으로 화면에 보여준다.

    #include <iostream>
    using namespace std;

    int main()
    {
        cerr.tie(0);        // 푼다.
        cout << "first (buffered) line to cout ";
        cerr << "first (unbuffered) line to cerr\n";
        cout << "\n";

        cerr.tie(&cout);    // cout을 cerr에 묶는다.
        cout << "second (buffered) line to cout ";
        cerr << "second (unbuffered) line to cerr\n";
        cout << "\n";
    }
    /*
        출력:

        first (unbuffered) line to cerr
        first (buffered) line to cout
        second (buffered) line to cout second (unbuffered) line to cerr
    */

스트림을 결합하는 또다른 방법은 스트림이 streambuf 객체를 사용하도록 만드는 것이다. 이것은 ios::rdbuf(streambuf *) 멤버 함수로 구현할 수 있다. 이런 식으로 두 스트림은 각자의 형식을 사용할 수 있다. 한 스트림은 입력에 사용하고 다른 스트림은 출력에 사용할 수 있다. 그리고 운영 체제의 호출 말고 스트림 라이브러리로 방향전환을 구현할 수 있다. 예제는 다음 절을 참고하라.

6.6: 고급 주제

6.6.1: 스트림 이동하기

(이 장에 다룬 모든) 스트림 클래스는 이동할 수 있고 서로 바꿀 수 있다. 이것은 스트림 클래스 용도로 공장 함수를 설계할 수 있다는 뜻이다. 다음은 한 예이다.
    ofstream out(string const &name)
    {
        ofstream ret(name);             // ofstream을 생성한다.
        return ret;                     // 반환 값을 최적화한다.
    }                                   // 그러나 문제 없음. 이동을 지원하기 때문이다.
    
    int main()
    {
        ofstream mine(out("out"));       // 반환 값을 최적화한다.
                                         // 그러나 문제 없음. 이동을 지원하기 때문이다.

        ofstream base("base");
        ofstream other;

        base.swap(other);               // 스트림 교체 문제 없음

        other = std::move(base);        // 스트림 이동 문제 없음

        // other = base;                // 이것은 실패한다.
                                        // 스트림에는 복사할당이 불가능하기 때문이다.
    }

6.6.2: 스트림 방향전환하기

ios::rdbuf를 사용하면 스트림이 streambuf 객체를 공유하도록 강제할 수 있다. 그래서 한 스트림에 씌여지는 정보는 실제로 또다른 스트림에 씌여진다. 이것을 방향전환(redirection)이라고 부른다. 방향전환은 주로 운영 체제 수준에서 구현되지만 여전히 필요한 경우가 가끔 있다 (24.2.3항).

방향전환이 유용한 상황은 주로 에러 메시지를 표준 에러 스트림이 아니라 파일로 써야 할 때이다. 파일 기술자 번호 2로 나타낸다. 유닉스 운영 체제에서 bash 쉘을 사용하면 다음과 같이 이것을 실현할 수 있다.

    program 2>/tmp/error.log
이 명령어 이후로 program이 쓰는 모든 에러 메시지는 화면에 나타나는 대신에 /tmp/error.log 파일에 저장된다.

다음은 streambuf 객체로 이것을 구현하는 예이다. program은 에러 메시지를 쓸 파일의 이름을 정의한 인자를 기대한다고 간주하자. 다음과 같이 호출할 수 있다.

    program /tmp/error.log
프로그램은 다음과 같다. 소스에 설명을 달았다.
    #include <iostream>
    #include <fstream>
    using namespace std;

    int main(int argc, char **argv)
    {
        ofstream errlog;                                // 1
        streambuf *cerr_buffer = 0;                     // 2

        if (argc == 2)
        {
            errlog.open(argv[1]);                       // 3
            cerr_buffer = cerr.rdbuf(errlog.rdbuf());   // 4
        }
        else
        {
            cerr << "Missing log filename\n";
            return 1;
        }

        cerr << "Several messages to stderr, msg 1\n";
        cerr << "Several messages to stderr, msg 2\n";

        cout << "Now inspect the contents of " <<
                argv[1] << "... [Enter] ";
        cin.get();                                      // 5

        cerr << "Several messages to stderr, msg 3\n";

        cerr.rdbuf(cerr_buffer);                        // 6
        cerr << "Done\n";                               // 7
    }
    /*
        argv[1] 파일에 출력

        cin.get()에 출력:

    Several messages to stderr, msg 1
    Several messages to stderr, msg 2

        프로그램의 끝에 출력:

    Several messages to stderr, msg 1
    Several messages to stderr, msg 2
    Several messages to stderr, msg 3
    */

6.6.3: 스트림을 읽고 쓰기

스트림에 읽고 쓰려면 std::fstream 객체를 만들어야 한다. ifstream 객체와 ofstream 객체처럼 생성자는 열 파일의 이름을 받는다.
        fstream inout("iofile", ios::in | ios::out);
상수 ios::inios::out를 사용한 것을 눈여겨보라. 파일이 읽기와 쓰기로 열려야 함을 알리고 있다. bitor 연산자로 결합하여 다중 모드 표시자를 사용할 수도 있다. 다른 방법으로 ios::out 대신에 ios::app를 사용하고 단순히 (파일의 끝에) 쓰기만 할 수도 있다.

같은 파일에 읽고 쓰는 것은 언제나 약간 어색하다. 파일이 존재하지 않으면 어떻게 할까. 이미 존재하면 더 쓸 수는 없는 건가? 이런 문제를 가지고 좀 씨름을 한 후에 필자는 이제 다음과 같은 접근법을 사용한다.

    #include <fstream>
    #include <iostream>
    #include <string>

    using namespace std;

    int main()
    {
        fstream rw("fname", ios::out | ios::in);

        if (!rw)            // 파일이 아직 존재하지 않는다.
        {
            rw.clear();     // 다시 시도, ios::trunc를 사용하여 생성한다.
            rw.open("fname", ios::out | ios::trunc | ios::in);
        }

        if (!rw)            // 여전히 생성할 수 없으면 벗어난다.
        {
            cerr << "Opening `fname' failed miserably" << '\n';
            return 1;
        }

        cerr << "We're at: " << rw.tellp() << '\n';

                            // 무언가를 쓴다.
        rw << "Hello world" << '\n';

        rw.seekg(0);        // 다시 돌아가 무엇을 썼는지 읽는다.

        string s;
        getline(rw, s);

        cout << "Read: " << s << '\n';
    }

이 접근법에서 첫 번째 생성 시도가 실패하면 fname이 아직 존재하지 않는 것이다. 그러나 ios::trunc 깃발을 사용하여 open을 시도해 볼 수 있다. 이미 파일이 존재하면 생성에 성공할 것이다. rw를 정의할 때 ios::ate를 지정하면 최초의 읽기/쓰기 행위는 기본으로 EOF에서 일어난다.

DOS-류의 운영 체제는 \r\n 문자 연속열을 사용하여 텍스트 파일의 줄을 구분하므로 ios::binary 깃발이 있어야 이진 파일을 처리할 수 있다. 확실하게 \r\n 조합을 두 개의 문자로 처리한다. 일반적으로 (텍스트가 아닌) 이진 파일을 처리하려면 ios::binary를 지정해야 한다. 기본값으로 파일은 텍스트 파일로 열린다. 유닉스 운영 체제는 텍스트 파일을 이진 파일과 구분하지 않는다.

fstream 객체에서 파일 깃발을 조합하면 스트림이 열릴 때 빈 채로 생성될지 아니면 비지 않은 채로 (재)생성될지 확인할 수 있다. 자세한 것은 6.4.2.1목을 참고하라.

파일이 읽기와 쓰기 모드로 열리면 << 연산자로 정보를 파일에 삽입할 수 있다. 반면에 >> 연산자를 사용하면 파일로부터 정보를 추출할 수 있다. 이런 연산은 순서에 상관없이 수행할 수 있지만 삽입과 추출 사이를 전환할 때는 seekgseekp가 필요하다. 위치 이동 연산을 사용하면 읽기와 쓰기에 사용된 스트림의 데이터를 활성화할 수 있다 (그리고 그 반대로 비활성화할 수 있다). fstream 객체의 istreamostream은 스트림의 데이터를 공유하고 위치 이동 연산을 수행함으로써 스트림은 자신의 istream이나 ostream 부분을 활성화한다. 위치 이동 연산을 생략하면 읽은 후에 쓰기와 쓰기 후에 읽기는 그냥 실패한다. 다음 예제는 공백으로 분리된 단어를 파일로부터 읽어서 파일에서 방금 읽은 단어가 끝나는 곳을 넘어선 곳에 또다른 문자열을 쓰는 예를 보여준다. 마지막으로, 방금 씌여진 문자열이 끝난 곳을 넘어선 곳에 보이는 문자열을 더 읽는다.

    fstream f("filename", ios::in | ios::out);
    string  str;

    f >> str;       // 첫 번째 단어를 읽는다.

                    // 유명한 문구를 쓴다.
    f.seekg(0, ios::cur);
    f << "hello world";

    f.seekp(0, ios::cur);
    f >> str;       // 다시 읽는다.
같은 파일에 읽기와 쓰기 (추출과 삽입) 연산을 전환할 때 seek 또는 clear 연산을 요구하기 때문에 하나의 표현식 서술문에 일련의 << 그리고 >> 연산을 실행하는 것은 불가능하다.

물론 삽입과 추출은 거의 사용되지 않는다. 일반적으로 삽입과 추출은 파일에서 잘 알려진 위치에서 일어난다. 그런 경우에 삽입과 추출이 요구되는 곳은 seekgseekp 그리고 tellgtellp 멤버로 제어할 수 있다 (6.4.1.2목6.5.1.2목 참고).

예를 들어 파일 끝을 넘어서 읽거나 파일 끝에 도달하거나 아니면 파일 시작 앞에 위치하기 때문에 일어나는 에러 조건은 clear 멤버 함수로 정리할 수 있다 (6.3.1항). clear 다음에 처리를 계속한다. 예를 들어,

    fstream f("filename", ios::in | ios::out);
    string  str;

    f.seekg(-10);   // 이것은 실패한다. 그러나...
    f.clear();      // 계속해서 f를 처리한다.

    f >> str;       // 첫 번째 단어를 읽는다.
파일에 읽고 쓰는 상황은 데이터베이스 어플리케이션에서 볼 수 있다. 파일은 크기가 고정된 레코드로 구성되고 정보의 크기와 위치도 알려져 있다. 다음 프로그램은 텍스트 줄을 (존재하는) 파일에 추가한다. 파일에 줄 번호를 지정하면 특정 줄을 읽는 데에도 사용할 수 있다. index 이진 파일을 사용하면 줄의 위치를 신속하게 열람할 수 있다.
    #include <iostream>
    #include <fstream>
    #include <string>
    #include <climits>
    using namespace std;

    void err(char const *msg)
    {
        cout << msg << '\n';
    }

    void err(char const *msg, long value)
    {
        cout << msg << value << '\n';
    }

    void read(fstream &index, fstream &strings)
    {
        int idx;

        if (!(cin >> idx))                          // 읽기 인덱스
        {
            cin.clear();                            // 다시 읽기 허용
            cin.ignore(INT_MAX, '\n');              // 이 줄은 건너뛴다.
            return err("line number expected");
        }

        index.seekg(idx * sizeof(long));            // 인덱스-오프셋으로 간다.

        long offset;

        if
        (
            !index.read                             // 라인-오프셋을 읽는다.
            (
                reinterpret_cast<char *>(&offset),
                sizeof(long)
            )
        )
            return err("no offset for line", idx);

        if (!strings.seekg(offset))                 // 라인 오프셋으로 간다.
            return err("can't get string offset ", offset);

        string line;

        if (!getline(strings, line))                // 줄을 읽는다.
            return err("no line at ", offset);

        cout << "Got line: " << line << '\n';       // 줄을 보여준다.
    }

    void write(fstream &index, fstream &strings)
    {
        string line;

        if (!getline(cin, line))                  // 줄을 읽는다.
            return err("line missing");

        strings.seekp(0, ios::end);               // 문자열로 위치 찾기
        index.seekp(0, ios::end);                 // 인덱스로 위치 찾기

        long offset = strings.tellp();

        if
        (
            !index.write                          // 오프셋을 인덱스에 쓴다.
            (
                reinterpret_cast<char *>(&offset),
                sizeof(long)
            )
        )
            return err("Writing failed to index: ", offset);

        if (!(strings << line << '\n'))           // 줄을 쓴다.
            return err("Writing to `strings' failed");
                                                  // 줄쓰기 확인
        cout << "Write at offset " << offset << " line: " << line << '\n';
    }

    int main()
    {
        fstream index("index", ios::trunc | ios::in | ios::out);
        fstream strings("strings", ios::trunc | ios::in | ios::out);

        cout << "enter `r <number>' to read line <number> or "
                                    "w <line>' to write a line\n"
                "or enter `q' to quit.\n";

        while (true)
        {
            cout << "r <nr>, w <line>, q ? ";       // 프롬프트를 보여준다.

            index.clear();
            strings.clear();

            string cmd;
            cin >> cmd;                             // cmd를 읽는다.

            if (cmd == "q")                         // cmd를 처리한다.
                return 0;

            if (cmd == "r")
                read(index, strings);
            else if (cmd == "w")
                write(index, strings);
            else if (cin.eof())
            {
                cout << "\n"
                        "Unexpected end-of-file\n";
                return 1;
            }
            else
                cout << "Unknown command: " << cmd << '\n';
        }
    }

파일을 읽고 쓰는 또다른 예제를 다음 프로그램에 보여준다. NTB 문자열을 처리하는 방법도 보여준다.

    #include <iostream>
    #include <fstream>
    using namespace std;

    int main()
    {                                       // 파일 읽기/쓰기
        fstream f("hello", ios::in | ios::out | ios::trunc);

        f.write("hello", 6);                // NTB 문자열을 2개 쓴다.
        f.write("hello", 6);

        f.seekg(0, ios::beg);               // 파일의 처음으로 재설정한다.

        char buffer[100];                   // 아니면: char *buffer = new char[100]
        char c;
                                            // 앞의 `hello'를 읽는다.
        cout << f.get(buffer, sizeof(buffer), 0).tellg() << '\n';
        f >> c;                             // NTB 가름자를 읽는다.

                                            // 두 번째 `hello'를 읽는다.
        cout << f.get(buffer + 6, sizeof(buffer) - 6, 0).tellg() << '\n';

        buffer[5] = ' ';                    // asciiz를 ' '으로 변환한다.
        cout << buffer << '\n';             // `hello'를 두 번 보여준다.
    }
    /*
        생성된 출력:
    5
    11
    hello hello
    */

streambuf 멤버를 사용하면 완전히 다른 방식으로 스트림을 읽고 쓰는 방법을 구현하는 것도 가능하다. 지금까지 언급한 모든 연구는 여전히 유효하다 (예를 들어, 쓰기 연산 다음에 읽기 연산을 하려면 먼저 seekg를 사용해야 한다). streambuf 객체를 사용할 때 istream 객체가 또다른 ostream 객체의 streambuf 객체와 연관되거나 아니면 ostream 객체가 또다른 istream 객체의 streambuf 객체와 연관된다. 다음은 다시 이전의 프로그램이다. 이제는 연관 스트림을 사용한다.

    #include <iostream>
    #include <fstream>
    #include <string>
    using namespace std;

    void err(char const *msg);      // 이전 예제 참조
    void err(char const *msg, long value);

    void read(istream &index, istream &strings)
    {
        index.clear();
        strings.clear();

        // 이전 예제의 read() 함수를 삽입한다.
    }


    void write(ostream &index, ostream &strings)
    {
        index.clear();
        strings.clear();

        // 이전 예제의 write() 함수를 삽입한다.
    }

    int main()
    {
        ifstream index_in("index", ios::trunc | ios::in | ios::out);
        ifstream strings_in("strings", ios::trunc | ios::in | ios::out);
        ostream  index_out(index_in.rdbuf());
        ostream  strings_out(strings_in.rdbuf());

        cout << "enter `r <number>' to read line <number> or "
                                    "w <line>' to write a line\n"
                "or enter `q' to quit.\n";

        while (true)
        {
            cout << "r <nr>, w <line>, q ? ";       // 프롬프트를 보여준다.

            string cmd;

            cin >> cmd;                             // cmd를 읽는다.

            if (cmd == "q")                         // cmd를 처리한다.
                return 0;

            if (cmd == "r")
                read(index_in, strings_in);
            else if (cmd == "w")
                write(index_out, strings_out);
            else
                cout << "Unknown command: " << cmd << '\n';
        }
    }

이 예제에서 다음과 같은 사실을 알 수 있다.