CPP/씹어먹는 c++

[Cpp 개념공부]표준스트림과 입출력

뜨거운 개발자 2023. 3. 4. 21:42

C++의 입출력 라이브러리

모든 입출력 클래스의 기반 ios_base 클래스

C++ 의 모든 입출력 클래스는 ios_base 를 기반 클래스로 하게 됩니다. ios_base 클래스는 많은 일은 하지 않고, 스트림의 입출력 형식 관련 데이터를 처리 합니다.

예를 들어 실수 형을 출력할 때 정밀도를 어떤 식으로 할 것인지에 대해

아니면 정수형을 출력 시에 10진수로 할지 16진수로 할지 등을 이 클래스에서 처리 합니다.

스트림 버퍼를 초기화하는 ios 클래스

ios클래스는 실제로 스트림 버퍼를 초기화 합니다.

스트림 버퍼란

스트림버퍼란, 데이터를 내보내거나 받아들이기 전에 임시로 저장하는 곳이다.

쉬운 예시로 우리가 하드디스크에서 파일을 하나 읽을 때 일어나는 일을 생각하면 된다.

만약 사용자가 1 바이트씩 읽을때 실제로 프로그램은 1byte 씩 읽는 게 아니다.

실제로는 한 뭉터기 (예를 들어서 512 바이트) 를 한꺼번에 읽어서 스트림 버퍼에 잠시 저장해 놓은 뒤에 사용자가 요청할 때 마다 1 바이트 씩 꺼내는 것이다.

만일 버퍼를 다 읽는다면 다시 하드에서 512 바이트를 다시 읽어오는 식으로 작동한다.

스트림 버퍼를 사용하는 이유

이렇게 하는 이유는 하드디스크에서 데이터를 읽어오는 작업이 매우 느리기 때문이다.

한 번 읽을 때 하드디스크에서 1 바이트 읽으면 읽는 속도보다 하드디스크에서 작동 속도가 더 느려서 큰 딜레이가 발생하게 된다. 즉 한번 읽을 때 많은 양의 데이터를 읽어서 하드디스크에서 발생하는 딜레이를 최소화 하려는 원리이다.

이는 쓰는 작업에서도 마찬가지이다.

쓸 때도 우리가 1 문자를 출력 하게 되면, 하드에 바로 쓰는 것이 아니라 일단 버퍼에 보관한 후, 어느 정도 모인 뒤에 출력하게 된다.

아마 printf함수와 write함수를 동시해 사용해본 사람이라면 write함수가 printf보다 먼저 출력이 되는 모습을 볼 수 있었을 텐데 그게 이런 원리때문에 그런 것이다.

ios클래스의 현재 입출력 작업의 상태 처리하기

ios 클래스에선 그 외에도, 현재 입출력 작업의 상태를 처리 한다.

예를 들어, 파일을 읽다가 끝에 도달했는지 안했는지 확인하려면 eof 함수를 호출하면 된다.

또, 현재 입출력 작업을 잘 수행할 수 있는지 확인하려면 good 함수를 호출하면 된다.

eof함수

eof함수는 ios클래스에 정의되어있고 eofbit가 설정되었는지 확인합니다.

eofbit 플래그는 입력 작업 중 파일 끝(End Of File) 에 도달하였을 때 설정되는 플래그 이다.

스트림의 오류 상태 플래그 eofbit 이 설정되었을 때 true 를 리턴한다.

good함수

이 함수는 스트림의 오류 상태 플래그(eofbit, failbit, badbit) 이 모두 설정되지 않을 때 true 를 리턴한다.

한 가지 주의할 점은 이 함수는 bad 함수와 정 반대가 아니라는 사실이다. bad 함수는 badbit 플래그가 설정되어 있는지의 여부만 확인하는 함수이다.

오류 상태 플래그들은 eof, fail, bad 함수들을 이용해서 독립적으로 체크할 수 있다.

