http://www.dong-il.freeserve.co.uk/turbock.htm
A. 화일 입출력
일반적으로 화일 처리라 하면 새로운 화일을 만들고 화일의 내용을 읽고 쓰는 것을 의미하는데,
C에서는 이를 기본 기능으로 제공하지 않으며 표준 라이브러리 함수들을 통해 수행하도록 하고
있다. 이 라이브러리 함수에는 두 가지 종류가 있는데, 이른바 고수준의 화일 처리 함수들로 이것
을 사용하면 다양한 포맷으로 입출력을 수행할 수 있으나 효율이 떨어지는 단점이 있다. 반면에
저수준의 입출력 함수를 사용하면 포맷을 지정할 수 없는 단점이 있으나 매우 효율적으로 입출력
을 수행하게 된다. 따라서 실무 프로그램에서는 오히려 저수준의 화일 입출력 함수들을 선호하는
경우가 많다.
< 고수준의 화일 입출력 함수들 >
고수준의 화일 입출력에서는 FILE이라는 구조를 공통적으로 사용하고 있다. 이는 stdio.h에 다음
과 같이 정의되어 있으며, 따라서 고수준의 화일 입출력을 이용하기 위해서는 반드시 stdio.h 화
일을 포함시켜야 한다.
[code]
typedef struct { │
short level; /* fill/empty level of buffer */ │
unsigned flags; /* File status flags */ │
char fd; /* File descriptor */ │
unsigned char hold /* Ungetc char if no buffer */ │
short bsize; /* Buffer size */ │
unsigned char *buffer; /* Data transfer buffer */ │
unsigned char *curp; /* Current active pointer */ │
unsigned istemp; /* Temporary file indicator */ │
short token; /* Used for validity checking */ │
} FILE; /* This is the FILE object */ │
[/code]
화일을 사용하기 위해서는 먼저 화일을 여는 것이 필요하며 이때 다음과 같이 fopen함수를 사용
하여야 한다.
[code] FILE *fp;
fp = fopen( 화일 이름", 모드 );
[/code]
그러면 fopen은 지정한 화일 이름의 화일을 찾아 이를 사용할 수 있는 형태로 열고 이 화일에
대한 FILE 구조를 하나 만든 후 이 구조에 대한 포인터를 계산 결과로 돌려주게 된다. 두번째의
모드는 앞의 화일을 어떤 형태로 사용할 것인가를 나타내는 것으로 다음과 같은 것들이 있다.
┌─────┬───────────────────────────────────────┐
│ 모드 │ 의 미 │
├─────┼───────────────────────────────────────┤
│ "r" │ 화일을 읽기 전용으로 연다. 해당 화일이 이미 존재해 있어야 한다. │
├─────┼───────────────────────────────────────┤
│ "w" │ 화일을 쓰기 전용으로 연다. 이미 존재하면 그 화일의 내용은 제거되며, 없으면 │
│ │ 새로운 화일이 자동으로 만들어진다. │
├─────┼───────────────────────────────────────┤
│ "a" │ 화일을 쓰기 전용으로 연다. 화일의 내용이 있는 경우 그 내용은 지워지지 않으며 │
│ │ 화일의 맨 끝으로 자동으로 이동해 맨 끝에 덧붙여 쓸 수 있도록 한다. 만약 해당 │
│ │ 화일이 없으면 새로운 화일이 자동으로 생성된다. │
│ │ │
├─────┼───────────────────────────────────────┤
│ "r+" │ 화일을 읽기와 쓰기 두 가지 목적으로 연다. 해당 화일이 없으면 에러가 난다. │
├─────┼───────────────────────────────────────┤
│ "w+" │ 화일을 읽기와 쓰기 두 가지 목적으로 연다. 해당 화일이 있으면 그 내용은 지워 │
│ │ 지며 없으면 새로운 화일이 만들어진다. │
├─────┼───────────────────────────────────────┤
│ "a+" │ 화일을 읽기와 쓰기 두 가지 목적으로 연다. 해당 화일이 있으면 그 화일 맨 끝으 │
│ │ 로 이동하여 그 위치부터 읽기와 쓰기가 이루어지게 되며, 화일이 없으면 새로운 │
│ │ 화일이 생성된다. │
│ │ │
└─────┴───────────────────────────────────────┘
쓰기용("w")을 사용할 때, 이미 존재하는 화일을 열려고 하면 그 화일이 없어져버리므로 사용시
에 주의를 해야 한다. 그리고 추가용("a")은 화일의 끝에 데이터를 추가하기 위해서 사용하며 갱
신용("+")은 화일을 열어서 읽기와 쓰기를 한 화일에 한꺼번에 할 필요가 있을 때 사용한다.
예를 들어 test.txt란 화일의 내용을 읽기 위해서는 다음과 같이 한다.
[code]fp = fopen("test.txt", "rb");[/code]
열기 형식에는 일반적으로 텍스트 화일("t")이나 이진 화일("b")이 들어가야 하는데 생략을 하게
되면 텍스트 화일 형식이 된다.
test.txt란 화일을 지우고 새로 쓰고 싶을 때에는 다음과 같이 하면 된다.
[code]fp = fopen("test.txt", "w"); /* 생략했으므로 텍스트 화일이 된다 */[/code]
반면에 test.txt란 화일을 지우지 않고 화일의 끝에 덧붙이고 싶을 때에는 다음과 같이 한다.
[code] fp = fopen("test.txt", "a");[/code]
화일명에는 드라이브와 패스를 지정할 수 있다. 패스를 표시할 때는 역슬래쉬('\')를 이중('\\')으
로 해야 하는데, 그렇지 않으면 확장열이 되어 엉뚱한 결과를 가져오게 되므로 주의해야 한다.
┌────────────────────────────────────────────┐
│ "c:\\work\\test.txt" │
└────────────────────────────────────────────┘
화일을 열다가 에러가 발생하면(예를 들어 읽기전용으로 화일을 열 때 화일이 디스크상에 없거
나, 화일을 열려는 드라이브에 디스크가 들어있지 않다거나 할 때 발생한다) fopen함수는 NULL
값을 계산 결과로 산출한다. 따라서 다음과 같이 항상 조사하는 것이 필요하다.
[code]
if ((fp = fopen("test.txt", "rb")) == NULL) {
printf("File open error.\n");
exit(-1); /* exit함수를 만나서 프로그램을 종료한다 */
}
[/code]
그리고 fp 대신에 사용할 수 있는 것이 있는데, C의 입출력에서는 데이터를 하나 또는 그 이상
의 물리적(스크린이나 프린터, 플로터 등의 컴퓨터에 연결될 수 있는 모든)장치로 지향되는 스트
림(stream)의 형태로 나타내기도 한다.
다음의 표는 스트림을 이용한 표준 입출력 장치인데, stdio.h헤더에 정의되어 있다.
[code]
#define stdin (&_streams[0])
#define stdout (&_streams[1])
#define stderr (&_streams[2])
#define stdaux (&_streams[3]) /* 보조 입출력 장치, 보통 COM1에 연결되어 있다 */
#define stdprn (&_streams[4]) /* 표준 프린터 장치, LPT1에 연결되어 있다 */
[/code]
stdin은 표준 입력 장치로 입력을 받아 들일 때 화일을 사용하지 않고 scanf나 getchar 등을 사
용하여 입력을 받아들이던 곳을 의미하는데, 이는 보통 사용하고 있는 컴퓨터의 키보드가 된다.
반면에 stdout은 표준 출력 장치로 printf나 putchar를 사용하여 출력했을 때 그 출력이 나가는
곳을 의미하며, 이는 일반적으로 사용하고 있는 컴퓨터의 화면이 된다. stderr은 표준 에러출력
장치를 말하며, 에러 메시지가 나가는 곳으로 표준 출력과 유사하게 이도 역시 보통 사용하고 있
는 컴퓨터의 화면이 된다. Unix에서는 표준 출력과 표준 에러가 다르지만 DOS에서는 이 둘을 구
별하지 않기 때문에 실제로 차이가 없게 된다. 따라서 고수준의 화일 입출력을 사용할 때에 키보
드로 부터 입력을 받아들이고 싶으면 fp 대신에 stdin이란 이름을 사용하면 되고 화면에 출력하고
싶으면 역시 fp란 이름 대신에 stdout이나 stderr을 사용하면 된다.
화일을 열었으면 읽거나 써야 하는데, 화일로 부터 읽어들이고자 할 때에는 다음의 fscanf함수를
사용한다. 예를 들어 두 개의 정수값을 test.dat란 화일로부터 읽어들이고자 할 때에는 다음과 같
이 하면 된다.
[code]
FILE *fp; │
int i, j; │
if ((fp = fopen("test.dat", "r")) == NULL) {
printf("Error: Cannot open test.dat\n");
return (1);
}
fscanf(fp,"%d%d",&i,&j); [/code]
실제로 지금까지 사용해 온 scanf함수는 다음과 같은 의미를 갖는다.
┌────────────────────────────────────────────┐
│ fscanf(stdin,"포맷 ,변수 리스트); │
└────────────────────────────────────────────┘
다음은 fscanf를 사용하는 한 예이다.
[code]
#include <stdio.h>
main() {
FILE *fp; /* +---------------+ */
int i; /* | DATA.DAT | */
int no = 0; /* | 100 | */
long int sum = 0l; /* | 78 | */
if ((fp = fopen("data.dat", "r")) == NULL) { /* | 52 | */
printf("Error: Cannot open data.dat\n"); /* | 34 | */
return (1); /* | 86 | */
} /* | 32 | */
while (fscanf(fp,"%d",&i) != EOF) { /* | 16 | */
sum += i; /* | 82 | */
++no; /* | 99 | */
} /* | 75 | */
if (no == 0) /* +---------------+ */
printf("No data.\n");
else {
printf("Total %d numbers.\n",no);
printf("Sum = %ld\n",sum);
printf("Average = %.2f\n",(float)sum/(float)no);
}
fclose(fp); /* 화일을 열어서 사용한 후에는 반드시 화일을 닫아야 한다. 화일의 닫음은
버퍼에 들어있던 내용을 모두 디스크로 보내서 버퍼를 비우고 fopen 함수로 화일을
열어서 되돌림 값을 받을 때 사용했던 포인터를 재사용 할 수 있게 해준다.
fclose 함수의 인자는 fopen 함수에서 되돌려 받았던 포인터가 들어가고 되돌림
값은 닫기가 성공일 때 0, 에러일 때 EOF를 되돌린다 */
}
[/code]
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
C:\TC>test 뇌
Total 10 numbers.
Sum = 654
Average = 65.40
scanf 함수가 표준 입력 장치(stdin)로 부터 입력받던 것을 fscanf 함수는 화일로부터 읽어오는
데 사용한다.
┌────────────────────────────────────────────┐
│ int a; │
│ long int b; │
│ double c; │
│ char d[10] = { 0, }; │
│ ..... │
│ fscanf(fp,"%d %ld %lf %s",&a,&b,&c,d); /* scanf 함수와 마찬가지로 fscanf 함수의 │
│ 입력 데이터들간의 구분은 공백문자(' ', \t, \n 등)로 이루어진다 */ │
│ /* 화일에 다음과 같은 데이터가 들어있다고 가정한다 */ │
│ +-+-+-+-+-+-+-+-+-+-+-+-+-+--+--+-+-+-+-+-+-+-+-+-+-+-+--+--+ │
│ |5| |4|0|0|0|0| |1|2|.|3|4|\r|\n|H|e|l|l|o| |w|o|r|l|d|\r|\n| │
│ +-+-+-+-+-+-+-+-+-+-+-+-+-+--+--+-+-+-+-+-+-+-+-+-+-+-+--+--+ │
│ /* 위의 fscanf 함수가 실행되고 나면 각 변수에는 다음과 같은 값이 들어가게 된다 */ │
│ a = 5 │
│ b = 40000 │
│ c = 12.34 │
│ d = "Hello" │
└────────────────────────────────────────────┘
또 getchar와 같이 한 글자씩 화일로부터 읽어들일 수도 있는데 이때에는 getc를 사용하면 된다.
┌────────────────────────────────────────────┐
│ char c; │
│ FILE *fp; │
│ c = getc(fp); /* fp가 가리키는 화일로부터 한 문자를 읽어 이를 계산 결과로 산출 */ │
└────────────────────────────────────────────┘
따라서 getchar는 다음과 같은 의미를 갖는다.
┌────────────────────────────────────────────┐
│ getc(stdin); │
└────────────────────────────────────────────┘
다음은 getc를 사용하여 DOS의 type과 같은 일을 하는 프로그램의 한 예이다.
[code]#include <stdio.h>
main(int argc, char *argv[]) {
FILE *fp;
char c;
if (argc == 1) { /* 인자가 없으면 */
printf("USAGE : %s filename(1) ...filename(n)\n",*argv);
return (1);
}
while (--argc > 0) { /* 인자가 있으면 */
if ((fp = fopen(*++argv, "r")) == NULL) {
printf("Error: Cannot open %s\n",*argv);
continue;
}
printf("\n*** %s ***\n",*argv);
while ((c = getc(fp)) != EOF)
putchar(c);
}
fclose(fp);
}
[/code]
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
C:\TC>fileview \config.sys \autoexec.bat 뇌
지금까지는 입력에 대해 알아보았다. 출력의 경우도 마찬가지인데 다음과 같이 fprintf 함수를 사
용하면 된다.
┌────────────────────────────────────────────┐
│ fprintf(fp,"포맷 ,변수 리스트); │
└────────────────────────────────────────────┘
따라서 printf는 다음과 같은 의미를 갖게 된다.
┌────────────────────────────────────────────┐
│ fprintf(stdout,"포맷 ,변수 리스트); │
└────────────────────────────────────────────┘
fprintf를 사용하면 stdout이 아닌 stderr으로도 출력을 내보낼 수 있는데, 이때에도 다음과 같이
사용하면 된다.
┌────────────────────────────────────────────┐
│ fprintf(stderr,"포맷 ,변수 리스트); │
└────────────────────────────────────────────┘
stderr은 에러 메시지를 내보내기 위한 것이기 때문에, 에러 메시지는 이를 통해 내보내는 것이
좋다.
다음 프로그램은 sort [-r] file(1) file(2)와 같이 사용하는 것으로 [ ]는 생략할 수 있는 부분을
의미하며 file(1)화일을 라인 단위로 정렬하여 file(2)에 출력하는데 -r을 지정하면 큰 순서에서 작
은 순서로 정렬하고, 지정하지 않으면 작은 순서에서 큰 순서로 출력하는 것이다.
[code]#include <stdio.h>
#define REVERSE -1
#define NORMAL 1
#define MAXLINENO 100
#define MAXCNO 80
#define TRUE EOF*-1
main(int argc, char *argv[]) {
static char text[MAXLINENO][MAXCNO];
char t[MAXCNO];
FILE *fp1, *fp2;
int direction = NORMAL;
int no = 0;
int i, j;
if ((argc != 3 && argc != 4) || (argc == 4 && strcmp(*(argv+1), "-r"))) {
fprintf(stderr,"USAGE: %s [-r] filename(1) filename(2)\n",*argv);
return (1);
}
if (argc == 4) { /* -r을 주면 역으로 정렬 */
direction = REVERSE;
++argv;
}
if ((fp1 = fopen(*++argv, "r")) == NULL) {
fprintf(stderr,"Error: Cannot open %s\n",*argv);
return (2);
}
if ((fp2 = fopen(*++argv, "w")) == NULL) {
fprintf(stderr,"Error: Cannot create %s\n",*argv);
return (3);
}
while (get_line(fp1, text[no++]) != EOF);
for (i = 0; i < no - 2; i++)
for (j = i + 1; j < no - 1; j++)
if (strcmp(text[i], text[j]) * direction > 0) {
strcpy(t, text[i]);
strcpy(text[i], text[j]);
strcpy(text[j], t);
}
for (i = 0; i < no - 1; i++)
fprintf(fp2,"%s\n",text[i]);
fclose(fp1);
fclose(fp2);
}
int get_line(FILE *fp, char *s) { /* 화일로부터 한 라인을 읽어들임 */
char *cp = s; /* +--------------------+ */
while ((*s = getc(fp)) != EOF) /* | ( DATA ) | */
if (*s == '\n') { /* | Lee Seung Wook | */
*s = '\0'; /* | Kwon Oh Jin | */
return (TRUE); /* | Oh Jung Pyo | */
} /* | Kim Joon Sik | */
else /* | Yun Young Il | */
++s; /* | Kwon Oh Chang | */
if (cp == s) /* | Kim Chang Ju | */
return (EOF); /* | Hong Gil Dong | */
*s = '\0'; /* | Kim Young Hoon | */
return (TRUE); /* | Kim Jong Min | */
} /* +--------------------+ */
[/code]
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
C:\TC>sort data sdata 뇌
C:\TC>type sdata 뇌
Hong Gil Dong
Kim Chang Ju
Kim Jong Min
Kim Joon Sik
Kim Young Hoon
Kwon Oh Chang
Kwon Oh Jin
Lee Seung Wook
Oh Jung Pyo
Yun Young Il
C:\TC>sort -r data rdata 뇌
C:\TC>type rdata 뇌
Yun Young Il
Oh Jung Pyo
Lee Seung Wook
Kwon Oh Jin
Kwon Oh Chang
Kim Young Hoon
Kim Joon Sik
Kim Jong Min
Kim Chang Ju
Hong Gil Dong
다음의 예제는 키보드로부터 입력받은 문장들에 줄번호를 붙여서 test.txt 화일로 저장하는 일을
하는 것으로, 프로그램의 종료는 아무 문장도 입력하지 않고 엔터키를 치면 된다.
[code]
#include <stdio.h>
void main(void) {
FILE *fp;
char str[80];
int line = 0;
if ((fp = fopen("test.txt", "w")) == NULL) {
printf("File open error ... \n");
exit(-1);
}
for ( ; ; ) {
gets(str);
if (str[0] == '\0')
break;
line++;
fprintf(fp,"%3d : %s\n",line,str);
}
fclose(fp);
}
[/code]
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
C:\TC>test 뇌
Time 뇌
can 뇌
never 뇌
mend. 뇌
뇌
C:\TC>type test.txt 뇌
1 : Time
2 : can
3 : never
4 : mend.
또 putchar와 같이 한 글자씩 출력하는 함수도 있는데, 이의 이름은 putc이다. putc는 다음과 같
은 형태로 사용한다.
┌────────────────────────────────────────────┐
│ char c; │
│ FILE *fp; │
│ putc(c,fp); /* fp가 가리키는 화일에 c가 출력되게 된다 */ │
└────────────────────────────────────────────┘
따라서 putchar(c)는 다음과 같은 의미를 갖는다.
┌────────────────────────────────────────────┐
│ putc(c,stdout); │
└────────────────────────────────────────────┘
다음은 putc의 한 사용예이다.
[code]#include <stdio.h>
main() {
char c;
char fname1[20], fname2[20], fname3[20];
FILE *fp1, *fp2, *fp3;
printf("Input the first file name -> ");
gets(fname1);
printf("Input the second file name -> ");
gets(fname2);
printf("Input the file name to create -> ");
gets(fname3);
if ((fp1 = fopen(fname1, "r")) == NULL) {
printf("Error : Cannot open %s\n",fname1);
exit(-1);
}
if ((fp2 = fopen(fname2, "r")) == NULL) {
printf("Error : Cannot open %s\n",fname2);
fclose(fp1);
exit(-1);
}
if ((fp3 = fopen(fname3, "w")) == NULL) {
printf("Error : Cannot create %s\n",fname3);
fclose(fp1);
fclose(fp2);
exit(-1);
}
while ((c = getc(fp1)) != EOF) /* 화일로부터 한자씩 읽어들일 때에는 getc를, */
putc(c,fp3); /* 출력할 때에는 putc를 사용하면 된다 */
fclose(fp1);
while ((c = getc(fp2)) != EOF)
putc(c,fp3);
fclose(fp2);
fclose(fp3);
}
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
Input the first file name -> \config.sys 뇌
Input the second file name -> \autoexec.bat 뇌
Input the file name to create -> sysbat.txt 뇌
이 밖에도 화일에서 한 글자를 입출력하는 함수로 fgetc와 fputc가 있다.
┌────────────────────────────────────────────┐
│ int fgetc(FILE *fp) │
│ int fputc(int c, FILE *fp) │
└────────────────────────────────────────────┘
fgetc 함수는 지정한 화일에서 읽어들인 문자를 되돌리고 화일 포인터를 한 개 증가시킨다. 화일
포인터가 화일의 끝에 도달했을 경우에는 EOF를 되돌린다. 이 EOF값과 0xff값을 구분하기 위해
서 읽어들인 문자는 int형으로 변환되어 반환된다.
fputc함수는 스트림(stream)으로 지정한 화일에 문자 c를 출력한다. 디스크가 꽉찼을 경우나 출
력시 에러가 발생하면 EOF값을 되돌리고, 텍스트 모드로 오픈되어 있으면 '\n'을 CR·LF로 변
환한다.
다음의 예제는 fgetc 함수와 fputc 함수를 이용한 화일복사 프로그램이다.
[code]#include <stdio.h>
void main(int argc, char *argv[]) {
FILE *srcfp, *objfp;
int ch;
if (argc != 3) {
printf("USAGE : %s sourcefile objectfile\n",*argv);
exit(-1);
}
if ((srcfp = fopen(argv[1], "rb")) == NULL) {
printf("File open error : %s\n\a",argv[1]);
exit(-1);
}
if ((objfp = fopen(argv[2], "wb")) == NULL) {
printf("File creation error : %s\n\a",argv[2]);
exit(-1);
}
printf("FileCopy %s to %s\n\n",argv[1],argv[2]);
while ((ch = fgetc(srcfp)) != EOF)
fputc(ch,objfp);
fclose(srcfp);
fclose(objfp);
}
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
C:\TC>scopy tc.exe ec.exe 뇌
FileCopy tc.exe to ec.exe
< 저수준 화일 입출력 함수들 >
저수준의 화일 입출력에서는 FILE이란 구조 대신 간단하게 각 화일마다 번호를 사용하는데, 이
를 화일 식별자(file descriptor), 또는 핸들(handle)이라고 한다. 이 핸들은 0 이상의 값을 가지고
있는데 실제로 0과 1, 2 는 고정된 의미(핸들 0은 표준 입력을 위한 번호이며 1은 표준 출력, 그
리고 2는 표준 에러로 사용)를 갖고 있어서 화일을 처음 열게 되면 그 화일의 핸들은 3이 된다.
저수준의 화일 입출력에서도 화일을 사용하기 위해서는 화일을 먼저 열어야 하며, 이때 다음과
같이 open 함수를 사용한다.
┌────────────────────────────────────────────┐
│ int fd; │
│ fd = open("화일 이름 , 액세스 방식[, 모드]); │
└────────────────────────────────────────────┘
위에서 [ ]는 역시 생략할 수 있는 부분을 의미하며, 화일 이름은 fopen과 같이 열 화일의 이름
이고 액세스 방식은 이 화일을 어떻게 열 것인가인데, fopen과는 달리 다음과 같은 형태로 사용
한다.
┌──────┬─────┬──────────────────────────────────┐
│ 모드 │ 값 │ 내 용 │
├──────┼─────┼──────────────────────────────────┤
│ O_RDONLY │ 0x0001 │ 읽기 전용으로 화일을 연다. │
├──────┼─────┼──────────────────────────────────┤
│ O_WRONLY │ 0x0002 │ 쓰기 전용으로 화일을 연다. │
├──────┼─────┼──────────────────────────────────┤
│ O_RDWR │ 0x0004 │ 읽고 쓰기 위해 화일을 연다. │
├──────┼─────┼──────────────────────────────────┤
│ O_CREAT │ 0x0100 │ 화일이 없을 경우 새로운 화일을 만든다. │
├──────┼─────┼──────────────────────────────────┤
│ O_TRUNC │ 0x0200 │ 현재 있는 화일의 내용을 0으로(제거) 한다. │
├──────┼─────┼──────────────────────────────────┤
│ O_EXCL │ 0x0400 │ O_CREAT과 함께 사용하며, 화일이 없을 경우에만 연다. │
├──────┼─────┼──────────────────────────────────┤
│ O_APPEND │ 0x0800 │ 화일을 쓰기용으로 열고 화일 포인터를 화일의 끝에 위치시킨다. │
├──────┼─────┼──────────────────────────────────┤
│ O_TEXT │ 0x4000 │ 화일을 텍스트 형식으로 연다. │
├──────┼─────┼──────────────────────────────────┤
│ O_BINARY │ 0x8000 │ 화일을 이진 형식으로 연다. │
└──────┴─────┴──────────────────────────────────┘
위의 O_로 시작하는 것들은 모두 상수로 이의 정의는 fcntl.h(이것은 file control의 약자)에 들어
있기 때문에 open 문을 사용하려면 반드시 fcntl.h를 포함하여야 한다.
위의 것들은 액세스 방식의 한 조건들로 여러 개를 동시에 같이 사용할 수 있으며 이 때에는 각
조건들을 '|'(비트 연산자 OR)를 이용해서 묶으면 된다.
data.dat 란 화일을 읽기 전용으로 열고자 할 때에는 다음과 같이 하면 된다.
┌────────────────────────────────────────────┐
│ fd = open("data.dat", O_RDONLY); │
└────────────────────────────────────────────┘
그리고 기존의 화일이 있으며, 이를 지우고 쓰기 전용으로 열 때에는 다음과 같이 하면 된다.
┌────────────────────────────────────────────┐
│ fd = open("data.dat", O_WRONLY | O_TRUNC); │
└────────────────────────────────────────────┘
O_TRUNC이 추가 되었는데, 만약 이를 써 주지 않으면 쓰기 전용이라도 화일의 내용은 없어지
지 않는다. 반면에 화일의 끝에 추가하고자 할 때에는 다음과 같이 O_APPEND를 써 주면 된다.
┌────────────────────────────────────────────┐
│ fd = open("data.dat", O_WRONLY | O_APPEND); │
└────────────────────────────────────────────┘
fopen 문과 다른 것은 해당 화일이 없는 경우 화일이 만들어지지 않는다는 것이다. 만약 화일이
없을 때 화일을 만들도록 하고 싶으면 다음과 같이 O_CREAT를 지정하여야 하며, 이 때에는 세
번재 인자가 필요하다.
┌────────────────────────────────────────────┐
│ fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC, 모드); │
│ /* 화일이 있으면 그 내용을 지우고 없으면 생성, fopen의 "w"와 같음 */ │
│ fd = open("data.dat", O_WRONLY | O_CREAT | O_APPEND, 모드); │
│ /* 화일이 있으면 그 끝으로 이동하고 없으면 생성, fopen의 "a"와 같음 */ │
└────────────────────────────────────────────┘
세번째 인자인 모드에는 액세스 방식에서 O_CREAT 플래그가 지정된 경우, sys\stat.h(TC의 경
우)에 정의되어 있는 다음 기호 중 하나가 사용된다(일반적으로 8진수 0700을 주면 된다).
┌────────────────────────────────────────────┐
│ S_IWRITE /* 써 넣기 가능 */ │
│ S_IREAD /* 읽어 내기 가능 */ │
│ S_IREAD | S_IWRITE /* 읽기 / 쓰기 가능 */ │
└────────────────────────────────────────────┘
읽기용 화일에서는 모드를 생략해도 화일의 사용에는 별 지장이 없다.
그밖에 O_RDWR은 읽고 쓰고자 할 때 사용하며 O_EXCL은 O_CREAT하고만 같이 사용하는데,
지정한 화일이 있으면 에러가 난다. 즉 화일이 없는 상태에서 새로 만들고자 할 때에는 O_EXCL
과 O_CREAT를 같이 사용하면 된다.
open이 제대로 화일을 열게 되면 음이 아닌 정수값을 반환하는데, 그 화일의 핸들을 계산 값으
로 산출하게 되며 에러 발생의 경우 -1을 계산 결과로 산출하게 된다. 따라서 다음과 같이 항상
조사하는 것이 필요하다.
┌────────────────────────────────────────────┐
│ if ((fd = open("data.dat", O_RDONLY)) == -1) { │
│ fprintf(stderr,"Error: Cannot open data.dat\n"); │
└────────────────────────────────────────────┘
화일을 열었으면 이를 읽고 써야 하는데, 읽을 때에는 다음과 같이 read란 함수를 사용한다.
┌────────────────────────────────────────────┐
│ int n; │
│ char *buf; │
│ int size; │
│ int fd; │
│ n = read(fd, buf, size); │
└────────────────────────────────────────────┘
여기서 fd는 open에서 넘겨준 화일의 번호이고 buf는 읽어들일 데이터를 저장할 공간(버퍼)을
가리키는 포인터이다. 그리고 size는 몇 개의 바이트를 읽어들일 것인가, 그 크기를 나타낸다. 즉
read는 화일 fd로부터 size 만큼의 바이트를 읽어들여 이를 buf가 가리키는 곳에 저장하게 된다.
화일 내에 size 만큼의 데이터가 있지 않을 수도 있는데, 이를 조사하기 위해 read는 자신이 실
제로 읽어들인 바이트의 수를 계산 결과로 산출하고 있다. 따라서 read를 사용할 때에는 항상 위
의 n 값을 잘 조사하여야 하며, 이것이 실제 읽어들인 데이터의 수이기 때문에 buf가 가리키는
공간에는 바로 n 바이트가 존재하게 된다. 이 n이 0이라는 것은 읽어들인 데이터가 없다, 즉
EOF라는 의미이며 이 값이 -1이면 이는 읽을 때 에러가 발생한 것을 의미한다. 예를 들어
data.dat란 화일로부터 80 바이트의 데이터를 읽어들이고자 할 때에는 다음과 같이 사용한다.
┌────────────────────────────────────────────┐
│ int fd; │
│ char buf[80]; │
│ int n; │
│ fd = open("data.dat", O_RDONLY); │
│ n = (fd, buf, 80); /* 두번째 인자(buf)에 들어가는 값은 포인터이어야 한다 */ │
└────────────────────────────────────────────┘
이때 n이 80이면 잘 읽은 것이고 다른 값이면 데이터가 부족하거나 뭔가 잘못된 것이다. 일반적
으로 읽을 데이터의 크기는 다르지만 버퍼의 크기는 보통 고정적으로 사용하는데 stdio.h에 정의
되어 있는 BUFSIZ를 사용하는 것이 관례로 되어 있다. 이 크기는 한 화일로부터 가져올 수 있는
데이터의 최적치이기 때문에 많은 양의 데이터를 읽거나 쓸 때에는 이 것을 사용하는 것이 성능
을 최대한 높일 수 있다.
다음은 read 함수의 한 예로서 size file(1) ... fine(n)과 같이 사용하여 각 화일들의 크기를 계산
하여 출력하는 프로그램이다.
[code]
#include <stdio.h>
#include <fcntl.h>
main(int argc, char *argv[]) {
int fd;
long int size;
int n;
char buf[BUFSIZ];
if (argc == 1) {
fprintf(stderr,"USAGE: %s file(1) file(2) ... file(n)\n",*argv);
return (0);
}
while (--argc > 0) {
if ((fd = open(*++argv, O_RDONLY | O_BINARY)) == -1) {
fprintf(stderr,"Error: Cannot open %s\n",*argv);
continue;
}
size = 0l;
while ((n = read(fd, buf, BUFSIZ)) > 0)
size += n;
if (n == 0)
fprintf(stdout,"%s: %ld bytes.\n",*argv,size);
else
fprintf(stderr,"Error in reading %s\n",*argv);
}
close(fd); /* 핸들이 다루는 화일을 닫는다. 되돌림 값은 화일 닫기가 */
} /* 성공하면 0, 실패하면 -1의 값을 되돌린다 */
[/code]
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
C:\TC>size tc.exe tcc.exe 뇌
tc.exe: 290249 bytes.
tcc.exe: 179917 bytes.
저수준의 화일 입출력에서 화일로부터 읽어들이는 것은 이 read 밖에 없다. 즉 문자 하나씩 밖
에 읽어들이지 않기 때문에 만약 정수값을 읽어들이고자 할 때에는 읽어들인 것을 정수로 변환하
여야 한다. 따라서 복잡한 포맷으로 된 데이터를 읽어들일 때에는 고수준의 화일 입출력을 사용
하는 것이 좋다.
read 와 반대로 화일에 쓰고자 할 때는 다음과 같이 write이라는 함수를 사용한다.
┌────────────────────────────────────────────┐
│ int fd; │
│ char *buf; │
│ int size; │
│ int n; │
│ n = write(fd, buf, size); │
└────────────────────────────────────────────┘
fd는 open에 의해 넘겨 받은 화일의 핸들이며 buf는 출력할 데이터가 들어 있는 곳을 가리키는
포인터이고, size는 출력할 데이터의 크기(바이트수)를 의미한다. 즉 fd가 나타내는 화일에 size만
큼의 바이트를 buf로부터 가져와 출력하라는 의미가 된다. 이때 write 함수는 실제로 쓴 바이트의
수를 계산 결과로 산출하게 된다. 이는 항상 size와 같지는 않은데, 예를 들어 디스크가 꽉차서
더 이상 쓸 공간이 없게 되면 지정한 크기 보다 적은 수를 쓰게되므로 n이 size보다 작을 수 있
다. 그리고 에러가 발생한 경우에는 read와 같이 -1을 계산 결과로 산출한다.
다음은 read와 write 함수를 사용하여 한 화일을 똑같이 복사하는 프로그램이다.
[code]#include <stdio.h>
#include <fcntl.h>
main(int argc, char *argv[]) {
int fd1, fd2;
char buf[BUFSIZ];
int n;
if (argc != 3) {
fprintf(stderr,"USAGE: %s sourcefile objectfile\n",*argv);
return (1);
}
if ((fd1 = open(*(argv+1), O_RDONLY | O_BINARY)) < 0) {
fprintf(stderr,"Error: Cannot open %s\n",*(argv+1));
return (2);
}
if ((fd2 = open(*(argv+2), O_WRONLY | O_TRUNC | O_CREAT | O_BINARY, 0700)) < 0) {
fprintf(stderr,"Error: Cannot create %s\n",*(argv+2));
return (3);
}
printf("FileCopy %s to %s\n\n",*(argv+1),*(argv+2));
while ((n = read(fd1, buf, BUFSIZ)) > 0)
if (write(fd2, buf, n) != n) { /* read와 마찬가지로 저수준의 화일 입출력에서 출력
함수는 write 밖에 없다. 따라서 문자 데이터를 출력하기에는 괜찮지만 정수나 실수값을
출력하고자 할 때에는 이를 ASCII 코드 형태로, 즉 문자 형태로 변환한 다음 출력하여야
하기 때문에 그러한 경우에는 고수준의 입출력 함수를 사용하는 것이 더 편리하다 */
fprintf(stderr,"Error in writing %s\n",*(argv+2));
return (4);
}
if (n < 0)
fprintf(stderr,"Error in reading %s\n",*(argv+1));
close(fd1);
close(fd2);
}
[/code]
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
C:\TC>slcopy tc.exe ec.exe 뇌
FileCopy tc.exe to ec.exe
A. 화일 입출력
일반적으로 화일 처리라 하면 새로운 화일을 만들고 화일의 내용을 읽고 쓰는 것을 의미하는데,
C에서는 이를 기본 기능으로 제공하지 않으며 표준 라이브러리 함수들을 통해 수행하도록 하고
있다. 이 라이브러리 함수에는 두 가지 종류가 있는데, 이른바 고수준의 화일 처리 함수들로 이것
을 사용하면 다양한 포맷으로 입출력을 수행할 수 있으나 효율이 떨어지는 단점이 있다. 반면에
저수준의 입출력 함수를 사용하면 포맷을 지정할 수 없는 단점이 있으나 매우 효율적으로 입출력
을 수행하게 된다. 따라서 실무 프로그램에서는 오히려 저수준의 화일 입출력 함수들을 선호하는
경우가 많다.
< 고수준의 화일 입출력 함수들 >
고수준의 화일 입출력에서는 FILE이라는 구조를 공통적으로 사용하고 있다. 이는 stdio.h에 다음
과 같이 정의되어 있으며, 따라서 고수준의 화일 입출력을 이용하기 위해서는 반드시 stdio.h 화
일을 포함시켜야 한다.
[code]
typedef struct { │
short level; /* fill/empty level of buffer */ │
unsigned flags; /* File status flags */ │
char fd; /* File descriptor */ │
unsigned char hold /* Ungetc char if no buffer */ │
short bsize; /* Buffer size */ │
unsigned char *buffer; /* Data transfer buffer */ │
unsigned char *curp; /* Current active pointer */ │
unsigned istemp; /* Temporary file indicator */ │
short token; /* Used for validity checking */ │
} FILE; /* This is the FILE object */ │
[/code]
화일을 사용하기 위해서는 먼저 화일을 여는 것이 필요하며 이때 다음과 같이 fopen함수를 사용
하여야 한다.
[code] FILE *fp;
fp = fopen( 화일 이름", 모드 );
[/code]
그러면 fopen은 지정한 화일 이름의 화일을 찾아 이를 사용할 수 있는 형태로 열고 이 화일에
대한 FILE 구조를 하나 만든 후 이 구조에 대한 포인터를 계산 결과로 돌려주게 된다. 두번째의
모드는 앞의 화일을 어떤 형태로 사용할 것인가를 나타내는 것으로 다음과 같은 것들이 있다.
┌─────┬───────────────────────────────────────┐
│ 모드 │ 의 미 │
├─────┼───────────────────────────────────────┤
│ "r" │ 화일을 읽기 전용으로 연다. 해당 화일이 이미 존재해 있어야 한다. │
├─────┼───────────────────────────────────────┤
│ "w" │ 화일을 쓰기 전용으로 연다. 이미 존재하면 그 화일의 내용은 제거되며, 없으면 │
│ │ 새로운 화일이 자동으로 만들어진다. │
├─────┼───────────────────────────────────────┤
│ "a" │ 화일을 쓰기 전용으로 연다. 화일의 내용이 있는 경우 그 내용은 지워지지 않으며 │
│ │ 화일의 맨 끝으로 자동으로 이동해 맨 끝에 덧붙여 쓸 수 있도록 한다. 만약 해당 │
│ │ 화일이 없으면 새로운 화일이 자동으로 생성된다. │
│ │ │
├─────┼───────────────────────────────────────┤
│ "r+" │ 화일을 읽기와 쓰기 두 가지 목적으로 연다. 해당 화일이 없으면 에러가 난다. │
├─────┼───────────────────────────────────────┤
│ "w+" │ 화일을 읽기와 쓰기 두 가지 목적으로 연다. 해당 화일이 있으면 그 내용은 지워 │
│ │ 지며 없으면 새로운 화일이 만들어진다. │
├─────┼───────────────────────────────────────┤
│ "a+" │ 화일을 읽기와 쓰기 두 가지 목적으로 연다. 해당 화일이 있으면 그 화일 맨 끝으 │
│ │ 로 이동하여 그 위치부터 읽기와 쓰기가 이루어지게 되며, 화일이 없으면 새로운 │
│ │ 화일이 생성된다. │
│ │ │
└─────┴───────────────────────────────────────┘
쓰기용("w")을 사용할 때, 이미 존재하는 화일을 열려고 하면 그 화일이 없어져버리므로 사용시
에 주의를 해야 한다. 그리고 추가용("a")은 화일의 끝에 데이터를 추가하기 위해서 사용하며 갱
신용("+")은 화일을 열어서 읽기와 쓰기를 한 화일에 한꺼번에 할 필요가 있을 때 사용한다.
예를 들어 test.txt란 화일의 내용을 읽기 위해서는 다음과 같이 한다.
[code]fp = fopen("test.txt", "rb");[/code]
열기 형식에는 일반적으로 텍스트 화일("t")이나 이진 화일("b")이 들어가야 하는데 생략을 하게
되면 텍스트 화일 형식이 된다.
test.txt란 화일을 지우고 새로 쓰고 싶을 때에는 다음과 같이 하면 된다.
[code]fp = fopen("test.txt", "w"); /* 생략했으므로 텍스트 화일이 된다 */[/code]
반면에 test.txt란 화일을 지우지 않고 화일의 끝에 덧붙이고 싶을 때에는 다음과 같이 한다.
[code] fp = fopen("test.txt", "a");[/code]
화일명에는 드라이브와 패스를 지정할 수 있다. 패스를 표시할 때는 역슬래쉬('\')를 이중('\\')으
로 해야 하는데, 그렇지 않으면 확장열이 되어 엉뚱한 결과를 가져오게 되므로 주의해야 한다.
┌────────────────────────────────────────────┐
│ "c:\\work\\test.txt" │
└────────────────────────────────────────────┘
화일을 열다가 에러가 발생하면(예를 들어 읽기전용으로 화일을 열 때 화일이 디스크상에 없거
나, 화일을 열려는 드라이브에 디스크가 들어있지 않다거나 할 때 발생한다) fopen함수는 NULL
값을 계산 결과로 산출한다. 따라서 다음과 같이 항상 조사하는 것이 필요하다.
[code]
if ((fp = fopen("test.txt", "rb")) == NULL) {
printf("File open error.\n");
exit(-1); /* exit함수를 만나서 프로그램을 종료한다 */
}
[/code]
그리고 fp 대신에 사용할 수 있는 것이 있는데, C의 입출력에서는 데이터를 하나 또는 그 이상
의 물리적(스크린이나 프린터, 플로터 등의 컴퓨터에 연결될 수 있는 모든)장치로 지향되는 스트
림(stream)의 형태로 나타내기도 한다.
다음의 표는 스트림을 이용한 표준 입출력 장치인데, stdio.h헤더에 정의되어 있다.
[code]
#define stdin (&_streams[0])
#define stdout (&_streams[1])
#define stderr (&_streams[2])
#define stdaux (&_streams[3]) /* 보조 입출력 장치, 보통 COM1에 연결되어 있다 */
#define stdprn (&_streams[4]) /* 표준 프린터 장치, LPT1에 연결되어 있다 */
[/code]
stdin은 표준 입력 장치로 입력을 받아 들일 때 화일을 사용하지 않고 scanf나 getchar 등을 사
용하여 입력을 받아들이던 곳을 의미하는데, 이는 보통 사용하고 있는 컴퓨터의 키보드가 된다.
반면에 stdout은 표준 출력 장치로 printf나 putchar를 사용하여 출력했을 때 그 출력이 나가는
곳을 의미하며, 이는 일반적으로 사용하고 있는 컴퓨터의 화면이 된다. stderr은 표준 에러출력
장치를 말하며, 에러 메시지가 나가는 곳으로 표준 출력과 유사하게 이도 역시 보통 사용하고 있
는 컴퓨터의 화면이 된다. Unix에서는 표준 출력과 표준 에러가 다르지만 DOS에서는 이 둘을 구
별하지 않기 때문에 실제로 차이가 없게 된다. 따라서 고수준의 화일 입출력을 사용할 때에 키보
드로 부터 입력을 받아들이고 싶으면 fp 대신에 stdin이란 이름을 사용하면 되고 화면에 출력하고
싶으면 역시 fp란 이름 대신에 stdout이나 stderr을 사용하면 된다.
화일을 열었으면 읽거나 써야 하는데, 화일로 부터 읽어들이고자 할 때에는 다음의 fscanf함수를
사용한다. 예를 들어 두 개의 정수값을 test.dat란 화일로부터 읽어들이고자 할 때에는 다음과 같
이 하면 된다.
[code]
FILE *fp; │
int i, j; │
if ((fp = fopen("test.dat", "r")) == NULL) {
printf("Error: Cannot open test.dat\n");
return (1);
}
fscanf(fp,"%d%d",&i,&j); [/code]
실제로 지금까지 사용해 온 scanf함수는 다음과 같은 의미를 갖는다.
┌────────────────────────────────────────────┐
│ fscanf(stdin,"포맷 ,변수 리스트); │
└────────────────────────────────────────────┘
다음은 fscanf를 사용하는 한 예이다.
[code]
#include <stdio.h>
main() {
FILE *fp; /* +---------------+ */
int i; /* | DATA.DAT | */
int no = 0; /* | 100 | */
long int sum = 0l; /* | 78 | */
if ((fp = fopen("data.dat", "r")) == NULL) { /* | 52 | */
printf("Error: Cannot open data.dat\n"); /* | 34 | */
return (1); /* | 86 | */
} /* | 32 | */
while (fscanf(fp,"%d",&i) != EOF) { /* | 16 | */
sum += i; /* | 82 | */
++no; /* | 99 | */
} /* | 75 | */
if (no == 0) /* +---------------+ */
printf("No data.\n");
else {
printf("Total %d numbers.\n",no);
printf("Sum = %ld\n",sum);
printf("Average = %.2f\n",(float)sum/(float)no);
}
fclose(fp); /* 화일을 열어서 사용한 후에는 반드시 화일을 닫아야 한다. 화일의 닫음은
버퍼에 들어있던 내용을 모두 디스크로 보내서 버퍼를 비우고 fopen 함수로 화일을
열어서 되돌림 값을 받을 때 사용했던 포인터를 재사용 할 수 있게 해준다.
fclose 함수의 인자는 fopen 함수에서 되돌려 받았던 포인터가 들어가고 되돌림
값은 닫기가 성공일 때 0, 에러일 때 EOF를 되돌린다 */
}
[/code]
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
C:\TC>test 뇌
Total 10 numbers.
Sum = 654
Average = 65.40
scanf 함수가 표준 입력 장치(stdin)로 부터 입력받던 것을 fscanf 함수는 화일로부터 읽어오는
데 사용한다.
┌────────────────────────────────────────────┐
│ int a; │
│ long int b; │
│ double c; │
│ char d[10] = { 0, }; │
│ ..... │
│ fscanf(fp,"%d %ld %lf %s",&a,&b,&c,d); /* scanf 함수와 마찬가지로 fscanf 함수의 │
│ 입력 데이터들간의 구분은 공백문자(' ', \t, \n 등)로 이루어진다 */ │
│ /* 화일에 다음과 같은 데이터가 들어있다고 가정한다 */ │
│ +-+-+-+-+-+-+-+-+-+-+-+-+-+--+--+-+-+-+-+-+-+-+-+-+-+-+--+--+ │
│ |5| |4|0|0|0|0| |1|2|.|3|4|\r|\n|H|e|l|l|o| |w|o|r|l|d|\r|\n| │
│ +-+-+-+-+-+-+-+-+-+-+-+-+-+--+--+-+-+-+-+-+-+-+-+-+-+-+--+--+ │
│ /* 위의 fscanf 함수가 실행되고 나면 각 변수에는 다음과 같은 값이 들어가게 된다 */ │
│ a = 5 │
│ b = 40000 │
│ c = 12.34 │
│ d = "Hello" │
└────────────────────────────────────────────┘
또 getchar와 같이 한 글자씩 화일로부터 읽어들일 수도 있는데 이때에는 getc를 사용하면 된다.
┌────────────────────────────────────────────┐
│ char c; │
│ FILE *fp; │
│ c = getc(fp); /* fp가 가리키는 화일로부터 한 문자를 읽어 이를 계산 결과로 산출 */ │
└────────────────────────────────────────────┘
따라서 getchar는 다음과 같은 의미를 갖는다.
┌────────────────────────────────────────────┐
│ getc(stdin); │
└────────────────────────────────────────────┘
다음은 getc를 사용하여 DOS의 type과 같은 일을 하는 프로그램의 한 예이다.
[code]#include <stdio.h>
main(int argc, char *argv[]) {
FILE *fp;
char c;
if (argc == 1) { /* 인자가 없으면 */
printf("USAGE : %s filename(1) ...filename(n)\n",*argv);
return (1);
}
while (--argc > 0) { /* 인자가 있으면 */
if ((fp = fopen(*++argv, "r")) == NULL) {
printf("Error: Cannot open %s\n",*argv);
continue;
}
printf("\n*** %s ***\n",*argv);
while ((c = getc(fp)) != EOF)
putchar(c);
}
fclose(fp);
}
[/code]
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
C:\TC>fileview \config.sys \autoexec.bat 뇌
지금까지는 입력에 대해 알아보았다. 출력의 경우도 마찬가지인데 다음과 같이 fprintf 함수를 사
용하면 된다.
┌────────────────────────────────────────────┐
│ fprintf(fp,"포맷 ,변수 리스트); │
└────────────────────────────────────────────┘
따라서 printf는 다음과 같은 의미를 갖게 된다.
┌────────────────────────────────────────────┐
│ fprintf(stdout,"포맷 ,변수 리스트); │
└────────────────────────────────────────────┘
fprintf를 사용하면 stdout이 아닌 stderr으로도 출력을 내보낼 수 있는데, 이때에도 다음과 같이
사용하면 된다.
┌────────────────────────────────────────────┐
│ fprintf(stderr,"포맷 ,변수 리스트); │
└────────────────────────────────────────────┘
stderr은 에러 메시지를 내보내기 위한 것이기 때문에, 에러 메시지는 이를 통해 내보내는 것이
좋다.
다음 프로그램은 sort [-r] file(1) file(2)와 같이 사용하는 것으로 [ ]는 생략할 수 있는 부분을
의미하며 file(1)화일을 라인 단위로 정렬하여 file(2)에 출력하는데 -r을 지정하면 큰 순서에서 작
은 순서로 정렬하고, 지정하지 않으면 작은 순서에서 큰 순서로 출력하는 것이다.
[code]#include <stdio.h>
#define REVERSE -1
#define NORMAL 1
#define MAXLINENO 100
#define MAXCNO 80
#define TRUE EOF*-1
main(int argc, char *argv[]) {
static char text[MAXLINENO][MAXCNO];
char t[MAXCNO];
FILE *fp1, *fp2;
int direction = NORMAL;
int no = 0;
int i, j;
if ((argc != 3 && argc != 4) || (argc == 4 && strcmp(*(argv+1), "-r"))) {
fprintf(stderr,"USAGE: %s [-r] filename(1) filename(2)\n",*argv);
return (1);
}
if (argc == 4) { /* -r을 주면 역으로 정렬 */
direction = REVERSE;
++argv;
}
if ((fp1 = fopen(*++argv, "r")) == NULL) {
fprintf(stderr,"Error: Cannot open %s\n",*argv);
return (2);
}
if ((fp2 = fopen(*++argv, "w")) == NULL) {
fprintf(stderr,"Error: Cannot create %s\n",*argv);
return (3);
}
while (get_line(fp1, text[no++]) != EOF);
for (i = 0; i < no - 2; i++)
for (j = i + 1; j < no - 1; j++)
if (strcmp(text[i], text[j]) * direction > 0) {
strcpy(t, text[i]);
strcpy(text[i], text[j]);
strcpy(text[j], t);
}
for (i = 0; i < no - 1; i++)
fprintf(fp2,"%s\n",text[i]);
fclose(fp1);
fclose(fp2);
}
int get_line(FILE *fp, char *s) { /* 화일로부터 한 라인을 읽어들임 */
char *cp = s; /* +--------------------+ */
while ((*s = getc(fp)) != EOF) /* | ( DATA ) | */
if (*s == '\n') { /* | Lee Seung Wook | */
*s = '\0'; /* | Kwon Oh Jin | */
return (TRUE); /* | Oh Jung Pyo | */
} /* | Kim Joon Sik | */
else /* | Yun Young Il | */
++s; /* | Kwon Oh Chang | */
if (cp == s) /* | Kim Chang Ju | */
return (EOF); /* | Hong Gil Dong | */
*s = '\0'; /* | Kim Young Hoon | */
return (TRUE); /* | Kim Jong Min | */
} /* +--------------------+ */
[/code]
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
C:\TC>sort data sdata 뇌
C:\TC>type sdata 뇌
Hong Gil Dong
Kim Chang Ju
Kim Jong Min
Kim Joon Sik
Kim Young Hoon
Kwon Oh Chang
Kwon Oh Jin
Lee Seung Wook
Oh Jung Pyo
Yun Young Il
C:\TC>sort -r data rdata 뇌
C:\TC>type rdata 뇌
Yun Young Il
Oh Jung Pyo
Lee Seung Wook
Kwon Oh Jin
Kwon Oh Chang
Kim Young Hoon
Kim Joon Sik
Kim Jong Min
Kim Chang Ju
Hong Gil Dong
다음의 예제는 키보드로부터 입력받은 문장들에 줄번호를 붙여서 test.txt 화일로 저장하는 일을
하는 것으로, 프로그램의 종료는 아무 문장도 입력하지 않고 엔터키를 치면 된다.
[code]
#include <stdio.h>
void main(void) {
FILE *fp;
char str[80];
int line = 0;
if ((fp = fopen("test.txt", "w")) == NULL) {
printf("File open error ... \n");
exit(-1);
}
for ( ; ; ) {
gets(str);
if (str[0] == '\0')
break;
line++;
fprintf(fp,"%3d : %s\n",line,str);
}
fclose(fp);
}
[/code]
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
C:\TC>test 뇌
Time 뇌
can 뇌
never 뇌
mend. 뇌
뇌
C:\TC>type test.txt 뇌
1 : Time
2 : can
3 : never
4 : mend.
또 putchar와 같이 한 글자씩 출력하는 함수도 있는데, 이의 이름은 putc이다. putc는 다음과 같
은 형태로 사용한다.
┌────────────────────────────────────────────┐
│ char c; │
│ FILE *fp; │
│ putc(c,fp); /* fp가 가리키는 화일에 c가 출력되게 된다 */ │
└────────────────────────────────────────────┘
따라서 putchar(c)는 다음과 같은 의미를 갖는다.
┌────────────────────────────────────────────┐
│ putc(c,stdout); │
└────────────────────────────────────────────┘
다음은 putc의 한 사용예이다.
[code]#include <stdio.h>
main() {
char c;
char fname1[20], fname2[20], fname3[20];
FILE *fp1, *fp2, *fp3;
printf("Input the first file name -> ");
gets(fname1);
printf("Input the second file name -> ");
gets(fname2);
printf("Input the file name to create -> ");
gets(fname3);
if ((fp1 = fopen(fname1, "r")) == NULL) {
printf("Error : Cannot open %s\n",fname1);
exit(-1);
}
if ((fp2 = fopen(fname2, "r")) == NULL) {
printf("Error : Cannot open %s\n",fname2);
fclose(fp1);
exit(-1);
}
if ((fp3 = fopen(fname3, "w")) == NULL) {
printf("Error : Cannot create %s\n",fname3);
fclose(fp1);
fclose(fp2);
exit(-1);
}
while ((c = getc(fp1)) != EOF) /* 화일로부터 한자씩 읽어들일 때에는 getc를, */
putc(c,fp3); /* 출력할 때에는 putc를 사용하면 된다 */
fclose(fp1);
while ((c = getc(fp2)) != EOF)
putc(c,fp3);
fclose(fp2);
fclose(fp3);
}
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
Input the first file name -> \config.sys 뇌
Input the second file name -> \autoexec.bat 뇌
Input the file name to create -> sysbat.txt 뇌
이 밖에도 화일에서 한 글자를 입출력하는 함수로 fgetc와 fputc가 있다.
┌────────────────────────────────────────────┐
│ int fgetc(FILE *fp) │
│ int fputc(int c, FILE *fp) │
└────────────────────────────────────────────┘
fgetc 함수는 지정한 화일에서 읽어들인 문자를 되돌리고 화일 포인터를 한 개 증가시킨다. 화일
포인터가 화일의 끝에 도달했을 경우에는 EOF를 되돌린다. 이 EOF값과 0xff값을 구분하기 위해
서 읽어들인 문자는 int형으로 변환되어 반환된다.
fputc함수는 스트림(stream)으로 지정한 화일에 문자 c를 출력한다. 디스크가 꽉찼을 경우나 출
력시 에러가 발생하면 EOF값을 되돌리고, 텍스트 모드로 오픈되어 있으면 '\n'을 CR·LF로 변
환한다.
다음의 예제는 fgetc 함수와 fputc 함수를 이용한 화일복사 프로그램이다.
[code]#include <stdio.h>
void main(int argc, char *argv[]) {
FILE *srcfp, *objfp;
int ch;
if (argc != 3) {
printf("USAGE : %s sourcefile objectfile\n",*argv);
exit(-1);
}
if ((srcfp = fopen(argv[1], "rb")) == NULL) {
printf("File open error : %s\n\a",argv[1]);
exit(-1);
}
if ((objfp = fopen(argv[2], "wb")) == NULL) {
printf("File creation error : %s\n\a",argv[2]);
exit(-1);
}
printf("FileCopy %s to %s\n\n",argv[1],argv[2]);
while ((ch = fgetc(srcfp)) != EOF)
fputc(ch,objfp);
fclose(srcfp);
fclose(objfp);
}
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
C:\TC>scopy tc.exe ec.exe 뇌
FileCopy tc.exe to ec.exe
< 저수준 화일 입출력 함수들 >
저수준의 화일 입출력에서는 FILE이란 구조 대신 간단하게 각 화일마다 번호를 사용하는데, 이
를 화일 식별자(file descriptor), 또는 핸들(handle)이라고 한다. 이 핸들은 0 이상의 값을 가지고
있는데 실제로 0과 1, 2 는 고정된 의미(핸들 0은 표준 입력을 위한 번호이며 1은 표준 출력, 그
리고 2는 표준 에러로 사용)를 갖고 있어서 화일을 처음 열게 되면 그 화일의 핸들은 3이 된다.
저수준의 화일 입출력에서도 화일을 사용하기 위해서는 화일을 먼저 열어야 하며, 이때 다음과
같이 open 함수를 사용한다.
┌────────────────────────────────────────────┐
│ int fd; │
│ fd = open("화일 이름 , 액세스 방식[, 모드]); │
└────────────────────────────────────────────┘
위에서 [ ]는 역시 생략할 수 있는 부분을 의미하며, 화일 이름은 fopen과 같이 열 화일의 이름
이고 액세스 방식은 이 화일을 어떻게 열 것인가인데, fopen과는 달리 다음과 같은 형태로 사용
한다.
┌──────┬─────┬──────────────────────────────────┐
│ 모드 │ 값 │ 내 용 │
├──────┼─────┼──────────────────────────────────┤
│ O_RDONLY │ 0x0001 │ 읽기 전용으로 화일을 연다. │
├──────┼─────┼──────────────────────────────────┤
│ O_WRONLY │ 0x0002 │ 쓰기 전용으로 화일을 연다. │
├──────┼─────┼──────────────────────────────────┤
│ O_RDWR │ 0x0004 │ 읽고 쓰기 위해 화일을 연다. │
├──────┼─────┼──────────────────────────────────┤
│ O_CREAT │ 0x0100 │ 화일이 없을 경우 새로운 화일을 만든다. │
├──────┼─────┼──────────────────────────────────┤
│ O_TRUNC │ 0x0200 │ 현재 있는 화일의 내용을 0으로(제거) 한다. │
├──────┼─────┼──────────────────────────────────┤
│ O_EXCL │ 0x0400 │ O_CREAT과 함께 사용하며, 화일이 없을 경우에만 연다. │
├──────┼─────┼──────────────────────────────────┤
│ O_APPEND │ 0x0800 │ 화일을 쓰기용으로 열고 화일 포인터를 화일의 끝에 위치시킨다. │
├──────┼─────┼──────────────────────────────────┤
│ O_TEXT │ 0x4000 │ 화일을 텍스트 형식으로 연다. │
├──────┼─────┼──────────────────────────────────┤
│ O_BINARY │ 0x8000 │ 화일을 이진 형식으로 연다. │
└──────┴─────┴──────────────────────────────────┘
위의 O_로 시작하는 것들은 모두 상수로 이의 정의는 fcntl.h(이것은 file control의 약자)에 들어
있기 때문에 open 문을 사용하려면 반드시 fcntl.h를 포함하여야 한다.
위의 것들은 액세스 방식의 한 조건들로 여러 개를 동시에 같이 사용할 수 있으며 이 때에는 각
조건들을 '|'(비트 연산자 OR)를 이용해서 묶으면 된다.
data.dat 란 화일을 읽기 전용으로 열고자 할 때에는 다음과 같이 하면 된다.
┌────────────────────────────────────────────┐
│ fd = open("data.dat", O_RDONLY); │
└────────────────────────────────────────────┘
그리고 기존의 화일이 있으며, 이를 지우고 쓰기 전용으로 열 때에는 다음과 같이 하면 된다.
┌────────────────────────────────────────────┐
│ fd = open("data.dat", O_WRONLY | O_TRUNC); │
└────────────────────────────────────────────┘
O_TRUNC이 추가 되었는데, 만약 이를 써 주지 않으면 쓰기 전용이라도 화일의 내용은 없어지
지 않는다. 반면에 화일의 끝에 추가하고자 할 때에는 다음과 같이 O_APPEND를 써 주면 된다.
┌────────────────────────────────────────────┐
│ fd = open("data.dat", O_WRONLY | O_APPEND); │
└────────────────────────────────────────────┘
fopen 문과 다른 것은 해당 화일이 없는 경우 화일이 만들어지지 않는다는 것이다. 만약 화일이
없을 때 화일을 만들도록 하고 싶으면 다음과 같이 O_CREAT를 지정하여야 하며, 이 때에는 세
번재 인자가 필요하다.
┌────────────────────────────────────────────┐
│ fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC, 모드); │
│ /* 화일이 있으면 그 내용을 지우고 없으면 생성, fopen의 "w"와 같음 */ │
│ fd = open("data.dat", O_WRONLY | O_CREAT | O_APPEND, 모드); │
│ /* 화일이 있으면 그 끝으로 이동하고 없으면 생성, fopen의 "a"와 같음 */ │
└────────────────────────────────────────────┘
세번째 인자인 모드에는 액세스 방식에서 O_CREAT 플래그가 지정된 경우, sys\stat.h(TC의 경
우)에 정의되어 있는 다음 기호 중 하나가 사용된다(일반적으로 8진수 0700을 주면 된다).
┌────────────────────────────────────────────┐
│ S_IWRITE /* 써 넣기 가능 */ │
│ S_IREAD /* 읽어 내기 가능 */ │
│ S_IREAD | S_IWRITE /* 읽기 / 쓰기 가능 */ │
└────────────────────────────────────────────┘
읽기용 화일에서는 모드를 생략해도 화일의 사용에는 별 지장이 없다.
그밖에 O_RDWR은 읽고 쓰고자 할 때 사용하며 O_EXCL은 O_CREAT하고만 같이 사용하는데,
지정한 화일이 있으면 에러가 난다. 즉 화일이 없는 상태에서 새로 만들고자 할 때에는 O_EXCL
과 O_CREAT를 같이 사용하면 된다.
open이 제대로 화일을 열게 되면 음이 아닌 정수값을 반환하는데, 그 화일의 핸들을 계산 값으
로 산출하게 되며 에러 발생의 경우 -1을 계산 결과로 산출하게 된다. 따라서 다음과 같이 항상
조사하는 것이 필요하다.
┌────────────────────────────────────────────┐
│ if ((fd = open("data.dat", O_RDONLY)) == -1) { │
│ fprintf(stderr,"Error: Cannot open data.dat\n"); │
└────────────────────────────────────────────┘
화일을 열었으면 이를 읽고 써야 하는데, 읽을 때에는 다음과 같이 read란 함수를 사용한다.
┌────────────────────────────────────────────┐
│ int n; │
│ char *buf; │
│ int size; │
│ int fd; │
│ n = read(fd, buf, size); │
└────────────────────────────────────────────┘
여기서 fd는 open에서 넘겨준 화일의 번호이고 buf는 읽어들일 데이터를 저장할 공간(버퍼)을
가리키는 포인터이다. 그리고 size는 몇 개의 바이트를 읽어들일 것인가, 그 크기를 나타낸다. 즉
read는 화일 fd로부터 size 만큼의 바이트를 읽어들여 이를 buf가 가리키는 곳에 저장하게 된다.
화일 내에 size 만큼의 데이터가 있지 않을 수도 있는데, 이를 조사하기 위해 read는 자신이 실
제로 읽어들인 바이트의 수를 계산 결과로 산출하고 있다. 따라서 read를 사용할 때에는 항상 위
의 n 값을 잘 조사하여야 하며, 이것이 실제 읽어들인 데이터의 수이기 때문에 buf가 가리키는
공간에는 바로 n 바이트가 존재하게 된다. 이 n이 0이라는 것은 읽어들인 데이터가 없다, 즉
EOF라는 의미이며 이 값이 -1이면 이는 읽을 때 에러가 발생한 것을 의미한다. 예를 들어
data.dat란 화일로부터 80 바이트의 데이터를 읽어들이고자 할 때에는 다음과 같이 사용한다.
┌────────────────────────────────────────────┐
│ int fd; │
│ char buf[80]; │
│ int n; │
│ fd = open("data.dat", O_RDONLY); │
│ n = (fd, buf, 80); /* 두번째 인자(buf)에 들어가는 값은 포인터이어야 한다 */ │
└────────────────────────────────────────────┘
이때 n이 80이면 잘 읽은 것이고 다른 값이면 데이터가 부족하거나 뭔가 잘못된 것이다. 일반적
으로 읽을 데이터의 크기는 다르지만 버퍼의 크기는 보통 고정적으로 사용하는데 stdio.h에 정의
되어 있는 BUFSIZ를 사용하는 것이 관례로 되어 있다. 이 크기는 한 화일로부터 가져올 수 있는
데이터의 최적치이기 때문에 많은 양의 데이터를 읽거나 쓸 때에는 이 것을 사용하는 것이 성능
을 최대한 높일 수 있다.
다음은 read 함수의 한 예로서 size file(1) ... fine(n)과 같이 사용하여 각 화일들의 크기를 계산
하여 출력하는 프로그램이다.
[code]
#include <stdio.h>
#include <fcntl.h>
main(int argc, char *argv[]) {
int fd;
long int size;
int n;
char buf[BUFSIZ];
if (argc == 1) {
fprintf(stderr,"USAGE: %s file(1) file(2) ... file(n)\n",*argv);
return (0);
}
while (--argc > 0) {
if ((fd = open(*++argv, O_RDONLY | O_BINARY)) == -1) {
fprintf(stderr,"Error: Cannot open %s\n",*argv);
continue;
}
size = 0l;
while ((n = read(fd, buf, BUFSIZ)) > 0)
size += n;
if (n == 0)
fprintf(stdout,"%s: %ld bytes.\n",*argv,size);
else
fprintf(stderr,"Error in reading %s\n",*argv);
}
close(fd); /* 핸들이 다루는 화일을 닫는다. 되돌림 값은 화일 닫기가 */
} /* 성공하면 0, 실패하면 -1의 값을 되돌린다 */
[/code]
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
C:\TC>size tc.exe tcc.exe 뇌
tc.exe: 290249 bytes.
tcc.exe: 179917 bytes.
저수준의 화일 입출력에서 화일로부터 읽어들이는 것은 이 read 밖에 없다. 즉 문자 하나씩 밖
에 읽어들이지 않기 때문에 만약 정수값을 읽어들이고자 할 때에는 읽어들인 것을 정수로 변환하
여야 한다. 따라서 복잡한 포맷으로 된 데이터를 읽어들일 때에는 고수준의 화일 입출력을 사용
하는 것이 좋다.
read 와 반대로 화일에 쓰고자 할 때는 다음과 같이 write이라는 함수를 사용한다.
┌────────────────────────────────────────────┐
│ int fd; │
│ char *buf; │
│ int size; │
│ int n; │
│ n = write(fd, buf, size); │
└────────────────────────────────────────────┘
fd는 open에 의해 넘겨 받은 화일의 핸들이며 buf는 출력할 데이터가 들어 있는 곳을 가리키는
포인터이고, size는 출력할 데이터의 크기(바이트수)를 의미한다. 즉 fd가 나타내는 화일에 size만
큼의 바이트를 buf로부터 가져와 출력하라는 의미가 된다. 이때 write 함수는 실제로 쓴 바이트의
수를 계산 결과로 산출하게 된다. 이는 항상 size와 같지는 않은데, 예를 들어 디스크가 꽉차서
더 이상 쓸 공간이 없게 되면 지정한 크기 보다 적은 수를 쓰게되므로 n이 size보다 작을 수 있
다. 그리고 에러가 발생한 경우에는 read와 같이 -1을 계산 결과로 산출한다.
다음은 read와 write 함수를 사용하여 한 화일을 똑같이 복사하는 프로그램이다.
[code]#include <stdio.h>
#include <fcntl.h>
main(int argc, char *argv[]) {
int fd1, fd2;
char buf[BUFSIZ];
int n;
if (argc != 3) {
fprintf(stderr,"USAGE: %s sourcefile objectfile\n",*argv);
return (1);
}
if ((fd1 = open(*(argv+1), O_RDONLY | O_BINARY)) < 0) {
fprintf(stderr,"Error: Cannot open %s\n",*(argv+1));
return (2);
}
if ((fd2 = open(*(argv+2), O_WRONLY | O_TRUNC | O_CREAT | O_BINARY, 0700)) < 0) {
fprintf(stderr,"Error: Cannot create %s\n",*(argv+2));
return (3);
}
printf("FileCopy %s to %s\n\n",*(argv+1),*(argv+2));
while ((n = read(fd1, buf, BUFSIZ)) > 0)
if (write(fd2, buf, n) != n) { /* read와 마찬가지로 저수준의 화일 입출력에서 출력
함수는 write 밖에 없다. 따라서 문자 데이터를 출력하기에는 괜찮지만 정수나 실수값을
출력하고자 할 때에는 이를 ASCII 코드 형태로, 즉 문자 형태로 변환한 다음 출력하여야
하기 때문에 그러한 경우에는 고수준의 입출력 함수를 사용하는 것이 더 편리하다 */
fprintf(stderr,"Error in writing %s\n",*(argv+2));
return (4);
}
if (n < 0)
fprintf(stderr,"Error in reading %s\n",*(argv+1));
close(fd1);
close(fd2);
}
[/code]
┌───┐
│ 결과 │
└───┘
---------------------------------------------------------------------------------
C:\TC>slcopy tc.exe ec.exe 뇌
FileCopy tc.exe to ec.exe