C - 1.5. 문자 입출력

목차

1. 문자 입출력

우리는 이제부터 문자 데이터를 다루는 프로그램에 대해 배울 것이다. C언어의 표준 라이브러리의 문자 입출력은 단순하다. 프로그램에는 다양한 입출력 스트림이 들어오고 그것을 처리하는 것이다. 이러한 입력 또는 출력은 사용자의 키보드 입력과 상호작용하는 콘솔 창일 수도 있고, 어떤 파일일 수도 있다. 입출력 스트림을 어디로 연결할 것인지에 대해서도 추후에 다룰 것이다.

하지만 그 입출력이 어디 스트림에서 오고 어디서 가든 C언어의 입출력 함수는 그저 입출력을 처리할 뿐이다. 이러한 입출력 함수 중 가장 간단한 것은 getchar()putchar(c) 이다. 두 함수는 각각 단 하나의 문자를 입력받고, 출력한다.

getchar()는 입력 스트림에서 하나의 문자를 입력받은 후 리턴한다. 그리고 putchar(c)는 보통은 하나의 문자를 나타내는 어떤 c를 받아서 출력 스트림에 출력한다. 이때 putchar(c) 와 우리가 썼던 printf 는 함께 쓰일 수 있다. 그냥 우리가 생각하는 대로, 호출이 일어나는 순서대로 출력된다.

시험용으로 다음과 같은 코드를 짜본다.

#include <stdio.h>
 
/* putchar 와 printf를 함께 사용해 본다 */
main() {
    char c='t';
    putchar(c);
    printf("est\n");
}

위의 코드를 실행하면 test 와 개행이 출력된다. char형 변수 c에 담긴 t 한 글자가 putchar에 의해 출력된 이후 printf에 의해 다음 글자들이 출력되는 것이다. 출력 함수를 두 종류 사용했지만 딱히 충돌은 일어나지 않음을 확인할 수 있다.

2. 입력받은 대로 출력하기

고작 한 글자씩만 입력받고 또 출력하는 함수 getchar()putchar(c)만을 배웠지만 이것만으로도 꽤 많은 예제를 실행시켜볼 수 있다. 예를 들어, 한 글자씩 입력받을 때마다 출력해 주는 프로그램을 만들어 볼 수 있다.

가령 입력이 파일에서의 스트림으로부터 온다면 우리는 파일의 끝(End Of File, EOF)까지 입력을 받으면서 그대로 출력을 해준다고 생각할 수 있을 것이다. 추후에 입출력 리디렉션을 배우고 나면 그런 동작을 하는 코드를 실제로 작성해 볼 수 있다.

또는, 나는 이 블로그 글을 치면서도 타이핑을 하고 있는데 만약 문서 프로그램이 키보드 입력만 받고 어떤 입력이 들어왔는지 출력해 주지 않는다면 매우 곤란할 것이다.

물론 문서 프로그램이나, 실제로 쓰이는 즉시 출력 알고리즘은 우리가 짜는 예제보다 더 복잡한 코드와 최적화 따위가 들어가 있을 것이다. 하지만 기본적인 원리는 비슷함을 충분히 생각할 수 있다는 것이다.

다음 코드는 EOF가 입력될 때까지 문자를 입력받아서 그대로 출력하는 코드이다.

#include <stdio.h>
 
/* 입력받은 내용을 EOF가 나올 때까지 그대로 출력해 준다 */
main() {
    int c;
 
    c = getchar();
    while (c != EOF) {
        putchar(c);
        c = getchar();
    }
}

참고로 !=는 같지 않음을 뜻하는 것이다. 이 코드를 시험해 보면 EOF입력이 들어올 때까지 입력받은 대로 출력해 주는 것을 볼 수 있다. 이때 콘솔 창에서 어떻게 EOF를 입력할 수 있을지도 문젠데, 윈도우의 경우 Ctrl+Z 키를, 리눅스의 경우에는 Ctrl+D 키를 누르면 EOF를 콘솔창에 입력할 수 있다.

그런데 이상한 점을 하나 찾을 수 있다. 우리는 분명 char, 즉 문자 하나를 다루는 함수들을 사용하였다. getcharputchar 라는 이름에서부터 char를 다룬다는 것이 느껴지지 않는가? 그런데 위 코드에서는int형으로 c를 선언하였다. 그리고 getchar() 함수의 명세를 찾아보아도 int형을 리턴한다는 것을 알 수 있다. 왜 그런 것일까?

