티스토리 뷰
주의 사항!
- 이 일지는 작성하고 있는 현시점, 공부와 병행하면서 작성되고 있습니다.
- 공부 중에 떠오른 생각이나 그때그때의 개념정리 같은 내용이 포함됩니다.
- 따라서 이 일지의 내용은 제가 공부하고 이해한 대로 작성되기 때문에 실제 사실과는 다를 수 있습니다.
우리가 만들 프로그램이 하드디스크에 있는 데이터를 처리하기 위해 하드디스크를 직접 제어해야 한다면
괴장히 복잡한 과정을 거쳐야 합니다.
다행스럽게도 C언어는 이 과정을 쉽게 수행할 수 있도록 다양한 파일 입출력 함수들을 제공합니다.
지금까지 우리는 메모리를 관리하는 방법에 대해서만 배워왔습니다.
조립 컴퓨터에 관심이 있다면 램이라는 것도 알텐데요.
제 컴퓨터에는 16G의 램이 장착되어 있습니다.
우리는 지금까지 이 램에 관해서만 데이터를 다루는 방법들을 배워왔습니다.
하지만 컴퓨터에는 램 뿐만 아니라 데이터 저장 장치로 하드디스크와 SSD도 있습니다.
이제 이들의 데이터를 다루는 방법을 배우게 될 것 같습니다.
파일을 입출력하려면 먼저 어떤 용도로 사용할지를 결정하여 원하는 데이터 파일을 하드디스크에서 찾아야 합니다.
이렇게 데이터를 입출력하기 전에 준비하는 과정이 '파일 개방'입니다.
그리고 사용이 끝난 파일은 다시 닫아주어야 합니다.
fopen함수로 파일을 개방하고, fclose함수로 파일을 닫습니다.
예제를 통해 살펴보겠습니다.
#include <stdio.h>
int main(void)
{
FILE* pFile;
pFile = fopen("a.txt", "r");
if (pFile == NULL)
{
printf("파일이 열리지 않았습니다.\n");
return 1; //프로그램 종료
}
printf("파일이 열렸습니다.\n");
fclose(pFile);
return 0;
}
FILE* pFile;
먼저 파일 포인터 자료형을 사용해 파일의 포인터를 선언해주었습니다.
pFile = fopen("a.txt", "r");
if (pFile == NULL)
{
printf("파일이 열리지 않았습니다.\n");
return 1; //프로그램 종료
}
그리고 해당 포인터에 개방할 파일의 주소를 대입하고, 주소가 올바르게 대입되었는지 확인하고 있습니다.
개방할 파일을 찾지 못하면 fopen함수는 NULL포인터를 반환합니다.
이후 반환값이 NULL이면 프로그램을 종료합니다.
이 형태는 메모리 공간을 동적으로 할당하던 형태와 매우 유사합니다.
fopen함수에 주어진 두 인수는 '파일의 경로'과 '개방 모드'입니다.
위와 같이 파일의 경로를 앞에 적지 않고 이름만 적으면,
이 함수는 현재 "a.txt"라는 이름의 파일을 찾고 있는 이 프로그램의 현재 경로를 바탕으로 경로를 설정합니다.
예를 들어 설명해보겠습니다.
여러분은 '김철수'라는 사람을 찾고 싶습니다.
그런데 어디에 있는 김철수를 찾아야 할까요? 서울? 울산? 부산? 북한??
이런 위치, 경로를 알려주지 않으면 프로그램은 바로 여러분이 있는 곳에서 김철수를 찾습니다.
여러분이 '서울시 구로구 구로동'에 위치해 있다고 가정하겠습니다.
그 상태에서 프로그램에게 '김철수'를 찾아달라고 한다면, 프로그램은 서울시 구로구 구로동에서 김철수를 찾습니다.
만약 '부산 해운대구 우동'의 김철수를 찾고 싶다면 프로그램에 '부산 해운대구 우동 김철수'를 찾아달라고 해야 합니다.
마찬가지로 만약 프로그램의 실행파일의 위치가 'C:\programFile'이라면,
프로그램은 'C:\programFile\a.txt'를 찾습니다.
만약 'C:\source\a.txt'를 찾길 원한다면 해당 경로를 같이 적어주어야 합니다.
그런데 이때 주의할 점이 하나 있습니다.
백스래시는 두번씩 사용해야 합니다. 즉 'C:\source\a.txt'가 아닌 'C:\\source\\a.txt'로 적어야 합니다.
왜냐면 문자열에서 백슬래시(\)는 제어문자의 시작을 뜻하기 때문입니다.
\n은 줄바꿈, \t는 탭, \a는 알림소리 등과 같이 백슬래시(\)는 제어문자의 시작을 나타내므로
문자열 안에서 백슬래시를 적고 싶다면 백슬래시백슬래시(\\)로 적어야 합니다.
단, 이 기준은 프로그램 개발이 다 끝나고 실행 파일을 직접 실행할 때 적용됩니다.
VC++같은 통합 개발 환경에서는 프로젝트 폴더가 현재 작업 디렉터리가 되므로
예제를 실습할 때는 개방할 파일을 프로젝트 폴더에 저장합니다.
개방 모드는 개방할 파일의 용도를 표시합니다.
기본적인 개방모드는 세 가지가 있습니다.
r : 읽기 위해 개방 / 파일이 없으면 NULL 반환
w : 파일의 내용을 지우고 새로 쓰기 위해 개방 / 파일이 없으면 해당 이름의 파일을 생성
a : 파일의 끝에 내용을 추가하기 위해 개방 / 파일이 없으면 해당 이름의 파일을 생성
fclose(pFile);
사용이 끝난 파일은 fclose 함수를 이용해 다시 닫아주어야 합니다.
파일을 성공적으로 닫으면 0을 반환하고, 그렇지 않으면 EOF(-1)를 반환합니다.
파일을 개방했다고 해서 정말로 그 파일을 불러와 데이터를 수정하거나 삭제하거나 추가할 수 있는 것이 아닙니다.
파일을 개방하면 '스트림 파일'을 메모리에 만듭니다.
스트림 파일은 실제 파일이 저장되어 있는 저장장치와 프로그램 사이에서 징검다리 역할을 합니다.
실제 파일의 복사본을 떠와서 파일의 데이터를 수정하는 것을 임시로 저장하는 역할을 한다고 보면 쉽습니다.
fclose함수를 이용해 이 스크림 파일을 정상적으로 닫으면 수정된 내용이 실제 파일에 그대로 저장됩니다.
스트림 파일은 문자 배열 형태의 버퍼를 가지고 있습니다.
프로그램에 숫자나 문자 등을 입력할 때 배웠던 버퍼와 비슷합니다.
실제 저장장치에서 데이터를 처리하는 속도는 프로그램이 메모리에서 데이터를 처리하는 것보다 훨씬 느립니다.
때문에 파일에 데이터를 출력할 땐 버퍼에 출력하고 버퍼에서 파일로 데이터를 출력하며
파일에서 데이터를 입력할 때는 버퍼에 우선 한꺼번에 담아두었다가 필요할 때 버퍼에서 꺼내쓰는 식입니다.
파일이 개방되면 데이터를 입출력할 준비가 된 겁니다.
실질적인 데티어 입추력은 함수를 통해 수행하면 이때 파일 포인터를 함수의 인수로 줍니다.
간단한 예제를 통해 함수 사용법과 데이터 입력 과정을 살펴보겠습니다.
아, 우선 먼저 해야할 일이 있습니다.
현재 코드를 작성하고 있는 프로젝트 폴더 안에 'a'라는 이름의 텍스트 파일을 생성합니다.
그리고 해당 텍스트 파일에 아무 문장을 적고 저장합니다.
저는 "hello world, my name is KOEY!"라고 적었습니다.
#include <stdio.h>
int main(void)
{
FILE* pFile;
pFile = fopen("a.txt", "r");
if (pFile == NULL)
{
printf("파일이 열리지 않았습니다.\n");
return 1;
}
int ch;
while (1)
{
ch = fgetc(pFile);
if (ch == EOF) break;
putchar(ch);
}
fclose(pFile);
return 0;
}
/*
실행결과
hello world, my name is KOEY!
*/
FILE* pFile;
먼저 스트림 파일의 주소를 저장할 파일 포인터를 선언했습니다.
ch = fgetc(pFile);
if (ch == EOF) break;
putchar(ch);
문자를 저장할 변수 ch를 int형으로 선언했습니다.
보통 문자를 저장하는 변수의 자료형은 char형을 사용했습니다.
하지만 fgetc 함수의 반환형이 int형이기 때문에 int형으로 선언했습니다.
문자를 입력하는데 왜 반환형이 int형인지는 아직 모르겠습니다.
fgetc 함수는 더 이상 읽을 데이터가 없으면 EOF를 반환합니다.
처음 a.txt 파일에 한글을 저장해봤는데 한글은 이상하게 깨져서 옵니다. 무언가 다른 방법이 필요해 보입니다.
fgetc함수가 파일에서 데이터를 읽어들이는 과정은 다음과 같습니다.
우선 파일의 데이터를 모두 스트림 파일의 버퍼에 가져옵니다.
이때 버퍼의 크기만큼만 가져옵니다.
만약 파일의 크기가 버퍼보다 작다면 파일의 모든 데이터를 버퍼로 가져올 수 있지만
파일의 크기가 버퍼보다 더 크다면 버퍼에 수용할 수 있을 만큼의 데이터만 가져옵니다.
그리고 fgetc 함수를 사용할 때마다 버퍼에 저장된 데이터를 하나씩 가져오게 됩니다.
만약 버퍼에 더 이상 남는 데이터가 없는데 또 fgetc 함수를 호출하면
파일의 나머지 데이터를 버퍼의 크기만큼 다시 가져옵니다.
만약 더 불러들일 데이터가 없다면 EOF를 반환합니다.
fgetc 함수가 하드디스크에 있는 파일의 입력이 끝났음을 확인하는 방법은
파일의 크기와 현재까지 읽어 들인 데이터의 크기를 비교하여 판단합니다.
문자열 같은 경우 문자열의 끝에 널문자를 넣어 문자열의 끝을 표시하였지만
파일의 경우 이렇게 끝을 포시하는 어떤 정보도 포함되지 않습니다.
한 문자를 파일로 출력할 때는 fputc 함수를 사용합니다.
fputc 함수에 출력할 문자와 파일 포인터를 인수로 주면 파일로 문자를 출력합니다.
반환값은 출력한 문자를 다시 반환하며 에러가 발생하면 EOF를 반환합니다.
예제를 통해 살펴보겠습니다.
#include <stdio.h>
int main(void)
{
FILE* pFile;
char str[] = "banana";
pFile = fopen("a.txt", "w");
if (pFile == NULL)
{
printf("파일을 만들지 못했습니다.\n");
return 1;
}
int i = 0;
while (str[i] != '\0')
{
fputc(str[i], pFile);
i++;
}
fputc('\n', pFile);
fclose(pFile);
return 0;
}
위 예제를 실행하면 앞서 생성했던 a.txt 파일의 내용이 banana로 바뀌게 됩니다.
파일의 데이터를 읽을 때는 한글이 이상하게 읽어졌던 것과 반대로
한글을 파일에 출력하는 것은 정상적으로 기능합니다.
fputc 함수를 호출하면 스트림 파일의 버퍼에 우선 해당 문자를 저장합니다.
그러다가 중간에 버퍼가 가득 차게 되면 버퍼의 데이터를 모두 파일에 저장하고 버퍼의 처음부터 다시 문자를 채웁니다.
또 버퍼가 가득 차지 않아도 중간에 개행문자(\n)를 출력하거나 새로운 입력을 수행하는 경우
버퍼의 데이터를 파일로 출력합니다.
운영체제는 프로그램을 실행할 때 기본적으로 3개의 스트림 파일을 만듭니다.
그리고 이들을 키보드와 모니터 등에 연결하여 입출력 함수들이 파일 포인터 없이 사용할 수 있도록 제공합니다.
다음 예제를 통해 운영체제가 개방한 스트림 파일을 사용하는 과정을 살펴보겠습니다.
#include <stdio.h>
int main(void)
{
int ch;
while (1)
{
ch = getchar();
if (ch == EOF) break;
putchar(ch);
}
return 0;
}
/*
실행결과
KOEY!
KOEY!
^Z
*/
운영체제에 따라 기본적으로 개방하는 스트림 파일 수는 다를 수 있지만
다음 3개의 스트림 파일은 공통적으로 개방합니다.
stdin, stdout, stderr
stdin은 키보드와 연결된 입력 스트림 파일이고, 나머지는 모니터와 연결되어 있습니다.
stdout은 표준 출력 스트림 파일이고, stderr는 표준 에러 스트림 파일입니다.
getchar함수는 내부적으로 stdin 스트림 파일을 사용합니다.
운영체제가 기본적으로 개방하는 스트림 파일은 scanf, printf, getchar, putchar, gets, puts 등
표준 입출력 함수들이 사용하지만 파일 포인터를 인수로 받는 함수도 사용할 수 있는 방법이 있습니다.
stdin, stdout, stderr를 직접 fgetc 함수나 fputc 함수 등의 인수로 사용하는 것입니다.
예제를 통해 확인해 보겠습니다.
#include <stdio.h>
int main(void)
{
int ch;
while (1)
{
ch = fgetc(stdin);
if (ch == EOF) break;
fputc(ch, stdout);
}
return 0;
}
/*
실행결과
banana KOEY
banana KOEY
^Z
*/
fgetc 함수에 인수로 stdin을 주어서 프로그램이 키보드 입력을 받도록 하였습니다.
fgetc 함수에 파일 포인터를 인수로 주어 파일의 내용을 입력하던 것과 다릅니다.
fputc 함수도 마찬가지 원리가 적용되었습니다.
파일은 데이터의 기록 방식에 따라 텍스트(text) 파일과 바이너리(binary) 파일로 나눕니다.
텍스트 파일은 데이터를 아스키 코드값에 따라 저장한 것이며, 그 이외의 방식으로 저장된 파일은 바이너리 파일입니다.
바이너리 파일은 해당 기록 방식을 적용한 별도의 프로그램을 사용해야 합니다.
예를 들어 텍스트 파일은 메모장에서 내용을 확인할 수 있으나 그림 파일을 보기 위해서는 그림판을 사용해야 합니다.
파일 입출력 함수들도 파일의 형태에 따라 데이터를 읽고 쓰는 방식이 다릅니다.
따라서 파일을 개방할 때 개방 모드에 파일의 형태도 함께 표시해야 합니다.
개방 모드에 텍스트 파일은 't', 바이너리 파일은 'b'를 추가하여 개방합니다.
처음 개방 모드에 대해 r, w, a 세 가지를 배웠습니다. 바이너리 파일의 경우 rb, wb, ab라고 표시합니다.
파일의 형태를 별도로 표시하지 않을 경우 텍스트 파일로 개방합니다.
만약 파일의 형태와 개방 모드가 다르면 문제는 심각해집니다.
간단한 예제를 통해 확인해보겠습니다.
#include <stdio.h>
int main(void)
{
FILE* pFile;
int ary[10] = {13, 10, 13, 13, 10, 26, 13, 10, 13, 10};
int res;
pFile = fopen("a.txt", "wb");
for (int i = 0; i < 10; i++)
{
fputc(ary[i], pFile);
}
fclose(pFile);
pFile = fopen("a.txt", "rt");
while (1)
{
res = fgetc(pFile);
if (res == EOF) break;
printf("%4d", res);
}
fclose(pFile);
return 0;
}
/*
실행결과
10 13 10
*/
pFile = fopen("a.txt", "wb");
for (int i = 0; i < 10; i++)
{
fputc(ary[i], pFile);
}
fclose(pFile);
먼저 a.txt 파일을 바이너리 모드로 개방했습니다.
이후 해당 파일에 아스키코드로 10개의 문자를 출력했습니다.
아스키 코드로 13은 \r, 10은 \n, 26은 ctrl+Z입니다.
해당 파일을 열어 직접 확인해보면 크기가 10바이트로 총 10개의 문자가 저장돼 있음을 확인할 수 있습니다.
pFile = fopen("a.txt", "rt");
while (1)
{
res = fgetc(pFile);
if (res == EOF) break;
printf("%4d", res);
}
fclose(pFile);
그리고 이 파일을 이번에는 텍스트 파일로 개방하고, 해당 파일에 저장된 데이터를 모두 입력받습니다.
그리고 입력 받은 데이터를 화면에 출력합니다.
출력된 결과를 보면 10 13 10 이렇게 세 개만 출력이 되어 있습니다.
이유는 fgetc 함수가 텍스트 모드로 개방된 파일을 읽는 방식 때문입니다.
fgetc 함수는 \r과 \n이 연달아 나오면 이 둘을 하나의 \n처럼 입력합니다.
그리고 ctrl+Z를 읽으면 EOF를 반환합니다.
따라서 처음 입력한 10개의 문자 중 첫 번째와 두 번째 문자인 13과 10은 통틀어 10으로 읽고,
이어 세번째의 13을 읽고,
이어서 네번째의 13과 다섯번째의 10이 연달아 나오므로 이를 통틀어 10으로 읽습니다.
이후 ctrl+Z값을 읽게 되므로 이를 파일의 끝으로 판단하고 EOF를 반환하며 화면에 데이터 출력을 종료합니다.
그래서 실행결과는 10 13 10으로 나옵니다.
만약 파일을 rt가 아닌 rb 개방모드로 개방했다면 10개의 문자가 모두 올바르게 나옵니다.
파일 개방 모드는 기본적으로는 읽고(r모드), 쓰고(w모드), 붙이는(a모드) 세 가지 모드가 있으나
'+'를 사용하면 일고 쓰는 작업을 함께 할 수 있습니다.
개방 모드 | 파일이 있을 때 |
r+ | 텍스트 파일에 읽고 쓰기 위해 개방 |
w+ | 텍스트 파일의 내용을 지우고, 읽거나 쓰기 위해 개방 |
a+ | 텍스트 파일을 읽거나 파일의 끝에 추가하기 위해 개방 |
rb+ | 바이너리 파일에 읽고 쓰기 위해 개방 |
wb+ | 바이너리 파일의 내용을 지우고, 읽거나 쓰기 위해 개방 |
ab+ | 바이너리 파일을 읽거나 파일의 끝에 추가하기 위해 개방 |
예제를 통해 '+'모드의 사용법을 살펴보겠습니다.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
FILE* pFile;
pFile = fopen("a.txt", "w+");
if (pFile == NULL)
{
printf("파일을 읽지 못했습니다.\n");
return 1;
}
while (1)
{
char str[20];
printf("과일 이름 : ");
fgets(str, 20, stdin);
str[strlen(str) - 1] = '\0';
if (strcmp(str, "end") == 0) break;
if (strcmp(str, "list") == 0)
{
fseek(pFile, 0, SEEK_SET); //버퍼의 위치 지시자를 맨 처음으로 이동
while (1)
{
fgets(str, sizeof(str), pFile);
if (feof(pFile)) break;
printf("%s", str);
}
}
else
{
char* pStr;
pStr = (char*)malloc(sizeof(char) * (strlen(str) + 1));
if (pStr == NULL)
{
printf("메모리가 부족합니다.\n");
exit(1);
}
strcpy(pStr, str);
fprintf(pFile, "%s\n", pStr);
free(pStr);
}
}
fclose(pFile);
return 0;
}
/*
실행결과
과일 이름 : apple
과일 이름 : banana
과일 이름 : list
apple
banana
과일 이름 : strawberry
과일 이름 : list
apple
banana
strawberry
과일 이름 : end
*/
pFile = fopen("a.txt", "w+");
파일을 개방할 때 개방모드는 w+입니다.
따라서 파일 안의 내용은 모두 지우고, 이후 새로 쓰거나 파일의 내용을 읽을 수 있게 합니다.
char str[20];
printf("과일 이름 : ");
fgets(str, 20, stdin);
str[strlen(str) - 1] = '\0';
이후 반복문 안에서 문자열을 저장할 문자 배열을 선언하고, 문자를 입력받습니다.
fgets 함수는 stdin 스트림 파일의 버퍼로부터 데이터를 입력받아 str에 저장합니다.
stdin 스트림 함수는 운영체제 자체에서 개방하는 스트림 파일로서 키보드와 연결되어 있습니다.
fgets함수는 문자를 입력하고 마지막에 치는 엔터까지 같이 문자로서 읽어들이기 때문에
str의 마지막 요소에는 '\n' 개행문자가 입력되어 있습니다. 따라서 이 개행문자를 널문자로 바꿔주었습니다.
그런데 이렇게 하니 파일에 값을 출력하고 다시 읽어들이는 과정에서
앞서 출력했던 문자를 제대로 입력받지 못하는 오류가 발생했습니다.
fgets 함수로 문자를 입력받고 이를 다시 파일로 출력하는 과정에서
이 문자배열의 어떤 요소가 문제를 일으키는 듯 보였습니다.
앞에서 '\n' 개행문자를 널문자로 바꿔주었기 때문에,
혹시 'str배열에 널문자가 두개가 들어갔나?' 하는 생각이 들었습니다.
fgets 함수가 개행문자까지 입력받고 이후 그 뒤에 자동으로 널문자를 추가저장했다고 가정하면
제가 개행문자를 널문자로 바꿔주었으므로 결국 해당 문자열의 끝에 널문자가 2개 들어가게 됩니다.
이것 때문에 문제가 발생하는 것으로 생각했고, 임시 문자를 새로 저장하는 메모리 공간을 동적으로 할당하여
파일에 출력할 문자열에 널문자가 2개씩 들어가는 일이 없도록 했습니다.
fseek(pFile, 0, SEEK_SET);
fseek 함수는 저도 이번에 처음 봤습니다. 이 함수는 버퍼 안에서 위치 지시자를 다루는 함수인 것 같습니다.
버퍼에서 값을 하나씩 읽어들일 때,
데이터 하나를 읽으면 자동으로 그 다음 데이터를 읽도록 하는 것이 이 위치 지시자의 역할입니다.
위치 지시자가 없다면 버퍼의 첫 번째 데이터만 사용할 수 있을 것입니다.
하나를 읽으면 그 다음 거, 그리고 또 그 다음 거,
이렇게 순서대로 버퍼의 내용을 읽을 수 있도록 하는 것이 위치 지시자입니다.
fseek 함수를 이용해 pFile 스트림 파일의 버퍼의 위치 지시자의 위치를 0으로 설정합니다.
첫 번째 인수는 어느 스트림 파일의 버퍼의 위치 지시자를 다룰 것인지,
두 번째 인수는 offset 값으로 기준 위치로 부터 다음 방향이면+, 이전 방향이면- 의 얼마만큼 이동 시킬 것인지,
세 번째 인수는 기준 위치는 어디인지를 알립니다.
SEEK_SET은 가장 처음위치, SEEK_CUR는 현재 위치, SEEK_END는 가장 끝 위치를 반환합니다.
위 함수는 pFile 스트림 파일의 버퍼의 위치 지시자를 가장 처음 위치에서 0만큼 offset한 위치로 설정하므로
버퍼 위치 지시자를 가장 처음으로 이동시키게 됩니다.
다음 시간은 '다양한 파일 입출력 함수'에 대해 공부하겠습니다.
'공부 일지 > C언어 공부 일지' 카테고리의 다른 글
파일 입출력 실전문제1 (0) | 2021.02.01 |
---|---|
다양한 파일 입출력 함수 (0) | 2021.02.01 |
사용자 정의 자료형 실전문제2 (0) | 2021.01.31 |
사용자 정의 자료형 실전 문제1 (0) | 2021.01.31 |
구조체 활용, 공용체, 열거형 (0) | 2021.01.31 |