[C] 17. 파일 입출력

Wonder_Land🛕·2022년 8월 1일
0

[C]

목록 보기
18/18
post-thumbnail
  1. 스트림
  2. 파일에 입출력하기
  3. Q&A
  4. 마치며

우리가 지금까지 만든 프로그램은, RAM상에 존재하는 데이터였습니다.
즉, 프로그램이 종료되면 해당 데이터도 날라갑니다.

우리가 사용하는 프로그램이나 대부분의 문서는 비휘발성 저장매체인 하드 디스크에 저장되어 있어,
껐다 켜도 사라지지 않습니다.

하드 디스크에 데이터를 보관할 때는 '파일'단위로 데이터를 보관하게 됩니다.

여기서는, 어떻게 파일을 만들고, 파일에 데이터를 저장하고, 파일을 읽어들일 수 있는지 배워볼 것입니다.


1. 스트림(Stream)

우리가 printf 함수를 사용하면 어떤 과정이 발생할까요?

컴퓨터는,
출력할 문자열을 구성해,
모니터에 전달해서 출력하라는 명령을 내릴 것입니다.

이 과정은 절대 쉽지 않습니다.
모니터에 명령을 내리기 위해서는,
모니터 회사마다 그 방식이 다를 것이며, 어떠한 명령을 해야하는지도 다를 것입니다.

그런데 우리는, printf문 하나로 처리했습니다.

그 이유가 바로 '스트림'에 있습니다.

  • 스트림(Stream)
    : 추상화된 장치(Abstract Devies).
    주변 장치(모니터, 키보드, HDD)를 추상화 시켜, 사용자가 마치 동일한 장치에 접근하는 것처럼 사용할 수 있게 함. 이 때, 스트림은 책장과 같이 설계하여, 데이터를 순차적으로 나열해 데이터의 끝까지 차례대로 읽어들일 수 있도록 만들어짐.

스트림은 두 개의 완전히 다른 장치를 이어주는 '파이프'처럼 생각할 수 있습니다.

이 스트림은 우리가 구현하는 것이 아닌,
운영체제가 스스로 처리해줍니다.

우리와 모니터 사이의 스트림을 이용한다면, 운영체제는 모니터에 맞는 명령을,
키보드 사이의 스트림을 이용한다면, 운영체제는 키보드에 맞는 명령을 내릴 것입니다.

즉, 우리는 신경 안써도 되는 것이죠.

그래서 그런지, 우리가 프로그램을 만들면서 한 번도 스트림을 구축한 적이 없습니다.

사실, 모니터와 키보드에 대한 스트림은 '표준 스트림(Standard Stream)'이라고 해서,
프로그램이 실행되면 자동으로 생성됩니다.

모니터에 대한 스트림은 stdout,
키보드에 대한 스트림은 stdin입니다.
(stderr은 표준 오류 스트림인데, 오류 메세지를 출력하는 스트림으로, stdout과 유사합니다)


2. 파일에 입출력하기

1) 파일에 출력하기

다음 예시를 봅시다.

#include <stdio.h>

int main() {
  FILE *fp;
  fp = fopen("a.txt", "w");

  if (fp == NULL) {
    printf("Write Error!!\n");
    return 0;
  }

  fputs("Hello World!!! \n", fp);

  fclose(fp);
  return 0;
}
FILE *fp;
fp = fopen("a.txt", "w");

FILE이라는 구조체의 포인터 변수 fp를 이용하여, 파일을 사용할 수 있게 됩니다.

그리고 fopen 함수는 파일과 소통할 수 있도록 스트림을 만들어줍니다.
첫번째 인자로, 파일명이 들어가서, 해당 파일과 소통할 수 있도록 합니다.
두번째 인자로, "w"가 들어갔는데, 이는 파일에 오직 '쓰기'만 가능하다라는 말입니다.

즉, 출력 스트림만 만든 것입니다.
해당 파일에는 쓰기만 가능합니다.

"w"의 특징은, 해당 파일명이 존재하지 않는다면, 그 이름으로 새로운 파일을 만들고,
파일명이 존재한다면, 해당 파일의 모든 내용을 지웁니다.
그리고 경로가 아닌 파일명만 지정한다면, 소스코드와 동일한 경로에서 파일을 찾습니다.
경로를 지정할 때는 \\\로 적어야 합니다.

if (fp == NULL) {
    printf("Write Error!!\n");
    return 0;
}

만약 파일을 어떠한 경우로 열지 못한 경우,
fopen함수는 NULL를 리턴합니다.

파일을 열었는지 검사하는 내용은 꼭 습관화합시다!!!!

fputs("Hello World!!! \n", fp);

