본문 바로가기
프로젝트(Project) 모음

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

by eteo 2022. 5. 2.

 

 

도서 관리 프로그램 개선 5탄이자 마지막. 자료구조를 기존의 배열에서 이중 연결 리스트(Doubly Linked List)로 바꾸었다.

 

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

 

이건 개선이라기보다 거의 새로 짜는거에 가까웠다..프로젝트 처음 시작할때 자료구조를 먼저 결정하고 ADT를 정의하는 것의 중요성을 체감한다.

 

참고로 이중 연결 리스트는 헤드/테일 더미노드가 있고 꼬리쪽에 삽입하는 버전이다. 사실 처음에는 진짜 제일 간단한 단순 연결 리스트(Single Linked List)로 구현하려고 했었는데 여기까지 온 이유가 있다. 내가 프로그램을 만들면서 참고한 자료구조 책의 저자인 윤성우 님도 이중 연결 리스트가 어렵다는 건 편견이고 오히려 사용하기 편하다고 하셨는데 그말이 정말 맞는것 같다고 계속 느꼈다.

 

 

깃허브 주소 : https://github.com/joeteo/BookManager_C

 

GitHub - joeteo/BookManager_C: Book Mangager program in C

Book Mangager program in C. Contribute to joeteo/BookManager_C development by creating an account on GitHub.

github.com

 

 

 

먼저 헤더

linkedListBook.h

#ifndef __LINKED_LIST_BOOK_H__
#define __LINKED_LIST_BOOK_H__
#define DEBUG

#define FALSE 0
#define TRUE 1

typedef struct {
	char year;
	char month;
	char day;
}_stDate;

typedef struct _node {
	int price;
	int page;
	char title[30];
	char author[10];
	_stDate date;
	struct _node* prev;
	struct _node* next;
} NODE;

typedef struct {
	NODE* head;
	NODE* tail;
	NODE* curr;
	int numOfData;
} LIST;

typedef int (*_comp)(const void* arg1, const void* arg2);

void listInit(LIST* ptrlist);					// 리스트 초기화			
void addNode(LIST* ptrlist, NODE* tempBook);	// 노드 삽입
int listCount(LIST* ptrlist);					// 노드 개수 count
void deleteNode(LIST* ptrlist, int numToDelete);// 특정 노드 지우기
void delLastNode(LIST* ptrlist);				// 마지막 데이터 노드 지우기
void freeAllNode(LIST* ptrlist);				// 모든 노드 메모리 할당 해제
void listSearch(LIST* ptrlist);					// 원하는 노드만 찾아 출력
void listPrint(LIST* ptrlist);					// 리스트 전체 출력
void copyList(LIST* destination, LIST* source);	// 리스트 복사하기
void putNPushNode(NODE* compNode, NODE* curr);	// 특정 노드 원하는 위치에 끼워넣기
//void swapNode(NODE* compNode, NODE* curr);	// 스왑 노드(미사용)

void insertionSort(LIST* ptrlist, _comp criteria);
int compare_price(const NODE* arg1, const NODE* arg2);
int compare_page(const NODE* arg1, const NODE* arg2);
int compare_date(const NODE* arg1, const NODE* arg2);
int compare_title(const NODE* arg1, const NODE* arg2);
int compare_author(const NODE* arg1, const NODE* arg2);

void readFromFile(LIST* ptrlist);
void writeToFile(LIST* ptrlist);

int dispMenu(int numOfData);
void testAdd(LIST* ptrlist);
void listInsert(LIST* ptrlist);	
void deleteMenu(LIST* ptrlist);
void listSearchForDel(LIST* ptrlist, int* searchedNum);
void sortMenu(LIST* ptrlist);

#endif
  • NODE 구조체 안에 다음 노드와 직전 노드 주소를 저장할 포인터인 prev와 next가 있다.
  • 리스트 관리에 필요한 head, tail, curr포인터와 numOfData는 구조체로 묶어 선언한다. 이렇게 하니까 정말 편리하다. 이전 버전 까지는 main 변수 안에 도서개수를 저장하는 datanum 선언해놓고 매번 매개변수로 넘겨야 했기에 불편했었다.
  • 자세한 내용은 주석처리함

 

 

 

