[Advanced C++] 72. I/O stream, format flag & manipulator

dev.kelvin·2025년 8월 9일
1

Advanced C++

목록 보기
72/74
post-thumbnail

1. I/O stream

I/O stream

C++에서 입출력 기능은 STL을 통해 제공된다 (std namespace에 포함되어 있음), < iostream >헤더를 include하여 cin과 cout을 사용할 수 있다

iostream 헤더에는 입출력 기능을 제공하는 클래스가 존재한다, iostream 라이브러리는 다중 상속구조이다

C++의 입출력은 stream으로 구현된다, stream이란 순차적으로 접근할 수 있는 byte의 연속적인 흐름이다 (쉽게 말해 데이터가 흐르는 흐름, 통로)

  • 입력 스트림 : 키보드, 파일, 네트워크와 같은 데이터 생상자로부터 입력을 담는데 사용된다, 예를들어 키보드 입력이 되면 이 입력은 입력 스트림으로 들어가서 해당 프로그램이 이 입력을 가져갈 준비가 될 때까지 기다린다

  • 출력 스트림 : 모니터, 파일, 프린터와 같은 데이터 소비자를 위한 출력을 담는데 사용된다, 마찬가지로 출력 장치가 준비 될 때까지 이 데이터는 출력 스트림에 머무른다

이렇게 데이터가 머무르는 임시 저장소를 buffer라고 한다

결국 프로그래머는 다양한 종류의 장치에 데이터를 읽고 쓰기위해 stream에만 접근하면 된다, 자세한 상호작용은 OS에서 담당한다

ios란 입력 스트림, 출력 스트림 모두에 공통적인 여러가지를 정의하는 std::basic_ios< char >에 대한 typedef이다

istream은 입력 스트림을 다룰때 사용되는 클래스이다, 이때 operator>>를 사용하여 해당 스트림에서 값을 추출한다
ex) std::cin >> variable;은 입력 스트림에서 값을 추출해서 variable에 저장한다

ostream은 출력 스트림을 다룰때 사용되는 클래스이다, 이때 operator<<를 사용하여 해당 스트림에 값을 추가한다
ex) std::cout << "Hi";는 Hi문자열을 ostream에 추가한다

따라서 iostream은 istream, ostream을 모두 상속받은 다중 상속 클래스로 읽기 쓰기 모두 해야할 때 사용한다

표준 스트림은 프로그램 환경에 의해 미리 연결되어 제공되는 스트림을 의미한다

  • cin : 표준 입력에 연결된 istream 객체
  • cout : 포준 출력에 연결된 ostream 객체
  • cerr : 표준 에러에 연결된 ostream 객체 (unbuffered)
  • clog : 표준 에러에 연결된 ostream 객체 (buffered)

unbuffered 출력은 그 즉시 처리하지만 buffered 출력은 저장되었다가 한번에 출력된다

istream

operator>>를 이용하여 입력 스트림으로부터 데이터를 추출할 수 있다

이때 가장 주의해야 할 점은 입력이 buffer를 넘치지 않게 하는것이다

	char buf[10]{};
    std::cin >> buf;

위 코드에서 10글자를 넘겨서 입력하게 되면 buffer overflow가 발생하게 된다 (다른 메모리 영역을 침범할 가능성이 있기 때문에 굉장히 위험함)

사용자가 몇 글자를 입력할 지 모르니 이를 예측하는 형태로 코드를 작성하는건 굉장히 좋지 않은 방식이다

만약 읽어들일 문자의 수를 제한하고 싶다면 < iomanip > 헤더에 있는 setw라는 manipulator를 사용해야 한다

    char buf[10]{};
    std::cin >> std::setw(10) >> buf;

이렇게 하면 입력 스트림으로부터 시작부터 9글자(종단 문자 \0 1개를 위해 하나 빠짐)만 추출하고 나머지 남은 문자들은 다음 추출이 있을때까지 입력 스트림에 남아있게 된다

이전에 정리했듯 operator>>는 공백을 건너뛴다 (space, tab, enter)

    int main()
    {
        char ch{};
        while (std::cin >> ch)
        {
            std::cout << ch;
        }

        return 0;
    }

위 코드에 "Hello My Name Is Kelvin"을 입력하게 되면 공백 문자가 무시되어 HelloMyNameIsKelvin으로 출력되게 된다

