reference: "명품 C++ 프로그래밍" / 황기태 / 생능출판
컴퓨터 기술에서 스트림은 연속적인 데이터의 흐름 혹은 데이터를 전송하는 소프트웨어 모듈을 일컫는다.
C++는 스트림(stream)이라는 정교한 입출력 매커니즘을 제공한다. 스트림은 입력 스트림과 출력 스트림으로 나뉜다.
입력 스트림
: 키보드, 네트워크, 파일 등 입력 장치로부터 입력된 데이터를 순서대로 프로그램에 전달하는 객체이다.
출력 스트림
: 프로그램에서 출력한 데이터를 프린터나 하드 디스크, 스크린, 네트워크, 파일 등과 같은 목적 장치로 순서대로 내보내는 객체이다.
C++ 응용 프로그램에서는 출력 장치에 직접 출력하는 대신 출력 스트림에 출력하고, 입력 장치로부터 직접 입력받는 대신 입력 스트림으로부터 입력을 받는다.
C++의 표준 입력 스트림 객체는 cin이며, 표준 출력 스트림 객체는 cout이다.
C++ 입출력 스트림의 중요한 특징은 스트림이 버퍼를 가진다는 점이다.
키보드에 연결된 cin 입력 스트림이 존재한다. 사용자가 입력한 'H', 'e', 'l', 'l', 'u' 키를 순서대로 저장하고 프로그램에게는 전달하지 않는다. 아직 사용자의 키 입력이 끝났다고 볼 수 없기 때문이다.
source: "명품 C++ 프로그래밍" / 황기태 / 생능출판
사용자가 <Backspace>
키를 입력하면 가장 최근에 입력된 문자인 'u'를 버퍼에서 지울 수 있다. <Backspace>
키는 버퍼에 저장되는 대신 버퍼를 제어하는 제어키의 역할을 한다. 그리고 <Enter>
키를 입력하면 비로소 'LOVE' 문자들은 입력을 기다리고 있는 C++ 프로그램에게 전달된다.
스크린에 연결된 cout 출력 스트림이 존재한다. 출력 스트림은 보통 '\n'이 도착하거나 버퍼가 꽉 찰 때 스크린에 출력시킨다.
source: "명품 C++ 프로그래밍" / 황기태 / 생능출판
하지만 cout.flush() 명령을 내리면 출력 스트림은 버퍼에 있는 내용을 모두 장치에 출력한다.
C++ 입출력 스트림은 운영체제 API(시스템 콜)를 호출하여 입출력 장치와 프로그램 사이에서 데이터를 전송한다.
파일 출력 스트림에서 만일 버퍼가 없다면, 프로그램이 몇 바이트씩 파일 쓰기를 실행할 때마다 시스템 콜을 호출하고, 그때마다 하드 디스크나 네트워크 장치가 자주 작동하게 되어 시스템의 효율이 나빠진다(비록 운영체제 또는 HW적으로 버퍼 기능이 있을 지라도 분명 오버헤드는 있다. 예를 들어 커널과의 문맥 교환이 있겠다).
버퍼가 있다면, 쓰기가 이루어진 데이터를 스트림 버퍼에 모아두었다가, 한 번에 운영체제에 요청하면 시스템 효율이 올라가게 된다.
또한 키 입력 스트림의 경우 입력된 키를 일단 버퍼에 저장하고, 프로그램에 전달하기 전 <Backspace>
와 같은 제어키를 통해 입력된 키들을 수정할 수 있다는 장점도 있다.
C++ 표준에서는 오직 스트림 입출력만을 지원한다. 따라서 게임과 특별한 응용 프로그램 제작을 위해, 대부분의 C++ 컴파일러 회사들이 저마다 독특한 저수준 입출력 라이브러리를 제공한다. 어쨌든 C++ 표준에서는 오직 스트림을 통한 입출력만 지원한다.
과거 입출력 시스템은 영어와 같이 문자 하나를 한 바이트로 표현하는 언어의 문장만 입출력하도록 작성되었다. 문자 하나가 2바이트로 구성되는 한글 문자를 입력할 수 없었다.
즉 과거의 입출력 클래스들은 아래와 같으며 모두 문자를 한 바이트로만 다루는 클래스들이다.
=> ios, istream, ostream, ifstream, ofstream, fstream
현재의 표준 C++ 입출력 라이브러리는 한 문자를 여러 바이트로 표현하는 다국어의 입출력을 위해 템플릿(template)을 사용하여 C++ 입출력 라이브러리를 일반화시켰다.
=> basic_ios, basic_istream, basic_ostream, basic_ifstream, basic_ofstream, basic_fstream
새 표준을 사용하는 C++ 개발자들은 이들 템플릿 클래스에 구체적인 타입을 대입하여 입출력 클래스를 구체화하여 사용해야 한다.
표준을 변했다 하더라도 지금도 여전히 cin으로는 한글을 문자 단위로 읽을 수 없다.
source: "명품 C++ 프로그래밍" / 황기태 / 생능출판
<iostream>
헤더 파일을 인클루드한 C++ 프로그램이 실행되기 시작하면, cin, cout, cerr 등 표준 입출력 객체가 생성되며, 바로 이들을 사용할 수 있다.
cin
: 키보드 장치와 연결된 istream 타입의 표준 입력 스트림 객체
cout
: 스크린 장치와 연결된 ostream 타입의 표준 출력 스트림 객체
cerr과 clog
: cerr와 clog 객체는 둘다 표준 오류 출력 스트림 객체, clog는 버퍼를 거치지만 cerr는 버퍼를 거치지 않고 스크린에 오류 메시지 출력
ifstream 클래스에서 비트 시프트 연산을 하는 >>
연산자를 오버로딩하였다. >>
연산자는 스트림 추출 연산자로 불리우며 왼쪽 피연산자인 스트림 객체로부터 데이터를 읽어 오른쪽 피연산자에 지정된 변수에 삽입한다.
사용자 입력이 일차적으로 cin의 스트림 버퍼에 저장되며, <Enter>
키가 입력되면 비로소 cin 입력 버퍼에서 키 값을 끌어내어 변수에 저장한다.
<<
연산자는 출력 스트림에 데이터를 출력하는 연산자이고, 스트림 삽입 연산자라고 부른다. ostream 클래스에서 비트 단위로 시프트하는 <<
연산자를 오버로딩하여 작성하였다. 정수, 문자, 문자열 등 기본 타입 또는 문자열에 대해서만 오버로딩되어 있기에 사용자 정의 클래스의 객체를 이 삽입 연산자의 피연산자로 두려면 해당 클래스에서 연산자 오버로딩을 하여야 한다.
cout << 'a' << 123 << endl;
위 문장은 객체, 입출력 스트림, 연산자 중복, 참조 매개변수, 참조 리턴이 절묘하게 연결되어 있는 문장이다.
ostream 클래스 내에 중복 작성된 모든 <<
연산자들은 다음과 같이 ostream&
을 리턴한다.
ostream& operator << (char c);
<< 연산자는 출력 스트림에 데이터를 삽입한 후, 출력 스트림(*this)
를 리턴한다. 즉 스트림의 참조가 리턴된다.
함수 내에서 선언된 지역변수가 아닌 객체 자기 자신을 반환하는 것이다. 고로 참조로 반환이 가능하다.
cout << 'a' << 123;
먼저 cout << 'a'
에서 <<
연산자를 호출한다. 컴파일러는 아래와 같이 변형하여 컴파일한다.
cout.<<('a')
이 코드는 cout 객체 내의 연산자 함수(operator<<(char c)
)하고 'a'를 매개변수에 넘겨준다.
ostream& operator<<(char c) {
// .. 현재 스트림 버퍼에 c('a')를 삽입한다.
// .. 버퍼가 차면 장치에 출력한다.
return (*this);
}
실행 결과 cout의 버퍼에 'a'가 삽입되고, cout에 대한 참조가 리턴되었다. 그러므로 << 123
은 `cout << 123'을 실행하는 것과 같고, 'a'가 들어 있는 cout의 버퍼에 123을 출력하는 것이다.
ostream& operator<<(char c) {
// .. 현재 스트림 버퍼에 n(123)를 삽입한다.
// .. 버퍼가 차면 장치에 출력한다.
return (*this);
}
결국 cout의 버퍼에는 "a123"으로 변경되며, 적절한 시점에 화면에 출력된다.
만약
>>
연산자 재정의 함수의 리턴 타임이 참조가 아닌 그냥 ostream이라면 어떻게 될까?
스트림의 복사본(*this)이 리턴되고, 그 다음에 실행되는<<
연산은 복사된 스트림에 출력하게 되어, 연속되는 두<<
연산이 서로 다른 출력 스트림에 출력하게 되므로 예상대로의 출력이 이루어지지 않을 수있다.