[C++ 프로그래밍 / 이론] Chapter9. 입출력 스트림

YH·2023년 7월 31일
0

Chatper9. 입출력 스트림

C++은 C의 super set이기 때문에 C에서 사용하던 로우 레벨 입출력 함수들을 사용할 수 있다. 그러나 에러 처리가 완전하지 않고 커스텀 타입을 다룰 수 없다는 한계가 분명하다.
C++는 스트림이라 불리는 유연하고 객체지향적인 I/O 시스템을 제공한다. 스트림 개념은 데이터를 받거나 생성하는 객체라면 모두 적용할 수 있다. 대표적으로 콘솔 스트림, 파일 스트림, 문자열 스트림이 있다.
출력 스트림은 헤더에 입력 스트림은 헤더에 정의되어 있는데 두 헤더를 포함하고 있어 보통은 헤더로 인클루드한다. iostream은 인클루드만 해도 메모리 사용량을 증가시키는데, 이는 cin, cout, cerr, clog와 같은 스트림 객체가 자동으로 인스턴스화 되기 때문이다.
출력 스트림을 이용하는 가장 기본적인 방법은 <<연산자를 이용하는 것이다. 출력 스트림 연산이라고도 부르는데 이 연산은 피연산자의 타입에 따라 적절한 포맷으로 출력을 해준다. 이 연산자는 스트림 객체를 참조로서 리턴하기 때문에 연이어 나열함으로써 여러 데이터를 출력할 수도 있다.

int i = 7;
cout << i << endl;
char ch = 'a';
string str = "output test";
cout << ch << '\n' << str << endl;

줄바꿈을 위해서 C 스타일의 이스케이프 시퀀스를 이용한 개행문자를 출력할수도 있고 endl 매니퓰레이터를 사용할 수도 있다. endl은 스트림에 개행문자를 추가하고 출력 버퍼를 밀어내는(flush) 역할도 한다. 너무 잦은 flush는 성능에 큰 영향을 주므로 잦은 개행이 필요한 경우 개행문자를 출력하여 개행하는 것이 좋다.
put, write 메서드를 이용하면 가공을 하지 않는 저수준의 출력도 가능한다.

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

  • 매니퓰레이터

스트림에는 단순히 데이터 뿐만 아니라 명령을 집어넣을 수도 있다. 이를 스트림 매니퓰레이터라고 한다. 앞서 사용하였던 endl도 줄바꿈과 버퍼 비워내기 동작을 트리거하는 스트림 매니퓰레이터이다. 이들은 iomanop 헤더에 정의되어 있으며 다음은 몇가지 유용한 출력 스트림 매니퓰레이터에 대한 소개이다.

매니퓰레이터설명
boolalphabool 값 출력시 true는 "true", false는 "false"로 출력하도록 한다.
noboolalphabool 값 출력시 true는 1, false는 0으로 출력하도록 한다.
hex숫자 출력시 16진수 포맷을 사용하도록 한다.
oct숫자 출력시 8진수 포맷을 사용하도록 한다.
dec숫자 출력시 10진수 포맷을 사용하도록 한다.
showbase16진수 출력시 0x, 8진수 출력시 0을 앞에 붙여 출력하도록 해준다.
setprecision(int)부동소수점 값 출력시 정수부와 소수부를 합친 자릿수를 지정한다. 자리수에 맞춰 반올림이 발생한다.
fixed부동소수점 값 출력시 필요한 소수점 이하 자릿수보다 설정된 precision이 큰 경우라도 주어진 precision까지 출력하도록 한다.
setw(int)숫자 출력을 위한 자릿수 크기를 지정한다. 한번의 I/O 작업을 마치면 원래의 너비로 자동 복구된다.
setfill(char_type)숫자 출력을 위해 지정된 자릿수보다 숫자의 값이 작을 떄 빈 공간을 채울 문자를 지정한다. 기본 값은 공백 문자이다.

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과 같은 문자에 주로 사용되는 타입이라면 입력데이터를 문자로 취급하여 가공하며, int나 double이라면 입력데이터를 수로 취급하여 가공한다.

char charInput;
int intInput;
string strInput;
cin >> charInput >> intInput >> strInput; //userInput: a 123 hello
//charInput = 'a', intInput = 123, strInput = "hello"

연산자는 디폴트로 공백 및 개행을 구분자로 하여 입력값들을 토큰화한다. 공백을 포함하는 문자열을 한번에 받고 싶다면 get, getline 메서드 또는 함수를 이용하거나 skipws 매니퓰레이터를 활용해야 한다.
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 메서드를 통해 스트림의 버퍼에서 빼내지 않으면서 미리 데이터를 확인할 수 있다.
예를 들어 다음의 코드는 수 입력 또는 문자열 입력이 미리 알 수 없는 순서로 발생하는 시나리오에서 peek 메서드를 통해 구분하여 입력받는다.

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

공백이 아니라 개행문자만으로 구분하여 문자열을 입력받고 싶은 경우가 많다. 이럴 때는 getline 메서드나 함수를 이용하면 편리하다.

char buffer[kBufferSize];
cin.getline(buffer, kBufferSize); //메서드 버전

string myString;
std::getline(cin, myString); //전역 함수 버전
std::getline(cin, myString, '~'); //개행문자 대신 ~를 만날 때까지 읽음

