배낭 알고리즘(Knapsack algorithm)

 

알고리즘에서 DP를 배울때 등장하는 유명한 문제가 있습니다. 제목과 같이 바로 배낭알고리즘이지요. 배낭알고리즘도 여러가지가 있지만, 우리는 조건이 가장 간단한 0/1 배낭 알고리즘 문제를 알아보도록 하겠습니다.

 

상황은 이렇습니다.

어떤 REAKWON이라는 이름의 도둑놈이 있습니다. 이 도둑은 새벽 늦은 시간 도둑질을 하기 위해서 남의 집에 들어가는데 까지는 성공했습니다. 그리고 훔쳐갈 물건들을 담기 위해 배낭도 챙겼지요. 데헷

하지만 이 도둑은 안타깝게도 아이큐가 70입니다. 배낭의 용량은 한정되어 있고 무거우면 들고갈 수도 없으므로 가장 값어치가 비싸고, 무게가 적은 물건들을 최대한 많이 담아가야합니다.

하지만 조건이 있습니다.

1) 물건을 부분적으로 담을 수는 없습니다.

2) 물건들은 모두 한개씩만 있습니다. 예를 들어 똑같은 반지가 2개일 수 없다는 이야기입니다.

 

도둑은 현재 15의 무게를 담을 수 있는 배낭을 가지고 있습니다. 그리고 아래와 같이 그 집의 물건이 있지요. 

 

items 

 weight

 value

 [0]

 5

 8

 [1]

 8

 11

 [2]

 3

 3

 [3]

 4

 6

 [4]

 2

 4

 

 

이 상황에서 도둑이 훔쳐갈 수 있는 최대의 값을 구하는 것입니다.

 

어떻게 도둑을 도와줄것인가?

아 그냥 무게가 가장 적은거 순서대로 훔쳐가면 되지 않을까요?

라고 생각하신다면 다시 한번 생각해봅시다.

위의 물건들을 가장 작은 무게가 나가는 것을 훔쳐가게 된다면 items[4], items[2], items[3], items[0]만 가져갈 수 있고, 총 가치는 21이 되고 총 용량은 14가 됩니다.

하지만 items[0], items[1], items[4]를 선택한다면 총 가치는 23이 되고, 용량은 15로 더 비싼물건을 훔칠 수 있습니다.

 

그렇기 때문에 무게가 덜 나가는 순으로 정렬해서 문제를 해결할 수가 없는것이죠.

그래서 단순 무식하게 한번 접근해봅시다.

도둑은 그 물건을 훔쳐갈 수 있느냐, 훔쳐갈 수 없느냐 이 둘만 고려합니다. 

 

1) 훔쳐갈때 

만약 어떤 물건을 훔쳐갈 수 있다고 한다면 현재 무게가 그 물건의 무게만큼 무거워지요. 하지만 배낭 용량을 초과할 수는 없습니다. 그럴땐 그 물건을 배낭에 담을 수가 없습니다.

현재 그 물건들을 배열로 표현(items)하고, 어떤 물건을 고려 할때 그 아이템의 인덱스를 pos라고 합시다. 그리고 현재 용량을 capacity라고 해봅시다. V는 그 물건의 가치, W는 그 물건의 용량이라고 친다면 우리는 이런 식을 쓸 수 있습니다.

 knapsack(pos + 1, capacity - items[pos][W]) + items[pos][V]

 

knapsack이라는 함수는 현재 물건 이후 가장 큰 값을 반환합니다. 그러니까 당연히 용량(capacity)는 그 물건의 무게(items[pos][W])만큼 줄어들 것이고, 그 반환된 가장 큰 값에서 현재 물건의 가치를 더함으로써 지금 이 물건을 훔쳤을때 얻을 수 있는 값을 얻을 수 있습니다.

 

 

2) 훔쳐가지 않을 때

그리고 또 그냥 그 물건을 안가져갈 수도 있습니다. 그럴때는 현재 무게가 그대로 유지되면서 다음 물건을 고려하는 겁니다.

 

그러니 아래의 식이 가능합니다.

 

knapsack(pos + 1, capacity )

 

 

1)훔쳐갈 경우2)훔쳐가지 않을 경우 중에서 가장 큰 값이 우리가 원하는 답이 되는 것이죠.

 

