많은 프로그래밍 작업은 구조체(structures)라고 하는 C의 데이터형을 통해서 단순화될 수 있다. 구조체는 프로그램에서 필요한 데이터형을 프로그래머가 직접 구성하여 사용하는 데이터 저장 방식의 한 가지이다. 오늘은 다음과 같은 내용을 배운다.

·단순 구조체와 복합 구조체에 대해서
·구조체를 정의하고 선언하는 방법
·구조체에 포함되어 있는 데이터를 사용하는 방법
·배열을 포함하는 구조체와 구조체의 배열을 생성하는 방법
·구조체 내에서 포인터를 선언하는 방법과 구조체에 대한 포인터를 선언하는 방법
·함수의 인수로 구조체를 전달하는 방법
·공용체를 정의하고, 선언하고, 사용하는 방법
·구조체로 새로운 데이터형을 정의하는 방법

1. 단순 구조체
 : 구조체(structure)는 여러 개의 변수를 쉽게 사용할 수 있도록 하나의 이름으로 묶은 하나 이상의 변수의 집합이다. 구조체에 포함되는 변수는 배열에서와는 달리 여러 가지 데이터형 이 될 수 있다. 구조체는 배열이나 다른 구조체 등 C의 모든 데이터형을 포함할 수 있다. 구조체에 포함되는 각각의 변수를 구조체 멤버(member)라고 한다. 우선, 단순 구조체에 대해서 알아보도록 하자. 이렇게 구조체를 두 가지 형태로 구분하여 설명하는 것이 이해하기 쉽지만, 실제로 C 언어에서는 단순 구조체와 복합 구조체를 구분해서 사용하지 않는다는 것을 기억하기 바란다.

1.1 구조체의 정의와 선언
 : 만약 그래픽 프로그램을 작성한다면, 프로그램은 화면 상에 출력되는 점의 좌표를 다룰 필요가 있을 것이다. 화면 좌표는 수평 좌표를 나타내는 x값과 수직 좌표를 나타내는 y값으로 구성된다. 다음과 같이 화면에서 점의 위치를 x와 y값으로 나타내는 coord라는 이름의 구조체를 정의할 수 있다.

  struct coord {
     int x;
     int y;
  };

구조체 정의가 시작된다는 것을 알려주는 struct 키워드 다음에는 반드시 구조체의 이름이나 태그(tag)가 포함되어야 한다. 이것은 C에서 다른 변수를 선언할 때와 같다. 구조체 이름 뒤에 있는 중괄호에는 구조체 멤버인 변수의 목록이 포함된다. 각각의 구조체 멤버에 대해서 변수의 형태와 이름이 사용되어야 한다. 앞의 예제는 두 개의 정수형 변수 x와 y를 가지는 coord라는 이름의 구조체를 정의한다. 그러나 실제로 구조체 coord형의 변수(instances)를 생성하지는 않는다. 즉, 구조체형에 대한 메모리 영역을 보존하지 않는다. 구조체형의 변수를 실제로 생성하는 두 가지 방법이 있는데, 하나는 다음과 같이 구조체 정의문 다음에 하나 이상의 변수 이름을 포함시키는 것이다.

  struct coord {
     int x;
     int y;
  } first, second;

앞의 문장은 구조체 coord를 정의하고 coord형 변수인 first와 second라는 이름의 두 구조 체를 선언한다. first와 second는 coord형으로 선언된 구조체형 변수이다. first는 x와 y라는 이름의 두 가지 정수형 멤버를 가지고, second도 first와 같은 멤버를 가짐. 구조체를 선언하는 첫 번째 방법은 구조체의 선언문과 정의를 결합시킨 형태이다. 두 번째 방법은 소스 코드의 서로 다른 부분에서 구조체를 정의하고 구조체형 변수를 선언하는 것이다. 그래서 다음과 같은 문장도 coord형의 두 변수를 선언한다.

  struct coord {
     int x;
     int y;
     };
  /* 그 밖의 프로그램 문장 */
  struct coord first, second;


1.2 구조체 멤버를 사용하는 방법
 : 개별적인 구조체 멤버는 동일한 형태의 일반적인 변수와 마찬가지 방법으로 사용될 수 있다. 구조체 멤버는 구조체 멤버 연산자(structure member operator)나 멤버 연산자(dot operator)라고 하는 마침표(.)를 사용하여 참조할 수 있다. 그래서 first라는 이름의 구조체가 좌표 x=50, y=100의 값으로 화면 위치를 표현하도록 하기 위해서는 다음과 같은 문장을 사용할 수 있을 것이다.

  first.x = 50;
  first.y = 100;

구조체 second에 저장된 화면의 좌표값을 출력하기 위해서는 다음과 같은 문장을 사용할 수 있다.

  printf("%d, %d", second.x, second.y);

여기서, 개별적인 변수 대신에 구조체를 사용하는 것이 어떤 장점을 제공하는지 의문을 가질 것이다. 한 가지 중요한 장점은 간단한 할당문으로 동일한 형태의 구조체 간에 모든 값을 복사할 수 있다는 것이다. 앞의 예에서 다음 문장은

  first = second;

다음과 같은 뜻을 가진다.

  first.x = second.x;
  first.y = second.y;

프로그램에서 많은 멤버를 가지는 복잡한 구조체를 사용할 때 이렇게 한 번의 할당문으로 값을 복사할 수 있다는 사실은 매우 효율적인 시간 절약 방법이다. 구조체의 다른 한 가지 장점은 약간 더 고급 기능을 배울 때 이해할 수 있을 것이다. 일반적으로, 여러 가지 형태의 변수를 동시에 다룰 필요가 있을 때에는 항상 구조체를 유용하게 사용할 수 있을 것이다. 예를 들어, 우편용 주소록 데이터베이스를 처리할 때 각각의 자료를 구조체로 만들어 하나의 자료에 포함되는 이름, 주소, 전화번호 등을 구조체 멤버로 취급할 수 있을 것이다.

2. 더욱 복잡한 구조체
지금까지 간단한 형태의 구조체에 대해서 알아보았으므로, 이제 더욱 흥미롭고 복잡한 형태의 구조체에 대해서 알아보자. 복잡한 형태의 구조체란 다른 어떤 구조체를 멤버로 가지거나 또는 배열을 멤버로 가지는 구조체를 말한다.

2.1 구조체를 가지는 구조체
: 앞에서도 설명했듯이, C의 구조체는 C에서 사용되는 모든 데이터형을 포함할 수 있다. 예를 들어, 구조체는 다른 어떤 구조체를 가질 수 있다. 앞에서 사용했던 예를 다시 보자. 그래픽 프로그램이 좌표뿐 아니라 사각형을 다루어야 한다고 가정해 보자. 사각형(rectangle)은 축을 중심으로 상반되는 위치에 있는 좌표로 표현할 수 있다. 앞에서는 이미 하나의 점을 표현하기 위해서 두 좌표값을 가지는 구조체를 정의하는 방법에 대해서 설명했다. 사각형을 표현하는 구조체를 정의하기 위해서는 두 개의 점을 표현하는 구조체가 필요할 것이다. coord형의 구조체가 이미 정의되어 있다고 가정하면 다음과 같은 구조체를 정의할 수 있을 것이다.

  struct rectangle {
     struct coord topleft;
     struct coord bottomrt;
  };

이 문장은 두 개의 coord형 구조체 변수를 가지는 rectangle이라는 구조체를 정의한다. 두 개의 coord형 구조체 변수는 topleft와 bottomrt라는 이름을 가진다. 앞의 에는 단지 rectangle형의 구조체를 정의한다. 실제로 구조체형 변수를 선언하기 위해서는 다음과 같은 문장을 사용해야 한다.

  struct rectangle mybox;

또한, 앞에서 coord형 구조체를 선언할 때와 마찬가지로 구조체 정의와 선언을 결합시킬 수 있을 것이다.

  struct rectangle{
      struct coord topleft;
      struct coord bottomrt;
   } mybox;

여기서 int형의 데이터를 참조하기 위해서는 멤버 연산자(.)를 두 번 사용해야 할 것이다. 그래서 다음 수식은

  mybox.topleft.x

mybox라는 이름의 rectangle형 구조체 변수에서 topleft라는 멤버 내의 x라는 멤버를 나타내는 것이다. 좌표가 (0, 10), (100, 200)인 사각형을 정의하기 위해서는 다음과 같은 문장을 작성할 것이다.

  mybox.topleft.x = 0;
  mybox.topleft.y = 10;
  mybox.bottomrt.x = 100;
  mybox.bottomrt.y = 200;

이 문장은 다소 혼란스러울 것이다. rectangle형 구조체와 여기에 포함된 두 개의 coord형 구조체, 각각의 coord형 구조체가 가지는 두 개의 int형 변수의 관계를 보여주는 <그림 11.1> 을 살펴보면 지금까지 설명한 내용을 쉽게 이해할 수 있을 것이다.

구조체는 앞의 예에서와 같은 이름을 가진다.


<그림 11.1> 구조체, 구조체 내의 구조체, 구조체 멤버 간의 관계를 보여주는 그림

이제, 구조체를 포함하는 구조체의 사용 예를 살펴보도록 하자. <리스트 11.1>에 있는 프로그램은 사각형의 좌표값을 읽어들이고, 사각형의 면적을 계산하여 출력한다. 프로그램의 앞 부분에 있는 주석문에서 설명되는 프로그램의 조건을 주의해서 살펴보자.

<리스트 11.1> 다른 구조체를 가지는 구조체의 사용 예

 /* 다른 구조체를 가지는 구조체의 사용 예 */


 /* 사각형의 구석 좌표를 읽어서 면적을 구한다.

    오른쪽 하단 구석의 y좌표가 왼쪽 상단 구석의 y좌표보다 크고,

    오른쪽 하단 구석의 x좌표가 왼쪽 상단 구석의 y좌표보다 크며,

    모든 좌표가 양수라고 가정한다. */


 #include <stdio.h>


 int length, width;

 long area;


 struct coord{

     int x;

     int y;

 };


 struct rectangle{

     struct coord topleft;

     struct coord bottomrt;

 } mybox;


 main()

 {

    /* 좌표 입력 */


    printf("\nEnter the top left x coordinate: ");

    scanf("%d", &mybox.topleft.x);


    printf("\nEnter the top left y coordinate: ");

    scanf("%d", &mybox.topleft.y);


    printf("\nEnter the bottom right x coordinate: ");

    scanf("%d", &mybox.bottomrt.x);


    printf("\nEnter the bottom right y coordinate: ");

    scanf("%d", &mybox.bottomrt.y);


    /* 길이와 높이 계산 */


    width = mybox.bottomrt.x - mybox.topleft.x;

    length = mybox.bottomrt.y - mybox.topleft.y;


    /* 면적 계산과 출력 */


    area = width * length;

    printf("\nThe area is %ld units.\n", area);


    return 0;

 }

C는 구조체의 종속 단계에 제한을 두지 않는다. 메모리가 허용하는 범위 내에서는 구조체를 가지는 구조체, 이런 구조체를 가지는 또다른 구조체등 몇 단계로 구성되는 구조체를 사용할 수 있다. 그러나 여러 단계의 종속된 구조체가 효율적이지 않은 경우도 있다. 대부분의 C 프로그램에서는 3단계 이상 종속된 구조체를 사용하지 않는다.

2.2 배열을 가지는 구조체
 : 또한, 하나 이상의 배열을 멤버로 포함하는 구조체를 정의할 수 있다. 이때 사용되는 배열은 int, char등 C의 어떤 데이터형이든지 될 수 있다. 예를 들어, 다음 문장은

  struct data{
     int x[4];
     char y[10];
  };

4개의 요소를 가지는 x라는 이름의 정수형 배열과 10개의 요소를 가지는 y라는 이름의 문자형 배열을 구조체 멤버로 포함하는 data형 구조체를 정의한다. 그리고 나서 다음과 같이 data형 구조체 변수 record를 선언할 수 있다.

  struct data record;

이 구조체의 상태가 <그림 11.2>에 나타나 있다. 그림에서 배열 x의 각 요소는 배열 y의 각 요소보다 2배나 많은 공간을 차지한다는 것을 주목하기 바란다. 이것은 int형이 대개 2바이트의 저장 영역을 요구하는 반면에 char형이 1바이트를 차지하기 때문이다.


<그림 11.2> 배열을 구조체 멤버로 가지는 구조체

구조체 멤버인 배열의 개별적인 요소를 사용하기 의해서는 멤버 연산자와 배열의 첨자를 함께 사용해야 한다.

   record.x[2] = 100;
   record.y[1] = 'x';

아마도 문자 배열은 문자열을 저장하기 위해서 가장 많이 사용된다는 것을 기억할 것이다. 또한 "포인터에 대해서"에서 설명했듯이 괄호를 포함함지 않는 배열의 이름은 배열에 대한 포인터를 뜻한다는 사실을 기억할 것이다. 이런 사실은 구조체 멤버로 사용되는 배열에도 적용되므로 다음 수식은

   record.y

구조체 record의 멤버인 배열 y[]의 첫 번째 요소에 대한 포인터이다. 그래서 다음과 같은 문장을 사용하여 y[]의 내용을 화면 상에 출력할 수 있을 것이다.

   puts(record.y);

이제 다른 예제를 살펴보자. <리스트 11.2>에 있는 프로그램은 float형 변수와 두 개의 char형 배열을 가지는 구조체를 사용하고 있다.

<리스트 11.2> 배열을 멤버로 가지는 구조체의 사용 예

 /* 배열을 멤버로 가지는 구조체 */


 #include <stdio.h>


 /* 데이터를 저장할 구조체 정의와 선언 구조체는 하나의 부동 소수형 변수와

    두 개의 문자형 배열을 가진다. */


 struct data{

    float amount;

    char fname[30];

    char lname[30];

 } rec;


 main(}

 {

    /* 키보드에서 데이터 입력 */


    printf("Enter the donor's first and last names,\n");

    printf("separated by a space: ");

    scanf("%s %s", rec.fname, rec.lname);


    printf("\nEnter the donation amount: ");

    scanf("%f", &rec.amount);


    /* 정보 출력 */

    /* 참고 : %2f는 부동 소수형 값을 소수점 이하 두 자리까지 출력하도록 지정한다. */

    /* 화면상에 데이터 출력 */


    printf("\nDonor %s %s gave $%.2f.\n", rec.fname, rec.lname, rec.amount);


    return 0;

 }

3. 구조체 배열
 : 배열을 멤버로 가지는 구조체를 사용할 수 있다면 구조체의 배열을 사용하는 것도 가능할까? 가능하다고 생각할 것이다. 실제로 구조체의 배열은 아주 강력한 프로그래밍 도구다. 구조체의 배열에 대해서 알아보도록 하자.
 앞에서는 프로그램에서 필요한 데이터형을 정의하기 위해서 구조체를 사용할 수 있다고 설명했다. 프로그램은 대부분의 경우 여러 개의 데이터를 사용한다. 예를 들어, 전화번호부를 관리하는 프로그램에서는 사람의 이름과 전화번호를 저장하기 위해서 구조체를 정의할 수 있다.

   struct entry{
      char fname[10];
      char lname[12];
      char phone[8];
   };

그러나 전화번호부는 여러 사람의 데이터를 포함하는 것이므로 entry 구조체형 변수를 한 번만 선언하는 것은 유용하지 않을 것이다. 여기서 필요한 것은 entry형 구조체의 배열이다. 그래서 구조체를 정의하고 나면 다음과 같이 구조체의 배열을 선언할 수 있다.

   struct entry list[1000];

이 문장은 1,000개의 요소를 가지는 list라는 이름의 배열을 선언한다. 모든 요소는 entry형 구조체 변수이고, 각 요소는 일반적인 배열 요소와 마찬가지로 첨자에 의해서 구분된다. 배열 요소인 각각의 구조체는 세 개의 char형 배열을 구조체 멤버로 포함하고 있다. 구조체의 배열을 선언할 때에는 다양한 방법으로 데이터를 다룰 수 있다. 예를 들어, 한 배열 요소의 데이터를 다른 배열 요소에 할당하기 위해서 다음과 같이 할 수 있을 것이다.

   list[1] = list[5];

이 문장은 list[5]의 각 멤버에 저장된 값을 구조체 list[1]에서 대응하는 멤버에 할당한다. 또한, 개별적인 구조체의 멤버들 간에 데이터를 이동시킬 수도 있다. 다음 문장은

   strcpy(list[1].phone, list[5].phone);

list[5].phone에 저장된 문자열을 list[1].phone에 복사하는 것이다. 라이브러리 함수 strcpy()는 한 문자열을 다른 문자열에 복사한다. 자세한 내용은 "문자열 다루기"에서 상세히 설명하겠다. 또한, 필요하다면 구조체 멤버의 하나인 배열의 개별적인 요소들 간에도 데이터를 이동시킬 수 있다.

   list[5].phone[1] = list[2].phone[3];

이 문장은 list[5]에서 전화번호 값을 저장하는 배열의 두 번째 위치에 list[2]에서 전화번호값을 저장하는 배열의 네 번째 문자를 할당한다. 첨자는 항상 0에서부터 시작한다는 것을 잊지 않도록 하자.

<리스트 11.3>에 있는 프로그램은 구조체 배열의 사용 예를 보여준다. 또한, 이 프로그램은 배열을 멤버로 가지는 구조체의 배열을 사용하고 있다.

    <리스트 11.3> 

 /* 구조체의 배열 사용 예 */


 #include <stdio.h>


 /* 항목을 저장할 구조체 정의 */


 struct entry{

    char fname[20];

    char lname[20];

    char phone[10];

 };


 /* 구조체의 배열 선언 */


 struct entry list[4];


 int i;


 main()

 {

    /* 네 명의 데이터 입력 */


    for(i = 0; i < 4; i++)

    {

       printf:\nEnter first name: ");

       scanf("%s", list[i].fname);

       printf("Enter last name: ")"

       scanf("%s", list[i].lname);

       printf("Enter phone is 123-4567 format: ");

       scanf("%s", list[i].phone);

    }

    /* 두 개의 빈줄 출력 */

    printf("\n\n");


    /* 데이터 출력 */


    for(i = 0; i < 4; i++)

    {

       printf("Name: %s %s", list[i].fname, list[i].lname);

       printf("\t\tPhone: %s\n", list[i].phone);

    }

    return 0;

 }

<리스트 11.3>에서 사용된 프로그래밍 방식에 익숙해지도록 하자. 많은 프로그래밍 작업에서는 배열을 멤버로 가지는 구조체의 배열을 사용할 필요가 있을 것이다.

4. 구조체의 초기화
 : C의 다른 변수형과 마찬가지로 구조체를 선언하는 동시에 초기화할 수 있다. 수행 과정은 배열을 초기화하는 것과 비슷하다. 구조체 선언문에는 등호와 함께 쉼표에 의해서 구분되고, 중괄호에 포함되는 초기화값의 목록이 나타난다. 예를 들어, 다음 예제를 살펴보자.

   struct sale {
      char customer[20];
      char item[20];
      float amount;
   } mysale = {"Acme Industries",
               "Left-handed widget",
                1000.00
              };

 이 문장이 실행되면 다음과 같은 동작이 수행된다.

① 1번째 줄부터 5번째 줄까지는 sale이라는 이름의 구조체형을 정의한다.
② 5번째 줄에서는 mysale이라는 이름의 sale형 구조체 변수가 선언된다.
③ 5번째 줄에서는 구조체 멤버 mysale.customer를 문자열 'Acme Industries'로 초기화한다.
④ 6번째 줄에서는 구조체 멤버 mysale.item을 문자열 'Left-handed widget'으로 초기화한다.
⑤ 7번째 줄에서는 구조체 멤버 mysale.amount를 1000.00의 값으로 초기화한다.

구조체를 멤버로 가지는 구조체의 경우에는 순서대로 초기화값을 나열하면 된다. 초기화값은 구조체를 정의하는 문장에서 멤버가 나타나 있는 순서대로 구조체 멤버에 할당된다. 다음은 앞의 예제를 약간 보충한 다른 하나의 예제이다.

   struct customer {
      char firm[20];
      char contact[25];
   }

   struct sale {
      struct customer buyer;
      char item[20];
      float amount;
   } mysale = {{"Acme Industries", "George Adams"),
                "Left-handed widget",
                 1000.00
               };

이 예제는 다음과 같은 초기화를 수행한다.

① 10번째 줄에서 구조체 멤버 mysale.buyer.firm은 문자열 'Acme Industries'로 초기화된다.
② 10번째 줄에서 구조체 멤버 mysale.buyer.contact는 문자열 'George Adams'로 초기화된다.
③ 11번째 줄에서 구조체 멤버 mysale.item은 문자열 'Left-handed widget'으로 초기화된다.
④ 12번째 줄에서 구조체 멤버 mysale.amount는 1000.00의 값으로 초기화된다.

또한, 구조체의 배열을 초기화할 수 있다. 입력되는 초기화값은 배열에 포함되어 있는 구조체에 순서대로 할당된다. 예를 들어, sale형 구조체의 배열을 선언하고 나서 처음 두 배열 요소인 두 개의 구조체를 초기화하기 위해서는 다음 예제와 같이 할 수 있을 것이다.

  struct customer {
     char firm[20];
     char contact[25];
     };

  struct sale {
     struct customer buyer;
     char item[20];
     float amount;
     };

  struct sale y1990[100] = {
     {{"Acme Industries", "George Adams"},
       "Left-handed widget",
       1000.00
      }

     {{"Wilson & Co.", "Ed Wilson"},
       "Type 12 gizmo",
        290.00
      }
     };

이 코드에서는 다음과 같은 동작이 수행된다.

① 14번째 줄에서 구조체 멤버 y1990[0].buyer.firm은 문자열 'Acme Industries'로 초기화된다.
② 14번째 줄에서 구조체 멤버 y1990[0].buyer.contact는 문자열 'George Adams'로 초기화됨.
③ 15번째 줄에서 구조체 멤버 y1990[0].item은 문자열 'Left-handed widget'으로 초기화된다.
④ 16번째 줄에서 구조체 멤버 y1990[0].amount는 1000.00의 값으로 초기화된다.
⑤ 18번째 줄에서 구조체 멤버 y1990[1].buyer.firm은 문자열 'wilson & Co.'로 초기화된다.
⑥ 18번째 줄에서 구조체 멤버 y1990[1].buyer.contact는 문자열 'Ed Wilson'으로 초기화된다.
⑦ 19번째 줄에서 구조체 멤버 y1990[1].item은 문자열 'Type 12 gizmo'로 초기화된다.
⑧ 20번째 줄에서 구조체 멤버 y1990[1].amount는 290.00의 값으로 초기화된다.

5. 구조체와 포인터
 : 포인터가 C에서 아주 중요한 부분을 차지한다는 사실을 인식한다면 구조체와 함께 사용할 수 있다는 사실도 쉽게 짐작할 수 있을 것이다. 포인터를 구조체의 멤버로 사용할 수 있으며, 구조체에 대한 포인터를 선언할 수도 있다. 이런 내용들을 하나씩 살펴보도록 하자.

5.1 구조체 멤버로 사용되는 포인터
 : 포인터를 구조체 멤버로 사용하면 더욱 융통성 있는 프로그램을 작성할 수 있다. 포인터는 구조체 멤버로 사용될 때 일반적인 경우와 동일한 방법으로 선언된다. 즉, 간접 연산자(*)를 사용하여 선언된다. 다음은 예이다.

   struct data {
      int *value;
      int *rate;
      } first;

이 문장은 int형에 대한 두 개의 포인터를 멤버로 가지는 구조체를 정의하고 선언한다. 다른 모든 포인터에서와 마찬가지로 포인터를 선언하는 것만으로 사용할 수는 없다. 포인터에는 변수의 주소를 할당하여 포인터가 어떤 영역을 지적하도록 초기화해야 한다. 만약 cost와 interest가 int형 변수로 선언되어 있다면 다음과 같은 문장을 작성할 수 있을 것이다.

   first.value = &cost;
   first.rate = &interest;

이제 포인터가 초기화되었으므로 간접 연산자(*)를 사용할 수 있다. 수식 *first.value는 cost에 저장된 값을 뜻하고, 수식 *first.rate는 interest에 저장된 값을 뜻한다. 구조체 멤버로 가장 많이 사용되는 포인터는 아마도 char형에 대한 포인터일 것이다. 앞 강의 "문자와 문자열"에서는 '첫 번째 문자를 지적하는 포인터와 마지막을 표시하는 널 문자 사이의 일련의 문자로 문자열이 구성된다'는 것을 설명했었다. 메모리를 효율적으로 사용하기 위해서는 다음과 같이 char형에 대한 포인터를 선언하고 문자열을 지적하도록 초기화해야 한다.

   char *p_message;
   p_message = "Teach Yourself C in 21Days";

또한, char형에 대한 포인터를 멤버로 가지는 구조체를 사용하여 동일한 결과를 얻을 수 있다.

   struct msg {
      char *p1;
      char *p2;
      } myptrs;
   myptrs.p1 = "Teach yourself C in 21Days";
   myptrs.p2 = "By SAMS Publishing";

구조체 멤버로 포함되어 있는 포인터는 일반적인 포인터를 사용할 수 잇는 어떤 곳에서든지 사용할 수 있다. 예를 들어, 포인터가 지적하는 문자열을 출력하기 위해서는 다음과 같은 문장을 사용할 수 있을 것이다.

   printf("%s %s", myptrs.p1, myptrs.p2);

구조체 멤버로 char형의 배열을 사용하는 것과 char형에 대한 포인터를 사용하는 것의 차이점은 무엇일가? 두 가지 모두 구조체에 문자열을 '저장'하기 위한 방법이다. 다음은 두 가지 방법을 모두 사용하는 구조체 msg이다.

   struct msg(
       char p1[30];
       char *p2;
       } myptrs;

