5. 링크드 리스트
: 링크드 리스트(linked list)는 C에서 쉽게 구현할 수 있는 유용한 데이터 저장방법이다. 왜 포인터에 대한 주제를 다루면서 링크드 리스트를 언급하는 것일까? 잠시 후에 배울 것처럼 포인터는 링크드 리스트의 핵심이다.

단순 링크드 리스트, 이중 링크드 리스트, 이진 트리를 포함하여 많은 종류의 링크드 리스트가 있다. 각각의 형태는 데이터를 저장할 필요가 있는 특별한 경우에 적절히 사용된다. 이런 링크드 리스트에서 공통적인 사항은 데이터 항목 간의 결합이 데이터 항목 자체 내에 포함되어 있는 정보에 의해 포인터의 형식으로 정의된다는 것이다. 이런 사실은 데이터 항목 간의 결합이 배열의 배치와 저장을 기반으로 하는 배열과 링크드 리스트를 분명히 구분하는 기준이다. 이 단원에서는 가장 간단한 형태의 링크드 리스트이고 간단히 링크드 리스트라고도 하는 단순 링크드 리스트에 대해서 설명한다.

5.1 링크드 리스트의 기초
: 링크드 리스트의 각 데이터 항목은 구조체에 포함되어 있다. 구조체는 데이터를 저장하는 데 필요한 요소들을 가지고 있다. 어떤 데이터 요소를 가지는지는 프로그램에 따라 달라진다. 링크드 리스트에는 한 가지 요소가 추가되는데 바로 포인터이다. 이 포인터는 링크드 리스트에서 연결 방법을 제공한다. 다음은 간단한 예제이다.

   struct person{
      char name[20];
      struct person *next;
   };

 이 코드는 person이라는 구조체를 정의한다. 데이터만을 이야기한다면 person은 단지 20개 의 요소로 구성되는 문자형 배열을 가진다. 여러분은 일반적으로 이런 간단한 데이터에 대해 링크드 리스트를 사용하지 않겠지만, 여기서는 예로 들어 설명하는 것이므로 실용성에 대해서는 생각할 필요가 없다. person 구조체는 person형의 포인터를 가진다. 다시 말해서 같은 형의 다른 구조체에 대한 포인터이다. 이것은 person형의 각 구조체가 데이터의 모음을 가질 뿐 아니라 다른 person 구조체를 지적할 수 있음을 뜻한다. <그림 15.7>은 이런 구조체가 링크드 리스트에서 서로 연결되는 방법을 보여준다.



<그림 15.7> 링크드 리스트에서의 연결 상태

<그림 15.7>에서 각 person구조체가 다음 person 구조체를 지적한다는 것에 주목하기 바란다 마지막 person구조체는 어떤 것도 지적하지 않는다. 링크드 리스트에서 마지막 요소는 NULL의 값으로 할당되는 포인터 요소에 의해 정의된다.

* 참고
: 링크드 리스트에서 하나의 요소가 되는 각 구조체를 링크드 리스트의 링크(link), 노드(node) , 요소(element)라고 한다.

 여러분은 링크드 리스트에서 마지막 링크가 어떻게 구분되는지 배웠다. 첫번째 링크는 어떻게 구분될까? 첫 번째 링크는 헤드 포인터(head pointer)라는 특별한 포인터(구조체가 아닌 포인터)에 의해 구분된다. 헤드 포인터는 항상 링크드 리스트에서 첫번째 링크를 지적한다. 첫 번째 링크는 두 변째 링크에 대한 포인터를 가지고, 두 번째 링크는 세 번째에 대한 포인터를 가지고, 이런 연결 상태는 포인터가 NULL인 마지막 링크를 만날 때까지 계속된다. 만약 전체 링크드 리스트가 비어 있다면, 즉 어떤 링크도 없다면 헤드 포인터가 NULL로 설정된다. <그림 15.8>은 링크드 리스트가 시작되기 전과 링크드 리스트에 첫 번째 링크가 추가된 후의 헤드 포인터를 보여준다.



<그림 15.8> 링크드 리스트의 헤드 포인터

5.2 링크드 리스트 다루기
: 링크드 리스트를 사용할 때에는 각 링크를 추가, 삭제, 변경할 수 있다. 링크를 변경하는 것은 크게 어렵지 않다. 그러나 링크를 추가하고 삭제하는 것은 약간 복잡하다. 앞에서도 설명했듯이 링크드 리스트에서 링크들은 포인터로 연결된다. 링크를 추가하고 삭제하는 것은 대부분 이런 포인터를 다루는 일이다. 각 링크는 링크드 리스트의 시작, 중간, 마지막에 추가될 수 있다. 어디에 추가하느냐에 따라 포인터를 변경하는 방법이 달라진다. 이 장에서는 간단한 링크드 리스트 예제와 함께 더욱 복잡한 프로그램을 살펴볼 것이다. 그러나 복잡한 프로그램을 설명하기 전에 링크드 리스트를 사용할 때 필요한 동작을 하나씩 살펴보도록 하자. 이 단원에서는 앞에서 사용했던 person 구조체를 계속해서 사용할 것이다.
 
▶ 준비 작업
: 링크드 리스트를 사용하려면 링크드 리스트에서 사용할 데이터 구조체를 정의할 필요가 있고 헤드 포인터를 선언할 필요가 있다. 링크드 리스트는 비어있는 상태로 시작하므로 헤드 포인터는 NULL로 초기화되어야 한다. 또한, 링크를 추가하는 데 사용할 링크드 리스트의 구조체형에 대한 포인터를 추가로 선언해야 한다. 잠시 후에 설명할 것처럼 하나 이상의 포인터가 필요하다. 다음은 예제이다.

  struct person{
      char name[20];
      struct person *next;
   };
   struct person *new;
   struct person *head;
   head = NULL;

▶ 링크드 리스트의 시작 부분에 링크 추가하기
: 만약 헤드 포인터가 NULL이면 링크드 리스트는 비어 있는 것이고 새로운 링크는 유일한 멤버가 될 것이다. 헤드 포인터가 NULL이 아니라면 링크드 리스트는 이미 하나 이상의 링크를 가지고 있는 것이다. 어떤 경우든지 링크드 리스트의 시작 부분에 새로운 링크를 추가하는 방법은 똑같다.

① malloc()을 사용하여 메모리 공간을 할당하며 구조체형 변수를 생성한다.

② 새로운 링크의 next 포인터를 헤드 포인터의 현재 값으로 설정한다. 이 값은 링크드 리스트가 비어 있다면 NULL이 될 것이고, 그렇지 않다면 현재 첫 번째 링크의 주소가 될 것이다.

③ 헤드 포인터가 새로운 요소를 지적하게 한다.

다음은 이런 작업을 수행하는 코드이다.

   new = (person*)malloc(sizeof(struct person));
   new -> next = head;
   head = new;

malloc()은 새로운 링크를 위한 메모리를 할당하는 데 사용된다는 것에 주목하기 바란다. 각 새로운 링크를 추가할 때 단지 해당 링크에 필요한 메모리만이 할당된다. calloc() 함수를 사용할 수도 있을 것이다. 두 함수의 차이점에 주의할 필요가 있다. 주요 차이점은 calloc()이 새로운 링크를 초기화한다는 것이다. malloc()은 새로운 링크를 초기화하지 않는다.

▶ 링크드 리스트의 마지막에 링크 추가하기
: 링크드 리스트의 마지막에 추가하기 위해서는 헤드 포인터에서 시작하고 마지막 링크를 찾을 때까지 링크드 리스트를 통해 차례대로 진행할 필요가 있다. 마지막 링크를 발견하면 다음과 같은 단계를 따른다.

① malloc()을 사용하여 메모리 공간을 할당하며 구조체형 변수를 생성한다.

② 마지막 링크의 next 포인터가 새로운 링크를 지적하게 설정한다. 새로운 링크의 주소는 malloc()에 의해 복귀된다.

③ 새로운 링크가 링크드 리스트에서 마지막 항목이라는 것을 표시하기 위해 새로운 링크의 next 포인터를 NULL로 설정한다.

다음은 코드이다.

   person *current;
   …
   current = head;
   while(current -> next != NULL)
   current = current -> next;
   new = (person*)malloc(sizeof(struct person));
   current -> next = new;
   new -> next = NULL;

▶ 링크드 리스트의 중간에 링크 추가하기
: 힝크드 리스트를 사용하다 보면 대개는 링크드 리스트의 중간에 어디든지 링크를 추가하게 될 것이다. 정확히 새로운 링크가 위치되는 곳은 링크드 리스트를 어떻게 사용하느냐에 따라 다르다. 예를 들어, 하나 이상의 데이터 요소에 따라 링크드 리스트를 정렬한다면 링크를 추가하고 나서 정렬해야 하므로 새로운 링크의 위치가 달라진다. 다음과 같이 진행하기 바란다.

① 리스트에서 새로운 링크가 그 다음에 위치될 기존의 링크를 찾는다. 이것을 표식 요소(marker element)라고 하자.

② malloc()을 사용하여 메모리 공간을 할당하며 구조체형 변수를 생성한다.

③ 표식 요소의 next 포인터가 새로운 링크를 지적하게 한다. 새로운 링크의 주소는 malloc()에 의해 반환된다.

④ 새로운 링크의 next 포인터를 표식 요소가 지적하던 기존의 링크를 지적하게 설정한다. 다음은 예제 코드이다.

   person *marker;
   /* 링크드 리스트에서 원하는 위치를 지적하도록 표식을 설정하는 코드 */
   …
   new = (LINK)malloc(sizeof(PERSON));
   new -> next = marker -> next;
   marker -> next = new;

▶ 링크드 리스트에서 링크 삭제하기
: 링크드 리스트에서 링크를 삭제하는 것은 포인터를 다루면 될 정도로 간단한 문제이다. 정확한 과정은 링크드 리스트에서 링크의 위치에 따라 다르다.

·첫 링크를 삭제하기 위해서 헤드 포인터가 링크드 리스트에서 두 번째 링크를 지적하도록 설정한다.

·마지막 링크를 삭제하기 위해서 마지막 바로 앞의 링크의 next 포인터를 NULL로 설정한다

·다른 어떤 링크를 삭제하기 위해서 삭제되는 바로 앞 링크의 next 포인터를 삭제되는 바로 다음 링크를 지적하도록 설정한다.

다음은 링크드 리스트에서 첫 링크를 삭제하는 코드이다.

  head = head -> next;

다음은 링크드 리스트에서 마지막 링크를 삭제하는 코드이다.

   person *current1, *current2;
   current1 = head;
   current2 = current1 -> next;
   while (current2 -> next != NULL)
   {
      current1 = current2;
      current2 = current1 -> next;
   }
   current1 -> next = NULL;
   if(head == current1)
     head = null;

마지막으로, 다음 코드는 링크드 리스트에서 특정 링크를 삭제한다.

   person *current1, *current2;
   /* current1이 삭제되는 바로 앞 링크를 지적하도록 설정하는 코드 */
   current2 = current1 -> next;
   current1 -> next = current2 -> next;

어느 부분에서 링크를 삭제하든지 삭제된 링크는 여전히 메모리에 남아 있지만 링크드 리스트에서 지적하는 포인터가 없으므로 제거된다. 실제 프로그래밍에서는 "삭제된" 링크가 차지하던 메모리를 해제시켜 확보하기 원할 것이다. free() 함수를 이용하면 되는데, 자세한 내용은 나중에 "메모리 다루기"에서 설명할 것이다.

5.3 간단한 링크드 리스트 예제
: <리스트 15.12>는 링크드 리스트를 사용하는 기본적인 방법을 보여준다. 이 프로그램은 사용자 입력을 받아들이지 앟고 대부분의 기본적인 링크드 리스트 작업에 요구되는 코드를 보여주는 것 외에 특별한 동작을 수행하지 않으며 예제로만 사용되는 것이다.

이 프로그램은 다음과 같은 작업을 수행한다.

① 링크드 리스트를 위한 구조체와 포인터를 정의한다.

② 링크드 리스트에 첫 링크를 추가한다.

③ 링크드 리스트의 마지막에 링크를 추가한다.

④ 링크드 리스트의 중간에 링크를 추가한다.

⑤ 링크드 리스트의 내용을 화면에 출력한다.

<리스트 15.12> 링크드 리스트의 기초

 /* 링크드 리스트의 기본적인 사용 방법을

    보여주는 예제 */


 #include <stdlib.h>

 #include <stdio.h>

 #include <string.h>


 /* 링크드 리스트인 data 구조체 */

 struct data {

    char name[20];

    struct data *next;

 };


 /* 구조체에 대한 typedef형을 정의하고 포인터를 사용해서 지적한다. */


 typedef struct data PERSON;

 typedef PERSON *LINK;


 main()

 {

    /* head, new, current 요소 포인터 */

    LINK head = NULL;

    LINK new = NULL;

    LINK current = NULL;


    /* 첫 번째 요소를 추가한다. */

    /* 이 예제에서는 링크드 리스트가 항상 비어 있지만

       비어 있는 것으로 가정하지 않는다. */


    new = (LINK)malloc(sizeof(PERSON));

    new -> next = head;

    head = new;

    strcpy(new -> name, "Abigail");


    /* 링크드 리스트의 마지막에 요소를 추가한다. */

    /* 링크드 리스트에 최소한 하나의 요소가 있다고 가정한다. */


    current = head;

    while(current -> next != NULL)

    {

       current = current -> next;

    }


    new = (LINK)malloc(sizeof(PERSON));

    current -> next = new;

    new -> next = NULL;

    strcpy(new -> name, "Catherine");


    /* 링크드 리스트의 두 번째 위치에 새로운 요소 추가 */

    new = (LINK)malloc(sizeof(PERSON));

    new -> next = head -> next;

    head -> next = new;

    strcpy(new -> name, "Beatrice");


    /* 모든 데이터 항목을 차례대로 출력 */

    current = head;

    while(current != NULL)

    {

       printf("\n%s", current -> name);

       current = current -> next;

    }


    printf("\n");

    return(0);

 }

=> 아마도 최소한 코드의 일부분을 이해할 수 있을 것이다. 9번째 줄부터 12번째 줄까지는 링크드 리스트를 위한 데이터 구조를 선언한다. 16번째 줄과 17번째 줄은 데이터 구조체와 데이터 구조체에 대한 포인터를 위한 typedef문을 정의한다. 이런 typedef가 반드시 필요하지는 않지만 struct data 대신에 PERSON을 사용하고, struct data* 대신에 LINK를 사용하게 해주므로 코드를 단순화할 수 있을 것이다.

22번째 줄부터 24번째 줄까지는 헤드 포인터를 선언하고 링크드 리스트를 다룰 때 사용할 다른 한 쌍의 포인터를 선언한다. 모든 포인터는 NULL로 초기화된다.

