KoreanFoodie's Study

C++ 기초 개념 7-1 : istream와 ostream 클래스 본문

Tutorials/C++ : Beginner

C++ 기초 개념 7-1 : istream와 ostream 클래스

GoldGiver 2022. 1. 10. 10:20

모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!

C++ 입출력 라이브러리

C++ 입출력 클래스는  ios_base를 기반으로 하며, 스트림의 입출력 형식 관련 데이터를 처리한다(정밀도, 10진수/16진수 등 결정). ios 클래스에서는 실제로 스트림 버퍼를 초기화하고 입출력 작업의 상태를 처리한다(파일의 끝에 도달했을 경우 eof 함수 호출, good 함수 호출로 입출력 작업 체크)

 

 

istream 클래스

istream은 실제로 입력을 수행하는데, operator>>가 istream 클래스에 정의된 연산자이다. 또한 cin은 istream 클래스의 객체 중 하나이다.

cin은 엔터나 공백을 입력 시에 무시해 버린다. 또한, operator>>는 다음과 같은 경우 주의해야 한다.

int t;
while(true) {
    std::cin >> t;
    std::cout << "입력 :: " << t << std::endl;
    if (t == 0) break;
}

 

위의 코드를 실행시키고 c를 입력하면 아래와 같이 무한 루프에 빠지게 된다.

 

ios 클래스에서 스트림의 상태를 관리하는데, 이때 사용하는 flag(1 bit)은 4가지가 있다.

goodbit : 스트림에 입출력 작업이 가능할 때

badbit : 스트림에 복구 불가능한 오류 발생시

failbit : 스트림에 복구 가능한 오류 발생시 

eofbit : 입력 작업시에 EOF 도달시

위와 같이 타입에 맞지 않는 값을 넣어서 오류가 발생하는 경우는 failbit가 켜진다. 그 후, 입력받을 받지 않고 리턴한다. 즉, 그냥 리턴을 하며 버퍼에 남아 있는 'c\n'은 그대로 남아있게 되어 while loop 안에서 문자열이 계속 읽히게 된다. 이 문제를 간단히 해결하려면 다음과 같이 수정하면 된다.

int t;
while(std::cin >> t) {
    std::cout << "입력 :: " << t << std::endl;
    if (t == 0) break;
}

 

ios에는 다음과 같은 함수가 있다.

operator void*() const;

이 함수는 ios 객체를 void*로 변환해 준다. NULL 포인터가 아닌 값을 리턴하는 조건이, failbit와 badbit가 모두 off일 때이다. 만약 while 문에서 c 를 입력한다면 operator>> 는 cin 객체의 failbit를 켜게 되고, ios 객체 -> void* -> bool로 가는 변환을 통해 while문을 빠져나올 수 있다(NULL 포인터는 bool상 false이다).

 

하지만 이미 failbit가 켜진 상태이므로, 더 이상 cin을 통한 입력을 진행할수가 없다(플래그를 초기화해야 함).

int t;
while(true) {
    std::cin >> t;
    std::cout << "입력 :: " << t << std::endl;
    if (std::cin.fail()) {
        std::cin.clear();		// 플래그들 초기화
        std::cin.ignore(100, '\n');	// 개행문자가 나올때까지 무시
    }

    if (t == 1) break;

fail 함수는 failbit가 true거나 badbit가 true면 true를 리턴한다. clear 는 플래그를 goodbit으로 초기화시키고, ignore함수는 최대 첫번째 인자 만큼 (100), 두 번째 인자가 나올 때까지 ('\n'), 버퍼에서 무시한다.

 

 

형식 플래그(format flag)와 조작자(Manipulator)

// 형식 플래그를 이용한 방법
std::cin.setf(std::ios_base::hex, std::ios_base::basefield);
std::cin >> t;

// 조작자를 이용한 방법
// std::cin >> std::hex >> t;

std::cout << "입력 :: " << t << std::endl;
if (std::cin.fail()) {
    std::cout << "제대로 입력해주세요" << std::endl;
    std::cin.clear();
}
if (t == 0) break;

setf 함수는 인자를 1개만 받아 인자로 준 형식 플래그를 적용할 수도 있고, 2개 취해서 두 번쨰 인자(위에서는 basefield)의 내용을 초기화하고, 첫 번째 인자(hex)를 적용하는 방식이 있다.

두 번째 방식은 std::hex를 바로 이용하여 cin에서 수를 받는 방식을 바꾸는 것이다. 이처럼 hex와 같이, 스트림을 조작하여 입력 혹은 출력 방식을 바꿔주는 함수를 조작자라고 부른다.

 

첫 번째 방식의 ios_base::hex는 단순한 상수 '값'이고, 아래의 조작자 hex는 ios에 정의된 '함수'이다.

우리가 흔히 사용하는 std::endl은 사실 ostream에 정의된 함수로, flush 와 개행문자 출력을 해 주는 함수이다!

 

 

스트림 버퍼

모든 입출력 객체들은 이에 대응되는 스트림(문자들의 순차적인 나열) 객체를 가지고 있게 된다. C++의 입출력 라이브러리에는 이에 대응되는 streambuf 클래스가 있다. 그리고 std::stringstream을 통해 평범한 문자열을 마치 스트림인 것처럼 이용할수도 있게 해준다.

streambuf 클래스는 버퍼의 시작 부분을 가리키는 시작 포인터와, 다음으로 읽을 문자를 가리키고 있는 포인터 (우리가 흔히 말하는 스트림 위치 지정자), 그리고 버퍼의 끝 부분을 가리키고 있는 끝 포인터가 있다. streambuf 클래스는 입력 버퍼와 출력 버퍼를 구분해서 각각 get area와 put area라 부르는데, 이에 따라 각각을 가리키는 포인터도 g와 p를 붙여 표현한다.

#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;
}

 

위의 코드에서, hello world 를 입력 값으로 주면 출력이 다음과 같이 나오게 된다.

hello world 두 번째 단어 맨 앞글자 : w 다시 읽으면 : world

 

위의 출력값이 나오는 이유만 이해하면 일단 스트림 버퍼의 기초 개념은 이해한 것이다.

Comments