괄호를 포함하지 않는 배열의 이름은 배열의 처 번째 요소에 대한 포인터라는 것을 기억하자 그래서 앞의 구조체 멤버는 다음과 같이 사용할 수 있다.

   strcpy(myptrs.p1, "Teach Yourself C In 21 Days");
   strcpy(myptrs.p2, "By SAMS Publishing");
   /* 그 밖의 프로그램 문장 */
   puts(myptrs.p1);
   puts(myptrs.p2);

두 가지 방법의 실제 차이점은 무엇일까? 만약 구조체 멤버로 char형 배열을 가지는 구조체를 정의하면, 이 구조체형으로 선언되는 모든 변수는 일정한 크기의 배열만을 저장할 수 있는 저장 영역을 가지게 된다. 또한, 저장 영역의 크기는 제한되고 구조체에는 지정된 것보다 긴 문자열을 저장할 수 없다. 다음은 사용 예이다.

   struct msg {
      char p1[10];
      char p2[10];
      } myptrs;
      …
   strcpy(p1, "Minneapolis");  /* 문자열이 배열보다 길다. */
   strcpy(p2, "MN");          /* 문자열이 배열보다 잛지만 저장 영역이 낭비되고 있다. */

반면에, char형에 대한 포인터를 가지는 구조체를 정의하면 이런 제한은 적용되지 않는다. 각각의 구조체 변수는 포인터를 저장하기 위한 저장 영역만을 가지게 된다. 실제 문자열은 메모리 내의 다른 어떤 곳에 저장되지만 메모리의 위치에 대해서는 신경 쓸 필요가 없다. 이렇게 포인터를 사용하면 문자열의 길이에 제한이 없고 저장 영역이 낭비되지 않는다. 실제 문자열은 구조체의 일부분이 아닌 상태로 메모리의 특정 부분에 저장된다. 구조체에 포함되는 각 포인터는 어떤 길이의 문자열도 지적할 수 있다. 실제 문자열이 구조체에 저장되는 것은 아니지만 구조체의 일부분과 관련된다.

5.2 구조체에 대한 포인터
: C 프로그램에서는 다른 어떤 데이터형에 대한 포인터와 마찬가지로 구조체에 대한 포인터 를 선언하고 사용할 수 있다. 이 장에서 나중에 설명할 것처럼 구조체에 대한 포인터는 함수 의 인수로 구조체를 전달할 때 가끔 사용된다. 또한, 구조체에 대한 포인터는 링크드 리스트 (linked lists)라고 알려져 있는 매우 강력한 데이터 저장 방법에서 사용된다. 링크드 리스트는 나중에 "포인터 : 고급 기능들"에서 설명할 것이다.
 여기서는 프로그램에서 구조체에 대한 포인터를 생성하고 사용하는 방법에 대해서 알아보도록 하자. 우선, 구조체를 정의하겠다.

   struct part {
      int number;
      char name[10];
      };

이제 part형 구조체에 대한 포인터를 선언한다.

   struct part *p_part;

선언문에서 사용되는 간접 연산자(*)는 p_part가 part형 구조체 변수가 아니라 part형 구조체 에 대한 포인터라는 사실을 알려준다는 것을 기억하기 바란다. 이제 포인터를 초기화할 수 있을까? 그렇지 않다. 구조체 part는 정의되었지만 어떤 구조체 변수도 선언되지 않았다. 앞의 문장은 구조체 변수 선언문이 아니라 구조체에 대한 포인터 선언문이라는 것을 기억하자. 이제 데이터를 저장하기 위해서 메모리 내의 저장 영역을 보존 하는 변수 선언문이 필요하다. 포인터가 어떤 주소를 지적하도록 하기 위해서는 우선 part형 구조체 변수를 선언해야 한다. 다음은 선언문이다.

   struct part gizmo;

이제 포인터를 초기화할 수 있다.

   p_part = &gizmo;

이 문장은 gizmo의 주소값을 p_part에 저장한다.

<그림 11.5>에는 구조체와 구조체에 대한 포인터의 관계가 나타나 있다.


<그림 11.5> 구조체에 대한 포인터는 구조체의 첫 번째 바이트를 지적한다.

이제, 구조체 gizmo에 대한 포인터를 생성했다. 이 포인터를 어떻게 사용할 것인가? 한 가지 방법은 간접 연산자(*)를 이용하는 것이다. 이런 사실을 여기에 적용하면 p_part는 구조체 gizmo에 대한 포인터이고, *p_part는 gizmo 자체를 뜻한다는 것을 알 수 있다. 이제 gizmo의 각 멤버를 사용하기 위해서는 구조체 멤버 연산자(.)를 사용하자. gizmo.number에 100의 값을 저장하기 위해서는 다음과 같은 문장을 사용할 수 있을 것이다.

   (*p_part).number = 100;

여기서 멤버 연산자(.)는 간접 연산자(*)보다 높은 우선 순위를 가지고 있으므로 *p_part는 괄호 내에 포함되어야 한다. 구조체에 대한 포인터를 사용하여 구조체 멤버를 참조하는 두 번째 방법은 하이픈(-)과 부등호(>)로 구성되는 간접 멤버 참조 연산자(indirection membership operator)를 사용하는 것이다. 하이픈과 부등호를 함께 사용하면 C에서 하나의 새로운 연산자로 간주된다는 것을 기억하기 바란다. 이 연산자는 포인터 이름과 멤버 이름 사이에 위치된다. p_part 포인터를 사용하여 gizmo의 멤버인 number를 참조하기 위해서는 다음과 같은 문장을 사용할 수 있다.

   p_part -> number

다른 하나의 예를 살펴보자. 만약 str이 구조체이고, p_str이 str에 대한 포인터이며, memb가 str의 멤버라면 다음과 같은 문장을 통해서 str.memb를 사용할 수 있을 것이다.

   p_str->memb

그래서 전체적으로 구조체 멤버를 참조하는 세 가지 방법이 있음을 알 수 있다.

·구조체의 이름을 사용한다.
·간접 연산자(*)와 구조체에 대한 포인터를 함께 사용한다.
·간접 멤버 참조 연산자(->)와 구조체에 대한 포인터를 함께 사용한다.

p_str이 구조체 str에 대한 포인터라면 다음의 세 가지 수식은 모두 동일한 것이다.

   str.memb
   (*p-str).memb
   p_str->memb

5.3 포인터와 구조체의 배열
: 앞에서는 구조체의 배열과 구조체에 대한 포인터가 매우 강력한 프로그래밍 도구라는 것을 설명했다. 또한, 구조체를 참조하는 포인터를 배열 요소로 사용해서 두 가지를 결합시킬 수도 있다. 앞에서 예제로 사용했던 구조체를 다시 한 번 살펴보도록 하자.

   struct part{
      int number;
      char name[10];
      };

구조체 part가 정의되면 part형 배열을 선언할 수 있다.

   struct part data[100];

다음으로 part형에 대한 포인터를 선언하고, 포인터가 배열 data의 첫 번째 구조체를 지적하도록 초기화할 수 있다.

   struct part *p_part;
   p_part = &data[0];

괄호를 포함하지 않는 배열의 이름이 배열의 첫 번째 요소에 대한 포인터라는 것을 기억하자 그래서 두 번째 문장은 다음과 같이 작성할 수도 있을 것이다.

   p_part = data;

이제 part형 구조체의 배열과 배열의 첫 번째 요소, 즉 배열의 첫 번째 구조체에 대한 포인터를 생성했다. 다음과 같은 문장을 사용하여 첫 번째 요소의 내용을 출력할 수 있을 것이다.

   printf("%d %s", p_part->number, p_part->name);

만약 배열의 모든 요소를 출력하기 원한다면 어떻게 해야 할까? 아마도 순환문이 한 번 반복될 때마다 배열의 요소를 하나씩 출력하는 for문을 사용할 것이다. 포인터를 사용하여 구조체 멤버를 참조하기 위해서는 순환문이 반복될 때마다 포인터 p_part가 배열의 다음 요소, 즉 배열의 다음 구조체를 지적하도록 변경해 주어야 한다. 어떻게 해야 여기서 의도 하는 대로할 수 있을까? 여기서는 C의 포인터 연산을 사용할 필요가 있다. 단항 증가 연산자(++)는 포인터와 함께 사용될 때 특별한 의미를 가지게 된다. 즉, '포인터가 지적하는 데이터형의 크기를 반영하여 포인터의 값을 증가시켜라'는 것을 뜻한다. 예를 들어, obj형의 변수를 지적하는 포인터 ptr이 있다면 다음 문장은

   ptr++;

다음과 같은 뜻을 가진다.

   ptr += sizeof(obj);

포인터 연산의 이런 특징은 배열에서 특히 유용하게 사용된다. 배열의 각 요소는 메모리에 순서대로 저장된다. 포인터가 배열의 요소를 지적하고 있을 때 증가(++) 연산자를 사용하면 포인터는 n + 1번째의 요소를 지적하게 된다. <그림 11.6>에서는 4바이트 크기의 요소로 구성되는 x[]라는 이름의 배열을 예로 들어 이런 사실을 설명한다. 그림에서는 2바이트 길이의 두 int형 변수를 멤버로 가지는 구조체가 사용되고 있다. 포인터 ptr은 x[0]을 지적하도록 초기화되어 있고 ptr의 값이 증가될 때마다 배열의 다음 요소를 지적한다.


<그림 11.6> 포인터의 값이 증가될 때 포인터는 배열의 다음 요소를 '지적'한다.

결국, 프로그램에서 포인터를 증가시켜 구조체의 배열이나 다른 어떤 데이터형의 배열을 순서대로 사용할 수 있다는 사실을 알 수 있다. 이 방법은 같은 작업을 수행하기 위해서 배열의 첨자를 사용하는 것보다 쉽고 간단하다. <리스트 11.4>에 있는 프로그램은 지금까지 설명한 내용을 예제로 보여준다.

<리스트 11.4> 포인터를 증가시켜 배열의 요소를 순서대로 사용하는 프로그램

 /* 포인터 표기를 사용하여 구조체의 배열을 차례대로 사용하는 예 */


 #include <stdio.h>


 #define MAX 4


 /* 구조체를 정의 후 4구조체의 배열을 선언하고 초기화한다. */


 struct part {

    int number;

    char name[10];

 } data[MAX] = {1, "Smith", 2, "Jones", 3, "Adams", 4, "Wilson"};


 /* part형에 대한 포인터와 카운터 변수 선언 */


 struct part *p_part;

 int count;


 main()

 {

    /* 첫 배열 요소에 대한 포인터 초기화 */


    p_part = data;


    /* 순환할 때마다 포인터를 증가시키며 배열을 사용한다. */


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

    {

       printf("\nAt address %d: %d %s", p_part, p_part->number, p_part->name);

       p_part++;

    }


    return 0;

 }

출력되는 주소값을 자세히 살펴보자. 실제 값은 시스템마다 달라질 수 있지만 간격은 일정하게 증가할 것이다. 이 간격은 구조체 part의 크기로 대부분의 시스템에서는 12가 될 것이다. 이 프로그램은 포인터를 증가시킬 때 포인터가 지적하는 변수의 크기를 반영하여 주소값이 증가된다는 사실을 분명히 보여준다.

5.4 함수의 인수로 구조체를 전달하는
 : 다른 데이터형과 마찬가지로 함수의 인수로 구조체를 전달할 수도 있다. <리스트 11.5>에 있는 프로그램은 이런 사실을 증명한다. 이 프로그램은 <리스트 11.2>에 있는 프로그램을 수정한 것이다. 여기에서는 화면 상에 데이터를 출력하는 함수를 사용하는 반면에, <리스트 11.2>는 main()의 일부분에 해당하는 문장에서 데이터를 출력한다.

<리스트 11.5> 함수의 인수로 구조체를 전달하는 프로그램

 /* 함수에 구조체를 전달하는 예 */


 #include <stdio.h>


 /* 데이터를 저장할 구조체 선언과 정의 */


 struct data {

    float amount;

    char fname[30];

    char lname[30];

 } rec;


 /* 함수 원형. 함수는 복귀값이 없고 하나의 인수로 data형의 구조체를 받아들인다. */


 void print_rec(struct data x);


 main()

 {

    /* 키보드에서 데이터 입력 */


    printf("Enter the donor's first and last name,\n");

    printf("separated by a space: ");

    scanf("%s %s", rec.fname, rec.lname);


    printf("\nEnter the donation amount: ");

    scanf("%f", &rec.amount);


    /* 출력 함수 호출 */

    print_rec( rec );


    return 0;

 }


 void print_rec(struct data x)

 {

    printf("\nDonor %s %s gave $%.2f.\n", x.fname, x.lname, x.amount);

 }

구조체의 주소, 즉 구조체에 대한 포인터를 인수로 사용하여 함수에 구조체를 전달할 수도 있다. 이전에는 C에서 구조체를 인수로 사용하는 것이 유일한 방법이었다. 이제는 이렇게 구조체를 직접 전달하지 않지만, 아직까지도 가끔 구조체를 전달하는 경우를 볼 수 있다. 구조체에 대한 포인터를 인수로 전달하면 함수에서는 구조체 멤버를 참조하기 위해서 간접 멤버 참조 연산자(->)를 사용해야 한다는 것을 기억하기 바란다.

6. 공용체
: 공용체(unions)는 구조체와 비슷하다. 공용체는 구조체와 같은 방법으로 선언되고 사용된다. 공용체는 한 번에 하나의 멤버만이 사용될 수 있다는 점에서 구조체와 다르다. 그 이유는 간단하다. 공용체의 모든 멤버는 메모리에서 같은 영역을 차지하고 있다. 모든 멤버는 겹쳐져 있는 셈이다.

6.1 공용체 정의와 선언, 그리고 초기화
: 공용체는 구조체와 같은 방법으로 정의되고 선언된다. 선언문에서 유일한 차이점은 키워드 struct 대신에 union이 사용된다는 점이다. char 변수와 정수형 변수의 간단한 공용체를 정의하기 위해 다음과 같이 작성할 수 있다.

   union shared {
      char c;
      int i;
      };

이 공용체 shared는 문자값 c나 정수값 i의 하나를 가질 수 있는 공용체 변수(instance)를 생성하는 데 사용될 수 있다. 이것은 OR 조건문과도 같다. 두 값을 모두 가지게 되는 구조체와 달리 공용체는 한 번에 하나의 값만을 가질 수 있다.

<그림 11.7>은 shared 공용체가 메모리에 저장되는 방법을 보여준다.


<그림 11.7> 공용체는 한 번에 하나의 값을 가질 수 있다.

공용체를 선언하는 동시에 초기화할 수도 있다. 그러나 한 번에 하나의 멤버만이 사용될 수 있으므로 하나만 초기화될 수 있다. 일반적으로, 혼란을 막기 위해서 공용체의 첫 멤버만이 초기화될 수 있다 .다음 코드는 shared 공용체형 변수가 선언되고 초기화되는 것을 보여준다.

   union shared generic_variable = ('@'};

6.2 공용체 멤버 사용하기
 : 개별적인 공용체 멤버는 구조체 멤버와 마찬가지로 멤버 연산자인 마침표(.)를 이용해서 사용할 수 있다 .그러나 공용체 멤버를 사용할 때에는 중요한 한 가지 차이점이 있다. 한 번에 하나의 공용체 멤버만이 사용되어야 한다는 것이다. 공용체는 서로 겹쳐져 있으므로 한 번에 하나의 멤버를 사용하는 것은 매운 중요하다. <리스트 11.6>은 예제를 보여준다.

<리스트 11.6> 공용체의 잘못된 사용 예

 /* 한번에 하나 이상의 공용체 멤버 사용 */

 #include <stdio.h>


 main()

 {

    union shared_tag {

       char c;

       int i;

       long l;

       float f;

       double d;

    } shared;


    shared.c = '$';


    printf("\nchar c = %c", shared.c);

    printf("\nint i = %d", shared.i);

    printf("\nlong l = %ld", shared.l);

    printf("\nfloat f = %f", shared.f);

    printf("\ndouble d = %f", shared.d);


    shared.d = 123456789.8765;


    printf("\nchar c = %c", shared.c);

    printf("\nint i = %d", shared.i);

    printf("\nlong l = %ld", shared.l);

    printf("\nfloat f = %f", shared.f);

    printf("\ndouble d = %f", shared.d);


    return 0;

 }

7. typedef와 구조체
 : 구조체를 정의할 때에는 동의어인 typedef 키워드를 사용할 수 있다. 예를 들어, 다음 문장은 구조체에 대한 동의어로 coord를 사용할 수 있도록 정의해준다.

   typedef struct {
      int x;
      int y;
      } coord;

이제 coord를 사용하여 구조체형의 변수를 선언할 수 있다.

   coord topleft, bottomright;

typedef는 이 장의 앞부분에서 설명한 구조체 태그와는 다르다는 것을 주의하자. 만약 다음과 같은 구조체를 정의하면

   struct coord {
       int x;
       int y;
       };

coord는 구조체의 태그로 사용된 것이다. 구조체 변수를 선언하기 위해서 이런 태그를 사용할 수 있지만 typedef와 달리 반드시 struct 키워드를 포함시켜야 한다

   struct coord topleft, bottomright;

구조체를 선언할 때 typedef를 사용해야 하는지 구조체 태그를 사용해야 하는지의 여부는 결정하기 어려운 문제이다. typedef를 사용하는 경우에는 struct 키워드가 필요하지 않으므로 더 간결하다고 볼 수 있다. 반면에, 태그를 사용하고 struct 키워드를 포함시키면 구조체라는 사실을 분명히 알 수 있어 편리하다.

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

9장 포인터에 대해서  (0) 2019.06.02
10장 문자와 문자열  (0) 2019.06.02
12장 변수의 범위  (0) 2019.06.02
13 장 고급 프로그램 제어문  (0) 2019.06.02
14장 화면, 프린터, 키보드 사용하기  (0) 2019.06.02

함수 내에서 선언된 변수가 함수의 밖에서 선언된 변수와 분명히 구분된다는 것을 설명했다. 이런 사실은 C 프로그래밍에서 중요한 변수의 범위(variable scope)를 부분적으로 설명하는 예이다.

·변수의 범위
·외부 변수의 의미와 외부 변수를 사용하지 않아야 하는 이유
·지역 변수에 대해서
·정적 변수와 자동 변수의 차이
·지역 변수와 블록에 대해서
·적절한 변수의 형태를 선택하는 방법

1. 변수의 범위란?
 : 변수의 범위(scope)는 프로그램에서 변수를 사용할 수 있는 범위나 또는 변수가 프로그램 내에서 효과를 나타낼 수 있는(visible) 범위를 말한다. C에서 변수에 대해서 설명할 때에는 변수의 값을 사용할 수 있는 가능성(accessibility)과 주어진 범위 내에서 효과를 나타내는 것을 뜻하는 유효성(visibility)을 함께 사용한다. 변수의 범위를 언급할 대 변수(variable)는 C의 모든 데이터형을 뜻한다. 즉, 간단한 변수에서부터 배열, 구조체, 포인터 등을 모두 포함한다. 또한, const 키워드로 정의된 기호 상수도 포함한다. 변수의 범위는 변수의 생명, 즉 메모리에서 변수의 값이 보존되는 기간이나 변수에 저장 영역이 할당되고 해제되는 시기에 영향을 준다. 우선, 변수의 유효성(visibility)에 대해서 알아보도록 하자.

1.1 변수의 범위를 설명하는 예제
 : <리스트 12.1>에 있는 프로그램을 살펴보자. 프로그램은 5번째 줄에서 변수 x를 정의하고, 11번째 줄에서 x의 값을 출력하기 위해서 printf() 함수를 호출하며 다시 x의 값을 출력하기 위해 printf_value()를 호출한다. 함수 print_value()에는 x의 값이 인수로 전달되지 않는다는 것에 주의하기 바란다. 이 함수는 19번째 줄에서 printf() 함수에 대한 인수로 x를 사용하고 있다.

<리스트 12.1> 변수 x는 함수 print_value내에서도 사용될 수 있다.

 /* 변수의 범위를 설명하는 에제 */


 #include <stdio.h>


 int x = 999;


 void print_value(void);


 main()

 {

    printf("%d\n", x);

    print_value();


    return 0;

 }


 void print_value(void)

 {

    printf("%d\n", x);

 }

-> 출 력
  999
  999

이 프로그램은 아무런 문제 없이 컴파일되고 실행된다. 이제 이 프로그램에서 x를 정의하는 문장을 main() 함수 내의 위치로 이동시켜 보자. 변경된 소스 코드는 <리스트 12.2>에 나타나 있다.

<리스트 12.2> 변수 x는 함수 print_value 내에서 사용될 수 없다.

 /* 변수의 범위를 설명하는 예제 */


 #include <stdio.h>


 void print_value(void);


 main()

 {

    int x = 999;


    printf("%d\n", x);

    print_value();


    return 0;

 }


 void print_value(void)

 {

    printf("%d\n", x);

 }

<리스트 12.2>를 컴파일하면 컴파일러는 다음과 비슷한 에러 메시지를 출력할 것이다.

   list1202.c[19] : Error: undefined identifier 'x'.

에러 메시지에서 괄호 내에 나타나는 숫자는 에러가 발생한 프로그램의 문장 번호를 뜻한다. 19번째 줄은 print_value() 함수 내에서 printf() 함수를 호출하는 문장이다. 에러 메시지는 print_value() 함수 내에서 변수 x가 '정의되지 않았다.'는 것을 알려준다. 즉, 함수 내에서는 변수 x가 유효하지 않다. 그러나 11번째 줄에 있는 printf()함수에서는 에러 메시지가 발생하지 않는다는 것에 주목할 필요가 있다. 프로그램의 11번째 줄에서는 변수 x가 유효한 것이다.

<리스트 12.1>과 <리스트 12.2>의 유일한 차이는 변수 x가 정의된 위치이다. x를 정의하는 문장을 이동시키면 변수의 범위가 바뀐다. <리스트 12.1>에서 x는 외부(external) 변수로 선언되었고, 이때 변수의 유효 범위는 전체 프로그램이다. 변수 x는 main() 함수와 print_value() 함수에서 모두 사용할 수 있다. 그러나 <리스트 12.2>에서 x는 지역(local) 변수로 선언되었고, 이때 변수의 유효 범위는 main() 함수로 제한된다. 그래서 함수 print_value()에서는 x가 존재하지 않는 것으로 간주된다 지역 변수와 전역 변수에 대해서는 나중에 상세히 설명할 것이므로 여기서는 변수 범위의 중요성을 알아둘 필요가 있다.

1.2 변수의 범위가 중요한 이유
 : 변수의 범위가 중요하다는 것을 이해하기 위해서는 구조화 프로그래밍에 대해서 다시 한 번 언급할 필요가 있다. 구조화 프로그래밍에서는 특정 작업을 수행하는 독립된 함수로 프로그램을 분할한다. 여기서 중요한 것은 독립적(independent)이라는 단어이다. 완전히 독립된 함수를 작성하기 위해서는 함수 내의 변수가 다른 함수에 의해서 간섭되지 않아야 한다. 함수 내에서 독립된 변수를 사용하면 프로그램의 다른 부분에 영향을 받지 않고 주어진 작업을 정상적인 상태로 수행하는 함수를 작성할 수 있다. 여기서, 함수들 간에 완전히 독립된 데이터를 사용하는 것이 항상 바람직하다는 것을 짐작할 수 있을 것이다. 사용되는 변수의 범위를 지정하면 함수들간에 독립된 데이터를 사용할 수 있다. 변수의 범위를 지정하는 것은 이처럼 함수의 독립성에 영향을 준다.

2. 외부 변수
 : 외부(external) 변수는 어떤 함수의 바깥에서 정의되는 것이다. main() 함수도 하나의 함수이므로 외부 변수는 main()의 박에서 선언되는 것을 포함한다. 외부 변수는 가금 전역(global) 변수라고도 한다.

2.1 외부 변수의 범위
 : 외부 변수의 범위는 전체 프로그램이다. 그래서 외부 변수는 main()이나 프로그램 내의 다른 모든 함수에서 사용될 수 있다. 예를 들어, <리스트 12.1>에 있는 변수 x는 외부 변수다. 프로그램을 컴파일하고 실행하여 살펴보았듯이 x는 두 개의 함수 main()과 print_value()에서 유효하다. 그러나 정확히 말해서 외부 변수의 유효 범위가 전체 프로그램이라는 사실은 잘못된 것이다. 외부 변수의 범위가 변수를 정의하는 전체 소스 코드 파일이라는 표현이 더 정학하다. 전체 프로그램이 하나의 소스 코드 파일로 구성된다면 두 가지 범위는 같다. 대부분의 간단한 C 프로그램은 하나의 파일로 구성되고 프로그램도 이렇게 하나의 파일로 구성된다.

2.2 외부 변수를 사용하는 시기
 : 몇 가지 예제 프로그램에서 외부 변수를 사용하고 있지만 실제로는 외부 변수를 사용하지 않도록 해야 한다. 그 이유는 무엇일까? 외부 변수를 사용하면 구조화 프로그래밍에서 중심이 되는 모듈의 독립성(modular independence)이 사라지게 된다. 모듈의 독립성은 프로그램을 구성하는 각각의 함수나 모듈이 주어진 작업을 수행하기 위해서 필요로 하는 모든 코드와 데이터를 내부에 포함하고 있는 것을 뜻한다. 지금까지 사용된 간단한 프로그램에서는 이런 모듈의 독립성이 중요하지 않게 생각될 수도 있지만, 더 크고 복잡한 프로그램을 작성하게 되면 외부 변수에 대한 함수의 의존성은 중요한 문제가 될 수 있다. 그렇다면 외부 변수는 어떤 경우에 사용해야 할까? 프로그램을 구성하는 대부분의 함수나 또는 모든 함수가 사용해야 하는 변수를 선언하는 경우에만 외부 변수를 사용하는 것이 좋다 const 키워드를 사용하여 정의되는 기호 상수는 외부 변수로 선언하기에 적합한 것이다. 변수가 몇 개의 함수에서만 사용된다면 외부 변수로 선언하지 말고 함수에 인수로 전달하자.