30번째 줄부터 33번째 줄까지는 링크드 리스트의 시작 부분에 새로운 링크를 추가한다. 30번째 줄은 새로운 데이터 구조체를 할당한다. malloc()의 결과가 성공적이라고 가정한다는 것에 주목하기 바란다. 실제 프로그래밍에서는 이렇게 가정해서는 안된다.

31번째 줄은 이 새로운 구조체의 next 포인터가 헤드 포인터를 지적하도록 설정한다. 왜 단순히 이 포인터에 NULL을 설정하지 않을까? 링크드 리스트가 현재 비어 있기 때문이다. 이 코드는 링크드 리스트에 이미 다른 링크가 있더라도 그대로 사용할 수 있을 것이다. 새로운 첫 링크는 여러분이 원하는 대로 이전의 첫 번째 링크를 지적하게 될 것이다.

32번째 줄은 헤드 포인터가 새로운 링크를 지적하게 하고, 33번째 줄은 링크에 임의의 데이터를 저장한다.

링크드 리스트의 마지막에 새로운 링크를 추가하는 것은 약간 더 복잡하다. 예제에서는 링크드 리스트에 하나의 링크만 존재하지만 실제로 프로그램을 작성할 때에는 이런 사실을 알 수 없으므로 전체 링크드 리스트에서 마지막 링크를 찾을 때까지 반복해야 한다. next 포인터가 NULL을 지적하는 링크가 마지막 링크이다. 이런 과정은 38번째 줄부터 42번째 줄까지 수행된다. 일단 마지막 링크를 찾았다면 새로운 데이터 구조를 할당하고, 이전의 마지막 링크가 새로운 구조체를 지적하게 하고, 새로운 링크의 next 포인터를 NULL로 설정하면 된다. 이것은 44번째 줄부터 47번째 줄까지의 동작이다.

다음 작업은 링크드 리스트의 중간에 링크를 추가하는 일이다. 이 예제에서는 두 번째 위치에 링크를 추가하면 된다. 50번째 줄에서 새로운 데이터 구조체를 할당하고 나서 새로운 링크의 next 포인터가 두 번째로 사용되는 링크를 지적하도록 설정한다. 그리고 51번째 줄에서 두 번째 링크를 세 번째 링크로 만들며, 52번째 줄에서 첫 링크의 next 포인터가 새로운 링크를 지적하게 한다.

마지막으로, 프로그램은 링크드 리스트의 모든 내용을 출력한다. 헤드 포인터가 지적하는 링크에서부터 NULL 포인터로 표시되는 마지막 링크를 찾을 때까지 전체 링크드 리스트를 통해서 차례대로 진행하면 된다. 56번째 줄부터 61번째 줄까지 이런 작업을 수행하고 있다.

5.4 링크드 리스트 구현하기
: 이제 링크드 리스트에 링크를 추가하는 방법을 보았으므로 실제로 사용하는 방법을 살펴볼 필요가 있다. <리스트 15.13>은 5개의 항목을 가지는 링크드 리스트를 사용하는 다소 긴 프로그램이다. 입력된 문자들은 링크드 리스트를 사용해서 메모리에 저장된다. 이런 문자들은 단지 이름, 주소, 다른 어떤 데이터를 표현할 정도로 간단하다. 예제를 가능한 한 쉽게 만들기 위해 한 문자씩 저장하도록 하겠다.

이 링크드 리스트 프로그램을 복잡하게 만드는 원인은 문자를 입력하고 나서 링크를 정렬하기 때문이다. 물론 이 기능이 프로그램을 상당히 가치있게 만드는 특징이기도 하다. 각 링크들은 값에 다라 시작, 중간, 마지막에 추가되고 링크드 리스트 전체는 항상 정렬된다. 만약 간단히 링크를 마지막에 추가하는 프로그램을 작성했다면 전체적인 구조는 훨씬 더 간단할 것이다. 그러나 프로그램은 그리 유용하지 않을 것이다.

