C++ Principle 10편
10편
입력과 출력
스트림이란
스트림이라고 하면 제트 스트림란 단어가 먼저 떠오른다. 꽤나 잘 써지던 가성비 좋은 펜이어서 종종 사용했었던 기억이 난다. 스트림은 흐름이라는 말이다. 우리가 키보드를 통해 컴퓨터에 입력하고 컴퓨터에서 화면에 글을 출력해주는 과정이 모두 스트림이다.
물이 흐르는 수도관처럼 입력과 출력 스트림에는 규격이 있고 일련의 규칙이 있다. 수도관을 흐르는 물처럼 입 출력 통로에는 바이트 스트림이 흐른다.
스트림의 흐름은 다음과 같다.
- 입력 장치 : 키보드
- 장치 드라이버 : 키보드에서 입력하는 값을 처리해서 입력 라이브러리에 넘겨준다.
- 입력 라이브러리 : 우리 C++에서는 istream
- 우리 프로그램 : C++
- 출력 라이브러리 : 우리 C++에서는 ostream
- 장치 드라이버 : 출력라이브러리에서 스트림을 받아 출력장치로 넘겨준다.
- 출력장치 : 화면 ~
C++의 스트림
C++ 표준 라이브러리에서는 입력 스트림을 처리하는 istream 타입과 출력 스트림을 다루는 ostream 타입을 제공하는데, 지금까지 cin이라는 표준 istream과 cout이라는 표준 ostream을 사용했으므로 표준 입출력 라이브러리(iostrea)의 기본적 사용법을 알고 있는 셈이다.
- C++의 스트림은 문자열을 핵심으로 기억해야 한다.
ostream
- C++의 다양한 타입의 값을 문자열로 변환한다.
- 그 문자열을 어딘가(콘솔, 파일, 메인 메모리등) 으로 보낸다.
- ‘a’, 1, “asdf” 등등이 다 문자열로 바뀐다.
- 출력 라이브러리는 버퍼를 통해서 전달하는데 데이터를 쓴 시점과 실제 목적지에 그 문자열이 나타나는 시점에 지연이 있다면 문자열이 아직 버퍼에 있을 확률이 높다.
istream
- 문자열을 여러 가지 타입의 값으로 변환한다.
- 어딘가에서 그 문자열을 읽어 온다.
- 문자열을 int, char, string 등으로 바꾼다.
파일
- 파일은 형식을 가진다. 이 형식 규칙을 바탕으로 각 바이트의 의미를 정의한다. 예를 들어 텍스트 파일에서 첫 4바이트는 그대로 첫 네문자를 의미한다. 반면 정수 이진 표현을 사용한 파일에서는 첫 4바이트는 첫 정수를 의미한다.
- 즉 파일의 형식은 메인 메모리(c++이 다루는 영역)의 객체와 비슷한 역할을 한다.
ostream은 파일을 다룰 때 메인 메모리의 객체를 바이트 스트림으로 바꾸어 디스크에 쓴다.
스트림 처리
파일을 열 때 실패할 경우가 있는데 이를 모르고 계속 작업하게 되는 경우가 많다.
- 스트림을 연 후에는 반드시 성공 여부를 확인하자
ifstream ist {filename};
if(!ist) error("입력 파일을 열 수 없음 : " + filename);
스트림 오류 처리
- 입력을 다룰 때는 반드시 오류를 염두에 두고 적절히 처리해야 한다.
- 오류의 종류는 무심코 잘못 누른 키보드, 파일 형식이 잘못 되거나 프로그래머가 형식을 잘 못 이해한 경우 등등 다양한 원인이 있지만 오류는 4가지로만 분류되어 있다.
스트림 | 상태 |
---|---|
good() | 연산 성공함 |
eof() | 입력의 끝(파일의 끝(end of file))에 다다름 |
fail() | 예상치 못한 일이 발생함( 숫자를 기대했는데 ‘x’를 읽음 |
bad() | 예상치 못한 심각한 일이 발생함 (디스크 읽기 오류) |
- 이 4가지 상태를 잘 알고있어야 한다.
- fail()의 경우에 eof()도 포함하는 큰 범위이기 때문에 유의해야 한다.
- bad()는 프로그래머가 해결하기 어려운 하드웨어 오류인 경우이기 때문에 에러를 발생시키는 방식으로 해결한다.
- fail()의 경우 unget() 메소드를 사용해서 읽어온 객체를 다시 반납하고, clear() 메소드를 사용해서 다시 good() 상태로 바꾸는 방법이 있다.
- failbit을 사용하는 경우를 알아두면 유용하다.
입출력 연산자 오버로딩하기.
입출력 관련해서 2가지 오버로딩이 가능하다. 낯선 방식일 수도 있어서 정리해 놓는다.
출력 연산자 «
c++에서는 특이하게 «라는 출력 연산자가 있다. 처음에는 어색했는데 이제 익숙해졌다. 우리는 이제 « 연산자는 항상 cout이라는 출력스트림과 함께 움직이는 것을 알고있다.
// 년, 월, 일 날짜를 가지고 있는 Date 클래스에 출력을 정의하려고 한다.
ostream& operator<<(ostream& os, Date& d)
{
return os << d.year <<"년" << d.month << "월" << d.day << "일" ;
}
- ostream객체 os(내가 정한 임의 변수라는 건 기본)는 cout을 의미한다.
- +, -, *, /, «와 같은 연산자들을 오버로딩하기 위해서 메소드이름으로 operator라는 값을 같이 붙여서 써주어야 한다. 약속임.
- operator«는
1 + 2
와 같이 연산자가 가운데에 들어간다.cout << d
을 해석하면operator<<(cout, d)
와 같다는 사실이 중요한데 잊기 쉽다. - operator«가 ostream&을 레퍼런스로 반환하는 것을 볼 수 있다. 즉 값을 변경해 ostream에 담은 뒤에 다시 리턴하기 때문에 cout이 계속 갱신되어 담긴다. 따라서
cout << d1 << d2
과같이 연속적인 구조가 가능하다. cout << d1 << d2 ---> cout << d2
그 외 공부하다가
const &는 왜 쓰는 걸까.
문득 책에서 const string&라는 함수 인자에 쓰인 코드를 발견하고 궁금했다. &는 주소값을 넘겨줘서 주소값에 담긴 값을 변경해주는 방식이 필요할 때 쓴다고 생각하고 있어서 그랬나 보다.
- 스택오버 플로를 찾아보니, &는 원 값을 변경할 때뿐만 아니라, 새롭게 변수를 만들어 메모리를 할당하고 싶지 않을 때 쓴다고 한다.
- 알고 있었는데, 벡터처럼 메모리를 많이 잡아먹는 것만 그렇게 하면 되겠지 했는데, 사실 데이터 타입과 상관없이 데이터를 복제하고 싶지 않을 때 쓰면 된다.
vector hour{24, 1} vs vector hour(24,1)
- 정수가 요소타입으로 변환될 수 있는 vector의 경우에는 요소 수를 지정할 때 ()초기화 구문을 사용해야 한다.
- 즉 초기화 구문은 () {}가 있지만 차이가 있다.