22.3 Formatted I/O


이 섹션에서는, 포맷 스트링을 사용해서 입력과 출력을 제어하는 라이브러리 함수들을 알아본다. printf와 scanf를 포함하는 이 함수들은 입력시 문자 형태의 데이터를 숫자 형태의 입력으로 변환하고, 출력시 숫자 형태의 데이터를 문자 형태로 변환한다. 다른 I/O 함수들은 그런 변환을 할 수 없다.


The ...prinf Functions

int fprintf(FILE * restrict stream,

            const char * restrict format, ...);

int printf(const char * restrict format, ...);


fprintf와 printf 함수는 출력의 외양을 제어하는 포맷 스트링을 사용해서 가변 갯수의 데이터 아이템을 출력 스트림에 쓴다. 두 함수의 원형은 ... 기호(an ellipsis)로 끝난다. 이것은 인자가 가변 갯수만큼 추가될 수 있음을 나타낸다. 두 함수 모두 쓰인 문자의 갯수를 리턴한다. 에러 발생시에는 음의 값을 리턴한다.


 printf와 fprintf의 유일한 차이는 printf는 항상 stdout(표준 출력 스트림)에 쓰는 반면, fprintf는 첫번째 인자가 지정하는 스트림에 쓴다는 점이다.

printf("Total: %d\n", total);         /* writes to stdout */

fprintf(fp, "Total: %d\n", total);    /* writes to fp */


printf를 출력하는 것은 fprintf의 첫번째 인자를 stdout으로 출력하는 것과 동일하다.

fprintf를 단지 디스크의 파일에만 데이터를 저장하는 함수로 생각하면 안 된다. <stdio.h>의 많은 함수들과 마찬가지로, fprintf는 어떤 출력 stream에든 잘 작동한다. fprintf의 가장 흔한 용도 중 하나는 에러 메시지를 디스크의 파일과는 아무런 관련이 없는 stderr(표준 에러 스트림)으로 출력하는 것이다.

fprintf(stderr, "error: data file can't be opened.\n");


stderr에 메시지를 쓰는 경우 stdout이 redirect된 경우에도 메시지가 화면에 나타난다.

formatted output을 스트림에 쓰는 함수는 이 외에도 vfprintf와 vprintf가 있지만 잘 쓰이지 않는다. 두 함수는 <stdarg.h>헤더에 선언된 va_list 형식에 의존한다.(나중에 설명)


...printf Conversion Specifications

printf와 fprintf 함수는 일반적인 문자와 conversion specifier를 모두 포함할 수 있는 format string을 필요로 한다. 일반적인 문자들은 있는 그대로 출력되고, conversion specification은 나머지 인자들이 어떻게 문자로 변환되어 출력될지를 지정한다.

 ...printf 함수의 conversion specifaction은 % 문자와, 그 뒤에 오는 5개의 개별적인 아이템으로 구성된다.


Flags(optional; 한개 이상 사용 가능)


Minimum field width(optional). 이 숫자만큼의 자리를 점유하지 못하는 작은 아이템의 경우, 공백이 추가된다. 기본값으로 공백이 왼쪽에 추가되어 우측 정렬된다. 이 숫자보다 더 많은 글자를 차지해야 하는 큰 아이템이더라도 전체가 다 표시된다. 필드 너비는 정수이거나 문자 * 이어야 한다. 만약 *가 된 경우, 필드 너비는 그 다음 인자로부터 얻어진다. 만약 인자가 음수라면, - flag가 붙은 양의 정수로 취급된다.


Precision(optional). precision의 의미는 conversion에 따라 다르다.

d, i, o, u, x, X: 숫자들의 최소 개수(만약 더 적은 숫자만으로 표시되는 경우 0이 앞쪽에 추가됨)

a, A, e, E, f, F: 소수점 이후에 오는 숫자의 개수

            g, G: 유효숫자의 개수

               s: 바이트의 최대 숫자

precision은 마침표(.) 뒤에 정수가 오거나 문자 * 이 온다. 만약 * 이 사용된 경우, precision은 다음 인자로부터 얻어진다. 만약 인자가 음수인 경우, precision을 지정하지 않은 것과 같게 된다. 마침표만 사용된 경우, precision은 0이 된다.