코드

이런 프로그램을 구현한 것이 바로 아래 소스 코드입니다.

 

#include <stdio.h>
#define W 0
#define V 1
#define N 5
#define MAX(a,b) a>b ? a:b;

int items[N][2] = {
	,
	,
	,
	,
	
};

int knapsack(int pos,int capacity) {
	if (pos == N) return 0;
	
	int ret = 0;
	if (items[pos][W] <= capacity) //지금 pos의 물건을 훔칠 수 있을때
		ret = knapsack(pos + 1, capacity - items[pos][W])
		+ items[pos][V];
        //지금 pos의 물건을 훔치지 않을때와 ret 중에 큰 값
	ret = MAX(ret, knapsack(pos + 1, capacity)); 
	return ret;
}
int main() {
	int capacity = 15;
	printf("knapsack(%d,%d)=%d\n",0,capacity, knapsack(0, capacity));
	return 0;
}

답은 원하는 결과대로 나옵니다.

 

허나 이런 무식한 해결방법을 원한 것을 아닙니다.

만약 물건의 수가 많게 된다면 답을 구하기 전에 도둑은 감방으로 직행하게 될 것입니다. knapsack함수는 분명 같은 값을 여러번 중복해서 계산합니다. knapsack(pos,capacity)에서 같은 pos와 같은 capacity가 이전에 계산했었음에도 불구하고 또 다시 한번 더 계산한다는 것이죠.

 

그래서 DP가 사용됩니다. 단지 계산된 결과를 기억하고 있다가, 같은 계산을 수행할때 바로 값을 반환해주면 됩니다.

 

아래의 수정된 코드처럼 말입니다.

 

#include <stdio.h>
#include <stdlib.h>
#define W 0
#define V 1
#define N 5
#define MAX_CAPACITY 1000
#define MAX(a,b) a>b ? a:b;

int dp[N][MAX_CAPACITY];
int items[N][2] = {
	,
	,
	,
	,
	
};

int knapsack(int pos,int capacity) {
	if (pos == N) return 0;
	
	int ret = dp[pos][capacity];
	if (ret != -1) return ret;
	if (items[pos][W] <= capacity)
		ret = knapsack(pos + 1, capacity - items[pos][W])
		+ items[pos][V];
	ret = MAX(ret, knapsack(pos + 1, capacity));
	return dp[pos][capacity]=ret;
}
int main() {
	int capacity = 15;
	memset(dp, -1, sizeof(dp));

	printf("knapsack(%d,%d)=%d\n",0,capacity, knapsack(0, capacity));
	return 0;
}

우선 dp라는 2차원 배열을 -1로 셋팅합니다. 그리고 dp[pos][capacity]가 -1이 아니면 바로 그 값을 반환합니다. 배낭문제에서 음수는 나올 수 없습니다. 만약 계산되지 않은 값이라면 계산 후 dp[pos][capacity]에 값을 넣어주는 것입니다. 기억하고 있습니다.

 

결국 도둑은 가장 비싼 물건들을 훔쳐갈 수 있었지만, CCTV에 걸려 감방에 끌려가게 됩니다. 그걸 도와준 저도 같이 갑니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

포인터배열

포인터라는 것은 조금 알겠는데 포인터배열은 무엇일까요... 포인터도 힘들게 배우는데 말이죠. 정말 산넘어 산입니다.

포인터배열이란 포인터를 원소로 갖는 배열을 의미합니다. 포인터 각각을 배열로 표현한 것이지요.

느낌이 오시나요?

코드와 그림으로 알아보도록 합시다.

 

#include <stdio.h>
int main() { 
        int a = 10; 
        int b = 20; 
        int c = 30; 
        int *pArr[3] = { &a,&b,&c };

        printf("%d\n", *pArr[0]);
        printf("%d\n", *pArr[1]);
        printf("%d\n", *pArr[2]);
}


여기, 포인터배열을 간단하게 알아볼 수 있는 코드입니다. pArr은 평소에 보던 배열과는 다르게 앞에 *(pointer)를 볼 수 있지요?

연산자 우선순위에 의하면 배열첨자([])가 포인터(*)연산보다 먼저입니다. 그렇기 때문에 배열 3개가 있고, 그 배열의 원소는 포인터라는 의미가 됩니다. 