그렇다면 공백을 무시하지 않으려면 어떻게 해야할까? get()이나 getline()을 사용해야 한다

    char ch{};
    while (std::cin.get(ch))
    {
        std::cout << ch;
    }

get()은 입력 스트림에서 문자 하나를 가져오는 함수이다, 이때 공백도 가져오게 된다

get()에는 입력 스트림에서 읽어올 최대 문자 수를 지정할 수 있다

    char ch[11]{};
    std::cin.get(ch, 11);
    std::cout << ch;

이제 Hello My N까지만 출력되게 된다

이때 get()은 \n은 읽어들이지 않는다 따라서 Hello를 치고 엔터를 치면 Hello만 출력되게 된다

    char ch[11]{};
    std::cin.get(ch, 11);
    std::cout << ch;

    std::cin.get(ch, 11);
    std::cout << ch;

분명 밑에 std::cin.get(ch, 11);코드가 하나 더 있지만 \n이 입력 스트림에 남아있기 때문에 그대로 종료된다

따라서 이런 상황에서는 getline()을 사용해야 한다, getline()은 줄바꿈 문자를 입력 스트림에서 읽고 버린다

    char ch[11]{};
    std::cin.getline(ch, 11);
    std::cout << ch;

    std::cin.getline(ch, 11);
    std::cout << ch;

이제 엔터를 쳐도 밑에서 이어서 입력이 가능하다

getline()으로 몇개의 문자가 입력 스트림에서 추출되었는지 알기 위해서는 gcount()를 사용한다

    char ch[11]{};
    std::cin.getline(ch, 11);
    std::cout << ch << '\n';
    std::cout << std::cin.gcount();

Hello를 치고 enter를 치면 총 6이 나오게 된다 (\n까지 포함해서)

char타입이 아닌 std::string타입에도 std::getline()으로 사용이 가능하다 (< string > 헤더 포함 필수)

    std::string strBuf{};
    std::getline(std::cin, strBuf);
    std::cout << strBuf;

std::getline()은 istream의 멤버 함수가 아니다 (cin.getline()은 istream의 멤버함수)

std::string을 사용하면 사용자의 입력 개수를 걱정할 필요가 없다, std::string은 입력 길이에 맞춰 자동으로 메모리를 조절하기 때문이다 (buffer overflow 신경 안 써도 됨)

이외에 다양한 입력 스트림 제어 함수들이 존재한다

  • ignore() : stream의 첫번째 문자를 버린다
  • ignore(int nCount) : 첫 nCount개의 문자를 버린다
    std::string s;

    std::cin >> s;
    std::cin.ignore();

    std::cout << s;

Kelvin입력 후 엔터키를 누르게 되면 입력 스트림에는 K e l v i n \n 이 남이있게 되고 std::cin >> 에 의해 공백 문자 이전 값인 k e l v i n만 s에 들어가게 된다

이제 남은 입력 스트림에는 \n만 남아있는 상태고 std::cin.ignore()로 해당 stream의 첫번째 문자인 \n이 제거 되게 된다

이때 ignore(3)을 하게 된다면 \n말고 2개를 더 지워야 하기 때문에 추가 입력을 기다리게 된다

    std::string s;

    std::cin >> s;
    std::cin.ignore(3);

	std::getline(std::cin, s);
    std::cout << s;

kelvin을 치고 enter, park을 치게되면 s는 rk만 나오게 된다 (공백문자 1개, 뒤에 p, a까지 해서 3개가 ignore()됨)

  • peek() : stream에서 다음 문자를 제거하지 않고 읽게 해준다
    char ch = std::cin.peek();
    std::cout << ch;
    
    //kelvin 입력 시 k가 나오게 된다, 이때 스트림에서 제거되지 않는다
  • unget() : 마지막으로 읽은 문자를 stream으로 되돌려 놓는다, 다음 호출에서 다시 읽을 수 있음
	char ch[11]{};
    std::cin >> ch;
    std::cin.unget();
    std::cin >> ch;
    std::cout << ch;
    
    //kelvin입력 시 해당 스트림의 마지막인 n을 되돌려 놓기 때문에 추가 입력 없이 buffer의 남은 n이 출력된다
  • putback(char ch) : 원하는 문자를 stream으로 되돌려 놓아 다음 호출에서 다시 읽을 수 있게 해준다
	char ch[11]{};
    std::cin >> ch;
    std::cin.putback('k');
    std::cin >> ch;
    std::cout << ch;
    
    //kelvin입력 후 지정한 k를 해당 스트림으로 되돌려 놓기 때문에 추가 입력 없이 buffer의 남은 k가 출력된다