Length modifier(optional). length modifier가 존재한다면 표시될 아이템의 형식이 특정 conversion specification의 일반적인 형식보다 길거나 짧다는 것을 나타낸다. (예를 들어, %d는 보통 int 값을 표시하고, %hd는 short int를, %ld는 long int를 표시할 때 사용된다.)


Conversion specifier. 다음 표에 있는 문자들만 conversion specifier가 될 수 있다. f, F, e, E, g, G, a, A는 double 값을 나타내기 위해 설계되었지만, float 값에 대해서도 잘 작동한다. default argument promotion 덕분에 가변 인자를 갖는 함수에 전달된 float 인자는 double로, 자동적으로 변환된다. 비슷하게, ...printf 로 전달된 char 인자는 int로 변환되고, %c 변환이 잘 작동하게 된다.


C99 Changes to ...printf Conversion Specifications


C99에서 printf와 fprintf의 conversion specification에 생긴 몇가지 변화들


추가된 length modifiers. hh, ll, j, z, t length modifier가 추가되었다. hh와 ll은 추가적인 길이 옵션을 제공하고, j는 가장 큰 정수 타입을 쓸 수 있게 해 주고, z와 t는 각각 size_t와 ptrdiff_t 형식의 값을 쓰기 쉽게 해 준다.


추가된 conversion specifiers. F, a, A conversion specifier가 추가되었다. F는 infinity와 NaN을 쓰는 방법을 제외하면 f와 동일하다. a와 A 변환은 드물게 사용된다. 16진수 부동 소수점 상수와 연관되어 있다.


infinity와 NaN을 쓸 수 있음. IEEE 754 floating-point standard에서는 부동 소수점 연산 결과를 infinity, negative infinity, 또는 NaN("not a number")으로 할 수 있게 허용했다. 예를 들어 1.0을 0.0으로 나누면 positive infinity, -1.0을 0.0으로 나누면 negative infinity, 0.0을 0.0으로 나누는 것은 NaN(결과가 수학적으로 정의되지 않았기 때문)이 된다. C99에서 a, A, e, E, f, F, g, G conversion specifier는 이러한 특별한 값을 출력할 수 있게 해 준다. 소문자 변환지정자들은 inf (또는 infinity), - inf (또는 -infinity), nan, -nan으로, 대문자 변환 지정자들은 똑같은 것들을 대문자로 표시한다.


wide 문자 지원. fprintf를 통해 와이드 문자를 쓸 수 있게 되었다. %lc로는 와이드 문자 하나를 쓴다. %ls로는 와이드 문자열을 쓴다.


정의되지 않은 conversion specificasions들이 허용됨. C89에서는 %le, %lE, %lf, %lg, %lG가 정의되지 않았었다. C99에서는 이제 이런 표현들이 허용된다.(l length modifier가 무시된다)



Examples of ...printf Conversion Specifications


#include <stdio.h>

int main(void)
{
    printf("Result of applying conversion to 123\n");
    printf(  "%8d\n", 123);
    printf( "%-8d\n", 123);
    printf( "%+8d\n", 123);
    printf( "% 8d\n", 123);
    printf( "%08d\n", 123);
    printf("%-+8d\n", 123);
    printf("%- 8d\n", 123);
    printf("%+08d\n", 123);
    printf("% 08d\n", 123);

    printf("Result of applying conversion to -123\n");
    printf(  "%8d\n", -123);
    printf( "%-8d\n", -123);
    printf( "%+8d\n", -123);
    printf( "% 8d\n", -123);
    printf( "%08d\n", -123);
    printf("%-+8d\n", -123);
    printf("%- 8d\n", -123);
    printf("%+08d\n", -123);
    printf("% 08d\n", -123);
}
/*
Result of applying conversion to 123
ººººº123
123ººººº
ºººº+123
ººººº123
00000123
+123ºººº
º123ºººº
+0000123
º0000123
Result of applying conversion to -123
ºººº-123
-123ºººº
ºººº-123
ºººº-123
-0000123
-123ºººº
-123ºººº
-0000123
-0000123
*/



#include <stdio.h>