따라서 배열의 원소인 pArr[0]은 a의 주소를 갖고있고, pArr[1]은 b의 주소를 갖고 있고, pArr[2]는 c의 주소를 갖고 있습니다.

이 코드의 상황을 그림으로 나타냈습니다.

 

 

 

만약 pArr[0]을 찍어보면 a의 주소가 나오게 됩니다. a의 값에 접근하고 싶다면 포인터 연산을 해주면 됩니다. 바로 *pArr[0], 이렇게요.

그렇게 포인터 원소를 배열로 나열했기 때문에 포인터배열이라 부릅니다.

 

포인터배열로 배열 가리키기

포인터가 배열의 시작주소를 가리킬 수 있다는 것은 이제 잘 알겁니다. 아닌가? 그렇다면 어떤 배열들을 포인터 배열로 가리킬 수도 있다는 느낌이 오시나요?

코드를 통해 느껴봅시다. 

 

#include <stdio.h>  
int main() { 
        int i, j; 
        int a[5] = { 1,2,3,4,5 };
        int b[6] = { 10,20,30,40,50,60 }; 
        int c[7] = { 100,200,300,400,500,600,700 };
        int *pArr[3] = { a,b,c }; 
        int sub_len[3] = 
        { sizeof(a) / sizeof(int), sizeof(b) / sizeof(int), sizeof(c) / sizeof(int) }; 
        int len = sizeof(pArr) / sizeof(int*);   

        for (i = 0; i < len; i++) { 
                for (int j = 0; j < sub_len[i]; j++) 
                        printf("%d ", pArr[i][j]); 
                printf("\n"); 
        } 
}

a,b,c 배열의 길이는 전부 다릅니다. 하지만 문제없지요. 왜냐면 포인터는 배열의 시작주소만 알면 되기 때문입니다.

각각의 포인터들(pArr[0], pArr[1], pArr[2])은 배열의 시작주소 a, b, c를 가리키고 있습니다.

 

포인터 역시 배열처럼 첨자를 쓸수도 있다는 것을 다들 아실겁니다.

 

 

 

 

 

배열포인터

배열포인터는 무엇일까요? 아까 포인터배열은 포인터를 배열로 나열한 것이라고 설명했으니, 배열포인터는 배열을 가리키는 포인터가 아닐까요?

 

배열포인터는 다음과 같이 정의합니다.

int (*pArr)[3]

 

앞서 본 포인터배열과는 다르게 괄호가 추가 되었죠. 이 한 끗 차이에 의미가 변하게 됩니다. 우선 포인터이긴한데, 길이 3을 갖는 int형 배열만을 가리킬 수 있다는 점입니다. 우선 다음 기본적인 코드를 봅시다. 우리가 아는 내용입니다.

#include <stdio.h> 
int main() { 
        int i = 0; 
        int arr[5] = { 1,2,3,4,5 };
        int *pArr = arr; 

        for (i = 0; i < sizeof(arr)/sizeof(int); i++) 
                printf("%d ", pArr[i]);
        printf("\n");
}  
r


pArr은 일차원배열 arr의 시작주소를 가리키고 있다는 내용입니다. 그래서 배열처럼 인덱싱을 통하여 각 원소를 출력하고 있지요. arr의 길이 5는 신경쓰지 않습니다. 단지 가리키기만 하고 있습니다.

이제 위의 정의를 다시 봅시다. 

 

int arr[행][열] ={ {...}, {...}, {...}};

int (*pArr)[열]= arr;

 

(*pArr)은 arr의 가장 높은 차원의 길이 3(행)은 신경쓰지 않습니다. 단지 그 시작주소만 가리키기만 하면 되거든요. 하지만 그 보다 낮은 차원의 길이4(열)는 알아야만 합니다. 그래야만 다음 행을 구해낼 수 있기 때문이죠.

어떻게??

pArr[0]은 4개의 int배열을 가리키고 있는 포인터입니다. 따라서 sizeof(pArr[0])을 찍어보면 그 길이가 16이라는 것을 알 수 있습니다. 그래서 주소를 계산할때 지금 행의 주소에 16을 더해야 다음 행을 구할 수 있습니다. 위 코드에서는 1차원 배열이고 각 원소의 길이는 단순히 1이니까 쓰지 않는 것입니다.