linkedListMain.c

#include <stdio.h>
#include "linkedListBook.h"
#include <Windows.h>
#define DEBUG

#ifdef DEBUG
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#endif

int main(void) {
	//_CrtSetBreakAlloc();

	LIST booklist;
	listInit(&booklist);	// 링크드리스트 초기설정

	readFromFile(&booklist);	// 파일로부터 데이터 읽어들이기

	int menu, exit = FALSE;

	while (exit == FALSE) {

		system("cls");
		menu = dispMenu(listCount(&booklist));	// 메뉴 출력
		system("cls");

		if (menu == 1) listInsert(&booklist);	// 도서 추가
		else if (menu == 2) deleteMenu(&booklist);	// 도서 삭제
		else if (menu == 3) sortMenu(&booklist);	// 도서 정렬
		else if (menu == 4) listSearch(&booklist);	// 도서 검색
		else if (menu == 5) listPrint(&booklist);	// 도서 출력
		else if (menu == 6) {	// 종료
			writeToFile(&booklist);	// 파일에 출력
			exit = TRUE;	// 루프 탈출
			freeAllNode(&booklist);	// 모든 노드 메모리 할당 해제
		}
#ifdef DEBUG
		else if (menu == 7)	testAdd(&booklist); // 테스트용 도서 추가
		else if (menu == 8) delLastNode(&booklist);	// 테스트용 도서 삭제
#endif
		else {
			rewind(stdin);	// 다른 숫자나 문자(열)이 들어왔을 땐 입력버퍼 비움
		}
	}
#ifdef DEBUG
	_CrtDumpMemoryLeaks();	// 디버그용 memory leaks 탐지
#endif
	return 0;
}
  • 먼저 booklist를 하나 만들고 listInit 함수로 초기설정을 한다.  readFromFile 함수로 바이너리 파일로부터 데이터를 읽어들여 노드를 그만큼 추가하고 while문 돌면서 메뉴 반복. 6번 종료 입력시 wirteToFile함수로 다시 노드의 데이터를 바이너리 파일에 저장하고 리스트의 모든 메모리를 할당 해제 후 종료한다.

 

 

 

 

void listInit(LIST* ptrlist); // 리스트 초기 설정 함수

void listInit(LIST* ptrlist)
{
	ptrlist->head = (NODE*)malloc(sizeof(NODE));	// 헤드더미노드 생성

	if (ptrlist->head != NULL) {	// malloc 반환이 NULL인 경우 예외처리

		ptrlist->tail = (NODE*)malloc(sizeof(NODE));	// 테일더미노드 생성

		if (ptrlist->tail != NULL) {	// malloc 반환이 NULL인 경우 예외처리

			ptrlist->head->prev = NULL;	// 헤드더미노드의 prev는 NULL
			ptrlist->head->next = ptrlist->tail;	// 헤드더미노드의 next는 테일더미노드
			ptrlist->tail->prev = ptrlist->head;	// 테일더미노드의 prev는 헤드더미노드
			ptrlist->tail->next = NULL;	// 테일더미노드의 next는 NULL
			ptrlist->numOfData = 0;	// 데이터개수 0
		}
		else printf("메모리 할당에 실패하였습니다.");
	}
	else printf("메모리 할당에 실패하였습니다.");
}
  • 헤드와 테일 더미노드를 생성하고 서로를 가리키게 양방향 연결해준 뒤 테이터 개수는 0으로 초기화한다.

 

 

 

void readFromFile(LIST* ptrlist); // 파일에서 데이터 읽어오기

