프로그래밍 언어/C언어

C언어 19. 파일 입출력-2

닉네임못짓는사람 2020. 7. 24. 17:14
반응형

저번 글에선 파일 입출력이 무엇인지 알아보았습니다.

이어서 이번 글에선 실제로 파일에 데이터를 출력하고, 파일로부터 입력을 받아봅시다.

 

일단 복습하는 의미로 파일 포인터를 파일과 연결해보도록 합시다.

FILE* ifp;
	ifp = fopen("C:\\asd\\ex.txt", "r");
	if (ifp == NULL) {
		return 1;
	}

파일에 하나의 문자를 입, 출력하자


먼저 파일을 개방한 뒤, 하나의 문자를 입출력해보겠습니다.

이때 사용하는 함수는 입력용이 fgetc, 출력용이 fputc인데, 먼저 fgetc부터 알아보겠습니다.

fgetc의 함수 원형은 다음과 같습니다.

 

int fgetc(FILE*);

이 함수는 파일 포인터를 통해 파일과 연결한 뒤, 파일로부터 프로그램에 한 개의 문자를 입력받습니다.

위의 코드에 있는 ex.txt파일에서 문자 하나를 입력받는 코드를 작성해보도록 하겠습니다.

ex.txt파일의 내용은 다음과 같습니다.

#include <stdio.h>

int main() {
	FILE* ifp;
	char ch;
	ifp = fopen("C:\\asd\\ex.txt", "r");
	if (ifp == NULL) {
		printf("입력파일 개방 오류");
		return 1;
	}
	ch = fgetc(ifp);
	printf("입력받은 문자 : %c", ch);
	fclose(ifp);
	return 0;
}

파일 포인터 ifp를 선언한 뒤, c드라이브 asd폴더의 ex.txt파일을 입력용으로 개방해서 연결해줍니다.

그 뒤 fgetc를 사용해 하나의 문자를 입력받은 뒤, 이를 출력하고 파일은 닫아주도록 합니다.

이런 일련의 과정들을 좀 더 자세히 알아보도록 합시다.

 

위의 코드에서 fgetc함수를 다시 한번 실행하면 과연 어떤 값이 출력될까요?

실제로 코드로 확인해보면 다음 값은 파일의 두 번째 글자인 e가 출력됩니다.

그럼 이 작업을 여러 번 실행할 때, fgetc함수는 매번 파일에서 한 글자씩 읽어오는 것일까요?

실제론 위와 같이 매번 한 글자씩 읽어오는 것이 아닙니다.

스트림 파일에 입력 요청이 발생하면 연결된 파일에서 적당한 크기의 데이터를 한 번에 버퍼에 저장하는데,

이때 이 버퍼의 크기는 512byte 또는 1024byte입니다.

 

그런 뒤 fgetc함수에서 이 버퍼의 첫 번째 문자를 읽어서 반환해줍니다.

그다음 두 번째 fgetc함수를 사용할 때를 생각해보도록 합시다.

이때 필요한 것이 파일 포인터에 있는 위치 지시자로써,

fgetc함수에게 어느 위치의 문자를 읽어야 하는지 알려줍니다.

fgetc함수는 이 위치 지시자를 따라서 그 위치에 있는 문자를 읽어오는 것입니다.

이 위치 지시자의 값은 파일을 개방했을 때 0의 초기값을 가지며,

입출력 함수들이 실행될 때마다 그만큼 값을 증가시킵니다.

예를 들어 fgetc함수를 2번 실행하게 되면 위치 지시자의 값은 2가 되어 l을 가리키게 됩니다.

 

버퍼에 있는 데이터를 모두 읽은 뒤, fgetc함수가 실행되면,

연결된 파일에서 다시 버퍼로 적당한 크기만큼 데이터를 가져옵니다.

이런 과정을 반복해서 파일의 데이터를 모두 입력받게 되면 fgetc함수는 -1을 반환합니다.

이 -1은 보통 EOF로 기호화해서 사용하는데, 기호화 방법은 다음에 알아보도록 하겠습니다.

이제 fgetc함수를 반복 사용해서 파일의 데이터를 모두 읽어와 보도록 합시다.

#include <stdio.h>

