안녕하세요, katte입니다.
오늘은 C++이 입출력을 처리하는 방법에 대해 자세히 알아보도록 하겠습니다.
이전에 다른 글에서 간단하게 언급했던 적이 있는 스트림에 대해 자세히 다뤄보려고 합니다.
C++은 모든 입력과 출력을 바이트의 흐름으로 간주하여 처리합니다.
이때, 바이트의 흐름 그 자체를 스트림(stream)이라고 합니다.
좀 더 직관적으로 이해를 돕자면, 스트림은 어떤 장치(혹은 파일)와 프로그램 사이를 잇는 호스와 같은 존재라고 생각할 수 있을 것 같습니다. 매개체죠.
예를 들어, 입력 스트림의 경우 바이트가 흘러 들어오는, 바이트의 발생지(파일, 키보드 등)와 바이트를 받아들여 정보를 처리할 프로그램 사이를 매개합니다.
반면 출력 스트림은, 프로그램과 바이트를 진열할 최종 목적지(모니터, 파일 등) 사이를 매개합니다.
마치 스트림이 호스이고, 바이트들이 스트림이라는 호스를 타고 흐르는 느낌을 주죠. 스트림이 왜 'stream'이라는 이름을 갖게 되었는지 감이 오지 않나요?
이렇게 스트림을 사용하여 입출력을 다루면, 정보가 어디로부터 유입되는지, 어디로 흘러가는지 신경 쓸 필요가 없게 됩니다. 우리는 그저 스트림을 따라 흐르는 일련의 바이트들만 신경쓰면 되니까요.
또한 스트림이 바이트 단위의 흐름이라는 것은 모든 입출력의 시작과 끝이 문자라는 것을 시사합니다.
즉, 우리가 숫자 34를 입력해도, 입력 스트림은 문자 '3'과 문자 '4'를 1바이트 단위로 가져온다는 것을 의미합니다.
만약 우리가 콘솔창에 다음과 같이 입력했다고 가정하고, 경우의 수를 나눠서 생각해 봅시다.
38.5 19.2
1)
char ch;
std::cin >> ch;
첫번째는 문자형으로 입력을 받는 경우입니다.
이 경우 cin은 입력행의 첫번째 문자인 '3'을 받아온 후, 변환 없이 문자 코드 '3'이 저장됩니다.
2)
int ch;
std::cin >> ch;
다음은 int형으로 받아오는 경우입니다.
이 경우, cin은 숫자가 아닌 첫 문자가 나올 때까지 읽습니다. 첫 문자는 '.'이기 때문에, '3'과 '8'을 가져온 후, 이 두 문자가 수치값 38에 해당한다고 판단하여 38의 바이너리 코드를 저장하게 됩니다.
3)
double ch;
std::cin >> ch;
double형의 경우, 부동소수점 수의 일부가 아닌 첫 문자가 나올 때까지 읽습니다. 이 경우 빈칸이기 때문에, cin은 '3', '8', '.', '5' 네 문자를 가져오고 이 네 문자가 38.5에 해당한다고 판단하여 부동소수점 형식의 바이너리 코드를 저장합니다.
4)
char word[50];
std::cin >> word;
이 경우에 cin은 빈칸이 나올 때까지 읽습니다. 즉 '3', '8', '.', '5' 네 문자를 읽어온 후, 변환 없이 그대로 저장합니다. 그리고 끝에 널 문자를 추가합니다.
5)
char word[50];
std::cin.getline(word, 50);
이 경우 cin은 개행문자가 나올 때까지 읽습니다. 변환은 하지 않고 끝에 널 문자를 추가합니다.
출력에선 정 반대의 변환이 일어납니다.
정수들이 숫자 문자들의 시퀀스로 변환되고, 부동 소수점 수들이 숫자 문자와 기타 문자들의 시퀀스로 변환되며, 문자 데이터는 변환이 일어나지 않습니다.
그러나 결국 결론은 모든 입력의 시작은 문자이며, 모든 출력의 끝 또한 문자라는 것입니다.
우리가 입출력을 위해 스트림을 사용하기 때문이죠.
말이 길었지만, 결론적으로 스트림에 의해 우리가 입력을 다루기 위해 해야 할 일은 이렇게 간략화 됩니다.
1. 하나의 입력 스트림을 프로그램의 입력에 연결한다.
2. 그 입력 스트림을 파일에 연결한다.
파일 쪽의 연결은 파일이 될 수도 있고, 키보드가 될 수도 있고, 혹은 또 다른 프로그램이 될 수도 있습니다.
출력 또한 마찬가지입니다.
1. 하나의 출력 스트림을 프로그램에 연결한다.
2. 그 출력 스트림을 출력 목적지에 연결한다.
그런데 입출력에 있어서 사실 문제가 한가지 더 있습니다.
프로그램은 한번에 1바이트씩 읽어들여 이를 처리합니다. 반면 디스크 드라이브와 같은 장치들은 512바이트 이상의 블록 단위로 정보를 전달할 수 있습니다. 디스크가 한번에 많은 정보를 전송할 수 있음에도 불구하고 프로그램이 디스크로부터 한 문자씩 읽어들이는 것은 상당히 비효율적이죠. 시간도 매우 낭비될 것입니다.
이런 이유에서 사용하는 것이 버퍼(Buffer)입니다.
버퍼는 임시로 메모리를 저장하는 메모리 블록입니다. 일반적으로 512바이트의 크기거나 그 배수의 크기를 가지고 있습니다.
버퍼는 한꺼번에 많은 데이터를 읽어들여 그것을 버퍼에 저장해둡니다. 그리고 버퍼가 꽉 차면 프로그램은 버퍼로부터 한 문자씩 읽어들이고, 버퍼를 깨끗하게 비우게 됩니다. 이를 버퍼 비우기(flushing the buffer)라고 합니다.
그러나 버퍼가 꽉 차기 전에도 개행문자('\n')을 만나면 버퍼가 자동으로 비워집니다. 그 외에도 긴급 입력을 받아들여야 할 때도 버퍼가 비워지지만, 이것에 관한 이야기는 후에 cout에 대한 글에서 이야기하도록 하겠습니다.
이렇게 버퍼를 사용하는 것은 하드 디스크와 같은 곳에 위치한 파일로 출력을 보내거나, 혹은 입력을 읽어올 때 매우 큰 이득을 줄 수 있습니다. 디스크에 접근하는 것은 상대적으로 시간이 오래 걸리지만, 버퍼에 접근하는 것은 훨씬 빠르기 때문입니다.
그러면 C++이 스트림과 버퍼를 다루는 방법에 대해 좀 더 알아보도록 합시다.
C++에서 입출력을 다루기 위한 클래스는 다음과 같습니다.
1) streambuf
버퍼로 사용하는 메모리와 관련된 함수입니다.
버퍼로 사용할 메모리를 제공하고, 버퍼를 채우고, 버퍼의 내용에 접근하고, 버퍼를 비우고, 버퍼 메모리를 관리하는 등의 클래스 멤버함수를 제공합니다.
ios 클래스의 멤버가 streambuf 객체를 가리키고 있습니다.
2) ios_base
스트림의 일반적인 특성을 나타내는 클래스입니다.
어떤 스트림이 2진 스트림인지, 텍스트 스트림인지, 혹은 읽을 수 있게 열려있는지 등의 특성을 관리합니다.
3) ios
ios_base의 유도 클래스입니다. streambuf 객체를 가리키는 포인터를 멤버로 가지고 있습니다.
4) ostream, istream
ios의 유도 클래스입니다. 입출력 멤버함수를 제공합니다.
5) iostream
ostream과 istream의 유도 클래스로, 이 두 클래스의 멤버함수를 상속받습니다.
우리는 이러한 클래스들의 객체를 만들거나, 혹은 클래스 내에 정의된 상수 등을 이용하면서 입출력을 제어할 수 있습니다.
예를 들어, 우리가 지금까지 계속 사용해왔던 cout은 ostream의 객체입니다.
ostream은 위의 그림과 같이 ios_base와 ios의 멤버들을 상속받으므로,
ostream의 객체인 cout을 만듦으로써 하나의 출력 스트림이 열리게 되고 버퍼가 생성되며 스트림이 버퍼에 자동으로 연결되게 됩니다.
cout 객체가 생성되면 cout 객체는 데이터 표시에 사용할 필드 폭, 소수점 아래 자릿수, 정수 표시에 사용할 진법, 출력 흐름을 제어하는 데 사용할 버퍼를 서술하는 streambuf 객체의 주소 등, 출력과 관련된 모든 정보를 담고 있는 데이터 멤버들을 상속 받아 갖게 됩니다.
std::cout << "free";
예를 들어 위와 같은 구문은 "free"라는 문자열을 구성하는 문자 바이트들을 streambuf 객체를 지시하는 과정을 통해 cout이 관리하는 버퍼에 넣습니다. ostream 클래스가 이때의 operator<<를 정의합니다.
또한 C++은 버퍼로부터 나오는 출력이 표준출력으로 향하도록 주선합니다. 즉 출력 스트림의 한쪽 끝은 프로그램, 다른 쪽 끝은 표준출력과 연결되어 있습니다.
이렇게 cout 객체는 streambuf형 객체의 도움을 받아 스트림을 통과하는 바이트들의 흐름을 제어할 수 있습니다.
입출력을 위해 필요한 각각의 클래스의 객체들은 iostream에 선언되어 있습니다.
iostream 파일을 include하면 총 8개의 스트림 객체가 생성되게 되는데, 그 종류는 다음과 같습니다.
표준 스트림 | char (8bit) / wchar_t (16bit) | |
표준 입력 스트림 | cin | 키보드와 같은 표준 입력 장치에 연결 |
wcin | ||
표준 출력 스트림 | cout | 모니터와 같은 표준 출력 장치에 연결 |
wcout | ||
표준 에러 스트림 | cerr | 에러 메시지를 표시하는 데 사용 표준 출력 장치와 연결되지만 버퍼를 사용하지 않음 |
wcerr | ||
clog | 표준 출력 장치와 연결되며 버퍼를 사용 |
|
wclog |
* 해당 글은 'C++ 기초 플러스 6판'을 참고하여 작성되었습니다.
'Computer > C++' 카테고리의 다른 글
[C++] cout 관련(2) - 출력 버퍼 비우기 (0) | 2022.11.06 |
---|---|
[C++] cout 관련(1) - write, put 출력 멤버함수 (0) | 2022.11.06 |
[C++] cin 잘못된 입력 판별하기, 입력 버퍼 비우기 (0) | 2022.10.23 |
[C++] cctype 라이브러리 소개 (0) | 2022.10.23 |
[C++] EOF에 관한 이야기 (0) | 2022.10.22 |