이제 2차원 배열을 배열포인터로 구현해봅시다.

#include <stdio.h>
int main() { 
        int i,j; 
        int arr[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} }; 
        int(*pArr)[4] = arr; 
        int row = sizeof(arr) / sizeof(arr[0]);
        int col = sizeof(arr[0]) / sizeof(arr[0][0]);

        for (i = 0; i < row; i++) { 
                for (j = 0; j < col; j++) 
                        printf("%d ", pArr[i][j]); 
                printf("\n"); 
        } 
}

 

arr의 각 행 길이와 pArr의 행의 길이를 맞추고 있다는 것을 보세요. 그리고 pArr[0:2]는 각각 사이즈가 16이며 마치 배열처럼 동작이 가능합니다. 

 

이것을 어디다가 활용할 수 있을까요??

 

함수에 전달인자로 배열을 받을때 주로 사용합니다.

 

#include <stdio.h>

void printArr(int(*pArr)[4],int row,int col) {
        int i, j; 
        for (i = 0; i < row; i++) { 
                for (j = 0; j < col; j++) 
                        printf("%d ", pArr[i][j]); 
                printf("\n"); 
        }
} 

int main() { 
        int arr[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12}}; 
        int row = sizeof(arr) / sizeof(arr[0]); 
        int col = sizeof(arr[0]) / sizeof(arr[0][0]);
        printArr(arr,row,col); 
}


매개변수를 받으려면 위와 같이 전달받고, 배열처럼 인덱싱을 편하게 사용할 수 있습니다.

 

더 간편한 방법으로는 int (*pArr)[4]를 int pArr[][4]로 바꿔줘도 실행이 가능합니다. 왜냐면 *pArr은 pArr[]과 거의 같은 의미이기 때문입니다.

void printArr(int pArr[][4],int row,int col) { 
        int i, j; 
        for (i = 0; i < row; i++) { 
                for (j = 0; j < col; j++) 
                        printf("%d ", pArr[i][j]);
                printf("\n"); 
        }
}

이상 끝~~~~~~~~~~~~~~~~~~~~~~~~~~~~

반응형
블로그 이미지

REAKWON

와나진짜

,

C언어 파일 입출력

 

C언어에서 scanf와 printf 함수를 통해서 키보드로 입력을 받고 모니터로 출력해주는 그런 프로그램들을 많이 보았을 겁니다. 이런 키보드나 모니터같은 입출력 장비를 콘솔이라고 합니다. 그래서 콘솔 입출력을 해왔던 것이죠.

여기서는 파일 입출력에 대해서 설명합니다. 사실 콘솔 입출력과는 별로 다를바가 없습니다. 단지 그 대상이 모니터나 키보드가 아닌 파일이기 때문이죠. 본격적으로 파일 입출력을 설명하기 전에 우리는 스트림에 대한 개념을 먼저 알아야합니다. 

> 그전에 왜 C 표준입출력을 사용하나요?

리눅스를 배우셨던 분들은 open, read, write, close를 이용해서 파일을 다뤄보셨을 겁니다. 그때는 open에 필요에 따라 여러 플래그들을 줄 수가 있는데요. 예를 들어 O_RDONLY, O_CREAT 등 말이죠. 이거 구차하게 일일히 헤더 추가한 다음에 파일 디스크립터를 가져와서 write, read하는 것을 C 표준입출력 라이브러리에서는 stdio.h만 포함해서 사용할 수 있습니다.  아주 개꿀이라는 얘기죠. 그리고 변태같은 플래그들을 포함하지 않아도 사용하기에 적합한 플래그들을 미리 조합해놨기 때문에 상큼하게 그걸 사용하면 됩니다. 또한 내부적으로 버퍼를 사용하기 때문에 read, write 함수들을 최적으로 사용하게 됩니다.

 

스트림(Stream)

영어를 그대로 직영하게 되면 흐름이라는 건데요. 비슷하게 생각하시면 됩니다. 프로그램에서 파일이 열리면 C표준입출력은 스트림(stream)이라는 파일과 프로그램 사이의 추상적인 흐름이 일어나는 파이프를 생성합니다. 그래서 파일이 열리게 되면 개념적으로 스트림을 통해서 파일에 기록하거나 읽을 수 있습니다. 만약 파일을 읽기만 하겠다하면 읽기 전용의 스트림을 여는 것이고, 파일을 쓰기만 할 것이라면 쓰기 전용의 스트림을 열어서 거기에 기록을 하면 됩니다. 

