본문 바로가기
카테고리 없음

C File read 정리 & 테스트

by SuldenLion 2023. 4. 3.
반응형

C에서 File 단위로 데이터를 입력받고 관리하기

 

FILE *를 사용하여 프로그램 내에서 text 파일을 읽어와 보고 몇 가지 문제 있는 방식과 개선된 방식, 파일에 담긴 데이터의 양을 dynamic하게 메모리에 할당받아 관리하는 방식 등을 알아보겠다.

 

 

#include <stdio.h>
#include <string.h>

typedef struct _alcohol {
	char name[10];
	int ABV; //alcohol by volumn
} Alcohol;

void print(Alcohol *al, int n) {
	int i;
	for (i = 0; i < n; i++) printf("%10s %5d\n", al[i].name, al[i].ABV);
}

main() 
{
	Alcohol al[100];
	FILE *inFile;
	int lineCount;
	int i;

	inFile = fopen("alcohol.txt", "r");	
	lineCount = 0;
	while (!feof(inFile)) {
		char name[10];
		int ABV;

		fscanf(inFile, "%s %d", name, &ABV);
		strcpy(al[lineCount].name, name);
		al[lineCount].ABV = ABV;
		lineCount++;
	}

	fclose(inFile);
	printf("Line count = %d\n", lineCount);

	print(&al[0], lineCount);
}

 

Alcohol이라는 구조체가 있고, 이름과 도수를 멤버 변수로 가질 것이다. 

Alcohol의 이름과 도수 정보가 담긴 텍스트 파일을 읽어와서 print() 해보는 프로그램을 만들어 보겠다.

 

우선 Alcohol들을 관리할 배열 al을 100정도의 사이즈로 정의한다.

불러올 파일의 포인터 inFile, 그리고 파일의 전체 라인수를 보관할 lineCount 변수를 둔다.

이런식으로 alcohol의 정보가 있는 txt 파일을 간단하게 만들어주고 fopen("alcohol.txt", "r")을 한 결과를 inFile에 저장해준다. (alcohol.txt란 파일을 read only mode로 open해주는 함수)

inFile이 feof(= file_end of file)가 아닌동안 loop를 돌아주면서, name과 ABV라는 local 변수에 fscanf()로 각각 값을 넣어준다. 그리고 strcpy()를 통해 al의 lineCount번째 배열 요소의 name에 입력받은 name을, al의 lineCount번째 ABV에 입력받은 ABV를 넣어준다. lineCount를 증가시켜주며 다음칸의 배열에 값을 넣을 수 있게 한다.

 

fclose()로 파일을 닫아주고, line수와 al의 내용들을 출력해보면

 

이런식으로 나올것이다.

 

하지만 위의 프로그램은 문제가 많다.

배열값을 미리 100으로 잡아놔서 안쓰는 부분만큼 메모리 낭비가 생긴다던가, fopen() / fscanf() 함수 들의 불안정함, strcpy()의 문제점 등 문제점들이 보인다.

 

이런점들을 보완하여 프로그래밍 하는 방법을 생각해 보겠다.

 

먼저, Alcohol을 관리할 배열의 사이즈 문제를 보겠다.

파일을 읽어올 때, 그 파일이 갖고 있는 라인 수 만큼 Alcohol 배열의 사이즈도 딱 맞게 할당해주면 좋을 것이다.

하지만 우리는 파일의 라인 수가 얼마나 되는지 모른다. 그래서 파일 라인수를 먼저 알아내야 한다.

그러기 위해선 크게 세 가지 방법을 통해 라인 수를 알아낼 수 있다.

 

첫 번째로, 읽어올 파일의 맨 마지막 줄에다 -1과 같은 수를 둬서 파일의 끝임을 알리는 방법이다.

파일을 전부 읽은 후 마지막에 적힌 -1를 확인하고 빠져나온다.

 

두 번째는 파일의 맨 앞에 라인수를 적어주는 것이다.

하지만 이 방식은 파일 작성자가 일일히 라인수를 적어줘야 한다는 불편함 점과 n을 실수로 잘못 명시할 수 있다는 약점이 있다.

 

그래서 세번째 방법으로, 소스 코드 내에서 라인 수를 알아내기 위한 용도로 파일 open을 한번하여 루프를 돌아 n을 구해낸 후 파일을 닫아주고, 파일을 다시 한번 더 열어서 작업을 해주는 것이다.