참고

  • ios::fail : failbit 이나 badbit 이 설정되어 있는지 확인한다.
  • ios::bad : badbit 이 설정되어 있는지 확인한다.
  • ios::good: 스트림에 어떠한 오류 플래그도 설정되지 않았는지 확인한다.
  • ios::rdstate : 오류 상태 플래그를 얻는다.
  • ios::clear : 오류 상태 플래그를 설정한다.
  • ios::setstate : 오류 상태 플래그를 설정한다.
  •  

istream 클래스

ios_base 와 ios 클래스들이 입출력 작업을 위해 바탕을 깔아주는 클래스 였다면, istream 은 실제로 입력을 수행하는 클래스이다.

operator>>

대표적으로 우리가 항상 사용하던 operator>> 가 이 istream 클래스에 정의되어 있는 연산자이다.

operator>> 의 또 다른 특징으로는, 모든 공백문자 (띄어쓰기나 엔터, 탭 등)을 입력시에 무시해버린다는 점입니다. 그렇기 때문에, 만일 cin 을 통해서 문장을 입력 받는 다면, 첫 단어 만 입력 받고 나머지를 읽을 수 없습니다.

operator>>의 주의할 점

#include <iostream>
using namespace std;
int main() {
  int t;
  while (true) {
    std::cin >> t;
    std::cout << "입력 :: " << t << std::endl;
    if (t == 0) break;
  }
}

만약 사용자가 입력으로 숫자만 넣었을 때 이 코드는 정상적으로 작동 합니다.

만일 사용자가, 숫자가 아니라 문자를 입력하면 무한 루프에 빠지게 됩니다.

그 이유는 operator>>가 어떻게 입력을 처리하는지를 확인하면 알 수가 있습니다.

무한 루프에 빠지는 원인

ios 클래스에서 스트림의 상태를 관리한다.

스트림의 상태를 관리하는 플래그 (flag - 그냥 비트 1 개라 생각하면 됩니다) 는 4 개가 정의되어 있다.

이 4 개의 플래그들이 스트림이 현재 어떠한 상태인지에 대해서 정보를 보관한다

4 개의 플래그는 각각 goodbit, badbit, eofbit, failbit가 있다.

각각의 비트가 켜져있는지 꺼져있는지에 따라서 우리는 스트림의 상태를 알 수가 있다.

각각의 상황에 따라서 스트림 관리 비트가 켜지는 조건

  • goodbit : 스트림에 입출력 작업이 가능할 때
  • badbit : 스트림에 복구 불가능한 오류 발생시
  • failbit : 스트림에 복구 가능한 오류 발생시
  • eofbit : 입력 작업시에 EOF 도달시

숫자 입력에서 문자를 넣었을 때 켜지는 비트

만일 위와 같이 숫자를 입력해야 하는 상황에서 문자를 입력할 경우 operator>> 는 어떤 비트를 킬까?

일단 끝에 도달한 것이 아니라서 eofbit 는 확실히 아니다.

badbit 는 스트림 상에서 복구할 수 없는 문제시 켜지는데 위 경우는 그렇게 심각한 것은 아니다. 그냥 현재 스트림 버퍼에 들어가 있는 'c\n' 이 문자열을 제거해버리면 되기 때문이다.

타입에 맞지 않는 값을 넣어서 오류가 발생하는 경우에는 failbit 가 켜지게 된다. 그리고, 입력값을 받지 않고 리턴해버린다.

이렇게 그냥 리턴해버리면 버퍼에 남아있는 문자는 계속해서 남아있다. 즉 숫자를 입력하려고 했는데 문자 ‘c\n’을 넣어주게 되면 스트림 버퍼에 계속해서 ‘c\n’이 남아있는 것이다.

그렇게 입력 스트림의 버퍼를 비워주지 않고 계속해서 cin으로 접근하게 되면 이미 버퍼안에 값이 있기 때문에 계속해서 읽고 badbit초기화 후 리턴을 반복하게 되는 것이다.

무한 루프 해결법

while (std::cin >> t) 위의 while문에 조건을 저렇게 해주면 된다.

ios 에 정의되어 있는 함수들중 operator void*() const; 라는 함수가 있는데 이 함수가 ios 객체를 void* 로 변환해준다.