fputs함수를 통해 파일에 출력(기록)을 할 수 있습니다.
첫번째 인자로, 출력할 내용을,
두번째 인자로, 어떠한 스트림을 택할지 포인터를
입력하면 됩니다.

따라서 두 번째 인자로 우리가 사용하는 파일 스트림인 fp를 주면 됩니다.
여기서 stdout을 주게 되면, 우리의 콘솔 화면에 문자열에 출력됩니다.

fclose(fp);

를 통해 연결되었던 스트림을 꼭 닫아주어야 합니다.

만약 닫지 않는다면, 스트림이 남아있게 되어, 파일은 계속 쓰기 상태("w")가 됩니다.
(마치 동적 할당 후 free를 해주는 것과 비슷하네요)

fclose함수를 통해 표준 스트림도 닫을 수 있습니다.
fclose(stdout)을 하게 되면 표준 스트림도 닫을 수 있죠.


2) 파일에서 입력받기

다음 예시를 봅시다.

#include <stdio.h>

int main() {
  FILE *fp = fopen("a.txt", "r");
  char buf[20];  // 내용을 입력받을 곳
  
  if (fp == NULL) {
    printf("READ ERROR !! \n");
    return 0;
  }
  
  fgets(buf, 20, fp);
  printf("입력받는 내용 : %s \n", buf);
  
  fclose(fp);
  return 0;
}

[Result]
Hello World!!

FILE *fp = fopen("a.txt", "r");

앞선 예제와 달리,
이번에는 "r"로 열었습니다.
이번에는 오직 읽기 형식으로 파일을 열게 됩니다.

if (fp == NULL) {
  printf("READ ERROR !! \n");
  return 0;
}

파일에서 입력을 받을 때는 더 주의해야합니다.
쓰기 형식에서는, 파일이 존재하지 않는다면 파일을 새로 만들었지만,
읽기 형식에서는 파일이 존재하지 않는다면 NULL을 반환하고 스트림을 만들지 않습니다.

파일이 제대로 열렸는지 꼭 확인하기!!!!!!!!

fgets(buf, 20, fp);

fgets라는 함수를 통해 파일로부터 문자열을 입력받습니다.
첫번째 인자로, 어디에 입력을 받을지,
두번째 인자로, 입력받을 바이트 수,
세번째 인자로, 어떤 스트림을 통해 입력받을지
를 적으면 됩니다.

fgetsscanf와 달리 안정적입니다.
scanf는 문자열을 입력받을 때, 크기의 제한을 두지 않아 오버플로우가 발생할 수 있습니다.
그러나 fgets는 입력받는 글자수를 제한하기 때문에 이를 방지할 수 있습니다.

또 다른 예시를 봅시다.

#include <stdio.h>

int main() {
  FILE *fp = fopen("a.txt", "r");
  char c;

  while ((c = fgetc(fp)) != EOF) {
    printf("%c", c);
  }

  fclose(fp);
  return 0;
}

위의 코드는 한 글자씩 파일로부터 입력 받습니다.

while ((c = fgetc(fp)) != EOF) {
    printf("%c", c);
}

fgetc함수는 fp에서 한 개의 문자를 입력 받습니다.

이 떄 파일의 맨 마지막에는 EOF(End of File)을 나타내는 값 -1이 있습니다.
(마치, 문자열의 끝이 NULL이 있는 것처럼말이죠.)

따라서, cEOF인지 비교해, 파일의 끝까지 입력 받았는지 확인할 수 있습니다.


3) 파일에 동시에 쓰기&읽기

#include <stdio.h>

int main() {
	FILE* fp = fopen("some_data.txt", "r+");
	char data[100];

	fgets(data, 100, fp);
	printf("현재 파일에 있는 내용 : %s \n", data);

	fputs(" Wonder_Land!!", fp);

	fclose(fp);
}
FILE* fp = fopen("some_data.txt", "r+");

여기서 사용된 "r+"를 통해,
파일을 읽기 및 쓰기형식으로 열 수 있습니다.
만약 파일이 없다면 열지 않습니다.
만약 파일이 있다면, 해당 내용을 지우지 않습니다.
("w+"는 파일이 존재하지 않으면, 파일을 새로 만들고, 있다면 내용을 다 지웁니다.)


4) 파일 위치 지정자(Position Indicator)

스트림의 기본모토는 "순차적으로 입력을 받는다!"입니다.

파일 입출력을 받을 때,
항상 파일의 시작부분에서 끝부분으로 나아갔습니다.
즉, 이전에 입력 받았던 데이터는 다시 입력 받지 않았는데요,
바로 '파일 위치 지정자(Position Indicator)' 덕분입니다.

파일 위치 지정자는 파일에서 다음에 입력 받을 부분을 위치하고 있습니다.