2.3 extern 키워드
 : 함수에서 외부 변수를 사용할 필요가 있을 때에는 extern 키워드를 사용하여 함수 내에서 변수를 선언하는 것이 좋다. 변수는 다음과 같은 형식으로 선언된다.

  extern type name;

type은 변수의 형태이고, name은 변수의 이름이다. 예를 들어, <리스트 12.1>에 포함디어 있는 함수 main()과 print_value()에는 변수 x를 선언하는 문장을 포함시킬 수 있다. <리스트 12.3>에는 수정된 프로그램이 나타나 있다.

<리스트 12.3> 변수 x는 함수 main()과 print_value에서 외부 변수로 선언된다.

 /* 외부 변수 선언 */


 #include <stdio.h>


 int x = 999;


 void print_value(void);


 main()

 {

    extern int x;


    printf("%d\n", x);

    print_value();


    return 0;

 }


 void print_value(void)

 {

    extern int x;

    printf("%d\n", x);

 }

3. 지역 변수
 : 지역 변수(local variable)는 함수 내에서 정의되는 변수이다. 지역 변수의 범위는 변수가 정의된 함수로 제한된다. 다섯번째 강의 "함수의 기본"에서 함수 내에서 사용되는 지역 변수, 지역 변수를 정의하는 방법, 지역 변수의 장점에 대해서 설명했다. 지역 변수는 컴파일러에 의해서 자동으로 0의 값으로 초기화되지 않는다. 지역 변수를 정의할 때 변수를 초기화하지 않으면 지역 변수에는 임의의 값(garbage)이 저장된다. 그래서 지역 변수를 사용하기 전에는 반드시 필요한 값을 저장해야 한다. 또한, main() 함수 내에서도 지역 변수를 사용할 수 있다. <리스트 12.2>에 있는 변수 x는 main() 함수에서 사용되는 지역 변수의 예이다. 변수 x는 main() 내에서 정의되고 프로그램의 결과로도 알 수 있듯이 main() 내에서만 유효하다.

3.1 정적 변수와 자동 변수
: 지역 변수는 기본적으로 자동 변수이다. 자동 변수는 함수가 호출될 때마다 새롭게 생성되고 함수의 실행이 끝나면 사라지는 변수를 뜻한다. 좀더 정확히 표현하자면 자동 변수는 변수가 정의되어 있는 함수가 호출될 때마다 변수의 값을 보존하지 않는다는 것을 뜻한다. 지역 변수 x를 사용하는 함수가 프로그램에서 호출된다고 하자. 또한, 함수가 처음 호출될 때 변수 x에는 100의 값이 할당된다고 가정하자. 함수의 동작이 안료되면 제어는 다시 함수를 호출한 프로그램으로 돌아가고 함수는 나중에 다시 호출된다. 변수 x는 여전히 100의 값을 가지고 있을까? 그렇지 않다 처음에 생성되었던 변수 x는 제어가 프로그램으로 다시 전달될 때 사라졌다. 함수가 다시 호출될 때에는 변수 x가 새롭게 생성된다. 과거의 x는 완전히 사라진다. 그렇다면 함수가 호출될 때마다 지역 변수의 값을 보존해둘 필요가 있을 때에는 어떻게 할 것인가? 예를 들어, 데이터를 인쇄하는 함수는 새로운 페이지로 진행해야 하는 시기를 결정하기 위해서 이미 프린터로 전송된 줄(line)의 수를 기억할 필요가 있을 것이다. 함수가 호출될 때 계속해서 지역 변수의 값을 보존하기 위해서는 static 키워드를 사용하여 변수를 정적(static) 변수로 정의해야 한다. 예를 들어, 다음과 같이 할 수 있다.

   void func1( int x )
   {
      static int a;
      /* 추가적인 코드 */
   }

<리스트 12.4>에 있는 프로그램은 자도 변수와 정적 지역 변수 간의 차이를 보여준다.

<리스트 12.4> 자동 변수와 정적 지역 변수 간의 차이를 보여준다.

 /* 자동 변수와 정적 지역 변수의 사용 예 */

 #incude <stdio.h>


 void func1(void);


 main()

 {

    int count;


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

    {

       printf("At iteration l%d: ", count);

       func1();

    }


    return 0;

 }


 void func1(void)

 {

    static int x = 0;

    int y = 0;


    printf("x = %d, y = %d\n", x++, y++);

 }

프로그램은 다음과 같은 결과를 보여준다. 정적 변수로 선언된 x는 함수가 호출될 때 값을 보존하므로 변수의 값은 계속해서 증가한다. 반면에, 자도 변수로 선언된 y는 함수가 호출될 때마다 0으로 다시 초기화된다. 또한, 이 프로그램은 두 가지 형태의 변수에서 초기화가 수행되는 방법의 차이점을 보여준다. 즉, 변수가 정의되는 것과 초기화되는 시기에 대해서 알려준다. 정적 변수는 단지 함수가 처음 호출될 때에만 초기화된다. 나중에 다시 함수가 호출될 때 프로그램은 정적 변수가 이미 초기화되었다는 사실을 기억하고 있으므로 다시 초기화하지 않는다. 변수는 함수가 최근에 실행되었을 때 저장되어 있던 값을 그대로 가지게 된다. 이와는 대조적으로, 자동 변수는 함수가 호출될 때마다 지정된 값으로 다시 초기화된다. 자동 변수를 사용해보면 지금까지 설명한 내용을 충분히 이해할 수 있을 것이다. 예를 들어, <리스트 12.4>에 있는 프로그램에서 두 개의 지역 변수가 초기화되지 않도록 변경하면 함수 func1()은 다음과 같은 내용이 될 것이다.

  void func1(void)
  {
     static int x;
     int y;

     printf("x = %d, y = %d\n", x++, y++);
   }

프로그램을 이렇게 변경하여 실행하면 y의 값은 함수가 호출될 때마다 증가한다는 사실을 알 수 있다. 함수가 호출될 때 y의 값이 계속 보존된다는 것을 알 수 있는 것이다. 그렇다면 지금까지 자동 변수에 대해서 설명했던 내용은 잘못된 것일까? 절대로 그렇지 않다. 지금까지 설명한 내용은 모두 사실이다. 앞에서 설명한 내용과 프로그램의 실행 결과를 보고 알 수 있듯이, 함수가 반복적으로 호출될 동안 자동 변수가 값을 보존하는 것은 단지 우연한 일에 불과하다. 좀더 구체적으로 살펴보자. 함수가 호출될 때마다 새로운 y가 생성된다. 컴파일러는 이전에 함수가 호출되었을 때 사용하던 곳과 동일한 메모리 영역을 새로운 y에 할당하여 사용할 것이다. y가 함수 내에서 분명하게 초기화되지 않는다면 메모리의 저장 영역에는 y가 이전에 가지고 있던 값이 저장되어 있을수 있다. 그래서 변수는 계속해서 값을 보존하는 것처럼 보일 수 있지만, 사실은 우현한 일치에 불과한 것이다. 이렇게 같은 값을 가지게 되는 상황이 항상 발생한다고 볼 수는 없는 것이다.
 지역 변수는 기본적으로 자동 변수의 형태를 가지므로 변수를 정의할 때 지정할 필요가 없다 그러나 필요하다면 다음과 같이 변수의 데이터형을 표현하는 키워드 앞에 auto 키워드를 추가할 수 있다.

   void func1(int y)
   {
      auto int count;
      /* 그 밖의 프로그램 문장 */
   }

3.2 매개 변수의 범위
 : 함수의 헤더에서 매개 변수로 사용되는 변수는 지역(local) 변수와 같은 유효 범위를 가진다. 예를 들어, 다음 함수를 살펴보자.

   void func1(int x)
   {
      int y;
      /* 그 밖의 프로그램 문장 */
   }

 x와 y는 모두 함수 func1() 내에서만 유효하게 사용되는 지역 변수이다. 물론, x는 처음에 함수를 호출한 프로그램에서 전달되는 어떤 값을 가진다. 이렇게 전달된 값을 사용할 때에는 x를 다른 어떤 지역 변수와 같은 방법으로 사용할 수 있다. 매개 변수는 항상 대응하는 인수에 의해서 전달되는 값을 가지게 되므로 static이나 auto로 지정하는 것은 아무런 의미가 앖다.

3.3 외부 정적 변수
 : 외부 변수를 정의할 때 static 키워드를 포함시키면 정적 변수로 말들 수 있다.

    static float rate;
    main()
    {
      /* 그 밖의 프로그램 문장 */
    }

일반적인 외부 변수와 정적 외부 변수와의 차이는 유효 범위에 있다 .일반적인 외부 변수는 파일 내에 포함되어 있는 모든 함수에서 유효하고 다른 파일에 포함되어 있는 함수에 의해서도 사용될 수 있다. 정적 외부 변수는 단지 변수가 정의된 파일 내에서 변수가 정의된 부분 이후에 있는 함수에만 유효하다.

3.4 레지스터 변수
 : register 키워드는 자동 지역 변수가 메모리 대신에 프로세서의 레지스터에 저장되도록 컴파일러에게 지시하는 데 사용된다. 프로세서의 레지스터(processor register)는 무엇이고 레지스터를 사용할 때의 장점은 무엇일까? 컴퓨터의 CPU(central processing unit)에는 레지스터라는 몇 개의 데이터 저장 영역이 존재한다. 덧셈이나 뺄셈과 같은 실제 데이터 연산은 CPU의 레지스터에서 수행된다. CPU는 데이터를 처리하기 위해서 메모리 내에서 레지스터로 값을 읽어들이고 처리된 결과를 다시 메모리에 저장한다. 데이터를 메모리에서 읽어들이고 다시 메모리로 저장하는 일련의 과정에서는 약간의 시간이 소모된다. 만약 어떤 변수의 값을 레지스터에 저장하여 사용한다면 변수의 값을 더 빨리 사용할 수 있을 것이다.

자동 변수를 정의할 때 register 키워드를 포함시키면 변수를 레지스터에 저장하도록 '요쳥' 할 수 있다. 다음 예제를 살펴보자

   void runc1(void)
   {
      register int x;
      /* 그 밖의 프로그램 문장 */
   }

앞에서 '요청'이라는 단어를 사용했다는 것에 주의하기 바란다. 프로그램의 상태에 따라 변수의 값을 저장하기 위한 레지스터가 남아 있지 않을 수도 있다. 이때 컴파일러는 변수를 일반적인 레지스터 변수가 아니라 일반적인 자동 변수로 취급할 것이다. register 키워드는 명령하는 것이 아니라 일반적인 자동 변수로 취급할 것이다.

register 키워드는 명령하는 것이 아니라 '요청'하는 것이다 .레지스터 변수의 가장 큰 장점은 순환문의 카운터와 같이 함수 내에서 자주 사용되는 변수에서 증명된다. register 키워는 배열이나 구조체가 아니라 간단한 숫자 변수에 대해서만 사용될 수 있다. 또한 , 정적 변수나 외부 변수로 사용할 수 없다. 레지스터 변수에 대한 포인터를 정의하는 것도 불가능하다.

4. 지역 변수와 main() 함수
 : 지금가지 설명한 내용은 모두 main()과 다른 함수에 적용되는 사항이다. 정확히 말하면, main()은 다른 모든 함수와 마찬가지로 하나의 함수이다. main() 함수는 프로그램이 운영체제에서 실행될 때 호출되고 프로그램이 종료될 때 제어는 main()에서 운영체제로 돌아간다. 이것은 main()에서 정의된 지역 변수가 프로그램이 시작될 때 생성되고 프로그램이 종료될 때 사라진다는 것을 뜻한다. 그래서 main() 함수가 호출될 때 값을 보존하는 static 상태의 지역 변수를 생성하는 것은 아무런 의미가 없다.

변수는 프로그램이 실행될 때마다 값을 보존할 수 없다. 결과적으로, main() 함수에서는 자동 변수와 정적 지역 변수의 차이가 없다. main() 내에서는 물론 static 키워드를 사용하여 지역 변수를 정의할 수 있지만 아무런 효과를 나타내지 않는다.

# 잠깐 노트
 ·main()은 다른 모든 함수와 비슷한 구조를 가지는 하나의 함수라는 사실을 기억하자
 ·main() 내에서 정적 변수는 아무런 효과도 나타내지 않으므로 정적 변수를 생성한지 않도록 하자.

5. 어떤 형태의 변수를 생성해야 하는가?
 : 프로그램에서 특정 변수의 형태를 결정해야 할 때에는 다음 도표를 참고하면 도움이 될 것이다. <표 12.1>에는 C에서 사용할 수 있는 5가지 종류의 변수 형태가 나타나 있다.

<표 12.1> C 5가지 변수 형태

종 류

키워드

지속 시간

정의되는 위치

유효 범위

자동 변수

없음¹

일시적

함수 내부

지역적

정적 변수

static

일시적

함수 내부

지역적

레지스터 변수

register

일시적

함수 내부

지역적

외부 변수

없음²

지속적

함수 외부

전체적(모든 파일)

정적 외부 변수

static

지속적

함수 외부

전체적(특정 파일)

 ¹auto 키워드는 선택적으로 사용할 수 있다.

 ²extern 키워드는 다른 곳에서 이미 정의된 정적 외부 변수를 선언하기 위해서

   함수 내에서 사용되는 것이다.

변수의 형태를 결정할 때에는 가능하다면 자동 변수를 사용해야 하고, 필요한 경우에만 다른 형태를 선택해야 한다. 다음은 몇 가지 참고 사항이다.

·일단 모든 변수를 자동 지역 변수의 형태로 사용하자.
·변수가 자주 사용된다면 register 키워드를 사용하여 레지스터 변수로 정의하자.
·main()이 아닌 다른 함수 내에서 함수가 호출될 때 변수의 값이 보존되어야 한다면 정적 변수로 정의하자.
·변수가 프로그램의 대부분이나 또는 모든 함수에 의해서 사용된다면 외부 변수로 정의하자

6. 지역 변수와 블록
 : 지금까지 설명한 내용은 단지 함수 내에서 지역 변수를 사용하는 것을 중심으로 했다. 지역 변수는 기본적으로 함수 내에서만 사용되지만 중괄호 내에 포함된 프로그램의 어떤 블록 내에서도 지역적으로 사용되는 변수를 생성할 수 있다. 블록 내에서 사용되는 변수를 선언할 때에는 가장 먼저 변수가 선언되어야 한다는 것을 기억하자. 예를 들어, <리스트 12.5>를 살펴보자.

<리스트 12.5> 프로그램의 블록 내에서 지역 변수 정의하기

 /* 블록 내에서 지역 변수 사용하기 */


 #include <stdio.h>


 main()

 {

    /* main()에 지역적인 변수 정의 */


    int count = 0;


    printf("\nOutside the block, count = %d", count);


    /* 블록의 시작 */

    {

       /* 블록에 지역적인 변수 정의 */


       int count = 999;

       printf("\nWithin the block, count = %d", count);

    }


    printf("\nOutside the block again, count = %d\n", count);

    return 0;

 }

이렇게 지역 변수를 사용하는 것은 C 프로그래밍에서 흔하지 않고 필요하다고 생각되지도 않을 것이다. 실제로, 이렇게 같은 이름의 변수를 지역적으로 사용하는 방법은 프로그램에서 문제점을 찾는 경우에 가장 많이 사용된다. 즉, 프로그램의 일부분을 괄호 내에 포함시켜 일시적으로 독립시키고 잘못된 내용을 찾기 위해서 지역 변수를 사용할 수 있다. 다른 한 가지 장점은 변수가 사용되는 부분과 가까운 곳에서 변수를 선언하고 초기화할 수 있다는 것이다. 이렇게 하면 프로그램을 이해하기 쉽게 만들 수 있다.

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

10장 문자와 문자열  (0) 2019.06.02
11장 구조체  (0) 2019.06.02
13 장 고급 프로그램 제어문  (0) 2019.06.02
14장 화면, 프린터, 키보드 사용하기  (0) 2019.06.02
15장 포인터 : 고급 기능들  (0) 2019.06.02

여섯번째 강의 "기본적인 프로그램 제어문"에서는 프로그램에서 다른 문장의 실행을 제어하는 C의 프로그램 제어문을 간단히 살펴보았다. 이 장에서는 지금까지 다루지 않았던 goto문과 프로그램의 순환문에서 사용할 수 있는 유용한 프로그램 제어문을 살펴보도록 하겠다. 오늘은 다음과 같은 내용을 배운다.

·break와 continue문을 사용하는 방법
·무한 루프는 무엇이고, 왜 필요한가?
·goto문을 사용하는 방법과 프로그램에서 goto문을 사용하지 않아야 하는 이유
·switch문을 사용하는 방법
·프로그램의 종료를 제어하는 방법
·프로그램이 종료될 때 자동으로 함수를 실행하는 방법
·프로그램 내에서 시스템의 명령을 실행하는 방법

1. 순환문을 미리 종료하는 방법
 : 여섯번째 강의에서는 for문, while문, do...while문이 프로그램의 실행을 제어한다는 것을 배웠다. 이런 순환문은 프로그램의 조건에 따라 C의 문장이나 블록을 전혀 실행하지 않거나 한 번 실행하거나 또는 여러 번 실행한다. 이런 세 가지 순환문에서는 주어진 조건에 일치하는 상황이 발생하는 경우에만 순환문을 종료할 수 있다. 그러나 가끔 순환문의 반복 상태를 직접 제어할 수 있기를 원할 것이다. break와 continue문은 이런 순환문의 실행을 제어할 수 있게 해준다.

1.1 break문
 : break문은 for문, while문 또는 do...while문 내에서만 사용할 수 있다. switch문에서도 사용되지만 자세한 내용은 이 장의 후반부에서 다시 설명할 것이다. 간단히 말해서 break문이 나타나면 순환문은 즉시 종료된다. 다음은 예제이다.

   for(count = 0; count < 10; count++)
   {
      if(count == 5)
         break;
   }

일반적인 경우에 for문은 10번 실행될 것이다. 그러나 여기에서는 for문이 6번째 반복되기 전에 count가 5의 값을 가지므로 break문이 실행되고 for문이 종료된다. 제어는 for문 바로 다음에 있는 문장으로 전달될 것이다. 종속된 순환문에서 break문이 사용될 때에는 가장 내부에서 사용되는 순환문이 종료된다.

