스트림(stream)이란, 프로그램과 입출력장치 사이에서 입출력자료들을 연결시켜 주는 역할을 담당합니다.
모니터와 키보드 같은 외장기기는 프로그램과 연결되어 있지 않습니다.
따라서 이 기기들을 프로그램과 연결시켜주는 다리가 필요합니다. 이 다리 역할을 하는 매개체가 바로 '스트림'이라고 합니다.
C언어가 제공하고 있는 표준 입출력 스트림은 다음과 같습니다.
이름 | 스트림의 종류 | 입출력장치 |
---|---|---|
stdin | 표준 입력 스트림 | 키보드 대상으로 입력 |
stdout | 표준 출력 스트림 | 모니터 대상으로 출력 |
stderr | 표준 에러 스트림 | 모니터 대상으로 출력 |
스트림은 내부에 문자 배열 형태의 버퍼(buffer)라는 임시 메모리 공간을 가지고 있습니다. 이 버퍼를 이용하면 입력과 출력을 좀 더 효율적으로 처리할 수 있게 됩니다.
코딩을 하다보면 가끔 출력버퍼를 비워야할 때가 있습니다. 버퍼에 찌꺼기들이 남아 있기 때문에 정상동작을 하지 않을 수 있기 때문입니다.
그 때 쓰는 것이 바로 fflush() 함수입니다. fflush 함수는 인수로 전달된 스트림에 연결된 버퍼를 비워줍니다.
출력버퍼를 비운다는 것은 출력버퍼에 저장된 데이터를 지우는 것이 아니라, 출력버퍼에 저장된 데이터를 최종 전송함을 뜻합니다.
fflush 함수의 원형은 다음과 같습니다.
int fflush(FILE * stream); 함수 호출 성공 시 0, 실패 시 EOF 반환
EOF는 End Of File의 뜻으로, 오류가 발생했는지 또는 파일의 데이터를 모두 읽었는지 확인할 때 사용합니다. 실제로 이 값은 -1을 나타냅니다.
이번에는 입력 버퍼의 동작을 알아보겠습니다. scanf 함수로는 학번을 입력받고, fgets 함수로는 이름을 입력받는 프로그램입니다.
입력 버퍼 문제는 문자 형식을 받을 때 일어납니다. 입력 버퍼에 남아있는 문자때문에, 원하는 동작이 일어나지 않을 수 있는데 대개 엔터키의 잔여물인 \n 때문입니다.
scanf 함수처럼 형식에 맞게 입력을 받을 때 사용자의 입력 내용이 형식 문자열보다 더 많은 내용을 포함하고 있으면 버퍼에 처리되지 않은 데이터가 남게 됩니다.
|
실행결과 학번을 입력하세요 : 1234(입력) 이름을 입력하세요 : 학번 : 1234 이름 : |
학번을 입력한 뒤 엔터 키를 누르면 이름 입력 부분은 그대로 넘어가버립니다.
scanf 함수로 값을 입력하고 엔터를 입력하면 버퍼에 \n값이 남게 되는데, 남겨진 \n를 다음에 호출되는 fgets 함수가 데이터로 받아들이기 때문입니다.
이런 문제를 해결하려면 다음과 같이 입력 버퍼를 비우는 함수를 만들어줘야 합니다. 입력 버퍼에서 문자를 계속 꺼내고 \n를 꺼내면 반복을 중단하는 함수입니다.
while(getchar()!='\n');
|
실행결과 학번을 입력하세요 : 1234(입력) 이름을 입력하세요 : 김세종(입력) 학번 : 1234 이름 : 김세종 |
스트림의 종류는 다양한데 기본적으로 다음 두 가지 기준을 통해서 스트림을 구분하게 됩니다.
- 읽기 위한 스트림? 쓰기 위한 스트림?
파일에 데이터를 쓰는데 사용하는 스트림과 데이터를 읽는데 사용하는 스트림으로 구분합니다.
- 텍스트 데이터를 위한 스트림? 바이너리 데이터를 위한 스트림?
출력의 대상이 되는 데이터의 종류에 따라서 텍스트 모드와 바이너리 모드 두 가지로 나뉩니다.
스트림은 데이터 이동방향을 기준으로 다음과 같이 4가지로 구분할 수 있습니다.
데이터 READ 스트림 | 데이터 WRITE 스트림 | 데이터 APPEND 스트림 | 데이터 READ/WRITE 스트림 |
읽기만 가능 | 쓰기만 가능 | 덧붙여 쓰기만 가능 | 읽기, 쓰기 모두 가능 |
C언어에서는 이를 바탕으로 6가지로 스트림을 세분화합니다.
모드 | 스트림의 성격 | 파일이 없으면 |
r | 읽기 가능 | 에러 |
w | 쓰기 가능 | 생성 |
a | 파일에 끝에 덧붙여 쓰기 가능 | 생성 |
r+ | 읽기/쓰기 가능 | 에러 |
w+ | 읽기/쓰기 가능 | 생성 |
a+ | 읽기/덧붙여 쓰기 가능 | 생성 |
웬만하면 r, w, a를 사용하여 입력스트림과 출력스트림을 따로 생성해 사용하는 것이 좋습니다.
+가 들어간 개방 모드는 읽기에서 쓰기로, 쓰기에서 읽기로 작업을 변경할 때마다 메모리 버퍼를 비워줘야 하는 등의 불편함과 더불어 잘못된 사용의 위험성도 뒤따르기 때문입니다.
스트림의 성격은 데이터의 종류에 따라서 다음과 같이 두 가지로 나뉩니다.
텍스트 모드 스트림 | 바이너리 모드 스트림 |
문자 데이터를 저장하는 스트림 | 바이너리 데이터를 저장하는 스트림 |
텍스트 파일은 데이터를 아스키코드 또는 유니코드 값에 따라 저장한 것이며, 그 외의 방식으로 저장된 파일은 바이너리 파일입니다.
텍스트 파일은 메모장과 같은 프로그램에서 확인할 수 있으며, 바이너리 파일은 해당 기록 방식을 적용한 별도의 프로그램을 사용해야 합니다.
예로, 텍스트 파일은 메모장 프로그램에서 내용을 확인할 수 있으나 그림 파일을 보기 위해서는 그림판 프로그램을 사용해야 합니다.
텍스트 파일 | 바이너리 파일 |
---|---|
|
바이너리 파일을 전용 프로그램이 아닌 메모장에서 사용하면 |
.txt | .jpg, .png, .mp3, .exe 등 |
r, w, a, r+, w+, a+ 옵션 이외에도 개방 모드를 조합해 텍스트 모드(rt, wt, at)로 열지, 바이너리 모드(rb, wb, ab)로 열지 이렇게 옵션을 추가할 수 있습니다.
기본적으로 아무것도 입력하지 않으면 텍스트 모드로 파일에 접근합니다.
fopen 함수는 스트림을 형성할 때 호출하는 함수입니다.
스트림 파일을 만드는 것을 파일 개방이라고 하며 f는 file을 말합니다. 즉 file open이라는 함수입니다.
이 함수를 통해서 프로그램에서 파일과의 스트림을 형성할 수 있습니다.
FILE * fopen(const char * filename, const char * mode); 개방에 성공하면 FILE 구조체 변수의 주소값(포인터), 실패하면 널(NULL) 포인터 반환
개방할 파일은 현재의 작업 프로젝트 디렉토리에서 생성됩니다.
1
|
FILE * fp = fopen("sejong.txt", "w");
|
경로를 포함해서 다음과 같이 파일의 이름을 지정해도 됩니다.
1
|
FILE * fp = fopen("C:\\project\\sejong.txt", "w");
|
문자열 안의 백슬래시는 제어 문자의 시작을 뜻합니다. 디렉토리를 표시하는 백슬래시는 문자열 안에 있으므로 두 번 사용합니다.
fopen 함수가 개방에 성공하면 스트림파일을 만들고 파일포인터를 반환합니다.
스트림파일은 데이터를 저장하는 버퍼와 버퍼를 관리하는 여러 정보를 파일 구조체변수에 저장하고 있습니다. 이 구조체변수의 포인터를 파일포인터라고 합니다.
파일포인터를 포인터변수에 저장하면 입출력 준비작업이 끝난 것입니다.
1
2
|
FILE * fp;
fp = fopen("sejong.txt" , "w");
|
파일포인터를 fp라는 이름으로 생성하고, 파일을 열어서 fp가 그 위치를 기억하게 해주는 것입니다.
파일 개방에 실패하면 fopen함수는 널 포인터를 반환합니다.
널 포인터를 사용하면 실행할 때 에러가 발생하므로 반드시 개방에 성공 했는지를 검사해야 합니다.
1
2
3
4
5
|
if (fp == NULL)
{
printf("파일이 없습니다.");
return 1;
}
|
int fclose(FILE *); 성공하면 0, 오류가 발생한 경우 EOF
사용이 끝난 파일은 파일을 닫아서 스트림파일을 제거해줍니다.
파일 열기를 통해 만들어진 스트림파일은 메모리를 사용합니다. 그런데 파일을 닫아주지 않으면 메모리가 남아있게 되어, 그만큼의 자원손실을 초래합니다.
또한 스트림파일에 남아있는 데이터가 장치에 기록되기 전에 지워질 수 있으므로 사용이 끝난 파일은 닫아서 스트림파일의 데이터를 장치에 기록하는 것이 좋습니다.
개방된 파일은 프로그램이 종료되면 자동으로 닫히면서 메모리에서 제거되지만 안정성을 위해 명시적으로 닫는 것이 좋습니다.
데이터를 입출력하기 전에는 파일을 여는 과정이 필요합니다. 또한 사용이 끝나면 파일을 닫는 과정이 필요합니다.
fopen 함수는 파일을 열고, fclose 함수는 파일을 닫습니다.
실제로 파일에 데이터가 저장되는지 확인해보는 예제입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#include <stdio.h>
int main(void)
{
FILE * fp = fopen("sejong.txt", "w"); // 출력 스트림 형성
if (fp == NULL) // fp가 널 포인터면
{
printf("파일 열기 실패\n");
return -1; // 비정상적 종료를 의미하기 위해 -1 반환
}
fputc('s', fp); // fp가 지칭하는 파일 sejong.txt에 s 저장
fputc('j', fp); // fp가 지칭하는 파일 sejong.txt에 j 저장
fputc('u', fp); // fp가 지칭하는 파일 sejong.txt에 u 저장
fclose(fp); // 파일 닫기
return 0;
}
|
위의 코드를 실행하면 다음과 같이 실제로 데이터가 저장되었음을 확인할 수 있습니다.
출력전용 모드는 같은 이름의 파일이 있을 때 그 내용을 모두 삭제하고 개방하므로 주의해야 합니다.
일단 읽기전용 모드로 개방한 후에 파일 존재여부를 확인하고 다시 출력전용으로 개방할 수 있습니다.
1
2
3
4
5
|
ifp = fopen("sejong.txt", "r");
if (ifp == NULL)
{
fp = fopen("sejong.txt", "w");
}
|
Visual Studio 2013 버전 이상부터 scanf() 함수를 사용하면 에러가 나면서 컴파일이 안됩니다.
에러 메시지를 보면 C4996 'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation,
use _CRT_SECURE_NO_WARNINGS. 라고 나옵니다.
fputc 함수는 File Put Character를 조합한 글자로, 파일에서부터 글자를 하나씩 쓰는 함수입니다.
int fputc(int, FILE *) 출력한 문자, 오류가 발생한 경우는 EOF
fputc함수가 문자를 출력할 때도 fgetc함수와 마찬가지로 개방된 스트림 파일의 버퍼를 사용합니다.
한 문자를 출력할 때마다 일단 버퍼에 출력이 된 후에 개행문자가 출력되면 하드디스크의 파일로 출력됩니다.
다음은 키보드로부터 입력되는 데이터를 파일로 출력하는 예제입니다.
EOF 값은 보통 텍스트파일의 끝에서 얻을 수 있는 값이지만 키보드의 'ctrl+z'를 누르고 엔터키를 누르면 getchar 함수가 -1을 반환합니다.
|
실행결과 데이터를 입력하세요. sejong(입력) ^Z(입력종료) |
출력결과는 sejong.txt 파일을 메모장으로 열어 확인해봅니다.
fgetc 함수는 File Get Character를 조합한 글자로, 파일에서부터 글자를 하나하나 읽는다는 뜻의 함수입니다.
int fgetc(FILE *) 입력한 문자, 오류나 파일에 데이터가 없을 때 EOF
fgetc 함수는 스트림파일의 버퍼에서 데이터를 가져옵니다.
처음에는 버퍼가 비어있으므로 파일에서 버퍼 크기 만큼의 데이터를 가져와 한 번에 버퍼로 읽어들입니다.
파일의 크기가 버퍼보다 작으면 모든 데이터가 한 번에 버퍼에 저장됩니다.
그 이후에 호출되는 입력함수는 버퍼에 데이터가 없을 때까지 버퍼로부터 데이터를 입력합니다.
버퍼로부터의 입력 위치는 파일구조체의 멤버인 위치지시자로 확인합니다. 스트림파일에는 문자를 입력할 버퍼의 위치를 알려주는 지시자가 있습니다.
위치지시자는 0으로 시작하며 fgetc 함수가 한 문자씩 읽을 때 1씩 증가합니다.
위치지시자의 값이 파일의 크기와 같아지면 데이터를 모두 읽은 것이 되며, 이 때 함수는 EOF를 반환합니다.
다음 예제 코드가 데이터를 입력 받을 파일은 sejong 문자열이 저장되어 있는 "sejong.txt" 파일입니다.
개방한 파일로부터 한 문자를 입력, 읽어들인 문자를 화면에 출력을 무한 반복합니다.
원본 파일의 끝까지 도달하게 되면 반환값이 EOF가 되어 break를 사용해 while 문을 빠져 나갑니다.
|
실행결과 sejong |
fputs 함수는 문자열을 한번에 출력할 때 사용합니다.
첫 번째 인자에는 출력할 문자열의 주소값을 주고 두 번째 인자를 통해서는 파일포인터를 인수로 줍니다.
int fputs(const char *, FILE *); 출력에 성공하면 음수가 아닌 값, 실패하면 EOF
파일에 문자열을 출력하는 fputs 예제입니다.
fopen 함수를 이용해서 파일을 w 모드로 만들고 열어서, fputs 함수를 이용해 문자열 "I have a dream."을 파일에 씁니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <stdio.h>
int main(void)
{
char str[20] = "I have a dream.";
FILE * fp = fopen("fputs_test.txt", "w");
fputs(str, fp);
fclose(fp);
return 0;
}
|
생성된 파일을 확인해보면 다음과 같습니다.
fputs대신 puts 함수를 사용할 수도 있지만, puts 함수는 항상 줄을 바꾸므로 문자열을 이어서 출력할 수 없습니다.
따라서 문자열의 출력은 안전하고 정확하게 수행되는 fputs 함수를 사용하는 것이 좋습니다.
fgets 함수는 문자열을 한번에 입력 할 때 사용합니다.
char * fgets(char *, int, FILE *) 입력한 char 배열, 파일의 끝이면 NULL
fgets 함수는 인자가 3개입니다. fgets 함수가 의미하는 바는 다음과 같습니다.
파일포인터와 연결된 파일로부터 두 번째 전달인자로 주어진 바이트 수에 따라 데이터를 읽어와서 첫 번째 전달인자로 주어진 배열에 저장합니다.
fgets(str, sizeof(str), fp); |
str(첫 번째 인자) : 파일에서 가지고 온 문자열을 넣는 변수입니다. 읽어들인 문자열을 저장할 char 배열을 가리키는 포인터입니다.
sizeof(str)(두 번째 인자) : 한번에 가지고 올 문자열의 길이 정보 변수입니다. 일반적으로 str로 전달된 배열의 길이(마지막 NULL 문자 포함)가 사용됩니다.
fp(세 번째 인자) : 문자를 읽을 스트림을 식별하는 파일포인터입니다.
먼저 이 예제 코드가 사용할 "fgets_test.txt"파일은 아래와 같은 문자열이 저장되어 있는 텍스트파일입니다.
5바이트의 크기를 갖는 배열에 문자열을 입력 받는 경우
|
20바이트의 크기를 갖는 배열에 문자열을 입력 받는 경우
|
|||||
실행결과 Sejo 널문자가 저장될 공간을 제외하고 4바이트만 입력됩니다. |
실행결과 Sejong University 입력 받을 데이터의 크기보다 파일의 크기가 작으면 파일 끝까지 읽어 들입니다. |
fgets 대신 gets 함수를 사용할 수도 있지만, gets 함수는 입력할 저장 공간의 크기를 인수로 줄 수 없으므로 문자열을 입력할 때 할당하지 않은 메모리 공간을 침범할 가능성이 있습니다.
따라서 문자열의 입력은 안전하고 정확하게 수행되는 fgets 함수를 사용하는 것이 좋습니다.
fprintf 함수는 printf 함수와 사용법이 같습니다. 단, 출력 대상을 파일포인터로 지정해 줄 수 있습니다.
fprintf 함수가 printf 함수와 차이를 보이는 부분은 첫 번째 전달인자가 파일구조체의 포인터라는 점입니다.
그래서 printf와 달리 fprintf는 첫 번째 인자로 전달된 파일구조체의 포인터가 지칭하는 파일로 출력이 이루어집니다.
int fprintf(FILE *, const char *, ...); 출력한 문자의 바이트 수, 실패하면 음수
다음은 fprintf 함수를 사용한 예제입니다.
"21012345 김세종" 이렇게 만들어진 문자열이 첫 번째 전달인자가 가리키는 파일에 저장이 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include <stdio.h>
int main(void)
{
int number = 21012345;
char name[10] = "김세종";
FILE * fp = fopen("student.txt", "w");
if (fp == NULL)
printf("파일 열기 실패\n");
else
{
fprintf(fp, "%d %s", number, name);
fclose(fp);
}
return 0;
}
|
위의 예제를 통해서 생성된 파일 student.txt를 열어보면 데이터가 저장되었음을 확인할 수 있습니다.
fscanf 함수는 scanf 함수와 사용법이 같습니다. 단, 입력 대상을 파일포인터로 지정해 줄 수 있습니다.
fscanf가 scanf와 차이를 보이는 부분은 첫 번째 인자로 파일구조체의 포인터가 전달된다는 점입니다.
따라서 fscanf는 첫 번째 인자로 전달된 포인터가 지칭하는 파일로부터 데이터를 읽어들입니다.
int fscanf(FILE *, const char *, ...); 입력에 성공한 데이터 수, 파일에 데이터가 없을 때 EOF
다음 예제 코드가 데이터를 입력 받을 파일은 "21012345 김세종" 문자열이 저장되어 있는 "student.txt" 파일입니다.
|
실행 시, student.txt에 저장됐던 "21012345 김세종"이
실행결과 21012345 김세종 |