본문 바로가기
프로그래밍/C & C++

[C++] 스트림(stream) 클래스 (1)

by 별준 2022. 12. 13.

References

  • C++ Standard Library 2nd

Contents

  • Background of I/O Streams
  • Fundamental Stream Classes and Objects
  • Standard Stream Operators << and >>
  • State of Streams

I/O 형식을 위한 클래스는 C++ 표준 라이브러리에서 가장 중요한 부분이며, I/O가 없는 프로그램은 쓸모가 거의 없다고 봐도 됩니다. C++ 표준 라이브러리에서 제공하는 I/O 클래스는 파일이나 스크린, 키보드에 제한되지 않으며 임의의 데이터를 받고 임의의 '외부 표현장치(external representations)'에 액세스할 수 있도록 해주는 확장 가능한 프레임워크입니다.

 

이번 포스팅에서는 IOStream 라이브러리에서 중요한 컴포넌트와 테크닉에 대해서 살펴보고 실제로 활용되는 방식에 대해서 자세히 살펴보도록 하겠습니다. 내용이 많아서 여러 포스팅으로 나누어서 진행될 것 같습니다.

 

참고로 C++11에서 추가된 중요한 기능들은 다음과 같습니다.

  • 예외에 대한 더 자세한 정보를 제공하기 위해서 예외를 위한 클래스가 std::exception에서 직접 파생되지 않고, std::system_error에서 파생됨
  • 문자열 스트림과 파일 스트림 클래스에서 rvalue와 move semantics를 지원. 따라서, 이동 생성, 이동 할당과 문자열 스트림 또는 파일 스트림을 swap할 수 있으며, 이 덕분에 임시 문자열이나 임시 파일 스트림도 I/O에 사용할 수 있음
  • 파일 스트림에서 const char* 뿐만 아니라 std::string으로도 파일 이름을 전달할 수 있음
  • 출력과 입력 연산자인 '<<'과 '>>'은 이제 long long과 unsigned long long에 대해서도 오버로딩됨
  • I/O 스트림은 이제 부분적으로 동시성을 지원
  • char16_t와 char32_t 데이터 타입에 대한 character traits가 제공됨
  • 새로운 클래스 wbuffer_convert가 도입되어 UTF-8과 같은 다른 문자 집합을 읽거나 쓸 수 있음

 

Common Background of I/O Streams

스트림 클래스에 대해 자세히 살펴보기 전에 스트림에 대한 기본적인 내용을 살펴보도록 하겠습니다. iostream에 대한 기본적인 지식이 있다면 생략해도 무방합니다.

 

Stream Objects