void readFromFile(LIST* ptrlist) {

	FILE* fp = NULL;
	errno_t err = fopen_s(&fp, "booklist.bin", "rb");	// 백업해놓은 리스트 사용하려면 booklist_backup.bin

	if (err == 0) {

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

		int numberOfNode = (int) fileSize / sizeof(NODE);	// 파일에 저장된 노드의 개수

		NODE* tempBook = (NODE*)malloc(sizeof(NODE));
		if (tempBook != NULL) {	// malloc 반환이 NULL인 경우 예외처리

			for (int i = 0; i < numberOfNode; i++) {	// numberOfNode 만큼 반복하며
				fread(tempBook, sizeof(NODE), 1, fp); // 파일 읽어 들이기
				addNode(ptrlist, tempBook);
			}

			free(tempBook);	// tempBook 메모리 할당 해제

		} else printf("메모리 할당에 실패하였습니다.");

		fclose(fp);	// 파일 스트림 닫기

	}
	else {
		printf("파일오픈 실패\n");
		printf("에러코드 : %d\n", err);
		system("pause");
	}
}
  • 일단 fseek+ftell+rewind 함수의 조합으로 현재 파일에 저장된 노드(데이터)의 개수를 확인하고 그만큼 반복하며 fread로 읽어올 것이다.
  • 이를 위한 임시노드 tempBook을 하나 만들어주고 읽어온 데이터는 tempBook에 쓰고 노드를 추가하는 addNode함수를 호출해 인수로 넘겨서 성공적으로 추가하고 나면 또 거기에 다음 데이터를 덮어씌워서 addNode를 다시호출하고 이런식으로 파일에 있는 데이터를 모두 추가할 때까지 반복한다.

 

 

void addNode(LIST* ptrlist, NODE* tempBook); // 노드 추가 함수

void addNode(LIST* ptrlist, NODE* tempBook) {
	NODE* newNode = (NODE*)malloc(sizeof(NODE));	// 새 노드 생성, 임시 포인터인 newNode에 주소값 대입
	if (newNode != NULL) {	// malloc 반환이 NULL인 경우 예외처리

		// 도서 데이터 저장 여기부터
		newNode->price = tempBook->price;
		newNode->page = tempBook->page;
		newNode->date.year = tempBook->date.year;
		newNode->date.month = tempBook->date.month;
		newNode->date.day = tempBook->date.day;
		strcpy_s(newNode->title, sizeof(newNode->title), tempBook->title);
		strcpy_s(newNode->author, sizeof(newNode->author), tempBook->author);
		// 도서 데이터 저장 여기까지

		// 꼬리 추가 방식
		newNode->prev = ptrlist->tail->prev;	// 새 노드의 prev가 직전 마지막 노드(첫 추가인 경우 헤드더미노드)를 가리킴
		ptrlist->tail->prev->next = newNode;	// 직전 마지막 노드(첫 추가인 경우 헤드더미노드)의 next가 새 노드를 가리킴

		newNode->next = ptrlist->tail;	// 새 노드의 next가 테일더미노드를 가리킴
		ptrlist->tail->prev = newNode;	// 테일더미노드의 prev가 새노드를 가리킴

		(ptrlist->numOfData)++;	// 데이터 개수 증가

	}
	else printf("메모리 할당에 실패하였습니다.");
}
  • 얘는 위의 파일에서 데이터 읽어오기 함수 뿐아니라 홈메뉴에서 1번 도서추가를 선택했을 때 / 7번 테스트용 도서추가를 선택했을 때 호출당한다. 매개변수는 리스트의 포인터와 데이터가 담긴 노드의 포인터이다. 일단 새 노드를 생성하고 매개변수로 받은 노드의 도서데이터를 옮겨 담는다.
  • 그림으로 그리면 이런 느낌이다.. 꼬리추가 방식이라 테일더미노드 바로 앞에 새노드를 낑겨넣는다.
  • 마지막엔 데이터 개수를 증가해준다.

 

 

LIST 구조체의 현재 데이터 개수 반환해주는 함수

int listCount(LIST* ptrlist)
{
	return ptrlist->numOfData;
}

 

 

 

void writeToFile(LIST* ptrlist); // 파일에 저장