물론 파일의 크기가 심하게 크다면 비효율적일 순 있다. 상황에 맞는 방식을 써서 하면 될 것이다.

 

+ 이 경우에도 주의할 점이 있는데, 마지막 데이터 입력 이후 키보드 커서(cursor)의 위치를 그 데이터값 바로 뒤에 위치하게 해야 한다는 것이다. 데이터 입력후 Enter를 쳐서 줄바꿈이 된다면 라인수 입력받는데 문제가 생길 것이다.

 

 

다음으로, 위 프로그램의 fopen() / fscanf() 함수 사용에 대한 문제이다. 이 함수들은 fopen_s()와 fscanf_s()로 바꿔서 사용해주면 되는데, 기존 fopen을 _s(safe)하게 사용할 수 있게 해준다 정도의 의미가 될 것이다. fopen()과 fscanf()가 c언어 초창기때 사용되던 함수이다보니 불안정한 부분이 있는데 그 부분을 보완한 함수이다.

아래에서 코드 부분을 보고 설명을 추가해 보겠다.

 

그 전에 또 한가지 문제점으로 strcpy()를 써서 al의 name에 입력받은 name을 저장해준 것인데, 이렇게 쓴다면 다음과 같은 문제를 야기시킬 수 있다.

바로 복사할 문자열이 복사될 문자열 공간보다 크면 복사하는 도중에 문자열이 짤린다. Alcohol 구조체에는 name을 위한 배열이 10의 size까지 만큼 받을 수 있게 되어 있다.

만약 위의 파일을 받은 채로 컴파일하여 프로그램을 돌려보게 되면

 

이렇게 되면서 프로그램이 중단될 것이다.

 

이런 문제를 해결하기 위해 문자 복사 시에 동적 할당을 해주고 그 첫번째 주소 포인터를 리턴해주는 strdup() 함수를 써야 될 것이다.

 

문제점들을 수정한걸 반영해서 코드를 만들어 보겠다.

 

#include <stdio.h>
#include <string.h>
#include <malloc.h>

typedef struct _alcohol {
	char *name;
	int ABV; //alcohol by volumn
} Alcohol;

void print(Alcohol *al, int n) {
	int i;
	for (i = 0; i < n; i++) printf("%10s %5d\n", al[i].name, al[i].ABV);
}

main() 
{
	Alcohol *al;
	FILE *inFile;
	int lineCount;
	int i;

	fopen_s(&inFile, "alcohol.txt", "r");
	lineCount = 0;
	while (!feof(inFile)) {
		char name[BUFSIZ];
		int ABV;

		fscanf_s(inFile, "%s %d", name, BUFSIZ, &ABV);
		lineCount++;
	}

	fclose(inFile);
	printf("Line count = %d\n", lineCount);

	al = (Alcohol *)malloc(sizeof(Alcohol)*lineCount);

	fopen_s(&inFile, "alcohol.txt", "r");

	for (i = 0; i < lineCount; i++) {
		char name[BUFSIZ];
		int ABV;

		fscanf_s(inFile, "%s %d", name, BUFSIZ, &ABV);
		al[i].name = _strdup(name);
		al[i].ABV = ABV;
	}

	print(al, lineCount);
	fclose(inFile);
}

 

Alcohol 구조체들을 관리할 Alcohol * al을 두고 file을 한번 읽어내어 line수를 알아낸다.

알아온 line수를 가지고 al에 Alcohol 구조체의 크기와 lineCount를 곱한 것만큼 메모리 할당을 해준다.

그리고 al에다가 파일에 있는 데이터를 저장하기 위해 파일을 불러오고 lineCount만큼 loop를 돌아준다.

strdup()를 위한 char array형 지역변수 name은 그 크기를 BUFSIZ (= Buffer size)만큼 할당시켜준다. BUFSIZ에는 매크로로 512의 값이 저장되어 있는데, 그 만큼 넉넉하게 문자들을 입력받을 수 있는 것이다. fscanf_s()를 통해 파일에 적힌 이름이 얼마나 길든 name에다 담아줄 수 있게 되고 그 name을 _strdup()를 통해 al[i]의 name에다 동적으로 복사시켜 줄 수 있게된다.

 

컴파일하여 실행시켜 보면

 

이전의 이름이 길어서 짤리던 문제가 해결된다.

 

 

이상 File read에 관한 내용이었다.

반응형

댓글