6 minute read

🏫Chapters 13 : Inout/Output

Input/Output

  • 기본적인 단위는 바이트 단위
  • 바이트의 1차원적인 흐름(Stream)

  • Ostream : Output Stream
  • Istream : Input Stream
    폰노이만 방식의 컴퓨터에서 모든 데이터에 대한 처리는 cpu를 거치는데, 즉 cpu로 들어오는게 인풋, 나가는게 아웃풋이다.

    이때 셔틀버스와 같은 버퍼라는 개념이 있다. 모두 여기에 저장되었다가 한번에 나간다

    «, » 함수를 통하지 않고 연산자 오버로딩을 허용하기 때문에 여기에도 연산자 오버로딩을 통한 확장성을 가지게 된다.

여태껏 standard 인풋/아웃풋인 cincout을 써왔다. 이것을 file로도 확장할 수 있고, 데이터가 오가는 모든 지엽적인 것들에도 쓸 수 있도록 확장할 수 있다.(다형성)


Files

  • 파일을 쓰고 읽는 것은 이미 많이 해봤는데(그런가..?), stream이라는 전략을 택한다. stream을 택하면 다형성이라는 이익을 얻을 수 있다. stream은 바이트의 1차원의 flow라고 해석할 수 있는데, 화면에 출력하는 것도 파일에 원하는 내용을 쓰는 것도 네트워크 하드에게 데이터를 전송하는 것도 데이터를 받는 것도 스캐너로부터 이미지를 받는 것도 모두 바이트의 1차원 flow이다. 이러한 바이트의 1차원의 flow를 «, » 라고 하는 일반적인 stream 연산자를 통해 똑같이 표현할 수 있다는 것이다.

  • 예전에는 키보드로부터 데이터를 읽느냐, 스캐너로부터 읽느냐, 파일로부터 읽느냐 등에 따라 모두 읽는 함수가 달랐다. 그러나 다형성이 추구하는 철학에 따라 추상적인 역할은 똑같으므로 표기법을 통일한다. 따라서 연산자 오버로딩이 지원되어야 했고, 다형성이 지원된다.

  • cin, cout을 파일에도 똑같이 적용해보자. 이 때 핸들을 잡아야 파일에 접근할 수 있고, 핸들의 개수는 제한되어 있다. 예전에는 이 핸들의 개수가 매우 적었다. 그래서 열고 닫는것이 매우 중요했다. 소프트웨어가 점점 발전하면서 핸들의 개수가 늘어났다. 그래서 핸들의 개수가 꽉 차서 생기는 문제는 거의 없지만, I/O작업은 기본적으로 버퍼를 통하기 때문에 버퍼에 차있는데 핸들이 닫혀버리는(프로그램이 끝나는) 문제가 생길 수 있다. 버퍼에만 있고 실제로는 write되지 않았지만 컴퓨터는 write된 것으로 인식되고 buffer의 내용은 증발된다. 따라서 핸들의 클로즈는 매우 중요하다

  • 마지막으로, 어떤 문서를 띄우고, 그 문서를 지우려 하면 지울 수 없는 알람이 뜨는데, 이것이 바로 이 걸린것이다. 다른 프로세스가 그 문서를 변경하고 접근하지 못하도록 락을 걸어준다. close를 해주지 않으면 락이 풀리지 않기 때문에 파일을 오픈하고 클로즈하는 것은 매우 중요한 원칙이다.

  • 여러사람이 동시에 파일을 읽기 위해 열 수는 있다. 그러나 쓰겠다고 열면 다른 사람은 접근하지 못한다. 파일은 읽기와 쓰기를 다르게 취급한다. 자원의 효율적인 활용 측면에서 지원하는 것이다. 아무튼 반드시 파일을 close 해주어야 하는데 문제는 잘 close 해주지 않는다. 그래서 파일을 close 하는 것은 destructor(소멸자)에 포함된다. c++에서는 파일에 접근하는 것을 클래스화 시키고, 소멸자에 파일을 close하도록 시키고 있다.