int main() {
	FILE* ifp;
	char ch;
	ifp = fopen("C:\\asd\\ex.txt", "r");
	if (ifp == NULL) {
		printf("입력파일 개방 오류");
		return 1;
	}
	while ((ch = fgetc(ifp)) != -1) {
		printf("%c", ch);
	}
	fclose(ifp);
    	return 0
}

반복문 설명 때 이야기했던 것처럼 파일에서 데이터를 입력받을 땐 주로 while문을 사용합니다.

fgetc의 값이 -1이 될 때까지 계속해서 반복하면 되기 때문에 편리하게 사용할 수 있습니다.

위에서 말한 일련의 과정은 모든 파일 입력 함수에 똑같이 적용된다는 점 기억해두시기 바랍니다.

 

다음은 파일에 하나의 문자를 출력하는 fputc함수에 대해 알아보도록 하겠습니다.

fputc함수의 원형은 다음과 같습니다.

int fputc(int, FILE*);

fputc함수의 첫 번째 전달 인자는 출력할 문자를 입력하고, 두번째 전달인자는 파일 포인터입니다.

또한 리턴값으로 자신이 출력한 문자를 다시 반환해줍니다.

위의 ex.txt파일의 내용을 모두 지우고 fputc함수를 사용해 문자를 출력해봅시다.

#include <stdio.h>

int main() {
	FILE* ofp;
	char ch = 'a';
	ofp = fopen("C:\\asd\\ex.txt", "w");
	if (ofp == NULL) {
		printf("출력파일 개방 오류");
		return 1;
	}
	while ((ch = getchar()) != -1) {
		fputc(ch, ofp);
	}
	fclose(ofp);
	return 0;
}

이전에 배웠던 getchar함수를 사용해서 사용자에게 문자를 입력받아 파일에 입력하는 코드입니다.

결과를 확인해보면 정상적으로 실행되었는지 한눈에 확인 가능하실 겁니다.

또한 w모드로 개방하면 기존 파일의 내용은 모두 지워지기 때문에 따로 지우실 필요는 없습니다.

 

이 fputc함수 또한 fgetc함수처럼 스트림 파일의 버퍼를 사용해서

문자를 저장해두었다가 한 번에 파일로 출력하는 형태로 작동합니다.

이와 같은 형태는 getchar에도 동일하게 적용됩니다.

getchar함수를 실행한 뒤 사용자가 연속된 문자열 형태로 데이터를 입력하게 되면,

이를 스트림 파일의 버퍼에 저장해두었다가 getchar함수가 실행될 때마다 하나씩 가져오는 것입니다.

운영체제가 개방하는 스트림 파일


그런데 우리는 지금까지 getchar에 사용될 스트림 파일을 개방했던 적이 없습니다.

그럼 이 스트림 파일은 어디서 가져와 사용하는 것일까요?

사실 이 스트림 파일은 우리가 따로 개방하는 것이 아니고 운영체제가 개방해주는 파일입니다.

운영체제는 프로그램이 실행될 때 기본적으로 몇 개의 스트림 파일을 만들어놓고,

이들을 키보드와 모니터 등에 연결해줍니다.

그리고 이것들을 입출력 함수들이 편리하게 사용할 수 있도록 해주는 것입니다.

운영체제에서 개방해주는 스트림 파일은 운영체제마다 다르지만 다음의 3가지는 공통으로 개방합니다.

스트림 파일의 이름은 운영체제가 만든 스트림 파일의 파일 포인터입니다.

이 파일 포인터를 사용해 프로그램 내에서 스트림 파일에 접근하여 사용할 수 있는 것입니다.

 

따라서 우리가 지금까지 사용했던 getchar함수와 putchar함수는

내부적으로 stdin과 stdout파일 포인터를 사용하고 있었던 겁니다.

이 파일 포인터들은 위의 두 함수뿐만이 아니라 fgetc와 fputc와 같은 함수에서도 사용할 수 있습니다.

#include <stdio.h>

int main() {
	char ch;

	while ((ch = fgetc(stdin)) != -1) {
		fputc(ch, stdout);
	}
	return 0;
}

이처럼 말이죠. 또한 fgetc함수도 getchat함수와 동일하게 ctrl+z를 입력하면 -1을 리턴합니다.

파일에 문자열을 입출력하자


그러면 이번엔 문자열을 한 번에 입출력하는 함수에 대해 알아보도록 하겠습니다.