그래서 아래와 같이 어떤 프로그램에서 File이라는 이름의 파일을 읽고 쓰기 위해서 스트림을 열면 아래와 같은 상황이 발생하게 됩니다. 그래서 바이트 단위던, 줄 단위던 입력이 흐름이 가능한 상태가 됩니다.

 

기본적으로 보통 프로그램에서는 3개의 스트림이 열려있습니다. 바로 표준 입력 스트림(stdin), 표준 출력 스트림(stdout), 표준 에러 스트림(stderr)입니다. 이 3개는 콘솔에 대해서 열려있는 스트림들입니다. 

 

stdin, stdout, stderr

키보드로 입력받고, 모니터로 출력하는 것도 C표준 입출력에서는 스트림으로 간주하게 됩니다. 그래서 우리가 stdin을 통해서 입력을 받는다면 키보드를 통해서 입력을 받는 것이고, 표준 출력 스트림으로 출력한다면 모니터 화면에다가 출력이 되는 겁니다. 그래서 파일 대신 모니터와 키보드가 스트림 끝에 놓여있는 것을 보세요.

 

 

파일의 종류 ( 텍스트 파일 , 이진 파일)

파일을 사람이 쓰고 읽냐, 컴퓨터가 쓰고 읽냐에 따라서 텍스트 파일(text-file), 이진 파일(binary-file)로 나누게 됩니다. 이와 같은 구분은 문자열로 입출력을 하느냐, 아니면 바이너리로 입출력을 하느냐를 위해서 구분합니다. 맨 처음 파일에 대해서 스트림을 생성할 때 결정이 됩니다.

 

1. 파일 열기 fopen

파일 함수는 표준입출력(stdio.h) 헤더파일에 존재합니다. 파일에 어떤 데이터를 읽고, 쓰고, 추가하려면 일단 파일을 열어야겠지요. 함수를 한번 보시죠.

FILE *fopen(const char *filename, const char *mode);

filename : 파일명을 말합니다. 절대 경로나 상대 경로로 줄 수 있습니다. 상대 경로는 그 프로젝트 위치를 기준으로 합니다.

mode : 파일을 어떤 방식으로 열건지 정합니다. 스트림 방식을 정하는 겁니다. 입력 스트림인지, 출력 스트림인지.

  -동작 모드

 모드  설명 flag 조합
 r(read)  1. 파일을 읽기 전용으로 엽니다. 
 2. 파일이 있어야합니다.
O_RDONLY
 w(write)  1. 파일을 쓰기 전용으로 엽니다.
 2. 주의해야합니다. 파일이 존재한다면 기존의 내용을 지우고 쓰기 때문이죠.

 3. 파일이 없으면 새로 생성합니다.
O_WRONLY | O_CREAT | O_TRUNC
 a(append)  1. 파일이 있으면 파일의 끝에 내용을 추가합니다.
 2. 파일이 없으면 생성해서 내용을 추가합니다.
O_WRONLY | O_CREAT | O_APPEND
 r+  1. 파일을 읽고 쓰기 위해 엽니다.
 2. 파일이 반드시 있어야 합니다.
O_RDWR
 w+  1. 파일을 읽고 쓰려고 엽니다.
 2. r+와 다르게 파일이 있는 경우 내용을 덮어쓰고 없으면 생성해서 데이터를 씁니다. 
O_RDWR | O_CREAT | O_TRUNC
 a+  1. 파일을 읽고 갱신하기 위해 엽니다.
 2. 파일이 없으면 생성해서 데이터를 추가합니다.
O_RDWR | O_CREAT | O_APPEND

 

  - 이진 또는 텍스트 모드(t, b)

텍스트모드가 기본(default)입니다. 이진 모드로 파일을 열려면 b를 추가합니다. 

ex) 이진모드로 읽기 위해 파일을 open -> rb

파일을 여는 데 성공했다면 그 파일에 대한 포인터를 return합니다.

하지만 파일을 여는 데 실패했으면 NULL을 반환하죠.

 