Opening a file for reading

  // …
  int main()
  {
    cout << "Please enter input file name: ";
    string iname;
    cin >> iname;
    ifstream ist {iname}; // ifstream is an“input stream from a file”
    // defining an ifstream with a name string
    // opens the file of that name for reading
    if (!ist) error("can’t open input file ", iname);
    // …
  • Istream, Ostream을 상속받은 ifstream(input file stream), iname을 주면 생성자에서는 저 파일을 자동으로 오픈한다. 그리고 나서 명시적으로 클로즈 하지 않아도, ist(지역변수)에 의해 소멸자가 호출되고 자동으로 파일을 클로즈 한다.


Opening a file for writing

    // …
    cout << "Please enter name of output file: ";
    string oname;
    cin >> oname;
    ofstream ofs {oname}; // ofstream is an “output stream from a file”
    // defining an ofstream with a name string
    // opens the file with that name for writing
    if (!ofs) error("can’t open output file ", oname);
    // …
  }

  • 위와 같다. Ofstream(output file stream)


Reading from a file

  0 60.7
  1 100
  2 926
  3 2.22
  • sample.txt 라는 파일을 하나 만들어보자. 맨 앞 숫자는 시간, 그 뒤의 숫자는 화씨를 적어 놓은 것이다.
    #include<iostream>
    #include<fstream>
    #include<vector>

    using namespace std;

    class Reading
    {
    public:
      Reading(int hour, double temp);
      ~Reading();
      int hour;
      double temp;
    private:

    };

    Reading::Reading(int hour, double temp)
    {
      this->hour = hour;
      this->temp = temp;
    }

    Reading::~Reading()
    {
    }

    int main(void) {
      vector<Reading> temps;
      ifstream ist( "E:/sample.txt" );
      
      int hour;
      double temp;
      
      while (ist >> hour >> temp) {
        if (hour < 0 || 23 < hour) {
          cout << "hour out of " << temp << endl;
        }
        cout << hour << ", " << temp << endl;
        temps.push_back(Reading{ hour, temp });
      }
    }


I/O error handling

  • IOstream의 클래스는 자신의 state를 알리는 함수를 virtual로 가지고 있다

    good() // the operation succeeded
    eof() // we hit the end of input (“end of file”)
    fail() // something unexpected happened
    bad() // something unexpected and serious happened

    • bad는 복구 불가능한 문제상황
    • fail은 복구 불가능인지 아닌지 모름
  • 에러 핸들링 함수를 추가해보자

    • int만 쭉 있는데, eof이거나 약속되어져 있는 terminator char를 만나면 멈추도록 한다
    void fill_vector(istream& ist, vector<int>& v, char terminator)
    { 
      // read integers from ist into v until we reach eof() or terminator
      for (int i; ist >> i; )   // read until “some failure”
        v.push_back(i);         // store in v
        
      if (ist.eof()) return;              // fine: we found the end of file
      if (ist.bad()) error("ist is bad"); // stream corrupted; let’s get out of here!
        
      if (ist.fail()) {   // clean up the mess as best we can and report the problem
        ist.clear();      // clear stream state, so that we can look for terminator
        char c;
        ist >> c;         // read a character, hopefully terminator
          
        if (c != terminator) {          // unexpected character
          ist.unget();                  // put that character back
          ist.clear(ios_base::failbit); // set the state back to fail()
        }
      }
    }
    
    • for문이 멈추는 이유는 bad, fail이거나 eof때문에
    • eof인 경우 파일의 끝이므로 괜찮다
    • bad는 큰일이므로 에러메세지
    • fail이라면 왜 그런지 고민해보자는 것. state를 클리어 시키고, ist로부터 무엇때문에 문제인지 문자를 하나 읽는다. 이 문자가 약속된 char이면 그대로 종료시키면 되고, 그것때문이 아니라면 fail이라고 해주어야 한다.


