본문 바로가기
프로그래밍/C

[ C언어 ] 프로젝트(삽질일기) : 도서 관리 프로그램 - (4) - 파일 입출력 - fopen / fopen_s, .bin 바이너리 파일 읽기 / 쓰기

by eteo 2022. 4. 25.

파일 입출력 기능을 추가한 도서 관리 프로그램 개선 4탄.

 

2022.04.22 - [Language/C] - [C언어] 프로젝트(삽질일기) : 도서 관리 프로그램 - (3) - 동적 메모리 할당 - 구조체 배열 크기 동적 조절 (확대/축소)

 

이전까지는 내가 프로그램을 켜고 도서 추가/삭제를 해도 끄고 나서 프로그램을 다시키면 여전히 내가 소스파일에 입력해놓은 초기데이터가 보였었다.

하지만 나는 프로그램 실행중에 데이터를 가공하고 껐을 때 그 데이터가 어딘가 잘 저장되어 있다가 다시 키면 읽어오는 방식으로 연속적으로 관리하고 싶어졌다.

 

 

 

결과물 투척

 

 

파일 읽기

	FILE* fp = NULL;
	errno_t err = fopen_s(&fp, "booklist.bin", "rb");

	if (err == 0) {

		fseek(fp, 0, SEEK_END);    // 파일 포인터를 파일의 끝으로 이동시킴
		size_t fileSize = ftell(fp);          // 파일 포인터의 현재 위치를 얻음
		rewind(fp);		// 파일 포인터의 위치를 다시 맨앞으로 보냄

		for (;;) {
			if (fileSize > sizeof(_stBook) * listSize) {	// 파일 사이즈가 구조체 배열 사이즈보다 크다면 10씩 더 메모리 확대 확보
				expandM(&booklist);
			}
			else if (fileSize <= sizeof(_stBook) * listSize) break;
		}

		//파일 읽어들이기
		fread((void *)booklist, sizeof(_stBook), fileSize/sizeof(_stBook), fp);
		fclose(fp);	// 파일 스트림 닫기

	}
	else {
		printf("파일오픈 실패\n");
		printf("에러코드 : %d\n", err);
		system("pause");
	}
  • FILE 포인터를 만들고 NULL 값 대입
  • fopen_s 으로 입력 스트림 열기. 그냥 fopen 이랑은 사용법이 좀 다르다.
 fopen_s(FILE **_Stream, const char *_FileName, const char *_Mode)
 fopen_s의 원형(파일포인터의 주소, "파일명", "개방모드")
  • fopen은 리턴값을 파일포인터에 대입하는데, fopen_s는 인수에 이미 파일포인터의 주소를 넘겨주고 리턴값은 성공여부에 따라 성공했으면 0, 실패했으면 오류코드를 반환한다. 반환값을 errno_t 변수에 담는데 이건 이런 오류코드들을 담기위해 VS에서 제공하는 자료형 타입이라고 한다.
  • 파일의 개방모드는 "rb" 바이너리 파일 읽어오기 모드이다.

 

  • 그다음 딱 파일사이즈만큼만 읽어오기 위해 먼저 파일사이즈 구해보겠다. 파일을 열었을 때는 파일포인터가 맨앞을 가리키고 있다. fseek(fp, 0, SEEK_END); 로 파일의 끝으로 파일 포인터를 이동시킨 후 0바이트 움직인다. ftell(fp); 는 파일포인터의 현재위치를 반환하는데 이걸로 파일사이즈를 알 수 있다. (ex. 파일크기가 560바이트라면 560을 반환한다.)
  • rewind(fp)로 파일포인터의 위치를 다시 맨앞으로 보내줘야 이후 읽어오는데 문제가 없다!
  • 파일을 읽기 전에 만약 파일사이즈가 구조체배열크기보다 크다면 배열사이즈를 키워 메모리를 재할당 하도록 한다.

 

  • fread로 .bin 파일 읽어오기. 파일에서 size * count 바이트만큼 읽어온다. 그리고 반환값은 읽어오는데 성공한 count 개수이다. 만약 4 byte, 10 count였을 경우 10개 다 읽어오는 데 성공했으면 10반환, 하나도 못읽어왔으면 0반환 이런식이다. 나는 여기서 반환값을 활용하진 않았다.
fread(void *_Buffer, size_t _ElementSize, size_t _ElementCount, FILE *_Stream)
fread 함수의 원형((void *형변환)읽어올 값을 저장할 곳의 주소, size, count, 파일포인터)
  • fopen으로 파일스트림을 열고 나서 fclose 를 까먹으면 계속 컴퓨터자원 을 사용하므로 반드시 fclose로 닫아주자.

 

 

참고 : 파일의 개방모드

b를 붙이면 바이너리파일 모드이고 안붙이면 text파일 모드이다.

참고. 주요 모드

Mode Description 파일이 없는 경우
r 읽기 에러
w 쓰기 생성
a 파일의 끝에 덧붙여 쓰기 생성
r+ 읽기/쓰기 가능 에러
w+ 읽기/쓰기 가능 생성
a+ 읽기/덧붙여 쓰기 가능 생성

파일의 개방 모드 중 r+, w+, a+ 등은 읽기/쓰기가 동시에 가능하지만 읽기에서 쓰기, 쓰기에서 읽기로 작업을 변경할 때마다 메모리 버퍼를 비워줘야 하는 불편함과 더불어 사용상 잘못될 위험이 따르기 때문에 r, w, a 중 하나를 선택하여 파일 스트림을 형성하는 것이 좋다.

 

 

