36-2-다.입출력

앞에서 이미 사용해 봐서 알겠지만 string 객체를 화면으로 출력할 때는 << 연산자를 사용하여 cout으로 보내기만 하면 된다. 아주 당연해 보이겠지만 이 출력 코드가 동작하는 이유는 cout과 string 객체를 인수로 취하는 << 전역 연산자가 오버로딩되어 있기 때문이다. cout이 string을 알아서 인식하는 것이 아니라 string 헤더 파일에 다음과 같은 연산자가 정의되어 있으므로 이런 출력 코드가 동작한다.

 

template<class _Elem, class _Traits, class _Alloc>

inline basic_ostream<_Elem, _Traits>& __cdecl operator<<(basic_ostream<_Elem, _Traits>& _Ostr, const basic_string<_Elem, _Traits, _Alloc>& _Str);

 

선언문이 상당히 복잡해 보이는데 다양한 타입을 허용하는 템플릿이 개입되면 원래 복잡해 질 수밖에 없다. 그래서 템플릿 코드는 가독성이 떨어진다고 하는 것이다. 이 선언문을 좀 읽기 쉽게 정리해 보면 다음과 같다.

 

ostream& operator<<(ostream& cout, string &s);

 

cout, string 객체를 인수로 취해 cout으로 string의 문자열을 출력하고 다시 cout 객체의 레퍼런스를 리턴하는 << 연산자이다. 리턴값이 cout의 레퍼런스이므로 연쇄적인 출력도 가능하다. 선언문이나 내부 코드는 복잡하겠지만 표준 입출력 스트림을 쓰는 방법과 동일하므로 string 객체를 출력하고 싶으면 무조건 cout << 로 보내기만 하면 된다.

콘솔 프로젝트를 만들 일이 없어 활용성은 떨어지지만 cin 표준 입력을 통해 문자열을 입력받는 것도 가능하다. string 헤더 파일이 << 연산자를 정의하는 것과 마찬가지로 >> 연산자도 정의하고 있기 때문이다. 그러나 >> 연산자는 한 단어만 입력할 수 있는데 getline 전역 함수를 사용하면 개행 코드전까지의 한 행을 모두 입력받을 수 있다. getline은 멤버 함수가 아니라 cin과 string을 인수로 받아들이는 전역 함수로 오버로딩되어 있다.

 

: stringin

#include <iostream>

#include <string>

using namespace std;

 

void main()

{

     string name, addr;

 

     cout << "이름을 입력하시오 : ";

     cin >> name;

     cout << "입력한 이름은 " << name << "입니다." << endl;

     cin.ignore();

     cout << "주소를 입력하시오 : ";

     getline(cin,addr);

     cout << "입력한 주소는 " << addr << "입니다." << endl;

}

 

이 예제는 두 개의 string 객체를 선언하고 이름과 주소를 입력받는다. 이름은 한 단어이므로 >> 연산자로 입력받을 수 있고 주소는 한 행이므로 반드시 getline으로 입력받아야 한다. >> 연산자는 공백을 만나면 입력을 완료해 버리므로 주소같은 긴 문장을 입력받지는 못한다. 또한 cin은 문자열 입력후 버퍼에 개행 코드를 남겨 두므로 ignore 함수로 이 개행 코드를 버리는 처리도 필요하다.

getline으로 문장을 입력받을 때 이 함수가 입력받은 문자열의 길이만큼 string 객체의 길이를 자동으로 관리하므로 아무리 긴 문자열이 입력되더라도 배열 범위를 넘어서는 것은 걱정하지 않아도 된다. 이에 비해 cin으로 문자 배열에 문자열을 입력받을 때는 배열 범위를 넘어설 때 다운될 위험성이 있다. 배열은 끝 표식이 없기 때문에 항상 위험하다.

문자열의 개별 문자들을 액세스하고 싶을 때는 [ ] 연산자 또는 at 멤버 함수를 사용한다. 둘 다 첨자 번호를 인수로 전달받아 첨자 위치의 문자를 읽는다. 이 함수들은 상수 버전과 비상수 버전이 각각 준비되어 있다.

 

char& operator[](size_type _Off)

char& at(size_type _Off);

const char& operator[](size_type _Off) const

const char& at(size_type _Off) const;

 

[ ] 연산자와 at 함수는 기능이 거의 동일한데 인수로 주어진 첨자의 문자를 읽되 레퍼런스가 리턴되므로 좌변에 사용하여 값을 변경하는 것도 가능하다. 단, 상수 객체인 경우는 좌변에 쓸 수 없으므로 상수 버전의 [ ] 연산자와 at 함수가 따로 존재한다. 다음 예제는 문자열의 개별 문자를 읽어 화면으로 출력한다.

 

: stringat

#include <iostream>

#include <string>

using namespace std;

 

void main()

{

     string s("korea");

     size_t len,i;

 

     len=s.size();

     for (i=0;i<len;i++) {

          cout << s[i];

     }

     cout << endl;

     s[0]='c';

 

     for (i=0;i<len;i++) {

          cout << s.at(i);

     }

     cout << endl;

}

 

s 문자열의 길이를 조사한 후 길이만큼 루프를 돌며 s[i] 연산문으로 개별 문자를 읽어 보았다. [ ] 연산자를 사용하면 string 객체를 마치 C의 문자 배열처럼 사용할 수도 있다. s[0]를 대입 연산자의 좌변에 두어 개별 문자의 값을 변경하는 것도 가능하다. 실행 결과는 다음과 같다.

 

korea

corea

 

처음에는 [ ] 연산자로 개별 문자를 출력해 보았고 s[0]를 'c'로 변경한 후 at 함수로 개별 문자를 다시 읽어 보았다. s를 const로 변경하면 상수 객체가 되므로 s[0]='c'; 대입문은 에러로 처리된다. 그러나 상수 버전의 [ ] 연산자도 정의되어 있으므로 s[i]로 개별 문자를 읽는 연산은 허용된다.

[ ] 연산자와 at 함수는 배열의 경계를 점검하는가 아닌가만 다르다. [ ] 연산자는 첨자가 길이보다 더 커도 경계 점검을 하지 않으므로 잘못된 동작을 할 수 있지만 at 함수는 길이보다 더 큰 첨자를 인수로 전달할 경우 out_of_range 예외를 발생시킨다. at 함수가 좀 더 안전하다고 할 수 있지만 매번 첨자의 유효성을 점검해야 하므로 속도는 그만큼 느릴 것이다. 확실히 범위를 넘어서지 않는다는 보장이 있다면 [ ] 연산자가 더 유리하다. 예제의 두 번째 for 루프는 at 함수를 사용하고 있는데 i가 문자열의 길이까지만 반복되므로 [ ] 연산자를 쓰는 것이 합리적이다.