int main(void)
{
    printf("Result of applying conversion to 123\n");
    printf( "%8o\n", 123);
    printf("%#8o\n", 123);
    printf( "%8x\n", 123);
    printf("%#8x\n", 123);
    printf( "%8X\n", 123);
    printf("%#8X\n", 123);

    printf("Result of applying conversion to 123.0\n");
    printf( "%8g\n", 123.0);
    printf("%#8g\n", 123.0);
    printf( "%8G\n", 123.0);
    printf("%#8G\n", 123.0);
}
/*
Result of applying conversion to 123
ººººº173
ºººº0173
ºººººº7b
ºººº0x7b
ºººººº7B
ºººº0X7B
Result of applying conversion to 123.0
ººººº123
º123.000
ººººº123
º123.000
*/


#include <stdio.h>

int main(void)
{
    char *s1 = "bogus", *s2 = "buzzword";

    printf(   "%6s\n", s1);
    printf(  "%-6s\n", s1);
    printf(  "%.4s\n", s1);
    printf( "%6.4s\n", s1);
    printf("%-6.4s\n", s1);

    printf(   "%6s\n", s2);
    printf(  "%-6s\n", s2);
    printf(  "%.4s\n", s2);
    printf( "%6.4s\n", s2);
    printf("%-6.4s\n", s2);
}
/*
ºbogus
bogusº
bogu
ººbogu
boguºº
buzzword
buzzword
buzz
ººbuzz
buzzºº
*/


#include <stdio.h>

int main(void)
{
    printf("%.4g\n", 123456.);
    printf("%.4g\n",  12345.6);
    printf("%.4g\n",   1234.56);
    printf("%.4g\n",    123.456);
    printf("%.4g\n",     12.3456);
    printf("%.4g\n",       1.23456);
    printf("%.4g\n",        .123456);
    printf("%.4g\n",        .0123456);
    printf("%.4g\n",        .00123456);
    printf("%.4g\n",        .000123456);
    printf("%.4g\n",        .0000123456);
    printf("%.4g\n",        .00000123456);
}

/*
1.235e+005
1.235e+004
1235
123.5
12.35
1.235
0.1235
0.01235
0.001235
0.0001235
1.235e-005
1.235e-006

The first two numbers have exponents of at least 4, so they're displayed in %e form.
The next eight numbers are displayed in %f form.
The last two numbers have exponents less than -4, so they're displayed in %e form.
*/

minimum field width와 precision에 * 문자를 사용하고 다음 인자로 숫자를 지정할 수 있기 때문에, 다음 문장들은 모두 동일한 출력을 생산한다.

printf("%6.4d", i);

printf("%*.4d", 6, i);

printf("%6.*d", 4, i);

printf("%*.*d", 6, 4, i);


min. field width와 precision을 별도의 인자로 지정할 수 있는 것은 몇가지 이점이 있다. 우선 width나 precision을 매크로를 사용해 지정할 수 있다.

printf("%*d", WIDTH, i);


width와 precision이 되는 인자는 constant가 아니고 프로그램 실행 중에 결정되는 값이어도 된다.

printf("%*d", page_width / num_cols, i);


%n 변환은 ...printf의 호출로 얼마나 많은 문자가 쓰였는지 알아내는 데 사용된다.

printf("%d%n\n", 123, &len);

printf가 %n 이전까지 3개의 문자(123)을 쓰게 되므로, len의 값은 3이 된다.



The ...scanf Functions

int fscanf(FILE * restrict stream,

           const char * restrict format, ...);

int scanf(const char * restrict format, ...);


fscanf와 scanf는 입력 스트림에서 데이터 아이템을 읽으며, 포맷 스트링을 사용해 입력의 레이아웃을 지정한다. 포맷 스트링 이후에는 객체를 가리키는 포인터들이 추가적인 인자로 온다. 입력된 아이템은 변환되어(포맷 스트링의 conversion specification에 따라) 이 객체들에 저장된다.

scanf는 언제나 stdin(표준 입력 스트림)으로부터 읽는 반면, fscanf는 첫번째 인자로 지정된 스트림으로부터 읽는다.

scanf("%d%d", &i, &j);         /* reads from stdin */

fscanf(fp, "%d%d", &i, &j);    /* reads from fp */


scanf 호출은 첫번째 인자가 stdin인 fscanf 호출과 동일하다.