C++에서는 스트림을 사용하여 I/O가 수행됩니다. 스트림(stream)은 데이터의 흐름(stream of data')를 의미하며 일련의 문자가 '흐른다(flow)'라는 걸 말합니다. 객체지향의 원칙에 따라 스트림은 클래스로 정의된 특성을 갖는 객체입니다. 출력은 스트림으로 흘러 들어오는 데이터이며, 입력은 스트림에서 나가는 데이터의 흐름으로 해석됩니다. 표준 I/O 채널을 위한 전역 객체는 predefined 되어 있습니다.

 

Stream Classes

다양한 종류의 I/O (ex, input, output, file access)가 있듯이, I/O의 종류에 따라 다양한 클래스가 있습니다. 아래 두 개의 클래스는 가장 중요한 스트림 클래스 입니다.

  • istream : 데이터를 읽는데 사용되는 입력 스트림을 정의
  • ostream : 데이터를 쓰는데 사용되는 출력 스트림을 정의

두 클래스는 각각 클래스 템플릿인 basic_istream<>과 basic_ostream<>을 char로 인스턴스화한 것 입니다. 사실 전체 IOStream 라이브러리는 데이터 타입에 종속되지 않습니다. 다만, IOStream 라이브러리 내의 대부분 클래스에서 character 타입이 클래스의 템플릿 인자로 사용됩니다. 이러한 파라미터화는 각 문자열 클래스와 대응되며, internationalization을 위해 사용됩니다.

 

포스팅 초반부에서는 좁은 의미의 스트림, 즉, char를 character 타입으로 다루는 스트림에서의 출력과 입력에 집중하여 다루고, 이후 포스팅에서 다른 타입에 대한 스트림에 대해 알아보도록 하겠습니다.

 

Global Stream Objects

IOStream 라이브러리에는 istream과 ostream 타입의 전역 객체를 여러 개 정의하고 있습니다. 이들 객체는 표준 I/O 채널에 대응됩니다.

  • cin : 클래스 istream으로 사용자 입력을 위해 사용되는 표준 입력 채널입니다. 이 스트림은 C의 stdin에 대응됩니다. 일반적으로 이 스트림은 OS를 통해 키보드와 연결됩니다.
  • cout : 클래스 ostream으로 프로그램 출력을 위해 사용되는 표준 출력 채널입니다. 이 스트림은 C의 stdout에 대응됩니다. 일반적으로 이 스트림은 OS를 통해 모니터와 연결됩니다.
  • cerr : 클래스 ostream으로 모든 종류의 에러 메시지를 위해 사용되는 표준 에러 채널입니다. 이 스트림은 C의 stderr에 대응됩니다. 이 스트림 역시 일반적으로 OS를 통해 모니터와 연결되며, 기본적으로 cerr는 버퍼링되지 않습니다.
  • clog : 클래스 ostream으로 모든 종류의 로그 메세지를 위해 사용되는 표준 로그 채널입니다. C에서는 이 스트림에 대응되는 것이 없습니다. 기본적으로 cerr와 같은 destination에 연결되지만 clog의 출력은 버퍼링됩니다.

'normal' output과 에러 메시지 output을 분리하면 프로그램을 실행할 때, 두 종류의 출력을 따로 취급할 수 있습니다. 예를 들어, 프로그램의 일반 출력은 파일로 전송하지만 에러 메시지는 콘솔로 계속 내보낼 수 있습니다. 물론 OS가 표준 I/O 채널의 redirection을 지원해야 합니다(대부분의 OS에서 지원함).

 

Stream Operators

이동 연산자(shift operator)인 입력을 위한 >> 연산자와 출력을 위한 << 연산자는 해당 스트림 클래스에 대해 오버로딩되어 있습니다. 따라서, C++에서 이동 연산자는 'I/O operator'가 되었습니다. 이 연산자를 사용하면 mutiple I/O operations을 엮을 수 있습니다.

 

예를 들어, 아래의 루프는 정수가 입력되는 동안 계속해서 표준 입력에서 정수값 두 개를 읽어 표준 출력으로 씁니다.

int a, b ;

// as long as input of a and b is successful
while (std::cin >> a >> b) {
  // output a and b
  std::cout << "a: " << a << " b: " << b << std::endl;
}

 

Manipulators

조작자(manipulators)는 스트림을 조작하는 데 쓰이는 특별한 객체입니다. 대체로 숫자의 기수인 dec, hex, oct를 위한 조작자처럼 입력을 해석하거나 출력 방식을 바꾸는데 사용됩니다. 따라서, ostream을 위한 조작자라고 하더라도 따로 output을 만들 필요는 없고, istream을 위한 조작자더라도 input을 소모해야만 하는 것은 아닙니다. 하지만 일부 조작자의 경우 중간에 새로운 동작이 필요할 수도 있습니다. 예를 들어, 조작자는 출력 버퍼를 내보내거나(flush) 입력 버퍼의 공백을 건너뛰도록 하기도 합니다.

 

조작자 중 하나인 'endl'은 "end line"을 의미하며, 다음의 두 가지 일을 수행합니다.

  • 개행 문자를 출력 ('\n')
  • 출력 버퍼를 내보낸다(flush). 즉, 스트림 메소드인 flush()를 사용하여 버퍼에 쌓인 모든 데이터를 해당 스트림에 write하도록 한다.

IOStream 라이브러리에 정의된 가장 중요한 조작자들은 아래와 같습니다.

 

Simple Example

스트림 클래스의 사용법을 아래의 예제 코드에서 보여주고 있습니다. 이 코드는 두 개의 부동소수점 값을 읽어서 이들의 곱을 출력합니다.

#include <iostream>
#include <cstdlib>

using namespace std;

int main()
{
  double x, y;

  cout << "Multiplication of two floating point values" << endl;
  // read first operand
  cout << "first operand:    ";
  if (!(cin >> x)) {
    // input error
    // -> error message and exit program with error status
    cerr << "error while reading the first floating value" << endl;
    return EXIT_FAILURE;
  }

  // read second operand
  cout << "second operand:   ";
  if (!(cin >> y)) {
    // input error
    // -> error message and exit program with error status
    cerr << "error while reading the second floating value" << endl;
    return EXIT_FAILURE;
  }

  // print operands and result
  cout << x << " times " << y << " equals " << x * y << endl;
}

 

Fundamental Stream Classes and Objects

Classes and Class Hierarchy

Class Hierarchy of the Fundamental Stream Classes

IOStream 라이브러리의 스트림 클래스는 위와 같은 계층 구조를 이룹니다. 클래스 템플릿인 경우, 위에는 클래스 템플릿의 이름을, 아래쪽에는 character 타입 char와 wchar_t에 대한 인스턴스화의 이름을 나타냅니다.

 

위 계층 구조에 있는 클래스들의 역할은 다음과 같습니다.

  • 베이스 클래스인 ios_base는 character 타입과 대응되는 character traits와 관계없는 모든 스트림 클래스의 속성을 정의합니다. 이 클래스의 대부분은 상태와 format flags에 대한 컴포넌트와 함수들로 구성되어 있습니다.
  • ios_base에서 파생되는 클래스 템플릿 basic_ios<>는 character 타입과 그에 맞는 character tratis에 종속되는 모든 스트림 클래스의 공통 속성을 정의합니다. 이 속성에는 사용되는 버퍼의 정의가 포함됩니다. 버퍼는 적절한 템플릿 인스턴스화를 거친 템플릿 클래스 basic_streambuf<>에서 파생된 클래스 객체입니다. 이 클래스는 실제 reading/writing을 수행합니다.
  • basic_ios<>를 가상 상속하는 클래스 템플릿 basic_istream<>과 basic_ostream<>은 각각 읽고 쓰는데 사용되는 객체를 정의합니다. basic_ios<>처럼 이들은 character 타입과 character tratis에 따라 파라미터화되는 템플릿입니다.
  • 클래스 템플릿 basic_iostream<>은 basic_istream<>과 basic_ostream<> 모두에서 파생되었습니다. 이 클래스 템플릿은 읽기와 쓰기 모두에 사용되는 객체를 정의합니다.
  • 클래스 템플릿 basic_streambuf<>는 IOStream 라이브러리의 핵심입니다. 이 클래스는 스트림에 쓰고 읽을 수 있는 모든 표현방식에 대한 인터페이스를 정의하며, 문자를 읽거나 다른 스트림 클래스에서 사용합니다. 일부 외부 표현장치를 사용해야 하면, basic_streambuf<>에서 파생된 클래스를 사용합니다. 스트림 버퍼 클래스에 대한 자세한 내용은 아래에서 조금 더 언급하도록 하겠습니다.

 

Purpose of the Stream Buffer Classes

IOStream 라이브러리는 책임 소재를 엄격하게 분리하도록 설계되었습니다. basic_ios로부터 파생된 클래스는 오직 데이터의 formatting 만을 다룹니다. 문자를 읽고 쓰는 것은 basic_ios 하위 객체가 관리하는 스트림 버퍼에 의해 수행됩니다. 스트림 버퍼는 읽고 쓰기 위한 문자 버퍼를 제공합니다. 또한, 파일이나 문자열과 같은 외부 표현 장치에 대한 추상화는 스트림 버퍼가 담당합니다.

 

따라서, 새로운 외부 표현 장치(소켓 또는 그래픽 사용자 인터페이스 컴포넌트)에서 I/O를 수행할 때나 스트림을 리다이렉트할 때, 또는 파이프라인을 만들기 위해 스트림을 결합시킬 때 스트림 버퍼가 중요합니다. 이에 대한 기법은 다음 포스팅에서 자세하게 살펴볼 예정입니다.

 

스트림 버퍼를 사용함으로써, 새로운 저장 장치와 같은 새로운 외부 표현장치에 대한 액세스를 정의하는 것은 꽤 쉽습니다. 해야할 것은 오직 basic_streambuf<> 또는 적절한 특수화로부터 새로운 스트림 버퍼 클래스를 파생시키고, 새로운 외부 장치로부터 문자를 읽고 쓰는 함수를 정의하는 것 입니다. formatted I/O에 대한 모든 옵션은 스트림 객체가 새로운 스트림 버퍼 클래스 객체를 사용하도록 초기화되면 자동으로 제공됩니다.

 

Detailed Class Definitions

IOStream 라이브러리의 다른 모든 클래스 템플릿들과 마찬가지로 클래스 템플릿 basic_ios<>는 아래와 같이 두 개의 인자로 파라미터화됩니다.

namespace std {
  template<typename _CharT, typename _Traits>
    class basic_ios;
}

템플릿 인자로 스트림 클래스에서 사용할 문자 타입과 그 문자 타입의 traits를 설명하는 클래스를 받습니다.

Traits 클래스의 예로 파일의 끝(EOF)를 나타내는 값과 일련의 문자를 복사하거나 이동시키는 방법에 대한 명령이 있습니다. 일반적으로 문자 타입에 대한 traits는 문자 타입과 쌍을 이루므로 클래스 템플릿의 특정 문자 타입에 대해 특수화된 클래스 템플릿을 정의하는 것의 합리적입니다. 따라서, 만약 _CharT가 문자 타입 인자라면 traits 클래스는 기본적으로 char_traits<_CharT> 입니다. C++ 표준 라이브러리에서는 char, char8_t, char16_t, char_32_t, wchar_t에 대한 char_traits 특수화를 제공합니다.

char_traits<char> specialization

 

가장 많이 사용되는 두 가지 문자 타입을 위한 클래스 basic_ios<>의 두 가지 인스턴스화는 다음과 같습니다.

namespace std {
  typedef basic_ios<char>    ios;
  typedef basic_ios<wchar_t> wios;
}

ios 타입은 AT&T에서 만든 'old-fashined' IOStream의 베이스 클래스에 대응되며 오래된 C++ 프로그램에 대한 호환성을 제공하기 위해 사용됩니다.

 

basic_ios에서 사용되는 스트림 버퍼 클래스도 비슷하게 정의됩니다.

typedef basic_streambuf<char> 	streambuf;

 

클래스 템플릿 basic_istream<>, basic_ostream<>, basic_iostream<> 또한 위와 같이 문자 타입과 traits 클래스로 파라미터화되며, 자주 사용되는 객체에 대한 타입 정의도 제공됩니다.

참고로 istream과 ostream은 8비트 문자 집합으로도 충분한 지역에서 자주 사용되는 데이터 타입이며, wchar_t를 사용하면 8비트보다 더 많은 문자 집합을 사용할 수 있습니다. char16_t와 char32_t에 대응되는 인스턴스화는 제공하지 않습니다.

 

C++ 표준 라이브러리에서는 파일과 문자열 포맷을 위한 I/O 스트림 클래스를 추가로 제공합니다. 이는 다음 포스팅에서 언급하도록 하겠습니다.

 

Global Stream Objects

스트림 클래스에 대한 여러 전역 스트림 객체가 정의되어 있습니다. 이 객체들은 위에서 언급했던 표준 I/O 채널에 액세스하는 용도로 char을 문자 타입으로 사용합니다. wchar_t를 사용하는 스트림 객체도 아래에 나열되어 있습니다.

기본적으로 이 표준 스트림들은 C의 표준 스트림과 동기화됩니다. 즉, C++ 표준 라이브러리는 C++ 스트림과 C 스트림으로 출력이 섞여 들어가도 그 순서를 지켜준다는 것을 의미합니다. 데이터를 쓰기 전에 표준 C++ 스트림의 버퍼는 대응되는 C 스트림의 버퍼를 내보내고 반대로도 동일합니다. 물론 이러한 동기화에는 시간이 소요되며, 만약 이 기능이 필요하지 않다면 입력이나 출력을 하기 전에 sync_with_stdio(false)를 호출하여 끌 수 있습니다.

 

C++11에서부터, 스트림 객체에 대해서도 몇 가지 동시성이 보장됩니다. C의 표준 스트림과 동기화될 때에는 다중 병렬 스레드에서 이들을 사용하더라도 정의되지 않은 동작을 하지 않습니다. 따라서, 멀티 스레드에서 읽고 쓸 수 있습니다. 하지만 글자가 서로 섞이게 되어 어떤 스레드가 어떤 동작을 하는지 정해지지 않는다는 점을 염두해야 합니다. 다른 스트림 객체나 이들 객체가 C 스트림과 동기화되지 않았다면 동시 읽기/쓰기는 정의되지 않은 동작을 수행합니다.

 

Header Files

스트림 클래스의 정의는 여러 헤더 파일로 흩어져 있습니다.

  • <iosfwd> - 스트림 클래스에 대한 전방 선언을 포함합니다. class ostream과 같은 간단한 전방 선언이 허용되지 않기 때문에 이 헤더 파일이 필요합니다.
  • <streambuf> - 스트림 버퍼의 베이스 클래스(basic_streambuf<>)가 정의되어 있습니다.
  • <istream> - 입력(basic_istream<>)만을 지원하는 클래스와 입출력(basic_iostream<>)을 지원하는 클래스가 정의되어 있습니다.
  • <ostream> - 출력 스트림 클래스(basic_ostream<>)가 정의되어 있습니다.
  • <iostream> - cin이나 cout과 같은 전역 스트림 객체에 대한 선언을 포함합니다.

대부분의 헤더는 C++ 표준 라이브러리의 내부 구조에 사용됩니다. 어플리케이션을 만들 때는 스트림 클래스 선언을 위해 <iosfwd>를, 입력과 출력 기능을 사용하려면 각각 <istream>이나 <ostream>을 include하는 것만으로 충분합니다. <iostream>은 표준 스트림 객체를 사용할 때만 include 되어야 합니다. 일부 구현에서는 번역 단위에 이 헤더가 포함되어 있으면 시작할 때 일부 코드를 실행합니다. 실행되는 코드 자체의 코스트가 높지는 않지만 대응되는 실행 파일의 페이지를 로드해야 하므로 시간이 좀 걸릴 수 있습니다. 일반적으로 필요한 것들만 정의한 헤더들만 include 하는 것이 좋습니다.

 

파라미터화된 조작자(manipulators), 파일 스트림, 또는 문자열 스트림과 같은 특별한 스트림을 위해 <iomanip>, <fstream>, <sstream>, <strstream>과 같은 몇 가지 헤더가 더 제공됩니다.

 

Standard Stream Operators << and >>

C와 C++에서 << 연산자와 >> 연산자는 각각 정수의 비트를 오른쪽, 왼쪽으로 쉬프트하는 데 사용됩니다. 클래스 basic_istream<>과 basic_ostream<>은 <<, >> 연산자를 오버로딩하여 표준 I/O 연산자로 사용합니다.

 

Output Operator <<

basic_ostream 클래스(+ ostream)는 <<을 출력 연산자로 정의하고 void와 nullptr_t를 제외한 거의 모든 기본 데이터 타입과 char*, void*에 대해 오버로딩합니다.

 

스트림에 대한 출력 연산자는 두 번째 인자를 해당 스트림으로 전달합니다. 따라서 데이터는 화살표의 방향으로 전달됩니다.

int i = 7;
std::cout << i; // output: 7

float f = 4.5;
std::cout << f; // output: 4.5

<< 연산자는 두 번째 인자가 임의의 데이터 타입을 가질 수 있도록 오버로딩할 수 있습니다. 따라서 자신만의 데이터 타입을 I/O 스트림에 통합할 수 있습니다. 컴파일러는 두 번째 인자를 출력하기 위한 올바른 함수를 부른다는 것을 보장하며, 이 함수는 두 번째 인자를 스트림으로 보낼 일련의 문자로 바꿉니다.

 

C++ 표준 라이브러리도 string, bitsets, complex number 등과 같은 특정 데이터 타입을 위한 출력 연산자를 제공하기 위해서 이러한 메커니즘을 사용하고 있습니다.

std::string s("hello");
s += ", world";
std::cout << s; // output: hello, world

std::bitset<10> flags(7);
std::cout << flags; // output: 0000000111

std::complex<float> c(3.1,7.4);
std::cout << c; // output: (3.1,7.4)

사용자 정의 데이터에 대한 출력 연산자를 작성하는 방법은 다음 포스팅에서 살펴보겠습니다.

 

위와 같은 방법을 통해 출력 메커니즘을 다른 데이터 타입에 확장할 수 있어서 좋습니다. printf()를 사용하는 C의 I/O 메커니즘은 출력할 객체의 데이터 타입을 명시해주어야 했기 때문에 훨씬 더 좋아졌습니다.

 

<< 연산자는 한 명령문 내에서 여러 객체를 출력하는데 사용될 수 있는데, 일반적으로 출력 연산자는 자신의 첫 번째 인자를 반환합니다. 따라서 출력 연산자의 결과는 출력 스트림입니다. 그렇기 때문에 다음과 같이 연결하여 호출할 수 있습니다.

std::cout << x << " times " << y << " is " << x * y << std::endl;

<< 연산자는 왼쪽에서부터 오른쪽으로 실행됩니다. x * y의 경우 * 연산자가 << 연산자보다 우선순위가 높기 때문에 따로 괄호가 필요없습니다만, 논지 연산자와 같은 연산자들은 더 낮은 우선순위를 가질 수 있으므로 유의해야 합니다.

C++11에서부터는 같은 스트림 객체에 대한 동시 출력이 가능하지만 문자가 서로 중간에 끼어들 수 있습니다.

 

Input Operator >>

basic_istream (+ istream) 클래스는 >>을 입력 연산자로 정의합니다. basic_ostream과 유사하게 이 연산자는 대부분의 기본 데이터 타입과 char*, void*에 대해 오버로딩되어 있습니다만, void와 nullptr_t에 대해서는 오버로딩되어 있지 않습니다. 스트림의 입력 연산자는 두 번째 인자에 읽은 값을 저장하도록 정의되어 있습니다. << 연산자처럼 화살표 방향으로 데이터를 전달합니다.

int i;
std::cin >> i;

float f;
std::cin >> f;

두 번째 인자가 수정되므로 상수가 아닌 비상수 레퍼런스로 전달되어야 합니다.

 

출력 연산자 << 처럼 입력 연산자도 임의의 데이터 타입에 대해 오버로딩할 수 있으며 연속 호출도 가능합니다.

float f;
std::complex<double> c;

std::cin >> f >> c;

이를 위해서는 기본적으로 앞서 나오는 공백(whitespace)를 스킵해야 합니다. 하지만, 이 공백에 대한 자동 스킵은 off될 수도 있습니다.

출력 연산자와 마찬가지로 같은 스트림 객체에 대해 동시 입력이 가능하지만, 어느 스레드에서 입력되는지는 보장되지 않습니다.

 

Input/Output of Special Type

대부분의 기본 데이터 타입(void와 nullptr_t 제외)과 char*, void*에 대해 표준 I/O 연산자가 제공됩니다. 그러나 일부 데이터 타입과 사용자 정의 타입에는 특별한 규칙이 적용됩니다.

 

Numeric Type

숫자 값을 읽을 때, 입력은 적어도 1 digit은 되어야 합니다. 그렇지 않으면 숫자 값은 0으로 설정되고, failbit가 set 됩니다.

int x;
std::cin >> x; // assigns 0 to x, if the next character does not fit

그러나 입력이 없거나 faitbit가 이미 set 되어 있다면, 입력 연산자를 호출해도 x를 수정하지 않습니다. 이는 bool도 마찬가지 입니다.

 

Type bool

기본적으로 불리언 값은 숫자로 출력되고 숫자로 읽습니다. false는 0으로 변환되고 true는 1로 변환됩니다. 읽을 때, 0이나 1이 아닌 값은 error로 간주됩니다. 이 경우, ios::faitbit가 set 되며, 그에 맞는 예외가 발생할 수 있습니다.

참고로, 스트림의 formatting option을 사용하면 특정 문자(ex, "true", "false")를 불리언 값으로 사용할 수도 있습니다.

 

Type char and wchar_t

>> 연산자로 char과 wchar_t를 읽을 때, 앞서 나오는 whitespace는 기본적으로 스킵됩니다. 이를 포함한 모든 문자를 읽고 싶다면 skipws 플래그를 clear 하거나 멤버 함수인 get()을 사용하면 됩니다.

 

Type char*

C-string(char*)은 단어 단위로 읽습니다. 즉, C-string을 읽을 때, 앞서 나오는 공백은 기본적으로 스킵되고 다른 공백 문자나 EOF가 나오기 전까지의 모든 단어를 읽습니다. 공백 스킵 여부는 skipws 플래그로 제어합니다.

이런 조건 때문에 읽는 문자열의 길이가 매우 길어질 수 있습니다. 따라서 문자열이 너무 길다면, 입력을 미리 잘라낼 필요가 있습니다. 이를 위해서는 항상 읽을 문자열의 최대 길이를 설정해야 하는데, 대체로 다음과 같은 방식으로 문자열 길이를 제한하여 읽을 수 있습니다.

char buffer[81]; // 80 characters and '\0'
std::cin >> std::setw(81) >> buffer;

여기서 사용된 조작자인 setw는 뒤에서 살펴보겠습니다.

 

C++ 표준 라이브러리에서 string 타입은 문자열의 길이에 맞춰 크기가 늘어나므로, char* 보다 string을 사용하는 것이 더 쉽고 에러도 적습니다. 또한, getline()을 사용하여 행 단위로 읽을 수 있으므로 가능하다면 C-string보다 string을 사용하는 편이 좋습니다.

 

Type void*

<< 연산자와 >> 연산자는 포인터를 출력하고 읽는 기능을 제공합니다. 만약 void* 타입의 파라미터가 출력 연산자에 전달되면 주소가 구현에 따라 포맷에 맞게 출력됩니다. 예를 들어, 아래의 구문은 C-string의 내용과 주소를 출력합니다.

char* cstring = "hello";
std::cout << "string \"" << cstring << "\" is located at address: "
          << static_cast<void*>(cstring) << std::endl;

입력 연산자에서 주소를 다시 읽을 수도 있습니다만, 주소는 일반적으로 일시적인 값에 불과합니다 (같은 객체라도 프로그램이 새로 시작하면 다른 주소값을 가질 수 있음). 주소를 읽거나 출력하는 것은 객체 식별을 위해 주소를 주고 받거나 공유 메모리에 있는 프로그램 등에서 유용합니다.

 

Stream Buffers

<< 연산자와 << 연산자를 사용하여 스트림 버퍼에서 직접 읽거나 스트림 버퍼에 직접 쓸 수 있습니다. C++ I/O 스트림을 사용해 파일을 복사하는 방법 중 이 방법이 가장 빠른 방법 중 하나인데, 이는 아래의 State of Stream에서 자세히 살펴보도록 하겠습니다.

 

User-Defined Types

원칙적으로 연산자를 오버로딩하여 사용자 정의 타입에 확장하는 것은 매우 쉽습니다. 하지만 가능한 모든 형식 데이터와 에러 조건을 고려하는 것은 생각보다 많은 노력이 필요합니다. 사용자 정의 타입에 대해 I/O 메커니즘은 다음 포스팅에서 자세히 살펴보도록 하겠습니다.

 

Monetary and Time Values

C++11에서부터는 조작자를 사용하여 화폐나 시간 값을 직접 읽고 쓸 수 있습니다. 예를 들어, 아래 코드는 현재 날짜와 시간을 쓰고 새로운 날짜를 읽습니다.

#include <iostream>
#include <iomanip>
#include <chrono>
#include <cstdlib>
using namespace std;

int main()
{
  // process and print current data and time
  auto now = chrono::system_clock::now();
  time_t t = chrono::system_clock::to_time_t(now);
  tm* nowTM = localtime(&t);
  cout << put_time(nowTM, "data: %x\ntime: %X\n") << endl;

  // read new time (same data)
  tm time(*nowTM); // copy data for new time
  cout << "new time [HH:MM]: ";
  cin >> get_time(&time, "%H:%M"); // read new time
  if (!cin) {
    cerr << "invalid format read" << endl;
    exit(EXIT_FAILURE);
  }
  // process difference in minutes
  auto tp = chrono::system_clock::from_time_t(mktime(&time));
  auto diff = chrono::duration_cast<chrono::minutes>(tp - now);
  cout << "difference: " << diff.count() << " minutes" << endl;
}

 

State of Streams

스트림은 상태를 갖습니다. 이를 통해 I/O가 성공했는지, 실패했다면 이유는 무엇인지를 알아낼 수 있습니다.

 

Contants for the State of Streams

스트림의 일반 상태를 나타내기 위해서 iostate 타입의 상수를 정의하고 플래그로 사용합니다.

iostate 타입은 ios_base 클래스의 멤버입니다. 상수의 정확한 타입은 구현 세부사항으로 구현에 따라 다릅니다. 즉, iostate가 열거형인지, 정수 타입의 정의인지 bitset 클래스의 인스턴스인지 정해지지 않습니다.

 

상수 goobit은 0으로 정의됩니다. 따라서, goodbit이라는 것은 모든 비트가 clear된다는 것을 의미합니다 (goodbit이라는 이름 때문에 헷갈릴 수 있는데, 어떤 비트가 set 되었다는 것을 의미하진 않습니다). faitbit와 badbit 간의 차이점은 기본적으로 badbit가 조금 더 치명적인 에러를 나타낸다는 것 입니다.

  • failbit는 만약 어떤 연산이 올바르게 처리되지는 않았지만 스트림은 일반적으로 괜찮다면 설정되는 값 입니다. 일반적으로 이 플래그는 읽는 동안 포맷 에러가 발생하면 set 됩니다. 예를 들어, 정수를 읽도록 했는데 문자가 들어오면 이 플래그가 설정됩니다.
  • badbit는 스트림이 어떤 방식으로든 손상되었거나 데이터가 소실된 경우 설정됩니다. 예를 들어, 이 플래그는 파일을 참조하는 스트림이 파일의 시작 이전 지점을 가리키는 경우 set 됩니다.

eofbit는 일반적으로 failbit와 함께 발생하는데, 파일의 끝 너머를 읽으면 EOF 조건이 검출되기 때문입니다. 마지막 문자를 읽고 난 후에는 아직 eofbit 플래그가 set 되지 않습니다. 그 다음에 문자를 읽으려고 시도하면 읽기에 실패하므로 eofbit와 failbit가 같이 set 됩니다. 참고로 예전 구현에서는 hardfail이라는 플래그도 지원하는데, 표준에서는 지원하지 않습니다.

 

이와 같은 상수들은 전역으로 정의되지 않고, 클래스 ios_base 내에서 정의되어 있습니다. 따라서 다음과 같이 사용해야 합니다.

std::ios_base::eofbit

또는 ios_base를 상속받은 클래스를 사용해도 됩니다. 예전 구현에서는 ios 클래스에 정의되었습니다. ios는 ios_base에서 파생된 타입이므로 이를 사용하면 아래와 같이 사용할 수 있습니다.

std::ios::eofbit

상수 플래그들은 basic_ios 클래스에서 관리하며 basic_istream이나 basic_ostream의 모든 객체에 존재합니다.

 

하지만 스트림 버퍼는 상태 플래그를 갖지 않으며, 한 스트림 버퍼는 여러 스트림 객체가 공유할 수 있기 때문에 eof 플래그는 마지막 연산에서 발생한 스트림의 상태만 나타냅니다.

 

Member Functions Accessing the State of Streams

플래그의 현재 상태는 아래의 표에 있는 멤버 함수들에 의해 정해집니다.

위의 표에서 위의 4개의 멤버 함수는 현재 상태를 결정하고 불리언 값을 반환합니다. fail()은 failbit나 badbit가 set 되었는지 여부를 반환한다는 것에 유의합니다. 이를 사용하면 한 번만 검사해도 에러가 발생했는지를 알 수 있습니다.

 

또한 플래그의 상태는 더 일반적인 멤버 함수를 사용하여 결정하거나 수정할 수 있습니다. 파라미터가 없는 clear()를 호출하면 eofbit를 포함한 모든 에러 플래그가 제거됩니다. 만약 clear()에 파라미터를 전달하면, 스트림의 상태는 주어진 파라미터에 맞는 상태로 수정됩니다. 이때, rebuf() == 0인 경우와 같이, 스트림 버퍼가 없는 경우에는 clear를 하더라도 항상 badbit가 설정되며 clear의 유일한 예외입니다.

아래 코드는 failbit가 설정되어 있는지 확인한 후, 필요하다면 플래그를 제거하는 것을 보여줍니다.

// check whether failbit is set
if (strm.rdstate() & std::ios::failbit) {
  std::cout << "failbit was set" << std::endl;
  
  // clear only failbit
  strm.clear(strm.rdstate() & ~std::ios::failbit);

 

스트림은 특정 플래그가 clear()나 setstate()로 설정되면 예외를 던지도록 되어 있습니다. 이런 스트림은 플래그를 수정하는 메소드의 끝에 해당 플래그가 설정되면 항상 예외를 던집니다.

 

에러 비트는 항상 명시적으로 삭제해야 하는데, C에서는 포맷 에러가 발생한 후에도 문자를 계속 읽을 수 있습니다. 따라서 읽기 연산은 실패하지만 입력 스트림은 여전히 good 인 상태로 있는 것 입니다. 이 점이 C++과는 다른데, C++에서는 failbit가 set 된다면 failbit가 명시적으로 제거되기 전까지는 이후의 모든 스트림 연산은 아무 동작도 하지 않습니다.

 

일반적으로 set된 비트는 이전에 어떤 일이 발생했다는 사실만을 반영합니다. 어떤 연산 이후에 비트가 set 되더라도, 이 연산 때문에 플래그가 설정된 것이 아니라 연산 이전에 이미 플래그가 set 되어 있었을 수도 있습니다.

 

Stream State and Boolean Conditions

불리언 표현식으로 스트림을 사용하기 위해 아래의 두 가지 함수가 정의되어 있습니다.

operator bool()을 사용하면 제어 구조에서 짧고 일반적인 방법으로 현재 스트림의 상태를 검사할 수 있습니다.

// while the standard input stream is OK
while (std::cin) {
  ...
}

제어 구조에서 불리언 조건으로 사용될 때, 데이터 타입을 직접 bool 타입으로 변환할 필요가 없습니다. 아래의 코드도 가능합니다.

if (std::cin >> x) {
  // reading x was successful
  ...
}

위 코드에서 std::cin >> x는 결과적으로 std::cin을 반환하며, 제어 구문은 std::cin을 검사합니다. 이 기법을 사용하는 전형적인 예시로는 객체를 읽고 처리하는 루프가 있습니다.

// as long as obj can be read
while (std::cin >> obj) {
  // process obj (in this case, simply output it
  std::cout << obj << std::endl;
}

위의 루프는 failbit나 badbit가 설정되면 중단합니다. 즉, 에러가 발생하거나 파일의 끝에 도달하면 끝난다는 의미입니다.

 

기본적으로, >> 연산자는 앞서 나오는 공백을 스킵합니다. 일반적으로 그 공백은 스킵해야 하기 때문인데, 만약 obj가 char 타입이라면 공백도 중요하게 처리해야할 수도 있습니다. 이 경우, 스트림의 멤버 함수인 put()과 get()을 사용할 수도 있고, 혹은 I/O filter를 구현하기 위해 istreambuf_iterator를 사용할 수도 있습니다.

 

연산자 !를 사용하면 반대로 검사할 수 있는데, 다음과 같이 사용할 수 있습니다.

if (!std::cin) {
  // the stream cin is not OK
  ...
}

참고로 ! 연산자는 우선 순위가 낮기 때문에 아래와 같이 괄호와 함께 사용해야 일반적으로 원하는 동작을 수행할 수 있습니다.

if (!(std::cin >> x)) {
  // the read failed
  ...
}

다만, fail()과 같은 멤버 함수를 사용하는 편이 가독성은 더 높습니다.

 

Stream State and Exceptions

C++에서 예외 처리가 도입되었지만, 스트림은 이전부터 사용되고 있었습니다. 따라서, 하위 호환성을 유지하기 위해 기본적으로 스트림은 예외를 던지지 않습니다. 하지만 표준화된 스트림에서 각 상태 플래그에 대해 해당 플래그가 예외를 발생시킬지 여부를 정할 수 있습니다. 이 값은 exception() 이라는 멤버 함수를 통해 정할 수 있습니다.

인자 없이 exceptions()를 호출하면 예외를 발생시킬 수 있도록 설정된 플래그들을 반환합니다. 만약 이 함수가 goodbit를 반환한다면, 예외를 던지지 않는다는 것을 의미합니다. 만약 exceptions()에 인자를 전달하여 호출하면, 해당 상태 플래그가 set되면 예외를 발생시키게 됩니다. 아래 예제 코드는 모든 플래그에 대해 예외가 발생하도록 스트림의 구성을 변경하는 방법을 보여줍니다.

strm.exceptions(std::ios::eofbit | std::ios::failbit | std::ios::badbit);

만약 0이나 goodbit가 인자로 전달되면, 예외는 발생되지 않습니다.

// do not generate exceptions
strm.exceptions(std::ios::goodbit);

 

스트림 예외가 유용하게 사용되는 곳은 이미 preformatted data를 읽을 때 입니다. 하지만 이런 경우에도 예외 처리를 사용하면 다른 문제가 발생할 수 있습니다. 예를 들어, 만약 파일의 끝까지 데이터를 읽으려고 한다면 파일의 끝에서 발생하는 예외를 제외한 나머지 에러에 대한 예외를 받을 수 없습니다. 이는 파일의 끝을 검출하는 것도 failbit, 즉, 객체를 성공적으로 읽지 못했음을 나타내는 비트를 set하기 때문입니다. 따라서, 입력 에러와 파일의 끝을 구분하기 위해서는 스트림의 상태를 검사해야만 합니다.

 

아래 예제 코드는 위와 같은 상황을 어떻게 처리할 수 있는지 보여줍니다.

double readAndProcessSum(std::istream& strm)
{
  double value, sum;

  // save current state of exception flags
  ios::iostate oldExceptions = strm.exceptions();

  // let failbit and badbit throw exceptions
  // NOTE: failbit is also set at end-of-file
  strm.exceptions(ios::failbit | ios::badbit);

  try {
    // while stream is OK, read value and add it to sum
    sum = 0;
    while (strm >> value) {
      sum += value;
    }
  }
  catch (...) {
    // if exception not caused by end-of-file
    // - restore old state of exception flags
    // - rethrow exception
    if (!strm.eof()) {
      strm.exceptions(oldExceptions); // restore exception flags
      throw; // rethrow
    }
  }

  // restore old state of exception flags
  strm.exceptions(oldExceptions);
  // return sum
  return sum;
}

위 함수는 스트림의 예외 설정을 oldExeceptions에 저장해두고, 마지막에 복구시킬 수 있도록 합니다. 그런 다음, 특정 조건에서 스트림이 예외를 던지도록 합니다. 작업을 수행하다가 파일의 끝에 도달하면, EOF가 예외를 던지는 상황을 막기 위해 eof()로 스트림의 상태를 검사하여 지역적으로 예외를 잡을 수 있습니다. 이때, eof()가 false일 때만 예외가 전파됩니다.

'프로그래밍 > C & C++' 카테고리의 다른 글

[C++] 파일 & 문자열 스트림  (0) 2022.12.16
[C++] 스트림(stream) 클래스 (2)  (0) 2022.12.15
[C++] 함수 객체 활용  (0) 2022.12.12
[C++] Iterator Traits와 User-Defined Iterators  (0) 2022.12.11
[C++] Clocks and Timers  (0) 2022.12.08

댓글