NULL 포인터가 아닌 값을 리턴하는 조건이, failbit 와 badbit 가 모두 off 일 때이다.(eof비트는 아니다.)

다만 이렇게 해주면 이미 iostream에서 failbit가 켜진 상태이기 때문에 무한루프는 없지만 더이상 입력을 받을 수는 없게 됩니다.

#include <iostream>
#include <string>

int main() {
  int t;
  while (true) {
    std::cin >> t;
    std::cout << "입력 :: " << t << std::endl;
    if (std::cin.fail()) {
      std::cout << "제대로 입력해주세요" << std::endl;
      std::cin.clear();            // 플래그들을 초기화 하고
      std::cin.ignore(100, '\\n');  // 개행문자가 나올 때 까지 무시한다
    }
    if (t == 1) break;
  }
}

위와 같이 cin의 객체의 비트 플래그들을 초기화 해주고 ‘\n’ 이 나올 때까지 무시해주면(즉 스트림 버퍼를 비워주면) 다시 입력을 사용 할 수있다.

만일 버퍼에 100자 이상을 집어 넣는다면 위와 같이 ignore 함수가 총 3번 호출됨을 알 수 있다. (버퍼에 남아 있는 문자들이 다 지워질때 까지)

참고 그림

외부함수로 연산자 오버로딩을 구현 하고 싶을 때

std::ostream& operator<<(std::ostream& os, const Complex& c) {
  os << "( " << c.real << " , " << c.img << " ) ";
  return os;
}

형식 플레그와 조작자

입출력 형식 바꾸기

우리는 ios_base 클래스에서 스트림의 입출력 형식을 바꿀 수 있다고 했다.(예: 10진수 입력을 16진수로)

예시 코드

#include <string>
#include <iostream>

int main() {
  int t;
  while (true) {
    std::cin.setf(std::ios_base::hex, std::ios_base::basefield);
    std::cin >> t;
    std::cout << "입력 :: " << t << std::endl;
    if (std::cin.fail()) {
      std::cout << "제대로 입력해주세요" << std::endl;
      std::cin.clear();  // 플래그들을 초기화 하고
                         // std::cin.ignore(100,'n');//개행문자가 나올 때까지
                         // 무시한다
    }
    if (t == 0) break;
  }
}

위의 예시 코드를 보면, cout을 건들지 않고 cin만 고치는 걸 볼 수가 있다.

여기서 스트림의 입출력 형식을 기본은 10진수로 되어있지만 setf 함수를 사용하면 객체의 입출력 형식을 변경할 수 있다는 것을 알 수가 있다.

직접 구현하려면 상당히 많은 if문으로 사용해야 할텐데, 이렇게 알아서 스트림 데이터를 입력 받는 방식을 바꿔주기 때문에 스트림 데이터 자체로 파싱을 해주므로 많은 예외처리가 필요가 없다

스트림의 설정을 바꾸는 setf 함수

setf함수는 인자 갯수에 따라서 두가지 형식이 있다.

  1. 인자가 1개인경우: 인자로 준 형식플레그를 적용하는 것.
  2. 보통 인자가 하나인 setf함수의 경우, 단항 서식 플래그(독립적으로 기능을 수행하는 것)들, boolalpha, showbase, showpoint, showpos, skipws, unitbuf, uppercase를 설정하기 위해 사용되고, 이들 역시 unsetf 에서도 사용된다
  3. 인자가 2개인경우: 2번째 인자의 내용은 마스크이고 첫번째 인자는 대상입니다.이렇게 인자가 두개인 경우는 보통 선택적인 플래그들(반드시 하나만 켜져 있어야 하는경우) 를 조작하기 위해서는 아래와 같은 비트 마스크와 플래그 값들을 이용한다fmtfl 서식 플래그 값(첫번째 인자) mask 필드 비트마스크(두번째 인자) 
    left, right, internal adjustfield
    dec, oct, hex basefield
    scientific, fixed floatfield
  4. 플래그와 마스크
  5. 따라서 두번째 인자를 첫번째 인자에 적용한다. 이렇게 생각하면 됩니다.