2. 파일 닫기 fclose

무엇이든 열었으면 닫는 것이 원칙이죠. 파일 스트림을 닫으려면 fclose를 사용하시면 됩니다.

int fclose(FILE *stream);

그냥 열었던 파일 포인터를 집어넣으면 됩니다. 성공하면 0을 반환하고 실패하면 EOF(-1)를 반환합니다.

 

3. 텍스트 파일 읽기 함수 

파일은 두 종류의 파일이 있다고 했죠? 사람이 읽을 수 있는 텍스트 형식의 파일과 컴퓨터가 읽고 처리하는 바이너리 파일, 즉 이진 파일이 있습니다. 우선 텍스트 파일을 읽는 함수는 쓰임새에 따라 여러가지가 있습니다.

    3.1 한문자 읽기 : fgetc, getc, getchar

#include <stdio.h>
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);

fgetc와 getc는 같은 기능을 하는 함수입니다. getchar() 함수는 키보드용 한문자 입력을 받는 함수와 같아서 getc(stdin)과 같습니다. 

getc = getc , getc(stdin) = getchar()

stream에서 한 글자를 읽어오는 함수이며, 일반적으로 반환형은 한 글자의 ASCII값인 정수형 값입니다. 파일의 끝에 도달할 시에 EOF를 return합니다. EOF는 End-Of-File로 -1입니다. 이렇게 반환형이 (signed) int인 이유는 이 EOF를 반환받기 위해서입니다. 

    3.2 한 줄 읽기 : fgets

#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);

stream에서 문자 한줄을 읽어올때 사용하는 함수이며 size 이하의 문자 한줄을 s로 읽어옵니다. 이때 개행문자까지 읽어옵니다. 그래서 개행문자('\n') 다음 문자의 끝을 나타내는 문자인 NULL('\0')이 붙습니다. 간단히 사용법을 확인해볼까요? 다음은 stdin으로 콘솔(키보드)로부터 입력을 받는 단순한 예제입니다.

//fgets_test.c
#include <stdio.h>
#include <string.h>

#define BUF_SIZE 32
int main(){

        char buf[BUF_SIZE] = {0,};

        printf("입력:");
        fgets(buf, BUF_SIZE, stdin);

        printf("출력:");
        printf("%s(%ld)", buf, strlen(buf));
}
# ./a.out 
입력:hello world
출력:hello world
(12)#

여기서 보이는 "hello world"의 문자열 길이는 공백을 포함해서 11글자이지만, 개행문자를 포함했기 때문에 12글자가 되고, 문자 길이도 한 줄 밑에 출력이 되었네요. 

    3.3 서식화된 파일 입력 : fscanf

#include <stdio.h>
int fscanf(FILE *stream, const char *format, ...);

키보드 입력에 대해서 입력 포맷팅 함수는 scanf였죠? 파일에 대해서 포맷팅 함수는 fscanf입니다. 

 

4. 텍스트 파일 쓰기 함수  

텍스트 파일에 쓰는 함수는 아래와 같습니다. 위의 텍스트 파일 읽기 함수의 네이밍을 따라갑니다. 함수에 대한 소개만하고 넘어가도록 합시다. 

    4.1 한문자 쓰기 : fputc, putc, putchar

#include <stdio.h>
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);

   putchar(c)는 putc(stdout, c)와 같습니다.

    4.2 한 줄 쓰기 : fputs, puts

#include <stdio.h>
int fputs(const char *s, FILE *stream);
int puts(const char *s);

    4.3 서식화된 파일 출력 : fprintf

#include <stdio.h>
int fprintf(FILE *stream, const char *format, ...);

 

예제 - 텍스트 데이터 저장, 읽어오기

//writer.c

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

#define NUM 3

typedef struct _student{
        char name[16]; //이름
        unsigned int age; //나이 
        unsigned int id; //학번
} student;


int main(){
        int i;
        student s[NUM] = {
                {"park", 18, 1234},
                {"jung", 18, 1235},
                {"kim", 19, 1111}
        };
        FILE *fp;

        //텍스트 파일, 없으면 새로운 파일 생성, 있으면 내용 덮어쓰기(w+)
        fp = fopen("info.txt", "w+");
        if(fp == NULL) {
                printf("fopen error\n");
                return 1;
        }

        for(i = 0;i < NUM; i++){
                fprintf(fp, "%s %d %d\n",
                        s[i].name, s[i].age, s[i].id);
        }
        fclose(fp);
}
//reader.c
#include <stdio.h>