참고. fseek 함수의 두번째 매개변수

SEEK_SET 파일의 처음부터 fseek(fp, 0, SEEK_SET); // 파일 포인터를 파일의 처음으로 이동
SEEK_CUR 현재 위치부터 fseek(fp, -10, SEEK_CUR); // 파일 포인터를 현재 위치에서 10바이트만큼 역방향으로 이동
SEEK_END 파일의 끝부터 fseek(fp, 0, SEEK_END); // 파일 포인터를 파일의 끝으로 이동

 

 

 

 

 

 

다음 main, while문 내에서 종료버튼을 눌렀을 때 실행되는 파일 출력 부분이다.

파일 쓰기

		if (menu == 6) {	// 종료 버튼

			FILE* fp;
			errno_t err = fopen_s(&fp, "booklist.bin", "wb");	// 파일 출력스트림 개방

			if (err == 0) {
				fwrite((void*)booklist, sizeof(_stBook), datanum, fp); // 파일 출력
				fclose(fp);	// 파일 스트림 닫기
			}
			else {
				printf("파일저장 실패\n");
				printf("에러코드 : %d\n", err);				
			}

			break;
		}
  • 파일포인터 만들고 NULL 대입
  • fopen_s 호출. "wb" 바이너리파일 쓰기 모드로 파일 출력스트림 개방
  • fwrite함수로 파일 쓰기. fwrite((void*)booklist, sizeof(_stBook), datanum, fp); booklist 주소에 있는 값을 sizeof(_stBook) * datanum 개수 byte만큼 fp 포인터가 가리키는 파일에 쓴다.

 

 

 

그리고 ㅂ 특수문자를 사용해서 메뉴판을 좀더 예쁘게 바꿀 수 있다.

	printf("┌────────────────────────────────────────────────────────────────────────────┐\n");
#ifdef DEBUG
	printf("│ ~ 테스트용 도서추가버튼 : 7, 도서삭제버튼 : 8 ~                            │\n");
	printf("│                                                                            │\n");	// 구조체배열 사이즈 출력
	printf("│                                 (현재 할당된 메모리크기 / 구조체크기 : %2d) │\n", _msize(list) / sizeof(_stBook)); // _msize 힙에할당된 메모리크기 반환 함수, MSVS 전용
#endif // DEBUG
	printf("│                                                                            │\n");
	printf("│                           도서 검색 프로그램                               │\n");
	printf("│                                                    (현재 데이터 개수 : %2d) │\n", num);
	printf("│                                                                            │\n");
	printf("│───────────────────────────────────MENU─────────────────────────────────────│\n");
	printf("│                                                                            │\n");
	printf("│  1. 도서 추가                                                              │\n");
	printf("│                                                                            │\n");
	printf("│  2. 도서 삭제                                                              │\n");
	printf("│                                                                            │\n");
	printf("│  3. 도서 정렬                                                              │\n");
	printf("│                                                                            │\n");
	printf("│  4. 도서 검색                                                              │\n");
	printf("│                                                                            │\n");
	printf("│  5. 도서 확인                                                              │\n");
	printf("│                                                                            │\n");
	printf("│  6. 종료                                                                   │\n");
	printf("│                                                                            │\n");
	printf("└────────────────────────────────────────────────────────────────────────────┘\n\n");

 

파일입출력 기능 추가 끝!

중간에 파일쓰기 기능이 갑자기 안되서 원인을 찾느라 고생했는데 알고보니 내가 파일읽기 부분에서 이것저것 수정하다가 fclose를 주석처리 해버린 탓이었다.ㅜㅜ

참고로 인터넷에 찾아보니 fopen_s는 파일을 한번만 열 수 있어서 여러번 열어서 읽고 쓰고 하려면 _fsopen함수를 쓰라고 하더라. 나같은 경우 위에선 rb 읽기만 아래에선 wb 쓰기모드로만 열어서 fopen_s를 두 번 사용해도 문제가 없었던거 같다.

 

 

후기.

끝. 기능적인 부분은 다 완성했는데 구조체배열을 링크드리스트로 바꿔보고 싶긴하다. 다만 기존에 인덱스로 쉽게 사용하던 부분을 전부 갈아엎어야해서 뭔가 대공사가 될거 같으니 시간을 두고 해보도록하겠다. 어쩌면 이게 마지막편이 될수도 있다!

 

Array List
배열
인덱스를 통해 원하는 요소에 바로 접근할 수 있어 탐색이 빠르다.
사용되지 않는 요소만큼의 메모리의 낭비가 있다.
요소 중간에 삽입/삭제를 할 때 데이터를 한 칸씩 밀거나 땡겨야 해서 번거롭다.
Linked List
링크드 리스트
크기가 고정적이지 않아서 메모리 사용이 효율적이다
삽입, 삭제가 용이하다
맨앞 노드부터 순차적으로 접근해야해서 검색이 느리다.
알고리즘이 배열보단 복잡하다.

 

 

다음편 :

https://eteo.tistory.com/42

 

[C언어] 프로젝트 : 이중 연결 리스트로 구현한 도서 관리 프로그램 - (5) - 링크드 리스트와 파일

도서 관리 프로그램 개선 5탄이자 마지막. 자료구조를 기존의 배열에서 이중 연결 리스트(Doubly Linked List)로 바꾸었다. 2022.04.25 - [Language/C] - [C언어] 프로젝트(삽질일기) : 도서 관리 프로그램 - (4).

eteo.tistory.com