함수 조작자

std::cin >> std::hex >> t;

이 코드로 하면 cin이 가능한 이유 : hex가 cin에서 수를 받는 방식을 바꿔버렸기 때문

<aside> 💡 hex와 같이, 스트림을 조작하여 입력 혹은 출력 방식을 바꿔주는 함수를 조작자라고 부른다!

</aside>

여기서 hex는 함수이다. 참고로, 위에서 사용하였던 형식 플래그 hex와 이 hex는 이름만 같지 아예 다른 것이다 . ios_base::hex 와 std:: hex의 차이다.

형식 플래그 hex는 ios_base에 선언되어 있는 단순한 상수 '값'

반면에 조작자 hex의 경우 ios에 정의되어 있는 '함수'이다.

이 함수가 해주는 것이 setf를 실행해주는 것과 같고, 이게 작동하는 이유는 조작자를 인자로 받는 연산자함수가 오버로딩이 되어 있기 때문이다.

조작자들의 종류는 위에서 설명한 hex말고도, 꽤 많은데 true나 false를 1 과 0 으로 처리하는 대신 문자열 그대로 입력 받는 boolalpha도 있고, 출력 형식으로 왼쪽 혹은 오른쪽으로 정렬 시키는 left와 right조작자 등 여러가지가 있다.

std::endl도 있습니다. endl은 hex와는 달리 출력을 관장하는 ostream에 정의되어 있는 조작자로, 한 줄 개행문자를 출력하는 것 말고도, 버퍼를 모두 내보내는(flush) 역할도 수행한다.

flush란

문자 1 개를 내보낸다고 해서 화면에 바로 출력되는 것이 아니라, 버퍼에 모은 다음에 버퍼에 어느 정도 쌓이면 비로소 출력하게 됩니다. 이렇게 한다고 해서 대부분의 경우 문제되지는 않습니다. 하지만 예를 들어 정해진 시간에 딱딱 맞춰서 화면에 출력해야 한다면 어떨까요? 이 경우 버퍼에 저장할 필요없이 화면에 바로 내보내야 할 것입니다.

이럴 경우를 위해서, 버퍼에 데이터가 얼마나 쌓여있든지 간에 바로 출력을 해주는 flush 함수가 있습니다. 따라서, std::endl 조작자는, 스트림에 '\n' 을 출력하는 것과 더불어 flush 를 수행해준다는 사실을 알 수 있습니다.

스트림버퍼

  1. 스트림(Stream)이란?스트림(stream)이란 실제의 입력이나 출력이 표현된 데이터의 이상화된 흐름을 의미합니다.
    • 일반적으로 데이터,패킷,비트 등의 일련의 연속성을 갖는 흐름을 의미
    • 음성,영상,데이터 등의 작은 조각들이 하나의 줄기를 이루며전송되는 데이터 열(列)
    • 호스트 상호간 또는 동일호스트 내프로세스 상호간통신에서큐에 의한메세지 전달방식을 이용한 가상 연결 통로를 의미하기도 함
    •  
  2. 즉, 스트림은 운영체제에 의해 생성되는 가상의 연결 고리를 의미하며, 중간 매개자 역할을 합니다.
  3. C++ 프로그램은 파일이나 콘솔의 입출력을 직접 다루지 않고, 스트림(stream)이라는 흐름을 통해 다룹니다.
  4. 프로그래밍언어상의스트림
    • ※C 언어에서 스트림     - 연속된 문자 또는 데이터.
    • 크게 텍스트(바이트) 스트림 및 바이너리(이진) 스트림으로 구분
    • ANSI C 의 표준파일 스트림 例 : stdin, stdout, stderr 등
    •  
  5. 스트림 사용 이유
    • 물리 디스크상의 파일,장치들을 통일된 방식으로 다루기 위한 가상적인 개념
    • 따라서, 스트림은 어디서 나왔는지 어디로 가는지 신경을 쓸 필요없이 자유롭게 어떤 장치 및 프로세스, 파일들과 연결될 수 있어 프로그래머에게 많은 편리성 줌

스트림이란 : 문자들의 순차적인 나열이다.