void writeToFile(LIST* ptrlist) {

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

	if (err == 0) {

		ptrlist->curr = ptrlist->head->next;	// 헤드더미노드의 next 값을 curr 포인터에 대입

		while (ptrlist->curr != ptrlist->tail) {	//	curr이 테일더미노드를 가리키게 될 때까지 반복
			fwrite(ptrlist->curr, sizeof(NODE), 1, fp); // 파일 출력
			ptrlist->curr = ptrlist->curr->next;	// curr에 next주소값 대입
		}
		fclose(fp);	// 파일 스트림 닫기
	}
	else {
		printf("파일저장 실패\n");
		printf("에러코드 : %d\n", err);
		system("pause");
	}
}
  • 일단 curr 포인터를 헤드더미노드의 next(=첫번째 데이터노드)를 가리키게 위치시키고 fwrite함수로 해당 노드를 출력한 후엔 curr을 다음칸으로 옮겨서 다시 fwrite를 하고 이것을 마지막 데이터노드까지(curr이 테일더미노드를 가리키게 될 때까지) 반복한다.

 

 

 

linkedListTest.c

#define DEBUG

#ifdef DEBUG

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "linkedListBook.h"
#include <time.h>

void testAdd(LIST* ptrlist) {	// 테스트용 도서 추가

	srand((unsigned)time(NULL));

	NODE* tempBook = (NODE*)malloc(sizeof(NODE));
	if (tempBook != NULL) {	// malloc 반환이 NULL인 경우 예외처리

		// 테스트용 데이터
		char buf[30];	// 정수->문자열 변환 위한 임시 buffer
		sprintf_s(buf, sizeof(buf), "%d번째 책(test)", listCount(ptrlist) + 1);	// 정수를 문자열로 변환해 버퍼에 저장해주는 함수
		strcpy_s(tempBook->title, sizeof(tempBook->title), buf);

		char name[][3] = {"이","조","한","정","강","윤","장","수","현","상","연","고","우","민","준","서","훈","아","원","지"};		
		strcpy_s(buf, sizeof(buf), name[rand() % 20]);
		strcat_s(buf, sizeof(buf), name[rand() % 20]);
		strcat_s(buf, sizeof(buf), name[rand() % 20]);
		strcpy_s(tempBook->author, sizeof(tempBook->author), buf);

		tempBook->price = rand()%50*1000;
		tempBook->page = rand()%1000;
		tempBook->date.year = rand() % 12 + 1;
		tempBook->date.month = rand() % 12 + 1;
		tempBook->date.day = rand() % 30 + 1;
		// 테스트용 데이터

		addNode(ptrlist, tempBook);	// 노드 추가 함수 호출

		free(tempBook);	// tempBook 메모리 할당 해제	

	}
	else printf("메모리 할당에 실패하였습니다");
}

#endif // DEBUG
  • 테스트용 도서 추가라는 역할에 잘 맞게 랜덤 값이 들어가도록 했다.
  • 임시 buf는 title 문자열 만드는데도 쓰고 author 문자열 만드는데도 재활용 했는데 이름 넣을땐 첫글자는 strcpy해주고 그 뒤엔 strcat으로 이어붙이면 된다. 이렇게 랜덤으로 만들어낸 데이터가 다 준비되면 addNode함수를 호출해 노드를 추가한다.
  • srand함수는 난수의 초기 seed를 설정하는 부분으로 원래는 프로그램 실행 초반에 한번만 실행되는 것이 좋은데 이건 testAdd()함수 내부에 있어서 이 함수를 호출할때마다 seed를 재설정 하니까 버튼을 너무 빨리누르면 같은 값이 나오기도 한다. 근데 main함수를 매우 깔끔하게 만들고 싶은 욕심에 이안에 넣었다.

 

 

 

 

 

void listPrint(LIST* ptrlist); // 도서리스트 출력