Throw an exception for bad

  • 사실 exception처리를 하는 것이 바람직하다
  • 예외가 뜨진 않았는데 우리가 예외로 띄울지 아닐지는 정할 수 있다는 것이다.
  • ist.exceptions(ist.exceptions()|ios_base::failbit); failbit는 예외를 날리라는 것이다. 기본적인 디폴트는 예외를 던지지 않고 참는 것이다. status에 fail이라고 기록만 해둔다. 그러나 좀 더 C++스러운 방식은 예외를 날리는 것이다. exception을 날려 명시적으로 문제 상황을 알리는 것이 C++ 답다. 기존의 비트는 그대로 유지하고 fail비트를 추가로 킨다고..? 뭔소리람..
  int main(void) {
    vector<Reading> temps;
    ifstream ist( "E:/sample2.txt" );
    fill_vector(ist, temps, 'q');

    ist.exceptions(ist.exceptions() | ios_base::failbit);
    try {
      fill_vector(ist, temps, 'q');
    } catch (ios_base::failure &e) {
      cout << "exception handling" << endl;
    }
    cout << ist.fail() << endl;		// 0이 찍히면 fail아님

eof도 fail의 하나로 본다
eof는 fail의 스페셜한 케이스로, 마지막 문자 하나를 가져오기 때문에 eof와 fail이 서로 다른 결과일 수 있다.

  • EX에제
  #include<iostream>
  #include<fstream>
  #include<vector>

  using namespace std;

  class Reading
  {
  public:
    Reading(int hour, double temp);
    ~Reading();
    int hour;
    double temp;
  private:

  };

  Reading::Reading(int hour, double temp)
  {
    this->hour = hour;
    this->temp = temp;
  }

  Reading::~Reading()
  {
  }

  void fill_vector(istream& ist, vector<int>& v, char terminator)
  { 
    // read integers from ist into v until we reach eof() or terminator
    for (int i; ist >> i; ) {       // read until “some failure”
      v.push_back(i);				// store in v
    }
    if (ist.eof()) return;              // fine: we found the end of file
    if (ist.bad()) cout << "ist is bad" << endl; // stream corrupted; let’s get out of here!

    if (ist.fail()) {     // clean up the mess as best we can and report the problem
      ist.clear();      // clear stream state, so that we can look for terminator
      char c;
      ist >> c;         // read a character, hopefully terminator

      if (c != terminator) {            // unexpected character
        ist.unget();                  // put that character back
        ist.clear(ios_base::failbit); // set the state back to fail()
      }
    }
  }

  int main(void) {
    vector<Reading> temps;
    ifstream ist("E:/sample.txt");
    int hour;
    double temp;
    
    while (ist >> hour >> temp) {
      if (hour < 0 || 23 < hour) {
        cout << "hour out of " << temp << endl;
      }
      cout << hour << ", " << temp << endl;
      temps.push_back(Reading{ hour, temp });
    }

    vector<int> temp2;
    ifstream ist2( "E:/sample2.txt" );
    fill_vector(ist2, temp2, 'q');

    //ist.exceptions(ist.exceptions() | ios_base::failbit);
    try {
      fill_vector(ist2, temp2, 'q');
    } catch (ios_base::failure &e) {
      cout << "exception handling" << endl;
    }
    cout << ist2.fail() << endl;		// 0이 찍히면 fail아님
    cout << ist2.eof() << endl;		

  }


Reading a single value

  • range를 체크해주는 것도 좋은 습관이다
  • 만약 원하는 범위 안의 값이 들어오지 않았을 때는 어떻게 하는 것이 좋을까?
    1. 눈을 질끈 감는다
    2. 문제 상황에 대한 해결을 차근차근 한다(C스타일)
      단, 정상과 비정상상황이 코드로 한번에 보이지 않는다
    3. 예외처리 해준다(C++ 스타일)
      문제상황을 무시할 수 없게 된다.