만약, a.txtabcdefg가 들어있고 fgetc로 입력 받는다고 할 때,
파일 위치 지정자는 맨 처음에는 파일의 맨 처음을 가리키고 있습니다.
(여기서는 a겠죠)
그리고 한 글자씩 입력받을 때마다, 다음을 가리킵니다.
(a -> b -> c ... 이런식이죠)

만약 중간에 다시 처음부터 입력받을려면?

  1. fopen으로 파일을 다른 스트림으로 열기
  2. 파일 위치 지정자를 맨 앞으로 옮기기

와 같은 방법이 있습니다.

2번 방법을 볼까요?

#include <stdio.h>

int main() {
  /* 현재 fp 에 abcdef 가 들어있는 상태*/
  FILE *fp = fopen("a.txt", "r");
  fgetc(fp);
  fgetc(fp);
  fgetc(fp);
  fgetc(fp);
  
  fseek(fp, 0, SEEK_SET);
  
  printf("다시 파일 처음에서 입력 받는다면 : %c \n", fgetc(fp));
  fclose(fp);
  return 0;
}
fseek(fp, 0, SEEK_SET);

이 문장을 통해 파일 위치 지정자를 맨 앞으로 이동시킬 수 있습니다.

fseek함수는
파일 위치 지시자의 위치를 사용자가 원하는대로 이동시킬 수 있습니다.

fseek함수는 fp를 세번째 인자로부터 두 번째 인자만큼 떨어진 곳으로 파일 위치지정자를 옮길 수 있습니다.
여기서는 SEEK_SET으로 0만큼 떨어진 곳,
즉, SEEK_SET으로 돌리는 것입니다.

여기서 SEEK_SET파일의 맨 처음을 의미하는 매크로 상수 입니다.
외에도, 현재의 위치를 표시하는 SEEK_CUR
맨 마지막을 표시하는 SEEK_END 상수가 있습니다.

출력 스트림을 사용할 때도 동일한 방식으로 사용하면 됩니다.


5) fscanf 함수

scanf함수는 stdin에서 입력을 받았다면,
fscanf함수는 임의의 스트림에서 입력을 받을 수 있는 보다 더 일반화된 함수입니다.

fscanf(스트림, 형식 지정자, 변수)
fscanf(stdin, "%s", data);
scanf("%s", data);

위 두 문장을 똑같은 문장이죠..

만약

fscanf(fp, "%s", data)

라고 한다면,
파일 스트림에서, 문자열로 데이터를 변수 data에 저장합니다.

fgets 함수는 \n가 나올 때까지 하나의 문자열로 보지만,
fscanf함수는 공백문자(" "(공백), \t, \n)가 나올 때까지 문자열로 봅니다.
그리고 파일 끝에 도달하면 EOF를 반환합니다.

/* 파일에서 'this' 를 'that' 으로 바꾸기*/
#include <stdio.h>
#include <string.h>

int main() {
  FILE *fp = fopen("some_data.txt", "r+");
  char data[100];

  if (fp == NULL) {
    printf("파일 열기 오류! \n");
    return 0;
  }

  while (fscanf(fp, "%s", data) != EOF) {
    if (strcmp(data, "this") == 0) {
      fseek(fp, -(long)strlen("this"), SEEK_CUR);
      fputs("that", fp);

      fflush(fp);
    }
  }

  fclose(fp);
}

위 예시에서,

fseek(fp, -(long)strlen("this"), SEEK_CUR);
fputs("that", fp);

를 통해, 문자열 "this"를 발견하면,
해당 문자열 길이 만큼 파일 위치 지정자를 왼쪽으로 옮기고,
파일 위치 지정자는 t를 가리키기 때문에, 그 상태에서 that을 쓴다면,
thisthat으로 대체합니다.

fflush(fp);

그리고 위 작업이 끝나면,
fflush를 통해 쓰기 작업에서 다시 읽기 작업으로 바뀝니다.

이처럼, 파일을 쓰기 작업에서 읽기 작업으로 바꿀 때는 꼭

fflush(fp);
fseek(fp, 0, SEEK_CUR);

둘 중 하나를 꼭 사용해야만 합니다.


3. Q&A

-


4. 마치며

파일 입출력...
엄청 오랜만이네요..

사실 학교에서 수업들을 때 파일 입출력은
이해도 못하고 그냥 외우기 부분이었는데.

[모두의 코드]덕분에 드디어 파일 입출력을 이해했습니다.

그 때는 스트림이고 뭐고 그냥 외우기만 했거든요.

이제 틀이 잡힌 느낌입니다.

정말로 C언어의 끝이 왔네요🎉

[Reference] : 위 글은 다음 내용을 참고, 인용하여 만들어졌습니다.

profile
아무것도 모르는 컴공 학생의 Wonder_Land

0개의 댓글