먼저 문자열을 입력받는 함수는 fgets함수이며, 함수 원형은 다음과 같습니다.

char* fgets(char*, int, FILE*);

fgets의 전달 인자는 순서대로 입력받은 문자를 저장할 char형 배열의 포인터,

파일로부터 읽을 문자열의 길이, 파일 포인터입니다.

위의 함수를 사용하면 두 번째 인자의 값만큼 연결된 파일에서 문자열을 읽어와

첫 번째 인자에 입력한 배열에 저장할 것입니다.

 

이때 주의해야 할 점은 fgets로 읽어오는 문자열은 두 번째 인자의 숫자보다 한 개 적습니다.

그 이유는 문자열의 끝에 NULL문자를 입력해주어야 하기 때문입니다.

따라서 banana가 입력돼있는 파일로부터 문자를 6개 입력받으면 실제론 banan만 입력받게 됩니다.

 

또한 데이터의 중간에 줄 바꿈 문자(\n)가 입력되어 있을 경우엔

그곳까지만 데이터를 읽고 리턴합니다.

따라서 fgets함수는 문자열을 줄단위로 입력받는 함수라고 할 수 있습니다.

 

이번엔 반환 값을 한번 살펴봅시다.

fgets의 반환값은 char형 포인터인데, 이는 데이터를 입력한 배열의 포인터입니다.

즉, 첫 번째 인자로 준 포인터 값을 그대로 반환하는 것인데,

fgets함수를 호출했을 때, 더 이상 읽어올 데이터가 없을 땐 NULL포인터를 리턴합니다.

때문에 fgets함수를 여러 번 호출하여 파일의 끝까지 데이터를 읽어오는 코드를 작성해야 할 땐

fgets의 값을 -1이 아닌 NULL과 비교하여 사용해야 합니다.

 

실제 코드를 통해 사용법을 알아보도록 합시다.

#include <stdio.h>

int main() {
	FILE* ifp;
	char ch_Arr[50];
	ifp = fopen("C:\\asd\\ex.txt", "r");
	if (ifp == NULL) {
		printf("입력파일 개방 오류");
		return 1;
	}
	while ((fgets(ch_Arr, sizeof(ch_Arr), ifp)) != NULL) {
		printf("%s", ch_Arr);
	}
	fclose(ifp);
	return 0;
}

위이 코드는 fgets의 반환값이 NULL이 될 때까지, 즉 파일의 끝이 될때까지

while문을 반복하여 파일에서 문자열을 한 줄씩 읽어와 ch_Arr배열에 저장한 뒤,

이 문자열을 printf를 사용해 출력해주는 코드입니다.

위에서 말했듯이 fgets함수는 문자열을 줄단위로 읽어오기 때문에

while문의 반복 횟수를 확인해보시면 5번 반복된 것을 알 수 있습니다.

 

다음은 문자열을 출력하는 fputs함수에 대해 알아보도록 하겠습니다.

fputs의 함수 원형은 다음과 같습니다.

int fputs(char*, FILE*);

fputs함수의 전달 인자는 순서대로 출력할 문자열, 파일 포인터입니다.

또한 puts의 경우 문자열을 출력하면 자동으로 줄 바꿈을 해주었는데, fputs는 그렇지 않습니다.

fputs함수가 정상적으로 실행되면 음수가 아닌 값(0또는 출력한 문자의 개수, 컴파일러마다 다름)이

출력되며, 실패하면 -1을 리턴합니다.

 

이제 코드를 통해 fgets함수와 fputs함수를 함께 사용해보도록 합시다.

#include <stdio.h>

int main() {
	FILE* ofp, *ifp;
	char ch;
	char ch_Arr[50];
	int i = 0;;

	ofp = fopen("C:\\asd\\ex.txt", "w");
	if (ofp == NULL) {
		printf("출력파일 개방 오류");
		return 1;
	}
	
	while (((ch = getchar()) != -1) && (i < sizeof(ch_Arr) -1)) {
		ch_Arr[i] = ch;
		i++;
	}
	ch_Arr[i] = NULL;
	
	fputs(ch_Arr, ofp);
	fclose(ofp);
	
	ifp = fopen("C:\\asd\\ex.txt", "r");
	if (ifp == NULL) {
		printf("입력파일 개방 오류");
		return 1;
	}

	while ((fgets(ch_Arr, sizeof(ch_Arr), ifp)) != NULL) {
		printf("%s", ch_Arr);
	}
	fclose(ifp);
	return 0;
}

