본문 바로가기

프로그래밍 언어/C

[K.N.K C Programming 정리] Chap 3. Formatted Input/Output

K.N.KING C Programming

안녕하세요 여러분! ㅎㅎ

3단원을 가져와봤습니다! C의 입출력, scanf 와 printf 를 다루는 단원입니다! 

scanf 와 printf에 찜찜한 점들이 좀 있었는데 잘 해결된거 같아 좋았습니다. ㅎㅎ 

자 그럼 가봅시다!


Chap3. Formatted Input/Output

In seeking the unattainable, simplicity only gets in the way


3.1 The printf  fuction

printf 함수는 출력을 위해 만들어졌다. 이 때 반드시 format string 과 같이 주어져야 한다. 

 

printf(string, expr1, expr2, ...);

 

format string 에는 보통의 문자들과 %로 시작하는 변환 지정자(conversion-specifier)가 담길 수 있다. 

변환 지정자는 출력 도중에 채워질 값을 나타낸다. (a place holder representing a value to be filled in during printing)

변환 지정자 라는 말의 유래는, % 뒤에 나오는 정보가 값이 어떻게 내부에 저장된 형태(binary) 에서 출력됬을때 형태(characters)로 바뀌는지 알려준다는 점에서 비롯되었다. (The information that follows the % character specifies how the value is converted from its internal form(binary) to printed form(characters) - that's where the term "conversion-specification" comes from)

예를 들면, %d 는 printf 가 정수값을 이진수 형태에서 10진 정수로 변환함을 알려준다.

%f는 실수값에 대해 똑같은 과정을 수행한다.

변환지정자가 아니라면 string에 작성한 그대로 출력된다. 

변환 지정자는 출력될 값으로 교체된다(be replaced).  이 때 주의할 점은 C 컴파일러는 변환 지정자의 숫자와 출력될 값들의 숫자가 서로 일치해야 하는지 검사해야할 의무는 없다는 점이다. 

 

printf("%d %d\n", i);

 

이 경우 i값을 우선 정확하게 출력할것이다. 그러나 출력되는 두번째 정수값은 아무 의미가 없는 쓰레기값이다.

더군다나, 컴파일러는 변환지정자가 출력될 문자와 타입이 일치하는지 검사해야할 의무도 없다. 만약 프로그래머가 옳바르지 않은 변환지정자를 사용한다면, 프로그램은 단순히 아무 의미없는 결과를 출력할것이다. 예를 들면, 다음과 같은 경우이다.

 

int i;
float x;

printf(%f %d \n", i, x);

 

Conversion Specifications

변환지정자는 프로그래머가 출력의 형태를 조정할 수 있게 해준다. 물론 그때문에 쓰기 복잡하고 읽기 어려울 수 있다.

사실 책의 초창기 부분에서 변환 지정자를 완벽하게 설명하는것은 몹시 힘들고 고된 일이다. 

그 대신에 중요한 부분만 간단하게 짚고 넘어가보자. 

 

우선, 변환지정자는 %m.pX 혹은 %-m.pX 의 형태를 지닌다. 

여기서 m과 p는 정수 상수들이고 X는 문자이다. m과 p는 모두 써도 되고 안써도된다. 

예를 들면, %10.2f 의 경우 m은 10이고 p는 2, X는 f 이다.

만약 p가 생략되면 m과 p를 구분짓는 . 도 같이 사라진다. 즉, %5d 인 경우 m이 5 고 p는 생략된 경우라는 뜻이다.

마찬가지로 %.2f 의 경우 p는 2 이고 m 이 생략된 경우이다.

 

m은 최소 공간너비(minimum field width) 이다. m은 출력될 공간의 최소 크기(숫자 개수)를 결정한다.(m specifies the minimum number of characters to print) 이때 만약 출력할 문자들의 수가 m보다 작다면 우측 정렬된다. 예를 들면,

int i = 123;
printf("%4d",i);

 

이해를 돕기 위해 ' ' , 즉 한 칸의 띄어쓰기를 ● 라고 칭하자. 위 코드를 실행할 경우 ●123 이 출력된다.

만약 출력할 문자들의 숫자가 m보다 크다면, 필요한 만큼 공간이 늘어나게 된다. 예를 들면,

 

int i = 12345;
printf("%4d",i);

이 경우 12345 가 출력된다. 표시되지 않는 숫자는 없다.

m앞에 음수 기호(-)를 붙이는 경우 좌측정렬된다. 예를 들면,

int i = 123;
printf("%-4d",i);

이 경우 123● 이 출력된다.

 

p는 precision을 의미한다. p는 X에 따라, 즉 변환지정자에 따라 의미가 달라진다.

X는 출력되기전에 값에 어떤 변환이 적용되야 할지 나타낸다. X의 경우에 따라 d를 설명하면,

  • d : 10진 정수의 형태로 정수를 출력한다. 이 경우 p는 꼭 나타내야 할 최소의 숫자 개수를 나타낸다.(p indicates the minimum number of digits to display)(이 경우 필요하면 숫자 0 이 제일 앞에 채워진다.) p 가 생략되면 1을 가지는것으로 가정한다.(따라서 %d는 %.1d 와같다.)
  • e : 부동소수점 숫자들을 지수형태로 나타낸다(과학적표기법). 이 경우 p 는 소수점 아래 몇 개의 숫자들이 와야하는지를 결정한다.(기본값은 6이다.) p가 만약 0이라면, 소수점은 표시되지 않는다. 
  • f : 부동소수점 숫자들을 고정소수점 형태로 나타낸다. 이 경우 p는 e에서와 똑같은 의미를 지닌다. 
  • g : 부동소수점 형태의 숫자들을 숫자의 크기에 따라 지수형태 혹은 고정소수점 형태로 나타낸다. 이 경우 p는 유효숫자의 갯수를 나타낸다. f 변환과 다르게 g는 뒤에 따라붙는 0을 나타내지 않는다.(g conversion wont show trailing zeros). g 지정자는 숫자의 크기가 예측되지 않는 숫자들을 출력할때 유용하다. 숫자가 아주 크거나 작은 경우 g 지정자는 지수형태로 출력하고, 적당히 크거나 적당히 작은 경우 고정소수점 형태로 출력한다. 

이 밖의 지정자들은 이후의 챕터에서 다루기로 하자. 

 

다음과 같은 코드를 생각해보자.

#include <stdio.h>

int main(void)
{
    int i;
    float x;
    
    i = 40;
    x = 839.21f;
    
    printf("|%d|%5d|%-5d|%5.3d|\n", i, i, i, i);
    printf("|%10.3f|%10.3e|%-10g|\n", x, x, x);
    
    return 0;
}

| 은 출력되는 각각의 포맷을 구분짓기 위해서 사용했다. 위 코드의 결과는 다음과 같다.

 

|40|   40|40   |  040|
|   839.210|  8.392e+02|839.21   |

이제 결과를 분석해보자.

  • %d : i를 10진 정수로 나타낸다. 
  • %5d : i를 10진 정수로 나타낸다. 이때 공간의 최소 크기는 5이고, 우측 정렬된 모습을 볼 수 있다. i 가 2개의 문자만을 요구하기 때문에 3칸의 공백이 생김을 알 수 있다.
  • %-5d : - 가 붙어서 좌측정렬된 모습을 볼 수 있다.
  • %5.3d : i를 10진 정수로 나타낸다. 이 때 공간의 최소 크기는 5이고, 최소 3개의 숫자를 이용함을 알 수 있다. 40이 2개의 문자를 요구하기 때문에 3개를 채우기 위해 앞에 0이 붙는다. 최소크기가 5인데 문자 3개를 썼으므로 앞에 두칸의 공백이 생긴다. 우측정렬되었다.%
  • %10.3f : x를 고정소수점 형태로 나타내었다. 이 때 m이 10 이므로 차지하는 공간의 크기는 10칸이다. 여기서 p가 3이므로 소수점 아래 3개의 숫자가 와야한다. 따라서 소수점아래 210이 오게 되었다. 8, 3, 9, ., 2, 1, 0 총 7개의 문자가 필요하므로 앞에 3칸의 공백이 생겼다. 
  • %10.3e : x를 지수형태로 나타낸다. 총 10개의 문자를 사용하고(m 이 10), 소수점 아래 3개의 숫자가 와야하므로(p가 3) x는 총 9개의 숫자가 필요하다. 따라서 앞에 한 칸의 공백이 생겼다.
  • %-10g : x를 고정소수점 혹은 지수형태로 나타내는데, 이 경우 printf 는 x를 고정소수점 형태로 나타내는걸 선택했다. 좌측정렬때문에 x뒤에 4칸의 공백이 생겼다.

3.2 The scanf Function

printf 가 출력결과를 특정 포맷으로 나타내듯이, scanf 는 입력을 특정 포맷을 참고해 읽는다(scanf reads input according to a particular format). scanf 의 format string 은 printf에서의 그것과 같이, 보통의 문자들(ordinary characters) 과 변환 지정자를 포함한다. printf 에서의 변환과 scanf에서의 변환은 동일하다!

대부분의 경우 scanf의 format string은 오직 변환지정자만 포함한다. 다음의 예처럼 말이다.

 

int i, j;
float x, y;

scanf("%d%d%f%f", &i, &j, &x, &y);

이 경우 유저가 1 -20 .3 -4 .0e3 을 입력한 경우 scanf는 한 줄을 읽고, 읽은 문자들을 나타내고자 하는 숫자로 변환한다. 그러고나서 1, -20, 0.3, -4000.0 을 각각 i, j, x, y 에 할당한다. scanf 에서는 "%d%d%f%f" 처럼 따닥따닥 붙어있는(tightly - packed) format string 이 주로 사용된다. 

scanf 는 printf와 마찬가지로 많은 주의를 요구한다. scanf를 쓸때면, 프로그래머는 반드시 변환지정자의 개수가 입력 변수의 개수와 동일한지, 그리고 각각의 변환이 그에 대응하는 변수와 일치하는지 따져야한다.(the programmer must check that the number of conversion specifications matches the number of input variables and that each conversion is appropriate for the correspoding variable - as with printf, the compiler isn't required to check for a possible mismatch). 그리고 한가지 더 조심해야 할 것은 & 이다. 설명은 이후의 챕터에서 하기로 하고, 일단은 무조건 까먹지 말자는것만 기억하고 넘어가자.

 

scanf를 호출하는 것은 매우 강력한 기능이지만 데이터를 읽기에 너무 힘든 방법이다.

많은 프로페셔널한 C 프로그래머들은 scanf 쓰는 것을 꺼려한다. 그 대신, 모든 데이터를 문자의 형태로 읽고 나중에 숫자의 형태로 바꾼다. scanf는 만약 유저가 예상치못한 값을 넣으면 올바로 작동하지 못할 확률이 너무 크다.

일단은 scanf 를 쓰는걸로 하고 넘어가도록 하자.

 

How scanf works

scanf 는 사실 이전에 기술한것보다 훨씬 많은 동작을 수행한다! 동작은 기본적으로 "pattern - matching" 이다. 이 말인 즉슨, 입력 문자들과 변환 지정을 짝지어준다는 것이다.(It is essentially a "pattern - matching" function that tries to match up groups of input characters with conversion specifications)

scanf가 호출되면, scanf는 scanf 인자 안에 들어있는 string을 왼쪽부터 처리하기 시작한다. 각각의 변환지정자에 대해 scanf 는 입력값안에서 적절한 타입의 아이템(덩어리, 청크 정도로 해석하시면 될거같습니다!)을 짝지으려 한다. 이 과정에서, 필요한 경우 공백은 스킵한다.

scanf는 그 아이템을 읽고, 그 아이템에 속할 수 없는 문자를 만날 경우 중지한다. 그 아이템이 성공적으로 읽혔다면, scanf는 나머지 format string 에 대해 작업을 속행한다. 만약 어떤 아이템이라도 제대로 읽히지 못했다면, scanf 는 나머지 부분은 쳐다보지도 않고 즉시 리턴한다.

 

숫자의 시작 부분을 찾으면서 scanf는 white - space characters 들을 모두 무시한다.(As it searches for the beginning of a number, scanf ignores white-space characters). 여기서 white - space characters 란 스페이스바, 수평혹은 수직 탭, \n)등을 모두 포함한다(계속 나오는 용어이니 짚고 넘어가자. 알겠지??). 따라서 숫자들은 한 줄로 압축되거나 여러 줄로 펼쳐질 수 있다. 다음과 같은 예시를 보자.

scanf("%d%d%f%f", &i, &j, &x, &y);
/* 입력값 : 
1
-20   .3
   -4.0e3
*/

유저가 위 처럼 세 줄에 걸쳐서 입력값을 넣었다고 해보자.

이 경우 scanf 는 이를 문자의 연속한 스트림으로 바라본다. 무슨 말이냐면, ' '(한 칸 공백)를 ● 이라하고, ◎를 '\n'(엔터(new line character)) 라고 한다면,

scanf는 이를 ●●1◎-20●●●.3◎●●●-4.0e3◎ 이렇게 바라본다는 뜻이다.

scanf는 각 숫자의 시작하는 부분을 찾아 갈때 white-space characters 를 스킵하기 때문에 scanf는 숫자들을 성공적으로 읽어낼 수 있다. 그말인 즉슨, 문자 아래의 s를 스킵되었다 라는 표시라 하고, r을 인풋값으로 읽었다 라는 표시라 한다면,

 

●●1◎-20●●●.3◎●●●-4.0e3◎

ssrsrrrsssrrssssrrrrrr 

 

이다. 이때 맨 끝의 \n 같은 경우 scanf 가 실제로 읽지는 않고 엿봤다가(peek) 만약 scanf가 또 쓰인다면, 다음 scanf 의 호출에서 첫번째 문자로 읽히게 된다.

 

그렇다면 scanf 가 정수 혹은 실수값을 인식하기 위해 따르는 규칙이 뭘까?

정수값을 읽도록 요청받는다면, scanf는 우선 숫자, 양수 부호, 음수 부호를 찾는다. 만약 숫자를 발견했다면 숫자가 아닌 문자가 나올때까지 계속해서 숫자를 읽는다. 

실수값을 읽도록 요청받는다면, scanf는 우선 양수 부호, 음수 부호를 찾는다. 그러고 나서 숫자의 시리즈(소수점을 포함한)를 찾는다. 그 후 지수(이건 선택적)를 찾는다.

 

만약 scanf 가 현재 읽고있는 덩어리의 일부가 될 수 없는 문자를 만난다면, 그 문자는 다음 덩어리 혹은 다음 scanf의 호출에서 다시 읽어질 수 있도록 돌려보내진다. (when scanf encounters a character that can't be part of the current item, the character is "put back" to be read again during the scanning of the next input item or during the next call of scanf)

 

아까 예시로 든 경우를 본격적으로 분석해보자.

scanf("%d%d%f%f", &i, &j, &x, &y);
/* 입력값 : 
1
-20   .3
   -4.0e3
*/

 

입력값이 1-20.3-4.0e3◎ 이 되었음을 짚고 넘어가자!(white - space characters 제거)

여기서 scanf는 다음과 같은 과정으로 입력값을 처리한다.

  • 변환지정자 : %d. 우선 공백이 아닌 첫 번째 입력 문자는 1 이다. 정수는 1로 시작할 수 있기때문에, scanf는 다음 문자를 읽는다. 다음 문자는 - 이다. -는 정수 안에서 나올 수 없는 문자이기 때문에(5-6 같은 숫자 봤나? -1 이라고 생각하고 있는거 아니지? 그렇지?) scanf는 i에 1을 저장하고 - 문자를 다시 돌려보낸다. 
  • 변환지정자 : %d. scanf 는 -, 2, 0 그리고 . 까지 읽는다. 정수는 소수점(.)을 가질 수 없기 때문에 scanf는 j에 -20을 저장하고 . 문자를 다시 되돌려보낸다.
  • 변환지정자 : %f. scanf는 ., 3, - 까지 읽는다. 부동소수점 숫자가 소수점 이하 자리에 - 를 포함할 수 없기때문에, scanf는 x에 0.3 을 저장하고 - 문자를 다시 되돌려보낸다.
  • 변환지정자 : %f. scanf 는-, 4, ., 0, e, 3 그리고 ◎(new - line) 까지 읽는다. 부동소수점 숫자가 \n 을 포함하지 않기 때문에 scanf 는 -4.0 X 10^3 을 y에 저장하고 \n을 다시 되돌려보낸다.

위 예시들에서 scanf는 format string안의 모든 변환지정자와 인풋 아이템들을 짝지을수있다.

 

Ordinary Characters in Format Strings

(이번 단원에서 제일 얻어간게 많았던 단원이었다! 자세히 살펴보자 ㅎㅎ)

scanf의 이러한 pattern - matching 은 ordinary characters가 담긴 format string 을 작성함으로써 더 한 발 나갈 수 있다.(The concept of pattern - matching can be taken one step further by writing format strings that contain)

scanf가 ordinary character를 다룰때 취하는 행동들은 ordinary character 들이 white - space character 이냐 아니냐에 따라 달라진다. 무슨말인지 모르겠지? 예를 들어 살펴보자. 그전에, 두개만 짚고 넘어가자.

  • white-space characters : 만약 format string안에서 하나 혹은 연이은 white - space character들을 만나게 되면, scanf는 입력값들안에서 white-space character가 아닌 문자들이 나올때까지 white-space character들을 읽어내린다(무시한다고 보면됨).(When it encounters one or more consecutive white-space characters in a format string, scanf repeatedly reads white-space characters from the input until it reaches a non-white space character(which is "put back").format string 안에서 white-space character의 개수는 아무 관계없다(중요하지않음). format string 안에 있는 단 한개의 white - space character는 입력값 안에 존재하는 0개 혹은 한 개, 혹은 수많은 개수의 white - space character 와 짝지어진다(무슨 말인지 잘 모르겠지? 뒤에서 더 자세히 설명). format string안에 white - space character를 넣었다고 해서 입력값에 white - space character를 넣어야하는것은 아니다.
  • other characters : 만약 format string안에 white - space character가 아닌 문자들을 만나게 되면, scanf는 그것을 입력값과 비교한다. 만약 비교하는 두 문자가 일치한다면, scanf는 입력값을 버리고 format string을 계속해서 처리해나간다. 만약 일치하지 않는 경우, scanf는 문제가 되는 문자를 입력값으로 돌려보내고, 이후의 작업을 그 즉시 중단한다. 

이제 진짜 예를 들어서 무슨말인지 살펴보자.

// format string : %d/%d 
// there is no white - space character in the format string.
int i, j;
scanf("%d/%d", &i, &j);

 

만약 입력으로 ●5/●96 이 들어왔다고 해보자.(●는 스페이스바, ' ' 임에 유의.)

scanf 는 우선 첫번째 공백을 무시한다. 그러고나서 %d 와 5를 짝짓는다. / 와 / 를 짝짓고, 공백을 무시한다. 96과 %d 를 짝지어준다.

 

그런데 만약 입력값으로 ●5●/●96 가 들어왔다고 해보자. 

이 경우, scanf 는 우선 첫번째 공백을 무시한다. 그러고나서 %d와 5를 짝지어준다. 이제 format string안의 /를 짝지어줘야 하는데, 다음 입력값이 ● 이다! 매칭이 이루어지지 않는다. 따라서 공백을 다시 돌려보낸다(put back).

아직 처리해야할 입력값, ●/●96 이 남아있지만 다음 scanf 가 오기를 기대하며 scanf가 종료된다.

이 경우 의도한대로 옳바르게 작동시키기 위해선, format string으로 "%d/%d"가 아닌 "%d /%d" 를 입력해야한다.

 

이제 우리가 조심해야 할것들이 보인다.

 

scanf("%d, %d", &i, &j);

이 경우 역시, 만약 5 5 이렇게 입력하는 경우 i에만 5가 저장되고 j에는 아무것도 저장되지 않은 채 scanf가 종료될것이다. scanf와 printf는 비슷해보이지만,  분명한 차이가 존재한다! 이 차이를 무시하는건 위험한 일이다.

 

이제 우리는 다음과 같은 경우도 왜 해서는 안되는지 알 수 있다. scanf에 '\n' 을 추가하는 경우이다.

scanf("%d\n", &i);

아까 format string 안에 white - space characters 가 들어갈때 어떻게 되는지를 잘 읽어봤다면 사실 설명 안해도 왜 저렇게 쓰면 안되는건지 알 수 있다. 

scanf에서 format string 안에서 \n 은 스페이스, 즉 ' ' 와 똑같은 취급을 받는다. 그말인 즉슨, 둘 다 scanf 에게 white - space character 가 아닌 문자를 찾을때 까지 계속해서 입력값을 읽게 한다. 예를 들어 위의 예시의 경우, 입력값으로 

 

'   5\n' 이 들어온 경우 scanf 는 먼저 앞의 공백들을 무시하고 5를 i 와 짝지어주고, format string안의 \n 때문에 white - space character 가 아닌 문자를 만날때까지 계속해서 input값을 읽어나간다. 즉, 1 혹은 p 이런 white - space character가 아닌 문자를 유저가 입력할때까지 scanf는 종료되지 않고 프로세스를 잡아먹는다.

 

 

Q&A

Q : %i 와 %d의 차이는 뭔가요?

A : printf 에서는 둘 중에 뭘 쓰든 아무 상관이 없다! 그런데 scanf 에서는 주의를 좀 해야해. scanf 에서는 %d 는 10진 정수와만 짝지어지고, %i 는 8진정수, 10진정수, 16진 정수까지 짝지어 질수 있다. 만약 입력값이 056 이렇게 들어온다면 scanf는 이를 8진 정수로 해석한다. 만약 %d 대신 %i 를 쓰는 경우 유저가 실수로 숫자앞에 0을 입력하는 경우 꽤 골치아픈 일이 일어난다. 따라서 나(작가)는 %d 를 쓰는것을 추천한다!

 

Q : %는 어떻게 출력하나요?

A : %% 를 입력하면 된다.

 

Q : 만약 scanf를 쓰는 상황에서 유저가 숫자가 아닌값을 입력하면 어떻게 되나요?

A : 만약 scanf("%d", &i) 인 상황에서 유저가 foo 라는 값을 입력한다면, i에는 기존에 저장된 값이 그대로 들어있겠지. foo는 다음 scanf 호출을 위해 대기하게 될거야.

 

Q : 중간에 scanf 가 "put - back", 즉 입력값을 되돌려놓는다고 하셨는데 이게 어떻게 가능한거죠?

A : 프로그램은 유저가 입력한 값들을 타이핑될때 읽어내리지 않아. 그 대신에, input은 scanf가 접근가능한 숨겨진 버퍼(buffer) 에 저장됬다가, 되돌려놔야할 상황이 오면 버퍼에 다시 되돌려놓는거지. 22장에서 버퍼링을 설명해주겠네.

 

Q : 만약 scanf("%d%d", &i, &j) 인 상황에서 유저가 입력값으로 4, 28 이렇게 입력하면 어떻게 되나요?

A : 자네 오늘 공부를 대충한거같군! scanf가 입력값들을 처리할때, 우선 i에는 4가 저장되겠지. 그러고나서 scanf는 두번째 숫자의 시작점을 찾으려할건데, ',' 는 숫자의 시작점이 될수 있나? 당연히 없겠지! 콤마로 시작하는 숫자봤나?

따라서 scanf는 즉시 종료되고 콤마랑 공백, 28은 다음 scanf가 호출되기를 기다리겠지.

이제 이 경우를 어떻게 해결할지도 알겠지? format string 안에 콤마를 넣으면되겠지? 그렇지?

 


휴 ㅎㅎ printf, 특히 scanf 작동 원리가 너무 궁금했는데 조금이라도 알게되어서 기분이 좋네요ㅎㅎ 

읽어주셔서 감사합니다! 그럼 모두 안녕히~~ 다음 편에서 뵙겠습니다!