...scanf 함수는 input failure가 발생하거나(더 이상의 입력 문자를 읽을 수 없는 경우) matching failure가 발생하면(입력된 문자가 포맷 스트링과 맞지 않는 경우) 조기에 리턴한다. (C99에서는, input failure는 encoding error - multibyte character를 읽으려는 시도를 했지만 입력 문자에 multibyte character가 없는 경우)로 인해서 발생할 수도 있다) 두 함수는 모두 읽어들이고 객체에 할당한 데이터 아이템의 갯수를 리턴한다.


scanf의 리턴 값을 테스트하는 반복문은 흔하게 쓰인다. 다음 while 문은 정수를 하나씩 읽어들이고, 첫번째로 문제가 발생하는 지점에서 멈춘다.

while (scanf("%d", &i) == 1) {

    ...

}


...scanf Format Strings

...scanf 함수들은 ...printf 함수들과 비슷하지만, 작동 방식은 꽤 다르다. scanf와 fscanf를 "pattern-matching" 함수로 생각하면 도움이 된다. 포맷 스트링은 ...scanf 함수가 입력을 읽어들여서 매치하려고 시도할 패턴을 나타낸다. 만약 입력이 포맷 스트링과 매치되지 않으면, 그 mismatch를 찾자마자 바로 함수는 리턴한다. 그리고 매치되지 않은 문자는 나중에 읽을 함수를 위해 "pushed back"된다.


...scanf의 포맷 스트링은 세가지를 포함할 수 있다.