void listPrint(LIST* ptrlist) {

	printf("------------------------------------------------------------------------------\n");
	printf("                제목                 가격      저자     페이지    발행연도   \n");
	printf("==============================================================================\n");

	int i = 1;
	ptrlist->curr = ptrlist->head->next;	// 헤드더미노드의 next 값을 curr 포인터에 대입

	if (ptrlist->curr == ptrlist->tail) {	// curr 포인터가 테일더미노드를 가리키는 경우
		printf("   출력할 데이터가 없습니다...\n\n   ");
		system("pause");
		return;
	} else {	// 데이터가 있는 경우
		while (ptrlist->curr!= ptrlist->tail) {	// 계속 next 주소로 노드를 순회하면서 curr이 테일더미노드를 가리킬 때까지 반복 출력
			printf("[%2d] %-30s %-9d %-10s %-8d %02d.%02d.%02d\n\n", i, ptrlist->curr->title, ptrlist->curr->price,
				ptrlist->curr->author, ptrlist->curr->page, ptrlist->curr->date.year, ptrlist->curr->date.month, ptrlist->curr->date.day);
			ptrlist->curr = ptrlist->curr->next;	// curr에 next주소값 대입
			i++;
		}
		printf("------------------------------------------------------------------------------\n");
		system("pause");
	}
}
  • curr이 헤드더미의 다음 노드(첫번째 데이터 노드)를 가리키게 하고 출력 후 한칸 씩 next로 이동해서 테일더미노드 직전(마지막 데이터노드)까지 반복하며 출력한다.
  • 링크드리스트는 배열처럼 인덱스는 없지만 i++;을 이용해 도서번호를 출력할 수 있다.

 

 

 

 

void listSearch(LIST* ptrlist); // 도서리스트 검색

void listSearch(LIST* ptrlist) {

	char keyword[100] = { 0, };
	char chk = 0;

	printf("------------------------------------------------------------------------------\n");
	printf("   검색어를 입력하세요.......................[                          ]\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b");
	scanf_s(" %99[^\n]", keyword, sizeof(keyword));
	printf("\n------------------------------------------------------------------------------\n");
	printf("                제목                 가격      저자     페이지    발행연도   \n");
	printf("==============================================================================\n");

	int i = 1;
	ptrlist->curr = ptrlist->head->next;

	if (ptrlist->curr == ptrlist->tail) {	// curr 포인터가 테일더미노드를 가리키는 경우
		printf("   출력할 데이터가 없습니다...\n\n");
	}
	else {
		while (ptrlist->curr != ptrlist->tail) { // curr이 순회하며 테일더미노드를 가리킬 때까지 반복 출력
			if (strstr(ptrlist->curr->title, keyword) != NULL) {	// str1 문자열이 str2 문자열을 포함하고 있으면 char * 반환
				printf("[%2d] %-30s %-9d %-10s %-8d %02d.%02d.%02d\n\n", i, ptrlist->curr->title, ptrlist->curr->price,
					ptrlist->curr->author, ptrlist->curr->page, ptrlist->curr->date.year, ptrlist->curr->date.month, ptrlist->curr->date.day);
				chk++;	// 검색 결과가 나올 때만 chk++
			}
			ptrlist->curr = ptrlist->curr->next;
			i++;	// 책번호 출력위해 검색결과랑 상관없이 i++
		}
		if (chk == 0) printf("   검색 결과가 없습니다.\n\n");
	}
	printf("------------------------------------------------------------------------------\n   ");
	system("pause");
}
  • strcmp 대신 strstr함수를 사용해 단어로 검색하고 일치하는 단어가 있으면(strstr 반환값이 !=NULL일때) 모두 출력한다. 역시 인덱스는 없지만 검색된 도서번호가 몇번인지 알 수 있다.

 

 

글이 너무 길어져 다음편에 계속..

https://eteo.tistory.com/43

 

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

이어서 이중 연결 리스트로 구현한 도서 관리 프로그램의 노드 삭제, 삽입정렬, 메모리 할당해제 기능 등을 소개한다. void deleteMenu(LIST* ptrlist); // 삭제할 도서를 도서명으로 검색하여 검색 결과

eteo.tistory.com