<리스트 13.1> break문 사용하기

 /* break문의 사용 에 */


 #include <stdio.h>


 char s[] = "This is a test string. It contains two sentences.";


 main(0

 {

    int count;


    printf("\noriginal string: %s", s);


    for(count = 0; s[count] != '\n0'; count++)

    {

       if(s[count] == '.')

       {

          s[count = 1] = '\n';

          break;

       }

    }

    printf("\nModified string: %s\n", s);


    return 0;

 }

-> 출 력

 Original string: This is a test string. It contains two sentences.
 modified string: This is a test string.

1.2 continue문
 : break문과 마찬가지로 continue문은 for문, while문, do...while문 내에서만 사용될 수 있다. continue문이 실행되면 제어는 순환훈의 마지막 부분으로 전달되고 다음 순환 동작이 시작된다. continue문과 순환문의 마지막 부분으로 전달되고 다음 순환 동작이 시작된다. continue문과 순환문의 마지막 부분 사이에 있는 문장은 실행되지 않는다. <리스트 13.2>에는 continue를 사용하는 프로그램이 나타나 있다. 이 프로그램은 키보드에서 문자열을 받아들이고 모든 소문자 형태의 모음을 제거하여 다시 출력한다.

<리스트 13.2> continue문 사용하기

 /* continue문의 사용 예 */


 #include <stdio.h>


 main()

 {

    /* 입력용 버퍼와 카운터 변수를 선언 */


    char buffer[81];

    int ctr;


    /* 한 줄의 텍스트 입력 */


    puts("Enter a line of text: ");

    gets(buffer);


    /* 소문자 모음이 아닌 문자만을 출력하며 문자열을 차례대로 사용 */


    for(ctr = 0; buffer[ctr] != '\n0'; ctr++)

    {


       /* 문자가 소문자 모음이면 출력하지 않고 다음으로 진행 */


       if(buffer[ctr] == 'a' || buffer[ctr] == 'e' || buffer[ctr] == 'i'

         || buffer[ctr] == 'o' || buffer[ctr] == 'u')

       continue;


       /* 모음이 아니면 출력 */


       putchar(buffer[ctr]0;

    }

    return 0;

 }

2. goto문
 : goto문은 C에서 조건 없이 이동(unconditional jump)하거나 분기(branching)하는 명령의 하나이다. 프로그램에서 goto문이 나타날 때 제어는 즉시 goto문에 의해 지정된 위치로 이동하거나 분기한다. goto문이 실행될 때에는 조건 없이(unconditional) 항상 분기 동작이 발생한다. 분기 동작은 if문처럼 프로그램의 어떤 조건에 의해서 발생하는 것이 아니다. goto문과 목적지의 레이블은 서로 다른 블록에 존재할 수 있지만 동일한 함수 내에 존재하는 것이어야 한다. goto문의 사용 예를 보여주는 <리스트 13.3>의 간단한 프로그램을 살펴보자.

<리스트 13.3> goto문 사용하기

 /* goto문의 사용 예 */


 #include <stdio.h>


 main()

 {

    int n;


 start: ;


    puts("Enter a number between 0 and 10: ");

    scanf("%d", &n);


    if(n < 0 || n > 10)

    goto start;

    else if(n ==0)

    goto location0;

    else if(n == 1)

    goto location1;

    else

    goto location2;


 location0: ;

    puts("You entered 0.\n");

    goto end;


 location1: ;

    puts("You entered 1.\n");

    goto end;


 location2: ;

    puts("You entered something between 2 and 10.\n");


 end: ;

    return 0;

 }

goto문의 목적지는 프로그램 내에서 goto문의 앞이나 뒤에 위치될 수 있다. 앞에서도 언급했듯이 goto문에 대한 유일한 제한 사항은 goto문과 목적지가 같은 함수 내에 존재해야 한다는 것이다. 그러나 함수 내의 서로 다른 블록에 위치될 수 있다. for문과 같은 순환문에서 제어를 순환문의 바깥으로 이동시키기 위해서 goto를 사용할 수도 있지만 goto문을 이렇게 사용해서는 안된다. 그리고 프로그램에서는 가능하다면 goto문을 사용하지 않는 것이 좋은데, 두 가지 이유가 있다.

·goto문은 필요하지 않다. goto문을 사용해야 하는 프로그래밍 작업은 없다. goto문이 필요한 경우가 있더라도 항상 C에서 제공되는 다른 분기문을 사용하여 필요한 작업을 할 수 있다.

·goto문은 위험하다. goto문이 특별한 프로그래밍 문제에 대한 이상적인 해결 방법처럼 생각되겠지만 실제로는 전혀 그렇지 않다. goto문에 의해서 프로그램이 분기될 때에는 어디에서 분기되었는지 알 수 없으므로 프로그램이 혼란스러워질 수 있다. 이런 형태의 혼잡한 프로그래밍(spaghetti-code)은 좋지 않다. 이런 두 가지 사실을 알고 주의해서 프로그램을 작성한다면 goto문을 사용하더라도 문제가 없는 프로그램을 작성할 수 있을 것이다. 어떤 경우에는 goto문을 주의해서 사용한다면 프로그래밍 문제를 가장 간단한 방법으로 해결할 수도 있을 것이다 .그러나 goto문의 사용이 유일한 해결 방법은 아니다. 앞에서 설명한 두 가지 이유를 무시하더라도 goto문을 사용할 때에는 적어도 주의할 필요는 있을 것이다.

3. 무한 루프
 : 무한 루프는 무엇이고, 프로그램 내에서 무한 루프가 필요한 이유는 무엇일까? 무한 루프는 실행을 마치는 상황이 발생하지 않고 계속해서 반복되는 순환문이다. 무한 루프는 for, while, do...while문이 될 수 있다. 예를 들어, 다음 문장은

   while(1)
   {
      /* 다른 프로그램 문장들 */
   }

무한 루프가 될 것이다. while네 주어진 조건은 항상 참으로 평가되고 프로그램이 실행되더라도 변경되지 않는 상수이다. 1은 결코 바뀌지 않는 값이므로 순환문은 절대로 끝나지 않을 것이다. 앞에서는 순환문을 벗어나기 위해서 break문을 사용할 수 있다는 것을 설명했다. break문을 사용하지 않는다면 무한 루프의 가치는 없다. 무한 루프는 break문과 함께 사용될 때 유용하다. 또한, 다음과 같이 for나 do...while을 사용하여 무한 루프를 생성할 수 있다.

   for( ; ; )
   {
      /* 다른 프로그램 문장들 */
   }
       do
   {
      /* 다른 프로그램 문장들 */
   } while(1);

이론적으로 이런 세 가지 형태의 순환문은 같은 것이다. 여기에서는 while문을 사용할 것이다. 무한 루프는 여러 개의 조건을 확인해서 순환문을 끝내야 하는지 결정하는데 사용할 수 있다 while문의 실행 조건이 입력되는 부분에 여러 개의 조건을 포함시키는 것은 어려울 것이다. 대신에, 순환문 내에서 개별적으로 여러 개의 조건을 확인해서 필요에 따라 break를 사용하여 종료하는 것이 더 낫다.

또한, 무한 루프는 프로그램의 동작을 지시하는 메뉴 체계를 구성하기 위해서 사용될 수 있다. 다섯번째 강의 "함수의 기본"에서는 프로그램의 main() 함수가 여러 가지 동작을 수행하는 다양한 함수의 실행을 지시하는 '교통 경찰'과 같은 역할을 수행하는 예를 살펴보았다. 프로그램의 사용자에게는 여러 가지 항목이 제공되고, 원하는 항목의 하나를 선택하여 필요한 동작을 수행할 수 있다. 또한, 이런 항목 중에는 프로그램을 종료하는 선택 사항이 포함되어야 한다. 일단 항목이 선택되면 선택된 항목에 따라 프로그램이 실행된다. <리스트 13.4>에 있는 프로그램은 메뉴 체계의 사용 예를 보여준다.

<리스트 13.4> 메뉴 체계를 사용하기 위한 무한 루프의 사용 예

 /* 메뉴 체계를 구현하기 위한 무한루프의 사용 예 */


 #include <stdio.h>

 #define DELAY 1500000   /* 지연에 사용되는 값 */


 int menu(void0;

 void delay(void);


 main()

 {

    int choice;


    while(1)

    {


       /* 사용자의 선택을 요구 */


       choice = menu();


       /* 입력에 따라 분기 */


       if(choice == 1)

       {

          puts("\nExecuting choice 1.");

          delay();

       }

       else if(choice == 2)

       {

          puts("\nExecuting choice 2.");

          delay();

       }

       else if(choice == 3)

       {

          puts("\nExecuting choice 3.l");

          delay();

       }

       else if(choice == 4)

       {

          puts("\nExecuting choice 4.");

          delay();

       }

       else if(choice == 5)

       {

          puts("\nExiting program now...\n");

          delay();

          break;

       }

       else

       {

          puts("\nInvalid choice, try again.");

          delay();

       }

    }

    return 0;

 }


 /* 메뉴를 출력하고 사용자의 선택을 읽어들인다. */

 int menu(void)

 {

    int reply;


    puts("\nEnter 1 for task A.");

    puts("Enter 2 for task B.");

    puts("Enter 3 for task C.");

    puts("Enter 4 for task D.");

    puts("Enter 5 to exit program.");


    scanf("%d", &reply);


    return reply;

 }


 void delay(void)

 {

    long x;

    for(x = 0; x < DELAY; x++)

    ;

 }

4. switch문
 : C에서 제공되는 가장 융통성 있는 프로그램 제어문은 프로그램 내에 포함되는 두 가지 이상의 값을 기본적으로 하여 여러 가지 문장을 실행하게 해주는 switch문이다. 지금까지 설명했던 if문과 같은 제어문에서는 단지 두 개의 값인 참이나 거짓으로 평가되는 수식만을 사용할 수 있었다. 두 개 이상의 값을 기준으로 해서 프로그램의 흐름을 제어하기 위해서는 <리스트 13.4>에 나타나 있듯이 여러 개의 종속된 if문을 사용해야 한다. switch문은 이런 종속문을 대신할 수 있다. switch문의 일반적인 형식은 다음과 같다.

   switch(expression)
   {
      case template_1:  statement(s);
      case template_2:  statement(s);
      …
      case template_n:  statement(s);
      default:  statement(s);
   }

여기서 expression은 long, int, char형과 같은 정수값으로 평가되는 수식이다. switch문은 expression을 평가하여 결과를 각각의 case 다음에 포함되어 있는 template과 비교하고 나서 다음과 같은 동작을 수행한다.

·expression의 결과와 template의 어떤 것이 일치한다면 해당 case에 포함되어 있는 문장이 실행된다.
·아무 것도 일치하지 않으면 선택적으로 사용되는 default에 포함되어 있는 문장이 실행된다
·아무 것도 일치하지 않고 default도 포함되어 있지 않다면 switch문의 바로 다음에 있는 문장이 실행된다.

<리스트 13.5>에는 사용자가 입력한 값에 따라 메시지를 출력하기 위해 switch문을 사용하는 간단한 프로그램이 나타나 있다.

<리스트 13.5> switch문의 사용 예

 /* switch문의 사용 예 */


 #include <stdio.h>


 main()

 {

    int reply;


    puts("Enter a number between 1 and 5: ");

    scanf("%d", &reply);


    switch(reply)

    {

       case 1:

          puts("You entered 1.");

       case 2:

          puts("You entered 2.");

       case 3:

          puts("You entered 3.");

       case 4:

          puts("You entered 4.");

       case 5:

          puts("You entered 5.");

       default:

          puts("Out of range, try again.");

    }

    return 0;

 }

=> 분명히 무언가 잘못되었다는 것을 알 수 잇을 것이다. 여기서 switch문은 처음에 일치하는 템플릿(template)을 발견하고 해당 case에 있는 문장뿐 아니라 이후의 모든 문장들을 실행하고 있다. 잘못된 결과이기는 하지만 switch문은 실제로 이렇게 조건에 따라 정해진 동작을 수행한다. switch문은 값이 일치하는 템플릿으로 분기한다. 그러나 값이 일치하는 템플릿에 포함된 문장만 실행하기 위해서는 필요한 부분에 break문을 포함시켜야 한다. <리스트 13.6>에는 break문을 사용하여 다시 작성한 프로그램이 나타나 있다. 이제 프로그램은 정상적으로 동작할 것이다.

<리스트 13.6> 필요한 부분에 break문을 포함시켜서 정상적으로 실행되는 switch문의 사용예

 /* switch문의 빠른 사용 예 */


 #include <stdio.h>


 main()

 {

    int reply;


    puts("Enter a number between 1 and 5: ");

    scanf("%d", &reply);


    switch(reply)

    {

       case 0:

          break;

       case 1:

       {

          puts("You entered 1.");

          break;

       }

       case 2:

       {

          puts("You entered 2.");

          break;

       }

       case 3:

       {

          puts("You entered 3.");

          break;

       }

       case 4:

       {

          puts("You entered 4.");

          break;

       }

       case 5:

       {

          puts("You entered 5.");

          break;

       }

       default:

       {

          puts("Out of range, try again.");

       }

    }      /* switch의 끝 */

    return 0;

 }


=> 프로그램을 컴파일하고 실행해보자. 정상적으로 동작할 것이다. switch문은 <리스트 13.4>에 나타나 있는 것과 같이 메뉴를 처리하는 경우에 가장 많이 사용된다. <리스트 13.7>에 있는 프로그램은 메뉴를 구현하기 위해 if문 대신에 switch문을 사용하고 있다. switch를 사용하는 것은 <리스트 1.34>에 나타나 있는 메뉴 프로그램의 이전 버전에서 사용되었던 종속된 if문을 사용하는 것보다 훨씬 낫다.

<리스트 13.7> 메뉴 체계를 구성하기 위한 switch문의 사용 예

 /* 메뉴 체계를 구현하기 이한 무한 루프와 switch문의 사용 예 */


 #include <stdio.h>

 #include <stdlib.h>


 #define DELAY 150000


 int menu(void);

 void delay(void);


 main()

 {

    while(1)

    {

       /* 사용자의 선택을 받아들이고 입력에 따라 분기 */


       switch(menu())

       {

          case 1:

          {

             puts("\nExecuting choice 1.");

             delay();

             break;

          }

          case 2:

          {

             puts("\nExecuting choice 2.");

             delay();

             break;

          }

          case 3:

          {

             puts("\nExecuting choice 3.");

             delay();

             break;

          }

          case 4:

          {

             puts("\nExecuting choice 4.");

             delay();

             break;

          }

          case 5:     /* 프로그램의 끝 */

          {

             puts("\nExiting program now...\n");

             delay();

             exit(0);

          }

          default:

          {

             puts("\nInvalid choice, try again.");

             delay();

          }

       }    /* switch의 끝 */

    }    /* while의 끝 */

    return 0;

 }


 /* 메뉴를 출력하고 사용자의 선택을 받아들인다. */

 int menu(void)

 {

    int reply;


    puts("\nEnter 1 for task A.");

    puts("Enter 2 for task B.");

    puts("Enter 3 for task C.");

    puts("Enter 4 for task D.");

    puts("Enter 5 to exit program.");


    scanf("%d", &reply);


    return reply;

 }


 void delay(void)

 {

    long x;

    for(x = 0; x < DELAY; x++)

       ;

 }

=> 이 프로그램에서는 새로운 함수가 사용되고 있다. case 5:에 포함된 48번째 줄의 라이브러리 함수 exit()를 주목하기 바란다. 여기에서는 <리스트 13.4>와 같이 break문을 사용할 수 없다. 여기서 break를 사용하면 무한 루프인 while문을 벗어나는 것이 아니라 switch문을 벗어나게 된다. 잠수 후에 설명하겠지만 exit() 함수는 프로그램 자체를 종료하는 기능을 가진다. 가끔 switch를 구성하는 여러 개의 항목을 동일하게 '처리'하는 것이 유용할 때가 있다. 예를 들어, 여러 가지 항목에서 어떤 것을 선택하든지 특정 문장을 실행할 필요가 있다고 하자. 이 때에는 break문을 사용하지 말고 필요한 문장 앞에 모든 case 템플릿을 입력하면 된다. 만약 조건 수식이 어떤 case에 일치한다면 여러분이 실행하기 원하는 코드 블록에 도달할 때까지 case 다음의 모든 문장이 실행될 것이다. <리스트 13.8>에 있는 프로그램은 이런 경우를 보여준다.

<리스트 13.8> switch문을 사용하는 다른 한 가지 방법

 /* switch문을 사용하는 다른 한 가지 방법 */


 #include <stdio.h>

 #include <stdlib.h>


 main()

 {

    int reply;


    while(1)

    {

       puts("\nEnter a value between 1 and 10, 0 to exit: ");

       scanf("%d", &reply);


       switch(reply)

       {

          case 0:

             exit(0);

          case 1:

          case 2:

          case 3:

          case 4:

          case 5:

          {

             puts("You entered 5 or below.\n");

             break;

          }

          case 6:

          case 7:

          case 8:

          case 9:

          case 10:

          {

             puts("You entered 6 or higher.\n");

             break;

          }

          default:

             puts("Between 1 and 10, please!\n");

       }    /* switch의 끝 */

    }    /* while의 끝 */

    return 0;

 }

=> 이 프로그램은 키보드에서 값을 읽어들이고 5이하인지 6이상인지 또는 1과 10사이의 값인지 알려준다. 입력된 값이 0이라면 18번째 줄에서는 exit() 함수를 호출하므로 프로그램은 종료.
 
5. 프로그램의 종료
 : C 프로그램은 일반적으로 main() 함수의 실행이 끝날 때 종료된다. 그러나 라이브러리 함수 exit()를 사용하면 언제든지 원하는 시기에 프로그램을 마칠 수 있다. 또한, 프로그램이 종료될 때 하나 이상의 함수를 자동으로 실행하게끔 지정할 수도 있다.

5.1 exit() 함수
 : exit() 함수는 프로그램 실행을 종료하고, 제어를 운영체제에 돌려준다. 이 함수는 프로그램이 성공적으로 실행되었는지 또는 실행에 문제가 있었는지를 지적하기 위해서 운영체제에 전달하는 하나의 int형 인수를 가진다. exit() 함수의 형식은 다음과 같다.

   exit(status);

status의 값이 0이라면 프로그램이 정상적으로 종료되었다는 것을 뜻한다. status가 1의 값을 가지면 어떤 에러가 발생해서 프로그램이 비정상적으로 종료되었다는 것을 뜻한다. 이런 복귀값은 대개 무시된다. DOS에서는 배치 파일과 if errorlevel문을 사용하여 이런 복귀값을 확인하고 사용할 수 있다. 그러나 여기서는 DOS 설명서가 아니므로 복귀값을 사용하는 방법에 대해서 알라보기 원한다면 DOS 관련 서적을 참고하기 바란다.

 exit() 함수를 사용하기 위해서는 프로그램에서 헤더 파일 STDLIB.H를 포함시켜야 한다. 또한, 이 헤더 파일에서는 exit() 함수에 대한 인수로 사용되는 두 개의 기호 상수를 다음과 정의하고 있다.

   #define EXIT_SUCCESS 0
   #define EXIT_FAILURE 1

그래서 복귀값을 0으로 설정하여 프로그램을 마치기 원한다면 exit(EXIT_SUCCESS)를 사용할 수 있다. 1의 값을 돌려주기 위해서는 exit(EXIT_FAILURE)를 사용하면 된다.

6. 프로그램 내에서 운영테제의 명령을 실행하는 방법
 : C의 표준 라이브러리는 실행 중인 C 프로그램 내에서 운영테제의 명령을 실행하게 해주는 함수 system()을 제공한다. 이 함수는 프로그램을 종료하지 않은 상태에서 디렉토리 목록을 사렾보고나 디스크를 초기화에 해주므로 때에 따라 유용하게 사용할 수 있다. system() 함수를 사용하기 위해서는 프로그램에 헤더 파일 STDLIB.H를 포함시켜야 한다. system()의 형식은 다음과 같다.

   system(command);

인수 command는 문자열 상수나 문자열에 대한 포인터가 될 수 있다. 예를 들어 DOS의 디렉토리 목록을 살펴보기 위해서 다음과 같은 내용을 사용할 수 있을 것이다.

   system("dir");

또는 다음 문장을 사용할 수도 있다.

   char *command = "dir";
   system(command);

운영체제의 명령을 실행하고 나면 프로그램의 제어는 system() 함수 바로 다음의 문장으로 전달된다. system()에서 사용한 명령이 운영체제에서 유효하지 않은 명령이라면 프로그램이 다시 실행되기 전에 'Bad command or file name'이라는 에러 메시지가 출력된다. system()의 사용 예가 <리스트 13.9>에 나타나 있다.

<리스트 13.9> 운영체제의 명령을 실행하기 위해서 system() 함수를 사용하는 프로그램

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

 #include <stdio.h>

 #include <stdlib.h>


 main(0

 {

    /* 입력을 저장할 버퍼 선언 */


    char input[40];


    while(1)

    {

       /* 사용자의 명령을 받아들인다. */


       puts("\nInput the desired system command, blank to exit");

       gets(input);


       /* 빈 줄이 입력되면 마친다. */


       if(input[0] == '\0'

          exit(0);


       /* 명령을 실행한다. */


       system(input);

    }

    return 0;

 }

system()에서 사용할 수 있는 명령은 디렉토리 목록을 살펴보거나 디스크를 초기화하는 것과 같은 간단한 명령에만 제한되지 않는다. 또한, 실행 가능한 파일이나 배치 파일의 이름을 전달하여 프로그램을 정상적으로 실행할 수도 있다. 예를 들어, system의 인수로 LIST1308을 전달하면 LIST1308이라는 프로그램이 실행될 것이다. 프로그램의 실행이 끝나면 제어는 다시 system() 함수가 사용되었던 곳으로 전달된다.

system()을 사용할 때 유일한 제한 사항이 있다면 메모리와 관련된 문제이다. system()이 실행될 때 원래의 프로그램은 컴퓨터의 RAM에 남게 되고 운영체제의 명령을 처리하는 코드와 함수를 통해서 실행되는 프로그램이 메모리에 읽어들여지게 된다. 이렇게 다른 프로그램을 실행하는 것은 컴퓨터에 충분한 메모리가 남아 있는 경우에만 가능하다. 그렇지 않다면 에러 메시지가 출력될 것이다.

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

11장 구조체  (0) 2019.06.02
12장 변수의 범위  (0) 2019.06.02
14장 화면, 프린터, 키보드 사용하기  (0) 2019.06.02
15장 포인터 : 고급 기능들  (0) 2019.06.02
16장 링크리스트  (0) 2019.06.02

대부분의 프로그램은 입출력 동작을 수행한다. 프로그램이 입출력 동작을 얼마나 효과적으로 처리하는지는 가금 프로그램의 유용성을 판단하는 가장 좋은 기준이 된다. 앞에서는 이미 몇 가지 기본적인 입출력을 수행하는 방법을 배웠다. 오늘은 다음과 같은 내용을 배운다.

 ·C에서 입출력 동작을 수행하기 위해 스트림(stream)을 사용하는 방법
 ·키보드에서 입력되는 내용을 받아들이는 여러 가지 방법
 ·화면 상에 텍스트와 숫자 데이터를 출력하는 방법
 ·프린터로 출력하는 방법
 ·프로그램의 입력과 출력을 전환시키는 방법

1. 스트림과 C
 : 프로그램에서의 입출력에 대한 상세한 내용을 다루기 전에, 우선 스트림에 대해서 알아보도록 하자. C에서의 모든 입출력은 데이터가 어느 곳에서 입력되고, 어느 곳으로 출력되든지 관계없이 스트림을 통해서 수행된다. 나중에 설명하겠지만 모든 입력과 출력을 수행하는 이런 표준 방법은 프로그래머에게 상당한 이점을 제공한다. 물론, 이런 장점을 이용하기 위해서는 기본적으로 스트림이 무엇이고 어떻게 사용하는지 이해할 필요가 있다. 그러나 우선 입출력(input/output)이라는 용어에 대해서 정확히 알 필요가 있다.

1.1 프로그램의 입출력이 뜻하는 것
 : 이 강의의 앞부분에서 배웠듯이 C 프로그램이 실행되는 동안 데이터는 읽고 쓰기가 가능한 메모리(RAM)에 저장된다. 이렇게 메모리에 저장되는 데이터는 프로그램에서 선언된 변수, 구조체, 배열의 형태로 존재한다. 그러면 데이터는 어떻게 생성되고, 프로그램은 데이터를 사용하여 무엇을 수행할 수 있을까?

·데이터는 외부로부터 프로그램으로 전달될 수 있다. 외부에서 프로그램이 사용할 수 있는 RAM으로 데이터가 전달되는 것을 입력(input)이라고 한다. 프로그램에 대한 입력으로 가장  많이 사용되는 것은 키보드와 디스크 파일이다.

·또한, 데이터는 프로그램의 외부로 전달될 수 있다. 이것은 출력(output)이라고 한다. 출력의 대상으로 가장 많이 사용되는 것은 화면(모니터), 프린터, 디스크 파일이다. 입력을 제공하는 것과 출력의 대상이 되는 것을 장치(devices)라고 한다. 키보드, 화면 등은 하나의 장치이다, 키보드와 같은 장치는 입력을 위해서만 사용된다. 화면과 프린터 등은 출력을 취해서만 사용된다. 그리고 디스크 파일과 같은 것은 입력과 출력을 위해서 사용된다. 사용되는 장치가 무엇이고, 입력이나 출력 중에서 어떤 것을 수행하든지 관계없이 C에서 모든 입력과 출력은 스트림에 의해서 수행된다.

1.2 스트림이란?
 : 스트림(stream)은 일련의 문자들이다. 더욱 정확하게 말하자면 일련의 바이트로 구성된 데이터이다. 프로그램으로 전달되는 바이트는 입력 스트림이다. 프로그램에서 외부로 이동되는 바이트는 출력 스트림이다. 스트림이 어디로 가고 어디에서 오는지에 대해서 신경 쓸 필요는 없다. 스트림의 한 가지 중요한 장점은 프로그램의 입출력 동작이 장치에 독립적인 상태(device independent)로 수행된다는 것이다. 프로그래머는 키보드, 디스크 등 각각의 장치를 위한 특별한 입출력 함수를 작성할 필요가 없다. 프로그램은 어느 것에서 입력 동작이 수행되고, 어느 곳으로 출력 동작이 수행되든지 관계없이 연속적인 바이트의 스트림을 통해서 입출력을 처리한다. C의 모든 스트림은 파일에 관련되어 있다. 여기에서 파일(file)이라는 것은 디스크에 저장되는 파일을 뜻하는 것이 아니라 프로그램이 다루는 스트림과 입력이나 출력을 수행하는 실제 물리적 장치간의 중간 과정을 뜻한다. 대부분의 경우에는 스트림, 파일, 장치간의 상세한 동작이 C의 라이브러리 함수와 운영체제에 의해서 자동으로 다뤄지므로 초보 프로그래머는 이런 파일에 대해서 관심을 가질 필요가 없다.

1.3 텍스트 스트림과 이진 스트림
 : C의 스트림에는 텍스트 스트림과 이진 스트림의 두 가지 모드가 있다. 텍스트(text) 스트림은 화면에 출력되는 텍스트 데이터와 같은 문자만으로 구성된다. 텍스트 스트림은 255자까지의 길이를 가질 수 있고, 문장의 마지막을 나타내는 문자(EOF)나 문자 진행(newline) 문자에 의해서 종료되는 문장으로 구성된다. 텍스트 스트림을 구성하는 특정 문자는 문장 진행(newline) 문자와 같이 특별한 의미를 가지는 것으로 인식된다. 이장에서는 텍스트 스트림을 다룰 것이다. 이진(binary) 스트림은 텍스트 데이터로 제한되지 않고 모든 종류의 데이터를 다룰 수 있다. 이진 스트림에 포함되는 데이터는 어떤 특별한 방법으로 전송되거나 특별한 의미를 가지지 않는다. 데이터는 있는 그대로 읽어들여지거나 기록된다.

1.4 정의되어 있는 스트림의 종류
 : ANSI C에는 표준 입출력 파일(standard input/output files)로 취급되는 3개의 스트림이 정의되어 있다. DOS를 실행중인 IBM PC 호환 기종에서 프로그래밍한다면 두 개의 추가 스트림이 유효하다. 이런 스트림은 C 프로그램이 실행될 때 자동으로 열리고 프로그램이 종료될 때 닫힌다. 프로그래머는 스트림을 사용하기 위해서 추가로 어떤 동작을 수행할 필요가 없다. <표 14.1>에는 표준 스트림과 스트림에 연결되어 있는 장치들이 나타나 있다. 모든 표준 스트림은 텍스트 모드의 스트림이다.

<표 14.1> 5개의 표준 스트림

이 름

스트림

장 치

stdin

표준 입력

키보드

stdout

표준 출력

화면

stderr

표준 에러

화면

stdprn*

표준 프린터

프린터(LPT1:)

stdaux*

표준 보조

직렬 포트(COM1:)

 * DOS에서만 지원된다.

지금까지 화면 상에 텍스트를 출력하기 위해서 printf()나 puts() 함수를 사용할 때 항상 stdout 스트림을 사용했었다. 이와 비슷하게, 키보드의 입력을 읽어들이기 위해서 gets()나 scanf()를 사용할 때에는 stdin 스트림을 사용한다. 표준 스트림은 자동으로 열리지만, 디스크에 저장된 자료를 다룰 때 사용되는 것과 같은 스트림은 프로그래머가 직접 열어야 한다. 스트림을 여는 방법에 대해서는 낭중에 설명할 것이다. 여기는 표준 스트림에 대해서 설명할 것이다.

2. C의 스트림 함수
 : C의 표준 라이브러리에는 스트림의 입력과 출력을 다루는 다양한 함수가 포함되어 있다. 이런 함수들은 대부분 두 가지로 나누어진다. 하나는 항상 표준 스트림의 하나를 사용하는 것이고, 다른 하나는 프로그래머가 스트림의 종류를 지정하게 해주는 것이다. <표 14.2>에는 스트림 함수가 요약되어 있다. 여기에는 C의 모든 입출력 함수가 나타나 있지 않고, 여기서 설명할 모든 함수가 나타나 있는 것도 아니지만 스트림 함수를 대략적으로 살펴볼 수 있을 것이다.

<표 14.2> 표준 라이브러리의 스트림 입출력 함수

표준 스트림의

하나를 사용하는 것

스트림의 이름을

입력하도록 요구하는 것

수행하는 동작

printf()

fprintf()

 형식화된 출력

vprintf()

vfprintf()

 인수의 목록을 이용해서 형식화된 출력

puts()

fputs()

 문자열 출력

putchar()

putc(), fputc()

 문자 출력

scanf()

fscanf()

 형식화된 입력

gets()

fgets()

 문자열 입력

getchar()

getc(), fgetc()

 문자 입력

perror()


 stderr로 문자열 출력

대부분의 함수들은 STDLIB.H를 요구하고 함수 perror()도 STDLIB.H를 요구한다. 함수 vprintf()와 vfprintf()를 사용하려면 STDARGS.H를 포함시켜야 한다. UNIX 시스템에서 vprintf()와 vfprintf()를 사용하려면 VARARGS.H를 포함시켜야 한다. 추가로 헤더 파일이 필요한지 알아보려면 컴파일러의 라이브러리 레퍼런스를 참조하기 바란다.

2.1 예제 프로그램
<리스트 14.1>에 있는 간단한 프로그램은 스트림의 사용 예를 보여준다.

<리스트 14.1> 스트림의 특성을 보여주는 예제

 /* 스트림 입력과 출력의 동일성을 보여주는 예 */

 #include <stdio.h>


 main(0

 {

    char buffer[256];


    /* 한 줄을 입력받고 나서 그대로 출력 */


    puts(gets(buffer));


    return 0;

 }

=> 10번째 줄에 있는 gets() 함수는 키보드(stdin)에서 한 줄의 텍스트를 읽어들이는데 사용된다. gets()는 문자열에 대한 포인터를 돌려주므로 이 포인터를 화면(stdout)에 문자열을 출력하는 puts() 함수의 인수로 사용할 수 있다. 프로그램을 시앻하면 사용자에게서 한 줄의 텍스트를 입력받고 즉시 화면 상에 문자열을 출력한다.

3. 키보드 입력 받아들이기
대부분의 C 프로그램은 키보드, 즉 stdin에서 어떤 형태의 입력을 받아들인다. 입력 함수는 세 가지 종류로 나누어진다. 문자 입력, 한 줄의 텍스트 입력, 형식화된 입력.
 
3.1 문자의 입력
 : 문자 입력 함수는 지정된 스트림에서 한 번에 한 문자씩 입력을 받아들인다. 이런 함수를 호출하면 스트림 내의 다음 문자를 돌려주는데, 파일의 마지막에 도달했거나 에러가 발생핬다면 EOF를 돌려준다. EOF는 STDIO.H에서 -1로 정의되어 있는 기호 상수이다. 문자 입력 함수는 버퍼의 존재 여부와 반향 여부에 따라 구분된다.

·일부의 문자 입력 함수는 버퍼를 가진다(buffered). 이것은 enter키를 누를 때까지 입력된 문자를 운영체제가 임시 저장 영역에 보관해둔 상태에서 시스템이 문자를 stdin으로 보낸다는 것을 뜻한다. 다른 함수는 버퍼를 사용하지 않으며(unbuffered) 각각의 문자는 입력되는 즉시 stdin으로 전달된다.

·일부의 입력 함수는 각각의 문자가 입력될 때 자동으로 stdout에 반향하여 출력한다. 다른 함수는 문자를 반향하지 않는다. 이때, 문자는 stdout이 아니라 stdin으로 전달된다. stdout은 화면이므로 입력된 문자가 반향되어 나타난다. 버퍼의 사용 여부, 반향 여주에 따라 사용되는 문자 입력 함수를 살펴보자.

▶ getchar() 함수
 : 함수 getchar()은 스트림 stdin에서 다음 문자를 읽어들인다. 이 함수는 버퍼를 사용하고 문자 입력을 반향하며, 다음과 같은 원형을 가진다.

  int getchar(void);

getchar()의 사용 예는 <리스트 14.2>에 나타나 있다. 이 장의 후반부에서 설명할 putchar() 함수는 단순히 화면 상에 한 문자를 출력한다는 것을 기억하자.

<리스트 14.2> getchar() 함수의 사용 예  
 

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


 #include <stdio.h>


 main()

 {

    int ch;


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

       putchar(ch);


    return 0;

 }

=> 9번째 줄에서는 getchar() 함수를 호출하고 stdin에서 문자를 받아들이기 위해서 대기한다. getchar()는 버퍼를 사용하는 입력 함수이므로 Enter키를 누를 때까지는 아무런 문자도 읽어들이지 않는다. 그러나 입력되는 각각의 문자는 화면 상에 반향된다. Enter키를 누르면 문장 진행(newlint) 문자를 포함하여 입력된 모든 문자가 운쳧체제에 의해서 stdin으로 전달된다. getchar() 함수는 입력된 순서대로 한 번에 한 문자씩 ch에 할당하여 문자를 돌려준다. 각각의 문자는 문장 진행 문자 '\n'과 비교되고 문장 진행 문자가 아니라면 putchar()를 사용하여 화면상에 출력된다. getchar()가 문장 진행 문자를 돌려줄 때 while문이 종료된다.

getchar() 함수는 <리스트 14.3>에 나타나 있듯이 한 줄의 텍스트를 입력하는데 사용할 수 있다. 그러나 이렇게 한 줄의 텍스트를 입력하기 위해서는 이 장에서 나중에 설명할 다른 입력 함수를 사용하는 것이 좋다.

 <리스트 14.3> 한 줄의 텍스트를 입력하기 위해서 getchar() 함수를 사용하는 프로그램

 /* 문자열을 입력하기 위해 getchar() 사용 */


 #include <stdio.h>


 #define MAX 80


 main()

 {

    char ch, buffer[MAX + 1];

    int x = 0;


    while((ch == getchar()) != '\n' && x < MAX)

       buffer[x++] = ch;


    buffer[x] = '\0';


    printf("%s\n", buffer);


    return 0;

 }

▶ getch() 함수
 : getch() 함수는 스트림 stdin에서 다음 문자를 읽어들인다. 이 함수는 버퍼를 사용하지 않는 상태로 문자 입력을 반향 없이 수행한다. getch() 함수는 ANSI표준이 아니다. 즉, 이 함수는 모든 시스템에서 유효하지 않을 수 있고, 환경에 따라 다른 헤더 파일을 요구할 것이다. 일반적으로 getch()의 원형은 헤더 파일 CONIO.H에 다음과 같이 정의되어 있다.

  int getch(void);

 함수가 버퍼를 사용하지 않으므로 getch()는 Enter키를 누를 때가지 대기하지 않고 문자가 입력되는 즉시 각각의 문자를 돌려준다. getch()는 입력된 문자를 반향하지 않으므로 문자는 화면 상에 나타나지 않는다. <리스트 14.4>에 있는 프로그램은 getch()의 사용 예를 보여준다. <리스트 14.4> getch() 함수의 사용 예를 보여준다.

<리스트 14.4> getch() 함수의 사용 예

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

 /* ANSI 비호환 예제 */

 #include <stdio.h>

 #include <conio.h>


 main()
 {

    int ch;


    while((ch = getch()) != '\r')

       putchar(ch);


    return 0;

 }

-> 출 력

 Testing the getch() function

=> 이 프로그램을 실행하면 getch()는 키를 누르는 즉시 해당하는 문자를 돌려준다. 프로그램은 Enter키를 누를 때까지 대기하지 않는다. 반향이 없으므로 입력된 문자는 putchar() 함수에 의해서만 화면 상에 출력된다. getch()의 동작을 정확히 이해하기 위해서 10번째 줄의 마지막에 세미콜론을 추가하고, 11번째 줄인 putchar(ch)를 제거하자. 프로그램을 다시 실행할 때 입력한 문자가 아무 것도 화면에 반향되지 않음을 알 수 있을 것이다. getch() 함수는 화면에 문자를 반향하지 않고 읽어들인다. 원래의 리스트가 문자를 출력하기 위해 putchar()를 사용했으므로 화면에 나타난다는 것을 알 수 있다.

 이 프로그램에서는 왜 각각의 문자를 \n이 아니라 \r과 비교하는 것일까? 코드 \r은 개행 문자(carriage return)를 뜻하는 이스케[이프 시퀀스이다. Enter키를 누를 때 키보드는 stdin으로 개행 문자를 전달한다. 버퍼를 사용하는 문자 입력 함수는 개행 문자를 자동으로 문장 진행 문자로 변환하므로 프로그램에서는 Enter키의 입력을 확인하기 위해서 \n과 비교해야 한다. 그러나 버퍼를 사용하지 않는 입력 함수는 개행 문자를 문장 진행 문자로 변환하지 않으므로 개행 문자는 \r로 입력된다. 그래서 프로그램에서 개행 문자를 비교하는 것이다.

 한 줄의 텍스트를 입력하기 위해서 getch()를 사용하는 방법이 <리스트 14.5>에서 설명된다. 이 프로그램을 실행해보면 getch()가 입력된 문자를 반향하지 않는다는 사실을 분명히 알 수 있을 것이다. 이 프로그램은 getchar()가 getch()로 대치되어 있다는 것을 제외하고는 <리스트 14.3>과 동일하다.

<리스트 14.5> 한 줄을 입력하기 위한 getch() 함수의 사용 예

 /* 문자열을 입력하기 위해 getch(0 사용 */

 /* ANSI 비호환 예제 */

 #include <stdio.h>

 #include <conio.h>


 #define MAX 80


 main()

 {

    char ch, buffer[MAX + 1];

    int x = 0;


    while (( ch = getch()) != '\r' && x < MAX)

       buffer{x++] = ch;


    buffer[x] = '\0';


    printf("%s", buffer);


    return 0;

 }

-> 입력/출력
 Here's a string
 Here's a string

▶ getche() 함수
 : getche()는 각각의 문자를 stdout에 반향한다는 것을 제외하고는 getch()와 비슷하므로 쉽게 이해할 수 있을 것이다. getch() 대신에 getche()를 사용하기 위해서 <리스트 1.4.>에 있는 프로그램을 변경하자. 프로그램을 실행할 때 입력되는 각각의 키는 화면 상에 두 번 출력된다. 한 번은 getche()에 의해서 반향되는 것이고, 한 번은 putchar()에 의해서 출력되는 것이다.

▶ getc()와 fgetc() 함수
 : 문자 입력 함수 getc()와 fgetc()는 자동으로 stdin을 사용하지 않는다. 대신에, 프로그램에서 입력 스트림을 지정하게 해준다. 이 함수들은 나중에 상세히 설명할 디스크 파일에서 문자를 읽어들이는 경우에 기본적으로 사용된다.

▶ ungetc()를 사용하여 문자를 '되돌리는' 방법
 : 문자를 '되돌린다'는 것은 무엇을 뜻하는 것일까? 예를 들어 알아보도록 하자. 프로그램이 입력 스트림에서 문자를 읽어들이고 필요하지 않은 문자가 입력될 때 입력이 끝났다는 것을 인식한다고 가정하자. 예를 들어, 숫자만을 입력할 수 있는 프로그램에서는 숫자가 아닌 문자가 처음 나타날 때 이력이 종료된다는 것을 알 수 있을 것이다. 숫자가 아닌 문자는 전체적인 데이터에서 의미있는 내용이지만 입력 스트림을 통해서 받아들여진 후에 무시된다. 그렇다면 이 문자는 사라진 것일까? 그렇지 않다. 이 문자는 다음 입력 동작에서 첫 번째 문자로 읽어들일 수 있도록 스트림에 '되돌려지거나' 전달될 수 있다.

 문자를 '되돌리기' 위해서는 라이브러리 함수 ungetc()를 사용한다. 함수는 다음과 같은 원형을 가진다.

  int ungetc(int ch, FILE *fp);

인수 ch는 되돌려지는 문자이다. 인수 *fp는 문자를 되돋리는 대상 스트림을 지정하는 것으로 어떤 입력 스트림이든지 될 수 있다. 두 번째 인수로는 대개 stdin을 사용하면 된다. 즉, ungetc(ch, stdin);을 사용한다. FILE *fp는 디스크 파일과 관련된 스트림에서 사용되는 것으로 나중에 상세히 설명할 것이다.

 각각의 읽기 동작에서는 스트림에 단지 한 문자만을 '되돌릴' 수 있고 EOF 문자는 항상 '되돌려질' 수 없다. 함수 UNGETC()는 성공적으로 실행되면 ch를 돌려주고, 만약 문자를 스트림에 되돌릴 수 없으면 EOF를 돌려준다.

3.2 문장의 입력
 : 문장 입력 함수는 입력 스트림에서 한 줄의 문자열을 읽어들이는데, 문장 진행 문자까지의 모든 문자를 읽어들인다. 표준 라이브러리에서는 두 개의 문장 입력 함수 gets()와 fgets()가 제공된다.

▶ gets() 함수
 : 열번째 강의 "문자와 문자열"에서는 gets()함수를 소개했었다. 이 함수는 stdin에서 한 줄을 읽어들이고 문자열로 저장하는 간단한 함수이다. 함수의 원형은 다음과 같다.

  char *gets(char *str);

함수 원형은 쉽게 이해할 수 있을 것이다. gets()는 char형에 대한 포인터를 인수로 받아들이고 char형에 대한 포인터를 돌려준다. gets() 함수는 문장 진행 문자(\n)가 나타나거나 파일의 마지막에 도달할 때까지 stdin에서 문자를 읽어들인다. 문장 진행 문자는 널 문자로 대치되고 문자열은 str이 지적하는 영역에 저장된다.

 복귀값은 str과 동일하게 문자열에 대한 포인터이다. gets()를 수행할 때 에러가 발생하거나 도는 어떤 문자가 입력되기 전에 파일의 마지막에 도달하면 함수는 널 포인터를 돌려준다.

 gets()를 호출하기 전에는 문자열을 지정하기 위한 충분한 메모리 영역을 할당해야 한다. 함수는 str이 지적하는 부분에 저장 영역이 할당되었는지의 여부를 전형 알지 못한다. 문자열은 어떤 경우든지 입력되고 str에서부터 시작하여 저장된다. 만약 저장 영역이 할당되지 않았다면, 문자열은 다른 어떤 데이터를 대치하게 되므로 프로그램은 문제를 일으킬 것이다.

<리스트 10.5>와 <리스트 10.6>은 gets()를 사용한다.

▶ fgets() 함수
 : fgets() 라이브러리 함수는 입력 스트림에서 한 줄의 텍스트를 읽어들인다는 점에서 gets()와 비슷하다. 이 함수는 프로그래머가 사용하는 특정 입력 스트림과 입력되는 최대 문자의 수를 지정할 수 있게 해주므로 더욱 유용하다. fgets() 함수는 나중에 설명할 디스크 파일에서 텍스트를 읽어들이는 경우에 가금 사용된다. stdin에서 입력을 수행하는 경우에 이 함수를 사용하려면 입력 스트림으로 stdin을 지정해야 한다. fgets()의 원형은 다음과 같다.

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

마지막에 있는 매개 변수 FILE *fp는 입력 스트림을 지정하는 데 사용된다. 여기에서는 스트림 인수로 간단히 표준 입력 스트림인 stdin을 지정하자. 포인터 str은 입력 문자열이 저장되는 곳을 지적한다. 인수 n은 입력되는 최대 문자의 수를 나타낸다. fgets() 함수는 문장 진행 문자가 나타나거나, 파일의 마지막에 도달하거나, 또는 n - 1문자가 입력될 때까지 지정된 입력 스트림에서 문자를 읽어들인다. 입력된 문자열에는 \0이 추가되기 전에 문장 진행 문자가 포함된다. fgets()의 복귀값은 gets()에서 설명한 것과 동일하다. 정확히 말해서, 문장 진행 문자로 긑나는 일련의 문자를 한 줄의 텍스트라고 정의한다면 fgets()는 한 줄의 텍스트를 읽어들이지 않는다. 한 줄의 내용이 n - 1 문자보다 많은 것으로 구성된다면 함수는 전체적인 줄의 내용보다 적은 것을 읽어들일 것이다. stdin에서는 Enter키를 누를 때까지 fgets()에서 제어가 복귀되지 않고 처음에 입력되는 n - 1 문자라면 문장 진행 문자가 문자열에 포함된다. <리스트 14.6>에 있는 프로그램은 fgets() 함수의 사용 예를 보여준다.

<리스트 14.6> 키보드 입력을 받아들이기 위한 fgets()의 사용 예

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


 #include <stdio.h>


 #define MAXLEN 10


 main()

 {

    char buffer[MAXLEN];


    puts("Enter text a line at a time; enter a blank to exit.");


    while(1)

    {

       fgets(buffer, MAXLEN, stdin);


       if(buffer[0] == '\n')

          break;


       puts(buffer);

    }

    return 0;

 }


3.3 형식화된 입력
 : 지금까지 설명한 입력 함수는 단순히 입력 스트림에서 하나 이상의 문자를 받아들이고 메모리 영역에 저장하는 것이었다. 입력된 문자는 변환되거나 형식화되지 않았으며, 숫자 변수에 대해서 여러 개의 문자를 입력하여 저장하는 방법도 없었다. 예를 들어, 12.86의 값을 키보드에서 입력하고 어떻게 float형 변수에 할당할 수 있을까? scanf()와 fscanf() 함수를 사용하면 된다. scanf()는 항상 stdin을 사용하는 반면에 fscanf()에서는 입력 스트림을 지정할 수 있다는 것을 제외하면 두 함수는 동일한 것이다. 여기에서는 scanf()를 다룰 것이다.

▶ scanf() 함수의 인수
 : scanf() 함수는 변칙적인 개수의 인수를 받아들인다. 함수는 최소한 두 개의 인수를 필요로 한다. 첫 번째 인수는 입력된 내용을 변환하는 방법을 scanf()에게 알려주기 위해서 특수한 문자를 사용하는 형식화 문자열이다. 두 번째 인수와 추가로 사용 가능한 인수는 입력된 데이터가 저장되는 변수의 주소이다. 다음은 예이다.

  scanf("%d", &x);

 첫 번째 인수 '%d'는 형식화 문자열이다. 여기에서 %d는 scanf()가 부호 있는 정수형값을 읽어들이도록 지시한다. 두 번째 인수는 입력된 값을 scanf()가 변수 x에 저장하도록 지시하기 위해서 주소(address of) 연산자인 &를 사용하고 있다. 이제, 형식화 문자열에 대해서 상세히 알아보도록 하자. scanf()의 형식화 문자열에는 다음과 같은 것이 포함될 수 있다.

·빈칸이나 탭 - 이것은 무시되지만 형식화 문자열을 더욱 읽기 쉽게 만들기 위해서 사용될 수 있다.

·%가 아닌 문자 - 이것은 입력되는 내용에서도 일치해야 하는 공백이 아닌 문자를 뜻한다.

·% 문자와 특수한 의미를 가지는 문자로 구성되는 하나 이상의 변환 문자(conversion specificaion) 일반적으로 형식화 문자열은 각각의 변수에 대응하는 하나의 변환 문자를 가지고 있다. 형식화 문자열에 반드시 포함되어야 하는 것은 변환 문자이다. 각각의 변한 문자는 백분율 기호(%)로 시작하고, 특별한 순서에 의해서 선택적으로 사용되거나 또는 반드시 포함되어야 하는 구성 요소를 가진다. scanf() 함수는 형식화 문자열에 포함되어 있는 변환 문자를 입력 필드에 순서대로 사용한다. 입력필드(input field)는 공백이 나타날 때 또는 폭이 지정되었다면 필드의 폭만큼 문자가 입력될 때 종료되는 공백이 아닌 일련의 문자들을 말한다. 변환 문자는 다음과 같이 구성된다.

·% 문자 바로 다음에 입력되는 선택적인 할당 제한(assignment suppression flag) 문자(*) - 이것은 scanf()가 현재의 변환 문자에 대응하는 변환을 수행하지만 결과를 무시하도록 지시한다. 즉, 값을 어떤 변수에 할당하지 않는다.

·선택적으로 사용되는 필드 폭(field width) - 필드 폭은 입력 필드의 폭을 문자 단위로 지정하는 값이다. 즉, 필드 폭은 scanf()에서 현재의 변환 동작을 수행하기 위해서 stdin에서 몇 개의 문자를 입력해야 하는지 지정한다 필드 폭을 지정하지 않으면 입력 필드의 폭은 다음에 나타나는 공백까지로 정해진다.

·선택적으로 사용되는 하나의 정밀도 지정 문자(precision modifier) - 이것은 h, l, L이 될 수 있다. 정밀도 지정 문자가 포함되면 형 지정자의 의미가 바뀐다.

·형 지정자(type specifier) - 이것은 변환 문자에서 % 외에 반드시 포함되어야 하는 하나의 구성 요소이다. 형 지정자는 scanf()가 입력 내용을 받아들이는 방법을 알려주는 하나 이상의 문자이다.

각각의 문자는 <표 14.3>에 나타나 있다. 인수는 대응하는 변수의 형태를 뜻한다. 예를 들어, 형지정자 d는 int형에 대한 포인터를 뜻하는 int *를 요구한다.

<표 14.3> 변환 문자에 포함되는 형 지정자

형태

인수

의미

d

int *

 10진 정수형

i

int *

 10진 정수형, 앞에 0을 포함하는 8진 정수형, 앞에 0x나 0X를

 포함하는 16진 정수형

o

int *

 앞에 0을 포함하거나 포함하지 않는 8진 정수형

u

unsigned int *

 부호 없는 10진 정수형

x

int *

 앞에 0x나 0X를 포함하거나 포함하지 않는 16진 정수형

c

char *

 하나 이상의 문자를 읽어들이고 인수에 의해서 지적되는 메모리

 영역에 순서대로 저장한다. \0은 추가되지 않는다.

 필드 폭이 지정되지 않으면 한 문자를 읽어들이고, 필드 폭이

 주어지면 공백을 포함하여 지정된 수의 문자를 읽어들인다.

s

char *

 공백이 아닌 문자로 구성되는 문자열을 지정된 메모리 영역에

 저장하고 \0을 추가한다.

e, f, g

float *

 부동 소수형 숫자. 숫자는 소수점 형태나 공학식 표기 방법으로

 입력할 수 있다.

[...]

char *

 문자열. 괄호 내에 포함되어 있는 문자만 입력된다. 입력 동작은

 일치하지 않는 문자가 나타나거나, 지정된 필드폭이 사용되거나

 Enter키를 누르는 즉시 종료된다. 문자 ]를 입력하기 위해서는

 []...]와 같이 처음에 위치시켜야 한다.

 문자열의 마지막에는 \0이 추가된다.

[^...]

char *

 괄호 내에 포함되지 않은 문자가 입력되는 것을 제외하면 [...]와

 동일하다.

%

없음

 문자 그대로의 % 문자를 읽어들인다. 할당 동작은 수행되지

 않는다.

scanf()의 사용 예를 살펴보기 전에 <표 14.4>에 나열된 정밀도 지정 문자에 대해서 이해할 필요가 있다.

<표 14.4> 정밀도 지정 문자

정밀도 지정 문자

의미

h

 문자 h는 형 지정자 d, i, n, o, u, x 앞에서 사용될 때 인수가 int형이

 아니라 short형에 대한 포인터라는 것을 지정한다. PC에서는 short형이

 int형과 동이하므로 정밀도 지정 문자 h는 전혀 필요하지 않다.

l

 문자 l은 형 지정자 d, i, n, o, u, x 앞에서 사용될 때 인수가 long형에

 대한 포인터라는 것을 지정한다. 문자 l은 형 지정자 e, f, g 앞에서

 사용될 때 인수가 double 형에 대한 포인터라는 것을 지정한다.

L

 문자 L은 형 지정자 e, f, g 앞에서 사용될 때 인수가 long double형에

 대한 포인터라는 것을 지정한다.

▶ 나머지 문자 다루기
 : scanf()에서 입력되는 내용은 벞에 저장된다. Enter키를 투를 때까지는 stdin에서 아무런 문자도 읽어들이지 않는다. Enter키를 누르면 전체 내용을 stdin에서 '읽어들이고' scanf()에서 순서대로 사용된다. scanf()의 형식화 문자열에 포함되어 있는 변환 문자에 일치하는 내용이 모두 입력되면 함수의 동작은 종료된다. 또한, scanf()는 형식화 문자열에서 지정된 변환 문자에 대응하는 내용만을 stdin에서 받아들인다, 추가 문자나 불필요한 문자가 있다면 stdin에서 '나머지로' 남게 된다. 이런 문자들은 문제를 일으킬 수 있는데, 어떤 문제가 발생하는지 알아보기 위해서 scanf()의 동작을 좀더 상세히 살펴보도록 하자. scanf() 함수가 호출되고 한 줄의 데이터를 입력할 때에는 다음과 같은 세 가지 상황이 발생할 수 있다. 우선, scanf("%d %d", &x, &y);라는 문장을 실행 중이라고 가정하자. 즉, scanf()는 두 개의 10진 정수형 값을 읽어들인다. 여기에서는 다음과 같은 상황이 발생할 수 있다.

·입력되는 내용이 형식화 문자열에 일치하는 경우
예를 들어, 12 14를 입력하고 Enter를 누르면 문제가 발생하지 않는다. scanf()는 필요한 내용을 받아들이고 stdin에는 나머지 문자가 존재하지 않는다.

·입력되는 내용이 형식화 문자열에서 요구하는 것보다 적은 요소를 포함하고 있는 경우
예를 들어, 12를 입력하고 Enter를 누르면 scanf()는 부족한 내용을 받아들이기 위해서 계속 대기한다. 일단 필요한 내용을 추가로 입력하면 실행이 계속되고 stdin에는 나머지 문자가 존재하지 않는다.

·입력되는 내용이 형식화 문자열에서 요구하는 것보다 많은 요소를 포함하고 있는 경우
예를 들어, 12 14 16을 입력하고 Enter를 누르면 scanf()는 12와 14를 읽어들이고 나서 프로그램으로 돌아간다. 나머지 문자인 1과 6은 stdin에서 남게 된다. 문제를 일으킬 수 있는 것은 세 번째 상황이고, 특히 나머지 문자들이 문제다. 이렇게 남은 문자들은 프로그램이 실행되는 동안 stdin에서 다시 입력을 받아들일 때까지 계속해서 남게 된다. 나중에 프로그램이 stdin에서 다시 입력을 받아들이면 나머지 문자는 다른 어떤 입력 내용보다 먼저 읽어들여진다. 이때 문제가 발생한다는 것은 쉽게 알 수 있다. 예를 들어, 다음 코드는 사용자에게 정수를 입력하고 나서 문자열을 입력하도록 요구한다.

  puts("Enter your age.");
  scanf("%d", &age);
  puts("Enter your first name.");
  scanf("%s", name);

 처음에 더욱 정확한 정수값을 입력하기 위해서 29.00을 입력하고 Enter를 누른다고 가정하자 첫 번째 scanf() 함수는 정수값을 받아들이므로 stdin에서 문자 29를 읽어들이고 변수 age에 할당한다. 문자 .00은 stdin에서 대기 상태로 남게 된다. 두 번째 scanf() 함수는 문자열을 받아들인다. 함수는 입력을 받아들이기 이해서 stdin을 사용하고, 대기중인 .00을 발견하게 된다. 결과적으로, 문자열 .00이 name에 저장된다.

 이 문제를 어떻게 방지할 수 있을까? 프로그램을 사용하는 사람이 이런 실수를 하지 않는다면 문제를 해결할 수 있을 것이다. 그러나 이것은 효과적인 에러 방지책이 아니다. 좀더 좋은 해결 방안은 입력 동작을 수행하기 전에 stdin에 문자가 남아 있지 않도록 해주는 것이다. stdin에서 문장의 마지막을 뜻하는 문자까지 모든 내용을 읽어들이는 gets() 함수를 호출하면 문제를 해결할 수 있다. 또한, 프로그램에서 gets()를 직접 호출하는 것보다는 좀더 해설적인 이름을 가지고 있는 clear_kb() 함수를 작성하는 것도 좋은 생각이다. <리스트 14.7>에서는 이 함수를 사용한다.

<리스트 14.7> 에러를 방지하기 위해서 stdin에서 나머지 문자를 제거하는 프로그램

 /* stdin에서 나머지 문자 제거 */


 #include <stdio.h>


 void clear_kb(void);


 main()

 {

    int age;

    char name[20];


    /* 사용자의 나이 입력 */


    puts("Enter your age.");

    scanf("%d", &age);


    /* stdin에서 나머지 문자 제거 */


    clear_kb();


    /* 사용자의 이름 입력 */


    puts("Enter your first name.");

    scanf("%s", name);

    /* 데이터 출력 */


    printf("Your age is %d.\n", age);

    printf("Your name is %s.\n", name);


    return 0;

 }


 void clear_kb(void)


 /* stdin에서 나머지 문자 제거 */

 {

    char junk[80];

    gets(junk);

 }

▶ fflush()로 나머지 문자 다루기
 : 입력된 나머지 문자를 정리할 수 있는 두 번째 방법이 있다. fflush() 함수는 표준 입력 스트림을 포함하여 특정 스트림에서 데이터를 지운다. fflush()는 일반적으로 디스크 파일에서 사용된다. 그러나 이 함수는 <리스트 14.7>을 더 간단히 만드는 데 사용될 수도 있다. <리스트 14.8>은 <리스트 14.7>에서 생성된 clear_kb() 함수 대신에 fflush() 함수를 사용하고 있다.

<리스트 14.8> fflush()를 사용하여 stdin에서 나머지 문자 지우기

 /* stdin에서 나머지 문자 제거 */

 /* fflush() 함수 사용 */

 #include <stdio.h>


 main()

 {

    int age;

    char name[20];


    /* 사용자의 나이 입력 */

    puts("Enter your age.");

    scanf("%d", &age);


    /* stdin에서 나머지 문자 제거 */

    fflush(stdin);


    /* 사용자의 이름 입력 */

    puts("Enter your first name.");

    scanf("%s", name);


    /* 데이터 출력 */

    printf("Your age is %d.\n", age);

    printf("Your name is %s.\n", name);


    return 0;

 }

▶ scanf()의 사용 예
 : scanf() 함수의 사용 방법을 익히는 가장 좋은 방법은 직접 사용해보는 것이다. 이 함수는 매우 강력하지만 처음에는 다소 혼란스러울 것이다. 함수를 사용하고 나타나는 결과를 살펴보자. <리스트 14.9>에 있는 프로그램은 scanf()를 사용하는 약간 독특한 방법을 보여준다. 이 프로그램을 컴파일하고 실행한 후에 scanf()의 형식화 문자열의 내용을 변경해가며 실험해보도록 하자.

<리스트 14.9> 키보드 입력을 받아들이기 위해서 scanf()를 사용하는 다양한 방법

 /* scanf()의 몇 가지 사용 예 */


 #include <stdio.h>


 main()

 {

    int i1, i2;

    long l1;


    double d1;

    char buf1[80], buf2[80];


    /* long 정수형과 배정도형을 입력하기 위해 사용 */


    puts("Enter an integer and a floating point number.");

    scanf("%ld %lf", &l1, &d1);

    printf("\nYou entered %ld and %lf.\n", l1, d1);

    puts("The scanf() format string used the l modifier to store");

    puts("your input in a type long and a type double.\n");


    fflush(stdin);


    /* 입력을 나누기 위해 필드 폭 지정 */


    puts("Enter a 5 digit integer (for example, 54321).");

    scanf("%2d%3d", &i1, &i2);


    printf("\nYou entered %d and %d.\n", i1, i2);

    puts("Note how the field width specifier in the scanf() format");

    puts("string split your input into two values.\n");


    fflush(stdin);


    /* 입력 내용을 두 문자열로 나누기 위해 형 지정자 사용 */


    puts("Enter your first and last names separated by a space.");

    scanf("%[^ ]%s", buf1, buf2);

    printf("\nYour first name is %s\n", buf1);

    printf("Your last name is %s\n", buf2);

    puts("Note how [^ ] in the scanf() format string, by excluding");

    puts("the space character, caused the input to be split.");


    return 0;

 }

4. 화면 출력
 : 화면 출력 함수는 입력 함수와 마찬가지로 대개 세 가지 종류로 구분된다. 즉, 문자 출력, 문장 출력, 형식화된 출력을 수행하는 함수가 있다. 앞에서는 몇 가지 함수를 설명했다. 여기에서는 좀더 상세히 살펴보도록 하자.

4.1 putchar(), putc(), fputc()를 사용한 문자 출력
 : C 라이브러리의 문자 출력 함수는 한 문자를 스트림으로 전달한다. 함수 putchar()는 일반적으로 화면을 뜻하는 stdout에 출력 내용을 보낸다. 함수 fputc()와 putc()는 출력 내용을 인수로 지정된 스트림으로 보낸다.

▶ putchar() 함수의 사용
 : putchar의 원형은 다음과 같이 STDIO.H에 정의되어 있다.

  int putchar(int c);

 함수는 c로 표현되는 문자를 stdout에 출력한다. 비록 원형이 int형 인수를 사용하도록 지시하고 있지만, 프로그램에서는 char형을 putchar()에 전달한다. 또한, 문자의 값이 0에서부터 255가지의 범위에 포함되는 것이라면 int형 값을 전달할 수 있다. 함수는 출력된 문자를 돌려주며, 에러가 발생한다면 EOF를 돌려준다. <리스트 14.2>에서는 이미 putchar()를 사용했었다. <리스트 14.10>에 있는 프로그램은 14와 127사이의 ASCII값을 가지는 문자를 출력한다.

<리스트 14.10> putchar() 함수

 /* putchar()의 사용 예 */


 #include <stdio.h>

 main(0

 {

    int count;


    for(count = 14; count < 128; )

       putchar(count++);


    return 0;

 }

문자열을 출력하기 위해서 다른 함수를 사용하는 것이 더 나을 수도 있겠지만 <리스트 14.11>에 나타나 있듯이 putchar() 함수를 사용할 수 있다.

<리스트 14.11> putchar()를 사용한 문자열 출력

 /* 문자열을 출력하기 위해 putchar() 사용 */


 #include <stdio.h>


 #define MAXSTRING 80


 char message[] = "Displayed with putchar().";

 main()

 {

    int count;


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

    {


       /* 문자열의 마지막을 검색하고 문장 진행 문자로 대치한 후 순환문을 마친다 */


       if(message[count] == '\0')

       {

          putchar('\n');

          break;

       }

       else


       /* 문자열의 마지막이 없으면 다음 문자를 추력한다. */

          putchar(message[count]);

    }

    return 0;

 }

▶ putc()와 fputc() 함수의 사용
 : 이런 두 함수는 한 문자를 지정된 스트림으로 전달하여 출력한다. putc()는 fputc()를 매크로의 형태로 사용하는 것이다. 매크로는 낭중에 "컴파일러의 고급기능"에서 설명할 것이다. 여기에서는 일단 fputc()만을 설명하겠다. 함수 원형은 다음과 같다.

  int fputc(int c, FILE *fp);

원형에서 FILE *fp라는 내용은 정확히 이해되지 않을 것이다. fputc()에서는 출력 스트림을 지정하는 인수로 사용된다. 만약 스트림으로 stdout을 지정하면 fputc()는 putchar()와 완전히 똑같이 사용된다. 그래서 다음 두 문장은 동일하다.

  putchar('x');
  fputc('x', stdout);

4.2 문자열 출력을 위한 puts()와 fputs()의 사용
 : 프로그램은 화면 상에 한 문자를 출력하는 것만큼이나 자주 문자열을 출력한다. 라이브러리 함수 putc()는 문자열을 출력한다. 함수 fputc()는 문자열을 지정된 스트림으로 전달하는데, 스트림을 지정한다는 것을 제외하고는 puts()와 동일하다. puts()의 원형은 다음과 같다.

  int puts(char *cp);

 *cp는 출력하기 원하는 문자열의 첫 번째 문자에 대한 포인터이다. puts() 함수는 문자열의 마지막 부분에 문장 진행 문자를 추가하고 종료를 뜻하는 널 문자를 포함하지 않은 상태로 전체적인 문자열을 출력한다. puts()는 성공적으로 실행되면 양수 값을 돌려주고, 에러가 발생하면 EOF를 돌려준다. EOF는 -1의 값을 가지는 기호 상수라는 것을 기억하자. 이 상수는 STDIO.H에 정의되어 있다. puts() 함수는 모든 형태의 문자열을 출력하는 데 사용될 수 있다. <리스트 14.12>는 함수의 사용 예를 보여준다.

<리스트 14.12> 문자열을 출력하기 위한 puts() 함수의 사용 예

 /* puts()의 사용 예 */


 #include <stdio.h>


 /* 포인터의 배열 선언과 초기화 */


 char *messages[5] = {"This", "is", "a", "short", "message."};


 main()

 {

    int x;


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

       puts(messages[x]);


    puts("Add this is the end!");


    return 0;

 }

4.3 형식화된 출력을 수행하는 printf()와 fprintf()의 사용
 : 지금까지 설명한 출력 함수는 문자와 문자열만을 출력했다. 그렇다면 숫자를 출력하는 경우에는 어떻게 해야 할까? 숫자를 출력하기 위해서는 C 라이브러리의 형식화 출력 함수인 printf()와 fprintf()를 사용해야 한다. 또한, 이런 함수들은 문자열과 문자를 추력할 수도 있다 printf()는 지금까지 거의 모든 프로그램에서 사용했다. 여기에서는 좀더 상세한 내용을 알아보도록 하자.

 printf()가 항상 stdout으로 출력하는 반면에 fprintf()는 출력 스트림을 지정할 수 있다는 것을 제외하면 두 함수 printf()와 fprintf()는 동일하다. printf() 함수는 변칙적인 개수의 인수를 받아들이고 최소한 하나의 인수를 필요로 한다. 반드시 포함되어야 하는 첫 번째 인수는 printf()에게 출력을 형식화하는 방법을 알려주는 형식화 문자열(format string)이다. 선택적으로 사용되는 인수는 값을 출력하기 원하는 변수와 수식이다. printf()에 대한 상세한 내용을 다루기 전에 기본적인 개념을 이해하기 위해서 몇 가지 간단한 예를 살펴보도록 하자.

·printf("Hello, world.");라는 문장은 메시지 Hello, world.를 화면 상에 출력한다. 이 것은 단지 하나의 인수인 형식화 문자열만을 포함하고 잇는 printf()를 사용하는 예이다. 여기에서는 형식화 문자열이 화면 상에 출력되는 문자 그대로의 문자열을 가지고 있다.

·printf("%d", i);라는 문장은 화면 상에 정수형 변수 i의 값을 출력한다. 형식화 문자열은 printf()에게 하나의 십진 정수형 값을 출력하도록 지시하는 변환 문자 %d만을 포함하고 있다. 두 번째 인수인 i는 값을 출력할 변수의 이름이다.

·printf("%d plus %d equals %d.", a, b, a + b);라는 문장은 a와 b가 각각 2와 3의 값을 가지는 정수형 변수라고 가정했을 때 화면 상에 2 plus 3 equals 5를 출력한다. 여기에서는 printf()가 4개의 인수를 가지고 있다. 즉, 변환 문자와 문자 그대로의 텍스트를 가지는 형식화 문자열, 두 개의 변수, 합을 출력하기 위한 수식이다. 이제 printf() 형식화 문자열에 대해서 좀더 상세히 살펴보도록 하자. 형식화 문자열에는 다음과 같은 내용이 포함될 수 있다.

·printf()에게 인수 목록에 있는 값을 출력하는 방법을 알려주는 하나 이상의 변환 문자. 변환 문자는 %와 하나 이상의 문자로 구성된다.

·변환 문자가 아닌 있는 그대로 출력되는 문자

 앞에서 세 번째 예제에 포함된 형식화 문자열은 %d plus %d equals %d이다. 여기서 세 개의 %d는 변환 문자이고, 공백을 포함한 문자열의 나머지 부분은 그대로 출력되는 문자이다. 이제, 변환 문자에 대해서 알아보자. 변환 문자의 형식은 다음과 같고 자세한 내용은 잠시 후에 설형할 것이다. 대괄호 내에 포함되어 있는 구성 요소는 선택적으로 사용할 수 있는 것들이다.

  %[flag][field_width][.[precision]][l]conversion_char

 conversion_char는 변환 문자에서 %와 함께 반드시 포함되어야 하는 내용이다. 변환 문자의 의미는 <표 14.5>에 나타나 있다.

<표 14.5> printf()와 fprintf()의 변환 문자

변환 문자

의    미

d, i

 부호 있는 10진 정수형 출력

u

 부호 없는 10진 정수형 출력

o

 부호 없는 8진 정수형 출력

x, X

 부호 없는 16진 정수형 출력. x는 소문자로 출력하고 X는 대문자로 출력한다.

c

 하나의 문자 출력. 문자의 ASCII코드가 인수로 사용된다.

e, E

 공학식 표기로 float나 double형 출력. 예를 들어, 123.45는 1.234500+002로

 출력된다. 잠시 후에 설명할 정밀도를 지정하지 않으면 소수점 아래 6자리

 까지 출력된다. 출력에서 대소문자를 지정하기 위해서 e나 E를 사용하자.

f

 소수점 표기로 float나 double형 출력. 예를 들어, 123.45는 123.450000으로

 출력된다. 정밀도를 지정하지 않으면 소수점 아래 6자리까지 출력된다.

g, G

 e, E, f에서 하나를 사용한다. 만약 지수가 -3보다 작거나 또는 기본적으로

 6에 설정된 정밀도보다 크다면 e나 E 형식이 사용된다. 그렇지 않다면

 f 형식이 사용된다. 숫자값 뒤에 나타나는 의미 없은 0은 제거된다.

n

 아무 것도 출력하지 않는다. 변환 문자 n에 대응하는 인수는 int형에 대한

 포인터이다. printf() 함수는 이 변수에 지금까지 출력된 문자의 수를 할당한다.

s

 문자열 출력. 인수는 char에 대한 포인터이다. 널 문자가 나타나거나 기본적

 으로 32,767에 설정되어 있는 precision을 다르게 지정한다면 그만큼의

 문자가 출력될 때까지 문자가 출력된다. 마지막의 널 문자는 출력되지 않는다.

%

 문자 % 출력

변환 문자 앞에는 형 지정 문자(modifier) 1을 포함시킬 수 있다. 이 문자는 변환 문자 o, u, x, X, i, d, b에만 적용된다. 이 지정 문자는 인수가 int형이 아니라 long형의 정밀도를 가진다는 것을 지정한다. 문자 1이 변환 문자 e, E, f, g, G에서 사용되면 인수가 double형의 정밀도를 가진다는 것을 지정한다. 1을 다른 어떤 변환 문자 앞에서 사용하면 무시된다.

 정밀도 지정 문자는 소수점(.)과 숫자로 구성된다. 정밀도 지정 문자는 단지 변환 문자 e, E, f, g, G, s에서만 사용할 수 있는데, 소수점 아래에 출력되는 숫자의 자릿수를 지정하거나 또는 s에서 사용될 때에는 출력되는 문자의 수를 지정한다. 만약 소수점만 사용되면 정밀도가 0이라는 것을 나타낸다. 필드 폭 지정자는 출력되는 문자의 폭을 결정한다. 필드 폭 지정자에는 다음과 같은 것을 사용할 수 있다.

·0으로 시작하지 않는 십진 정수형 값
  출력되는 내용의 왼쪽 부분에는 지정된 필드 폭을 채우기 위해서 공백이 포함된다.

·0으로 시작하는 십진 정수형 값
  출력되는 내용의 왼쪽 부분에는 지정된 필드 폭을 채우기 위해서 0이 포함된다

·*문자
 int형의 다음 인수의 값을 필드 폭으로 사용한다. 예를 들어, w가 10의 값을 가지는 int형 변수라면 printf("%*d", w, a);라는 문장은 필드 폭이 10인 상태로 a의 값을 출력한다

필드 폭이 지정되지 않거나 또는 지정된 필드 폭이 출력 내용보다 좁다면 출력되는 내용은 필요한만큼 확장된 필드 폭을 사용하게 된다.

 printf()의 형식화 문자열에서 선택적으로 사용되는 마지막 부분은 문자 % 바로 다음에 나타나는 플래그(flag)이다. 네 개의 플래그를 사용할 수 있다.

·- : 출력되는 내용을 출력 필드 내에서 왼쪽 기준으로 정렬한다. 기봊적으로는 오른쪽 기준으로 정렬하도록 되어 있다.

·+ : 부호 있는 숫자를 출력할 때 항상 +나 -와 같은 부호를 함께 출력한다.

·`' : 양수를 출력할 때 앞부분을 빈칸으로 채워서 나타낸다.

·# : 변환 문자 x, X, o에만 적용된다. 0이 아닌 숫자를 x나 X에서는 0X나 0x와 함께 출력하고, o에서는 0과 함께 출력한다.

printf()를 사용할 때 형식화 문자열은 printf()의 인수 목록에서 큰 따옴표 내에 포함되어 있는 문자 그대로의 문자열이 될 수 있다. 또한 printf()에 문자열에 대한 포인터를 전달할 때에는 널 문자를 가지고 있으며, 메모리에 저장되어 있는 문자열을 사용할 수 있다. 예를 들어, 다음 문장은

  char *fmt = "The answer is %f.";
  printf(fmt, x);

다음과 같다.

  printf("The answer is %f.", x);

printf()의 형식화 문자열은 출력되는 내용을 제어하게 해주는 이스케이프 시퀀스를 포함할 수 있다. <표 14.6>에는 가장 자주 사용되는 이스케이프 시퀀스가 나타나 있다. 예를 들어, 형식화 문자열에 문장 진행 문자(\n)를 포함시키면 그 다음의 내용은 다음 줄부터 시작하여 출력된다.

<표 14.6> 가장 자주 사용되는 이스케이프 시퀀스

부호

의미

\a

 경고음(bell)

\b

 백스페이스(backspace)

\n

 문장 진행(newline)

\t

 수평 탭

\\

 백슬래시

\?

 물음표

\'

 작은 따옴표

\"

 큰 따옴표

printf()는 상당히 복잡한 함수이다. 이 함수의 사용법을 익히는 가장 좋은 방법은 예제를 살펴보고 직접 사용해보는 것이다. <리스트 14.13>에 있는 프로그램은 prntf()를 사용할 수 있는 다양한 방법을 보여주고 있다.

<리스트 14.13> printf() 함수를 사용하는 다양한 방법

 /* printf()의 사용 에 */


 #include <stdio.h>


 char *m1 = "Binary";

 char *m2 = "Decimal";

 char *m3 = "Octal";

 char *m4 = "Hexadecimal";


 main()

 {

    float d1 = (float) 10000.123;

    int n = 0;


    puts("Outputting a number with different field widths.\n");


    printf("%5f\n", d1);

    printf("%10f\n", d1);

    printf("%15f\n", d1);

    printf("%20f\n", d1);

    printf("%25f\n", d1);


    puts("\n Press Enter to continue...");

    fflush(stdin);

    getchar();


    puts("\nuse the * field width specifier to obtain field width");

    puts("from a variable in the argument list.\n");


    for(n = 5; n <= 25; n += 5)

       printf("%*f\n", n, d1);


    puts("\n Press Enter to continue...");

    fflush(stdin);

    getchar();


    puts("\nInclude leading zeros.\n");


    printf("%05f\n", d1);

    printf("%010f\n", d1);

    printf("%015f\n", d1);

    printf("%020f\n", d1);

    printf("%025f\n", d1);


    puts("\n Press Enter to continue...");

    fflush(stdin);

    getchar();


    puts("\nDisplay in octal, decimal, and hexadecimal.");

    puts("use # to precede octal and hex output with 0 and 0X.");

    puts("use - to left-justify each value in its field.");

    puts("First display column labels.\n");


    printf("%-15s%-15s%-15s", m2, m3, m4);


    for(n = 1; n < 20; n++)

       printf("\n%-15d%-#15o%-#15x", n, n, n);


    puts("\n Press Enter to continue...");

    fflush(stdin);

    getchar();


    puts("\n\nuse the %n conversion command to count characters.\n");


    printf("%s%s%s%s%n", m1, m2, m3, m4, &n);


    printf("\n\nThe last printf() output %d characters.\n", n);


    return 0;

 }

5. 입력과 출력의 전환
 : stdin과 stdout을 사용하는 프로그램에서는 방향 전환(redirection, 또는 재지정)이라는 운영체제의 특성을 이용할 수 있다. 전환 기능을 통해서는 다음과 같은 결과를 얻을 수 있다.

·stdout으로 출력되는 내용을 화면이 아니라 디스크 파일이나 프린터로 전달할 수 있다.

·stdin을 사용하는 프로그램의 입력 동작을 키보드가 아니라 디스크 파일에서 수행할 수 있다.

 이런 전환 기능은 프로그램에서 수행되는 것이 아니라 프로그램을 실행할 때 DOS의 명령 라인(command line)에서 지정된다. DOS와 UNIX에서 입출력을 전환시킬 때 사용하는 기호는 >와 <이다. 우선 출력의 전환에 대해서 알아보도록 하자.

 여기서 처음 소개한 HELLO.C 프로그램을 기억할 수 있는가? HELLO>C는 'Hello, world'라는 내용을 화면 상에 출력하기 위해서 라이브러리 함수 printf()를 사용했었다. 지금까지 설명한 내용에 의하면 printf()는 출력 내용을 stdout으로 전달하므로 출력을 전환시키는 것이 가능하다. 명령 프롬프트에서 프로그램의 이름을 입력할 때 > 기호와 새로운 출력 장치의 이름을 입력하면 된다.

  hello > destination

hello > prn을 입력하면 프로그램의 출력은 화면이 아니라 프린터를 통해서 나타난다. prn은 DOS에서 사용되는 포트 LPT1:에 접속된 프린터의 이름이다. hello > hello.txt를 입력하면 출력 내용은 hello.txt라는 이름의 디스크 파일에 저장될 것이다.

 출력을 디스크 파일로 전환시킬 때에는 주의해야 한다. 지정된 파일이 이미 존재한다면 이전에 저장되어 있던 내용은 삭제되고 새로운 내용의 파일로 바뀔 것이다. 파일이 존재하지 않는다면 새롭게 생성된다. 또한, 출력을 파일로 전환시킬 때에는 >> 기호를 사용할 수도 있다. 이 기호는 지정된 파일이 이미 존재할 때 프로그램의 출력 결과를 파일의 마지막 부분에 추가한다. <리스트 14.14>에 있는 프로그램은 방향 전환의 예를 보여준다.

<리스트 14.14> 입력과 출력을 전환시키는 예를 보여주는 프로그램

 /* stdin과 stdout의 재지정 예제 */


 #include <stdio.h>


 main()

 {

    char buf[80];


    gets(buf);

    printf("The input was: %s\n", buf);

    return 0;

 }

5.1 입력의 전환
 : 이제, 입력의 전환에 대해서 알아보자. 우선, 자료 파일이 필요하다. 에디터를 사용하여 William Shakespeare라는 데이터를 가지는 INPUT.TXT라는 이름의 파일을 생성하자. 다음과 같은 내용을 DOS 프롬프트에서 입력하여 <리스트 14.14>를 실행하자.

  list1414 < INPUT.TXT

프로그램은 키보드에서 데이터를 입력하도록 요구하지 않을 것이다. 대신에, 실행되는 즉시 화면 상에 메시지를 출력한다.

  The input was: William Shakespeare

스트림 stdin은 디스크 파일인 INPUT.TXT로 전환되었으므로, 프로그램의 gets() 함수는 키보드가 아니라 지정된 파일에서 한 줄의 텍스트를 읽어들인다. 또한, 입력과 출력을 동시에 전환시킬 수도 있다. stdin을 파일 INPUT.TXT로 지정하고, stdout를 JUNK.TXT로 지정하기 위해서 다음 명령을 입력하여 프로그램을 실행해보자.

  list1414 < INPUT.TXT > JUNK.TXT

stdin과 stdout을 전환시키는 것은 특별한 상항에서 아주 유용할 것이다. 예를 들어, 정렬(sorting) 프로그램은 키보드에서 입력되는 내용이나 디스크 파일에 저장된 데이터를 모두 분류할 수 있을 것이다. 비슷하게, 우편용 주소록 관리 프로그램은 화면 상에 주소를 출력하고 우편용 레이블을 인쇄하기 위해서 프린터로 결과를 전달하거나 또는 다른 목적을 위해서 파일에 저장할 수도 있을 것이다.

6. fprintf()를 사용하는 경우
 : 앞에서도 언급했듯이, 라이브러리 함수 fprintf()는 출력 내용이 전달되는 스트림을 지정할 수 있다는 것을 제외하면 printf()와 동일하다. fprintf()의 용도는 주로 디스크 파일과 관련되어 있다. 여기에서는 두 가지 다른 용도를 알아보도록 하자.

6.1 stderr의 사용
 : Cㅇ서 정의되어 있는 스트림의 하나는 표준 에러인 stderr이다. 프로그램의 에러 메시지는 일반적으로 stdout이 아니라 stderr로 전달된다. 왜 그럴까? 방금 설명했듯이 stdout에 대한 출력 동작은 화면이 아닌 다른 곳으로 변경되어 수행될 수 있다. 에러 메시지가 stdout으로 출력된다면 stdout을 전환시킬 때 프로그램이 출력하는 어떤 에러 메시지도 볼 수 없을 것이다. stdout과 달리 stderr은 전환될 수 없고 항상 화면에 연결되어 있다. 적어도, DOS에서는 그렇다. UNIX 시스템은 stderr의 방향 전환을 허용할 것이다. 그래서 DOS에서는 에러 메시지를 stderr로 출력하면 사용자가 항상 에러 메시지를 볼 수 있는 것이다. 이것은 fprintf()를 사용하여 수행된다.

  fprintf(stderr, "An error has occurred.");

프로그램에서는 fprintf()를 직접 사용하지 않고 에러를 처리하는 함수를 작성하고 나서 에러가 발생할 때 함수를 호출할 수 있다.

  error_message("An error has occurred.");
     void error_message(char *msg)
     {
        fprintf(stderr. msg);
     }

fprintf()를 직접 호출하지 않고 함수를 만들어 사용하면 구조화 프로그래밍의 한 가지 장점인 추가적인 융통성을 발휘할 수 있다. 예를 들어, 특정 상황에서는 프로그램의 에러 메시지가 프린터나 디스크 파일로 출력되기 원할 것이다. 이럴 때 원하는 곳에서 출력 동작을 수행하도록 error_message() 함수를 변경하면 된다.

6.2 DOS에서의 프린터 출력
 : DOS나 윈도우 시스템에서는 프린터로 출력 동작을 수행하기 위해서 이미 정의되어 있는 스트림 stdprn을 사용한다. IBM PC와 호환 기종에서는 스트림 stdprn이 LPT1:에 접속되어 있다. LPT!:은 첫 번째 병렬 프린터 포트이다. <리스트 14.15>는 간단한 예를 보여준다.

<리스트 14.15> 출력을 프린터로 전달하기

 /* 프린터 출력의 예 */


 #include <stdio.h>


 main()

 {

    float f = (float) 2.0134;


    fprintf(stdprn, "\nThis message is printed.\r\n");

    fprintf(stdprn, "Add now some numbers:\r\n");

    fprintf(stdprn, "The square of %f is %f.", f, f * f);


    /* 종이 넘김(폼피드) */

    fprintf(stdprn, "\f");


    return 0;

 }

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

12장 변수의 범위  (0) 2019.06.02
13 장 고급 프로그램 제어문  (0) 2019.06.02
15장 포인터 : 고급 기능들  (0) 2019.06.02
16장 링크리스트  (0) 2019.06.02
17장 디스크 파일의 사용  (0) 2019.06.02

아홉번째 강의 "포인터에 대해서"에서는 C 프로그래밍 언어의 가장 중요한 주제인 포인터에 대한 기본적인 내용을 다루었다. 이 장에서는 더욱 유용한 프로그램을 작성할 수 있도록 포인터에 대한 좀더 많은 내용을 설명할 것이다. 오늘은 다음과 같은 내용을 배울 것이다.

·포인터에 대한 포인터를 선언하는 방법
·다차원 배열과 포인터를 함께 사용하는 방법
·포인터의 배열을 선언하는 방법
·함수에 대한 포인터를 선언하는 방법
·데이터 저장을 위한 링크드 리스트를 생성하기 위해 포인터를 사용하는 방법

1. 포인터에 대한 포인터
: 포인터는 다른 어떤 변수의 주소값을 가지는 숫자 변수이다. 포인터는 간접 연산자(*)를 사용하여 선언할 수 있다. 예를 들어, 다음 문장은

   int *ptr;

int형 변수를 지적하는 ptr이라는 이름의 포인터를 선언한다. 이런 포인터를 선언하고 나면 대응하는 형태의 어떤 변수를 지적하도록 하기 위해서 주소 연산자(&)를 사용한다. x가 int형 변수로 선언되어 있다고 가정한다면, 다음 문장은

   ptr = &x;

x의 주소를 ptr에 할당하여 ptr이 x를 지적하도록 한다. 또한, 간접 연산자를 사용하면 포인터가 지적하는 변수의 값을 참조할 수 있다. 다음 문장은 모두 x에 12의 값을 저장한다.

   x = 12;
   *ptr = 12;

포인터 자체는 숫자 변수이므로 컴퓨터 내의 메모리에서 특정 주소에 저장된다. 그래서 포인터에 대한 포인터, 즉 포인터 변수의 값이 다른 포인터의 주소인 변수를 생성할 수도 있다. 다음은 예이다.

   int x = 12;                /* x는 int형 변수이다. */
   int *ptr = &x;             /* ptr은 x에 대한 포인터이다. */
   int **ptr_to_ptr = &ptr;   /* ptr_to_ptr은 int형 변수에 대한 포인터의 포인터이다. */

포인터의 포인터를 선언할 때에는 이중 간접 연산자(**)를 사용한다는 것을 기억하자. 또한, 포인터의 포인터가 지적하는 변수를 참조할 때에도 이중 간접 연산자를 사용한다. 그래서 다음의 문장은

   **ptr_to_ptr = 12;

변수 x에 12의 값을 할당하고, 다음 문장은

   printf("%d", **ptr_to_prt);

x의 값을 화면 상에 출력한다. 여기에서 실수로 하나의 간접 연산자를 사용하면 에러가 발생한다. 다음 문장은

   *ptr_to_ptr = 12;

ptr에 12의 값을 할당하고 ptr은 이미 다른 어떤 값이 저장되어 있는 주소 12를 지적하게 된다. 이것은 분명히 잘못된 것이다.

포인터의 포인터를 선언하고 사용하는 것을 이중 간접 사용(multiple indirection)이라고 한다. <그림 15.1>에는 변수, 포인터, 포인터에 대한 포인터의 관계가 나타나 있다. 간접 사용의 단계에는 아무런 제한이 없다. 필요하다면 무한한(ad_infinitum) 단계의 포인터의 포인터를 사용할 수도 있지만, 일반적으로 2단게를 초과하는 포인터의 사용에는 특별한 장점이 없다. 오히려 복잡해짐에 따라 실수가 발생할 가능성만 높아진다.


<그림 15.1> 포인터의 포인터를 설명하는 그림

그렇다면 포인터의 포인터는 어떤 경우에 사용될까? 포인터의 포인터는 이 장의 후반부에서 설명할 포인터의 배열에서 가장 많이 사용된다.

2. 포인터와 다차원 배열
: 8번째 강의 "숫자 배열 사용하기"에서는 포인터와 배열간의 특별한 관계에 대해서 설명했었다. 특히, 대괄호를 포함하지 않는 배열의 이름이 배열의 첫 번째 요소에 대한 포인터라는 사실은 중요하다. 결과적으로, 특정 형태의 배열을 참조할 때에는 포인터식 표기 방법을 사용하는 것이 낫다. 그러나 지금까지의 예제는 일차원 배열에만 국한된 것이었다. 다차원 배려의 경우는 어떨까? 다차원 배열은 각각의 차원에 대해 대괄호를 사용하여 선언된다는 것을 기억하자. 예를 들어, 다음 문장은 8개의 int형 변수를 가지는 2차원 배열을 선언한다.

   int multi[2][4];

배열은 행과 열을 가지는 것으로 생각할 수 잇다. 앞의 배열은 2행 4열로 구성된다. 그러나 다차원 배열의 구조를 표현하는 다른 한 가지 방법이 있다. C가 실제로 배열을 다루는 방법에 더 가까운 것으로 multi를 두 개의 요소를 가지는 배열로 생각할 수 있다. 각각의 요소는 4개의 정수를 가지는 배열이다. 이런 사실을 이해하기 어렵다면 배열의 선언문을 4개의 구성 요소로 나누어서 설명하고 있는 <그림 15.2>를 참고하기 바란다.


<그림 15.2> 다차원 배열 선언문의 구성 요소

각각의 구성 요소는 다음과 같은 뜻을 가진다.

1. multi라는 이름의 배열을 선언한다.
2. 배열 multi는 두 개의 요소를 가진다.
3. 각각의 요소는 다시 네 개의 요소를 가지고 있다.
4. 네 개의 요소는 int형이다.

다차원 배열의 선언문은 배열의 이름에서부터 시작하여 오른쪽으로 이동하며 대괄호 내에 포함된 내용을 처리한다. 마지막 대괄호의 내용이 처리되고 나면 배열의 기본 데이터형을 결정하기 위해 선언문의 시작 부분으로 이동하게 된다.

이 장에서는 포인터에 대한 내용을 다루므로, 이제 원래의 주제인 포인터로 사용되는 배열의 이름에 대해서 다시 살펴보자. 1차원 배열에서와 마찬가지로 다차원 배열의 이름은 배열의 첫 번째 요서에 대한 포인터이다. 앞에서 사용된 예제의 경우, multi는 int multi[2][4]를 통해서 선언된 2차원 배열의 첫번째 요소에 대한 포인터이다. multi의 첫번째 요소는 정확히 무엇일까 multi의 첫번째 요소는 int형 변수인 multi[0][0]이 아니라 multi가 배열을 가지는 배열이므로 네 개의 int형 변수를 가지는 배열 multi[0]이라는 것을 기억하자. multi[0]은 multi에 포함된 두 배열의 하나이다. 또한, multi[0]이 하나의 배열이라면 어떤 값을 지적하는가? 실제로, multi[0]은 첫 번째 요소인 multi[0][0]을 지적한다. 이런 사실에 대해서는 의문을 가질 수 있다. 대괄호를 포함하지 않는 배열의 이름이 배열의 첫번째 요소에 대한 포인터라는 것을 기억하자. multi[0]은 대괄호를 포함하고 있는 multi[0][0]의 이름이므로 하나의 포인터라고 볼 수 있다. 지금까지의 내용이 혼란스럽더라도 걱정할 필요는 없다. 사실, 포인터와 배열의 관계는 이해하기 어려운 내용이다. n차원의 배열을 사용할 때 다음과 같은 규칙을 기억한다면 유용할 것이다.

·n개의 대괄호와 적절한 색인을 포함하는 배열의 이름은 배열의 데이터를 뜻한다. 즉, 지정된 배열의 요소에 저장된 데이터를 나타내는 것이다.

·n개 이하의 대괄호를 포함하는 배열의 이름은 배열의 요소에 대한 포인터를 뜻한다. 그래서 앞의 예제에서는 multi가 포인터이고, multi[0]도 포인터이며, multi[0][0]은 배열의 데이터를 나타내는 것이다.

 이제, 이런 모든 포인터가 실제로 지적하는 것이 무엇인지 살펴보도록 하자. <리스트 15.1>에 있는 프로그램은 앞에서 사용된 것과 비슷한 2차원 배열을 선언하고 관련된 포인터의 값을 출력한다. 또한 첫 번째 배열 요소의 주소를 출력한다.

<리스트 15.1> 다차원 배열과 포인터의 관계

 /* 포인터와 다차원 배열의 사용 예 */


 #include <stdio.h>


 int multi[2][4];


 main()

 {

    printf("\nmulti = %u", multi);

    printf("\nmulti[0] = %u", multi[0]);

    printf("\n&multi[0][0] = %u", &multi[0][0]);

    return(0);

 }

=> 실제 값을 시스템에 따라 1.328이 아닐 수도 있겠지만 세 값이 동일하다는 사실은 변함 없다. 배열 multi의 주소는 배열 multi[0]의 주소와 동일하고, 이런 값은 배열 multi[0][0]에 저장되어 있는 첫 번째 정수값의 주소와 동일하다. 세 포인터가 동일한 값을 가진다면 프로그램의 측면에서 실제 차이점은 무엇일까? 9번째 강의에서는 포인터가 지적하는 것을 C 컴파일러가 '알고 있다'고 설명했다. 더욱 정확히 표현하자면, 컴파일러는 포인터가 지적하는 항목의 크기를 알고 있다.
 앞에서 사용된 각 항목은 어떤 크기를 가지고 있을까? <리스트 15.2>는 이런 각 항목의 크기를 바이트 단위로 출력하기 위해서 sizeof() 연산자를 사용하고 있다.

<리스트 15.2> 각 항목의 크기 확인하기

 /* 다차원 배열 요소의 크기 */


 #include <stdio.h>


 int multi[2][4];


 main()

 {

    printf("\nThe size of multi = %u", sizeof(multi));

    printf("\nThe size of multi[0] = %u", sizeof(multi[0]));

    printf("\nThe size of multi[0][0] = %u", sizeof(multi[0][0]));

    return(0);

 }

=> IBM의 OS/2와 같은 32비트 운영체제를 사용중이라면 결과는 32, 16, 4가 될 것이다. 이것은 OS/2와 같은 운영체제에서 int형이 4바이트이기 때문이다. 결과에 대해서 생각해보자. 배열 multi는 네 개의 정수값을 가지는 두 개의 배열을 포함하고 있다. 각각의 정수는 2바이트를 차지한다. 전체적으로는 8개의 정수가 존재하므로 16바이트 라는 크기는 정확한 값이다. 다음으로, multi[0]은 네 개의 정수를 가지는 배열이다. 각각의 정수는 2바이트를 차지하므로 multi[0]의 크기가 8바이트로 표현된 것도 정확하다. 마지막으로, multi[0][0]은 정수이므로 정수형의 크기는 당연히 2바이트이다.

 이제, 이런 결과에 유의해서 9번째 강의에서 설명한 포인터 연산에 대해 생각해보도록 하자. C 컴파일러는 포인터가 지적하는 값의 크기를 '알고' 잇고 포인터 연산에서는 이런 크기를 사용한다. 포인터를 증가시키면 현재 지적하고 있는 어떤 내용의 '다음 위치에 있는 것'을 지적하기 위해서 필요한 만큼 증가된다. 즉, 포인터가 지적하고 있는 값의 크기만큼 증가된다.

 이런 포인터 연산의 개념을 앞의 예제에 적용해보자. multi는 8개의 요소를 가지는 정수형 배열에 대한 포인터로 크기는 8이다. 만약 multi를 증가시키면 포인터의 값을 네 개의 요소를 가지는 정수형 배열의 크기인 8만큼 증가된다. multi가 multi[0]을 지적하고 있다면 (multi + 1)은 multi[1]을 지적할 것이다. <리스트 15.3>에 있는 프로그램은 이런 사실을 증명한다.

<리스트 15.3> 다차원 배열에서의 포인터 연산 

 /* 다차원 배열에 대한 포인터로 포인터 연산하기 */


 #include <stdio.h>


 int multi[2][4];


 main()

 {

    printf("\nThe value of (multi) = %u", multi);

    printf("\nThe value of (mulit + 1) = %u", (multi + 1));

    printf("\nThe address of multi[i] = %u", &umlti[1]);

    return(0);

 }

=> 정확한 값은 시스템에 따라 달라질 수 있지만 개념은 같을 것이다. multi를 1증가시키면 실제 값은 8(32비트 환경에서는 16) 증가되고 배열의 다음 요소인 multi[1]을 지적하게 된다. 이 예제에서는 multi가 multi[0]에 대한 포인터라는 사실을 알 수 있다. 또한, multi[0] 자체는 multi[0][0]에 대한 포인터라는 것을 알 수 있다. 그래서 multi는 포인터에 대한 포인터이다. 배열의 데이터를 참조하는 경우에 multi를 사용하기 위해서는 이중 간접 연산자를 사용해야 한다. multi[0][0]에 저장된 값을 출력하기 위해서는 다음과 같은 세 문장의 하나를 사용할 수 있을 것이다.

   printf("%d", multi[0][0]);
   printf("%d", *multi[0]);
   printf("%d", **multi);

이런 개념은 3차원 이상의 배열에도 적용된다. 그래서 3차원 배열은 2차원 배열을 요소로 가지는 배열이라고 할 수 있다. 다시, 각각의 요소는 1차원 배열을 요소로 가진다. 여기서 설명한 다차원 배열과 포인터에 대한 내용은 다소 혼란스러울 것이다. 다차원 배열을 사용할 때에는 한 가지 사실만 기억하자. n차원의 배열은 n - 1차원의 배열을 요소로 가진다. n의 값이 1일 때 배열의 요소는 배열 선언문의 앞에서 지정한 데이터형의 변수이다. 지금까지는 포인터 상수이고 변경이 불가능한 배열의 이름을 사용하였다. 그렇다면 다차원 배열의 요소를 지적하는 포인터 변수를 어떻게 선언할 수 있을까? 2차원 배열을 선언하는 앞의 예를 다시 한 번 살펴보자.

   int multi[2][4];

multi의 각 요소, 즉 네 개의 요소를 가지는 정수형 배열을 지적할 수 있는 포인터 변수를 선언하기 위해서는 다음과 같은 문장을 작성할 수 있다.

   int (*ptr)[4];

그리고 나서 다음의 문장을 사용하여 ptr이 multi의 첫 번째 요소를 지적하도록 할 수 있다.

   ptr = multi;

포인터 선언에서 괄호를 사용한 이유를 이해할 수 있는가? 대괄호([])는 포인터 연산자(*)보다 우선 순위를 가진다. 만약 다음과 같은 문장을 작성했다면

   int *ptr[4];

int형에 대한 네 개의 포인터의 배열을 선언할 것이다. 사실, 포인터의 배열을 선언하고 사용할 수는 있다. 그러나 여기서 필요한 것은 포인터의 배열이 아니다. 다차원 배열의 요소에 대한 포인터를 어떻게 사용할 수 있을까? 일차원 배열에서와 마찬가지로 포인터는 함수에 배열을 전달하는 경우에 사용된다. 이런 사실은 함수에 다차원 배열을 전달하는 두 가지 방법을 사용하고 잇는 <리스트 15.4>에서 설명된다.

<리스트 15.4> 포인터를 사용하여 다차원 배열을 함수에 전달하기  
 

 /* 다차원 배열에 대한 포인터를 함수에 전달하는 예 */


 #include <stdio.h>


 void printarray_1(int (*ptr)[4]);

 void printarray_2(int (*ptr)[4], int n);


 main()
 {

    int multi[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};


    /* ptr은 4 정수 배열에 대한 포인터이다. */


    int (*ptr)[4], count;


    /* ptr이 multi의 첫 요소를 지적하게 한다. */


    ptr = multi;


    /* 순환문에서 ptr은 multi의 다음요소, 즉 다음 4요소 정수배열을 지적하도록 증가된다. */


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

       printarray_1(ptr++);


    puts("\n\nPress Enter...");

    getchar();

    printarray_2(multk, 3);

    printf("\n");

    return(0);

 }


 void printarray_1(int (*ptr)[4])

 {

    /* 4 요소 정수 배열의 요소들을 출력한다. */

    /* p는 INT형에 대한 포인터이다. */

    /* P를 PTR의 주소와 같게 하기 위해 형 변환을 사용해야 한다. */


    int *p, count;

    p = (int *)ptr;


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

       printf("\n%d", *p++);

 }


 void printarray_2(int (*ptr)[4], int n)

 {

    /* n번째 4 요소 정수 배열의 요소들을 출력한다. */


    int *p, count;

    p = (int *)ptr;


    for(count = 0; count < (4 * n); count++)

       printf("\n%d", *p++);

 }

3. 포인터의 배열
 : 8번째 강의 "숫자 배열 사용하기"에서는 배열이 동일한 데이터형을 사용하고 같은 이름으로 사용되는 집단적인 데이터 저장 영역이라고 설명했다. 포인터는 C에서 제공되는 한 가지 데이터형이므로 포인터의 배열을 선언하고 사용할 수 있다. 포인터의 배열은 특별한 상황에서 매우 유용할 수 있다. 아마도 포인터의 배열을 가장 많이 사용하는 경우는 문자열을 처리할 때일 것이다. 10번째 강의 "문자와 문자열"에서 배웠듯이 문자열은 메모리에 저장되는 일련의 문자들을 말한다. 문자열의 시작 부분은 첫 번째 문자에 대한 포인터, 즉 char형에 대한 포인터로 지적된다. 문자열의 마지막은 널 문자로 구분된다.. char형에 대한 포인터의 배열을 선언하고 초기화하면 포인터의 배열을 통해서 방대한 양의 문자열을 사용하거나 처리할 수 있다. 배열의 각 요소는 서로 다른 문자열을 가리키므로 배열을 통해서 순환하면서 차례대로 배열 요소를 이용할 수 있는 것이다.

3.1 문자열과 포인터에 대한 복습
 : 문자열 할당과 초기화를 되새기면서 열 번째 강의에서 설명했던 내용을 다시 한번 살펴보도록 하자. 문자열을 할당하고 초기화하는 한 가지 방법은 다음과 같이 char형 배열을 선언하는 것이다.

   char message[] = "This is the message.";

char형에 대한 포인터를 선언하여 문자열을 할당하고 초기화할 수도 있을 것이다.

   char *message = "This is the message.");

두 문장은 동이한 것이다. 어떤 문장을 사용하든지 컴파일러는 널 문자를 포함하는 문자열을 저장하기 위한 영역을 할당하며, 수식 message는 문자열의 시작 부분에 대한 포인터가 된다. 다음 두 문장은 어떤 뜻을 가지고 있는가?

   char message1[20];
   char *message2;

첫 번째 문장은 20자 길이의 char형 배열을 선언하고, message1은 배열의 첫 번째 요소에 대한 포인터이다. 배열의 저장 영역은 할당되었지만 배열이 초기화되지는 않았고 배열의 내용은 정해지지 않았다. 두 번째 문장은 char형에 대한 포인터 message2를 선언한다. 이 경우, 문자열을 저장하기 위한 영역이 할당되지 않고 단지 포인터를 저장하기 위한 공간만이 할당되어 있다. 만약 문자열을 생성하여 message2가 문자열을 지적하기 원한다면, 우선 문자열을 저장하기 위한 영역을 할당해야 한다. 열 번째 강의에서 이런 용도로 메모리 할당 함수 malloc()을 사용하는 방법을 배웠다. 모든 문자열을 저장하기 위해서는 컴파일 과정에서 미리 준비하거나 도는 프로그램 실행 과정에서 malloc()을 사용하여 필요한 공간을 할당해야 한다는 것을 기억하자.

3.2 char에 대한 포인터의 배열
 : 잠시동안 앞에서 배웠던 것을 복습했으므로 포인터의 배열을 선언해보자. 다음 문장은 char형에 대한 10개의 포인터를 가지는 포인터의 배열을 선언한다.

   char *message[10];

배열 message[]의 요소는 각각 char형에 대한 포인터이다. 짐작할 수 있듯이 배열을 선언할 대에는 문자열을 저장하기 위한 저장 영역을 할당하는 동시에 문자열을 초기화할 수 있다.

   char *message[10] = {"one, "two", "three"};

이 문장은 다음과 같은 동작을 수행한다.

·10개의 요소를 가지는 message라는 이름의 배열을 선언한다.  배열의 요소는 각각 char형에 대한 포인터이다.

·메모리 내에서 저장 영역을 할당하여 널 문자를 포함하는 초기화 문자열을 저장한다. 메모리의 정확한 위치는 신경 쓸 필요가 없다.

·message[0]은 첫 번째 문자열의 첫 번째 문자를 지적하고, message[1]은 두 번째 문자열의 첫 번째 문자를 지적하며, message[3]은 세 번째 문자열의 첫 번째 문자를 지적하도록 초기화된다.

이제 포인터의 배열을 사용하는 예제를 살펴보자.

<리스트 15.5> char형에 대한 포인터의 배열을 초기화하고 사용하는 프로그램

 /* char형에 대한 포인터의 배열 초기화 */


 #include <stdio.h>


 main()

 {

    char *message[8] = { "Four", "score", "and", "seven", "years", "ago,", "our", "forefathers"};


    int count;


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

       printf("%s ", message[count]);

    printf"\n");

    return(0);

 }

=> <리스트 15.5>에 있는 프로그램은 char형에 대한 8개의 포인터를 가지는 배열을 선언하고, 7번째 줄과 8번째 줄에서는 배열 요소인 포인터가 8개의 문자열을 지적하도록 초기화하고 있다. 그리고 나서 11번째 줄과 12번째 줄은 배열의 각 요소를 화면 상에 출력하기 위해서 for문을 사용한다.

 여기에서는 포인터의 배열을 다루는 것이 문자열 자체를 다루는 것보다 쉽다는 사실을 알 수 있을 것이다. 이런 장점은 이 장에서 나중에 설명할 복잡한 프로그램에서 더욱 분명하게 드러난다. 또한, 나중에 설명할 것처럼 이런 장점은 함수를 사용할 때 가장 강력하다. 함수에서 문자열을 출력하기 위해서 여러 개의 문자열을 전달하는 것보다는 포인터의 배열을 전달하는 것이 더욱 쉽다. 문자열을 출력하기 위해 함수를 사용하도록 <리스트 15.5>에 있는 프로그램을 변경하여 포인터의 배열을 전달하는 것이 더 쉽다는 사실을 확인할 수 있다. <리스트 15.6>에는 변경된 프로그램이 나타나 있다.

<리스트 15.6> 포인터의 배열을 함수에 전달하기

 /* 함수에 포인터의 배열 전달하기 */


 #include <stdio.h>


 void print_strings(char *p[], int n);


 main()

 {

    char *message[8] = { "Four", "score", "and", "seven", "years", "ago,", "our", "forefathers"};


    printf_strings(message, 8);

    return(0);

 }


 void print_strings(char *p[], int n)

 {

    int count;


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

       printf("%s", p[count]);

    printf("\n");

 }

앞에서 포인터의 포인터를 설명할 때 나중에 예제를 설명할 것이라고 언급했던 것을 기억하는가? 앞의 예제가 바로 포인터의 포인터를 사용하는 예제이다. <리스트 15.6> 에서는 포인터의 배열을 선언했다. 배열의 이름은 첫 번째 요소에 대한 포인터이다. 함수에 배열을 전달할 때에는 포인터(배열의 첫번째 요소)에 대한 포인터 (배열 이름)를 전달하는 것이다.

3.3 예제
 : 이제 좀더 복잡한 예제를 살펴보자. <리스트 15.7>에 있는 프로그램은 포인터의 배열을 포함하여 지금까지 설명한 여러 가지 내용을 활용하고 있다. 프로그램은 문자열이 입력될 때 문자열을 저장하기 위한 영역을 할당하고, char형에 대한 포인터의 배열을 사용하여 저장 영역을 관리하며, 여러 줄의 입력을 키보드에서 받아들인다. 빈 줄을 입력하여 마지막이라는 것을 알려주면 프로그램은 입력된 내용을 알파벳 순으로 정렬하고 화면 상에 출력한다. 만약 이 프로그램을 처음부터 작성한다면 구조화 프로그래밍 방식으로 작성할 것이다. 우선, 프로그램이 수행해야 하는 동작을 목록으로 만들어보자.

① 빈 줄이 입력될 때까지 한 번에 한 줄씩 키보드에서 입력을 받아들인다.

② 입력된 내용을 알파벳 순으로 정렬한다.

③ 정렬된 내용을 화면 상에 출력한다.

이 목록에서 알 수 있듯이 프로그램은 적어도 세 개의 함수를 사용해야 한다. 하나는 입력을 받아들이고, 다른 하나는 입력된 내용을 정렬하며, 세 번째 함수는 입력된 내용을 출력한다. 이제, 독립적으로 각각의 함수를 작성할 수 있을 것이다. get_lines()라는 입력 함수는 무엇을 수행할 필요가 있을까? 다시, 목록을 만들어 보자.

① 입력된 문장의 수를 일단 모든 내용이 입력되면 함수를 호출한 프로그램으로 문장의 수를 돌려준다.

② 미리 설정되어 있는 문장의 수보다 많은 내용은 받아들이지 않는다.

③ 각각의 문장을 저장하기 위한 영역을 할당한다.

④ 문자열에 대한 포인터를 배열에 저장하여 모든 내용을 관리한다.

⑤ 빈 줄이 입력되면 원래의 프로그램으로 돌아간다.

이제, 알파벳 순으로 문장을 정렬하는 두 번째 함수에 대해서 생각해보자. 이 함수를 sort() 라고 하자. 여기서 사용되는 정렬 방식은 인접한 두 개의 문자열을 비교해서 두 번째 문자열이 첫 번째 문자열보다 작으면 서로 교환하는 아주 간단한 방법이다. 더욱 정확히 말하자면, 함수는 포인터의 배열 내에서 인접한 두 개의 문자열을 비교하여 필요하다면 포인터를 교환한다.

 문자열을 완전히 정렬하기 위해서는 배열의 처음부터 마지막까지 필요할 때마다 각 쌍의 문자열을 비교해야 한다. n개의 요소를 가지는 배열에서는 배열을 n - 1번 통과하며 비교해야 한다. 왜 n - 1번이나 배열을 통과해야 하는 것일까? 배열을 한 번 통과할 때 각각의 요소는 기껏해야 한 칸씩 이동될 수 있다. 예를 들면, 맨 앞에 위치되어야 하는 문자열이 실제로 끝에 위치되어 있다면 처음에는 배열을 통과하며 문자열을 맨 끝에서 앞으로 한 번 이동시키고, 다음에는 다시 한 번 앞으로 이동시키며, 계속해서 이런 과정을 반복해야 한다. 맨 끝에 위치되어 있는 문자열을 배열의 맨 앞으로 옮기려면 n - 1번 이동시켜야 한다. 이것은 아주 비효율적이고 좋지 못한 정렬 방식이라는 것을 기억하자. 그러나 예제 프로그램에서는 간단한 데이터를 정렬할 것이므로 이 방법이 사용하기 쉽고 이해하기 쉬우며 가장 적절하다. 마지막 함수는 화면 상에 정렬된 문자열을 출력한다. 사실, 이 함수는 이미 <리스트 15.6>에서 작성한 것을 약간 변경한 것이므로 <리스트 15.7>에서 사용하려면 약간 수정하면 된다.

<리스트 15.7> 키보드에서 여러 줄의 텍스트를 읽어들이고 알파벳순으로 정렬하여 출력하는 프로그램

 /* 키보드에서 문자열을 읽어들이고 정렬하여 화면에 출력한다. */


 #include <stdio.h>

 #include <stdlib.h>

 #include <string.h>


 #define MAXLINES 25


 int get_lines(char *lines[]);

 void sort(char *p[], int n);

 void print_strings(char *p[], int n);


 char *lines[MAXLINES];


 main()

 {

    int number_of_lines;


    /* 키보드에서 여러 줄을 읽어들인다. */


    number_of_lines = get_lines(lines);


    if(number_of_lines < 0)

    {

       puts("Memory allocation error");

       exit(-1);

    }


    sort(lines, number_of_lines);

    print_strings(lines, number_of_lines);

    return(0);

 }


 int get_lines9char *lines[])

 {

    int n = 0;

    char buffer[80];   /* 각 줄에 대한 임시 저장 공간 */


    puts("Enter one line at time; enter a blank when done.");


    while((n < MAXLINES) && (gets(buffer) != 0) &&

         (buffer[0] != '\0'))

    {

       if((lines[n] = (char *)malloc(strlen(buffer) + 1)) == NULL)

          return -1;

       strcpy(lines[n++], buffer);

    }

    return n;


 }   /* get_lines()의 끝 */


 void sort9char *p[], int n)

 {

    int a, b;

    char *x;


    for(a = 1; a < n; a++)

    {

       for(b = 0; b < n-1; b++)

       {

          if(strcmp(p[b], p[b+1]) > 0)

          {

             x = p[b];

             p[b] = p[b+1];

             p[b+1] = x;

          }

       }

    }

 }


 void print_strings(char *p[], int n)

 {

    int count;


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

       printf("\n%s ", p[count]);

 }

=> 이 프로그램을 상세히 살펴보면 도움이 될 것이다. 프로그램에서는 문자열을 다루기 위해서 여러 가지 새로운 라이브러리 함수를 사용하고 있다. 여기에서는 이런 함수들을 간단히 설명하고, 상세한 내용은 낭중에 "문자열 다루기"에서 설명하도록 하겠다. 이런 함수들을 사용하는 프로그램에는 헤더 파일 STRING.H가 포함되어야 한다.

get_lines() 함수에서 41번째 줄과 42번째 줄의 다음과 같은 while문은 입력을 제어한다. 한 줄로 살펴보자.

   while((n < MAXLINES) && (gets(buffer) != 0) && (vuffer[0] != '\0'))

while문에서는 세 가지 조건이 확인되고 있다. 첫 번째 조건인 n < MAXLINES는 지정된 수의 문장이 입력되지 않았는지 확인한다. 두 번째 조건인 get(buffer) != 0은 키보드에서 버퍼로 문장을 읽어들이기 위해서 라이브러리 함수 gets()를 호출하여 파일의 마지막에 도달하거나 또는 다른 어떤 에러가 발생하지 않았는지 확인한다. 세 번째 조건인 buffer[0] != '\0'는 방금 입력된 문장의 첫 문자가 널 문자가 아닌지 확인한다. 널 문자는 빈 줄이 입력되었다는 것을 알려주는 역할을 한다.

 세가지 조건의 어떤 것도 만족되지 않으면, while문은 종료되고 함수는 지금까지 입력된 문장의 수를 프로그램으로 돌려주며 제어를 전달한다. 만약 세가지 조건이 모두 참이라면 44번째 줄에 있는 다음과 같은 if문이 실행된다.

   if((lines[n] = (char *)malloc9strlin(buffer) + 1)) == NULL)

이 문장은 방금 입력된 문자열의 저장 영역을 할당하기 위해서 malloc()를 호출한다. strlen() 함수는 인수로 전달된 문자열의 길이를 돌려준다. 복귀값에는 malloc() 함수가 문자열과 함께 널 문자를 저장할 수 있는 영역을 할당하도록 하기 위해서 1이 더해진다. 앞에서 설명했던 malloc() 함수는 포인터를 돌려준다. 이 문장은 malloc()이 돌려주는 포인터의 값을 포인터의 배열에서 대응하는 요소에 할당한다. 만약 malloc()이 NULL을 돌려주면, if문에서는 함수를 호출한 프로그램으로 -1의 복귀값을 돌려준다. main()에서는 get_lines()의 복귀값을 확인하여 함수가 0보다 작은 값을 돌려주는지 확인한다. 23번째 줄부터 27번째 줄까지는 메모리 할당에러를 알려주고 프로그램을 종료한다. 메모리의 할당이 성공적이었다면 프로그램의 46번째 줄에서는 임시 저장 영역의 버퍼에서 방금 malloc()에 의해 할당된 저장 영역으로 문자열을 복사하기 위해 strcpy()함수를 사용한다. 그리고 나서 while문은 다른 문장을 읽어들이기 위해 전체 과정을 반복한다. 메모리 할당에러가 발생하지 않았다고 가정하고, 일단 get_lines()에서 main()으로 제어가 전달되면 다음과 같은 동작이 수행된 것이다.

·키보드에서 여러 줄의 텍스트가 입력되고 널 문자를 포함하는 문자열 형태로 메모리에 저장되었다.

·배열 lines[]는 각 문자열에 대한 포인터를 가진다. 배열 내의 포인터의 순서는 문자열이 입력된 순서이다.

·변수 number_of_lines는 입력된 문장의 수를 가진다.

이제, 문자열의 정렬 부분을 살펴보자. 프로그램에서는 실제로 문자열을 이동시키는 것이 아니라 단지 배열 lines[]에 포함되어 있는 포인터의 순서만을 이동시킨다는 것을 기억하자. 함수 sort()의 내용을 살펴보자. 57번째 줄부터 68번째 줄까지는 for문 내에 종속되어 있는 다른 하나의 for문이 사용되고 잇다. 바깥쪽의 순환문은 number_of_lines - 1번 실행된다. 바깥쪽의 순환문이 실행될 때마다 안쪽의 순환문은  n = 0에서부터 n = number_of_lines-1이 될 때까지 포인터의 배열에서 (string n)을 (string n + 1)과 비교한다. 실제 비교 동작은 두 개의 문자열에 대한 포인터를 바다아들이는 61번째 줄의 라이브러리 함수 strcmp()에 의해서 수행된다. 함수 strcmp()는 다음 중에서 하나의 값을 돌려준다.

·첫 번째 문자열이 두 번째 문자열보다 크다면 -> 0보다 큰 값

·두 문자열이 동일하다면 -> 0

·두 번째 문자열이 첫 번째 문자열보다 크다면 -> 0보다 작은 값 프로그램에서 strcmp()가 0보다 큰 값을 돌려준다면 첫 번째 문자열이 두 번째 문자열보다 '크다'는 것을 뜻하므로 문자열을 서로 교환해야 한다. 즉, lines[]내의 포인터를 교환해야 한다

이런 교환 동작은 임시 변수인 x를 사용하여 수행된다. 63번째 줄부터 65번째 줄까지는 실제 교환을 수행한다.

sort()에서 프로그램으로 제어가 전달될 때 lines[]내의 포인터는 순서대로 정렬된 상태가 된다. '가장 작은' 문자열에 대한 포인터는 lines[0]에 저장되고, 다음으로 '작은' 문자열에 대한 포인터는 lines[1]에 저장된다. 마지막으로, 프로그램은 화면 상에 정렬된 문자열을 출력하기 위해서 함수 print_strings()를 호출한다. 이 함수는 앞의 예제와 비슷하다. <리스트 15.7>에 있는 프로그램은 지금까지 여기서 사용한 것들 중에서 가장 복잡한 예제이다 이 프로그램은 지금까지 설명한 C 프로그래밍에 대한 많은 내용을 다루고 있다. 코드 분석을 참고로 해서 이 프로그램의 동작과 각 단계별 내용을 이해하기 바란다. 만약 이해할 수 없는 부분이 있다면, 다음 주제로 진행하기 전에 프로그램을 완전히 이해할 할 수 있을 때까지 여기서 관련된 내용을 다시 한 번 읽어보도록 하자.

4. 함수에 대한 포인터
: 함수에 대한 포인터는 함수를 호출하는 또다른 방법을 제공해준다. 여기서 아마도 '함수에 대한 포인터를 어떻게 구할 수 있지? 포인터는 변수가 저장된 주소값을 가지는 것이 아닌가?'라는 의문을 가질 것이다. 이 질문에 대한 해답은 양면성을 가지고 있다. 포인터가 주소값을 가진다는 것을 사실이지만 반드시 변수가 저장된 주소값일 필요는 없다. 프로그램이 실행될 때 각 함수의 코드는 특정 주소에서부터 시작하는 메모리 영역에 위치된다. 함수에 대한 포인터는 이렇게 메모리 영역에 저장된 함수의 시작 주소값을 가진다. 그렇다면 함수에 대한 포인터를 사용하는 이유는 무엇일까? 앞에서도 언급했듯이, 함수를 호출하는 방법에 많은 융통성을 제공해준다. 함수에 대한 포인터는 프로그램이 여러 가지 함수들 중에서 현재 상황에 적합한 것을 '선택하여' 실행하게 해준다.

4.1 함수에 대한 포인터 선언
: 다른 일반적인 포인터와 마찬가지로 함수에 대한 포인터를 사용하려면 먼저 선언해야 한다. 포인터는 다음의 형식을 사용한다.

   type (*ptr_to_func)(parameter_list);

이 문장은 type형을 돌려주고 parameter_list에 포함되어 있는 매개 변수를 받아들이는 함수에 대한 포인터 ptr_to_func를 선언한다. 다음은 몇 가지 구체적인 예이다.

   int (*func1)(int x);
   void (*func2)(double y, double z);
   char (*func3)(char *p[]);
   void (*func4)();

첫 번째 문장은 하나의 int형 인수를 받아들이고 int형 값을 돌려주는 함수에 대한 포인터 func1을 선언한다. 두 번째 문장은 두 개의 double형 인수를 받아들이고 void의 복귀형을 가지는, 즉 복귀값이 없는 함수에 대한 포인터 func2를 선언한다. 세 번째 문장은 chart형에 대한 포인터의 배열을 인수로 받아들이고 char형의 값을 돌려주는 함수에 대한 포인터 func3을 선언한다. 마지막 문장은 아무런 인수도 받아들이지 않고 void의 복귀형을 가지는 함수에 대한 포인터 func4를 선언한다. 포인터의 이름 주위에 괄호를 사용할 필요가 있을까? 첫 번째 예제의 경우 다음과 같은 문장을 사용할 수 없는가?

   int *func1(int x);

이 것은 간접 연산자인 *의 우선 순위에 관련된 문제이다. 간접 연산자는 매개 변수의 목록을 둘러싸고 있는 괄호보다 상대적으로 낮은 우선 순위를 가진다. 첫 번째 예제에서 괄호를 생략한 선언문은 func1을 int형에 대한 포인터를 돌려주는 함수로 선언한다. 포인터를 돌려주는 함수는 8번째 강의 "함수를 효율적으로 사용하는 방법"에서 다루어진다. 함수에 대한 포인터를 선언할 때에는 항상 포인터의 이름과 간접 연산자 주위에 괄호를 사용해야 한다는 사실을 기억하자. 그렇지 않으면 분명히 문제가 발생할 것이다.

4.2 함수에 대한 포인터의 초기화와 사용
: 함수에 대한 포인터를 사용하려면 포인터를 선언해야 할 뿐 아니라 어떤 것을 지적하도록 초기화해야 한다. 물론 여기서 '어떤 것'은 함수이다. 포인터가 지적해야 하는 함수에는 아무런 제한이 없다. 한가지 주의해야 할 사항이 있다면 함수의 복귀형과 매개 변수의 목록이 포인터를 선언할 때 지정된 복귀형이나 매개 변수의 목록과 일치해야 한다는 것이다. 예를 들어, 다음 문장은 함수와 함수에 대한 포인터를 선언하고 정의한다.

   float square(float x);    /* 함수 원형 */
   float (*p)(float x);       /* 포인터 선언 */
   float square(float x)     /* 함수 정의 부분 */
   {
      return x * x;
   }

함수 square()와 포인터 p는 동일한 매개 변수와 복귀 형태를 사용하므로 다음과 같이 p가 square를 지적하도록 초기화할 수 있다.

   p = square;

이제, 다음과 같이 포인터를 사용하여 함수를 호출할 수 있다.

  answer = p(x);

함수에 대한 포인터는 이처럼 간단한 것이다. 실제 사용 예는 <리스트 15.8>에 있는 프로그램 을 컴파일하고 실행하여 살펴보도록 하자. 이 프로그램은 함수에 대한 포인터를 선언하고 초기화한 후에, 처음에는 함수의 이름을 사용하고 그 다음에는 포인터를 사용하여 함수를 두 번 호출한다. 함수를 어떤 방법으로 호출하든지 결과는 같다.

<리스트 15.8> 함수를 호출하기 위해서 함수에 대한 포인터를 사용하는 프로그램  
 

 /* 함수에 대한 포인터 선언과 사용 예 */


 #include <stdio.h>


 /* 함수 원형 */


 double square(double x);


 /* 포인터 선언 */


 double (*p)(duble x);


 main()

 {

    /* p가 square()를 지적하도록 초기화 */


    p = square;


    /* square()를 두 가지 방법으로 호출 */

    printf("%f %f", square(6.6), p(6.6));

    return(0);

 }


 double square(double x)

 {

    return x * x;

 }


=> 7번째 줄에서는 square()를 선언한고 ,11번째 줄에서는 double형 인수를 받아들이고 double값 을 돌려주며 square()의 선언문과 일치하는 함수에 대한 포인터 p를 선언한다. 17번째 줄에 서는 포인터 p를 square로 설정한다. square나 p에서 괄호가 사용되지 않았다는 것에 주의하자. 20번째 줄은 square()와 p()의 호출에서 복귀되는 값을 출력한다.
괄호를 포함하지 않는 함수의 이름은 함수에 대한 포인터이다. 이것은 배열의 경우와 비슷하다. 함수에 대한 포인터를 선언하고 사용하는 것과 어떤 차이점이 있을까? 함수의 이름 자체는 포인터 상수로 변경이 불가능하다. 이 사실은 배열에서와 같다. 그러나 포인터 변수는 변경할 수 있다. 특히, 필요할 때마다 다른 함수를 지적하도록 설정할 수 있다.

<리스트 15.9>에 있는 프로그램은 함수에 정수형 값을 인수로 전달하여 함수를 호출한다. 전달되는 인수의 값에 따라 함수는 포인터가 세 가지 다른 함수의 하나를 지적하도록 초기화하고 나서 대응하는 마수를 호출하기 위해서 포인터를 사용한다. 이런 세 함수의 각각은 화면 상에 독특한 메시지를 출력한다.

<리스트 15.9> 상황에 따라 다른 함수를 호출하기 위해서 함수에 대한 포인터를 사용하는 프로그램

 /* 서로 다른 함수를 호출하기 위한 포인터의 사용 예 */


 #include <stdio.h>


 /* 함수 원형 */


 void func1(int x);

 void one(void);

 void two(void);

 void other(void);


 main()

 {

    int a;


    for(;;)

    {

       puts("\nEnter an integer between 1 and 10, 0 to exit: ");

       scanf("%d", &a);


       if(a == 0)

          break;

       func1(a);

    }

    return(0);

 }


 void func1(int x)

 {

    /* 함수에 대한 포인터 */


    void(*ptr)(void);


    if(x == 1)

       ptr = one;

    else if(x == 2)

       ptr = two;

    else

       ptr = other;


    ptr();

 };


 void one(void)

 {

    puts("You entered 1.");

 }


 void two(void)

 {

    puts("You entered 2.");

 }


 void other(void)

 {

    puts("You entered something other than 1 or 2.");

 }

=> 이 프로그램의 16번째 줄에서는 0의 값이 입력될 때가지 프로그램을 계속 실행하기 위해서 무한 루프를 사용하고 있다. 0이 아닌 값이 입력될 때 입력된 값은 func1()에 전달된다. func1()의 32번째 줄에 나타나 있는 함수에 대한 포인터 ptr의 선언문을 주의해서 살펴보자. 이 선언문은 ptr이 func1()에 대해서 지역 변수의 상태가 되도록 해주는데, 프로그램의 다른 부분에서는 이 포인터를 사용할 필요가 없으므로 이렇게 지역 변수로 선언하는 것이 좋다. func1()의 34번째 줄부터 39번째 줄까지는 입력된 값에 따라 ptr을 특정 함수로 설정한다. 41번째 줄은 특정 함수에 설정된 ptr()을 호출한다. 물론, <리스트 15.9>에 있는 프로그램은 예제로 사용하기 위한 것이다. 이 프로그램에서는 함수에 대한 포인터를 사용하지 않고도 쉽게 동일한 결과를 얻을 수 있을 것이다. 이제, 여러 가지 함수를 호출하기 위해서 함수의 인수로 포인터를 전달하여 사용하는 방법을 알아보도록 하자. <리스트 15.10>에 있는 프로그램은 <리스트 15.9>를 수정한 것이다.

<리스트 15.10> 함수에 대한 포인터를 인수로 전달하기

 /* 함수에 대한 포인터를 인수로 전달하기 */


 #include <stdio.h>


 /* 함수 원형. 함수 func1()은 어떤 인수도 받아들이지 않고 복귀값을 가지지 않는 함수에

    대한 포인터를 하나의 인수로 받아들인다. */



 void func1(void (*p)(void));

 void one(void);

 void two(void);

 void other(void);


 main()

 {

    /* 함수에 대한 포인터 */


    void (*ptr)(void);

    int a;


    for(;;)

    {

       puts("\nEnter an integer between 1 and 10, 0 to exit: ");

       scanf("%d", &a);


       if(a == 0)

          break;

       else if(a == 1)

          ptr = one;

       else if(a == 2)

          ptr = two;

       else

          ptr = other;

       func1(ptr);

    }

    return(0);

 }


 void func1(void (*p)(void)

 {

    p();

 }


 void one(void)

 {

    puts("You entered 1.");

 }


 void two(void)

 {

    puts("You entered 2.");

 }


 void other(void)

 {

    puts("You entered something other than 1 or 2.");

 }

=> <리스트 15.9>와 <리스트 15.10>의 차이점을 주의해서 살펴보자. 함수에 대한 포인터의 선언문은 main()의 18번째 줄로 이동되었다. 이제 main()의 26번째 줄부터 33번째 줄까지는 사용자가 입력한 값에 따라 포인터가 정확한 함수를 지적하도록 초기화하고 나서 초기화된 포인터를 func1()에 전달한다. <리스트 15.10>에서는 함수 func1()이 어떤 특별한 동작을 수행하지 않고 있다. 단지 ptr이 지적하는 함수를 호출한다. 이 프로그램도 또한 예제로 사용된 것이다. 여기서 설명하는 내용은 잠시 후에 설명할 것과 같은 실용적인 프로그램에 응용될 수 있다.

 함수에 대한 포인터를 사용하는 프로그래밍 작업의 한 가지 예는 정렬 작업이 필요한 경우이다. 프로그램에서는 가금 여러 가지 정렬 방식을 사용하기 원할 것이다. 예를 들어, 한 번은 알파벳 순서로 데이터를 정렬하고, 한 번은 알파벳의 역순으로 정렬하기 원할 수 있다. 함수에 대한 포인터를 사용하면 프로그램은 정확한 정렬 함수를 호출할 수 있다. 더욱 정확히 말하자면, 일반적으로 여러 가지 비교 함수를 호출하여 사용할 수 있는 것이다. <리스트 15.7>에 있는 프로그램을 다시 한 번 살펴보자. sort() 함수의 실제 정렬 순서는 라이브러리 함수 strcmp()가 돌려주는 값에 의해서 결정된다. 이 함수는 프로그램에서 사용되는 문자열이 다른 하나의 문자열보다 '작은지' 또는 '큰지'의 여부를 알려준다. 만약 A가 Z보다 작은 것으로 평가되는 알파벳 순서에 의해서 정렬 동작을 수행하는 하나의 함수와 Z가 Z보다 큰 것으로 평가되는 알파벳 역순에 의해서 정렬 동작을 수행하는 하나의 함수를 작성한다면 어떨까? 프로그램은 사용자에게 어떤 순서로 정렬하기 원하는지 결정하도록 요구하고, 정렬 함수는 포인터를 사용하여 적절한 비교 함수를 호출할 수 있다. <리스트 15.11>은 <리스트 15.7>에 있는 프로그램을 수정하고 이런 기능을 결합시킨 것이다.

<리스트 15.11> 여러 가지 정렬 방식을 사용하기 위해서 함수에 대한 포인터를 사용하는 프로그램

 /* 키보드에서 문자열을 읽어들이고 오름차순이나 내림차순으로 정렬하여 화면에

    출력한다. */


 #include <stdlib.h>

 #include <stdio.h>

 #include <string.h>


 #define MAXLINES 25


 int get_lines(char *lines[]);

 void sort(char *p[], int n, int sort_type);

 void print_strings(char *p[], int n);

 int alpha(char *p1, char *p2);

 int reverse(char *p1, char *p2);


 char *lines[MAXLINES];


 main()

 {

    int number_of_lines, sort_type;


    /* 키보드에서 여러 줄을 읽어들인다. */


    number_of_lines = get_lines(lines);


    if(number_of_lines < 0)

    {

       puts("Memory allocation error");

       esit(-1);

    }


    puts("Enter 0 for reverse order sort, 1 for alphabetical: ");

    scanf("%d", &sort_type);


    sort(lines, number_of_lines, sort_type);

    print_strings(lines, number_of_lines);

    return(0);

 }


 int get_lines(char *lines[])

 {

    int n = 0;

    char buffer[80];   /* 각 줄에 대한 임시 저장 공간 */


    puts("Enter one line at a time; enter a blank when done.");


    while(n < MAXLINES && gets(buffer) != 0 && buffer[0] != '\0')

    {

       if((lines[n] = (char *)malloc(strlen(buffer)+1)) == NULL)

          return -1;

       strcpy(lines[n++], buffer);

    }

    return n;


 }   /* get_lines()의 끝 */


 void sort(char *p[], int n, int sort_type)

 {

    int a, b;

    char *x;


    /* 함수에 대한 포인터 */


    int(*compare)(char *s1, char *s2);


    /* 포인터가 인수 sort_type에 따라 적절한 비교 함수를 지적하도록 초기화한다. */



    compare = (sort_type) ? reverse : alpha;


    for(a = 1; a < n; a++)

    {

       for(b = 0; b < n - 1; b++)

       {

          if(compare(p[b], p[b+1]) > 0)

          {

             x = p[b];

             p[b] = p[b+1];

             p{b+1] = x;

          }

       }

    }

 }   /* sort()의 끝 */


 void print_strings(char *p[], int n)

 {

    int count;


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

       printf("\n%s", p[count]);

 }


 int alpha(char *p1, char *p2)

 /* 알파벳 여순 비교 */

 {

    return(strcmp(p1, p2));

 }

=> main()의 32번째 줄과 33번째 줄은 어떤 정렬 방식을 사용하기 원하는지 묻는다. 선택된 정렬 순서는 sort_type에 저장된다. 이 값은 <리스트 15.7>에서 설명했던 다른 값과 함께 sort() 함수에 전달된다. sort() 함수는 약간 변경되었다. 64번째 줄은 두 개의 문자형 포인터(문자열)를 인수로 받아들이는 함수에 대한 포인터 compare()를 선언한다. 69번째 줄에서는 sort_type의 값을 기본으로 하여 compare()를 리스트에 추가된 두 가지 새로운 함수의 하나로 서정한다. 두 개의 새로운 함수는 alpha()와 reverse()이다. alpha()는 <리스트 15.7>에서 사용했던 것과 같은 라이브러리 함수 strcmp()를 사용한다. reverse()는 역순으로 정렬을 수행하기 위해서 전달된 매개 변수의 위치를 변경하여 사용한다.





 

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

13 장 고급 프로그램 제어문  (0) 2019.06.02
14장 화면, 프린터, 키보드 사용하기  (0) 2019.06.02
16장 링크리스트  (0) 2019.06.02
17장 디스크 파일의 사용  (0) 2019.06.02
API 윈도우 창 띄우기  (0) 2019.05.25

+ Recent posts