ostream

operator>>는 출력 스트림에 데이터를 삽입하는데 사용된다

우리가 숫자, 문자열등을 다양한 형식으로 출력할 수 있는 이유는 ostream의 부모클래스인 ios에 해당 출력 포맷팅 옵션 기능이 있기 때문이다

이러한 출력 format을 변경할 수 있는 방법에는 flag와 manipulator 두 가지 방법이 존재한다

일반적으로 c++에서 양수에는 +를 출력하지 않지만 std::ios::showpos 플래그를 통해 +도 출력할 수 있다

이때 플래그 지정은 std::cout.setf()로 한다

    std::cout.setf(std::ios::showpos);
    std::cout << 10;
    
    //+10

이러한 플래그들은 bitor |로 여러개를 켤 수 있다

    std::cout.setf(std::ios::showpos | std::ios::uppercase);
    std::cout << 13123123123123.89f;
	
    //+가 붙고 e가 E가 되어 출력된다

플래그를 끄기 위해서는 std::cout.unsetf(플래그)를 해주면 된다

	std::cout.unsetf(std::ios::showpos);
    std::cout << 13123123123123.89f;

이러한 setf()를 사용할때는 주의해야 할 점이 있다

많은 flag들은 format group에 속해 있는데 대표적으로 basefiled라는 format group에는 숫자를 8진수, 10진수, 16진수로 표현해주는 플래그가 포함되어 있다

그렇다면 다음 코드는 어떤 결과를 출력할까?

	std::cout.setf(std::ios::hex);
    std::cout << 10;

hex flag를 켰으니 16진수로 나올 것 같지만 그대로 10진수인 10으로 출력되게 된다, 이는 바로 단순히 플래그만 켜는걸로는 기존의 flag를 끄지 않기 때문이다 (기본적으로 std::cout은 dec 플래그가 켜져있음)

첫 번째 방법은 기존에 켜져있는 dec 플래그를 끄는 방법이다

    std::cout.setf(std::ios::hex);
    std::cout.unsetf(std::ios::dec);
    std::cout << 10;

	//a

두 번째 방법은 두개의 플래그를 지정하는 방법이다

    std::cout.setf(std::ios::hex | std::ios::basefield);
    std::cout << 10;
    
    //a

하지만 이런식으로 setf()와 unsetf()를 사용하는건 번거롭다, 따라서 manipulator를 사용하는것이 일반적이다

    std::cout << std::hex << 10;
    
    //a

부동 소수점에 관련한 manipulator는 다음과 같다

  • std::fixed : 고정 소수점 표기법 (1.234)
  • std::scientific : 과학적 표기법 (1.234e+02)
  • std::showpoint : 소수점이 없어도 강제로 표기
  • std::noshowpoint : showpoint 해제
  • std::setprecision(int) : 부동 소수점의 소수점 정밀도 설정 (iomanip 헤더가 필요함)

이러한 manipulator들은 혼합해서 사용이 가능하다

또한 출력의 너비, 채움문자, 정렬도 가능하다

  • std::left : 왼쪽 정렬
  • std::right : 오른쪽 정렬
  • std::internal : 부호는 왼쪽, 값은 오른쪽 정렬
  • std::setw(int) : 출력 필드 너비를 int로 설정 (iomanip 헤더 필요)
  • std::setfill(char) : setw로 만든 빈칸을 char로 채운다 (iomanip 헤더 필요)
    std::cout << std::setfill('#') << std::setw(10) << 123 << '\n';
    std::cout << std::left << std::setw(10) << 123 << '\n';
	std::cout << std::internal << std::setw(10) << -123 << '\n';
    
    //#######123
    //123#######
    //-######123

이때 setfill()은 한번 설정하면 바꾸기 전까지 유지되지만 setw()는 매번 다시 설정해야 한다

profile
GameDeveloper🎮 Dev C++, DataStructure, Algorithm, UE5, Assembly🛠, Git/Perforce🌏

0개의 댓글