이때 유의할점은 스트림에서 개행문자는 스트림에서 빼내어지지만 입력으로 취급하지 않고 버려진다는 것 입니다. optional한 세 번째 파라미터를 통해 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>

int main()
{
	char c1, c2, c3;
    std::istringstream("a b c") >> c1 >> c2 >> c3;
    std::cout << "Default behavior: c1 = " << c1 << " c2 = " << c2 << " c3 = " << c3 << '\n';
    //Default behavior: c1 = a c2 = b c3 = c
    
    std::istringstream("a b c") >> std::noskipws >> c1 >> c2 >> c3;
    std::cout << "noskipws behavior: c1 = " << c1 << " c2 = " << c2 << " c3 = " << c3 << '\n';
    //noskipws behavior: c1 = a c2 =  c3 = b
}

위 코드는 skipws, noskipws의 동작을 보여주면서, 문자열 스트림에 대한 예시도 함께 보여준다. 앞서 설명했던 내용들은 콘솔 뿐만 아니라 문자열이나 파일을 대상으로 해도 통용되는 내용들이다.
stringstream는 istringstream, ostringstream, stringstream이 있으며 sstream 헤더에 정의되어 있다. ostringstream이나 stringstream 객체에서 str() 메서드를 호출하면 string 객체를 얻을 수 있고, istringstream이나 stringstream 객체에서 str(const string& s) 메서드를 호출하면 스트림 객체에 string을 세팅할 수 있다.

#include <string>
#include <iostream>
#include <sstream>

int main() {
	std::stringstream ss;
    ss.str ("Example string");
    std::string s = ss.str();
    std::cout << s << '\n';
}

파일 스트림은 fstream 헤더에 정의되어 있으며 ofstream, ifstream, fstream이 있다.
ofsteram은 생성자에서 파일명과 열기 모드를 파라미터로 받는다. 열기 모드는 다음과 같은 상수들로 정의되어 있고 디폴트 모드는 출력 모드이다.

모드 상숫값설명
ios_base::app파일을 연 다음 쓰기 작업이 시작되기 전 스트림의 위치를 파일의 제일 끝으로 옮긴다.
ios_base::ate파일을 열자마자 스트림의 위치를 파일의 제일 끝으로 옮긴다.
ios_base::binary바이너리 모드로 데이터 입출력을 한다.
ios_base::in입력 모드로 파일을 연다.
ios_base::out출력 모드로 파일을 연다.
ios_base::trunc파일을 열면서 기존의 데이터를 모두 삭제한다.
int main(int argc, char** argv)
{
	ofstream outFile("test");
    if (!outFile.good()) {
    	cerr << "Error while opening output file" << endl;
        return -1;
    }
    outFile << "There were " << argc << " arguments to this program." << endl;
    outFile << "They are: " << endl;
    for (int i = 0; i < argc; ++i)
    	outFile << argv[i] << endl;
}

파일 입출력이 더이상 불필요하면 close 메서드를 호출하여 파일 핸들을 놓아줄 수 있는데 보통은 스코프에 의해 스트림 객체가 소멸하면서 소멸자에 의해 자동으로 릴리즈 되도록 한다.


  • 스트림에서의 커서 조작

tell 메서드와 seek 메서드를 통해 스트림 위치(커서)에 접근하거나 위치를 옮길 수 있다. 모든 입출력 스트림에 정의되어 있지만 보통 파일 입출력에서만 사용된다.
tellg는 get할 위치를 리턴해주고, tellp는 put할 위치를 리턴해준다. 리턴 타입은 ios_base::streampos이다. 이는 스트림 시작 위치로부터의 오프셋이자 절대 위치이다.
seek 메서드는 스트림의 위치를 옮겨준다. tell과 마찬가리로 seekg와 seekp가 있으며 각각 입력 위치, 출력 위치를 옮겨준다. seek 메서드는 파라미터가 한 개인 버전과 두 개인 버전이 있다. 한 개인 버전은 streampos를 인자로 받는다. 두 개인 버전은 streamoffset과 seekdir를 차례로 인자로 받는다.

inStream.seekg(0); //시작 위치
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";
}

입력 스트림과 출력 스트림은 tie 메서드를 통해 서로 연관시킬 수 있다. 이렇게 연관된 스트림 쌍이 있으면 입력 스트림에서 데이터를 읽을 때 출력 스트림을 먼저 flush한다. 기본적으로 cin과 cout은 서로 연관되어 있으며, 덕분에 하나의 콘솔을 통해 입력과 출력을 번갈아서 하더라도 문제가 생기지 않는다. 그러나 이렇게 ux 측면에서 연관되어 있어야 할 필요가 없다면 tie를 풀어주는게 flush의 빈도를 낮춰주어 성능상 유리하다.
그 외에 cin과 cout은 C에서의 낮은 수준의 입출력 함수들과 버퍼를 동기화한다. 거기에 다중 스레드 환경에서 사용될 경우 레이스 컨디션을 막기 위해 적절한 락킹 매커니즘을 통해 동기화한다. 그러나 이런 동기화는 비용이 상당히 크기 때문에 필요한 상황이 아니라면 해제해주는 것이 좋다.

ios::sync_with_stdio(false); //콘솔 입출력에 대해서 스레드, 버퍼 동기화 해제
cin.tie(nullptr); //cin과 cout의 연결 해제
profile
Keep Recycling Your Dreams

0개의 댓글