이는 우리가 EOF를 구분해야 하기 때문이다. C언어 표준의 EOF는 <stdio.h> 에 정의되어 있는 매크로 상수인데, 어떤 char형 문자와도 혼동되지 않을 숫자여야 한다고만 명시되어 있다. 흔히는 -1을 사용한다. 따라서 우리는 이러한 EOF 또한 다룰 수 있도록 c를 정수형 int으로 선언하는 것이다. 만약 우리가 c를 char형으로 선언했다면 여기에 EOF가 담기지 못할 수도 있었을 것이다.

3. 코드의 개선

우리는 c=getchar()a=3 같은 할당문조차도 표현이며 값을 가진다는 것을 이용해, 위의 코드를 좀더 줄일 수 있다. 이때 할당문은 할당이 일어난 후 할당문 왼쪽의 값을 표현한다. 예를 들어 a=3 이라는 할당문이 있다면 이 할당문은 3이 할당되고 난 후에 a의 값, 즉 3을 표현한다.

이러한 c의 할당이 while 문 내부로 들어가는 것으로 코드가 짧아진다.

#include <stdio.h>
 
/* 입력받은 내용을 EOF가 나올 때까지 그대로 출력해 준다
 할당문을 이용해 간략화한 코드 */
main() {
    int c;
 
    while ((c = getchar()) != EOF) {
        putchar(c);
    }
}

while 문 내부에서 테스트가 진행되면서 getchar() 로 입력을 받고, 그 값을 c에 할당한 후 c 의 값을 EOF와 비교한다. 만약 c가 EOF가 아니면 while문 내부가 실행되고 EOF이면 반복문이 바로 종료될 것이다.

이렇게 할당을 이용해서 짜는 코드는 경험이 풍부한 사람에게는 더 가독성이 좋기도 하다. 그러나 이렇게 코드를 간략화하는 걸 지나치게 남용한다면 당연히 가독성이 떨어질 수밖에 없으므로 주의해야 한다. 아주 똑똑하게 짜인 코드가 잘 짜인 코드가 아니라, 오히려 누가 보아도 쉽게 의미를 알 수 있고 단순하게 짜인 코드가 좋은 코드이고 어쩌면 더 짜기 어렵다. 그저 할당문 또한 값을 갖는 표현이라는 걸 안 것으로 만족하자.

마지막으로, c = getchar() 에 괄호를 씌워 준 것을 볼 수 있는데 이는 연산자 우선순위 때문이다. !== 보다 우선순위가 높기 때문에 만약 저 할당문에 괄호를 씌워 주지 않으면 c = getchar() != EOFc = (getchar() != EOF) 와 같은 결과를 도출할 것이다.

이는 getchar() != EOF 의 결과에 따라 c에 0 또는 1(boolean true의 값은 보통 1이다) 만이 할당되는, 우리가 원하는 것과는 다른 결과를 낳게 된다.

3.1 할당문의 값에 대하여

C언어를 처음 배울 때 다음과 같은 실수를 흔히 하고는 한다.

#include <stdio.h>
 
/* a가 1이면 a가 1이라고 출력해 주려고 한 코드이다 */
main() {
    int a = 2;
 
    if (a = 1) {
        printf("a is 1");
    }
}

위 코드를 실행해 보면 a가 2로 할당되어 있는데도 if 문 내의 코드가 실행된다. 이는 우리가 원하는 결과가 아니다! 왜냐 하면 우리가 원하는 대로, a가 1과 같은지 비교하는 방법은 a==1 이기 때문이다. C언어를 조금이라도 잘 배운 사람이라면 이런 실수는 잡아낼 수 있을 것이다.

하지만, 우리가 원하는 결과는 아니더라도 이 코드는 딱히 버그 없이 잘 실행되고 어떤 결과도 나온다. 이 이유까지 속시원히 답변하는 사람을 그렇게 많이 보지 못했다. 나도 그랬다.

이 이유는 바로 위에서 설명한, C언어에서는 할당문에도 값이 있기 때문이다. 위의 코드 같은 경우 a=1 즉 a의 값인 1이 if문에 들어갔고 1은 무조건 참이므로 if문 내부의 코드가 실행된 것이다. 그리고 a에도 또한 1이 정상적으로 할당된다.

할당문에도 값이 존재하는 것은 이렇게 실수를 불러일으키기도 하지만, 위의 getchar() 를 이용하는 코드와 같이 코드를 더 간략하게 만들어 주기도 한다.