<리스트 15.13> 문자들의 링크드 리스트 구현하기  
 

 /*====================================================*

  * 프로그램 : list1513.c                               *

  * 도서명   : C 언어 21일 완성                       *

  * 목적     : 링크드 리스트 구현하기                 *

  *====================================================*/

 #include <stdio.h>

 #include <stdlib.h>


 #define NULL

 #define NULL 0

 #endif


 /* 링크드 리스트로 사용되는 구조체 */

 struct list

 {

    int ch;   /* char형을 저장할 int형 선언 */

    struct list *next_rec;

 };


 /* 구조체와 포인터에 대한 typedef형 */

 typedef struct list LIST;

 typedef LIST *LISTPTR;


 /* 함수 원형 */

 LISTPTR add_to_list(int, LISTPTR);

 void show_list(LISTPTR);

 void free_memory_list(LISTPTR);


 int main(void)

 {

    LISTPTR first = NULL;   /* 헤드 포인터 */

    int i = 0;

    int ch;

    char traash[256];   /* stdin 버퍼를 정리한 */


    while(i++ < 5)   /* 주어진 5항목을 근거로 링크드 리스트를 구성 */

    {

       ch = 0;

       printf("\nEnter character %d, ", i);


       do

       {

          printf("\nMust be a to z: ");

          ch = getc(stdin);   /* 버퍼에서 다음 문자를 구함 */

          gets(trash);   /* 버퍼에서 trash를 제거 */

       } while((ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z'));


       first = add_to_list(ch, first);

    }


    show_list(first);    /* 전체 링크드 리스트를 출력 */

    free_memory_list(first);    /* 모든 메모리를 해제 */

    return(0);

 }


 /*====================================================*

  * 함수   : add_to_list()

  * 목적   : 링크드 리스트에 새로운 링크를 추가

  * 입력값 : int ch = 저장할 문자

             LISTPTR first = 원래 헤드 포인터의 주소

  * 복귀값 : 헤드 포인터의 주소(first)

  *====================================================*/


 LISTPTR add_to_list(int ch, LISTPTR first)

 {

    LISTPTR new_rec = NULL;    /* 새로운 링크의 주소를 저장 */

    LISTPTR tmp_rec = NULL;    /* 임시 포인터를 저장 */

    LISTPTR prev_rec = NULL;


    /* 메모리 할당 */

    new_rec = (LISTPTR)malloc(sizeof(LIST));

    if(!new_rec)    /* 메모리 할당 불가 */

    {

       printf("\nunable to allocate memory!\n");

       exit(1);

    }


    /* 새로운 링크의 데이터 설정 */

    new_rec -> ch = ch;

    new_rec -> next_rec = NULL;


    if(first == NULL)   /* 링크드 리스트에 첫 링크 추가 */

    {

       first = new_rec;

       new_rec -> next_rec = NULL;   /* 불필요하지만 안전을 보장함 */

    }

    else   /* 첫 링크가 아니면 */

    {

       /* 첫 링크 앞인지 확인 */

       if(new_rec -> ch < first -> ch)

       {

          new_rec -> next_rec = first;

          first = new_rec;

       }

       else   /* 중간이나 긑에 추가됨 */

       {

          tmp_rec = first -> next_rec;

          prev_rec = first;


          /* 링크가 추가되는 곳을 확인 */


          if(tmp_rec == NULL)

          {

             /* 끝에 두 번째 링크를 추가 */

             prev_rec -> next_rec = new_rec;

          }

          else

          {

             /* 중간에 추가하는지 확인 */

             while(tmp_rec -> next_rec != NULL)

             {

                if(new_rec -> ch < tmp_rec -> ch)

                {

                   new_rec -> next_rec = tmp_rec;

                   if(new_rec -> next_rec != prev_rec -> next_rec)

                   {

                      printf("ERROR");

                      gets(stdin);

                      exit(0);

                   }

                   prev_rec -> next_rec = new_rec;

                   break;   /* 링크가 추가되고 while을 마침 */

                }

                else

                {

                   tmp_rec = tmp_rec -> next_rec;

                   prev_rec = prev_rec -> next_rec;

                }

             }


             /* 끝에 추가하는지 확인 */

             if(tmp_rec -> next_rec == NULL)

             {

                if(new_rec -> ch < tmp_rec -> ch)   /* 끝에서 두 번째 위치 */

                {

                   new_rec -> next_rec = tmp_rec;

                   prev_rec -> next_rec = new_rec;

                }

                else   /* 끝 위치 */

                {

                   tmp_rec -> next_rec = new_rec;

                   new_rec -> next_rec = NULL;   /* 중복 작업 */

                }

             }

          }

       }

    }

    return(first);

 }


 /*====================================================*

  * 함수 : show_list

  * 목적 : 링크드 리스트의 정보 출력

  *====================================================*


 void show_list(LISTPTR first)

 {

    LISTPTR cur_ptr;

    int counter = 1;


    printf("\n\nRec addr Position Data Next Rec addr\n");

    printf("======== ========  ====  ==============\n");


    cur_ptr = first;

    while(cur_ptr != NULL)

    {

       printf("  %x  ", cur_ptr);

       printf("     %2i     %c", counter++, cur_ptr -> ch);

       printf("      %x   \n", cur_ptr -> next_rec);

       cur_ptr = cur_ptr -> next_rec;

    }

 }


 /*====================================================*

  * 함수 : free_memory_list

  * 목적 : 링크드 리스트를 위해 정리된 모든 메모리 해제

  *====================================================*


 void free_memory_list(LISTPTR first)

 {

    LISTPTR cur_ptr, next_rec;

    cur_ptr = first;    /* 처음에서 시작 */


    while(cur_ptr != NULL)    /* 링크드 리스트의 끝까지 진행 */

    {

       next_rec = cur_ptr -> next_rec;    /* 다음 링크의 주소 구하기 */

       free(cur_ptr);     /* 현재 링크 해제 */

       cur_ptr = next_rec;   /* 현재 링크 정리 */

    }

 }

-> 입력 / 출력

  Enter character 1,
  Must be a to z: q

  Enter character 2,
  Must be a to z: b

  Enter character 3,
  Must be a to z: z

  Enter character 4,
  Must be a to z: c

  Enter character 5,
  Must be a to z: a

  Rec addr    Position    Data    Next Rec addr
  ========    ========    ====    ==============
  C3A        1           a       C22
  C22        2           b       C32
  C32        3           c       C1A
  C1A        4           q       C2A
  C2A        5           z       0

=> 분석
: 이 프로그램은 링크드 리스트에 링크를 추가하는 것을 보여준다. 예제를 이해하기는 어려울 것이다. 그러나 자세히 분석해 보면 앞에서 설명하는 링크를 추가하는 세 가지 방법을 모두 다루고 있음을 알 수 있다. 이 프로그램은 링크드 리스트의 시작, 중간, 마지막에 새로운 링크를 추가하는데 사용할 수 있다. 또한, 시작 부분에 추가되는 첫 링크와 중간에 추가되는 두 번째 링크를 새로 추가하는 경우에 대해서 중점적으로 다루고 있다. <리스트 15.13>의 시작 부분에 있는 내용들은 익숙하므로 쉽게 이해할 수 있을 것이다. 9번째 줄부터 14번째 줄까지는 NULL값이 이미 정의되어 있는지 확인해본다. 만약 그렇지 않다면 10번째 줄은 NULL값을 0으로 정의한다. 14번째 줄부터 22번째 줄까지는 링크드 리스트를 위한 구조체를 정의하고, 구조체와 포인터를 쉽게 사용할 수 있도록 하기 위해서 typedef를 사용하고 있다. main()함수는 이해하기 쉽다. first라는 헤드 포인터가 31번째 줄에서 선언된다. 이 포인터가 NULL로 초기화된다는 것에 주목하지 바란다. 여러분은 결코 포인터를 초기화하지 않은 상태로 내 버려두어서는 안된다는 것을 기억하기 바란다. 36번째 줄부터 49번째 줄까지는 사용자로부터 5문자를 읽어들이는 while 순환문이 있다. 다섯 번을 반복하는 이 외부 while 순환문 내에서 do...while은 입력된 각 문자가 영문자인지 확인하는 역할을 한다. 내부 순환문 대신에 isalpha()함수를 사용할 수도 있을 것이다. 데이터를 읽어들이고 나면 add_to_list()가 호출된다. 링크드 리스트의 시작에 대한 포인터와 링크드 리스트에 추가되는 데이터가 함수로 전달된다. main() 함수는 링크드 리스트의 데이터를 출력하기 위해 show_list()를 호출하고, 링크드 리스트에서 링크를 저장하기 위해 할당된 모든 메모리를 해제하는 free_memory_list()를 호출하면 끝난다. 이런 함수들은 비슷한 방법으로 동작한다. 각각은 헤드 포인터인 first를 사용하여 링크드 리스트의 링크드 리스트의 처음에서 시작한다. while 순환문은 특정 링크에 서 next_ptr 값을 사용하여 다음 링크로 진행한다. next_ptr이 NULL과 같을 때 링크드 리스트의 마지막에 도달한 것이므로 함수는 종료된다. 이 리스트에서 가장 중요하고 가장 복잡한 함수는 56번째 줄부터 149번째 줄까지의 add_to_list()이다. 66번째 줄부터 68번째 줄까지는 세 개의 서로 다른 링크를 지적하는 데 사용할 세 포인터를 선언한다. new_rec 포인터는 추가되는 새로운 링크를 지적할 것이다. tmp_rec 포인터는 링크드 리스트에서 기준이 되는 현재 링크를 지적할 것이다. 링크드 리스트에 하나 이상의 링크가 있다면 prev_rec 포인터는 기준이 되는 현재 링크 앞의 링크를 지적하는 데 사용된다.

71번째 줄은 추가되는 새로운 링크를 위한 메모리를 할당한다. new_rec 포인터는 malloc()에 의해 반환되는 값으로 설정된다. 메모리가 할당될 수 없다면 74번째 줄과 75번째 줄은 에러 메시지를 출력하고 프로그램을 마친다. 만약 메모리가 성공적으로 할당되었다면 프로그램은 계속된다.

79번째 줄은 구조체에 이 함수로 전달된 데이터를 저장한다. 이렇게 하기 위해서 단순히 함수로 전달된 문자 ch를 새로운 링크의 문자 필드인 new_rec -> ch로 설정하면 된다. 더 복잡한 프로그램에서는 여러 필드를 설정해야 할 것이다. 80번째 줄은 새로운 링크가 임의의 위치를 지적하지 않도록 next_rec을 NULL로 설정한다.

82번째 줄은 링크드 리스트에 어던 링크가 있는지 확인하면서 '링크 추가' 과정을 시작한다. 만약 헤드 포인터 first가 NULL로 지적되면, 즉 추가되는 링크가 링크드 리스트의 첫 번째 링크이면 헤드 포인터는 단순히 새로운 포인터로 설정되고 작업은 끝난다. 만약 새로운 링크가 처음이 아니면 함수는 87번째 줄의 else 내에서 계속 진행한다.

90번째 줄은 새로운 링크가 링크드 리스트의 시작 부분으로 이동되어야 하는지 확인해본다. 앞에서 배웠듯이 이것은 링크를 추가하는 세 가지 경우의 하나이다. 만약 링크를 처음에 위치시킨다면 92번째 줄은 새로운 링크의 next_rec 포인터가 이전의 '처음' 링크를 지적하게 한다. 그리고 나서 93번째 줄은 포인터 first가 새로운 링크를 지적하게 한다. 이렇게 해서 링크드 리스트의 시작 부분에 새로운 링크가 추가된다. 만약 새로운 링크가 비어 있는 링크드 리스트에 추가되는 첫 번째 링크가 아니고 기존 링크드 리스트의 첫 번째 위치에 추가되는 것도 아니라면 일크드 리스트의 중간이나 마지막에 위치하는 것임을 알 수 있다. 97번째 줄과 98번째 줄은 앞에서 선언한 tmp_rec와 prev_rec 포인터를 설정한다. 포인터 tmp_rec는 링크드 리스트에서 두 번째 링크의 주소로 설정되고, prev_rec는 링크드 리스트에서 첫 번째 링크로 설정된다. 링크드 리스트에 단지 하나의 링크만 있다면 tmp_rec가 NULL과 같아진다는 것에 주의하기 바란다. tmp_rec가 NULL로 설정되는 첫 링크의 next_ptr로 설정되기 때문이다.

102번째 줄은 이런 경우를 확인하고 있다. 만약 tmp_rec가 NULL이면 새로운 링크가 링크드 리스트에 추가되는 두 번째 링크라는 것을 알 수 있다. 새로운 링크가 첫 링크 앞에 위치되지 않는다는 것을 이미 알고 있으므로 마지막 링크가 되는 것이다. 이렇게 하기 위해서 단순히 prev_rec -> next_ptr을 새로운 링크로 설정하면 되고 작업은 끝난다. 만약 tmp_rec 포인터가 NULL이 아니라면 이미 링크드 리스트에 두 개 이상의 링크가 있다는 것을 알 수 있다. 110번째 줄부터 129번째 줄까지에 있는 while 순환문은 새로운 링크가 위치되어야 하는 곳을 결정하기 위해 링크의 나머지 부분을 차례대로 확인한다. 112번째 줄은 새로운 링크의 데이터 값이 현재 지적되는 링크보다 작은지 확인해본다. 만약 데이터 값이 작다면 여기에 링크를 추가하면 된다. 그러나 새로운 링크의 데이터가 현재 링크의 데이터보다 크다면 링크드 리스트의 다음 링크를 살펴볼 필요가 있다. 126번째 줄과 127번째 줄은 포인터 tmp_rec와 next_rec를 다음 링크로 설정한다. 만약 문자가 현재 링크의 문자보다 '작다면' 링크드 리스트의 중간에 링크를 추가하기 위해 앞에서 설명했던 방법을 따르면 된다. 이것은 114번째 줄부터 122번째 줄까지 나타나 있다. 114번째 줄에서 새로운 링크의 next 포인터를 현재 링크의 주소 tmp_rec와 같게 설정한다. 121번째 줄은 이전 링크의 next 포인터가 새로운 링크를 지적하도록 설정한다. 그리고 나서 작업이 끝난다. 코드는 while 순환문을 마치기 위해 break문을 사용한다.

앞에서 설명한 내용은 링크드 리스트의 중간에 추가되는 새로운 링크를 다루는 것이다. 만약 링크드 리스트의 마지막에 도달했다면 110번째 줄부터 129번째 줄까지의 while 순환문 은 링크를 추가하지 않고 끝날 것이다. 132번째 줄부터 144번째 줄까지는 링크를 마지막에 추가하는 작업을 수행한다.

만약 링크드 리스트의 마지막 링크에 도달하면 tmp_rec -> next_rec는 NULL과 같아질 것이다. 132번째 줄은 이것을 확인한다. 134번째 줄은 링크가 마지막 링크의 앞이나 뒤에 위치되어야 하는지 확인한다. 만약 새로운 링크가 마지막 링크 다음으로 가야 한다면 링크의 next_rec를 132번째 줄에서 새로운 링크로 설정하고 새로운 링크의 next 포인터를 142번째 줄에서 NULL로 설정한다.

▶ <리스트 15.13> 수정하기
: 링크드 리스트는 상당히 어려운 주제이다. 그러나 <리스트 15.13>에서 볼 수 있듯이 일정한 순서대로 데이터를 저장하는 가장 좋은 방법이기도 하다. 링크드 리스트에서 어디든지 새로운 데이터 항목을 추가하는 것이 쉬우므로 링크드 리스트로 데이터 항목의 목록을 정렬해서 사용하는 것이 배열을 사용하는 것보다 훨씬 더 간단하다. 앞의 예제는 이름, 전화번호, 다른 어떤 데이터를 정렬하도록 쉽게 수정할 수 있다. 또한, 앞의 프로그램에서는 오름차순(A에서 Z)으로 정렬했지만 내림차순(Z에서 A)으로 정렬하도록 수정하는 것도 어렵지 않다.

▶ 링크드 리스트에서 삭제하기
: 링크드 리스트에 정보를 추가하는 기능은 기본적이지만 가끔 정보를 제거하기 원할 때가 있을 것이다. 링크나 요소를 삭제하는 것은 추가하는 것과 비슷하다. 여러분은 링크드 리스트의 시작, 중간, 마지막에서 링크를 삭제할 수 있다. 각각의 경우에 적절한 포인터를 조절하면 된다. 또한, 삭제된 링크가 차지하고 있던 메모리를 해제시킬 필요가 있다.


'C 언어' 카테고리의 다른 글

14장 화면, 프린터, 키보드 사용하기  (0) 2019.06.02
15장 포인터 : 고급 기능들  (0) 2019.06.02
17장 디스크 파일의 사용  (0) 2019.06.02
API 윈도우 창 띄우기  (0) 2019.05.25
CreateWindow() - 10개의 인수  (0) 2019.05.25

* 디스크 파일의 사용
 대부분의 프로그램에서는 데이터나 도는 사용 환경을 저장하는 등 여러 가지 목적으로 디스크 파이를 사용한다. 오늘은 다음과 같은 내용을 다룰 것이다.

·디스크 파일에 관련된 스트림
·C에서 사용되는 두 가지 형태의 디스크 파일
·파일에 데이터를 저장하는 방법
·파일에서 데이터를 읽어들이는 방법
·파일을 닫는 방법
·디스크 파일의 관리
·임시 파일의 사용

1. 스트림과 디스크 파일
: C는 디스크 파일을 포함하여 모든 입력과 출력을 스트림으로 수행한다. 앞에서는 키보드,화면, 그리고 DOS 시스템에서 프린터와 같이 특정 장치와 연결되어 있는 C의 미리 정의된 스트림을 사용하는 방법을 다루었다. 디스크 파일 스트림은 기본적으로 동일한 방법으로 사용된다. 이것은 스트림을 통한 입출력의 한 가지 장점이다. 한 가지 스트림을 사용하는 방법은 다른 스트림에서 변화가 없거나 약간만 다르게 해서 사용할 수 있다. 디스크 파일 스트림에서 중요한 차이점이 있다면, 프로그램이 특정 디스크 파일과 관련된 스트림을 반드시 생성해야 한다는 것이다.

2. 디스크 파일의 종류
: 14번째 강의에서 C의 스트림에 텍스트(text)와 이진(binary)의 두 가지 종류가 있다는 것을 설명했다. 이런 두 가지 형태의 스트림 중에서 어떤 것도 파일과 관련될 수 있는데, 파일을 적절한 모드로 사용하기 위해서는 각각의 특성을 이해할 필요가 있다.

텍스트 스트림(text stream)은 텍스트 모드 파일과 관련되어 있다. 텍스트 모드 파일은 일련의 문장들로 구성된다. 각각의 문장은 문자로 구성되고 문장의 마지막을 나타내는 하나 이상의 문자를 포함한다. 문장의 최대 길이는 255자이다. '문장'은 C의 문자열과 다르다는 것을 기억할 필요가 있다. 문장에는 널 문자(\0)가 포함되지 않는다. 텍스트 모드의 스트림을 사용할 때 C의 문장 진행 문자(\n)와 운영체제가 디스크 파일에서 문장의 마지막을 표시하기 위해서 사용하는 문자 사이에는 변환이 수행된다. DOS 환경에서는 개행 문자(CR : carriage return)와 다음 줄 문자(LF : line feed)로 변환된다. 데이터가 텍스트 모드의 파일에 저장될 때 각각의 \n은 CR-LF로 변환된다. 데이터가 디스크 파일에서 읽어들여질 때 DR-LF는 다시 \n으로 변환된다. UNIX 환경에서는 어떤 변환도 수행되지 않으므로 문장 진행 문자가 변경되지 않고 그대로 남는다.

이진 스트림(binary stream)은 이진 모드 파일과 관련되어 있다. 모든 데이터는 있는 그대로 저장되거나 읽어들여지고, 문장 내에서 구분이나 문장의 마지막을 표시하는 문자는 사용되지 않는다. NULL과 문장의 마지막을 표시하는 문자는 특별한 의미를 가지지 않으며 다른 어떤 데이터와 똑같이 취급된다.

어떤 파일 입출력 함수는 한 가지 파일 모드에서만 사용될 수 있고, 다른 어떤 함수는 두 가지 모드에서 사용될 수 있다. 이 장에서는 어떤 함수를 어떤 모드에서 사용해야 하는지 알려줄 것이다.

3. 파일 이름
: 모든 디스크 파일은 이름을 가지고 있고 디스크 파일을 다룰 때에는 반드시 파일 이름을 사용해야 한다. 파일 이름은 다른 텍스트 데이터와 마찬가지로 문자열에 저장된다. 파일 이름을 위해 적용되거나 제한되는 규칙은 운영체제마다 다른다. DOS와 윈도우 3.x에서 파일 이름은 1 ~ 8자까지의 이름, 선택적으로 사용되는 마침표(.), 3자까지의 확장자로 구성된다. 반면에, 윈도우 95(98)와 NT, 대부분의 UNIX 시스템에서는 256자까지의 파일 이름을 사용할 수 있다.

운영체제마다 다른 것으로는 파일명에 허용되는 문자도 포함된다. 예를 들어, 윈도우 95에서는 다음 문자들이 허용되지 않는다.

   / \ : * ? " < > |

여러분은 사용중인 운영체제에 따라 파일명 규칙을 지키고 주의해야 한다. 또한, C 프로그램에서의 파일명은 경로 정보를 가질 수 있다. 경로(path)는 파일이 위치된 드라이브와 디렉토리(또는 폴더)를 가리킨다. 만약 경로 없이 파일명을 지정하면 프로그램은 파일이 운영체제가 현재 기본적으로 사용중인 디렉토리에 있다고 가정할 것이다. 파일명의 일부분으로 경로 정보를 항상 지정하는 것은 좋은 프로그래밍 습관이다. PC에서 백슬래시 문자(\)는 경로의 디렉토리 이름을 구분하는 데 사용된다. 예를 들어, DOS와 윈도우에서 다음은

   c:\data\list.txt

드라이브 C의 디렉토리 \DATA에 저장되어 있는 LIST.TXT라는 이름의 파일을 뜻한다. 백슬래시 문자가 문자열에서 사용될 때에는 C에 대해서 특수한 의미를 가진다는 것을 알고 있을 것이다. 그래서 백슬래시 문자 자체를 표현하려면 하나의 백슬래시를 추가해야 한다. 앞에 나타난 파일 이름은 C 프로그램에서 다음과 같이 표현될 것이다.

   char *filename = "c:\\daa\\list.txt;

키보드에서 파일 이름을 입력한다면 하나의 백슬래시만을 사용할 수 있다. 모든 시스템에서 백슬래시가 디렉토리 구분자로 사용되는 것은 아니다. 예를 들어, UNIX에서 는 일반적인 슬래시(/)를 사용한다.

4. 파일 열기
: 디스크 파일에 관련된 스트림을 생성하는 과정을 파일 열기(opening)라고 한다. 파일을 열게 되면 파일에서 프로그램으로 데이터를 읽어들이는 읽기(reading) 동작이나 프로그램에서 파일로 데이터를 저장하는 쓰기(writing) 동작, 또는 두 가지 모두가 가능하게 된다. 파일의 사용을 마치게 되면 닫아야 한다. 파일을 닫는 것에 대해서는 이 장에서 나중에 설명할 것이다. 파일을 열기 위해서는 라이브러리 함수 fopen()을 사용한다. fopen()의 원형은 STDIO.H에 나타나 있으며 다음과 같다.

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

이 원형은 fopen()이 STDIO.H에 선언된 구조체인 FILE형에 대한 포인터를 돌려준다는 사실을 알려준다. FILE 구조체의 멤버는 프로그램에서 여러 가지 파일 관리를 수행하기 위해 사용되지만, 여기에서는 자세한 내용을 알 필요가 없다. 그러나 열기 원하는 각각의 파일에 대해서는 FILE형에 대한 포인터를 선언해야 한다. fopen()을 호출하면 함수는 FILE 구조체형 변수를 생성하고, 생성된 구조체에 대한 포인터를 돌려준다. 파일을 사용하는 이후의 모든 동작에서는 이 포인터가 사용된다. 만약 fopen() 함수에서 문제가 발생하면 NULL을 돌려준다. 예를 들어, 이런 실패는 하드웨어 에러나 또는 초기화되지 않은 디스크에서 파일을 열려고 할 때 발생할 수 있다.

인수 filename은 열리는 파일의 이름이다. 앞에서도 언급했듯이 filename에는 경로 정보가 포함될 수 있고, 포함되는 것이 좋다. 인수 filename은 큰 따옴표 내에 포함된 일반적인 문자열이나 도는 문자열 변수에 대한 포인터가 될 수 있다. 인수 mode는 열릴 파일의 사용 모드를 지정하는 것이다. mode는 이진(binary)모드나 텍스트(text) 모드, 그리고 읽기(reading)나 쓰기(writing), 또는 두 가지 모두 중에서 어떤 상태로 열려야 하는지 제어한다. mode에 사용할 수 있는 값은 <표 16.1>에 나타나 있다.

<표 16.1> fopen() 함수의 mode의 값

모드

의미

r

 읽기 상태로 파일을 연다. 지정된 이름의 파일이 존재하지 않으면 fopen()은

 NULL을 돌려준다.

w

 쓰기 상태로 파일을 연다. 지정된 이름의 파일이 존재하지 않으면 생성된다.

 지정된 이름의 파일이 존재하면 경고 없이 삭제되고 새롭고 비어 있는 파일

 이 생성된다.

a

 데이터 추가 상태로 파일을 연다. 지정된 이름의 파일이 존재하지 않으면

 생성된다. 파일이 이미 존재한다면 새로운 데이터는 파일의 마지막에

 추가된다.

r++

 읽기와 쓰기 상태로 파일을 연다. 지정된 이름의 파일이 존재하지 않으면

 생성된다. 파일이 이미 존재한다면 새로운 데이터는 이전의 데이터를

 덮어쓰며 파일의 시작 부분에 위치된다.

w+

 읽기와 쓰기 상태로 파일을 연다. 지정된 이름의 파일이 존재하지 않으면

 생성된다. 파일이 이미 존재한다면 덮어써진다.

a+

 읽기와 데이터 추가 상태로 파일을 연다. 지정된 이름의 파일이 존재하지

 않으면 생성된다. 파일이 이미 존재한다면 새로운 데이터는 파일의 마지막에

 추가된다.

기본적으로 설정되어 있는 파일의 모드는 텍스트(text)이다. 파일을 이진 모드로 열기 위해서는 인수 mode에 b를 추가해야 한다. 인수 mode에 a를 포함시키면 텍스트 모드의 파일을 데이터 추가가 가능한 상태로 열어주고, ab를 포함시키면 이진 모드의 파일을 데이터 추가가 가능한 상태로 열어줄 것이다. 만약 에러가 발생하면 fopen()은 NULL을 돌려준다는 사실을 기억하자. 다음과 같은 경우에는 NULL의 복귀값을 얻게 될 것이다.

·유효하지 않은 파일 이름을 사용한 경우

·준비되지 않은 디스크에서 파일을 열려고 할 때. 예를 들어, 드라이브가 닫히지 않았거나 디스크가 초기화되어 있지 않을 때

·존재하지 않는 디렉토리나 디스크 드라이브의 파일을 열려고 할 때

·존재하지 않는 파일을 'r' 모드로 열려고 할 때

fopen()을 사용할 때에는 에러가 발생했는지 확인할 필요가 있다. 구체적으로 어떤 에러가 발생했는지 정확하게 알 수 있는 방법은 없지만, 적절한 메시지를 출력하고 다시 파일을 열도록 해주거나 프로그램을 종료할 수 있다. 대부분의 C 컴파일러는 에러의 상태에 대한 정보를 알 수 있게 해주는 ANSI 비 호환 확장 기능을 포함하고 있다.

<리스트 16.1> 다양한 모드로 디스크 파일을 여는 fopen()의 사용

 /* fopen() 함수의 사용 예 */


 #include <stdio.h>


 main()

 {

    FILE *fp;

    char ch, filename[40], mode[4];


    while(1)

    {


       /* 파일명과 모드 입력 */


       printf("\nEnter a filename: ");

       gets(filename);

       printf("\nEnter a mode (max 3 characters): ");

       gets(mode);


       /* 파일 열기를 시도함 */


       if((fp = fopen(filename, mode)) != NULL)

       {

          printf("\nSuccessful opening %s in mode %s.\n,

                 filename, mode);

          fclose(fp);

          puts("Enter x to exit, any other to continue.");

          if((ch = getc(stdin)) == 'x')

             break;

          else

             continue;

       }

       else

       {

          fprintf(stderr, "\nError opening file %s in mode %s.\n",

                  filename, mode);

          puts("Enter x to exit, any other to try again.");

          if((ch = getc(stdin) == 'x')

             break;

          else

             continue;

       }

    }

 }

-> 입력 / 출력

  Enter a filename : junk.txt

  Enter a mode (max 3 characters): w

  Successful opening junk.txt in mode w.
  Enter x to exit, any other to continue.
  j

  Enter a filename: morejunk.txt

  Enter a mode (max 3 characters): r

  Error opening morejunk.txt in mode r.
  Enter x to exit, any other to try again.
  x

5. 파일에 데이터 기록하고 읽어들이기
: 디스크 파일을 사용하는 프로그램에서는 파일에 데이터를 기록하거나 파일에서 데이터를 읽어들이고 또는 두 가지를 모두 수행할 수 있다. 디스크 파일에 데이터를 저장하기 위해서 는 세가지 방법을 사용할 수 있다.

·형식화된 데이터를 파일에 저장하기 위해서 형식화된 출력을 사용할 수 있다. 형식화된 출력은 텍스트 모드의 파일에서만 사용해야 한다. 형식화된 출력의 기본적인 용도는 스프레드시트나 데이터베이스 등의 다른 프로그램에서 사용되는 텍스트와 숫자 데이터를 가지는 파일을 생성하는 것이다. 또한, 드물기는 하지만 C 프로그램에서 사용할 파일을 생성하기 위해서 형식화된 출력을 사용한다.

·한 문자나 문장을 파일에 저장하기 위해서 문자 출력을 수행할 수 있다. 기술적으로 이진 모드의 파일에 문자 출력을 수행하는 것은 불가능하지만 특수한 방법으로는 가능할 수도 있다. 문자 출력은 텍스트 파일에만 제한하여 사용해야 한다. 문자 출력은 문서 작성기와 같은 프로그램뿐 아니라 C 에서도 사용할 수 있는 형식으로, 숫자가 아닌 텍스트 데이터를 저장하기 위해서 주로 사용된다.

·메모리의 일부분에 저장된 내용을 디스크 파일에 저장하기 위해서 직접 출력을 사용할 수 있다. 이 방법은 이진 파일에서만 사용된다. 직접 출력은 나중에 C 프로그램에서 사용하기 위한 데이터를 저장하는 가장 좋은 방법이다. 파일에서 데이터를 읽어들이는 경우에도, 앞에서 설명한 것과 마찬가지로 형식화된 입력, 문자 입력, 또는 직접 입력의 세 가지 방법을 사용할 수 있다. 파일을 읽어들이는 경우에 사용하는 방법은 대개 파일의 특성에 따라 다르다. 일반적으로, 여러분은 파일을 저장했을 때와 같은 방법으로 데이터를 읽어들이게 되지만 반드시 지켜야 하는 규칙은 아니다. 그러나 파일을 저장했을 때와 다른 모드로 파일을 읽어들이려면 C와 파일 형식에 대한 충분한 지식이 필요하다.

앞에서 파일 입력과 출력의 세 가지 형태를 잘 이해했다면 각각의 형태가 어떤 경우에 가장 적합한지 알 수 있을 것이다. 그러나 이런 구분은 결코 사용을 제한하기 위한 것이 아니다. C 언어의 한 가지 장점은 융통성이므로, 숙련된 프로그래머는 파일 출력의 형태를 대부분 필요성에 맞추러 사용할 수 있다. 초보적인 프로그래머는 경우에는 다음에서 설명할 내용을 참고로 해서 프로그래밍에 적용할 수 있을 것이다.

5.1 형식화된 파일 입력과 출력
: 형식화된 파일 입/출력은 특정 방법으로 형식화된 텍스트와 숫자 데이터를 다룬다. 이것을 14번째 강의에서 설명한 printf()와 scanf() 함수를 통해서 키보드 입력과 화면 출력을 형식화하는 것에 비유할 수 있다. 우선, 형식화된 출력에 대해서 설명한 후에 입력을 다룰 것이다.

▶ 형식화된 파일 출력
: 형식화된 파일 출력은 라이브러리 함수 fprintf()를 통해서 수행된다. fprintf()의 원형은 헤더 파일 STDIO.H에 정의되어 있으며, 다음과 같다.

  int fprintf(FILE *fp, char *fmt, ...);

첫 번째 인수는 FILE형에 대한 포인터이다. 데이터를 특정 디스크 파일에 기록하기 위해서는 fopen()을 사용하여 파일을 열었을 때 구해지는 포인터를 전달해야 한다. 두 번째 인수는 형식화 문자열이다. 형식화 문자열에 대한 내용은 14번째 강의에서 printf()를 설명할 때 다루었다. fprintf()에서 사용되는 형식화 문자열은 printf()에서 사용되는 것과 똑같은 규칙을 따른다. 상세한 내용은 14번깨 강의를 참조하기 바람.....^^;; 마지막 인수는 말줄임표(...)이다. 이것은 무슨 뜻일까? 함수 원형에서 사용되는 말줄임표(...)는 변칙적인 개수의 인수를 뜻한다. 즉, fprintf()는 파일 포인터와 형식화 문자열을 인수로 가지며, 추가로 필요한 만큼 많은 인수를 받아들일 수 있다. 이것은 printf()와 비슷하다. 추가로 사용되는 인수는 지정된 스트림으로 출력되는 변수의 이름이다. fprintf()는 인수 목록에서 지정된 스트림으로 출력 내용을 전달한다는 것을 제외하면 printf() 와 비슷하게 동작한다는 것을 기억하기 바란다. 사실, fprintf()에서 stdout을 스트림 인수로 지정한다면 fprintf()는 printf()와 동일하다. <리스트 16.2>에 있는 프로그램은 fprintf()를 사용하고 있다.

<리스트 16.2> fprintf()가 파일과 stdout으로 동일한 내용의 형식화된 출력을 수행한다는 것을 보여주는 예

 /* fprintf() 함수의 사용 예 */


 #include <stdio.h>

 #include <stdlib.h>

 void clear_kb(void);


 main()

 {

    FILE *fp;

    float data[5];

    int count;

    char filename[20];


    puts("Enter 5 floating point numerical values.");


    for(count = 0; count < 5; count++)

       scanf("%f", &data[count]);


    /* 파일명을 구하고 파일을 연다. */

    /* 우선 stdin에서 나머지 문자를 지운다. */


    clear_kb();


    puts("Enter a name for the file.");

    gets(filename);


    if((fp = fopen(filename, "w")) == NULL)

    {

       fprintf(stderr, "Error opening file %s.", filename);

       exit(1);

    }


    /* 숫자 데이터를 파일과 stdout으로 기록한다. */


    for(count = 0; count < 5; count++)

    {

       fprintf(fp, "\ndata[%d] = %f", count, data[count]);

       fprintf(stdout, "\ndata[%d] = %f", count, data[count]);

    }

    fclose(fp);

    printf("\n");

    return(0);

 }


 void clear_kb(void)

 /* stdin에서 나머지 문자를 지운다. */

 {

    char junk[80];

    gets(junk);

 }


▶ 형식화된 파일 입력
: 형식화된 파일 입력에서는 입력 동작이 stdin 대신에 지정된 스트림을 통해서 수행된다는 것을 제외하면 scanf()와 비슷하게 동작하는 라이브러리 함수 fscanf()를 사용한다. scanf()는 14번째 강의에서 설명했다. fcsanf()의 원형은 다음과 같다.

   int fscanf(FILE *fp, const char *fmt, ...);

인수 fp는 fopen()이 돌려주는 FILE형에 대한 포인터이고, fmt는 fscanf()가 입력을 받아들이 는 방법을 지정하는 형식화 문자열에 대한 포인터이다. 형식화 문자열의 구성 요소는 scanf()에서 사용되는 것과 동일하다. 마지막으로, 말줄임표(...)는 fscanf()가 입력된 값을 할당하는 변수의 주소인 하나 이상의 추가적인 인수를 뜻한다. fscanf()를 사용하기 전에 14번째 강의에서 설명한 내용을 한 번 더 읽어볼 필요가 있을 것이다. 함수 fscanf()는 stdin 대신에 지정된 스트림에서 문자를 읽어들인다는 것을 제외하면 scanf()와 똑같은 방법으로 사용된다. fscanf()를 사용해보기 위해서는 함수가 읽어들일 수 있도록 형식화된 약간의 숫자나 문자열 이 저장된 텍스트 파일이 필요하다. 에디터를  사용하여 공백(빈칸이나 문장 진행 문자)으로 구분되는 5개의 부동 소수형 숫자를 가지는 INPUT.TXT라는 이름의 파일을 생성하자. 예를 들어, 다음과 같은 파일을 생성할 수 있다.

   123.45      87.001
   100.02
   0.00456     1.0005

이제 <리스트 16.3>에 있는 프로그램을 컴파일하고 실행하자.]

<리스트 16.3> 디스크파일에서 형식화된 데이터를 읽어들이기 위해 fscanf()를 사용하는 예제

 /* fscanf()로 형식화된 파일 데이터 읽어들이기 */

 #include <stdlib.h>

 #include <stdio.h>


 main()

 {

    float f1, f2, f3, f4, f5;

    FILE *fp;


    if((fp = fopen("INPUT.TXT", "r")) == NULL)

    {

       fprintf(stderr, "Error opening file.");

       exit(1);

    }


    fscanf(fp, "%f %f %f %f %f", &f1, &f2, &f3, &f4, &f5);

    printf("The values are %f, %f, %f, %f, and %f.", f1, f2, f3, f4, f5);


    fclose(fp);

    return(0);

 }

5.2 문자 입력과 출력
: 디스크 파일에서 수행되는 문자 입출력(character I/O)은 한 문자뿐 아니라 문자들로 구성되는 문장을 대상으로 한다. 문장은 문장 진행(newline) 문자로 종료되는 일련의 문자들이라는 것을 기억하자. 문자 입출력은 텍스트 모드파일에서 수행해야 한다. 문자 입출력 함수에 대한 설명과 예제 프로그램을 살펴보도록 하자.
 
▶ 문자 입력
: 한 문자를 읽어들이기 위한 getc(), fgetc()와 문장을 읽어들이기 위한 fgets()의 세 가지 문자 입력 함수가 있다.

▶ getc()와 fgetc() 함수
: 함수 getc()와 fgetc()는 동일하므로 원하는 함수를 사용하면 된다. 두 함수는 지정된 스트림에서 한 문자를 읽어들인다. getc()의 원형은 STDIO.H에 정의되어 있다.

   int getc(FILE *fp);

인수 fp는 파일이 열릴 때 fopen()이 돌려주는 포인터이다. 함수는 입력된 문자를 돌려주거나 또는 에러가 발생하면 EOF를 돌려준다.

여러분은 키보드에서 문자를 입력하기 위해 앞의 프로그램에서 getc()를 사용했다. 이 함수는 C의 스트림의 융툥성을 확인할 수 있는 또다른 예이다. 키보드나 파일 입력을 위해 이 함수를 사용할 수 있기 때문이다. 만약 getc()와 fgetc()가 한 문자를 돌려준다면 원형에 왜 int형의 복귀값을 가지는 것일까? 파일을 읽을 때 파일의 마지막을 표시하는 문자를 읽을 필요가 있는데, 시스템에 따라서 이 문자가 char형이 아닌 int형이 될 수 있으므로 int형의 복귀값을 가지는 것이다. <리스트 16.10>에서 getc()를 사용하는 예를 볼 것이다.

▶ fgets() 함수
: 파일에서 문장을 읽어들이기 위해서는 fgets() 라이브러리 함수를 사용하자. 원형은 다음과 같다.

   char *fgets(char *str, int n, FILE *fp);

인수 str은 입력 내용이 저장되는 버퍼에 대한 포인터이고, n은 입력되는 문자의 최대 개수이며, fp는 파일이 열릴 때 fopen()이 돌려주는 FILE형에 대한 포인터이다.

fgets() 함수를 호출하면 fp에서 문자를 읽어들이고 str이 지적하는 메모리 영역에 읽어들인 문자를 저장한다. 함수는 문장 진행 문자가 나타나거나 또는 n-1자를 읽어들일 때까지 계속해서 문자를 읽어들인다. n의 값을 str에 할당된 메모리의 바이트 수와 같은 값으로 설정하면 입력 내용이 할당된 영역을 벗어나서 메모리를 겹쳐쓰지 않도록 방지할 수 있다. n-1은 fgets()가 문자열의 마지막에 추가하는 널 종료 문자(\0)을 위한 공간은 제외한 값이다. 입력이 성공적으로 끝나면 fgets()는 str을 돌려준다. 또한, 다음과 같이 두 가지 경우에 NULL값을 돌려주고 에러가 발생한다.

·str에 어떤 문자를 할당하기 전에 읽기 에러가 발생하거나 EOF 문자가 나타나면 함수는 NULL을 돌려주고, str이 지적하는 메모리의 내용은 변경되지 않는다.

·str에 하나 이상의 문자를 할당한 후에 읽기 에러가 발생하거나 EOF 문자가 나타나면 함수는 NULL을 돌려주고, str이 지적하는 메모리에는 쓸모없는 데이터가 저장된다. 여기서 fgets() 함수가 반드시 한 줄의 문장을 읽어들이지는 않는다는 것을 알 수 있을 것이다. 즉, 함수는 문장 진행 문자가 나타날 때까지 계속해서 문자를 읽어들이지 않는다. 만약 문장 진행 문자가 나타나기 전에 n-1자를 읽어들였다면 fgets()는 동작을 중단한다. 프로그램은 다시 입력 동작을 수행할 때 마지막으로 입력 동작을 중단한 곳에서부터 시작한다. fgets()가 문장 진행문자에서 중단할 때까지 전체 문자열을 읽어들이기 위해서는 입력 버퍼의 크기는 물론이고 fgets()에 전달되는 n의 값을 충분한 크기로 설정하자.

▶ 문자 출력
: 문자 출력 함수에는 putc()와 fputs()가 있다.
 
▶ putc() 함수
: 라이브러리 함수 putc()는 지정된 스트림에 한 문자를 출력한다. 함수의 원형은 STDIO.H에 정의되어 있으며, 다음과 같다.

  int putc(int ch, FILE *fp);

인수 ch는 출력되는 문자이다. 다른 문자 함수에서와 마찬가지로 이 함수에서도 int형이 사용되고 있지만 실제로는 하위 바이트만 사용된다. 인수 fp는 파일에 대한 포인터이다. 즉, 파일을 열 때 fopen()이 돌려주는 포인터이다. 함수 putc()의 동작이 성공적이었다면 출력된 문자를 돌려주고 에러가 발생한 경우에는 EOF를 돌려준다. 기호 상수 EOf는 STDIO.H에 정의되어 있으며 -1의 값을 가진다. 실제로 '어떤' 문자도 -1이라는 값을 가지고 있지 않으므로 텍스트 모드의 파일에서는 에러를 표현하는 문자로 EOF를 사용할 수 있다.

▶ fputs() 함수
: 지정된 스트림에 문장을 출력하기 위해서는 라이브러리 함수 fputs()를 사용하지. 이 함수는 14번째 강의에서 다루어진 puts()와 같은 방법으로 사용된다. 유일한 차이는 fputs()를 사용할 경우 출력 스트림을 지정할 수 있다는 것이다. 또한, fputs()는 문자열의 마지막에 문장 진행 문자를 추가하지 않는다. 원한다면 문장 진행 문자를 직접 포함시키도록 하자. STDIO.H에 정의되어 있는 함수의 원형은 다음과 같다.

   char fputs(char *str, FILE *fp);

인수 str은 스트림으로 출력되고 널 문자로 종료되는 문자열에 대한 포인터이고, fp는 파일을 열 때 fopen()이 돌려주는 FILE형에 대한 포인터이다. str이 지적하는 문자열은 마지막의 \0을 제거한 상태로 파일에 기록된다. 함수 fputs()의 동작이 성공적이라면 음수가 아닌 값을 돌려주고, 에러가 발생하면 EOF를 돌려준다.

5.3 직접 파일 입력과 출력
: 현재 사용 중인 C 프로그램이나 또는 다른 어떤 C 프로그램에서 나중에 사용하기 위한 데이터를 저장할 때에는 직접 파일 입출력을 가장 많이 사용한다. 직접 입출력은 이진 모드의 파일에서만 사용된다. 직접 출력을 수행할 때에는 데이터가 블록 단위로 메모리에서 디스크 파일로 저장된다. 직접 입력의 경우에는 이와 반대로 블록 단위의 데이터를 디스크 파일에서 메모리로 읽어들인다. 예를 들어, 직접 출력 함수를 한 번 호출하여 double형의 배열 전체를 디스크에 저장할 수 있고, 직접 입력 함수를 한 번 호출하여 다시 디스크에서 메모리로 전체 배열을 읽어들일 수 있다. 직접 입출력 함수는 fread()와 fwrite()이다.

▶ fwrite() 함수
: 라이브러리 함수 fwrite()는 메모리의 데이터를 블록 단위로 이진 모드의 파일에 기록한다. STDIO.H에 정의되어 있는 함수의 원형은 다음과 같다.

  int fwrite(void *buf, int size, int count, FILE *fp);

인수 buf는 파일에 기록할 데이터가 저장되어 있는 메모리 영역에 대한 포인터이다. 포인터의 형은 void이므로 어떤 데이터형에 대한 포인터가 될 수 있다. 인수 size는 개별적인 데이터 항목의 크기를 바이트 단위로 지정하는 것이고, count는 기록할 항목의 수를 지정한다. 예를 들어, 100개의 요소를 가지는 정수형 배열을 저장하기 원한다면 각각의 int형은 2바이트를 차지하므로 size는 2가 될 것이고, 배열은 100개의 요소를 가지므로 count는 100이 될 것이다. size 인수를 계산하기 위해서 sizeof() 연산자를 사용할 수 있다. 인수 fp는 물론 파일을 열 때 fopen()이 돌려주는 FILE형에 대한 포인터이다. gwrite() 함수의 동작이 성공적이면 기록한 항목의 개수를 돌려준다. 만약 함수가 돌려주는 값이 count보다 작다면 어던 에러가 발생했다는 것을 알 수 있다. 에러를 확인하기 위해서는 대개 다음과 같이 fwrite()를 사용한다.

  if((fwrite(buf, size, count, fp) != count)
      fprintf(stderr, "Error writing to file.");

fwrite()를 사용하는 몇 가지 예를 살펴보자. 하나의 double형 변수 X를 파일에 기록하기 위해서 다음과 같이 한다.

  fwrite(&x, sizeof(double), 1, fp);

50개의 address형 구조체를 가지는 배열 data[]를 파일에 기록하기 위해서는 두 가지 방법을 사용할 수 있다.

  fwrite(data, sizeof(address), 50, fp);
  fwrite(data, sizeof(data), 1, fp);

첫 번째 방법에서는 address형의 크기를 가지는 50개의 요소가 배열 포함되어 있는 것으로 계산하여 배열을 저장한다. 두 번째 방법에서는 배열을 하나의 '요소'로 취급한다. 두 가지 방법의 실행 결과는 동일하다. 다음 단원에서는 fread()를 설명하고 나서 fread()와 fwrite()를 사용하는 프로그램을 분석할 것이다.

▶ fread() 함수
: fread() 라이브러리 함수는 이진 모드의 파일에서 블록 단위의 데이터를 메모리로 읽어들인 다. STDIO.H에 정의되어 있는 함수의 원형은 다음과 같다.

  int fread(void *buf, int size, int count, FILE *fp);

인수 buf는 파일에서 읽어들인 데이터를 저장할 메모리 영역에 대한 포인터이다. fwrite()에서와 마찬가지로 포인터형은 void이다. 인수 size는 읽어들일 개별적인 데이터 항목의 크기를 바이트 단위로 지정하는 것이고, count는 읽어들일 항목의 개수를 지정한다. 이런 인수들이 fwrite()에서 사용되는 인수들과 같다는 것에 주목하지 바란다. 여기에서도 size 인수를 계산하기 위해서 sizeof() 연산자를 자주 사용한다. 인수 fp는 항상 그렇듯이 파일을 열 때 fopen()이 돌려주는 FILE형에 대한 포인터이다. fread() 함수는 읽어들인 항목의 개수를 돌려준다. 그러나 파일의 마지막에 도달하거나 또는 에러가 발생한다면 이 값은 count보다 작을 수 있다. <리스트 16.4>에 있는 프로그램은 fwrite()와 fread()의 사용 예를 보여준다.

<리스트 16.4> 직접 파일 입출력을 위한 fwrite()와 fread()의 사용

 /* fwrite()와 fread()를 사용한 직접 파일 입출력 */

 #include <stdlib.h>

 #include <stdio.h>


 #define SIZE 20


 main(0

 {

    int count, array1[SIZE], array2[SIZE];

    FILE *fp;


    /* array1[] 초기화 */


    for(count = 0; count < SIZE; count++)

       array1[SIZE] = 2 * count;


    /* 이진 모드 파일 열기 */


    if((fp = fopen("direct.txt", "wb")) == NULL)

    {

       fprintf(stderr, "Error opening file.");

       exit(1);

    }

    /* array1[]을 파일에 저장 */


    if(fwrite(array1, sizeof(int), SIZE, fp) != SIZE)

    {

       fprintf(stderr, "Error writing to file.");

       exit(1);

    }


    fclose(fp);


    /* 같은 파일을 이진 모드 읽기 상태로 연다. */


    if((fp = fopen("direct.txt", "rb")) == NULL)

    {

       fprintf(stderr, "Error epening file.");

       exit(1);

    }


    /* 데이터를 array2[]로 읽어들인다. */


    if(fread(array2, sizeof(int), SIZE, fp) != SIZE)

    {

       fprintf(stderr, "Error reading file.");

       exit(1);

    }


    fclose(fp);


    /* 두 배열이 같다는 것을 보여주기 위해 출력 */


    for(count = 0; count < SIZE; count++)

       printf("%d\t%d\n", array1[count], array2[count]);

    return(0);

 }

6. 파일 버퍼링 : 파일 닫기와 플러시
: 파일의 사용을 마칠 때에는 fclose() 함수로 파일을 닫아야 한다. 이 장에 나타난 프로그램 에서는 이미 fclose()를 사용했었다. 함수의 원형은 다음과 같다.

   int fclose(FILE *fp);

인수 fp는 스트림에 관련된 FILE형 포인터이다. fclose()의 동작이 성공적으로 수행되면 함수는 0을 돌려주고 에러가 발생하면 -1을 돌려준다. 파일을 닫을 때에는 파일 버퍼가 플러시(flush)된다. 즉, 파일에 기록된다. 또한, fcloseall() 함수를 사용하여 표준으로 정의되어 있는 stdin, stdout, stdprn, stderr, stdaux를 제외하고 열려 있는 모드 스트림을 닫을 수도 있다. 이 함수의 원형은 다음과 같다.

   int fcloseall(void);

이 함수는 모든 스트림의 버퍼를 플러시하고 나서 닫힌 스트림의 개수를 돌려준다.

main()의 마지막에 도달하거나 또는 exit() 함수를 실행하여 프로그램을 마칠 때에는 모든 스트림이 자동으로 플러시되고 닫힌다. 그러나 프로그래머가 스트림을 직접 닫는 것이 좋다. 특히, 디스크 파일과 관련된 스트림에서는 파일의 사용이 끝나는 즉시 닫아야 한다. 이것은 스트림 버퍼와 관련된 문제이다.

 디스크 파일에 관련된 스트림을 생성할 때에는 자동으로 버퍼가 생성되고 스트림과 연결된다 버퍼(buffer)는 파일에 기록되거나 파일에서 읽어들여지는 데이터를 임시로 저장하기 위해서 사용되는 메모리 영역이다. 버퍼는 디스크 드라이브가 블록 단위를 기본으로 하는 장치이므 로 필요하다. 이런 장치들은 데이터를 블록 단위로 읽어들이거나 기록할 때 효과적이므로 블록 단위 장치라고 한다. 이상적인 블록의 크기는 사용중인 하드웨어에 따라 다르다. 대개 수백에서 수천 바이트 사이의 크기이다. 그러나 정확한 블록의 크기에 대해서는 신경 쓸 필요가 없다.

 파일 스트림과 관련된 버퍼는 문자를 기본으로 하는 스트림과 블록을 기본으로 하는 디스크 장치 간의 중간 매체 역할을 한다. 프로그램에서 데이터를 스트림으로 기록할 때 데이터는 버퍼가 가득찰 대가지 저장되었다가 블록 단위로 디스크에 기록된다. 이것은 디스크 파일에서 데이터를 읽어들이는 경우에도 적용된다. 버퍼의 생성과 사용은 모두 운영체제에 의해서 자동으로 처리된다. 프로그래머가 신경 쓸 일은 없다. C는 버퍼를 관리하기 위한 몇 가지 함수를 제공하지만 자세한 내용은 생략한다. 그래서 실제로는 이런 버퍼의 동작이 이론과 다르게 이루어진다. 프로그램이 실행되는 동안 디스크에 '저장할' 데이터가 디스크에 기록되는 것이 아니라 버퍼 내에 존재한다는 뜻이다. 만약 전원이 차단되거나 다른 어떤 문제가 발생하여 프로그램이 '중단'되면 버퍼 내의 데이터는 사라질 것이고, 디스크 파일에 저장된 것을 정확히 파악할 수 없게 된다.

 파일을 닫지 않고 스트림에 관련된 버퍼를 플러시(flush)하기 위해서는 fflush()나 flushall() 라이브러리 함수를 사용한다. 파일을 계속해서 사용하는 동안 버퍼를 디스크에 기록하기 원한다면 fflush()를 사용하자. 열려 있는 모든 스트림의 버퍼를 플러시 하기 위해서는 flushall()을 사용하자. 두 함수의 원형은 다음과 같다.

   int fflush(FILE *fp);
   int flushall(void);

 인수 fp는 파일을 열 때 fopen()이 돌려주는 FILE 포인터이다. 만약 쓰기 가능한 상태로 파일을 열었다면 fflush()는 버퍼를 디스크에 저장한다. 그러나 읽기 가능한 상태로 파일을 열었다면 버퍼는 제거된다. 함수 fflush()의 동작이 성공적으로 수행되면 0을 돌려주고, 에러가 발생하면 EOF를 돌려준다. 함수 flushall()은 열린 스트림의 개수를 돌려준다.

7. 파일의 순차적인 사용과 무작위 사용
 : 열려 있는 모든 파일은 관련된 파일 위치 표시(file position indicator)를 가지고 있다. 위치 표시는 파일에서 읽기와 쓰기 동작이 수행되는 위치를 가리킨다. 위치는 항상 파일의 시작을 기준으로 해서 바이트 단위로 표현된다. 새로운 파일을 열 때 위치 표시는 항상 파일의 시작 부분인 위치 0을 가리킨다. 새로운 파일의 길이는 0이므로 다른 곳을 지적할 수 없다. 이미 존재하는 파일을 열 때 파일이 추가 가능한 상태로 열리면 위치 표시는 파일의 마지막을 지적하고, 파일이 다른 어떤 모드로 열리면 파일의 시작 부분을 지적한다.

 여기서의 범위를 벗어나는 내용이기는 하지만, 이 장의 앞 부분에서 설명한 파일 입출력 함수는 위치 표시를 사용한다. 쓰기와 읽기 동작을 현재의 위치 표시에서 수행하고 나서 위치 표시를 변경하는 것이다. 예를 들어, 읽기 가능한 상태로 파일을 열고 10바이트를 읽어 들이면 위치 0부터 9가지에 해당하는 10바이트를 파일의 처음부터 읽어들이는 것이다. 읽기 동작을 수행하고 나면 위치 표시는 위치 10을 지적하게 되고 다음 읽기 동작은 위치 10부터 시작된다. 그래서 파일의 모든 데이터를 순차적으로(sequentially) 읽어들이거나 또는 파일에 데이터를 순차적으로 기록하는 경우에는 스트림 입출력 함수가 자동으로 다루어 주므로 위치 표시에 대해서 신경 쓸 필요가 없다. 그러나 파일을 다른 방법으로 사용할 필요가 있을 때에는 위치 표시의 값을 결정하거나 변경하게 해주는 C 라이브러리 함수를 사용해야 한다. 위치 표시의 값을 제어하면 파일을 무작위(random) 상태로 사용할 수 있다. 여기서 '무작위'라는 것은 앞 부분의 모든 데이터를 읽어들이거나 또는 앞 부분에 기록하지 않고 파일에서 임의의 위치에 있는 데이터를 읽어들이거나 데이터를 기록할 수 있다는 것을 뜻한다.

7.1 ftell()과 rewind() 함수
 : 위치 표시가 파일의 시작 부분을 지적하도록 설정하기 위해서 라이브러리 함수 rewind()를 사용하자. STDIO.H에 정의되어 있는 함수의 원형은 다음과 같다.

    void rewind(FILE *fp);

인수 fp는 스트림과 관련된 FILE 포인터이다. rewind()를 호출한 후에 파일의 위치 표시는 파일의 시작 부분인 바이트 0을 지적하게 된다. 파일에서 약간의 데이터를 일어들인 후에 파일을 닫고나서 다시 열지 않고도 파일의 시작부터 읽어들이기 원한다면 rewind()를 사용함. 파일의 위치 표시 값을 설정하기 위해서는 ftell()을 사용하자. STDIO.H에 정의되어 있는 이 함수의 원형은 다음과 같다.

   long ftell(FILE *fp);

인수 fp는 파일을 열 때 fopen()이 돌려주는 FILE 포인터이다. 함수 ftell()은 파일의 시작부터 현재 파일의 위치까지를 바이트 단위로 나타내는 long형 값을 돌려준다. 첫 번째 바이트는 위치 0이다. 만약 에러가 발생하면 ftell()은 -1의 long형인 -1L을 돌려준다. rewind()와 ftell()의 동작을 이해하기 위해서 <리스트 16.5>에 있는 프로그램을 살펴보자.

<리스트 16.5> ftell()과 rewind()의 사용

 /* ftell()과 rewind()의 사용 예 */

 #include <stdlib.h>

 #include <stdio.h>


 #define VUFLEN 6


 char msg[] = "abcdefghijklmnopqrstuvwxyz";


 main()

 {

    FILE *fp;

    char buf[BUFLEN];


    if((fp = fopen("TEXT.TXT", "w")) == NULL)

    {

       fprintf(stderr, "Error opening file.");

       exit(1);

    }


    if(fputs(msg, fp) == EOF)

    {

       fprintf(stderr, "Error writing to file.");

       exit(1);

    }


    fclose(fp);


    /* 읽기 상태로 파일을 연다 */


    if((fp = fopen("TEXT.TXT", "r")) == NULL)

    {

       fprintf(stderr, "Error opening file.");

       exit(1);

    }

    printf("\nImmediately after opening, position = %ld", ftell(fp));


    /* 5 문자를 읽어들인다. */


    fgets(buf, BUFLEN, fp);

    printf("\nAfter reading in %s, position = %ld", buf, ftell(fp));


    /* 다음 5문자를 읽어들인다. */


    fgets(buf, BUFLEN, fp);

    printf("\n\nThe next 5 characters are %s, and position now = %ld",

           buf, ftell(fp));


    /* 스트림을 시작 부분으로 설정한다. */


    rewind(fp);


    printf("\n\nAfter rewinding, the position is back at %ld",

           ftell(fp));


    /* 5 문자를 읽어들인다. */


    fgets(buf, BUFLEN, fp);

    printf("\nand reading starts at the beginning again: %s", buf);

    fclose(fp);

    return(0);

 }  

7.2 fseek() 함수
: 스트림의 위치 표시(position indicator)를 더욱 정확하게 제어하기 위해서는 라이브러리 함수 fseek()를 사용할 수 있다. fseek()를 사용하면 위치 표시가 파일 내의 임의의 위치를 지적하도록 설정할 수 있다. STDIO.H에 정의되어 있는 함수 원형은 다음과 같다.

   int fseek(FILE *fp, long offset, int origin);

인수 fp는 파일과 관련된 FILE 포인터이다. 위치 표시가 이동되는 거리는 offset에 바이트 단위로 지정된다. 인수 origin은 이동이 시작되는 위치를 지정한다. origin에 사용할 수 있는 기호 상수는 IO.H에 정의되어 있는데, <표 16.2>에 나타나 있듯이 세 가지 값이 있다.

<표 16.2> fseek()에서 사용되는 origin의 값

 상수 이름

 값

 의미

SEEK_SET

0

 위치 표시를 파일의 시작부터 offset 바이트 뒤로 이동

SEEK_CUR

1

 위치 표시를 현재 위치에서 offset 바이트 뒤로 이동

SEEK_END

2

 위치 표시를 파일의 마지막부터 offset 바이트 앞으로 이동

함수 fseek()는 위치 표시를 성공적으로 이동시키면 0을 돌려주고, 에러가 발생하면 0이 아닌 값을 돌려준다. <리스트 16.6>에 있는 프로글매은 파일을 무작위 상태로 사용하기 위해서 fseek()를 사용하고 있다.

<리스트 16.6> fseek()를 사용하여 무작위 상태로 파일을 사용하는 예

 /* fseek()를 사용한 무작위 사용 */

 #include <stdlib.h>

 #include <stdio.h>

 #include <io.h>


 #define MAX 50


 main()

 {

    FILE *fp;

    int data, count, array[MAX];

    long offset;


    /* 배열의 초기화 */


    for(count = 0; count < MAX; count++)

       array[count] = count * 10;


    /* 쓰기 상태로 이진 파일 열기 */


    if((fp = fopen("RANDOM.DAT", "wb")) == NULL)

    {

       fprintf(stderr, "\nError opening file.");

       exit(1);

    }


    /* 배열을 파일에 기록하고 나서 닫는다. */


    if((fwrite(array, sizeof(int), MAX, fp)) != MAX)

    {

       fprintf(stderr, "\nError writing data to file.");

       exit(1);

    }


    fclose(fp);


    /* 읽기 상태로 파일 열기 */


    if((fp = fopen("RANDOM.DAT", "rb")) == NULL)

    {

       fprintf(stderr, "\nError opening file.");

       exit(1);

    }


    /* 읽어들일 요소를 요구한다. */

    /* 요소를 입력하면 출력해주며, -1을 입력하면 마친다. */


    while(1)

    {

       printf("\nError element to read, 0-%d, -1 to quit: ", MAX - 1);

       scanf("%ld", &offset);


       if(offset < 0)

          break;

       else if (offset > MAX-1)

          continue;


       /* 위치 표시를 지정된 요소로 이동시킨다. */


       if((fseek(fp, (offset*sizeof(int)), SEEK_SET)) != 0)

       {

          fprintf(stderr, "\nError using fseek().");

          exit(1);

       }


       /* 하나의 정수를 읽어들인다. */


       fread(&data, sizeof(int), 1, fp);


       printf("\nElement %ld has value %d.", offset, data);

    }


    fclose(fp);

    return(0);

 }

=> 14번째 줄부터 35번째 줄까지는 <리스트 16.5>와 비슷하다. 16번째 줄과 17번째 줄은 50개의 int형 값을 가지는 data라는 배열을 초기화한다. 각 요소에 저장되는 값은 색인을 10배한 것이다. 그리고 나서 배열은 RANDOM.DAT라는 이진 파일에 저장된다. 21번째 줄에서 'wb'를 사용하여 파일을 열었으므로 이진 모드라는 것을 알 수 있다. 39번째 줄에서는 무한 루프인 while문을 실행하기 전에 파일을 읽기 가능한 이진 모드로 다시 열고 있다. while문에서는 값을 읽어들이기 원하는 배열 요소의 번호를 입력하도록 요구한다. 53번째 줄부터 56번째 줄까지는 입력된 요소가 파일 내에 포함되는 것인지 확인해본다는 것에 주의하자. 그렇다면 파일의 마지막을 벗어난 요소를 읽어들인다는 뜻일까? 실제로 그렇다. 배열의 마지막을 지난 곳에 값이 저장될 수 있는 것과 마찬가지로, C는 파일의 마지막을 지난 곳에서 값을 읽어들이게 해준다. 만약 파일의 마지막을 지난 곳이나 시작 이전에서 값을 읽어들인다면 결과는 예상할 수 없을 것이다. 이 프로그램의 53번째 줄부터 56번째 줄까지에 나타나 있는 것처럼 수행하는 동작의 결과를 항상 확인해 보는 것이 좋다. 읽어들이기 원하는 요소의 번호를 확인하고 나면 60번째 줄에서는 fseek()를 사용하여 적절한 위치로 이동한다. SEEK_SET이 사용되므로 이동은 파일의 시작을 기준으로 수행된다. 파일 내에서 이동되는 거리는 offset의 값이 아니라 offset의 값에 요소의 크기를 곱한 만큼이라는 것을 기억하자. 그리고 나서 68번째 줄에서는 값을 읽고, 70번째 줄에서는 값을 출력한다.

8. 파일의 마지막을 찾는 방법
: 파일의 길이를 정확하게 알고 있는 경우에는 파일의 마지막을 찾을 필요가 없을 것이다. 예를 들어, 100개의 요소를 가지는 정수형 배열을 저장하기 위해서 fwrite()를 사용한다면 전체적인 파일은 200바이트의 길이(2바이트 정수라고 가정할 때)가 된다는 것을 알 수 있다. 그러나 파일의 정확한 길이를 모르는 상태에서 파일의 처음부터 마지막까지를 읽어들이기 원하는 경우에는 어떻게 할 것인가? 파일의 마지막을 찾는 두 가지 방법이 있다. 텍스트 모드의 파일에서 문자 단위로 값을 읽어들일 때에는 EOF 문자를 찾을 수 있다. 기호 상수 EOF는 STDIO.H에서 -1로 정의되어 있고 '실제' 문자에서 사용되지 않는 값이다. 그래서 문자 입력 함수가 텍스트 모드의 스트림에서 EOF를 읽어들일 때 파일의 마지막에 도달했다는 것을 알 수 있다. 예를 들어, 다음과 같은 문장을 작성할 수 있을 것이다.

    while((c = fgetc(fp)) != EOF)

이진 모드의 스트림에서는 -1의 값을 가지는 데이터가 사용될 수도 있으므로 EOF가 파일의 마지막을 뜻하지는 않는다. -1의 값을 파일의 마지막을 뜻하지 않을 것이다. 대신에, 이진 모드에서는 라이브러리 함수 feof()를 사용할 수 있다. 이 함수는 이진 모드와 텍스트 모드의 파일에서 사용할 수 있다.

   int feof(FILE *fp);

인수 fp는 파일을 열 때 fopen()이 돌려주는 FILE 포인터이다. 함수 feof()는 파일 fp의 마지막에 도달하지 않았다면 0을 돌려주고, 파일의 마짐가에 도달하면 0이 아닌 값을 돌려준다. feof() 함수를 사용했을 때 파일의 마지막에 도달했다는 것을 확인하면 rewind()를 수행하거나, fseek()를 사용하거나, 또는 파일을 닫고 다시 열 때가지 더 이상의 일기 동작이 허용되지 않는다. <리스트 16.7>에 있는 프로그램은 feof()의 사용 예를 보여준다. 프로그램이 파일의 이름을 입력하도록 요구할 때 텍스트 파일의 이름을 입력하자. 예를 들어, C 소스 파일이나 STDIO.H와 같은 헤더 파일의 이름을 입력할 수 있다. 파일이 현재 디렉토리에 있다는 것을 확인하거나 또는 파일명의 일부로 경로를 입력하기 바란다. 프로그램은 feof()가 파일의 마지막을 발견할 때까지 파일을 한 번에 한 줄씩 읽어들이고 stdout으로 출력한다.

<리스트 16.7> 파일의 마지막을 찾기 위한 feof()의 사용 예

 /* 파일의 마지막 찾기 */

 #include <stdlib.h>

 #include <stdio.h>


 #define BUFSIZE 100


 main()

 {

    char buf[BUFSIZE];

    char filename[60];

    FILE *fp;


    puts("Error name of text file to display: ");

    gets(filename);


    /* 읽기 상태로 파일 열기 */

    if((fp = fopen(filename, "r")) == NULL)

    {

       fprintf(stderr, "Error opening file.");

       exit(1);

    }


    /* 파일의 마지막에 도달하지 않았다면 한 줄을 읽고 출력한다. */


    while(!feof(fp))

    {

       fgets(buf, BUFSIZE, fp);

       printf("%s", buf);

    }


    fclose(fp);

    return(0);

 }

-> 입력 / 출력

  Enter name of text file to display:
  hello.c
  #include <stdio.h>
  main()
  {
     printf("Hello, world.");
     return(0);
  }

9. 파일 처리 함수
 : 파일 처리(file management)는 디스크에 존재하는 파일을 다루는 동작을 뜻한다. 즉, 파일에서 데이터를 읽어들이거나 파이에 저장하는 것이 아니라 파일 자체를 삭제하거나 파일의 이름을 변경하고 복사하는 것을 말한다. C 표준 라이브러리에서는 파일을 삭제하고, 이름을 변경하기 위한 함수가 제공되며, 자신만의 파일 복사 함수를 작성할 수 있다.

9.1 파일 삭제하기
 : 파일을 삭제하기 위해서는 라이브러리 함수 remove()를 사용한다. 함수 원형은 다음과 같이 STDIO.H에 정의되어 있다.

   int remove(const char *filename);

변수 *filename은 삭제되는 파일의 이름에 대한 포인터이다. 파일의 이름에 대해서는 이 장의 앞 부분을 참고하기 바란다. 지정된 파일이 존재한다면 DOS 프롬프트에서 DEL 명령이나 UNIX에서 rm 명령을 사용한 것과 마찬가지로 삭제되고, remove() 함수는 0을 돌려준다. 파일이 존재하지 않거나 또는 읽기 전용 상태로 되어 있거나, 사용자가 적절한 이용 권한을 가지지 않거나, 다른 어떤 에러가 발생하면 remove()는 -1을 돌려준다. <리스트 16.8>에 있는 간단한 프로그램은 remove()의 사용 예를 보여준다. 주의하자. 파일을 '삭제'하면 완전히 사라지는 것이다.

<리스트 16.8> 디스크 파일을 삭제하기 위한 remove()함수의 사용

 /* remove() 함수의 사용 예 */


 #include <stdio.h>


 main()

 {

    char filename[80];


    printf("Enter the filename to delete: ");

    gets(filename);


    if(remove(filename) == 0)

       printf("The file %s has been deleted.", filename);

    else

       fprintf(stderr, "Error deleting the file %s.", filename);

    return(0);

 }

9.2 파일의 이름 변경하기
: rename() 함수는 이미 존재하는 디스크 파일의 이름을 변경한다. 함수 원형은 다음과 같이 STDIO.H에 정의되어 있다.

   int rename(const char *oldname, const char *newname);

oldname과 newname이 지적하는 파일의 이름은 이 장의 앞부분에서 설명한 규칙을 따른다. 한 가지 제한이 있다면 두 이름은 동일한 디스크 드라이브를 지정해야 한다는 것이다. 서로 다른 디스크 드라이브에 존재하는 파일의 이름을 '변경'할 수는 없다. 함수 rename()의 동작이 성공적이라면 0을 돌려주고, 에러가 발생하면 -1을 돌려준다. 에러는 다음과 같은 경우에 발생할 수 있다.

·oldname이라는 파일이 존재하지 않는다.
·newname이라는 이름의 파일이 이미 존재한다.
·서로 다른 디스크에서 이름을 변경하려고 한다.

<리스트 16.9>에 있는 프로그램은 rename()의 사용 예를 보여준다.

<리스트 16.9> 디스크 파일의 이름을 변경하기 위한 rename()의 사용

 /* 파일명을 변경하기 위한 rename()의 사용 예 */


 #include <stdio.h>


 main()

 {

    char oldname[80], newname[80];


    printf("Enter current filename: ");

    gets(oldname);

    printf("Enter new name for file: ");

    gets(newname);


    if(rename(oldname, newname) == 0)

       printf("%s has been renamed %s.", oldname, newname);

    else

       fprintf(stderr, "An error has occurred renaming %s.", oldname);

    return(0);

 }

9.3 파일 복사하기
: 프로그램에서는 가끔 파일을 복사할 필요가 있다. 파일을 복사한다는 것은 다른 이름이나 또는 동일한 이름이지만 다른 드라이브나 디렉토리에 동일한 내용의 복사본을 만드는 것을 뜻한다. DOS에서는 COPY 명령을 수행하여 파일을 복사할 수 있고, 다른 운영체제에서는 같은 기능의 명령이 있다. C 프로그램에서는 어떻게 파일을 복사할 수 있을까? C 에서는 파일을 복사는 라이브러리 함수를 제공하지 않으므로 직접 자신만의 함수를 작성할 필요가 있다. 이것은 어렵게 생각될 수 있지만, C의 입출력용 스트림을 사용한다면 아주 간단하게 수행할 수 있다. 다음은 함수에서 수행할 필요가 있는 단계들이다.

① 원본 파일을 읽기 가능한 상태의 이진 모드로 연다. 이진 모드를 사용하는 이유는 텍스트 파일뿐 아니라 모든 종류의 파일을 복사할 수 있도록 하기 위해서이다.

② 목적 파일을 쓰기 가능한 상태의 이진 모드로 연다.

③ 원본 파일에서 문자를 읽어들인다. 파일이 처음 열릴 때 위치 표시는 파일의 사직 부분을 지적하고 있으므로 파일 포인터를 다시 위치시킬 필요는 없다는 사실을 기억하자.

④ 함수 feof()가 원본 파일의 마지막에 도달했다는 것을 알려주면 복사가 완료된 것이므로 두 개의 파일을 닫고 함수를 호출한 프로그램으로 돌아갈 수 있다.

⑤ 파일의 마지막에 도달하지 않았다면 읽어들인 문자를 목적 파일에 기록하고 나서 단계 3으로 돌아간다.

<리스트 16.10>에 있는 프로그램은 앞에서 설명한 순서대로 원본과 목적 파일의 이름을 전달받아서 복사 동작을 수행하는 함수 copy_file()을 사용하고 있다. 어떤 파일을 열 때 에러가 발생하면 이 함수는 복사를 수행하지 않고 함수를 호출했던 프로그램에 -1을 돌려줌. 복사 과정이 완료되면 프로그램은 두 개의 파일을 닫고 0을 돌려준다.

<리스트 16.10> 파일을 복사하는 함수

 /* 파일 복사 */


 #include <stdio.h>


 int file_copy(char *oldname, char *newname);


 main()

 {

    char source[80], destination[80];


    /* 원본과 목적 파일의 이름을 구한다. */


    printf("\nEnter source file: ");

    gets(source);

    printf("\nEnter destination file: ");

    gets(destination);


    if(file_copy(source, destination) == 0)

       puts("Copy operation successful");

    else

       fprintf(stderr, "Error during copy operation");

    return(0);

 }

 int file_copy(char *oldname, char *newname)

 {

    FILE *fold, *fnew;

    int c;


    /* 원본 파일을 읽기 상태의 이진 모드로 연다. */


    if((fold = fopen(oldname, "rb")) == NULL)

       return -1;


    /* 목적 파일을 쓰기 상태의 이진 모드로 연다. */


    if((fnew = fopen(newname, "wb")) == NULL)

    {

       fclose(fold);

       return -1;

    }


    /* 원본에서 한 번에 한 바이트를 읽는다. */

    /* 만약 파일의 마지막에 도달하지 않았다면 */

    /* 목적 파일에 바이트를 기록한다. */


    while(1)

    {

       c = fgetc(foid);


       if(!feof(foid))

          fputc(c, fnew);

       else

          break;

    }


    fclose(fnew);

    fclose(fold);


    return(0);

 }

10. 임시 파일 사용하기
: 어떤 프로그램은 실행되는 동안 하나이상의 임시 파일을 사용한다. 임시 파일(temporary file)은 프로그램에 의해서 생성되고 프로그램이 실행되는 동안 다른 목적으로 사용되다가 프로그램이 종료되기 전에 삭제되는 파일이다. 임시 파일을 생성할 때에는 나중에 삭제할 것이므로 파일의 이름에 대해서 신경 쓰지 않는다. 그러나 이미 사용중이 아닌 파일의 이름을 사용해야 한다. C 표준 라이브러리에서는 어떤 존재하는 파일과 충돌하지 않는 파일의 이름을 생성하는 함수 tmpnam()이 제공된다. STDIO.H에 정의되어 있는 함수의 원형은 다음과 같다.

   char *tmpnam(char *s);

인수 s는 파일 이름을 저장하기에 충분한 버퍼에 대한 포인터가 되어야 한다. 또한, 파일의 임시 이름을 tmpnam의 내장 버퍼에 저장하는 경우에는 널(NULL) 포인터를 전달할 수 있고, 함수는 버퍼에 대한 포인터를 돌려준다. <리스트 16.11>에 있는 프로그램은 파일의 임시 이름을 생성하기 위해서 tmpnam()을 사용하는 두 가지 방법을 보여준다.

<리스트 16.11> 파일의 임시 이름을 생성하기 위한 tmpnam()의 사용

 /* 임시 파일명의 사용 예 */


 #include <stdio.h>


 main()

 {

    char buffer[10], *c;


    /* 정의된 버퍼에 임시 이름을 저장한다. */


    tmpnam(buffer);


    /* 다른 이름을 구한다. */

    /* 이번에는 함수의 내장 버퍼에 저장한다. */


    c = tmpnam(NULL);


    /* 이름을 출력한다. */


    printf("Temporary name 1: %s", buffer);

    printf("\nTemporary name 2: %s", c);

 }


'C 언어' 카테고리의 다른 글

15장 포인터 : 고급 기능들  (0) 2019.06.02
16장 링크리스트  (0) 2019.06.02
API 윈도우 창 띄우기  (0) 2019.05.25
CreateWindow() - 10개의 인수  (0) 2019.05.25
1. 배열  (0) 2019.05.25

#include <windows.h>

LRESULT CALLBACK wndproc (HWND,UINT, WPARAM, LPARAM);
HINSTANCE ginst;
LPCTSTR lpct=TEXT("첫 번째 예재");

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
 HWND hwnd;
 MSG message;
 WNDCLASS wndclass;
 ginst=hInstance;

 wndclass.cbClsExtra=0;
 wndclass.cbWndExtra=0;
 wndclass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
 wndclass.hCursor=LoadCursor(NULL,IDC_ARROW);
 wndclass.hIcon=LoadIcon(NULL, IDI_APPLICATION);
 wndclass.hInstance=hInstance;
 wndclass.lpfnWndProc=wndproc;
 wndclass.lpszClassName=lpct;
 wndclass.lpszMenuName=NULL;
 wndclass.style=CS_HREDRAW | CS_VREDRAW;
 RegisterClass(&wndclass);

 hwnd=CreateWindow(lpct,lpct, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, (HMENU)NULL,hInstance,NULL);

 ShowWindow(hwnd,nShowCmd);

 while (GetMessage(&message,NULL,0,0))
 {
  TranslateMessage(&message);
  DispatchMessage(&message);
 }
 return (int)message.wParam;
}

LRESULT CALLBACK wndproc(HWND hWnd,UINT msg, WPARAM wParam, LPARAM lParam)
{
 switch (msg)
 {
 case WM_DESTROY : PostQuitMessage(0);
  return 0;
 }
 return (DefWindowProc(hWnd, msg, wParam, lParam));
}


윈도우 창하나 띄우는 API 소스다
교제에 첫번째 예재가 이러한데 책이 바이블식으로 되있는데도 이해가 힘들다.

LRESULT CALLBACK wndproc (HWND,UINT, WPARAM, LPARAM);

이부분에 대해..
보면 LRESULT와 CALLBACK이라는 데이터형이 있습니다. LRESULT는 윈도우 프로시저에서 반환되는 데이터형이고 CALLBACK은 FAR PASCAL을 재정의한 것으로 콜백루틴이나 프로시저에서 사용한다.

먼저 LRESULT에 대해 말해 보면 LRESULT는 비주얼 C++에서 다음과 같이 선언되 있습니다.

#define LRESULT LONG

 즉 LRESULT는 long 변수의 다른 이름일 뿐이다. long이라는 리턴값을 쓰지 않고 굳이 LRESULT라고 재선언한 것은 이 값이 리턴값임을 좀더 명확히 나타내기 위한 프로그래머의 의도라 생각하면 된다. 결국 LRESULT로 반환되는 값은 long값이라 생각하면 되는데 그렇다고 해서 꼭 숫자일 필요는 없다. long은 4 바이트 변수이므로 LRESULT에 포인터를 캐스팅해서 반환해도 무관하다. 포인터 역시 4바이트의 변수일 뿐이다. 대부분의 윈도우 프로그램에서는 LRESULT 값으로 객체의 포인터를 반환하는 것을 자주 볼 수 있을 것이다.

 CALLBACK은 FAR PASCAL의 다른 이름이다. FAR라는 것은 원거리 포인터임을 나타내는 것이고 PASCAL이라는 것은 함수 호출규약을 나타내는 것이다. 프로그래밍 내에서 함수호출이라는 것은 그 함수의 시작포인터로 점프하는 것을 의미하고, 또 윈도우에서는 가상 메모리 주소를 사용하므로 대부분의 함수 진입부가 원거리 포인터라는 뜻.

 하지만 윈도우 95이상에서는 원거리 포인터와 근거리 포인터의 구분이 없으므로 FAR라는 것은 무시해도 상관없다. PASCAL 호출규약이라는 것은 함수를 호출할 때 넘겨지는 인자가 스택에 어떠한 순서로 쌓일지에 대한 약이다. C에서는 인자를 왼쪽에서부터 오른쪽으로 차례로 인자 값을 스택에 넣어 넘겨주고 함수에 진입하면 이와는 반대순서로 스택에서 그 값을 받아와 사용한다. 하지만 C++에서는 이와 반대의 순서로 스택에 인자를 넣어 사용하게 된다. 또한 PASCAL 호출규약은 함수를 호출하면 이전에 사용되던 변수의 값 ax, bx, cx 등의 값을 먼저 스택에 넣어두었다가 함수가 리턴되기 전에 이 값을 복원시켜주지만 C에서는 함수를 호출하기 전 이러한 일들을 하고 함수가 끝나서 이전의 스텝으로 돌아오면 그제서야 변수값을 복원하는 일을 한다다.

 이러한 것이 바로 FAR PASCAL이 의미하는 것이고 이것을 CALLBACK이라고 선언해 사용하는 것은 이 함수가 콜백함수로 사용됨을 프로그래머가 명시하기 위한 것이다. 즉 자신이 짠 이 함수는 윈도우에서 CALLBACK되어 불리어지는 함수라는 것을 자신이나, 다른 사람이 봐서 금방 알 수 있도록 이렇게 이름 붙여놓은 것이다. 이 두 가지 모두 사용 용도를 명확히 하는 역할을 할 뿐 다른 것과의 차이점은 없는데, 이렇게 사용하는 이유는 윈도우 프로그램의 크기가 예전에 비해 무척 커져가고 작업도 여러 사람이 같이 하는 경우가 많아 서로의 의도를 다른 사람에게 명확하게 설명하고자 하는 제작자의 의도가 들어있다고 보면 된다.

'C 언어' 카테고리의 다른 글

16장 링크리스트  (0) 2019.06.02
17장 디스크 파일의 사용  (0) 2019.06.02
CreateWindow() - 10개의 인수  (0) 2019.05.25
1. 배열  (0) 2019.05.25
ASCII 코드표  (0) 2019.05.25

윈도우를 생성할때 쓰는 CreateWindow 함수..

HWND CreateWindow(lpszClassName, lpszWindowName, dwStyle, x, y, nWidth,, nHeight, hwndParent, hmenu, hinst, lpvParam)

10개의 인수를 취하는데 순서에 맞추어 정확하게 인수를 전달해야 한다. 한개씩 살펴보면..

■ lpszClassName
 : 생성하고자 하는 윈도우의 클래스를 지정. WndClass.lpszClassName 맴버에 대입했던 것을 같이 대입한다.

■ lpszWindowName
 : 윈도우의 타이틀 바에 나타날 문자열

■ dwStyle
 : 윈도우의 형태를 지정하는 인수
 WS_CAPTION - 타이틀 바를 가진다.
 WS_HSCROLL - 수평 스크롤바를 가진다.
 WS_VSCROLL - 수직 스크롤바를 가진다.
 WS_MAXIMIZEBOX - 최대화 버튼을 가진다.
 WS_MINIMIZEBOX - 최소화 버튼을 가진다.
 WS_SYSMENU - 시스템 메뉴를 가진다.
 WS_THICKFRAME  - 크기를 조절할 수 있는 경계선을 가진다.
 
 WS_OVERLAPPEDWINDOW - 타이틀 바, 시스템 메뉴, 크기 조절, 최소 최대 버튼등이 한번에 정의 되어있다.

■ X, Y, nWidth, nHeight
 : ,X, Y, 는 화면의 좌표값이고 nWidth, nHeight 는 윈도우의 높이와 넓이 이다. CW_USEDEFAULT 로 설정하면 운영체제가 적당한 크기를 찾게 된다.

■ hWndParent
 : 부모윈도우가 있을경우 부모 윈도우의 핸들을 지정한다. 없거나 최상위 윈도우일경우 NULL로 지정

■ hmenu
 : 윈도우에서 사용할 메뉴의 핸들. CreateWindows 함수로 만들어진 윈도우에만 적용되는 메뉴. 없으면 NULL

■ hinst
 : 프로그램의 핸들을 지정한다. WinMain의 인수 hInstance.

■ lpvParam
 : CREATESTRUCT라는 구조체의 번지. 보통은 NULL 값이며 잘 사용되지 않는다.

'C 언어' 카테고리의 다른 글

16장 링크리스트  (0) 2019.06.02
17장 디스크 파일의 사용  (0) 2019.06.02
API 윈도우 창 띄우기  (0) 2019.05.25
1. 배열  (0) 2019.05.25
ASCII 코드표  (0) 2019.05.25

배열(array)은 동일한 하나의 데이터형을 가진 연속된 원소들로 구성된다. 배열을 원한다면, 선언(declaration)을 사용하여 이를 컴파일러에게 알려야 한다. 배열 선언(array declaration)은 그 배열이 몇개의 원소를 가지고 있으며, 원소들의 데이터형이 무엇인지 컴파일러에게 알려 준다. 이 정보가 있어야만, 컴파일러는 배열을 바르게 설정할 수 있다. 배열 원소들은 보통의 변수들이 가질 수 있는 것과 동일한 데이터형들을 가질 수 있다. 다음과 같은 배열 선언의 예를 살펴보자

/* 배열 선언의 몇 가지 예 */
int main(void)
{
        float candy[365]; /* 465개의 float 형 값을 가지는 배열 */
        char code[12];  /* 12개의 char 형 값을 가지는 배열 */
        int states[50];  /*50개의 int형 값을 가지는 배열 */
        ......
}

각괄호([])가 candy, code, states가 배열이라는 것을 나타낸다. 각괄호 안에 있는 수는 그 배열 안에 있는 원소의 개수를 나타낸다.

배열 안에 있는 각 원소들은, 인덱스(index)라 부르는 첨자 번호를 사용함으로서 개별적으로 접근할 수 있다. 인덱스는 0부터 시작한다. 따라서, candy[0]은 candy 배열의 첫 번재 원소다. candy[364]는 365번째 원소, 즉 마지막 원소다.

이것은 우리가 이미 아는 내용이다. 이제 좀더 새로운 내용을 살펴보자.

초기화...
일반적으로, 배열은 프로그램에 필요한 데이터를 저장하는 데 사용된다. 예를 들면, 12개의 원소를 가지는 배열은 1년의 각 달의 날짜 수를 저장할 수 있다. 이러한 경우에는 프로그램의 시작 부분에서 배열을 초기화 시키는 것이 편리하다. 배열을 어떻게 초기화하는지 살펴보자

우리는, 단일 값을 가지는 변수를 -흔히 스칼라(scalar) 변수라고 부른다- 을 선언해서 다음과 같은 수직으로 초기화 시킬 수 있다는 것을 알고 있다.

int fix = 1;
float flax = PI * 2;

여기서 PI는 매크로로 이미 정의되어 있다고 가정했다. C는 다음과 같이, 새로운 신택스를  사용하여 초기화를 배열에 까지 확장한다.


int main(void)
{
        int powers[8] = {1,2,4,6,8,16,32,64}; /* ANSI에서만 가능하다 */
        ........
}

여기서 볼 수 있듯이, 배열은 콤마로 분리된 값들의 리스트를 중괄호로 감싸서 초기화한다. 원한다면 값과 콤마 사이에 스페이스를 넣을 수도 있다. 첫 번째 원소 (powers[0])에 값 1이 대입되고, 나머지 원소들에도 차례로 값이 대입된다. (컴파일러가 이 형식의 초기화를 신택스 에러로 인식한다면, ANSI 이전의 컴파일러 이기 때문이다. 그러한 경우에는 배열 선언 앞에 키워드 static를 붙이면 해결된다. 이 키워드의 의미에 대해서는 '12장 : 기억부류, 연계, 메모리 관리;에서 설명한다.) 리스트 10.1 은 각 달의 수를 출력하는 짧은 프로그램이다.

리스트10.1

/* day_mon1.c -- 각 달의 날짜 수를 출력한다 */
#include <stdio.h>
#define MONTHS 12
int main(void)
{
    int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};
    int index;

    for (index = 0; index < MONTHS; index++)
        printf("%2d월: 날짜 수 %2d\n", index+1, days[index]);

    return 0;
}



출력 결과는 다음과 같다.
 1월 : 날짜 수 31
 2월 : 날짜 수 28
 3월 : 날짜 수 31
 4월 : 날짜 수 30
 5월 : 날짜 수 31
 6월 : 날짜 수 30
 7월 : 날짜 수 31
 8월 : 날짜 수 31
 9월 : 날짜 수 30
10월 : 날짜 수 31
11월 : 날짜 수 30
12월 : 날짜 수 31

그다지 훌륭하지는 않지만, 이 프로그램은 4년에 딱 한달만 날짜 수가 틀린다. 이 프로그램은 콤마로 분리된 값들의 리스트를 중괄호({})로 감싸서 days[]를 초기화 한다.

이 예제는 배열 크기를 기호 상수 MONTHS를 사용하여 나타내고 있다. 이것은 일반적으로 추천할 만한 테크닉이다. 예를 들어, 갑자기 1년이 13개월로 바뀐다면, 해당하는 #defind 지시문만 수정하면 되기 때문에, 프로그램에서 배열 크기가 사용된 모든 곳을 일일이 찾아다니며 고칠 필요가 없다.

NOTE
배열에 const 사용하기
때로는 배열을 읽기 전용으로만 사용하고 싶을 때가 있다. 즉, 프로그램이 배열에서 값을 꺼내 오기는 하지만, 배열에 새로운 값을 써 넣지 않는 것이다. 이러한 경우에는, 배열을 선언하고 초기화할 때 const 키워드를 사용할 수 있고, 또한 사용해야 한다. 그러므로 리스트 10.1에서 배열을 다음과 같이 초기화하는 것이 더 나은 선택이다.

const int days [MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};

이것은, 프로그램이 배열에 있는 각 원소를 상수로 취급하게 만든다. 보통의 변수와 마찬가지로, 일단 const 로 선언되면 나중에 값들을 대입할 수 없기 때문에, const 데이터를 초기화하려면 선언을 사용해야 한다. 이것을 알게 되었으므로, 이제부터 나오는 예제에 const를 사용할 수 있다.


그런데 배열을 초기화 하는데 길패하면 어떻게 될까? 리스트 10.2는 그와 같은 경우에 무슨 일이 벌어지는지 보여준다.

리스트 10.2

/* no_data.c -- 초기화시키지 않은 배열 */
#include <stdio.h>
#define SIZE 4
int main(void)
{
    int no_data[SIZE];  /* 초기화시키지 않은 배열 */
    int i;
    printf("%2s%14s\n",
           "i", "no_data[i]");
    for (i = 0; i < SIZE; i++)
        printf("%2d%14d\n", i, no_data[i]);
 
    return 0;
}


 

다음은 프로그램의 실행 예다. (시스템에 따라 다른 결과가 나올 수 있다.)
i        no_date[!]
0        -858993460
1        -858993460
2        -858993460
3        -858993460

배열 원소는 보통의 변수와 같다. 사용자가 이들을 초기화하지 않는다면 그들은 아무 값이나 가질 수 있다. 컴파일러는 우연히 그 메모리 위치에 놓여 있는 값들을 사용한다. 사용자의 시스템이 이 실행 예와 다를 결과가 나오는 것도 바로 이 때문이다.

NOTE
기억 부류에 대한 사전 통고
배열도, 다른 변수들과 마찬가지로, 여러 가지 기억 부류(storage class)를 사용하여 생성할 수 있다. 이 주제에 대해서는 12장에서 설명한다. 지금 당장은, 이 장에서 설명하는 배열들이 모두 자동 기억 부류에 속한다는 것만 알면 된다. 이것은 그 배열들이 static 키워드 없이 함수 안에서 선언되고 있다는 것을 의미한다. 지금까지 사용한 모든 변수들과 배열은 자동 기억 부류에 속한다.

이 시점에서 기억 부류를 언급하는 이유는, 서로 다른 기억 부류는 서로 다른특성을 가지고 있기 때문이다. 그래서 이 장에서 설명하는 모든 것을 다른 기억 부류로 일반화시킬 수 ㅇ없다. 특히, 일부 다른 기억부류에 속하는 변수와 배열들은 사용자가 초기화하지 않을 경우 그들의 내용을 0으로 설정한다.

초기화 리스트에 들어 있는 항목들의 개수는 배열의 크기와 일치해야 한다. 이것이 일치하지 않으면 무슨 일이 벌어질까? 초기값의 개수가 두 개 모자란 상태의 리스트를 가지고 앞의 예제를 다시 시도해 보자

리스트 10.3
/* some_data.c -- 일부만 초기화된 배열 */
#include <stdio.h>
#define SIZE 4
int main(void)
{
    int some_data[SIZE] = {1492, 1066};
    int i;

    printf("%2s%14s\n",
           "i", "some_data[i]");
    for (i = 0; i < SIZE; i++)
        printf("%2d%14d\n", i, some_data[i]);

    return 0;
}

다음은 프로그램의 실행 예다. (시스템에 따라 다른 결과가 나올 수 있다.)
i        some_data[!]
0                    1492
1                    1066
2                        0
3                        0

여기서 볼 수 있듯이, 컴파일러는 문제를 알아채지 못했다. 컴파일러는, 초기값 리스트에 있는 값들을 다 사용하고 나서, 나머지 원소들을 0으로 초기화했다. 즉, 사용자가 배열을 전혀 초기화하지 않으면, 배열 원소들은, 초기화하지 않은 보통의 변수들처럼 쓰레기 값들을 갖게 된다. 그러나 배열을 일부분만 초기화하면, 나머지 원소들이 0으로 설정된다. 컴파일러는 이런 넉넉함을 에러로 간주한다. 그러나 컴파일러의 이런 변덕에 속앓이를 할 필요가 없다. 각괄호 안의 배열 크기를 생략하면 컴파일러가 스스로 초기값 리스트에 맞에 배열 크기를 설정한다.

리스트 10.4

/* day_mon2.c -- 컴파일러가 원소 개수를 카운트한다 */
#include <stdio.h>
int main(void)
{
    const int days[] = {31,28,31,30,31,30,31,31,30,31};
    int index;
    for (index = 0; index < sizeof days / sizeof days[0]; index++)
        printf("%2d월: 날짜 수 %2d\n", index +1,
               days[index]);

    return 0;
}

리스트 10.4에서 주목할 점은 다음 두 가지다.
■ 빈 각괄호를 사용하여 배열을 초기화하면, 컴파일러는 초기값리스트에 있는 항목들의 개수를 카운트하여 그것을 배열 크기로 가지는 배열을 만든다.
■ for 루프 제어 명령문에서 우리가 무엇을 했는지 주목하라. 우리는 정확하게 카운트하는 능력이 (당연히) 부족하기 때문에, 컴퓨터가 배열 크기를 계산해서 우리에게 알려 주도록 부탁한다. sizeof 연산자는 객체 또는 데이터형(type)의 크기를 바이트 수로 알아낸다. 그러므로 sizeof days는 바이트 수로 배열 전체의 크기다. sizeof days[0]은 바이트 수로 배열 원소 하나의 크기다. 벼열 전체의 크기를 배열 원소 하나의 크기로 나누면, 그 배열에 몇 개의 원소가 있는지 알 수 있다.

프로그램의 실행 결과는 다음과 같다.
 1월: 날짜 수 31
 2월: 날짜 수 28
 3월: 날짜 수 31
 4월: 날짜 수 30
 5월: 날짜 수 31
 6월: 날짜 수 30
 7월: 날짜 수 31
 8월: 날짜 수 31
 9월: 날짜 수 30
10월: 날짜 수 31
 
에라! 값을 10개만 입력했내 ㅡㅡ.. 배열 크기를 프로그램이 직접 알아내게 하는 이 방법은, 배열의 끝을 지나쳐서 출력하는 사태를 예방한다. 그러나 또한 이것은 자동 카운트의 잠재적인 단점을 경고한다. 원소의 개수가 틀렸더라도 컴파일러가 이 에러를 잡아내지 못한다는 것이다.

배열을 초기화하는 간단한 방법이 한 가지 더 있다. 그 방법은 문자열에만 적용되기 때문에, 다음 장에서 설명한다.


젠장... 손가락이 아프군요... 나중에 다시 이어서...ㅠㅠ

'C 언어' 카테고리의 다른 글

16장 링크리스트  (0) 2019.06.02
17장 디스크 파일의 사용  (0) 2019.06.02
API 윈도우 창 띄우기  (0) 2019.05.25
CreateWindow() - 10개의 인수  (0) 2019.05.25
ASCII 코드표  (0) 2019.05.25

+ Recent posts