프로그램에게 할 일을 말할 수 없거나, 프로그램이 자기가 처리한 일을 사용자에게 알려줄 수 없다면, 쓸모있는 프로그램이라고 말할 수 없을 것입니다. 따라서 거의 모든 프로그램은 입출력(I/O)을 처리한다고 말할 수 있습니다. C 언어에서 I/O는 라이브러리 함수를 통해서 제공됩니다 -- 표준 I/O 또는 “stdio” 라이브러리12.1 -- 그리고 이 함수들은 가장 많이 쓰이는 C 라이브러리 중의 하나입니다.
C 언어의 minimalist philosophy에 동조하기 위해, stdio 함수들은 간단한, 그러나 꼭 필요한(least-common-denominator) I/O 모델입니다. 여러분은 파일을 열고(open), 읽고(read), 쓸(write) 수 있습니다. 파일은 연속적인 문자의 스트림(sequential character stream)으로 취급되며, 찾기(seeking12.2)가 가능합니다. 필요하다면 파일을 텍스트(text)와 이진(binary) 로 구별하여 처리할 수 있으며, 파일에 문자열을 써서 이름을 지어줄 수 있습니다; 파일 이름에 관한 자세한 것은 OS에 따라 달라집니다. 파일 이름을 지을 때를 제외하고 디렉토리에 관한 개념은 C 언어에 없습니다. 즉, 파일 이름을 지정할 때 디렉토리 이름을 쓸 수 있지만, 디렉토리 자체를 취급하는 일, 디렉토리를 만들거나, 디렉토리를 나열하는 일을 처리하는 표준 방법은 없습니다 (19 절을 참고하기 바랍니다). 모든 프로그램은 처음 시작할 때 자동적으로 세 개의 predefined I/O 스트림이 열려지게 됩니다: 여러분은 대개 키보드로 연결된 stdin에서 읽을(read) 수 있고, 대개 스크린으로 연결된 stdout이나 stderr를 써서 쓸(write) 수 있습니다. 그렇지만 키보드나 스크린과 관련된 기능은 매우 적습니다 (다시 19 절을 참고하기 바랍니다).
이 절의 많은 부분이 printf (질문 12.6에서 12.11까지)와 scanf (질문 12.12에서 12.20까지)에 연관되어 있습니다. 질문 12.21에서 12.26은 다른 stdio 함수들을 설명합니다. 특정 파일을 access할 필요가 있다면, fopen (질문 12.27부터 12.32까지)을 써서 직접 열어서(open) 쓰거나(use), 표준 stream을 redirect해서 (질문 12.33에서 12.36까지) 쓸 수 있습니다. text I/O를 하려는 마음이 없다면, “binary” stream을 쓸 수 (질문 12.37에서 12.42까지) 있습니다. 이 모든 것을 자세히 알아보기 전에 먼저 간단한 것부터 알고 넘어갑시다. 다음 질문들을 읽어보기 바랍니다.
char c; while ((c = getchar()) != EOF) ...
위의 코드처럼 getchar()의 리턴값을 char에 담을 경우, 두 가지 결과를 예상할 수 있습니다.
'\377'
또는 '\xff'
) EOF와 같아집니다. 따라서 입력 도중에
입력 처리가 끝나버릴 수 있습니다.
이 버그는 char가 signed이고, 입력이 모두 7 비트 문자인 경우, 발견하기가 매우 힘듭니다 (char가 signed인지 unsigned인지는 implementation-defined입니다.)
while (!feof(infp)) { fgets(buf, MAXLINE, infp); fputs(buf, outfp); }
while (fgets(buf, MAXLINE, infp) != NULL) fputs(buf, outfp);
대개 feof()를 직접 쓸 이유는 없습니다 ( stdio 함수가 EOF나 NULL을 리턴한 경우, feof()나 ferror()를 써서 정말로 파일의 끝인지, 읽기(read) 에러인지 검사할 필요가 있긴 합니다.)
이런 경우를 제외하고, feof()를 직접 쓰는 경우는 매우 드뭅니다. 따라서 일반적으로 다음과 같은 코드보다:
while (!feof(fp)) fgets(line, max, fp);
다음과 같은 코드를 쓰기 바랍니다:
while (fgets(line, max, fp) != NULL) ...;
즉, feof()는, end-of-file인지 아닌지 검사하기 위해 쓰는 함수가 아니라, end-of-file과 error인 상황을 구별하기 위해 쓰는 함수라고 기억하는 것이 좋습니다. (물론 이 목적으로, ferror()를 대신 쓸 수 있습니다.)
\n
으로
끝나지 않는 경우), fflush(stdout)을 불러 주는 것이 좋습니다.
대개의 메커니즘이 적절한 시기에 자동으로 fflush()를 불러주지만,
이 메커니즘들은 stdout이 interactive한 터미널로 연결되어 있다고
가정한 것이기 때문에, 가끔
제대로 동작하지 않을 수 있습니다. (덧붙여 질문
12.24도 참고하시기 바랍니다.)
\%
'를 출력할 수 있을까요? \%
를
써 보았지만 출력되지 않더군요.
%%
를 쓰면 됩니다.
% 문자를 출력하는 것이 까다로운 이유는, printf()가
% 문자를 escape 문자로 쓰기 때문입니다. 아시다시피,
printf는 % 문자를 보면, 다음 글자를 읽어서 어떤
값을 출력해야 하는지 조사하게 됩니다. 이때, 다음 글자로
%를 쓰면 -- 즉 %%를 쓰면 -- `%'를 출력하게
됩니다.
\%
는 동작하지 않습니다. 이 방법이 왜 동작하지 않는지
이해하려면, 먼저 일단 백 슬래시(`\
')는 컴파일러의 escape
문자이며, 컴파일러가 여러분의 코드를 이해하는 길을 보여주는
목적으로 쓰인다는 것을 아셔야 합니다. 컴파일러 입장에서는
\%
는 정의되지 않은 escape 문자이며, 대개 `%' 한 글자를
의미하게 됩니다. 따라서 만약 printf()가 `\
'를 특별하게
처리할 수 있도록 만들어져 있다 하더라도, 컴파일러가 이를 먼저
처리해 버리게 되므로, `%'를 출력할 수 없게 됩니다.
덧붙여 질문 8.8, 19.17도 참고하시기 바랍니다.
long int n = 123456; printf("%d\n", n);
scanf는 인자로 포인터를 받기 때문에 그러한 promotion이 일어나지 않습니다. 따라서 완전히 다른 상황입니다. (포인터를 써서) float에 값을 저장하는 것은 double에 값을 저장하는 것과 다릅니다. 따라서 scanf는 %f와 %lf로서 구별합니다.
아래 표에서는 printf와 scanf에서 쓰이는, 각각의 타입에 따른 format specifier를 보여줍니다.
Format | printf | scanf |
%c | int | char * |
%d, %i | int | int * |
%o, %u, %x | unsigned int | unsigned int * |
%ld, %li | long int | long int * |
%lo, %lu, %lx | unsigned long int | unsigned long int * |
%hd, %hi | int | short int * |
%ho, %hu, %hx | unsigned int | unsigned short int * |
%e, %f, %g | double | float * |
%le, %lf, %lg | n/a | double * |
%s | char * | char * |
%[...] | n/a | char * |
%p | void * | void ** |
%n | int * | int * |
%% | none | none |
size_t
와 같은, 즉, 그 크기가 long인지,
다른 것인지 알 수 없는 typedef 타입을 출력할 때에는
어떤 포맷을 써야 하나요?
printf("%lu", (unsigned long)sizeof(thetype));
printf("%*d", width, x)
를 쓰면 됩니다.
Format specifier에서 별표는 필드 폭을 지정하기 위해 int 값이
인자로 들어온다는 것을 뜻합니다. (인자 목록에서 폭은 출력할 값보다
먼저 지정합니다.) 덧붙여 질문
12.15도 참고하시기 바랍니다.
<locale.h>
는 이러한 연산을 수행하기 위한 간단한 기능을
제공합니다. 그러나 이런 일을 하기 위한 표준 루틴은 없습니다.
(printf()가 할 수 있는 일은, locale 설정에 따라 소수점을 찍는 것
뿐입니다.12.7.
Locale 설정에 따른, 콤마로 구분되는 수치를 출력하기 위한, 간단한 함수를 제공합니다:
#include <locale.h> char * commaprint(unsigned long n) { static int comma = '\0'; static char retbuf[30]; char *p = &retbuf[sizeof(retbuf) - 1]; int i = 0; if (comma == '\0') { struct lconv *lcp = localeconv(); if (lcp != NULL) { if (lcp->thousands_sep != NULL && *lcp->thousands_sep != '\0') comma = *lcp->thousands_sep; else comma = '.'; } } *p = '\0'; do { if (i % 3 == 0 && i != 0) *-p = comma; *-p = '0' + n % 10; n /= 10; i++; } while (n != 0); return p; }
좀 더 나은 방법은 세개의 숫자로 구별된다고 가정하지 말고, lconv 구조체의 grouping 필드를 쓰는 것입니다. retbuf의 적당한 크기는 다음과 같습니다:
4 * (sizeof(long) * CHAR_BIT + 2) / 3 / 3 + 1질문 12.21을 참고하기 바랍니다.
<inttypes.h>
를 포함시키면 다음과 같은 매크로를
쓸 수 있습니다. 모든 매크로는 문자열 상수(character string literal)로
치환됩니다:
intN_t | int_leastN_t | int_fastN_t | intmax_t | intptr_t |
PRIdN | PRIdLEASTN | PRIdFASTN | PRIdMAX | PRIdPTR |
PRIiN | PRIiLEASTN | PRIiFASTN | PRIiMAX | PRIiPTR |
uintN_t | uint_leastN_t | uint_fastN_t | uintmax_t | uintptr_t |
PRIoN | PRIoLEASTN | PRIoFASTN | PRIoMAX | PRIoPTR |
PRIuN | PRIuLEASTN | PRIuFASTN | PRIuMAX | PRIuPTR |
PRIxN | PRIxLEASTN | PRIxFASTN | PRIxMAX | PRIxPTR |
PRIXN | PRIXLEASTN | PRIXFASTN | PRIXMAX | PRIXPTR |
모든 매크로는 PRI로 시작하며, printf() 계열의 함수에서 쓸 수 있는, %d, %i, %o, %u, %x, %X 포맷을 의미하는 글자인 d, i, o, u, x, X가 나온 다음, 각각의 타입을 대문자로 나타낸 이름이 나옵니다.
즉, uint_fast16_t를 썼고, %o를 쓴 효과를 얻으려면, PRIoFAST16을 쓰면 됩니다. 마찬가지로, 가장 큰 unsigned 타입의 정수값을 저장하고 싶다면 uintmax_t 타입의 오브젝트를 쓰면 됩니다. 예를 들면 다음과 같습니다:
#include <inttypes.h> #include <stdio.h> int main(void) { uint_fast16_t foo; uintmax_t i = UINTMAX_MAX; ... fprintf(stderr, "foo is %" PRIoFAST16 ".\n", foo); printf("The largest integer value is %020" PRIxMAX "\n", i); return 0; }
덧붙여 질문 1.1, 11.I, 12.B도 참고하시기 바랍니다.
scanf("%d", i)
는 동작하지 않나요?
scanf("%d", &i)
로 써야 합니다.
double d; scanf("%f", &d);
short int s; scanf("%d", &s);
#define WIDTH 3 #define Str(x) #x #define Xstr(x) Str(x) /* 질문 11.17 참고 */ scanf("%" Xstr(WIDTH) "d", &n);
폭의 크기가 실행 시간에만 알 수 있다면, format string을 실행 시간에 만들어야 할 것입니다:
char fmt[10]; sprintf(fmt, "%%%dd", width); scanf(fmt, &n);
이렇게 만든 scanf format은 표준 입력에서 읽을 때보다는, fscanf나 sscanf에서 쓸모있을 것입니다. 덧붙여 질문 11.17, 12.10도 참고하시기 바랍니다.
1234ABC5.678
"%d%3s%f"
를 써서
읽을 수 있습니다. (질문
12.19의 마지막 예를 참고하기 바랍니다.)
아래는 10개의 실수를 배열로 복사하는 간단한 예제입니다:
#include <stdlib.h> #define MAXARGS 10 char *av[MAXARGS]; int ac, i; double array[MAXARGS]; ac = makeargv(line, av, MAXARGS); for (i = 0; i < ac; i++) array[i] = atof(av[i]);(makeargv의 정의는 질문 13.6을 참고하기 바랍니다.)
가능하다면 데이터 파일의 형식과 입력 형식을 복잡한 파싱 과정이 필요없게 디자인하고, 첫번째 또는 두번째 방법을 써서 분석할 수 있도록 만드는 것이 좋습니다.
<inttypes.h>
를 포함시키면 다음과 같은 매크로를
쓸 수 있습니다. 모든 매크로는 문자열 상수(character string literal)로
치환됩니다:
intN_t | int_leastN_t | int_fastN_t | intmax_t | intptr_t |
SCNdN | SCNdLEASTN | SCNdFASTN | SCNdMAX | SCNdPTR |
SCNiN | SCNiLEASTN | SCNiFASTN | SCNiMAX | SCNiPTR |
uintN_t | uint_leastN_t | uint_fastN_t | uintmax_t | uintptr_t |
SCNoN | SCNoLEASTN | SCNoFASTN | SCNoMAX | SCNoPTR |
SCNuN | SCNuLEASTN | SCNuFASTN | SCNuMAX | SCNuPTR |
SCNxN | SCNxLEASTN | SCNxFASTN | SCNxMAX | SCNxPTR |
SCNXN | SCNXLEASTN | SCNXFASTN | SCNXMAX | SCNXPTR |
위 매크로들은 질문 12.A에서 설명한 것과 동일한 규칙을 가지며, PRI 대신 SCN으로 시작한다는 것만 다릅니다.
#include <inttypes.h> #include <stdio.h> main(void) { uint_fast16_t foo; uintmax_t i = UINTMAX_MAX; sscanf("123", "%" SCNuFAST16, &foo); printf("foo is %" PRIoFAST16 ".\n", foo); return 0; }
덧붙여 질문 1.1, 11.I, 12.A도 참고하시기 바랍니다.
scanf("%d\n", ...)
을 썼더니, 추가적인 줄을 입력하기 전까지는
멈춰버립니다. 왜 그럴까요?
\n
은 newline에 해당하는
것이 아니고,
공백(whitespace) 문자가 나오는 동안, 입력을 읽어서 무시하라는
뜻입니다.
(사실, scanf format string에 나오는 어떠한(any) 공백 문자라도,
공백 문자를 읽어서 무시하라는 뜻입니다. 게다가 %d와 같은
format도 역시 공백 문자를 무시합니다. 따라서 일반적으로 여러분이
공백 문자를 scanf format string에 쓸 이유는 없습니다.
따라서 "%d\n"
의
\n
은 scanf가 공백 문자가 아닌 문자가 나올 때 까지
읽으라는 뜻이 되고, 따라서 공백 문자가 아닌 문자를 찾기 위해
다음 줄까지 읽어 버리는 상황이 된 것입니다.
이 경우 단순히 "%d"
만
쓰면 해결됩니다. (물론 여러분의 프로그램은 수치를 입력받은 다음에
나오는 \n
을 처리해야 합니다. 질문
12.18을 참고하기 바랍니다.)
scanf 함수는 free-format input을 처리하기 위해 디자인되었습니다.
따라서 키보드에서 사용자에게 입력받기 위한 목적으로 쓰이는 경우는
거의 없습니다. “Free format”은 scanf가 \n
를 다른
공백문자와 똑같이 취급한다는 것을 뜻합니다.
따라서 "%d %d %d"
format은
입력이
1 2 3인 경우나
1 2 3인 경우 모두 쓸 수 있습니다.
(다른 언어들과 비교해 보면, C, Pascal, 그리고 LISP이 free-format이며, traditional BASIC과 FORTRAN이 free-format이 아닙니다.)
여러분이 꼭 scanf가 newline을 다르게 처리하게 만들고 싶다면, “scanset” directive를 쓰면 됩니다:
scanf("%d%*[\n]", &n);Scanset은 강력하기는 하지만, scanf의 모든 문제를 해결해 주지는 않습니다. 덧붙여 질문 12.20도 참고하시기 바랍니다.
scanf("%d", ...)
를 써서 여러 수치를 읽은 다음,
gets()를 써서 문자열을 읽으려 합니다:
int n; char str[80]; printf("enter a number: "); scanf("%d", &n); printf("enter a string: "); gets(str); printf("You typed %d and \"%s\"\n", n, str);
그런데 컴파일러는 gets() 호출을 무시합니다! 왜 그런가요?
42 a string이 때 scanf는 42를 읽지만, 그 뒤에 나오는 newline을 읽지 않습니다. 이 newline은 input stream에 남아 있고, gets()가 바로 이 newline을 읽게 됩니다. gets()는 newline이 나올 때까지의 입력을 문자열로 리턴해 주므로, 바로 빈 줄을 리턴하게 됩니다. 따라서 입력의 두번째 줄, “a string”은 아예 읽히지 않습니다. 만약 여러분이 다음과 같이 입력을 주었다면:
42 a string프로그램이 예상한 대로 동작할 것입니다.
일반적으로 scanf() 다음에 바로 gets()과 같은 다른 입력 루틴을 쓰는 것은 바람직하지 않습니다. scanf()의 이런 이상한(peculiar) 방식은 항상 문제를 일으킬 소지가 있기 때문입니다. 따라서 전체 입력을 모두 scanf로 받거나 아예 scanf를 쓰지 않는 것이 좋습니다.
덧붙여 질문 12.20, 12.23도 참고하시기 바랍니다.
int n; while (1) { printf("enter a number: "); if (scanf("%d", &n) == 1) break; printf("try again: "); } printf("You typed %d\n", n);그런데, 때때로 무한 루프에 빠지는 경우가 있더군요.12.9 왜 그럴까요?
왜 scanf가 처리할 수 없는 문자들을 input stream에 그대로 두는지 궁금하게 생각하실 것입니다. 여러분이 공백으로 구분되지 않은, 다음과 같은 입력을 처리해야 한다고 생각해보시기 바랍니다:
123CODE이 데이터를 scanf를 써서 처리하려 한다면,
"%d%s"
를
쓸 것입니다. 이 때 scanf가 매치되지 않은 문자를 input stream에
놓아 두지 않고 제거한다면, 여러분이 받은 데이터는 "CODE"
가
아닌 "ODE"
가 될 것입니다.
(The problem is a standard one in lexical analysis: When scanning
an arbitrary-length numeric constant or alphanumeric identifier,
you never know where it ends until you've read “too far.”
This is one reason that ungetc exists.)
일반적으로, scanf()는 아주 형식화된(relatively structured, formatted) input을 위해 design된 것입니다. (이름부터 “scan formatted”의 약자입니다). 따라서 주의깊게 사용하면 작업이 실패했는지의 여부는 알려 주지만, 왜, 어떻게 작업이 실패했는지는 알려주지 않습니다. 따라서 scanf()에서 에러가 발생했다면, 이를 처리하기가 거의 불가능합니다.
User input은 가장 구조화되지 않은 input이라 할 수 있습니다. 잘 디자인된 user interface는 사용자가 어떤 문자라도 입력할 수 있다는 것을 염두에 두어야 합니다 -- 숫자가 필요한 경우에 문자나 구두점을 입력하는 단순한 경우를 포함해서, 사용자가 그냥 Return 키만 친다거나, 바로 EOF를 입력한다거나 하는 모든 경우를 말합니다. 이런 모든 경우를 scanf로 처리한 다는 것은 거의 불가능합니다; (fgets()와 같은 함수를 써서) 줄 전체를 입력 받은 다음, 이를 sscanf와 같은 함수를 써서 원하는 형태로 끊어내는(strtol, strtok, atoi와 같은 함수를 써서) 방법을 쓰는 것이 좋습니다; 덧붙여 질문 12.16, 13.6도 참고하시기 바랍니다.) sscanf를 쓴다면, 리턴 값을 검사해서 원하는 만큼 입력을 처리했는지 확인하는 것이 중요합니다.
scanf의 이런 단점은 scanf에만 적용되는 것이지, fscanf나 sscanf와는 상관없다는 점을 아셔야 합니다. 문제가 되는 것은 표준 입력이 키보드로 연결되어 있고, scanf는 표준 입력을 처리하게 되어 있어서, least constrained인 사용자 입력을 scanf가 처리해버린다는 것입니다. 데이터 파일이 formatted인 경우, fscanf를 쓰는 것은 좋습니다. sscanf를 써서 줄을 parsing하는 것은 (리턴값을 검사하는 경우에) 매우 좋습니다. 왜냐하면, 입력을 취소하거나, 다시 입력을 검사하는 등 여러가지 작업을 할 수 있기 때문입니다.12.11
sprintf (buf, "You typed \"%s\"", answer);이 때의 버퍼 크기는 다음과 같이 계산합니다:
int bufsize = 13 + strlen (answer);정수에 대해서는 %d의 출력 결과에 필요한 문자 갯수가 다음 식을 넘지 않습니다:
((sizeof (int) * CHAR_BIT + 2) / 3 + 1) /* +1 for '-' */(
CHAR_BIT
는 <limits.h>
에 정의되어
있음) 그러나 이런 식으로 매번 계산하는 것은 지나치게 조심스러운
것12.12입니다. (위의 식은 수치를 8 진수로
표기한다고 가정했을 때의 계산 결과입니다. 10 진법을 쓴다면 위의 계산
결과와 같거나, 그보다 작은 공간을 차지할 수 있습니다.)
포맷 문자열이 매우 복잡하거나 런타임이 되기 전에는 알 수 없다면 버퍼 크기를 예측하는 것은 sprinf()를 새로 만드는 것처럼 어려운 일입니다. 그리고 복잡한 만큼 에러가 발생하기 쉽습니다 (그래서 권장하지 않습니다). 마지막 방법은 일단 그 포맷과 같은 포맷을 fprintf()를 써서 임시 파일에 저장한 다음, fprintf()의 리턴값을 받거나 그 파일의 크기를 조사하는 방법입니다 (그러나 질문 19.12를 꼭 읽기 바랍니다. 그리고 출력 에러를 생각해야 합니다).
주어진 버퍼가 크지 않을 경우, 오버플로우가 일어나지 않는다는 확실한 보장없이는 sprintf()를 부르기 꺼릴 것입니다. 포맷 스트링을 알 수 있다면 %s를 쓰는 대신 %.Ns(이 때 N은 어떤 수치)를 쓰거나 %.*s를 (덧붙여 질문 12.10도 참고하시기 바랍니다.) 쓸 수 있습니다.
가장 확실한 오버플로우 방지책은 길이를 제한하는 snprintf()를 쓰는 것입니다. 실행 방법은 다음과 같습니다:
snprintf(buf, bufsize, "You typed \"%s\"", answer);
snprintf()는 많은 stdio 라이브러리에서 (GNU와 4.4BSD 포함) 제공되고 있습니다. 그리고 이 함수는 [C9X] 표준에서 채택될 예정입니다.
[C9X] snprintf()를 쓸 수 있다면 배열의 크기를 미리 계산할 수 있습니다. [C9X] snprintf()는 버퍼에 기록한 문자의 수를 리턴하는 것이 아니라, 버퍼에 기록할 문자의 수를 리턴합니다. 게다가 이때 버퍼의 크기에 0, 또는 버퍼에 널 포인터를 전달할 수 있습니다. 그러므로 다음과 같은 호출을 사용하면:
nch = snprintf (NULL, 0, fmtstring, /* 다른 인자들 */);주어진 포맷 스트링(fmtstring)에 필요한 버퍼의 크기를 계산할 수 있습니다.
fgets와 gets의 다른 점 하나는, fgets는 문자열 마지막에
\n
을 제거하지 않는다는 것입니다. 그러나 이 newline을 없애기는
아주 쉽습니다. 질문
7.1을 보면 gets 대신 fgets()를
사용한 코드를 볼 수 있습니다.
줄의 길이에 상관없이 한 줄을 완벽하게 읽을 수 있는 getline 함수는 질문 21.6을 참고하기 바랍니다.
errno = 0; printf("This\n"); printf("is\n"); printf("a\n"); printf("test.\n"); if (errno != 0) fprintf(stderr, "printf failed: %s\n", strerror(errno));
그런데 왜 “printf failed: Not a typewriter”라는 이상한 메시지를 출력할까요?
fpos_t
를
사용합니다.
따라서 올바르게 만들어졌다면 아무리 큰 파일이더라도 offset을
표현할 수 있습니다.
이와는 달리 ftell과 fseek는 long int를 쓰기 때문에,
offset의 범위가 long int의 범위로 제한됩니다.
(long int type은 보다 큰 수를 가진다고 보장할 수
없습니다. 따라서 파일의 옵셋이 20억 바이트
()로 제한됩니다.
또한 fgetpos/fsetpos는
다중바이트(multibyte) 스트림에도 쓸 수 있습니다. 덧붙여 질문
1.4도 참고하시기 바랍니다.
아직 읽지 않은 문자(unread characters)들을 stdio input stream에서 무시하는(discard) 표준 방법은 존재하지 않습니다. 어떤 컴파일러 회사들은 fflush(stdin)이 읽지 않은 문자들을 취소할 수 있도록 라이브러리를 제공하기도 하지만, 이식성이 뛰어난 프로그램을 만들려면 절대로 써서는 안되는 기능입니다. (어떤 stdio 라이브러리는 fpurge나 fabort를 같은 기능으로 제공하기도 하지만, 마찬가지로 표준은 아닙니다.) 또한 input buffer를 flush하는 것이 꼭 해결책이라고 할 수도 없습니다. 왜냐하면 일반적으로 아직 읽히지 않은 입력은 OS-level input buffer에 존재하기 때문입니다.
Input을 flush할 방법이 필요하다면 (질문 19.1과 19.2에 나온 것처럼) 시스템 의존적인 방법을 찾아야 할 것입니다. 게다가 사용자가 매우 빠른 속도로 입력하고 있고, 여러분의 프로그램이 그 입력을 무시해버릴 수 있다는 것을 꼭 염두에 두어야 합니다.
또는 \n
이 나오기 전까지 문자를 읽어서 무시하거나,
curses에서 제공하는 루틴인 flushinp()를 쓰면 됩니다.
덧붙여 질문
19.1,
19.2도 참고하시기 바랍니다.
myfopen(char *filename, FILE *fp) { fp = fopen(filename, "r"); }
그런데 다음과 같이 호출하고 나면:
FILE *infp; myfopen("filename.dat", infp);
infp 변수가 제대로 설정(setting)되지 않습니다. 왜 그럴까요?
주어진 질문을 고치려면 다음과 같이 myfopen 함수가 FILE *를 리턴하도록 고치면 됩니다:
FILE * myfopen(char *filename) { FILE *fp = fopen(filename, "r"); return fp; }
그 다음 다음과 같이 씁니다.
FILE *infp; infp = myfopen("filename.dat");
또는 다음과 같이 myfopen이 FILE *에 대한 포인터12.16를 인자로 받게하면 됩니다:
myfopen(char *filename, FILE **fpp) { FILE *fp = fopen(filename, "r"); *fpp = fp; }
그리고 다음과 같이 씁니다:
FILE *infp; myfopen("filename.dat", &infp);
FILE *fp = fopen(filename, 'r');
"r"
과 같은
string이어야지, 'r'
과 같은 문자가 아니라는 것입니다.
덧붙여 질문
8.1도 참고하시기 바랍니다.
fopen("c:\newdir\file.dat", "r");
"r+"
모드로 파일을 연 다음,
원하는 문자열을 읽고 다시 이 문자를 쓰려고 했으나 제대로 동작하지
않습니다.
"+"
모드로 연 파일에서 읽기(reading)나
쓰기(writing)을 한 다음에는 반드시 fseek나 fflush를 써 주어야
합니다.
또 덮어쓰기 위해서는 기존의 문자열과 덮어 쓸 문자열의 길이가 같아야
합니다; 특정 위치에 문자들을 추가하거나(insert) 지우는(delete) 방법은
존재하지 않습니다.
텍스트 파일에서 덮어쓰는 작업을 하면, 파일을 덮어쓴 위치까지
잘려버릴 수도
있습니다12.17.
덧붙여 질문
19.14도 참고하시기 바랍니다.
freopen(file, "w", stdout); f();
FILE *ofp;
printf(...)를 전부 fprintf(ofp, ...)로 바꿉니다. (물론, putchar나 puts를 썼다면 이 함수들도 바꾸어야 할 것입니다.) 그런 다음, ofp가 stdout를 가리키도록 하는 등의 작업을 하면 됩니다.
혹시 다음과 같은 코드로 freopen을 대신할 수 있다고 생각하실지도 모르겠습니다:
FILE *savestdout = stdout; stdout = fopen(file, "w"); /* WRONG */
그 다음, 아래 코드로 stdout을 복원할 수 있다고 생각하실지도 모르겠습니다:
stdout = savestdout; /* WRONG */
이러한 코드는 동작한다고 말하기 어렵습니다. 왜냐하면 stdout은 (또 stdin, stderr) 보통 상수(constant)이기 때문에 다른 값으로 assign할 수 없기 때문입니다. (그렇기 때문에 freopen이라는 함수가 제공되는 것입니다.)
원래의 스트림에 관한 정보를 freopen()을 쓰기 전에 복사해 두었다가 복원시키는 방법도 있지만, 이는 시스템 의존적인, dup()과 같은 함수를 쓰기 때문에 이식성이 (portability) 떨어지며, 비안정적입니다 (unreliable.)
어떤 시스템에서는 controlling terminal을 (질문 12.36 참고) 직접 open하는 방법을 제공하기도 하지만, 이는 여러분이 원하는 것이 아닐 것입니다. 왜냐하면 원래의 input 또는 output이 (즉 freopen을 부르기 전의 stdin이나 stdout의 값) command line에서 redirect되어 들어올 수 있기 때문입니다.
어떤 subprogram을 실행시키고 그 결과를 capture하고 싶다면, freopen은 제대로 동작할 수 없습니다; 질문 19.30을 대신 참고하기 바랍니다.
<
”나
“>
”을 썼는지 확인할 수 있을까요?
"rb"
나 "wb"
와 같은)
"b"
modifier를 썼는지 (질문
12.38 참고) 확인하기 바랍니다.
그 다음, &와 sizeof operator를 써서 몇 바이트를 transfer할 것인지 확인하기 바랍니다. 보통 fread나 fwrite같은 함수를 써서 작업할 수 있습니다; 예제는 질문 2.11을 보기 바랍니다.
fread나 fwrite 자체가 binary I/O를 수행하는 함수는 아닙니다. 일단 binary mode로 파일을 열었다면, 어떤 I/O 함수라도 binary I/O를 할 수 있습니다 (질문 12.42의 예제 참고); Text mode로 파일을 열었다해도 원한다면 fread나 fwrite를 쓸 수 있습니다.
마지막으로, binary data file은 portable하지 않습니다; 질문 20.5 참고. 덧붙여 질문 12.40도 참고하시기 바랍니다.
"rb"
mode를 써서 텍스트 파일 변환(text file translation)이
일어나지 않도록 해야 합니다.
비슷하게, 바이너리 데이터를 쓰기 위해서는 "wb"
를 써야 합니다.
(UNIX와 같이 text와 binary 파일을 구별하지 않는 운영체제에서는
"b"
를 쓸 필요는 없지만, 썼다고 하더라도 문제될 것은 없습니다.)
텍스트/바이너리의 차이는 파일을 열 때에만 적용됩니다. 일단 파일이 열린 다음에는 어떤 I/O 루틴을 써도 상관없습니다. 덧붙여 질문 12.40, 12.42, 20.5도 참고하시기 바랍니다.
\n
문자로 바꾸어서 처리합니다. 따라서 C 프로그램에서
OS에서 사용하는 end-of-line 문자 표현 방식을 신경쓸 필요가 없습니다:
C 프로그램에서 '\n'
문자를 쓰면(write), stdio 라이브러리는
이 문자를 OS에서 사용하는 end-of-line 문자 표현 방식으로 바꾸어
쓰며(write), 읽을 때에는 OS가 사용하는 end-of-line 문자 표현 방식을
보면 단순히 \n
을 프로그램에 돌려줍니다.12.18
이와는 달리 binary mode에서는 프로그램에서 읽고 쓰는(write) 바이트와 파일 저장 방식과 완전히 같습니다. 즉 어떠한 변환도 이루어지지 않습니다. (MS-DOS 시스템에서는 control-Z를 end-of-file 문자로 해석하는 것도 이루어지지 않습니다.)
Text mode translation은 파일의 크기에도 영향을 끼칩니다. 왜냐하면 프로그램에서 처리하는 문자가 file에 저장될 문자와 1:1로 match될 필요가 없기 때문입니다. 같은 이유에서 fseek나 ftell이 파일에서 정확한 byte offset을 나타낼 이유도 없습니다. (엄격히 말해서 text mode에서는 fseek나 ftell이 리턴하는 값을 프로그래머가 직접 해석하려고 하면 안됩니다: ftell이 리턴하는 값은 fseek의 인자로만 써야 하며, 또한 ftell이 리턴하는 값만이 fseek의 인자로 쓰여야 합니다.)
Binary mode에서는 fseek와 ftell이 byte offset을 나타냅니다. 그러나 어떤 시스템에서는 완전한 레코드를 만들기 위해 여러 개의 null byte를 파일 뒤에 추가할 수도 있습니다.
덧붙여 질문 12.37, 19.12도 참고하시기 바랍니다.
예를 들어 여러분이 문자와 32-bit integer, 16-bit integer로 이루어진 structure를 주어진 fp에서 다음 structure 안으로 읽을 필요가 있다고 가정해 봅시다:
struct mystruct { char c; long int i32; int i16; }
다음과 같은 코드를 쓰면 해결될 것입니다:
s.c = getc(fp); s.i32 = (long)getc(fp) << 24; s.i32 |= (long)getc(fp) << 16; s.i32 |= (unsigned)(getc(fp) << 8); s.i32 |= getc(fp); s.i16 = getc(fp) << 8; s.i16 |= getc(fp);
이 코드는 getc가 8-bit 문자를 읽고 데이터가 최상위 바이트가 우선하여 (most significant byte first, “big endian”) 저장되어 있다고 가정했습니다. (long) 타입으로 casting한 것은 16-bit 또는 24-bit shift 연산이 long 값에 확실히 동작하도록 (질문 3.14 참고) 보장해주며, (unsigned)로 casting한 것은 부호 확장(sign extension)을 막기 위한 것입니다. (일반적으로 이러한 코드를 작성할 때에는 모든 data type을 unsigned로 하는 것이 좋습니다. 그러나 먼저 질문 3.19를 참고하기 바랍니다.)
Structure의 내용을 파일에 쓰는(write) 코드는 다음과 같습니다:
putc(s.c, fp); putc((unsigned)((s.i32 >> 24) & 0xff), fp); putc((unsinged)((s.i32 >> 16) & 0xff), fp); putc((unsigned)((s.i32 >> 8) & 0xff), fp); putc((unsigned)(s.i32 & 0xff), fp); putc((s.i16 >> 8) & 0xff, fp); putc(s.i16 & 0xff, fp);
Seong-Kook Shin