위의 코드는 getchar를 사용해 사용자로부터 문자열을 입력받아 이를 ch_Arr배열에 저장해줍니다.

그 후 fputs를 사용해 파일에 문자열을 출력하고, fgets를 사용해 위에서 저장했던 문자열을

파일에서 입력받아 화면에 출력해주는 코드입니다.

getchar를 사용하는 부분에선 사용자가 ctrl+z를 입력하거나

입력한 내용이 배열의 크기보다 커지면 반복문이 종료되도록 코드를 작성해보았습니다.

직접 코드를 작성해보시면서 사용법을 숙지해주시길 바랍니다.

자료형에 맞춰서 입출력하자


다음은 사용자가 원하는 자료형에 맞추어 입출력하는 함수를 알아보겠습니다.

무슨 뜻이냐 하면, 텍스트 파일에 있는 데이터는 모두 아스키코드 값으로 저장된 문자열이기 때문에

여러분이 파일에 12345라는 정수를 적어도 이는 모두 문자열로 처리됩니다.

따라서 여러분이 이 숫자를 파일로부터 읽어와 변수에 저장하려면

파일에서 데이터를 읽어와서 여러분이 원하는 문자열을 숫자로 변환해주어야 합니다.

 

문자열을 숫자로 바꾸는 함수는 atoi함수를 사용할 수 있는데

int num = atoi("12345");

위와 같이 작성해주시면 문자열"12345"가 정수형 숫자 12345로 변경되어 num변수에 저장됩니다.

그런데 이런 작업을 매번 해주려면 상당히 번거로울 것이라고 생각하지 않나요?

그래서 파일 입출력 함수에선 이와 같은 과정을 자동으로 수행해주는 함수가 존재합니다.

 

바로 fscanf와 fprintf인데요, 이 둘은 scanf, printf와 기본적인 기능은 해주면서

파일에 입출력하는 작업도 해줄 수 있는 함수입니다.

이 둘의 함수 원형은 다음과 같습니다.

int fscanf(FILE*, char*, ...);
int fprintf(FILE*, char*, ...);

첫 번째 인수는 파일 포인터이며, 이후는 scanf, printf와 동일합니다.

fscanf의 반환 값은 입력한 변수의 개수이며, fprintf의 반환값은 출력한 데이터의 바이트 수입니다.

fscanf는 파일로부터 데이터를 입력받는 함수이고, fprintf는 그 반대의 역할을 하는 함수입니다.

또한 fsacnf는 데이터를 모두 입력받으면 -1을 반환하기 때문에 반복문에서 활용하도록 합시다.

 

그럼 이제 코드를 통해 구체적인 사용법을 알아보도록 하겠습니다.

#include <stdio.h>

int main() {
	FILE* ofp, *ifp;
	char name[10];
	int age;
	double avr;

	ifp = fopen("C:\\asd\\ex.txt", "r");
	if (ifp == NULL) {
		printf("입력 파일 개방오류");
		return 1;
	}

	ofp = fopen("C:\\asd\\ex2.txt", "w");
	if (ofp == NULL) {
		printf("출력 파일 개방오류");
		return 1;
	}

	while ((fscanf(ifp, "%s%d%lf", name, &age, &avr)) != -1) {
		fprintf(ofp, "%d, %s, %.1lf\n", age, name, avr);
	}

	fclose(ifp);
	fclose(ofp);
}

왼쪽 : ex.txt    오른쪽 : ex2.txt

위의 코드는 먼저 ex.txt파일을 입력 모드로, ex2.txt파일을 출력 모드로 개방합니다.

그 후 while문으로 반복하면서 fscanf를 사용해 ex.txt로부터 데이터들을 자료형에 맞춰

입력받아 변수에 저장하고, 이를 fprintf를 사용해 다른 파일에 형식을 지정해 출력해줍니다.

모든 작업이 끝난 뒤엔 fclose를 사용해 파일 포인터를 모두 닫아주시는 걸 잊지 마시길 바랍니다.

 

이상으로 C언어에서의 파일 입출력에 대한 설명은 마치도록 하겠습니다.

다음 글에선 전처리와 분할 컴파일에 대해 알아보도록 하겠습니다.

감사합니다.

반응형