Conversion specifications. ...printf 함수의 것과 닮아 있다. 대부분의 conversion specification은 입력 아이템의 처음에 오는 white-space 문자를 무시한다(예외는 %[, %c, %n이다). conversion specification들은 뒤따르는 white-space 문자는 절대로 무시하지 않는다. 만약 입력 값의 앞에 공백, 123, 개행 문자가 오면 숫자 앞에 오는 공백은 무시되지만 개행 문자는 읽지 않은 채로 남긴다.

White-space characters. 하나 이상의 연속적인 white-space 문자들은 입력 스트림에 있는 0개 또는 그 이상의 white-space character들과 매치된다.

Non-white-space characters. %를 제외한 white-space가 아닌 문자들은 입력 스트림에서 동일한 문자와 매치된다.


...scanf Conversion Specifications

...printf 함수의 경우보다 약간 더 간단하다. ...scanf 함수의 conversion specification은 % 문자 뒤에 다음 항목을 포함한다(순서대로)


* (optional). * 문자가 있다면 assignment suppression을 나타낸다. 입력 아이템을 읽어들이기는 하지만 객체에는 할당되지 않는다. *을 사용한 매치된 아이템은 리턴 값에 포함되지 않는다.

Maximum field width(optional). 필드 최대 너비는 입력되는 값의 글자 수를 제한한다. 이 숫자에 도달하면 아이템의 변환이 종료된다. 변환의 처음 부분에서 스킵된 white-space 문자는 포함되지 않는다.

Length modifier(optional). length modifier가 있다면 객체의 형식이 특정 conversion specification의 보통 형식보다 길거나 짧은 형식이라는 뜻이다.


Conversion specifier. 다음 표의 문자들 중 하나가 되어야 한다.

수치 데이터 아이템은 항상 부호와 함께 시작할 수 있다. o, u, x, X specifier는 이런 아이템들을 unsigned 형태로 변환하므로(unsigned char를 입력받는 데 -3을 입력하면, 253으로 변환됨), 보통 음수를 읽는 데에는 사용되지 않는다.

 [ specifier는 s specifier의 약간 복잡하고 더 유연한 버전이다. 전체 conversion specification은 %[set] 또는 %[^set]이 된다(set은 문자열로 구성된 임의의 집합). 단, 집합 중 ]가 포함되어 있다면 가장 먼저 와야 한다. %[set]은 집합(the scanset)의 원소들로 이루어진 모든 연속된 문자를 매치한다. %[^set] 은 집합의 여집합의 원소들로 이루어진 모든 연속된 문자를 매치한다. %[abc]는 a, b, c로 이루어진 문자열을 매치하고, %[^abc]는 a, b, c가 포함되지 않은 모든 문자열을 매치한다.

...scanf 함수의 conversion specifier 중 다수는 <stdlib.h> 내의 numeric conversion 함수들과 밀접한 관련이 있다. 이 함수들은 문자열을 동등한 수치 값으로 변환한다. 예를 들어 d specifier는 +또는 - 부호(optional)를 찾고, 연속된 10진수 숫자를 찾는다. 이는 strtol 함수가 문자열을 10진수로 변환하도록 요청받았을 때 하는 것과 정확히 똑같다.


C99 Changes to ...scanf Conversion Specifications

...printf 함수들 만큼 광범위하지는 않다.


Additional length modifiers. hh, ll, j, z, t length modifier가 추가되었다. ...printf conversion specification과 대응된다.

Additional conversion specifiers. F, a, A conversion specifier가 추가되었다. 이는 ...printf와의 대칭성 때문에 제공된다. ...scanf에서는 e, E, f, g, G와 동일하게 간주된다.

Ability to read infinity and Nan. ...printf 함수들이 infinity와 NaN을 쓸 수 있는 것처럼, ...scnaf 함수들은 이 값들을 읽을 수 있다. 제대로 읽기 위해서는 printf 함수들이 쓰는 것과 같은 형태여야 하고, 대/소문자는 무시된다.

Support for wide characters. multibyte character를 읽고 변환해서 저장한다. %lc conversion specification은 하나의 또는 연속된 multibyte character를 읽는다. %ls는 multibyte character의 문자열을 읽고, 마지막에 null character를 추가한다. %l[set], %l[^set] 도 multibyte character의 문자열을 읽는다.



Detecting End-of-File and Error Conditions

void clearerr(FILE *stream);

int feof(FILE *stream);

int ferror(FILE *stream);

...scanf 함수를 사용해서 n개의 데이터 아이템을 읽어오도록 요청했을 때, 함수의 리턴 값은 n이 되어야 정상이다. 리턴이 n이 아니라면 무언가 잘못된 경우이고, 세 가지 가능성이 있다.


End-of-file. 함수가 포맷 스트링을 전부 매치하기 전에 end-of-file에 도달했다.

Read error. 함수가 스트림으로부터 문자를 읽을 수 없었다.

Matching failure. 데이터 아이템이 잘못된 포맷. 예를 들어, 포맷 스트링에서는 정수를 읽도록 지시했는데 문자를 읽은 경우.


모든 스트림에는 두 개의 indicator가 있다. 하나는 error indicator, 하나는 end-of-file indicator. 두 indicator들은 스트림이 열렸을 때 'cleared'된다. end-of-file을 만나면 end-of-file indicator가 set되고, 읽기 에러가 발생했을 때(또는 출력 스트림에서 쓰기 에러가 발생했을 때) error indicator가 set된다. 매칭 실패는 두 indicator를 바꾸지 않는다.


end-of-file indicator가 set되면, 명시적으로 clear되기 전까지 계속 같은 상태에 있다. 명시적 clear는 clearerr 함수 호출을 통해 가능하다. clearerr 호출시 end-of-file, error indicator를 모두 clear한다.

clearerr(fp);    /* clears eof and error indicators for fp */

feof, ferror 함수는 각각 end-of-file indicator와 error indicator가 set되었는지를 알려주는 함수이다. set되었으면 nonzero value를 리턴.

만약 scanf 함수가 예상한 값보다 더 적은 값을 리턴했을 때, feof와 ferror 함수를 호출해서 그 이유를 판별한다.



int file_int(const char *filename)
{
    FILE *fp = fopen(filename, "r");
    int n;

    if (fp == NULL)
        return -1;  /* can't open file */

    while (fscanf(fp, "%d", &n) != 1) {
        if (ferror(fp)) {
            fclose(fp);
            return -2;          /* read error */
        }
        if (feof(fp)) {
            fclose(fp);
            return -3;          /* integer not found */
        }
        fscanf(fp, "%*[\^n]");  /* skips rest of line */
    }

    fclose(fp);
    return n;
}


find_int는 파일에서 정수를 읽는 시도를 한다. 만약 시도가 실패하면(fscanf가 1이 아닌 다른 값을 리턴하면) ferror와 feof를 호출해서 문제가 read error인지 eof 에러인지 찾는다. 둘 다 아닌 경우에는 매칭 에러로 fscanf가 실패한 것이다.

conversion %*[^\n]은 다음 개행문자를 만날 때까지 모든 글자들을 스킵하고, 정수 읽기를 다시 시작한다.

+ Recent posts