ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • _C++_입출력 스트림
    Programming/_C++ 2023. 9. 26. 16:21

    C++은 C의 super set이기 때문에 C에서 사용하던 로우 레벨 입출력 함수들을 사용할 수 있다.

    그러나 에러 처리가 완전하지 않고 커스텀 데이터 타입을 다룰 수 없다는 한계가 존재한다.

     

    C++은 스트림이라 불리는 유연하고 객체지향적인 I/O 시스템을 제공한다.

    스트림 개념은 데이터를 받거나 생성하는 객체라면 모두 적용할 수 있으며, 대표적으로 콘솔 스트림, 파일 스트림, 문자열 스트림이 있다.

    우리가 자주 사용하는 <iostream>은 인클루드만 해도 메모리 사용량을 증가시키는데, 이는 cin, cout, cerr, clog와 같은 스트림 객체가 자동으로 인스턴스화 되기 때문이다.

    출력 스트림을 이용하는 가장 기본적인 방법은 <<연산자를 이용하는 것이다.

    #include <iostream>
    
    using namespace std;
    
    int main(){
        int i = 7;
        cout << i << endl;
        char ch = 'a';
        string str = "output test";
        cout << ch << '\n' << str << endl; // "\n", endl 모두 줄바꿈
        return 0;
    }

    줄바꿈을 위해 C스타일의 이스케이프 시퀀스, endl 매니퓰레이터를 사용할 수 있다.

    endl은 스트림에 개행문자를 투가하고 출력 버퍼를 밀어내는(flush) 역할도 한다.

    하지만, flush가 너무 잦으면 성능에 큰 영향을 미치므로 잦은 개행이 필요하다면 개행문자 "\n"을 사용하도록 하자.

     

    put과 write 메소드를 이용해서 저수준의 출력도 가능하다.

    void rawWrite(const char* data, int dataSize)
    {
    	cout.write(data, dataSize);
    }
    void rawPutChar(const char* data, int charIdx)
    {
    	cout.put(data[charIdx]);
    }

     

    보충내용 (매니퓰레이터)

    스트림에는 단순한 데이터 뿐만 아니라 명령을 집어넣을 수도 있다.

    이를 스트림 매니퓰레이터라고 한다. 

    아래는 몇 가지 유용한 출력 스트림 매니퓰레이터이며 <iomanip>에 정의되어 있다.

    매니퓰레이터 설명
    boolalpha bool값 출력시 숫자 1, 0이 아닌 true, false로 출력하도록 한다.
    noboolalpha bool값 출력시 true, false를 1, 0으로 출력한다.
    hex 숫자 출력시 16진수 포맷을 사용
    oct 숫자 출력시 8진수 포맷을 사용
    dec 숫자 출력시 10진수 포맷을 사용
    showbase 16진수 출력시 0x, 8진수 출력시 0를 앞에 붙여 출력하도록 해준다.
    fixed 부돈소수점 값 출력시 필요한 소수점 이하 자릿수보다 설정된 precison이 큰 경우라도 주어진 precison까지 출력하도록 한다.
    setw(int) 숫자 출력을 위한 자릿수 크기를 지정한다. 한번의 I/O 작업을 마치면
    setfill(char_type) 숫자 출력을 위해 지정된 자릿수보다 숫자의 값이 작을 때 빈 공간을 채울 문자를 지정한다.(default는 공백 문자)

    operator << 연산을 오버로딩하면 클래스 등과 같이 구조화된 타입에 대해서도 출력이 가능하다.

    struct point{
    	int x, y;	
    };
    ostream& operator<<(ostream& os, const point& p) 
    {
    	return os << '(' << p.x << ',' << p.y << ')';
    }
    int main()
    {
    	point p { 1, 2 };
    	cout << p; // output: (1,2)
    }

    입출력 스트림을 참조 파라미터로 받아서 함수 내에서 해당 스트림으로 I/O작업을 수행한다고 하면 const 한정자를 붙일 수 없다.

    이는 입출력 스트림 객체는 모든 I/O 작업에서 객체 내부의 상태(읽기 위치정보, 상태 플래그 등)를 업데이트 할 수 있기 때문이다.

    상태 플러그들은 입출력 등을 수행할 때 저절로 갱신되며 직접 접근하거나 good, eof, bad, fail등의 메소드를 통해 스트림의 상태를 점검할 수 있게 해준다.

     

    - 각 메소드가 검사하는 상태 표현식

    ios::good() ⇒ !eofbit && !failbit && !badbit
    ios::eof() ⇒ eofbit
    ios::fail() ⇒ failbit || badbit
    ios::operator bool ⇒ !failbit && !badbit (bool로 캐스팅되는 연산)
    ios::bad() ⇒ badbit

     

    입력 스트림은 출력 스트림과 반대로 >>연산자를 통해 입력을 받는다. >>연산자는 피연산자의 타입에 따라 적절하게 입력 값을 가공하여 넣어준다. 아래는 각 타입에 따라 값이 들어가는 것을 볼 수 있다.

    char charInput;    // 문자 입력
    int intInput;      // 숫자 입력(정수)
    string strInput;   // 문장 입력
    double doubleInput;// 숫자 입력(실수)
    
    cin >> charInput >> intInput >> strInput >> doubleInput; // userInput: a 123 hello 3.14
    //charInput = 'a', intInput = 123, strInput = "hello", double = 3.14

    만약 공백을 포함하는 문자열을 한번에 받고 싶다면 get, getline 메소드를 사용하거나 skipws 매니퓰레이터를 사용하면 된다.

    {
        string s;
        cin >> s;  // input "hellow cpp"
        cout << s; // output "hellow"
    }
    {
        string s;
        cin >> s;  // input "hellow cpp"
        cout << s; // output "hellow cpp"
    }

    get 메소드는 스트림으로부터 1byte 크기의 raw 데이터를 얻을 수 있게 해준다.

    string readName(istream& is)
    {
    	string name;
    	while (is.good()) {
    		int next = is.get(); //EOF와 같은 특수한 값을 리턴할 수도 있기 때문에 char가 아닌 int 반환
    		if (next == EOF) 
    			break;
    		name += next;//char로 묵시적 캐스팅 후 append됨
    	}
    	return name;
    }

    get메소드는 char&를 파라미터로 받는 버전이 있다. 해당 메소드는 입력 스트림 객체를 참조자로 리턴해서 코드가 간결해질 수 있다.

    string readName(istream& is)
    {
    	string name;
    	char next;
    	while (is.get(next)) //get 메서드가 평가된 후 is객체가 다시 리턴되고 bool로 묵시적 캐스팅 됨
    		name += next;
    	return name;
    }

     

    보충내용(peek)

    입력 스트림 연산의 자동 가공 기능을 활용하고 싶지만, 입력 스트림 버퍼에 있는 것을 어떤 타입으로 받아야할지 미리 알 수 없는 경우도 있다. 이때 peek메소드를 사용하여 스트림 버퍼에서 빼내지 않고 미리 데이터를 확인할 수 있다.

    string s;
    int i, test;
    while (true) {
    	test = cin.peek();
    	if (!cin.good()) 
    		break;
    	if (isdigit(test))
    		cin >> i;
    	else
    		cin >> s;
    }

    위으 코드를 살펴보면 수, 문자열 입력이 미리 알 수 없는 순서로 발생하는 시나리오이다.

    peek 메소드를 통해 구분하여 입력을 받는걸 볼 수 있다.

     

    공백이 아닌 개행문자만 구분하여 문자열을 입력받고 싶은 경우 getline메소드나 함수를 이용하면 편리하다.

    char buffer[kBufferSize];
    cin.genline(buffer, kBufferSize); //메서드 버전
    
    string myString;
    std::getline(cin, myString); //전역 함수 버전
    std::geline(cin, myString, '~'); //개행문자 대신 ~를 만날 때까지 읽음

    스트림에서 개행문자는 스트림에서 빼내어지지만, 입력으로 취급하지 않고 버려진다.

    getline의 세 번째 파라미터를 통해 delimiter를 변경해도 문자는 버려진다.

    출력 스트림과 마찬가지로 스트림 연산자 오버로딩을 통해 클래스 등의 구조화된 데이터 타입에 대해서도 입력 스트림 연산이 가능하다.

    struct point{
    	int x, y;	
    };
    istream& operator>>(istream& is, point& p) 
    {
    	return is >> p.x << p.y;
    }
    int main()
    {
    	point p;
    	cin >> p;
    }

     

    보충 내용 (입력스트림용 매니퓰레이터)

    입력 스트림도 매니퓰레이터를 통해 데이터를 읽는 방식을 바꿀 수 있다.

    출력 스트림에서 언급된 boolalpha, noboolalpha, hex, oct, dec등을 사용할 수 있다.

    이 외에도 몇 가지 유용한 매니퓰레이터가 있다.

    매니퓰레이터 설명
    skipws 입력 문자열을 토큰화할 때 공백을 생략한다.
    noskipws 입력 문자열을 토큰화할 때 공백을 포함한다.
    ws 현재 위치로부터 연이은 공백 문자들을 생략한다.
    #include <iostream>
    #include <sstream>
    
    using namespace std;
    
    int main()
    {
    	char c1, c2, c3;
    	istringstream("a b c") >> c1 >> c2 >> c3;
    	cout << "Default  behavior: c1 = " << c1 << " c2 = " << c2 << " c3 = " << c3 << '\n';
    	//Default behavior: c1 = a c2 = b c3 = c
    	//공백이 생략됨을 볼 수 있다.
    	
    	istringstream("a b c") >> std::noskipws >> c1 >> c2 >> c3;
    	cout << "noskipws behavior: c1 = " << c1 << " c2 = " << c2 << " c3 = " << c3 << '\n';
    	//noskipws behavior: c1 = a c2 =   c3 = b
    	//공백이 포함됨을 볼 수 있다.
    }

    <sstream>을 사용하여 skipws, noskipws의 동작을 보여준 코드이다.

    stringstream은 istringstream, ostringstream, stringstream이 있고, 모두 <sstream>헤더에 정의되어 있다.

     

    ostringstream과 stringstream 객체에서 str() 메서드를 호출하면 string 객체를 얻을 수 있다.

    istringstream이나 stringstream 객체에서 str(const string& s) 메서드를 호출하면 스트림 객체에 string을 세팅할 수 있다.

    //sstream으로 string 객체를 얻기
    
    #include <string>
    #include <iostream>
    #include <sstream>
    
    using namespace std;
    
    int main () {
    	stringstream ss;
    	ss.str ("Example string");
    	string s = ss.str();
    	cout << s << '\n';
    }

    file stream은 <fstream>헤더에 정의되어 있으며 ofstream, ifstream, fstream이 있다.

    ofstream은 생성자에서 파일명, 열기 모드를 파일로 받는다.

    열기 모드는 다음과 같은 상수들로 정의되어 있고, default는 출력 모드이다.

    모드 상수값 설명
    ios_base::app 파일을 연 다음 쓰기 작업이 시작되기 전 스트림의 위치를 파일의 제일 끝으로 옮김.
    ios_base::ate 파일을 열자마자 스트림의 위치를 파일의 제일 끝으로 옮김.
    ios_base::binary 바이너리 모드로 데이터 입출력을 한다.
    ios_base::in 입력모드로 파일을 열기.
    ios_base::out 출력모드로 파일을 열기.
    ios_base::trunc 파일을 열면서 기존의 데이터를 모두 삭제.

     

    파일 입출력이 불필요해지면 close메소드를 호출하여 파일 핸들을 놓아줄 수 있다.

    하지만, 보통 스코프에 의해 스트림 객체가 소멸하고 소멸자에 의해 자동으로 릴리즈 되도록 한다.

     

    보충 내용 (스트림에서의 커서 조작)

    • tell, seek 메소드를 통해 스트림 위치(커서)에 접근하거나 위치를 옮길 수 있다.
    • tellg는 get할 위치를 리턴, tellp는 put할 위치를 리턴해준다. (리턴 타입: ios_base::streampos, 이는 스트림 시작 위치로부터의 오프셋이자 절대 위치)
    • seek메소드는 스트림의 위치를 옮겨준다. tell과 마찬가지로 seekg, seekp가 있으며 각각 입력 및 출력 위치를 옮겨준다.
    • seek메소드는 파라미터가 1개, 2개인 버전이 있다.
      • 1개인 버전: streampos를 인자로 받음
      • 2개인 버전: streamoffset과 seekdir를 차례로 인자로 받음.
    // 1개인 버전
    inStream.seekg(0); //시작 위치
    // 2개인 버전
    outStream.seekp(2, ios_base::beg);
    inStream.seekg(-3, ios_base::end);

    seekdir은 오프셋의 기준이 되는 위치에 대한 심볼이다.

    • beg: 스트림의 시작 위치
    • end: 마지막 위치
    • cur: 현재 위치

    파일의 크기를 재기 위해 다음과 같이 활용할 수 있다.

    ifstream ifs("targetFile", ios::binary);
    	if (ifs)
    	{
    		ifs.seekg(0, ios_base::end);
    		auto curPos = ifs.tellg();
    		ifs.seekg(0, ios_base::beg);
    		cout << curPos << " bytes"; // 현재 pos가 몇 byte인지 확인
    	}

     

    입력 스트림과 출력 스트림은 tie메서드를 통해 서로 연관시킬 수 있다.

    이렇게 연관된 스트림 쌍이 있으면 입력 스트림에서 데이터를 읽을 때 출력 스트림을 먼저 flush 한다.

    기본적으로 cincout은 서로 연관되어 있다.

    하나의 콘솔을 통해 입력과 출력을 번갈아 하더라도 문제가 생기지 않는다는 장점이 있다.

    하지만, flush의 빈도가 많으면 성능이 불리해진다. 만약 ux 측면에서 연관될 필요가 없다면 tie를 푸는게 성능상 유리하다!!

     

    cincoutC언어에서의 낮은 수준의 입출력 함수들과 버퍼를 동기화 한다.

    다중 스레드 환경에서 사용될 경우 레이스 컨디션을 막기 위해 적절한 락킹 메커니즘을 통해 동기화한다.

    하지만 이러한 동기화는 비용이 상당히 크므로 필요한 상황이 아니면 사용하지 않는게 좋다.

     

    실제로 baekjoon 코테 문제를 풀어보면 아래와 같이 동기화를 푸는 경우가 많다.

    ios::sync_with_stdio(false); //콘솔 입출력에 대해서 스레드, 버퍼 동기화 해제
    cin.tie(nullptr); //cin과 cout의 연결 해제
    cout.tie(nullptr); //cout과 cin의 연결 해제
    
    //NULL이 아닌 nullptr을 사용하는 이유?
    #define NULL 0 // NULL은 C언어에서 사용되었고 C++로 계승이 되었다.
    
    func(int a);
    func(int* b);
    func(NULL); //NULL로 사용하면 0으로 인식할 수도 있다.
    
    func(int a);
    func(int* b);
    func(0);

    NULL과 nullptr은 컴파일러에서 다르게 인식하고, NULL은 값으로 치면 0이기 때문에 의미가 모호해지고 가독성이 좋지 않다.

     

    ios::sync_with_studio, cin.tie, cout.tie

    - 위의 세가지를 사용하기 위해서는 C와 C++의 입출력을 혼용하지 않으면서, thread를 사용할 때 주의를 해야 합니다. 1. ios::sync_with_studio(bool_sync); 위의 구문은 C++ 표준 스트림들이 C표준 스트림들과

    mokchanic.tistory.com

     

    'Programming > _C++' 카테고리의 다른 글

    _C++_알고리즘_Part_3  (0) 2023.09.26
    _C++_반복자  (0) 2023.09.25
    _C++_연관 컨테이너_비순차 연관 컨테이너  (1) 2023.09.25
    _C++_알고리즘_Part_2  (0) 2023.09.25
    _C++_Template  (1) 2023.09.24
Designed by Tistory.