#define NUM 3

typedef struct _student{
        char name[16]; //이름
        unsigned int age; //나이 
        unsigned int id; //학번
} student;

int main(){
        int i;
        FILE *fp;
        student s[NUM]; 

        //테스트 파일 읽기 전용
        fp = fopen("info.txt", "r");
        if(fp == NULL){
                printf("fopen error\n");
                return 1;
        }
        for(i = 0; i < NUM; i++){
                fscanf(fp,"%s %d %d", 
                                s[i].name, &(s[i].age), &(s[i].id));

                printf("[%d]\n", i);
                printf("name : %s, age : %u, id : %u\n",
                                s[i].name, s[i].age, s[i].id);
        }
        fclose(fp);
}
# gcc reader.c -o reader
# ./writer
# cat info.txt
park 18 1234
jung 18 1235
kim 19 1111

writer만 실행해보면 info.txt가 생겨났고 그 내용은 이렇게 적혀있습니다. 사람이 알아볼 수 있죠?

# ./reader 
[0]
name : park, age : 18, id : 1234
[1]
name : jung, age : 18, id : 1235
[2]
name : kim, age : 19, id : 1111

reader라는 프로그램으로도 아주 잘 읽을 수 있습니다.

 

5. 이진 파일 읽기 fread

파일을 읽는 함수는 fread입니다. 프리드라고 읽지마세요 제발. 앞에 f는 모두 file의 f입니다. 앞에 f가 붙은 함수는 거의 다 파일에 대한 함수라는 것을 기억하세요.  

#include <stdio.h>
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);

 

stream으로부터 자료형인 size를 count만큼 읽어서 buffer에 저장합니다. buffer가 void*인 이유는 어떤 자료형이건 받아와야하기 때문입니다.  파일을 읽은 길이(count)만큼 반환합니다. 

만약에 단순 바이너리를 읽는다면, 그러니까 바이트 단위를 읽는다면 size는 1입니다. 그러면 만약 크기 16바이트인 구조체를 3개를 읽는다면 아래와 같이 호출이 됩니다. 

fread(buffer, 16, 3, fp);

6. 이진 파일 쓰기 fwrite

파일에 쓰는 함수입니다. fread와는 반대 기능이죠.

#include <stdio.h> 
size_t fwrite(const void *buffer, size_t size, size_t count, FILE *stream);

buffer에 담긴 내용을 기록하는데 size만큼의 count 만큼 버퍼로부터 stream쪽으로 씁니다. 성공하면 count를 return하고 실패한다면 count가 아닐 수 있습니다.

 

예제 - 이진데이터 구조체 저장, 읽어오기

이진 파일을 사용할 수 있는 가장 큰 장점은 모든 자료를 이진데이터로 쓸 수 있다는 점입니다. 객체(구조체)도 그냉 냅다 쓸 수 있습니다. 모든 것을 이진 데이터로 쓰기 때문이지요. 다음은 구조체를 파일에 쓰고, 그 파일로부터 읽어오는 예제를 보여줍니다.

//info_writer.c
#include <stdio.h>

#define NUM 3

typedef struct _student{
        char name[16]; //이름
        unsigned int age; //나이 
        unsigned int id; //학번
} student;

int main(){
        FILE *fp;
        student s[NUM] = {
                {"kim", 16, 1234},
                {"lee", 16, 1235},
                {"lim", 17, 1111}
        };

        //이진(b)으로 쓰기용, 없으면 만들고 있으면 덮어쓴다(w+)
        fp = fopen("info.bin", "wb+");
        if(fp == NULL){
                printf("fopen error\n");
                return 1;
        }

        if(fwrite(s, sizeof(student), NUM, fp) != NUM){
                printf("fwrite erorr\n");
                fclose(fp);
                return 1;
        }

        printf("Student Information Saved OK \n");
        fclose(fp);
}

 

//info_reader.c
#include <stdio.h>

#define NUM 3

typedef struct _student{
        char name[16]; //이름
        unsigned int age; //나이 
        unsigned int id; //학번
} student;

