프로그래머가 지녀야 할 가장 중요한 능력은 문제 해결 능력이다.
다음은 C / C++ 에서 "표준 입력으로부터 입력받은 길이를 알 수 없는 문자열 저장하기"의 전형적인 문제의 예이다.
char s[LENGTH];
scanf("%s", s);
scanf ( ) 함수는 인자로 주어지는 형식지정자(format specifier)를 파싱해야 하는 오버헤드가 따른다.
공백 문자가 나타나면 읽기를 중단한다.
형 안정성을 보장받을 수 없다. ("%s" 대신 "%d"로 오타라도 낸다면?)
문자열의 예상되는 크기를 프로그래머도 알고 있어야 한다.
char s[LENGTH];
gets( s );
gets( ) 함수는 문자열을 입력받는 데에 거의 완벽한 기능을 제공한다.
scanf( ) 함수와 같이 형식 지정자를 파싱해야 하는 오버헤드도 없으며, 빈칸이 나오더라도 개행문자를 입력할 때까지
끊임없이 입력받는다. 그러나 이 함수 또한 다음의 문제점이 존재한다.
"표준 입력으로부터 입력이 문자열의 버퍼 크기를 넘어가는 경우에 어떤 결과가 따를지 예상할 수 없다."
위의 문제는 gets( ) 함수가 버퍼의 크기를 전혀 알지 못한다는 것에서 비롯되므로
다음과 같이 버퍼의 길이를 알아야 하는 함수를 사용할 수 있다.
char s[LENGTH];
fgets( s, LENGTH, stdin );
fgets( ) 함수는 파일로부터 문자열을 읽어들이는 함수이다.
stdin이라는 표준입력에 대응하는 파일포인터를 사용함으로써 표준입력으로부터 문자열을 입력받는데도 사용할 수 있음을 기억해야 한다. fgets( ) 두번째 인자로 버퍼의 크기를 줌으로써 버퍼 오버플로우 문제를 해결할 수 있다.
그러나 이 방법 또한 다음과 같은 문제가 따른다.
fgets( ) 함수가 리턴되더라도 모든 문자열이 입력된 것인지 알 수가 없다."
이제부터 문제가 복잡하게 발생한다.
fgets( ) 함수는 버퍼의 크기까지만 문자열을 읽기 때문에 단순히 함수가 리턴되었다는 것만으로는 아직 표준입력 스트림에 문자가 남아있는지 확인할 수 없다. 따라서 추가적인 로직을 필요로 한다.
char s[LENGTH];
char *t, *u;
int size = 0;
int len;
do{
s[LENGTH - 2] = 0;
fgets( s, LENGTH, stdin );
len = strlen( s );
size += len;
u = malloc( size ); // (1)
strcpy( u, t );
free ( t ); // (1)
strcat ( u, s );
t = u;
} while (len == LENGTH - 1 && s[LENGTH - 2] != '\n' );
코드가 다소 복잡해졌다.
여하튼 입력스트림으로부터 문자열을 모두 저장하기 위한 코드임에는 분명하다.
while 루프를 차지하고라도, 루프 내부 코드는 대부분 문자열 버퍼의 재할당을 위한 코드이다.
(1) 부분은 realloc( ) 으로 다음과 같이 간단히 사용할 수도 있다.
char s[LENGTH];
char *t = 0;
int size = 0;
int len;
do{
s[LENGTH - 2] = 0;
fgets ( s, LENGTH, stdin );
len = strlen(s);
size += len;
t = realloc (size);
strcat ( t, s);
} while( len == LENGTH - 1 && s[LENGTH - 2] != '\n');
여전히 루프 자체의 복잡성은 남아있다. 이유는 다음과 같다.
루프 내 코드 구성을 보면 크게 두 부분으로 나눌 수 있다.
먼저 입력 스트림으로부터 받은 문자열을 임시 버퍼 s에 저장하는 부분이 있다.
그리고 임시 버퍼로부터 받은 문자열을 완성된 문자열로 저장하는 부분이다.
이와 같은 문자열 조작의 불편함은 전적으로 C언어의 문자열이 '문자열'이 아니라 '문자 배열'이기 때문이다.
C++에서 다음의 문제를 해결해보자.
char s[LENGTH]
std::cin >> s;
cin 객체는 기본적으로 scanf( ) 함수와 비슷한 역할을 한다. 하지만 보다 좋은 장점들이 있다.
만약 s 를 선언할 때 잘못하여 int 로 썼더라도 cin >> s; 라는 문장을 컴파일 하는 과정에서
컴파일러가 에러를 잡아주어 형 안정성을 보장해준다.
하지만 여전히 "공백문자에서 멈춤" 문제는 남아있다.
scanf( ) -> gets( ) 함수로 넘어갈 때처럼 C++은 basic_stream 클래스의 getline( ) 메소드를 고려할 수 있다.
char s[LENGTH];
std::cin.getline( s, sizeof( s ));
getline( ) 메소드는 C에서 사용했던 gets( ) 함수와 비슷한 일을 하는 iostream 메소드이다.
차이점이라면 fgets( ) 함수와 비슷하게 버퍼 사이즈를 인자로 받는 정도일 뿐이다.
그렇다면 fgets( ) 함수와 마찬가지의 문제점을 지니고 있다고 할 수 있겠다.
fgets( )에서 스트림을 모두 비우는 루틴을 고려해 보자.
char s[LENGTH];
char *t;
int size = 0;
do{
cin.clear()
cin.getline( s, LENGTH );
size += strlen ( s );
t = realloc ( size );
strcat ( t, s );
} while ( cin.fail( ));
위의 코드가 제대로 돌아갈 것 같지만 C++를 제대로 사용한 코드라고 말할 수 없다.
객체라고는 cin밖에 쓰이지 않았고 이런 스타일의 코드는 C에서 C++로 옮겨가려는 사람이 쓰게되는
전형적인 스타일이라고 할 수 있다. 즉, C의 코드를 그대로 C++ 라이브러리로 옮기는 것이다.
strcat( ) 함수로 문자열을 합치는 부분을 C++ 표준 string 클래스를 사용하면 간편하게 될 듯 하다.
char s[LENGTH];
string t;
do{
cin.clear( );
cin.getline( s, LENGTH );
t += s;
} while ( cin.fail( ));
이전 코드보다 루프 내부가 한결 깔끔해졌다. 버퍼의 재할당과 문자열 복사라는 주요한 기능을 캡슐화한 string 클래스를 사용함으로써 코드의 절반을 절약하는 성과를 이루어냈다.
하지만 아직 뭔가 어색하다.
string 객체와 문자'배열'이 혼재하고 있는 스타일의 불일치가 보인다.
사람의 언어로 따지면 명사는 영어로 조사만 우리말로 쓰는 것과 같은 아주 어색한 말투에 비유할 수 있다.
그렇다면 입력버퍼로 사용하는 s가 문제이다.
위에서 말했듯, s는 문자'배열'이지 '문자열'이 아니다.
그렇다면 s를 string 객체로 대체할 수 있는 방법을 찾아보아야 한다.
먼저 cin 메소드 중에 string 객체를 인자로 받는 멤버를 생각해볼 수 있다.
검색을 하다 보면 '문자배열'만로 입력 받을 수 밖에 없다는 것을 확인할 수 있을 것이다.
하지만 조금 더 깊게 검색을 하게 되면 cin이 istream 클래스의 한 인스턴스라는 것을 확인할 수 있다.
추가로 istream 클래스를 아래에 istream_iterator 라는 항목이 존재하는 것을 발견할 수 있을 것이다.
여기서 istream_iterator는 iterator 패턴을 입력스트림에 구현한 클래스 템플릿이다.
그렇다면 istream_iterator를 사용해 다음과 같이 코드를 구현할 수 있게 된다.
string s ( istream_iterator ( cin ), istream_iterator ( ));
위 코드는 지금까지 문제점이 었던 "사전에 예상되는 문자열 길이 알기" 문제를 근본적으로 제거하였다.
하지만 istream_iterator 템플릿은 입력을 받을 때 operator>> 를 사용한다.
결과적으로 cin >> XXX 라는 동작을 반복하도록 되어 있는 iterator이다.
이는 다시 말해 공백문자를 무시하는 것이다.
다음은 공백문자를 무시하지 못하도록 구현한 코드이다.
cin.unsetf ( ios::skipws );
string s( istream_iterator( cin ), istream_iterator( ));
cin.setf( ios::skipws );
원하는데로 동작하도록 깔끔한 코드를 구현하였지만, 이 같은 코드에도 한가지 문제가 존재한다.
바로 입력을 종료하기 위해서는 'EOF'를 입력해야 한다는 것이다. ( ^Z로 EOF가 들어간다. )
이는 istream_iterator이 기본적으로 '파일스트림'에 대해 사용하도록 되어있는 클래스이기 때문이다.
이를 해결하기 위해서 어떻게 해야할까?
cin.getline( ) 함수를 보게 되면 그 답을 찾을 수 있다.
getline( ) 함수는 basic_istream 멤버인 getline( ) 메서드와 함께 전역 getline( ) 템플릿 함수도 나타난다.
getline( ) 템플릿 함수를 사용하여 문자열을 입력받는 전형적인 예는 다음과 같다.
string s;
getline( cin, s );
위 코드 형태는 분명 C 버전의 코드와 굉장히 유사하지만, 모든 것이 '객체'로 되어있다.
결국 단 두줄 밖에 안되는 깔끔한 코드로 "버퍼사이즈 미리알기" 문제가 근본적으로 해결됨을 확인할 수 있다.
위 과정을 통해 'C++'스러운 코드를 작성하기 위해서 많은 해결방법을 고려해보아야 한다는 것을 느꼈다.