모든 입출력 객체들은 이에 대응되는 스트림 객체를 가지고 있게 된다. 따라서 C++ 의 입출력 라이브러리에는 이에 대응되는 스트림 버퍼 클래스도 있는데, 이름이 streambuf클래스 이다.

우리가 화면에 입력하는 문자도 스트림을 통해서 프로그램에 전달되는 것이고, 하드디스크에서 파일을 읽는 것도, 다른 컴퓨터와 TCP/IP통신하는 것도 (결국 문자들을 쭈루륵 주고받는 것이니까), 모두 스트림을 통해 이루어진다는 것이다.

문자열을 스트림처럼 이용하는 방법

C++ 에서는 std::stringstream을 통해서 평범한 문자열을 마치 스트림인 것 처럼 이용할 수 도 있게도 해준다.

streambuf 클래스 : c++ 입출력 라이브러리에 대응되는 스트림버퍼 클래스이다. 즉 streambuf클래스는 스트림에 대한 기본적인 제어를 담당하고 있다.

스트림의 상태는 3개의 포인터를 이용해 나타낸다.

  1. 버퍼의 시작을 가리키는 포인터
  2. 다음으로 읽을 글자를 가리키는 포인터(스트림 위치 지정자)
  3. 버퍼의 끝을 가리키는 포인터
  • 입력버퍼 : get area, 출력버퍼 : put area 라고 부른다.

조작예시

#include <iostream>
#include <string>

int main() {
  std::string s;
  std::cin >> s;

  // 위치 지정자를 한 칸 옮기고, 그 다음 문자를 훔쳐본다 (이 때는 움직이지 않음)
  char peek = std::cin.rdbuf()->snextc();
  if (std::cin.fail()) std::cout << "Failed";
  std::cout << "두 번째 단어 맨 앞글자 : " << peek << std::endl;
  std::cin >> s;
  std::cout << "다시 읽으면 : " << s << std::endl;
}

rdbuff

입력 객체 cin의 rdbuf를 호출하게 되면, cin객체가 입력을 수행하고 있던 streambuf객체를 가리키는 포인터를 리턴하게 됩니다. 이 때, cin객체가 istream객체 이므로, 오직 입력만을 수행하고 있기에, 이 streambuf객체에는 오직 get area만 있음을 알 수 있습니다.

snextc함수는 스트림 위치 지정자를 한칸 전진 후 문자를 엿본다.

스트림 위치 지정자를 한 칸 전진시킨 후, 그 자리에 해당하는 문자를 엿봅니다 (읽는 것이 아닙니다).

엿보는 것과 읽는 것의 차이점은, 보통 streambuf객체에서 읽게 되면,스트림 위치 지정자를 한 칸 전진시켜서 다음 읽기 때 다음 문자를 읽을 수 있도록 준비해줍니다.

하지만 엿본다는 것은, 해당 문자를 읽고도 스트림 위치 지정자를 움직이지 않는다는 것입니다.

snextc함수 말고도 수 많은 함수들이 정의되어 있습니다. 물론 이 함수들을 직접 사용할 일은 거의 없겠지만, C++ 입출력 라이브러리는 스트림 버퍼도 추상화해서 클래스로 만들었다는 것 정도 알면 충분합니다.

C++ 에서 streambuf 를 도입한 중요한 이유 한 가지는, 1 바이트 짜리 문자 뿐만이 아니라,wchar_t, 즉 다중 바이트 문자들 (우리가 흔히 말하는 UTF-8 같은 것이지요)에 대한 처리도 용이하게 하기 위해서라고 합니다.

예를 들어서, 다중 바이트 문자의 경우, 사용자가 문자 한 개만 요구했음에도 스트림에서는 1 바이트만 읽을 수 있고, 2 바이트, 심지어 4 바이트 까지 필요한 경우가 있습니다. C++ 에서는 이러한 것들에 대한 처리를 스트림 버퍼 객체 자체에서 수행하도록 해서, 사용자가 입출력 처리를 이용하는데 훨씬 용이하게 하였습니다.

728x90