int main(){
        int i;
        FILE *fp;
        student s[NUM]; 

        fp = fopen("info.bin", "rb"); //읽기 전용
        if(fp == NULL){
                printf("fopen error\n");
                return 1;
        }

        if(fread(s, sizeof(student), NUM, fp) != NUM){
                printf("fread erorr\n");
                fclose(fp);
                return 1;
        }

        for(i = 0; i < NUM; i++){
                printf("[%d]\n", i);
                printf("name : %s, age : %u, id : %u\n",
                                s[i].name, s[i].age, s[i].id);
        }
        fclose(fp);
}

 

# gcc info_writer.c -o writer
# gcc info_reader.c -o reader
# ./writer 
Student Information Saved OK 
# ./reader 
[0]
name : kim, age : 16, id : 1234
[1]
name : lee, age : 16, id : 1235
[2]
name : lim, age : 17, id : 1111

 

7. 버퍼 

버퍼는 C표준입출력에서 입력과 출력을 효율적으로 처리하기 위한 일종의 저장공간입니다. 내부적으로 write, read를 적시에 한번만 호출하기 위한 것이 목적입니다. 그런데 이러한 버퍼의 처리 방식을 잘 모르면 낭패를 볼 수 있는데요. 아래의 코드를 봅시다.

//buffer.c
#include <stdio.h>

int main(){

        char c;

        printf("아무 글자나 하나 입력:");
        scanf("%c", &c);
        printf("입력받은 글자 : %c\n", c);

        printf("다시 입력 : ");
        scanf("%c", &c);
        printf("입력받은 글자 : %c\n", c);
}

 

실행하게 되면 두 번재 scanf에 입력을 주기도 전에 프로그램이 끝나게 됩니다. 분명 scanf를 통해서 한글자 입력을 받는 코드를 작성했으에도 말이죠. 

# ./a.out 
아무 글자나 하나 입력:H
입력받은 글자 : H
다시 입력 : 입력받은 글자 : 
#

 

이 프로그램은 내부적으로 이렇게 동작하게 됩니다. 'H'라는 문자를 입력하면 내부적으로 엔터에 해당하는 개행 문자 '\n'도 입력이 됩니다.

 

결국 버퍼에는 H와 '\n'이 입력이 되게 되며 변수 c에는 'H'가 담기게 되겠죠. 버퍼에 남아있는 건 개행문자 '\n'입니다. 그래서 다음 scanf는 이 개행문자를 입력받아 입력이 끝나게 되는 겁니다. 

 

위는 줄 단위 버퍼링의 사례로 결국에는 남아있는 버퍼를 비워줘야합니다. 버퍼를 비워주는 방법에는 여러 가지 방법이 있는데요.

7.1 버퍼를 비우는 방법들

- 간단한 방법은 단순히 문자 하나 입력받는 거죠. 아래와 같이말이죠. 

printf("아무 글자나 하나 입력:");
scanf("%c", &c);
getchar();
printf("입력받은 글자 : %c\n", c);

printf("다시 입력 : ");
scanf("%c", &c);
getchar();
printf("입력받은 글자 : %c\n", c);

 

- fflush 함수 사용

#include <stdio.h>
int fflush(FILE *stream);

fflush 함수를 사용할 수가 있는데, 이 방법은 표준이 아니므로 권장되지 않습니다. 실제 제 ubuntu시스템에서는 동작하지 않습니다.

 

- scanf에서 공백 사용

scanf("%c", &c) -> scanf(" %c", &c)

 

앞에 공백 문자 하나를 넣어주세요. 

 

- 개행문자가 나올때까지 제거 

while(getchar() != '\n');

어떤 시스템에는 \r\n으로 개행합니다. 그게 윈도우즈인데, 이럴 때는 getchar()만 사용하게 되면 \r만 제거 됩니다. 그래서 아예 \n까지 제거 할 수 있도록 while문을 도는 방식을 사용할 수 있습니다. 약간 고급진 말로 '\r'은 커서를 맨 앞으로 돌리는 CR(Carriage return)이라 하며 '\n'은 커서는 그자리이며 라인만 바꾸는 LF(Line Feed)라고 합니다.  

반응형
블로그 이미지

REAKWON

와나진짜

,