'pc관련/C언어'에 해당되는 글 19건

  1. 2019.03.25 C언어_구조체
  2. 2019.03.24 C언어_문자,문자열
  3. 2019.03.24 C언어_포인터에 대하여...
  4. 2019.03.24 C언어_숫자배열사용
  5. 2019.03.20 입출력의 기초
  6. 2019.03.20 숫자배열 사용하기
  7. 2019.03.17 C언어-프로그램제어문
  8. 2019.03.16 함수의 기본
pc관련/C언어2019. 3. 25. 20:20

많은 프로그래밍 작업은 구조체(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 키워드를 포함시키면 구조체라는 사실을 분명히 알 수 있어 편리하다.

'pc관련 > C언어' 카테고리의 다른 글

고급 프로그램제어문  (0) 2019.04.03
C언어_변수에 범위  (0) 2019.03.28
C언어_문자,문자열  (0) 2019.03.24
C언어_포인터에 대하여...  (0) 2019.03.24
C언어_숫자배열사용  (0) 2019.03.24
Posted by 둥이파파^^
pc관련/C언어2019. 3. 24. 21:02

문자(character)는 하나의 문자(letter), 숫자(number), 구두점(punctuation mark), 또는 그 밖의 기호를 가리키고, 문자열(string)은 이런 문자들이 결합된 상태를 말한다. 문자열은 문자, 숫자, 구두점, 그 밖의 기호 등으로 구성되는 텍스트 데이터를 저장하는데 사용된다. 문자와 문자열은 많은 프로그램에서 매우 유용하게 사용된다. 오늘은 다음과 같은 내용을 배울 것이다.
·하나의 문자값을 저장하기 위한 C의 데이터형(char)을 사용하는 방법
·여러 개의 문자값으로 구성되는 문자열을 저장하기 위한 char형 배열의 생성
·문자와 문자열을 초기화하는 방법
·문자열에서 초기화하는 방법
·문자와 문자열을 입력하고 출력하는 방법

1. char 데이터형
: C는 문자값을 저장하기 위해서 char형을 사용한다. char형이 숫자형이라면 어떻게 문자값을 저장하기 위해서 사용할 수 있을까? 해답은 C가 문자를 저장하는 방법에 있다. 컴퓨터의 메모리에는 모든 데이터가 숫자 형태로 저장된다. 문자를 직접 저장하는 방법은 없다. 그러나 각각의 문자에 대해서는 대응하는 숫자 코드가 존재한다. 이런 숫자 코드를 ASCII(American Standard Code for Information Interchange) 코드나 ASCII 문자라고 한다. 대·소문자, 숫자, 구두점, 그 밖의 기호에는 모두 0과 255 사이의 ASCII 코드값이 할당되어 있다. 예를 들어, 문자 a에는 97이라는 ASCII 코드값이 할당되어 있다. char형의 변수에 문자를 저장할 때 실제로는 97의 값이 저장되는 것이다. char형에 저장할 수 있는 숫자의 범위는 표준 ASCII 코드의 범위와 일치하므로, char는 이론적으로 한 문자를 저장하는 데 적합하다. 이런 내용은 다소 혼란스러울 것이다. C에서 문자를 숫자 형태로 저장한다면 프로그램에서는 주어진 char형 변수가 문자를 저장하는지 숫자를 저장하는지 어떻게 알 수 있을까? 나중에 설명하겠지만, 문자값을 사용하기 위해서는 변수를 char형으로 선언하는 것만으로 충분하지 않다. 변수에 대해서 다른 어떤 동작을 수행할 필요가 있다.
·C프로그램에서 문자를 출력해야 하는 곳에서 char형의 변수가 사용되면 문자로 간주된다.
·C프로그램에서 숫자를 출력해야 하는 곳에서 char형의 변수가 사용되면 숫자로 간주된다. 이런 규칙은 C에서 문자값을 저장하기 위해서 숫자 데이터형을 사용할 수 있다는 사실을 확실히 증명한다.

좀더 상세한 내용을 살펴보도록 하자.

2. 문자 변수의 사용
: 다른 변수와 마찬가지로 문자 변수를 사용하기 위해서는 먼저 선언해야 하는데, 변수를 선언하는 동시에 초기화할 수도 있다. 다음은 몇 가지 예이다.

  char a, b, c;       /* 초기화되지 않는 세 개의 char형 변수 선언 */
  char code = 'x';   /* code라는 이름의 char형 변수를 선언하고 */
  …                 /* 문자 x를 저장한다. */
  code = '!';         /* code라는 이름의 변수에 !를 저장한다.

문자 그대로의 값을 가지는 실제 상수를 할당하기 위해서는 하나의 문자를 작은 따옴표 내에 포함시키면 된다. 컴파일러는 실제 상수를 주어진 값에 대응하는 ASCII 코드로 변환해서 숫자 코드값을 변수에 할당하게 된다.
#define 지시어나 const 키워드를 사용하면 char형의 기호 상수를 정의할 수도 있다.

  #define EX 'x'
  char code = 'EX';   /* x를 저장하는 것과 동일하다. */
  const char A - 'Z';

이제, 문자 변수를 선언하고 초기화하는 방법을 배웠으므로, 예제를 살펴보도록 하자. <리스트 10.1>에 있는 프로그램은 printf() 함수를 사용하여 문자가 숫자 형태로 저장된다는 사실을 보여준다. 함수 printf()는 문자와 숫자를 출력하는 데 사용될 수 있다. 형식화 문자열인 %c는 printf()가 문자를 출력하도록 지시하는 반면에, %d는 십진 정수형 값을 출력하도록 지시한다. <리스트 10.1>은 두 개의 char형 변수를 초기화하고 각각의 값을 출력한다. 처음의 값은 문자형으로 출력되고, 두 번째 값은 숫자형으로 출력된다.

 

<리스트 10.1> char형 변수의 숫자 특성을 보여주는 예

 /* char 변수의 숫자형 특성 */


 

 #include <stdio.h>


 

 /* 두 char형 변수 선언과 초기화 */


 

 char c1 = 'a';

 char c2 = 90;


 

 main(0

 {

    /* 변수 c1을 문자형으로 출력하고 숫자형으로 출력 */


 

    printf("\nAs a character, variable c1 is %c", c1);

    printf("\nAs a number, variable c1 is %d", c1);


 

    /* 변수 c2를 문자형으로 출력하고 숫자형으로 출력 */


 

    printf("\nAs a character, variable c2 is %c", c2);

    printf("\nAs a number, variable c2 is %d\n", c2);


 

    return 0;

 }

 

<리스트 10.2>에 있는 프로그램은 확장 ASCII 코드의 문자를 일부분 출력하는 것이다.

<리스트 10.2> 확장 ASCII 문자 출력하기

 /* 확장 ASCII 문자 출력 */


 

 #include <stdio.h>


 

 unsigned char x;   /* 확장 ASCII를 위해 부호 없는 형을 사용해야 함 */


 

 main()

 {

    /* 180부터 203까지의 확장 ASCII 문자 출력 */


 

    for(x = 180; x < 204; x++)

    {

       printf("\nASCII code %d is character %c", x, x);

    }

    return 0;

 }

 

3. 문자열의 사용
: char형 변수는 단지 하나의 문자값만을 저장할 수 있으므로 유용성은 제한적이다. 프로그램을 작성할 때에는 일련의 문자들로 구성되는 문자열(strings)을 저장하는 방법이 필요할 것이다. 사람의 이름이나 주소 등은 문자열의 예이다. 문자열을 저장하기 위한 특정 데이터형이 존재하는 것은 아니지만, C에서는 문자형 배열을 통해서 문자열을 다루게 된다.

3.1 문자형 배열
: 예를 들어, 6개의 문자를 가지는 문자열을 저장하기 위해서는 7개의 요소를 가지는 char형 배열을 선언할 필요가 있다. char형 배열은 다른 데이터형의 배열과 동일한 방법으로 선언된다. 예를 들어, 다음 문장은

  char string[10];

10개의 요소를 가지는 char형 배열을 선언한다. 이 배열은 9개의 이하의 문자로 구성되는 문자열을 저장하는 데 사용할 수 있다. 그러나 10개의 요소를 가지는 배열이 왜 9개 이하의 문자만을 저장할 수 있는지에 대해서 의문을 가질 것이다. C에서 문자열에는 일련의 문자들 외에 \0으로 표현되는 특수한 널(null) 문자가 추가된다. 널 문자는 백슬래시와 숫자 0의 두 문자로 표현되지만 실제로는 한 문자로 간주되며, ASCII 코드에서 0의 값을 가지고 있다. 그래서 C 프로그램이 문자열 Alabama를 저장할 때 실제로는 7개의 문자인 A, l, a, b, a, m, a 외에도 널 문자인 \0을 포함하고 있으므로, 전체적으로는 8개의 문자를 저장한다. 결과적으로 문자 배열에는 배열 요소의 개수보다 한 문자 적은 문자열을 저장할 수 있는 것이다.

char형 변수는 1바이트의 크기를 가지므로 char형 변수의 배열에서 사용되는 바이트 수는 배열 내의 요소의 개수와 동일하다.

3.2 문자 배열의 초기화
: C의 다른 데이터형에서와 마찬가지로 문자 배열은 선언되는 동시에 초기화될 수 있다. 문자 배열에는 다음과 같이 각각의 요소에 값을 할당할 수 있다.

  char string[10] = {'A', 'l', 'a', 'b', 'a', 'm', 'a','\0'};

그러나 일련의 문자들로 구성되는 실제 문자열(literal string)을 큰 따옴표 내에 포함시켜서 사용하는 것이 더욱 편리하다.

  char string[10] = "Alabama";

프로그램 내에서 문자열을 사용할 때 컴파일러는 문자열의 마지막 부분에 자동으로 널 문자를 추가한다. 배열을 선언할 때 첨자의 수를 지정하지 않는다면 컴파일러는 배열의 크기를 자동으로 계산해준다. 그래서 다음 문장은 8개의 요소를 가지는 배열을 선언하고 초기화한다.

  char string[] = "Alabama";

문자열에는 마지막을 뜻하는 널 문자가 반드시 포함되어야 한다는 것을 기억하기 바란다. 함수가 문자열의 마지막 부분을 인식하는 다른 방법은 전혀 없다. 만약 널 문자가 생략되면, 프로그램은 문자열이 메모리에서 널 문자가 나타나는 곳까지 확장되어 있다고 간주할 것이다. 이런 프로그래밍 에러는 다루기 힘들다....

4. 문자열과 포인터
: 앞에서는 문자열이 마지막을 표시하는 널 문자를 가지고 있으며 char형 배열에 저장된다는 것에 대해서 설명했다. 물론, 문자열이 배열에 저장될 때에는 전체 배열을 차지하지 않을 수도 있다. 문자열의 마지막은 널 문자에 의해서 표시된다는 것을 배웠으므로, 이제 문자열의 시작을 표시하는 것은 무엇인지 알 필요가 있을 것이다. 문자열의 시작 부분은 어떤 것으로 표시되는 것이 아니라 지적(point)된다고 보는 것이 더욱 정확할 것이다. 이런 내용은 약간의 힌트를 제공한다. 앞서 배운 "포인터에 대해서"에서는 배열의 이름이 첫 번째 요소에 대한 포인터라는 것을 설명했다. 그래서 배열에 저장된 문자열을 다루기 위해서는 배열의 이름을 사용하면 된다. 실제로 C에서는 배열의 이름을 통해서 문자열을 사용하는 것이 가장 일반적인 방법이다. 구체적으로 말하자면, 문자열을 다루기 위해서 배열의 이름을 사용하는 것은 C의 라이브러리 함수에서 사용되는 방법이다. C의 표준 라이브러리에는 문자열을 다루는 여러 가지 함수가 포함되어 있다. 문자열을 다루는 함수의 하나에 문자열을 전달하기 위해서는 배열의 이름을 전달하면 된다. 예를 들어, 문자열 출력 함수인 printf()와 이 장의 후반부에서 설명할 puts()에도 배열의 이름을 전달하면 된다.

지금까지는 '배열에 저장되는 문자열'에 대해서 설명했다. 그렇다면 문자열을 배열에 저장하지 않고 사용할 수 있을까? 이런 방법에 대해서 알아보도록 하자.

5. 배열에 저장되지 않는 문자열
: 앞에서는 문자열이 문자형 배열의 이름과 널 문자로 정의된다고 설명했다. 배열의 이름은 문자열의 싲가을 가리키는 char형의 포인터이다. 널 문자는 문자열의 마지막을 가리킨다. 배열에서 문자열이 차지하는 실제 공간은 이런 사항들을 통해서 확인해야 한다. 실제로 배열을 사용하는 유일한 목적은 문자열을 위한 공간을 할당해서 제공하는 것이다. 만약, 배열을 사용하여 저장 영역을 할당하지 않고도 필요한 메모리 영역을 얻을 수 있다면 어떨까? 어떤 방법을 사용하든지 마지막에 널 문자를 포함하는 문자열을 저장할 수는 있다. 또한, 배열을 통해서 할당된 메모리에 문자열을 저장할 때와 마찬가지로 첫 본째 문자에 대한 포인터를 사용하여 문자열의 시작 부분을 지적할 수 있을 것이다. 그렇다면 어떻게 메모리 저장 영역을 할당할 수 있을까? 두 가지 방법이 있는데, 하나는 프로그램이 컴파일될 때 문자열을 저장하기 위한 영역을 할당하는 것이고, 다른 하나는 동적 메모리 할당이라고 하며 프로그램이 실행되는 동안 메모리 영역을 할당하기 위해서 malloc() 함수를 사용하는 것이다.

5.1 컴파일시에 문자열의 저장 영역을 할당하는 방법
: 앞에서도 설명했듯이 문자열의 시작 부분은 char형 변수에 대한 포인터에 의해서 지적된다. 포인터를 선언하는 방법은 다음과 같다.

  char *message;

이 문장은 message라는 이름의 char형 변수에 대한 포인터를 선언한다. 지금은 이 포인터가 어떤 것도 지적하지 않지만 다음과 같은 문장을 사용하여 포인터를 선언한다면 어떻게 될까?

  char *message = "Great Caesar\'s Ghost!";

이것은 문자열의 마지막을 표시하는 널 문자를 가지는 문자열 'Great Caesar's Ghost!를 메모리 내의 특정 영역에 저장하고, 포인터 message는 문자열의 첫번째 문자를 지적하도록 초기화된다. 메모리 내에서 문자열이 저장되는 부분에 대해서는 신경 쓸 필요가 없다. 컴파일러에 의해서 자동으로 다뤄지기 때문이다. 일단 message를 정의하고 나면 문자열에 대한 포인터로 사용할 수 있다.
앞의 선언과 초기화는 다음과 같은데, 두 가지 표기인 *message와 message[]는 모두 '문자열에 대한 포인터'를 뜻한다.

  char message[] = 'Great Caesar\'s Ghost!";

이렇게 문자열의 저장 영역을 할당하는 방법은 프로그램을 작성하는 동안 필요한 문자열을 알고 있을 때 유용하다. 그러나 프로그램을 작성할 때 미리 알 수 없는 사용자의 입력 동작 이나 다른 어떤 동작에 의해서 내용이 바뀌는 문자열을 저장할 필요가 있을 때에는 어떻게 해야 할까? 이 때에는 '필요에 따라' 저장 영역을 할당해주는 malloc() 함수를 사용해야 한다.

5.2 malloc() 함수
: malloc() 은 C에서 제공되는 메모리 할당(memory allocation) 함수의 하나이다. malloc()을 호출할 때에는 필요한 메모리 양(바이트 단위)을 전달해야 한다. malloc()은 필요한 만큼의 메모리 블록을 확보해서 보존하고 블록의 첫 번째 바이트의 주소값을 돌려준다. 메모리의 위치에 대해서는 신경 쓸 필요가 없다. 자동으로 다뤄진다.

malloc() 함수는 메모리의 주소값을 돌려주고, 함수의 복귀형은 void형에 대한 포인터이다. 왜 void형에 대한 포인터를 돌려주는 것일까? void형에 대한 포인터는 모든 데이터형과 호환성이 있다. malloc()함수에 의해서 할당되는 메모리는 C의 어떤 데이터형을 저장하기 위해서도 사용될 수 있어야 하므로 void형이 가장 적합하다.


5.3 malloc() 함수 사용하기
: 하나의 char형 변수의 값을 저장하는 메모리를 할당하기 위해서 malloc()을 사용할 수도 있다. 우선, char형에 대한 포인터를 선언한다.

  char *ptr;

다음으로 malloc()을 호출하고 원하는 메모리 블록의 크기를 전달한다. char형이 1바이트를 차지하므로 1바이트의 메모리 블록이 필요할 것이다. malloc()이 돌려주는 복귀값은 포인터에 할당된다.

  ptr = malloc(1);

이 문장은 1바이트의 메모리 블록을 할당하고 주소값을 ptr에 저장한다. 프로그램에서 선언되는 변수와 달리 이런 메모리 영역은 이름을 가지지 않는다. 단지 포인터를 통해서 메모리 영역을 사용할 수 있다. 예를 들어, 문자 'x'를 할당된 메모리에 저장하기 위해서는 다음을 사용할 것이다.

  *ptr = 'x';

malloc()을 사용하여 문자열을 저장하기 위한 영역을 할당하는 것은 char형의 변수값을 저장하기 위한 영역을 할당하기 위해서 malloc()을 사용하는 것과 거의 비슷하다. 중요한 차이점이 있다면 필요한 메모리의 양을 알 필요가 있다는 것이다. 중요한 차이점이 있다면 필요한 메모리의 양을 알 필요가 있다는 것이다. 즉, 문자열에 포함되어 있는 문자의 개수를 알아야 한다. 이런 필요량은 프로그램에 따라 달라진다. 여기에서는 99개의 문자를 가지는 문자열과 마지막을 표시하는 널 문자를 합하여 전체적으로 100개의 문자를 저장하는 영역을 할당하기 원한다고 하자. 우선, char형에 대한 포인터를 선언하고 나서 malloc()을 호출한다.

  char *ptr;
  ptr = malloc(100);

이제, ptr은 100개의 문자를 가지는 문자열을 저장하기 위해서 사용할 수 있는 100바이트의 할당된 메모리 블록을 지적한다. 이제 다음과 같은 배열 선언문을 통해서 메모리를 할당한 것처럼 ptr을 사용할 수 있을 것이다.

  char ptr[100];

malloc() 함수는 프로그램에서 필요한 만큼의 메모리를 즉시 할당할 수 있게 해준다. 물론, 사용할 수 있는 메모리의 양에는 제한이 있는데, 실제 값은 컴퓨터에 설치된 메모리의 양과 프로그램에서 사용하는 메모리의 양에 따라 달라진다. 충분한 메모리가 남아 있지 않다면 malloc()은 널(0)을 돌려준다. 프로그램에서는 항상 malloc()의 복귀값을 확인하여 요구한 메모리가 성공적으로 할당되었는지 알아보아야 한다. 메모리 할당이 실패하면 malloc()은 복귀값으로 STDLIB.H에 정의되어 있는 기호 상수 NULL을 돌려준다.
<리스트 10.3>은 malloc()의 사용 예를 보여준다. malloc()을 사용하는 모든 프로그램에는 헤더 파일 STDLIB.H가 포함되어야 한다.

 

<리스트 10.3> 문자열 데이터를 저장할 영역을 할당하기 위해 malloc() 함수를 사용하는 예

 /* 문자열 데이터를 위한 공간을 할당하기 위해 malloc()을 사용하는 예 */


 

 #include <stdio.h>

 #include <stdlib.h>


 

 char count, *ptr, *p;


 

 main()

 {

    /* 35바이트의 블록 할당. 성공 여부를 확인한다. */

    /* exit() 라이브러리 함수는 프로그램을 마친다. */


 

    ptr = malloc(35 * sizeof(char));


 

    if(ptr == NULL)

    {

       puts("Memory allocation error.");

       exit(1);

    }


 

    /* A ~ Z의 ASCII 코드인 65부터 90까지의 값으로 문자열을 채운다. */

    /* p는 문자열을 진행하는 데 사용되는 포인터이다. */

    /* ptr이 문자열의 시작을 지적하게 한다. */


 

    p = ptr;


 

    for(count = 65; count < 91; count++)

       *p++ = count;


 

    /* 종료 널 문자를 추가한다. */


 

    *p = '\0';


 

    /* 화면에 문자열을 출력한다. */


 

    puts(ptr);


 

    return 0;

 }

 

-> 출력
  ABCDEFGHIJKLMNOPQRSTUVWXYZ

=> malloc()이 항상 요구한 메모리 블록을 정상적으로 할당하지는 않는다는 사실을 기억하기 바란다. 실제로는 malloc() 함수에게 메모리를 보존하도록 '지시'하는 것이 아니라 메모리를 할당하도록 '요청'하는 것이다.

6. 문자와 문자열의 출력
 프로그램에서 문자열 데이터를 사용한다면 가끔 문자열을 화면 상에 출력할 필요가 있을 것이다. 문자열은 대개 puts()나 printf() 함수를 사용하여 출력된다.

6.1 puts() 함수
: 앞에서는 이미 여러 프로그램에서 라이브러리 함수 puts()의 사용 예를 보았다. puts() 함수는 문자열을 화면 상에 출력한다. 함수의 이름(put string)도 이런 기능을 반영한 것이다. put()는 출력되는 문자열에 대한 포인터를 인수로 받아들인다. 실제 상수(literal constant)는 문자열에 대한 포인터이므로 puts()는 변수에 저장된 문자열 뿐 아니라 실제 상수로 정의된 문자열을 출력하기 위해서도 사용될 수 있다. puts() 함수는 출력할 각 문자열의 마지막에 자동으로 문자 진행(newline) 문자를 추가하므로, puts()를 사용하여 출력되는 문자열은 각각 한 줄을 차지한다. <리스트 10.4>에 있는 프로그램은 puts()의 사용 예를 보여준다.

 

<리스트 10.4> 텍스트를 화면에 출력하기 위한 puts() 함수의 사용 예

/* puts()를 사용하여 문자열을 출력하는 예 */

 #include <stdio.h>


 

 char *message1 = "c"

 char *message2 = "is the"

 char *message3 = "best"

 char *message4 = "programming"

 char *message5 = "language!!"


 

 main()

 {

    puts(message1);

    puts(message2);

    puts(message3);

    puts(message4);

    puts(message5);


 

    return 0;

 }

 

-> 출력
 C
 is the
 best
 programming
 language!!

6.2 printf() 함수
: 또한, 라이브러리 함수 printf()를 사용하여 문자열을 출력할 수도 있다. 7번째 강의에서 printf()가 데이터의 출력 형태를 형식화하기 위해서 형식화 문자열(format string)과 변환 문자(conversion specifier)를 사용한다는 것에 대해서 설명했다. 문자열을 출력하기 위해서 변환 분자 %s를 사용하자. printf()의 형식화 문자열에서 %s가 사용되면 함수는 인수 목록에서 대응하는 인수를 %s에 일치시킨다. 문자열의 경우 이런 인수는 출력하기 원하는 문자열에 대한 포인터가 되어야 한다. printf() 함수는 문자열의 마지막을 뜻하는 널 문자가 나타날 때까지 문자열을 화면 상에 출력한다. 예를 들어, 다음과 같은 문장이 있다.

  char *str = "A message to display";
  printf("%s", str);

printf()에서는 여러 개의 문자열과 숫자 변수를 일반적인 텍스트 메시지와 섞어서 출력할 수 있다.

  char *bank = "First Federal";
  char *name = "John Doe";
  int balance = 1000;
  printf("The balance at %s for %s is %d.", bank, name, balance);

다음과 같은 결과가 출력된다.

  The balance at First Federal for John Doe is 1000.

지금까지 설명한 내용을 이해한다면 프로그램 내에서 문자열 데이터를 출력할 수 있다.

7. 키보드에서 문자열을 읽어들이는 방법
: 프로그램은 문자열을 출력하는 것 외에도 가끔 키보드를 통해서 입력되는 문자열 데이터를 받아들일 필요가 있다. C 라이브러리는 이런 입력 동작을 수행하기 위해서 사용할 수 있는 두 함수 gets()와 scanf()를 제공한다. 그러나 키보드에서 문자열을 읽어들이기 위해서는 우선 문자열을 저장하기 위한 영역을 확보해야 한다. 문자열을 저장하기 위한 영역은 앞에서 설명한 배열이나 malloc() 함수를 사용하여 확보할 수 있다.

7.1 gets() 함수를 사용하여 문자열을 입력하는 방법
: gets() 함수는 키보드에서 문자열을 읽어들인다. gets() 함수는 Enter키를 눌러서 문자 진행(newline) 문자를 입력할 때까지 키보드에서 입력되는 모든 문자를 읽어들인다. 함수는 마지막의 문자 진행 문자를 제고하고 널 문자를 추가하여 처음에 함수를 호출했던 프로그램으로 문자열을 돌려준다. 이렇게 읽어들인 문자열은 char형에 대한 포인터가 가리키는 메모리 영역에 저장된다. gets()를 사용하는 프로그램은 STDIO.H 파일을 포함해야 한다. <리스트 105>는 함수의 사용 예를 보여 준다.

 

<리스트 10.5> 키보드에서 문자열 데이터를 입력하기 위해 gets()를 사용하는 프로그램

 /* gets() 라이브러리 함수의 사용 예 */


 

 #include <stdio.h>


 

 /* 입력값을 저장하기 위한 문자형 배열 할당 */


 

 char input[81];


 

 main()

 {

    puts("Enter some text, then press Enter: ");

    gets(input);

    printf("You entered: %s\n", input);


 

    return 0;

 }

 

-> 출력
  Enter some text, then press Enter:
  This is a test
  You entered: This is a test

이 예제에서는 사용되지 않았지만  gets() 함수는 복귀값을 가지고 있다. gets()는 입력 문자열이 저장되는 주소값인 char형에 대한 포인터를 돌려준다. 이것은 gets()에 전달되는 것과 동일한 값이지만, 함수는 프로그램으로 복귀값을 돌려주어 빈 줄이 입력되었는지 확인할 수 있게 해준다.<리스트 10.6>에는 이 복귀값을 사용하는 예가 나타나 있다.

 

<리스트 10.6> 빈 줄이 입력되었는지 확인하기 위해서 gets()의 복귀값을 사용하는 프로그램

 /* gets()의 복귀값을 사용하는 예 */


 

 #include <stdio.h>


 

 /* 입력값을 저장하기 위한 문자형 배열과 포인터 선언 */


 

 char input[81], *ptr;


 

 main()

 {

    /* 안내문 출력 */


 

    puts("Enter text a line a time, then press Enter.");

    puts("Enter a blank line when done.");


 

    /* 입력이 빈줄이라면 계속 순환 */


 

    while(*(ptr = gets(input)) != NULL)

       printf("You entered %s\n", input);


 

    puts("Thank you and goodbye");


 

    return 0;

 }

 

<리스트 10.6>의 while문에서 이런 비교 동작을 수행한다. 이것은 다소 복잡한 것이므로 순서대로 주의해서 살펴보도록 하자. <그림 10.1>은 내용을 쉽게 이해할 수 있도록 도와주기 위해서 문장을 구성 요소별로 구분한 것이다.

<그림 10.1> 빈 줄이 입력되었는지 확인하는 while문의 구성 요소

 

① gets() 함수는 문자 진행(newline) 문자가 입력될 때가지 키보드에서 입력되는 내용을 받아들인다.

② 문자 진행 문자를 제거하고 널 문자를 추가한 상태의 입력 문자열은 input이 지적하는 메모리 영역에 저장된다.

③ 포인터 ptr에는 input과 동일한 값인 문자열의 주소가 저장된다.

④ 할당문의 결과는 등호의 왼쪽 부분에 있는 변수의 값에 의해서 결정된다. 그래서 수식 ptr = gets(input)는 ptr이 가지는 값을 뜻한다. 이 수식을 괄호 내에 포함시키고 수식의 앞부분에 간접 연산자(*)를 추가하면, 전체적인 수식은 ptr이 지적하는 주소에 저장되는 값을 뜻하게 된다. 물론, 이것은 입력 문자열의 첫 번째 문자를 뜻하는 것이다.

⑤ NULL은 헤더 파일 STDIO.H에 정의되어 있는 기호 상수로 널 문자(0)와 같은 값을 가진다.

⑥ 입력 문자열의 첫 번째 문자가 널 문자가 아니라면, 즉 빈줄이 입력되지 않았다면 비교문은 참으로 평가되고 while문이 실행된다. 만약 첫 번째 문자가 널 문자라면, 즉 빈 줄이 입력되었다면 비교문은 거짓으로 평가되고 while문은 종료된다.

포인터를 사용하여 데이터를 저장하는 gets()나 다른 어떤 함수를 사용할 때에는 포인터가 할당된 메모리 영역을 분명히 지적하도록 해주어야 한다. 다음과 같은 실수를 저지르는 것은 흔한 일이다.

   char *ptr;
   gets(ptr);

포인터 ptr은 선언되었지만 초기화되지 않았다. ptr은 어떤 영역을 지적하고 있지만 정확히 어느 곳인지 알 수는 없다. gets() 함수는 정확한 주소를 알지 못하는 상태에서 실행되므로 입력 문자열을 ptr이 지적하는 임의의 주소에 저장한다. 저장되는 입력 문자열은 프로그램이나 운영체제와 같은 중요한 내용을 대치할 수 있다. 컴파일러는 이런 문제를 찾아주지 않으므로, 프로그램을 작성할 때에는 이렇게 초기화되지 않은 포인터의 사용에 주의할 필요가 있다.

7.2 scanf() 함수를 사용하여 문자열을 입력하는 방법
: scanf() 라이브러리 함수가 키보드에서 숫자 데이터 값을 읽어들인다는 것을 설명했다. 이 함수는 문자열을 입력하는 경우에도 사용할 수 있다. scanf()는 입력되는 내용을 받아들이는 방법을 결정하기 위해서 형식화 문자열(format string)을 사용한다는 사실을 기억하자. 문자열을 읽어들이기 위해서는 scanf()으 형식화 문자열에 변환 문자 %s를 포함시켜야 한다. gets()와 마찬가지로 scanf()에는 문자열의 저장 영역에 대한 포인터가 전달된다.

scanf()는 문자열의 시작과 끚을 어떻게 구분할까? 문자열은 공백이 아닌 첫 번째 문자에서 부터 시작된다. 문자열의 마지막은 두 가지 방법으로 결정된다. 형식화 문자열에 %s를 포함시키면 문자열의 입력은 빈칸, 탭, 문자 진행(newline) 문자와 같은 공백이 입력될 때 끝나고 공백은 문자열에 포함되지 않는다. 형식화 문자열에 %ns를 포함시키면 scanf()는 n개의 문자가 입력되거나 또는 공백이 입력될 때가지 문자열을 읽어들인다. 여기서 n은 문자의 개수를 나타낸다. 또한, 형식화 문자열에 하나 이상의 %s를 포함시키면 하나의 scanf()를 사용하여 여러 개의 문자열을 입력할 수도 있다. 방금 설명했던 문자열의 입력에 대한 규칙은 형식화 문자열에 포함되어 있는 각각의 %s에 대해서 적용된다. 예를 들어, 다음과 같은 문장을 실행하고,

   scanf("%s%s%s", s1, s2, s3);

이 문장에 대응하여 January  February  March를 입력하면 문자열 s1에는 January가 할당되고, s2에는 February가 할당되며, s3에는 March가 할당된다. 문자열의 길이를 지정하는 경우에는 어떨까 ? 만약 다음과 같은 문장을 실행하고

   scanf("%3s%3s%3s", s1, s2, s3);

September를 입력하면 s1에는 Sep이 할당되고, s2에는 tem이 할당되며, s3에는 ber가 할당된다. scanf() 함수가 요구하는 것보다 적은 문자열을 입력하거나 많은 문자열을 입력하면 어떻게 될까? 요구하는 것보다 적은 개수의 문자열을 입력하면 scanf()는 부족한 내용을 받아들이기 위해서 계속 대기할 것이고, 프로그램은 나머지 문자열이 입력될 때까지 진행되지 않을 것이다. 예를 들어, 다음 문장을 실행하고

   scanf(%s%s%s", s1, s2, s3);

January February를 입력하면 프로그램은 scanf()의 형식화 문자열에서 지정된 세 번째 문자열을 입력하도록 요구할 것이다. 또한, 요구되는 것보다 많은 개수의 문자열을 입력하면 필요하지 않은 문자열은 키보드 버퍼 내에 남아 있게 되고, 나중에 scanf()나 다른 어떤 문자열 입력문이 실행될 때 읽어들여지게 된다. 예를 들어, 다음과 같은 문장을 실행하고

   scanf("%s%s", s1, s2);
   scanf("%s", s3);

January February March를 입력하면 문자열 s1에는 January가 할당되고, s2에는 February가 할당되며, s3에는 March가 할당된다.

scanf() 함수는 성공적으로 입력된 데이터의 수에 해당하는 정수값을 복귀값으로 가진다. 이 복귀값은 대개 무시된다. 텍스트만을 입력할 때에는 scanf()보다 gets() 함수가 더 많이 사용된다. scanf() 함수는 텍스트와 숫자 데이터를 함께 입력하는 경우에 유용하게 사용된다. <리스트 10.7>에 있는 프로그램은 이런 함수들의 사용 예를 보여준다. 7번째 강의에서 설명했듯이 scanf()에서 숫자 변수의 값을 읽어들일 때에는 주소 연산자(&)를 사욜해야 한다는 것을 기억하기 바란다.

 

<리스트 10.7> 숫자와 텍스트 데이터를 입력하기 위해서 scanf()를 사용하는 프로그램

 /* 숫자와 텍스트 데이터를 입력하기 위한 scanf()의 사용 예 */


 

 #include <stdio.h>


 

 charlname[81], fname[81];

 int count, id_num;


 

 main()

 {

    /* 사용자에게 안내 */


 

    puts("Enter last name, first name, ID number separated");

    puts("by spaces, then press Enter.");


 

    /* 세 데이터 항목 입력 */


 

    count = scanf("%s%s%d", lname, fname, &id_num);


 

    /* 데이터 출력 */


 

    printf("%d items entered" %s %s %d \n", count, fname, lname, id_num);


 

    return 0;

 }

'pc관련 > C언어' 카테고리의 다른 글

C언어_변수에 범위  (0) 2019.03.28
C언어_구조체  (0) 2019.03.25
C언어_포인터에 대하여...  (0) 2019.03.24
C언어_숫자배열사용  (0) 2019.03.24
입출력의 기초  (0) 2019.03.20
Posted by 둥이파파^^
pc관련/C언어2019. 3. 24. 19:17

이 장에서는 C언어의 중요한 요소인 포인터에 대해서 다룬다. 포인터는 프로그램에서 데이터를 관리하는 강력하고 융통적인 방법을 제공해준다. 오늘은 다음과 같은 내용을 배운다.
·포인터의 정의
·포인터의 사용
·포인터를 선언하고 초기화하는 방법
·간단한 변수나 배열과 함께 포인터를 사용하는 방법
·함수에 배열을 전달하기 위해서 포인터를 사용하는 방법

여기서 설명할 내용을 읽어 보더라도 포인터를 사용할 때 얻을 수 있는 장점을 쉽게 이해할 수는 없을 것이다. 포인터의 장점은 두 가지로 나누어볼 수 있다. 하나는 포인터를 사용하지 않는 것보다 사용하는 것이 더욱 좋은 경우가 있다는 것이고, 두번째는 포인터로만 수행할 수 있는 일이 있다는 것이다. 상세한 내용은 이 장에서 설명하는 내용과 이후의 장에서 다루어지는 내용을 읽어보면 이해할 수 있을 것이다. 여기에서는 뛰어난 C 프로그래머가 되기 위해서 포인터의 개념을 반드시 이해해야 한다는 것을 기억하도록 하자.

1. 포인터란 무엇인가?
: 포인터를 이해하기 위해서는 컴퓨터의 메모리에 자료가 저장되는 방법에 대한 기본적인 내용을 알 필요가 있다. PC의 메모리에 자료가 저장되는 방법에 대한 내용을 살펴보자.
 
1.1 컴퓨터의 메모리
: PC의 RAM은 아주 많은 일련의 저장 영역으로 구성되고, 각각의 위치는 독특한 주소에 의해서 구분된다. 컴퓨터에서 메모리의 주소는 0부터 시작하여 설치되어 있는 최대 메모리의 야까지이다. 컴퓨터를 사용할 때 운영체제는 약간의 시스템 메모리를 소모한다. 또한, 프로그램을 실행하면 프로그램의 코드(프로그램의 다양한 작업을 수행하기 위한 기계어 명령어)와 데이터(프로그램이 사용 중인 자료)도 시스템의 메모리를 어느 정도 소모한다. 여기에서는 메모리에 저장되는 프로그램의 데이터에 대해서 살펴보도록 하자.

C 프로그램에서 변수를 선언할 때 컴파일러는 변수를 저장하기 위해서 독특한 주소의 메모리 영역을 보존한다. 컴파일러는 변수의 이름을 특정 주소와 관련시킨다. 프로그램 내에서 변수의 이름을 사용할 때 변수는 이전에 관련된 메모리 영역을 자동으로 사용하게 된다. 실제로는 메모리 영역의 주소가 사용되는 것이지만, 프로그래머는 메모리 영역의 주소를 다룰 필요도 없고 이런 사실을 깨닫지도 못한다. <그림 9.1에는 방금 설명한 내용이 나타나 있다. rate라는 이름의 변수가 선언되고 100의 값으로 초기화되었다. 컴파일러는 변수를 저장하기 위해서 주소 1004에 해당하는 메모리 영역을 보존해 두었고, 변수의 이름 rate는 주소 1004와 관련되어 있다.

 

<그림 9.1> 프로그램의 변수는 특정 메모리 영역에 저장된다.

 

1.2 포인터의 생성
: 방금 설명한 변수 rate나 다른 어떤 변수의 주소는 숫자값이고, 이 주소 값은 C의 다른 어떤 숫자값과 마찬가지로 취급할 수 있다는 것을 기억하자. 만약 변수의 주소를 알고 있다면 주소 값을 저장하는 또다른 변수를 생성할 수 있다. 우선 rate의 주소 값을 저장하기 위한 변수를 선언하는 것이다. 예를 들어, p_rate라는 이름의 변수를 선언한다. p_rate는 초기화되지 않는다. p_rate를 저장하기 위한 메모리 영역이 보존되지만, p_rate의 실제 값은 <그림 9.2>에서 볼 수 있듯이 아직까지 결정되지 않았다.

 

<그림 9.2> 변수 p_rate를 저장하기 위한 메모리 영역이 할당되었다.

 

다음으로, 변수 p_rate에 변수 rate의 주소 값을 저장한다. 이제 p_rate에는 rate의 주소 값이 저장되어 있으므로 메모리에서 rate의 갓이 저장되어 있는 위치를 알려줄 것이다. 이런 상태를 C에서는 'p_rate가 rate를 지적한다.'고 하며 p_rate를 rate에 대한 '포인터(pointer)'라고 한다. <그림 9.3>은 이런 개념을 설명한다.

 

<그림 9.3> 변수 p_rate는 변수 rate의 주소값을 가지고, rate에 대한 포인터이다.

 

정리하면, 포인터는 다른 어떤 변수의 주소값을 가지는 변수이다. 이제, C 프로그램에서 포인터를 사용하는 것에 대한 상세한 내용을 살펴보도록 하자.

2. 포인터와 간단한 변수
: 앞의 예제에서, 포인터 변수는 배열이 아닌 간단한 변수를 지적하는 것이었다. 여기에서는 간단한 변수에 대한 포인터를 생성하고 사용하는 방법을 설명할 것이다.

2.1 포인터의 선언
: 포인터는 숫자 변수이고, 다른 모든 변수와 마찬가지로 사용하기 전에 선언해야 한다. 포인터 변수의 이름은 다른 변수에서와 동일한 규칙을 따르며, 독특한 것이어야 한다. 이 장에서는 변수 name에 대한 포인터가 p_name이라는 이름을 사용한다고 가정할 것이다. 실제로 p_name이라는 이름을 반드시 사용할 필요는 없다. C에서 변수 이름에 적용되는 규칙을 따른다면 원하는 이름을 포인터 변수로 사용할 수 있다.
포인터는 다음과 같은 형식으로 선언된다.

  typename *ptrname;

typename은 C의 데이터형으로, 포인터가 지적하는 변수의 형태를 말한다. 별표 문자(*)는 간접 연산자 (indirection operator)로 ptrname이 typename형의 변수가 아니라 typename형에 대한 포인터라는 것을 뜻한다. 포인터는 일반적인 변수와 함께 선언될 수도 있다. 다음은 몇 가지 예이다.

  char *ch1, *ch2       /* ch1과 ch2는 char형에 대한 포인터이다. */
  float *value, percent;  /* value는 float형에 대한 포인터이며,
                          percent는 일반적인 float형 변수이다. */

2.2 포인터의 초기화
: 이제 포인터를 선언했으므로 포인터를 사용하면서 어떤 작업을 수행할 수 있는지 알아보도록 하자. 포인터 변수는 어떤 변수를 지적하도록 초기화될 때까지 아무 것도 수행할 수 없다. 일반적인 변수에서와 마찬가지로 초기화되지 않은 포인터도 사용할 수 있지만, 나타나는 결과는 예상할 수 없는 것이며 잠재적으로 치명적인 것일 수도 있다. 즉, 포인터는 어떤 변수의 주소값을 가질 때가지 전혀 유용하지 않다. 변수의 주소값은 포인터에 자동으로 저장되는 것이 아니다. 프로그램 내에서는 주소 연산자(&)를 사용하여 주소값을 포인터에 저장해야 한다. 주소 연산자는 변수의 이름 앞에서 사용되고 변수의 주소값으 돌려준다. 그래서 포인터는 다음과 같은 형식으로 초기화될 수 있다.

  pointer = &variable;

<그림 9.3>에 있는 내용을 다시 한 번 살펴보자. 변수 p_rate가 변수 rate의 주소 값을 가지도록 초기화하는 프로그램 문장은 다음과 같은 내용이 될 것이다.

  p_rate = &rate;    /* rate의 주소 값을 p_rate에 할당. */

초기화를 수행하기 전에는 p_rate가 특별히 어떤 변수를 지적하지 않는다. 초기화가 수행되면 p_rate는 rate에 대한 포인터가 된다.

2.3 포인터의 사용
: 포인터를 선언하고 초기화하는 방법을 설명했으므로, 포인터를 사용하는 방법에 대해서 알아보기 원할 것이다. 여기에서는 다시 간접 연산자(*)를 사용한다. 간접 연산자를 포인터 변수 앞에 위치시키면 포인터가 지적하는 변수를 가리키게 된다. 여기서, 포인터 p_rate가 변수 rate를 지적하도록 초기화했던 앞의 예제를 사용하자. *p_rate는 변수 rate를 지적하도록 초기화했던 앞의 예제를 사용하자. *p_rate는 변수 rate를 뜻한다. 예제에서 100의 값을 가지고 있는 rate를 출력하기 원한다면 다음과 같은 문장을 작성할 수 있을 것이다.

  printf("%d", rate);

또한, 다음과 같은 문장도 사용할 수 있다.

  printf("%d", *p_rate);

C에서 이런 두 문장은 동일하다. 변수의 이름을 통해서 변수의 값을 사용하는 방법을 직접 사용(direct access)이라고 한다. 변수에 대한 포인터를 통해서 변수의 값을 사용하는 방법을 간접 사용(indirect access)이나 간접(indirection)이라고 한다. <그림 9.4>는 간접 연산자를 포함하는 포인터의 이름이 관련 변수의 값을 나타낸다는 사실을 설명하고 있다.

 

<그림 9.4> 포인터에서 간접 연산자의 사용.

 

여기서 잠깐동안 지금까지 설명한 내용을 정리해보도록 하자. 포인터는 C 언어에서 중요한 요소로, 기본적으로 반드시 알아두어야 하는 내용이다. 포인터에 대해서는 많은 사람들이 혼란스러움을 느끼고 있으므로, 지금까지의 내용을 정확히 이해하지 못했더라도 걱정할 필요는 없다. 처음부터 다시 읽어볼 필요가 있다고 느껴지면 그렇게 하도록 하자. 만약 변수 var를 지적하도록 초기화된 par이라는 포인터가 있다면, 다음은 사실이다.
·*ptr과 var는 어떤 값이 저장되어 있든지 관계없이 변수 var에 저장되어 있는 값을 뜻함.
·ptr과 &var는 var의 주소를 뜻하는 것이다.

두 번째 항목에서 알 수 있듯이, 간접 연산자를 포함하지 않는 포인터의 이름은 관련된 변수의 주소값인 포인터 자체의 값을 나타낸다. <리스트 9.1>에 있는 프로그램은 기본적인 포인터의 사용 예를 보여준다. 이 프로그램은 반드시 입력하고 컴파일하여 실행해보도록 하자.

 

<리스트 9.1> 기본적인 포인터의 사용 예

 

 /* 기본적인 포인터의 사용 예 */


 

 #include <stdio.h>


 

 /* int형 변수의 선언과 초기화 */


 

 int var = 1;


 

 /* int형에 대한 포인터 선언 */


 

 int *ptr;


 

 main()

 {

    /* ptr이 var를 가리키도록 초기화 */


 

    ptr = &var;


 

    /* var를 직접/간접 사용 */


 

    printf("\nDirect access, var = %d", var);

    printf("\nIndirect access, var = %d", *ptr);


 

    /* var의 주소를 두 가지 방법으로 출력 */


 

    printf("\n\nThe address of var = %d", &var);

    printf("\nThe address of var = %d", ptr);


 

    return 0;

 }

 

-> 출력
Direct access, var = 1
Indirect access, var = 1

The address of var = 4264228
The address of var = 4264228

3. 포인터와 변수의 형태
: 지금까지는 여러 가지 변수형을 사용하여 메모리에서 서로 다른 크기의 저장 영역을 소모하는 경우에 대해서 다루지 않았다. 대부분의 일반적인 PC 운영체제에서 int형은 2바이트, float형은 4바이트 등을 차지한다. 메모리의 개별적인 바이트는 자신만의 주소를 가지고 있으므로 여러 바이트를 차지하는 변수는 실제로 여러 개의 주소에 걸쳐서 저장된다.

그렇다면 포인터는 이렇게 여러 바이트를 사용하는 변수의 주소 값을 어떻게 관리할까? 다음과 같은 방법을 통해서 해결하고 있다. 포인터에 저장되는 변수의 주소는 실제로 변수가 차지하고 있는 메모리의 가장 낮은 바이트의 주소이다. 세 개의 변수를 선언하고 초기화하는 예를 통해서 이런 사실을 확인해보자.

  int vint = 12252;
  char vchar = 90;
  float vfloat = 1200.156004;

이런 세 개의 변수는 <그림 9.5>에 나타나 있는 대로 메모리에 저장된다. int형 변수는 2바이트를 차지하고, char형 변수는 1바이트를 차지하며, float형 변수는 4바이트를 차지한다.

 

<그림 9.5>

 

이제 세 개의 변수에 대한 포인터를 선언하고 초기화 하자.

  int *p_vint;
  char *p_vchar;
  float *p_vfloat;
  /* 그 밖의 프로그램 문장 */
  p_vint = &vint;
  p_vchar = &vchar;
  p_vfloat = &vfloat;

각각의 포인터에는 관련된 변수의 첫 번째 바이트의 주소값이 저장된다. 그래서 p_vint에는 1,000의 값이 저장되고, p_vchar에는 1003의 값이 저장되며, p_vfloat에는 1,006의 값이 저장된다. 각각의 포인터는 특정 형태의 변수를 지적하도록 선언되어 있다는 것을 기억하자. 컴파일러는 int형에 대한 포인터가 2바이트의 첫 번째 바이트를 지적하고, float형에 대한 포인터가 4바이트의 첫 번째 바이트를 지적해야 한다는 것을 알고 있다. 이런 내용이 <그림 9.6>에 설명되어 있다.
<그림 9.5>와 <그림 9.6>에는 세 변수들 간에 비어 있는 저장 영역이 나타나 있다. 이것은 시각적으로 각각의 변수를 분명히 구분하기 위해서 사용되었다. 실제로는 C 컴파일러가 변수의 크기를 고려하여 인접한 메모리 영역에 세 개의 변수를 저장한다.

 

<그림 9.6> 컴파일러는 포인터가 지적하는 변수의 크기를 "안다

 

4. 포인터와 배열
: 포인터를 간단한 변수와 함께 사용하는 것도 유용하지만 배열과 함께 사용할 때 더 유용하다. C에서 포인터와 배열은 밀접하게 관련되어 있다.

4.1 포인터로 사용되는 배열의 이름
: 괄호를 포함하지 않는 배열의 이름은 배열의 첫 번째 요소에 대한 포인터이다. 그래서 data[]라는 배열을 선언한다면, data는 첫 번째 배열 요소의 주소값을 나타낸다. 그렇다면 '주소값을 구하기 위해서 주소 연산자를 사용할 필요가 없는가?'라는 의문을 가질 수 있을 것이다. 그러나 배열의 첫 번째 요소의 주소값을 구하기 위해서는 &data[0] 이라는 수식을 사용할 수도 있다. C에서 (data == &data[0])이라는 관계 수식은 참으로 평가된다. 앞에서 배열의 이름이 배열에 대한 포인터를 뜻한다는 사실을 언급했다. 배열의 이름은 포인터 상수(pointer constant)이다. 프로그램이 실행되는 동안 배열의 주소는 변경될 수 없고 고정된 값을 가지게 된다. 실제로 이런 사실은 쉽게 이해할 수 있다. 만약 포인터 상수인 배열의 주소값을 변경한다면, 다른 어떤 메모리 영역을 지적하게 되므로 메모리 내에서 고정된 영역에 존재하는 배열을 지적하지 않게 될 것이다. 그러나 포인터 변수를 선언하고 배열을 지적하도록 초기화할 수 있다. 예를 들어, 다음 문장은 포인터 변수 p_array가 array[]의 첫 번째 요소의 주소값을 가지도록 초기화한다.

  int array[100], *p_array;
  /* 그 밖의 프로그램 문장 */
  p_array = array;

p_array는 포인터 변수이므로 다른 어떤 주소를 지적하도록 변경할 수도 있다. array와 달리 p_array는 array[]의 첫 번째 요소만을 지적하도록 제한되지 않는다. 예를 들어, p_array는 array[] 내의 다른 요소를 지적할 수도 있다. 어떻게 이런 일이 가능할까? 우선, 배열의 요소가 메모리에 저장되는 방법을 살펴보도록 하자.

4.2 배열의 요소가 메모리에 저장되는 방법
: 배열의 각 요소는 첫 번째 요소가 가장 낮은 주소값을 가지는 영역에 저장되는 순서대로 메로리에 저장된다. 0보다 큰 첨자값을 가지는 다른 요소는 더 높은 주소값의 저장 영역에 저장된다. 요소가 저장되는 메모리 영역의 차이는 char, int, float 등과 같은 배열의 형태에 따라 달라진다. int형의 배열을 예로 들어보자. "데이터 저장하기 : 변수와 상수"에서 배웠듯이 하나의 int형 변수는 메모리에서 2바이트를 차지할 것이다 .각각의 배열 요소는 앞의 요소와 2바이트 떨어진 곳에 저장되고, 각 배열 요소의 주소는 앞의 요소보다 2바이트 높은 곳을 가리킬 것이다. 반면에, float형 배열의 요소는 4바이트를 차지할 것이다. float형의 배열에서 배열의 각 요소는 앞의 요소와 4바이트 떨어진 곳에 저장되고, 각 배열 요소의 주소는 앞의 요소 보다 4바이트 높은 곳을 가리킬 것이다. <그림 9.7>은 6개의 요소를 가지는 int형 배열과 3개의 요소를 가지는 float형 배열에서 배열과 주소의 관계를 보여준다.

 

 

<그림 9.7> 서로 다른 형태의 배열이 자장되는 방법

 

<그림 9.7>을 살펴보면 다음의 관계 수식이 참으로 평가되다는 사실을 이해할 수 있을 것이다.

   1: x == 1000
   2: &x[0] == 1000
   3: &x[1] == 1002
   4: expenses == 1250
   5: &expenses[0] == 1250
   6: &expenses[1] == 1254

괄호를 포함하지 않는 배열의 이름 x는 첫 번째 요소 x[0]의 주소값을 뜻한다. <그림 9.7>을 보면 x[0]의 주소 1000에 저장되어 있다는 것을 알 수 있다. 또한, 2번 수식도 '배열 x의 첫 번째 요소의 주소가 1000이다'는 사실을 보여준다. 3번 수식은 1의 첨자값을 가지는 배열의 두 번째 요소의 주소가 1002라는 것을 알려준다. 다시 <그림 9.7>을 살펴보면 이 사실을 확인할 수 있다. 4, 5, 6번 수식은 1, 2, 3변 수식과 동일한 개념을 설명한다. 단지 두 배열 요소의 주소값의 차이가 다르다. int형 배열 x에서는 주소의 차이가 2바이트 이지만, float형 배열 expenses에서는 4바이트의 차이가 있다.

포인터를 사용하여 연속적인 배열 요소를 어떻게 다룰 수 있을까? 앞의 예제에서는 int형 배열의 각 요소를 사용하기 위해서 포인터를 2씩 중가시켜야 하고, float형 배열의 각 요소를 사용하기 위해서는 포인터를 4씩 증가시켜야 한다는 것을 알 수 있을 것이다. 여기서, 특정 데이터형의 배열에서 각각의 요소를 사용하기 위해서는 포인터를 sizeof(datatype)씩 증가시켜야 한다는 결론을 얻을 수 있다. <리스트 9.2>에 있는 프로그램은 int, float, double형의 배열을 선언하고 각 요소의 주소값을 출력하여 서로 다른 형태의 배열에서 주소와 요소의 관계를 보여준다.

 

 /* 서로 다른 데이터형의 배열의 주소와 요소 간의 관계 */


 

 #include <stdio.h>


 

 /* 세 배열과 카운터 변수 선언 */


 

 int i[10], x;

 float f[10];

 double d[10];


 

 main()

 {

    /* 테이블 헤더 출력 */


 

    printf("\t\tInteger\t\tFloat\t\tDouble");


 

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

    printf("=============================");


 

    /* 각 배열 요소의 주소 출력 */


 

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

    printf("\nElement %d: \t%d\t\t%d\t\t%d", x, &i[x], &f[x], &d[x]);


 

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

    printf("=============================");


 

    return 0;

 }

 

4.3 포인터 연산
: 지금까지 배열의 첫 번째 요소에 대한 포인터를 사용하는 방법을 배웠다. 포인터는 배열에 저장되는 데이터형의 크기만큼 증가한다. 포인터를 사용하여 어떻게 배열의 모든 요소를 사용할 수 있을까? 포인터 연산(pointer arithmetic)이 필요하다.
어떤 사람은 '골치 아프게 필요하지도 않은 또다른 연산을 배우는구나'하고 생각할 것이다. 그러나 포인터 연산은 아주 간단하고 프로그램 내에서 포인터를 더욱 사용할 수 있도록 도와주는 것이므로 전혀 걱정할 필요가 없다. 포인터 연산에는 증가와 감소를 수행하는 두 가지 동작이 존재한다.

▶ 포인터의 증가
: 포인터를 증가시키면 포인터에 저장된 주소값을 증가시키게 된다. 예를 들어, 배열을 가리키는 포인터가 있을 때, 포인터 연산을 통해서 포인터의 값을 1증가시키면 포인터는 자동으로 배열의 다음 요소를 지적하게 된다. 즉, C는 포인터의 선언을 통해서 포인터가 지적하는 데이터의형을 '알게 되므로' 데이터 형의 크기에 따라서 포인터에 저장된 주소값을 증가시킨다.

ptr_to_int가 int형 배열의 특정 요소에 대한 포인터 변수라고 가정하자. 만약 다음과 같은 문장을 사용하면

  ptr_to_int++;

ptr_to_int의 값은 일반적으로 2반이트를 차지하는 int형의 크기만큼 증가되므로 ptr_to_int는 이제 배열의 다음 요소를 지적한다. 만약 ptr_to_float가 float형 배열의 요소를 지적한다면, 다음 문장은

  ptr_to_float++;

대개 4바이트를 차지하는 float형의 크기만큼 ptr_to_float의 값을 증가시킨다. 이것은 1이상의 값을 증가시키는 경우에도 적용된다. 만약 포인터에 n의 값을 더하면, C는 배열의 데이터형에 따라 포인터가 배열에서 n요소만큼 이동된 곳을 지적하도록 값을 증가시킨다. 그래서 다음 문장은

  ptr_to_int += 4;

prt_to_int에 4를 더하므로, 정수형이 2바이트라고 가정할 때 실제 포인터 변수의 값에는 8이 더해지고, 포인터는 현재의 위치에서 4변째 뒤에 있는 배열 요소를 지적한다. 비슷하게, 다음 문장은

  ptr_to_float += 10;

ptr_to_float에 10을 더하므로, 부동 소수형이 4바이트라고 가정할 때 포인터 변수의 실제 값에는 40이 더해지고, 포인터는 현재의 위치에서 10변째 뒤에 있는 배열 요소를 지적하게 된다.

▶ 포인터의 감소
: 이런 개념은 포인터를 감소시키는 경우에도 적용된다. 포인터를 감소시키는 것은 실제로 음수(negative) 값을 더하는 증가의 특수한 경우이다. --나 -=연산자를 사용하여 포인터를 감소시키면 포인터는 자동으로 배열 요소의 크기에 따라 변경된다.

<리스트 9.3>은 배열의 요소를 사용하기 위해서 포인터 연산을 응용한 예를 보여준다. 이 프로그램은 포인터를 증가시켜 배열의 모든 요소를 효과적으로 관리한다.

 

<리스트 9.3> 배열의 요소를 다루기 위한 포인터 연산과 포인터 표기 사용하기

 

 

 /* 포인터 표기로 배열 요소를 사용하기 위해 */

 /* 포인터 연산을 사용하는 예 */


 

 #include <stdio.h>

 #define MAX 10


 

 /* 정수 배열 선언과 초기화 */


 

 int i_array{MAX] = {0, 1, 2, , 3, 4, 5, 6, 7, 8, 9};


 

 /* int 변수와 int 변수에 대한 포인터 선언 */


 

 int *i_ptr, count;


 

 /* 부동 소수형 배열 선언과 초기화 */


 

 float f_array[MAX] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};


 

 /* 부동 소수에 대한 포인터 선언 */


 

 float *f_ptr;


 

 main()

 {

    /* 포인터 초기화 */

    i_ptr = i_array;

    f_ptr = f_array;


 

    /* 배열 요소 출력 */

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

    printf("\n%d\t%f", *i_ptr++, *f_ptr++);


 

    return 0;

 }

 

<리스트 9.3>에 있는 프로그램은 포인터를 사용하지 않고 배열의 첨자만을 사용하여 동일한 작업을 수행할 수 있을 것이다. 이런 간단한 프로그래밍 작업에서는 포인터의 사용이 중요한 장점을 제공하지 않을 수도 있다. 그러나 더욱 복잡한 프로그램을 작성하게 되면, 포인터의 사용이 더욱 큰 장점을 제공해준다는 것을 알게 될 것이다.

포인터 상수에 대해서는 증가와 감소 연산을 수행할 수 없다는 것을 기억하기 바란다. 괄호를 포함하지 않는 배열의 이름은 포인터 상수이다. 또한, 배열의 요소에 대한 포인터를 사용할 때 C컴파일러는 배열의 시작과 마지막을 분명하게 구분하지 못한다는 것을 기억하자. 주의하지 않으면, 포인터는 메모리에서 배열의 이전이나 이후에 해당하는 어떤 부분을 지적하도록 증가되거나 감소될 수도 있을 것이다. 물론, 범위를 벗어나는 영역에는 어떤 값이 저장되어 있겠지만 배열의 요소가 아니다. 포인터를 사용할 때에는 주어진 범위를 벗어나지 않도록 해야 한다.

▶ 그 밖의 포인터 응용
: 포인터를 사용하는 다른 한 가지 연산 동작은 두 포인터의 간격을 계산하는 차이 계산 {differencing)이다. 만약 동일한 배열 내의 서로 다른 요소에 대한 두 포인터가 있고, 하나에서 다른 하나의 값을 뺀다면 두 요소가 얼마나 멀리 떨어져 있는지 알 수 있다. 여기에서도 포인터 연산은 자동으로 배열 요소의 크기를 고려하여 계산을 수행한다. 그래서 ptr1과 ptr2가 어떤 형태의 배열 요소에 대한 포인터라면, 두 요소가 얼마나 멀리 떨어져 있는지 알아보기 위해서 다음과 같은 문장을 사용할 수 있다.

  ptr1 - ptr2

포인터의 비교는 동일한 배열 요소를 지적하는 포인터에서만 유효하고, 관계 연산자인 ==, !=, >, <, >=, <=를 정상적으로 사용할 수 있다. 작은 첨자값을 가지는 배열의 요소는 항상 큰 첨자값을 가지는 배열의 요소보다 낮은 주소값을 가진다. 그래서 ptr1과 ptr2가 동일한 배열의 두 요소를 지적할 때, 만약 ptr1이 ptr2보다 배열 내에서 앞에 존재하는 요소를 지적한다면, 다음 문장은

  ptr1 < ptr2

참으로 평가된다. 지금까지 모든 포인터 연산을 다루었다. 곱셈이나 나눗셈과 같이 일반적인 변수에서 수행되는 많은 산술 동작을 포인터에서는 수행할 수 없다. C 컴파일러는 포인터에 대한 산술 동작을 허용하지 않는다. 예를 들어, ptr이 포인터라면 다음 문장은

  ptr *= 2;

에러 메시지를 나타낼 것이다. <표 9.1>에 나타나듯이 포인터에서 6가지 연산을 수행할 수 있고, 이 장에서는 모든 내용을 설명했다.

 

<표 9.1> 포인터 연산

 연산

설명 

 할당(Assingnment)

 

 포인터에 어떤 값을 할당할 수 있다. 할당되는 값은 주소연산자(&)로

 구하거나 또는 포인터 상수인 배열의 이름에서 구한 주소값이어야

 한다.

 간접 사용(Indirection)

 간접 연산자(*)는 포인터가 지적하는 곳에 저장되어 있는 값을 구한다.

 주소(Address)

 

 포인터의 주소를 얻기 위해서도 주소 연산자를 사용할 수 있으므로

 포인터에 대한 포인터를 선언할 수도 있다. 이것은 다소 깊이 있는

 내용이므로 15변째 강의("포인터 : 고급기능들"에서 다룰 것이다.

 

 증가(Incrementing)

 다른 메모리 위치를 지적하기 위해 포인터를 중가시킬 수 있다.

 감소(Decrementing)

 다른 메모리 위치를 지적하기 위해 포인터를 감소시킬 수 있다.

 차이 계(Differencing)

 다름 메모리 위치를 지적하기 위해 포인터에서 정수를 뺄 수 있다.

 비교(Comparison)

 동일한 배열의 두 요소를 지적하는 포인터에서만 유효하다.

 

5. 포인터에 대한 주의 사항
: 프로그램 내에서 포인터를 사용할 때에는 할당문의 왼쪽에 초기화되지 않은 포인터를 입력하는 것과 심각한 문제를 일으키지 않도록 주의해야 한다. 예를 들어, 다음과 같은 문장은 int형의 포인터를 선언한다.

  int *ptr;

그러나 이 변수는 아직까지 초기화되지 않았으므로 어떤 것도 지적하지 않는다. 더욱 정확히 말하면, 포인터는 알려져 있는(known) 어떤 영역을 지적하지 않는다. 초기화되지 않은 포인터도 어떤 값을 가지고 있기는 하지만 무엇인지는 알 수 없다. 대부분의 경우에 초기화되지 않은 포인터는 0의 값을 가진다. 할당문 내에서 초기화되지 않은 포인터를 사용한다면 다음과 같은 문제가 발생한다.

  *ptr = 12;

ptr이 지적하는 곳에는 12의 값이 저장된다. ptr이 지적하는 주소는 메모리 내의 임의의 영역이므로 운영체제가 저장되거나 또는 프로그램 자체가 저장된 곳이 될 수도 있다. 이렇게 중요한 메모리 영역에 임의로 12를 저장하면 이전에 저장되어 있던 중요한 자료가 바뀔 수 있으므로, 그로 인한 결과는 프로그램이 잘못 동작하거나 또는 시스템이 완전히 정지되는 것에 이르기까지 치명적인 상황이 될 수도 있다. 결국, 할담문의 왼쪽에서는 초기화도지 않은 포인터를 절대로 사용하지 않아야 한다. 프로그램 내에서 초기화도지 않은 포인터를 사용할 경우에는 덜 심각한 다른 문제가 발생할 수도 있겠지만 포인터를 사용하기 전에는 항상 초기화하는 것이 좋다. 이런 초기화 동작은 직접 수행해야 한다. 컴파일러가 포인터를 초기화해주는 것은 아니기 때문이다.

6. 배열의 첨자 표기 방법과 포인터
: 괄호를 포함하지 않는 배열의 이름은 첫 번째 요소에 대한 포인터이다. 그래서 간접 연산자를 사용하면 배열의 첫 번째 요소를 사용할 수 있다. 예를 들어, array[]라는 배열이 선언되어 있다면 *array는 배열의 첫 번째 요소이고, *(array + 1)은 배열의 두 번째 요소이다. 그래서 다음과 같은 수식은 모두 참으로 평가된다.

  *(array) == array[0]
  *(array + 1) == array[1]
  *(array + 2) == array[2]
  …
  *(array + n) == array[n]

이것은 배열의 첨자식 표기 방법과 포인터식 표기 방법이 동일하다는 것을 보여준다. 프로그램 내에서는 어떤 방법이든지 사용할 수 있다. C컴파일러는 두 가지 표기가 모두 포인터를 사용하여 배열의 데이터를 표현하는 방법이라고 생각할 것이다.

7. 함수에 배열을 전달하는 방법
: 이 장에서는 C에서 포인터와 배열 간에 특수한 관련이 있다는 것을 이미 언급했었다. 이런 관계는 특히 함수의 인수로 배열을 전달할 필요가 있을 때 중요하다. 함수에 배열을 전달하는 유일한 방법은 포인터를 사용하는 것이다.
 인수는 함수를 호출하는 프로그램이 함수에 전달하는 값을 말한다. 인수는 int형, float형 또는 다른 어떤 데이터형이 될 수 있지만, 반드시 하나의 숫자 값이어야 한다. 그래서 하나의 배열 요소는 인수가 될 수 있지만 배열 요소 전체는 인수가 될 수 없다. 그렇다면, 배열 전체를 함수에 전달할 필요가 있을 때에는 어떻게 해야 할까? 배열에 대한 포인터, 즉 배열의 첫 번째 요소의 주소값은 하나의 숫자값이다. 만약 배열의 주소값을 함수에 전달한다면, 함수는 배열의 주소를 '알 수' 있으므로 포인터를 사용하여 배열의 모든 요소를 다룰 수 있게 될 것이다.
 다른 한 가지 문제를 생각해보자. 배열을 인수로 받아들이는 함수를 작성할 때에는 함수가 다양한 크기의 배열을 받아들여서 사용하도록 할 필요가 있을 것이다. 예를 들어, 정수형 배열에서 가장 큰 값을 가지는 요소를 발견하는 함수를 작성한다고 가정해보자. 만약 함수가 고정된 크기, 즉 제한된 개수의 요소를 가지는 배열만을 받아들인다면 유용하게 사용할 수 없을 것이다.
 첫번째 주소값이 함수에 전달되는 배열의 크기를 어떻게 알 수 있을까? 함수에 전달되는 값은 배열의 첫 번째 요소의 주소가 될 수도 있고, 10,000개의 요소 중에서 첫 번째 요소의 주소가 될 수도 있다. 함수에게 배열의 크기를 알려주는 두 가지 방법이 있다. 우선 배열의 마지막에 특수한 값을 저장하여 구분하는 방법이다. 함수에서 배열을 사용할 때 각 요소에서 특수한 값을 확인하고 특수한 값이 발견되면 배열의 마지막에 도달한 것이다. 이 방법의 단점은 배열에 저장할 수 있는 실제 데이터에 제한을 주고, 배열의 마지막을 구분하기 위해서 특정값을 사용하지 말고 남겨 두어야 한다는 것이다. 다른 한 가지 방법은 훨씬 더 융통성 있고 직접적이며, 여기서 사용되는 방법으로 함수에 배열의 크기를 인수 형태로 전달하는 것이다. 이것은 간단한 ing형의 인수가 될 것이다. 그래서 함수는 두 개의 인수를 전달받는다. 첫번째 인수는 배열의 첫번째 요소에 대한 포인터이고, 두 번째 인수는 배열내의 요소의 수를 나타내는 정수값이다.

 

<리스트 9.4> 배열을 함수에 전달하는 예제 프로그램

 

 /* 함수에 배열 전달하기 */


 

 #include <stdio.h>


 

 #define MAX 10


 

 int array[MAX], count;


 

 int largest(int x[], int y);


 

 main()

 {

    /* 키보드에서 MAX값을 읽는다. */

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

    {

       printf("Enter an integer value: ");

       scanf("%d", &array[count]);

    }


 

    /* 함수를 호출하고 복귀값을 출력한다. */

    printf("\n\nLargest value = %d\n", largest(array, MAX);


 

    return 0;

 }

 /* 함수 largest()는 정수 배열에서 가장 큰 값을 돌려준다. */


 

 int largest(int x[], int y)

 {

    int count, biggest = -12000;


 

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

    {

       if(x[count] > biggest)

          biggest = x[count];

    }

    return biggest;

 }

 

-> 출력
10개의 수를 순서없이 입력한다. 그러면 그 중에서 가장 큰 값을 출력한다.

'pc관련 > C언어' 카테고리의 다른 글

C언어_구조체  (0) 2019.03.25
C언어_문자,문자열  (0) 2019.03.24
C언어_숫자배열사용  (0) 2019.03.24
입출력의 기초  (0) 2019.03.20
숫자배열 사용하기  (0) 2019.03.20
Posted by 둥이파파^^
pc관련/C언어2019. 3. 24. 19:04

배열은 C 프로그램 내에서 가끔 사용되는 데이터 저장 영역의 하나이아. 오늘은 다음과 같은 내용을 다룰 것이다.


·배열은 무엇인가?

·일차원 배열과 다차원 배열의 정의

·배열의 선언과 초기화


1. 배열이란 무엇인가?

: 배열(array)은 동일한 데이터형과 동일한 이름을 가지는 집단적인 데이터 저장 영역을 말한다. 배열의 각 저장 영역을 배열 요소(array element)라고 한다. 프로그램에서는 왜 배열을 사용해야 하는 것일까? 이것은 예제를 통해서 설명하도록 하겠다. 만약 1998년에 월별로 접수된 영수증을 사용하여 재정 상태를 관리한다면, 12개 서류철로 나누어 관리할 수도 있겠지만 12개의 부분으로 나누어진 하나의 서류철에서 관리하는 것이 더 나을 것이다.


이 예제를 컴퓨터 프로그래밍에 적용해보자. 재정 관리를 위한 프로그램을 작성한다고 가정하자. 프로그램은 12개의 독립된 변수와 합계를 저장하는 변수를 선언하여 월별 지출액을 관리할 수 있을 것이다. 이것은 월별 영수증을 보관하기 위한 12개의 독립된 서류철을 사용하는 것과 유사하다. 그러나 효율적인 프로그램이라면 월별 지출액을 저장하는 12개의 요소와 합계를 저장하기 위한 하나의 요소를 가지는 배열을 사용할 것이다. 이것은 하나의 서류철에서 월별 영수증을 모두 관리하는 것과 유사하다.


1.1 일차원 배열

: 일차원 배열(single-dimensional array)은 하나의 첨자만을 사용하는 배열이다. 첨자(subscript)는 배열의 이름 위에 있는 대괄호에서 사용되는 숫자이다. 이것은 개벽적인 배열 요소의 번호를 나타낸다. 재정 관리 프로그램에서 사용하기 위한 float형의 배열을 선언하기 위해서는 다음과 같은 문장을 작성할 수 있을 것이다.


  float expenses[12];


배열은 expenses라는 이름을 사용하고, 12개의 요소를 가진다. 12개의 각 요소는 개별적인 부동 소수형 변수와 동일하다. 배열에는 C의 모든 데이터형을 사용할 수 있다. C의 배열 요소는 항상 0에서부터 시작하므로 12개의 요소는 0부터 11까지의 번호를 사용할 것이다. 1월의 지출액은 expenses[0]에 저장되고, 2월의 지출액은 expenses[1]에 저장된다.


배열을 선언할 때 컴파일러는 전체 배열을 저장하기에 충분한 메모리 영역을 보존한다. 소스코드에서 배열이 선언되는 위치는 중요하다. 다른 변수에서와 마찬가지로, 배열이 선언되는 위치는 프로그램에서 배열을 사용하는 방법에 영향을 준다. 변수나 배열이 선언되는 위치가 프로그램에 미치는 영향은 낭중에 상세히 설명하고, 일단 main() 함수가 시작되기 전에 다른 변수 선언문과 함께 배열 선언문을 위치시키도록 하자.  배열의 요소는 배열이 아닌 동일한 형태의 변수를 사용할 수 있는 곳이면 프로그램 내에서 어디든지 사용할 수 있다. 배열의 각 요소는 배열의 이름과 대괄호 내에 포함된 요소의 첨자에 의해서 사용된다. 예를 들어, 다음 문장은 두 번째  배열 요소에 89.95의 값을 저장한다. 배열의 처 번째 요소는 expenses[1]이 아니라 expenses[0]이라는 것을 기억하자.


  expenses[1] = 89.95;


비슷하게, 다음의 문장은


  expenses[10] = expenses[11];


배열 오소 expenses[11]에 저장된 값을 배열 요소 expenses[10]에 할당한다. 배열의 요소를 지정할 때 배열의 첨자는 이런 예제에서 사용되었듯이 실제 상수{literal constant)이다. 그러나 프로그램에서는 C의 정수형 변수나 수식, 심지어 다른 배열 요소를 첨자로 사용할 수도 있다. 다음은 몇 가지 예이다.


  float expenses[100];

  int a[10];


  /* 그 밖의 프로그램 문장 */


  expenses[i] = 100;    /* i는 정수형 변수이다. */

  expenses[2 = 3] = 100;   /* expenses[5]와 동일하다. */

  expenses[a[2]] = 100;   / * a[]는 정수형 배열이다. */


마지막 줄에 대해서는 약간의 보충 설명이 필요하다. 예를 들어, a[]라는 이름의 정수형 배열이 있고 a[2]에는 8의 값이 저장되어 있다고 하자. 다음 문장은


  expenses[a[2]]


다음과 같은 내용이 될 것이다.


  expenses[8];


배열을 사용할 때에는 배열 요소의 개수를 기억하도록 하자. n개의 요소를 가지는 배열에서 사용할 수 있는 첨자는 0부터 n-1까지이다. 만약 첨자로 n을 사용하면 프로그램에서는 문제가 발생한다. C 컴파일러는 프로그램에서 범위를 벗어난 배열의 첨자가 사용되는지 확인하지 않는다. 프로그램은 정상적으로 컴파일되고 링크되지만, 범위를 벗어난 첨자의 사용은 잘못된 결과를 가져올 것이다. 가끔 프로그램에서는 배열이 1부터 n까지의 요소를 가지는 것처럼 다루기 원할 것이다. 예를 들어, 앞에서 설명한 예제의 경우에는 1월의 지출액을 expenses[1]에, 2월의 지출액을 expenses[2]에 저장하는 것이 더욱 자연스러울 것이다. 이렇게 하는 가장 간단한 방법은 필요한 것보다 하나 더 많은 요소를 가지는 배열을 선언하고, 첫 번째 요소인 0을 무시하는 것이다. 이때 다음과 같이 배열을 선언할 수 있다. 또한 관련 데이터, 예를 들어 연간 지출 합계를 요소 0에 저장할 수 있을 것이다.


  float expenses[13];


<리스트 8.1>에 있는 프로그램 EXPENSES.C는 배열의 사용 예를 보여준다. 이것은 실제로 사용되지 않는 간단한 프로그램이지만 배열의 사용 예를 보여주기에는 저당...


<리스트 8.1> EXPENSES.C는 배열의 사용 예를 보여준다.

 /* EXPENSES.C - 배열의 사용 예 */


 

 #include <stdio.h>


 

 /* 지출액을 저장하는 배열과 카운터 변수 선언 */


 

 float expenses[13];

 int count;


 

 main()

 {

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

    for(count = 1; count < 13; count++)

    {

       printf("Enter expenses for month %d: ", count);

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

    }


 

    /* 배열 내용 출력 */

   for(count = 1; count < 13; count++)

    {

       printf("\nmonth %d = $%.2f", count, expenses[count]);

    }

    return 0;

 }

 

1.2 다차원 배열
: 다차원 배열은 하나 이상의 첨자를 가진다. 이차원 배열(two-dimensional array)은 두 개의 첨자를 가지고 삼차원 배열(three-dimensional array)은 세 개의 첨자를 가진다. C에서 배열의 차원에는 아무런 제한이 없다. 그러나 이 장의 후반부에서 설명하겠지만 전체적인 배열의 크기에는 제한이 있다. 예를 들어, 보드 게임을 수행하는 프로그램을 작성한다고 가정하자. 보드는 8행 8열의 64개 요소를 가지고 격자 형태로 구성된다. 프로그램은 다음과 같이 이차원 배열의 형태로 보드를 정의할 수 있을 것이다.

  int checker[8][8];

결과적으로, 배열은 64개의 요소를 가지게 된다. 각각의 요소는 checker[0][0], checker[0][1] …, checker[7][7]이 된다. 다음은 이 차원 배열의 구조를 나타낸다.

 

 

비슷하게, 3차원 배열은 입체로 생각할 수 있다. 4차원 이상의 배열은 아마도 상상하기 힘들 것이다. 배열은 몇 차원으로 정의하든지 관계없이 메모리에 순서대로 저장된다. 배열이 메모리에 저장되는 방법에 대한 상세한 내용은 낭중에 다루겠다.

 

2. 배열의 이름을 지정하고 선언하는 방법

: 배열의 이름을 지정할 때에는 앞서 공부한 "데이터 저장하기 : 변수와 상수"에서 설명했던 변수 이름에 대한 것과 같은 규칙이 적용된다. 배열의 이름은 독특해야 한다. 즉, 다른 배열이나 어떤 변수, 상수 등에서 사용된 이름을 배열에서 다시 사용할 수 없다. 짐작할 수 있듯이, 배열의 선언은 배열의 이름 다음에 요소의 수를 지정해주는 것을 제외하고는 일반적인 변수를 선언하는 것과 동일한 형식을 따른다. 배열은 선언할 때에는 앞의 예제에서 사용했듯이 실제 상수를 사용하거나 또는 #define 지시어로 생성된 기호 상수를 사용하여 요소의 수를 지정할 수 있다.


  #define MONTHS 12

  int array[MONTHS];


앞의 문장은 다음과 같은 뜻을 가진다.


  int array[12];


그러나 대부분의 컴파일러에서는 const키워드를 통해서 생성된 기호 상수를 사용하여 배열을 선언할 수 없다.


  const int MONTHS = 12;

  int array[MONTHS];  /* 사용할 수 없다 */


<리스트 8.2>의 GRADES.C는 1차원 배열의 사용 예를 보여주는 다른 하나의 프로그램이다. GRADES.C에서는 10개의 점수를 저장하는 배열이 사용된다.

 

 /* GRADES.C - 배열을 사용하는 프로그램 */

 /* 10개의 점수를 읽어들이고 평균을 구한다. */


 

 #include <stdio.h>


 

 #define MAX_GRADE 100

 #define STUDENTS 10


 

 int grades[STUDENTS];


 

 int idx;

 int total = 0;   /* 평균을 저장한다. */


 

 main(0

 {

    for(idx = 0; idx < STUDENTS; idx++)

    {

       printf("Enter Person %d's grade: ", idx + 1);

       scanf("%d", &grades[idx]);


 

       while(grades[idx] > MAX_GRADE)

       {

          printf("\nThe highest grade possible is %d", MAX_GRADE);

          printf("\nEnter correct grade: ");

          scanf("%d", &grades[idx]);

       }

       total += grades[idx];

    }

    printf("\n\nThe average score is %d\n", (total / STUDENTS));


 

    return (0);

 }

 

2.1 배열의 초기화
: 배열을 처음 선언할 때에는 전체나 일부분을 초기화할 수 있다. 배열의 선언에서 등호와 함께 중괄호 내에 포함되며 쉼표로 구분되는 초기화 값의 목록을 입력하면 된다. 입력되는 값은 첫 번째 요소(0)부터 시작하여 배열의 각 요소에 할당한다. 예를 들어, 다음 문장은 array[0]에 100, array[1]에 200, array[2]에 300, array[3]에 400의 값을 저장한다.

  int array[4] = {100, 200, 300, 400};

배열의 크기를 지정하지 않으면 컴파일러는 단지 초기화 값을 저장하기에 충분히 큰 배열을 생성한다. 그래서 다음 문장은 이전의 배열 선언문과 동일한 결과를 나타낼 것이다.

  int array[] = {100, 200, 300, 400};

그러나 배열을 선언할 때에는 다음 문장에서처럼 필요한 초기값만을 입력할 수도 있다.

  int array[10] = {1, 2, 3}

배열의 요소를 분명하게 초기화하지 않는다면 프로그램이 실행될 때 배열에 어떤 값이 저장되어 있는지 알 수 없을 것이다. 또한, 배열의 요소의 수보다 많은 초기값을 입력하면 컴파일러는 에러를 발생할 것이다.

2.2 다차원 배열의 초기화
: 다차원 배열도 초기화할 수 있다. 주어진 초기값은 뒷 부분의 첨자가 먼저 증가하는 순서로 배열의 요소에 할당된다. 예를 들어, 다음 문장은

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

다음과 같은 뜻을 가진다.

  array[0][0] = 1
  array[0][1] = 2
   …
  array[3][1] = 11
  array[3][2] = 12

다차원 배열을 초기화할 때에는 초기값을 구분하기 위해서 추가적인 중괄호를 사용하고 여러 줄에 걸쳐서 표현하여 소스 코드를 더욱 이해하기 쉽게 만들 수 있다. 다음의 초기화 문장은 앞에서 사용한 것과 동일하다.

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

초기값이 쉼표에 의해 구분되어야 한다는 것을 기억하자. 초기값 사이에 중괄호가 있을 때에도 쉼표로 구분해야 하는 것은 당연하다. 또한, 모든 중괄호는 쌍으로 사용해야 하고, 그렇지 않은 경우에는 컴파일러가 에러 메시지를 출력한다.

이제 배열의 장점을 보여주는 예제를 살펴보도록 하자. <리스트 8.3>에 있는 프로그램 RANDOM.C는 1,000개의 요소를 가지는 3차원 배열을 생성하고 무작위로 값을 저장한다. 그리고 나서 프로그램은 하면 상에 배열의 요소를 출력한다. 배열이 아닌 일반적인 변수를 사용할 경우에는 이런 작업을 수행하기 위해서 얼마나 많은 변수가 필요할지 상상해보기 바란다. 이 프로그램에서는 새로운 라이브러리 함수 getch()가 사용된다. getch() 함수는 키보드에서 하나의 문자를 읽어들인다. <리스트 8.3>에서 getch()는 하나의 키가 눌러질 때까지 프로그램의 실행을 정지시킨다. getch()는 낭중에 더욱 상세히 설명하겠음....

<리스트 8.3> RANCOM.C는 다차원 배열을 생성한다

 

.

 /* RANDOM.C - 다차원 배열의 사용 예 */


 #include <stdio.h>

 #include <stdlib.h>

 /* 1,000개의 요소를 가지는 3차원 배열 선언 */


 int random_array[10][10][10];

 int a, b, c;


 main()

 {

    /* 배열을 임의의 수로 채운다. */

    /* C 라이브러리 함수 rand()는 임의의 수를 돌려준다. */

    /* 각 배열 첨자에 대해 하나의 for문을 사용한다. */

    for(a = 0; a < 10; a++)

    {

       for(b = 0; b < 10; b++)

       {

          for(c = 0; c < 10; c++)

          {

             random_array[a][b][c] = rand();

          }

       }

    }


    /* 이제 한번에 배열 요소 10개씩 출력한다. */


    for(a = 0; a < 10; a++)

    {

       for(b = 0; b < 10; b++)

       {

          for(c = 0; c < 10; c++)

          {

             printf("\nrandom_array[%d][%d][%d] = ", a, b, c);

             printf(l"%d", random_array[a][b][c]);

          }

          printf("\nPress Enter to continue, CTRL-C to quit.");


          getchar();

       }

    }

    return 0;

      /* main()의 곱 */

 }

 

2.3 배열의 최대 크기
: 바이트 단위로 표현되는 배열의 크기는 각 요소의 크기뿐 아니라 배열 요소의 개수에 의해서 결정된다. 각 요소의 크기는 배열의 데이터형과 컴퓨터의 환경에 따라 결정된다. <표 8.1>에는 <표 3.2>에서 설명했던 각 숫자 데이터형의 크기가 편의를 돕기 위해서 다시 나타나 있다. 대부분의 PC에서 사용되는 데이터형의 크기이다.

<표 8.1> 대부분의 PC에서 사용되는 숫자 데이터형의 저장 공간 요구량.

 

배열에서 필요한 저장 영역을 계산하기 위해서는 요소의 크기에 배열 내의 요소의 개수를 곱하면 된다. 예를 들어, 500개의 요소를 가지는 float형 배열은 500 * 4를 계산하여 2,000바이트의 저장 영역을 요구한다는 사실을 알 수 있다.


C의 sizeof() 연산자를 사용하면 프로그램 내에서 저장 영역의 크기를 계산할 수 있다. sizeof()는 함수가 아니라 단항 연산자인데, 변수의 이름이나 데이터형을 인수로 받아들이고 인수로 사용된 변수나 데이터형의 크기를 바이트단위로 계산하여 돌려준다. sizeof()의 사용 예는 <리스트 8.4>에 나타나 있다.


<리스트 8.4> 배열에서 필요로 하는 저장 영역의 양을 계산하기 위해 sizeof() 연산자를 사용하는 프로그램.

 /* sizeof() 연산자의 사용 예 */


 

 #include <stdio.h>


 

 /* 100개의 요소를 가지는 여러 개의 배열 선언 */


 

 int intarray[100];

 float floatarray[100];

 double doublearray[100];


 

 main{}

 {

    /* 숫자 데이터형의 크기 출력 */


 

    printf("\n\nSize of int = %d bytes", sizeof(int));

    printf("\nSize of short = %d bytes", sizeof(short));

    printf("\nSize of long = %d bytes", sizeof(long));

    printf("\nSize of float = %d bytes", sizeof(float));

    printf("\nSize of double = %d bytes", sizeof(double));


 

    /* 세 배열의 크기 출력 */


 

    printf("\nSize of intarray = %d bytes", sizeof(intarray));

    printf("\nSize of floatarray = %d bytes", sizeof(floatarray));

    printf("\nSize of doublearray = %d bytes", sizeof(doublearray));


 

    return 0;

 }

 

 

'pc관련 > C언어' 카테고리의 다른 글

C언어_문자,문자열  (0) 2019.03.24
C언어_포인터에 대하여...  (0) 2019.03.24
입출력의 기초  (0) 2019.03.20
숫자배열 사용하기  (0) 2019.03.20
C언어-프로그램제어문  (0) 2019.03.17
Posted by 둥이파파^^
pc관련/C언어2019. 3. 20. 21:51

대부분의 프로그램에서는 화면 상에 자료를 출력하거나 키보드에서 자료를 읽어들일 필요가 있다. 앞에서 사용했던 많은 프로그램은 이렇게 출력과 입력을 수행했지만, 상세한 내용에 대해서는 다루지 않았다. 오늘은 다음과 같은 내용을 알아보도록 하자.

·C의 입출력문의 기초

·라이브러리 함수 printf()와 puts()를 사용하여 화면상에 자료를 출력하는 방법

·화면 상에 출력되는 자료를 형식화하는 방법

·라이브러리 함수 scanf()를 사용하여 키보드에서 데이터를 읽어들이는 방법


이 장에서는 이런 내용을 아주 자세히 설명하지는 않겠지만, 프로그램을 작성하기 위해서 필요한 내용을 충분히 다룰 것이다. 입출력에 대한 상세한 내용은 책의 후반부에서 다루어질 것이다.


1. 화면상에 자료를 출력하는 방법

대부분의 프로그램에서는 화면 상에 자료를 출력할 필요가 있을 것이다. 이렇게 자료를 출력하는 가장 일반적인 두 가지 방법은 C의 라이브러리 함수인 printf()와 puts()를 사용하는 것이다.


1.1 printf() 함수

: 표준 C 라이브러리의 일부분인 printf() 함수는 프로그램에서 화면 상에 데이터를 출력하기 위해서 사용되는 아마도 가장 보편적인 함수일 것이다. 이미 많은 예제에서 printf()를 사용했었다. 여기에서는 printf()의 사용방법을 구체적으로 알아보도록 하자.

화면 상에 텍스트 메시지를 출력하는 것은 아주 간단하다. 큰 따옴표 내에 출력하기 원하는 메시지를 포함시켜 printf()함수를 호출하면 된다. 예를 들어, An error has occurred!라는 내용을 화면 상에 출력하기 위해서는 다음과 같은 문장을 사용한다.


   printf("An error has occurred!");


그러나 텍스트 메시지 외에도 프로그램의 변수의 값을 출력할 필요가 있을 것이다. 변수의 값을 출력하는 것은 메시지만을 출력하는 것보다 약간 더 복잡하다. 예를 들어, 약간의 메시지와 함께 숫자 변수 x의 값을 화면 상에 출력하기 원한다고 가정하자. 또한, 출력할 내용을 새로운 줄에서 시작하기 원한다면 다음과 같은 printf() 함수를 사용하여 원하는 결과를 얻을 수 있다.


   printf("\nThe value of x is %d", x);


x의 값이 12라고 가정할 때 결과는 다음과 같다.


   The value of x is 12


이 예제에서는 printf()에 두 개의 인수가 전달된다. 첫 번째 인수는 큰 따옴표 내에 포함될 것으로 형식화 문자열(format string)이라고 한다. 두 번째 인수는 출력되는 값을 가지는 변수의 이름(x)이다.


1.2 printf()의 형식화 문자열

: printf()의 형식화 문자열은 주어진 자료가 출력되는 상태를 지정한다. 형식화 문자열에는 다음과 같은 세 가지 구성 요소가 포함될 수 있다.


·일반적인 텍스트는 형식화 문자열에 입력되어 있는 그대로 출력된다. 앞의 예제에서는 'The'의 영문자 T로 시작하고 %를 포함하지 않는 내용이 일반적인 텍스트 문자열에 해당 된다.

·이스케이프 시퀀스(escape sequence, 특수 코드)는 특수한 형식화를 제어하기 위해서 사용된다. 이스케이프 시퀀스는 백슬래시(\)와 하나의 문자로 구성된다. 앞의 예제에서는 \n이 이스케이프 시퀀스이다. 이것은 문자 진행(newline) 문자로, '다음 줄의 첫 칸으로 이동하라'는 것을 뜻한다. 이스케이프 시퀀스는 또한 특수한 문자를 출력하는 데에도 사용 된다. 여러 가지 이스케이프 시퀀스가 <표 7.1>에 나타나 있다.

·변환 문자(conversion specifier)는 백분율 기호(%)와 하나의 문자로 구성된다. 앞의 예제에서는 %d가 변한 문자이다. 변환 문자는 printf()에게 변수의 출력 방법을 알려준다. %d는 printf()가 변수 x의 값을 부호 있는 십진 정수형으로 변환하여 출력하도록 지시한다.


<표 7.1. 자주 사용되는 Escape Sequence>

 

▶ printf()의 escape sequence
이제, 형식화 문자열의 구성 요소를 더욱 상세히 살펴보도록 하자. escape sequence는 화면의 커서를 이동시켜 자료의 출력 위치를 제어하는 데 사용된다. 또한, printf()에서 특별한 의미를 가지고 있는 문자를 출력하는 데 사용된다. 예를 들어, 하나의 백슬레시 문자를 출력하기 위해서는 형식화 문자열에서 두 개의 백슬래시(\\)를 사용해야 한다. 첫 번째 백슬래시는 두 번째 백슬래시가 escape sequence의 시작이 아니라 문자 그대로 출력되어야 한다는 것을 알려준다. 일반적으로 백슬래시는 printf()에서 그 다음에 있는 문자를 특별한 방법으로 처리하는 특수 문자이기 때문이다. 다음은 몇 가지 예이다.

 

 

<표 7.1>에는 C에서 가장 많이 사용되는 escape sequence가 나열되어 있다.
<리스트 7.1>은 자주 사용되는 몇 가지 escape sequence의 사용 예를 보여준다. 

 

/* 자주 사용되는 escape sequence의 사용 예 */

 #include <stdio.h>

 #define QUIT 3


 

 int get_menu_choice(void);

 void printf_report(void);


 

 main()

 {

    int choice = 0;

    while(choice != QUIT)

    {

       choice = get_menu_choice();

       if(choice == 1)

          printf("\nBeeping the computer\a\a\a");

       else

       {

          if(choice == 2)

             print_report();

       }

    }

    printf("You chose to quit!\n");

    return 0;

 }


 

 int get_menu_choice(void)

 {

    int selection = 0;

    do

    {

       printf("\n");

       printf("\n1 - Beep Computer");

       printf("\n2 - Display Report");

       printf("\n3 - Quit");

       printf("\n");

       printf("\nEnter a selection: ");


 

       scanf("%d", &selection);

    }while(selection < 1 || selection > 3);

    return selection;

 }


 

 void print_report(void)

 {

    printf("\nSAMPLE REPORT");

    printf("\n\nSequence\nMeaning");

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

    printf("\n\\a\t\tbell (alert)");

    printf("\n\\\t\tbackspace");

    printf("\n...\t\t...");

 }

 

 

▶ printf()의 변환 문자
형식화 문자열에는 각각의 변수에 대응하는 변환 분자가 포함되어야 한다. printf()는 대응하는 변환 문자가 지시하는 대로 각각의 변수를 출력하게 된다. 출력되는 변수의 형태에 대응하는 변환 문자가 반드시 사용되어야 한다는 것을 기억하자. 동일한 형태가 사용되어야 한다는 것은 어떤 뜻일까? 만약 부호가 있는 십진 정수형 (signed decimal integer)인 int나 long형 변수를 출력한다면 변환 문자 %d를 사용하자. 부호 없는 십진 정수형(unsigned decimal integer)인 unsigned int와 unsigned long형을 출력한다면 %u를 사용하자. 부동 소수형 변수(floating point variable)인 float와 double형에 서는 변환 문자 %f를 사용하자. <표 7.2>에는 가장 많이 사용되는 변환 문자가 나타나 있다.

<표 7.2> 가장 많이 사용되는 변환 문자

 

형식화 문자열에서 일반적인 텍스트는 escape sequence나 변환 문자가 아닌 내용을 말한다. 이런 일반적인 텍스트는 모든 공백을 포함하여 입력되어 있는 그대로 출력된다. 하나 이상의 변수의 값을 출력하는 경우에는 어떻게 해야 할까? printf()문에서 출력할 수 있는 변수의 수에는 제한이 없지만, 형식화 문자열에는 각각의 변수에 대응하는 변환 문자를 사용해야 한다. 변환 문자는 왼쪽에서 오른쪽으로 변수와 쌍을 이루게 된다. 그래서 다음과 같은 문자에서는


  printf("Rate = %f, amount = %d, rate, amount);


변수 rate가 변환 문자 %f와 대응하고, 변수 amount는 변환 문자 %d와 쌍을 이루게 된다. 형식화 문자열에서 변환 문자의 위치는 출력되는 값을 실제 위치를 결정한다. 만약 변환 문자보다 더 많은 수의 변수가 printf()에서 사용되면, 변환 문자를 가지지 않는 변수는 출력되지 않는다. 변수의 개수보다 많은 변환문자가 사용되면 남는 변환 문자의 위치에는 '알 수 없는 값'이 출력된다. printf()에서는 변수의 값 외에도 다른 것을 출력할 수 있다. printf()의 인수로는 C의 모든 수식을 사용할 수 있다. 예를 들어, x와 y의 합을 출력하기 위해서는 다음을 사용할 것이다.


  z = x + y;

  printf("%d", z);


또한, 다음과 같은 문장도 사용할 수 있다.


  printf("%d", x + y);


printf()를 사용하는 모든 프로그램은 헤더 파일 STDIO.H를 포함해야 한다. <리스트 7.2>는 printf()의 사용 예를 보여준다.


<리스트 7.2> 숫자값을 출력하기 위한 printf()사용하기 

 

 /* 숫자값을 출력하기 위한 printf()의 사용 예 */


 

 #include <stdio.h>


 

 int a = 2, b = 10, c = 50;

 float f = (float0 1.05, g = (float0 25.5, h = (float) -0.1;


 

 main()

 {

    printf("\nDecimal values without tabs: %d %d %d", a, b, c0;

    printf("\nDecimal values with tabs: \t%d \t%d \t%d", a, b, c);


 

    printf("\nThree floats on 1 line: \t%f\t%f\t%f", f, g, h);

    printf("\nThree floats on 3 lline: \nn\t%f\n\t%f\n\t%f", f, g, h0;


 

    printf("\nThe rate is %f%%", f);

    printf9"\nThe result of %f/%f = %f\n", g, f, g / f);


 

    return 0;

 }

 

 

1.3 puts()를 사용한 메시지 출력
: puts() 함수는 화면 상에 텍스트 메시지를 출력하는 데 사용될 수 있지만, 숫자 변수를 출력할 수 없다. puts()는 하나의 문자열을 인수로 받아들이고 자동으로 마지막에 문자 진행 (newline)을 추가하여 문자열을 출력한다. 예를 들어, 다음 문장은

  puts("Hello, world.");

다음과 같은 뜻을 가지고 있다.

  printf("Hello, world.\n);

puts()에 전달되는 문자열에는 \n을 포함하여 escape sequence를 사용할 수 있다. 여기서 사용되는 escape sequence는 printf()에서와 동일한 효과를 나타낸다. <표 7.1>을 참고 하도록 하자. puts()를 사용하는 프로그램은 헤더 파일 STDIO.H를 포함해야 한다. STDIO.H는 프로그램 내에서 단지 한 번만 포함되어야 한다는 것에 주의하자.

2. scanf()를 사용한 숫자 데이터의 입력
: 대부분의 프로그램에서 화면 사에 데이터를 출력할 필요가 있는 것과 마찬가지고, 프로그램은 키보드에서 데이터를 받아들일 필요가 있다. 프로그램이 키보드에서 데이터를 받아들이는 가장 좋은 방법은 라이브러리 함수 scanf()를 사용하는 것이다. scanf() 함수는 지정된 형식에 따라 키보드에서 데이터를 읽어들여서 하나 이상의 프로그램 변수에 할당한다. printf()와 마찬가지로, scanf()는 데이터의 입력 형식을 지정하는 형식화 문자열을 사용한다. 형식화 문자열은 printf()함수에서와 동일한 변환 문자를 상용한다. 예를 들어, 다음의 문장은

  scanf("%d", &x);

키보드에서 십진 정수형 값을 읽어들이고 정수형 변수 x에 할당한다. 비슷하게, 다음의 문장은 키보드에서 부동 소수형 값을 읽어들이고 변수 rate에 할당한다.

  scanf("%f", &rate);

변수의 이름 앞에 있는 기호(&)는 어떤 역할을 하는 것일까? 이 기호는 C의 주소 연산자 (address of operator)이다. 여기에서는 일단 scanf()의 인수로 사용되는 변수의 이름 앞에 이 기호를 사용할 필요가 있다는 것을 알아두자.

형식화 문자열 내에 여러 개의 변환 문자를 포함시키고 여러 개의 변수 이름을 사용하면 하나의 scanf()에서 하나 이상의 값을 입력할 수 있다. 인수의 목록에서 각각의 변수에는 & 부호를 사용해야 한다. 다음 문장은 정수형 값과 부동 소수형 값을 읽어들이고 변수 x와 rate에 각각 할당한다.

  scanf("%d %f", &x, &rate);

여러 개의 값이 입력될 때 scanf()는 입력되는 값을 구분하기 위해서 공백을 사용한다. 공백은 빈칸(space), 탭(tab), 문자 진행(newline)이 될 수 있다. scanf()의 형식화 문자열에 있는 각각의 변한 문자는 입력되는 내용에 대응한다. 각각의 이력 내용은 공백에 의해 구분된다. 이런 특징은 융통성을 제공한다. 앞에서 예제로 사용된 scanf()에 대해서 다음과 같은 내용을 입력할 수 있다.

  10 12.45

또한, 다음과 같은 내용을 입력할 수도 있을 것이다.

  10      12.45

다음과 같은 입력 내용도 받아들여질 것이다.

  10
  12.45

두 개의 입력 값 사이에 공백이 존재하는 한 scanf()는 각각의 값을 지정된 변수에 할당할 수 있을 것이다. 이 장의 앞 부분에서 설명한 다른 함수와 마찬가지로, scanf()를 사용하는 프로그램에는 STDIO.H 헤더 파일이 포함되어야 한다. <리스트 7.3>에는 scanf()의 사용 예가 있다.

<리스트 7.3> 숫자값을 얻기 위한 scanf()의 사용 

 

 /* scanf() 의 사용 예 */

 #include (stdio.h>


 #define QUIT 4


 int get_menu_choice(void);


 main()

 {

    int choice = 0;

    int  int_var = 0;

    float  float_var = 0.0;

    unsigned  unsigned_var = 0;


    while(choice != QUIT)

    {

       choice = get_menu_choice();


       if(choice == 1)

       {

          put("\nEnter a signed decimal integer (i.e. -123)");

          scanf("%d", &int_var);

       }

       if(choice == 2)

       {

          puts("\nEnter a decimal floating-point number (i.e. 1.23)");

          scanf("%f", &float_var);

       }

       if(choice == 3)

       {

          puts("\nEnter an unsigned decimal integer (i.e. 123)");

          scanf("%u", &unsigned_var);

       }

    }

    printf("\nYour values are: int: %d float: %f unsigned: %u\n",

               int_var, float_var, unsigned_var);


    return 0;

 }


 int get_menu_choice(void)

 {

    int selection = 0;


    do

    {

       puts("\n1 - Get a signed decimal integer");

       puts("2 - Get a decimal floating-point number");

       puts("3 - Get an unsigned decimal integer");

       puts("4 - Quit");

       puts("\nEnter a selection: ");


       scanf("%d", &selection);

    }while(selection < 1 || selection > 4);

   

    return selection;

 }

 

 

-> 입력 / 출력

1 - Get a signed decimal integer
2 - Get a decimal floating point number
3 - Get an unsigned decimal integer
4 - Quit
   Enter a selection:
   1
   Enter a signed decimal integer (i.e. -123)
   -123 
 

 

'pc관련 > C언어' 카테고리의 다른 글

C언어_포인터에 대하여...  (0) 2019.03.24
C언어_숫자배열사용  (0) 2019.03.24
숫자배열 사용하기  (0) 2019.03.20
C언어-프로그램제어문  (0) 2019.03.17
함수의 기본  (0) 2019.03.16
Posted by 둥이파파^^
pc관련/C언어2019. 3. 20. 21:36

배열은 C 프로그램 내에서 가끔 사용되는 데이터 저장 영역의 하나이아. 오늘은 다음과 같은 내용을 다룰 것이다.


·배열은 무엇인가?

·일차원 배열과 다차원 배열의 정의

·배열의 선언과 초기화


1. 배열이란 무엇인가?

: 배열(array)은 동일한 데이터형과 동일한 이름을 가지는 집단적인 데이터 저장 영역을 말한다. 배열의 각 저장 영역을 배열 요소(array element)라고 한다. 프로그램에서는 왜 배열을 사용해야 하는 것일까? 이것은 예제를 통해서 설명하도록 하겠다. 만약 1998년에 월별로 접수된 영수증을 사용하여 재정 상태를 관리한다면, 12개 서류철로 나누어 관리할 수도 있겠지만 12개의 부분으로 나누어진 하나의 서류철에서 관리하는 것이 더 나을 것이다.


이 예제를 컴퓨터 프로그래밍에 적용해보자. 재정 관리를 위한 프로그램을 작성한다고 가정하자. 프로그램은 12개의 독립된 변수와 합계를 저장하는 변수를 선언하여 월별 지출액을 관리할 수 있을 것이다. 이것은 월별 영수증을 보관하기 위한 12개의 독립된 서류철을 사용하는 것과 유사하다. 그러나 효율적인 프로그램이라면 월별 지출액을 저장하는 12개의 요소와 합계를 저장하기 위한 하나의 요소를 가지는 배열을 사용할 것이다. 이것은 하나의 서류철에서 월별 영수증을 모두 관리하는 것과 유사하다.


1.1 일차원 배열

: 일차원 배열(single-dimensional array)은 하나의 첨자만을 사용하는 배열이다. 첨자(subscript)는 배열의 이름 위에 있는 대괄호에서 사용되는 숫자이다. 이것은 개벽적인 배열 요소의 번호를 나타낸다. 재정 관리 프로그램에서 사용하기 위한 float형의 배열을 선언하기 위해서는 다음과 같은 문장을 작성할 수 있을 것이다.


  float expenses[12];


배열은 expenses라는 이름을 사용하고, 12개의 요소를 가진다. 12개의 각 요소는 개별적인 부동 소수형 변수와 동일하다. 배열에는 C의 모든 데이터형을 사용할 수 있다. C의 배열 요소는 항상 0에서부터 시작하므로 12개의 요소는 0부터 11까지의 번호를 사용할 것이다. 1월의 지출액은 expenses[0]에 저장되고, 2월의 지출액은 expenses[1]에 저장된다.


배열을 선언할 때 컴파일러는 전체 배열을 저장하기에 충분한 메모리 영역을 보존한다. 소스코드에서 배열이 선언되는 위치는 중요하다. 다른 변수에서와 마찬가지로, 배열이 선언되는 위치는 프로그램에서 배열을 사용하는 방법에 영향을 준다. 변수나 배열이 선언되는 위치가 프로그램에 미치는 영향은 낭중에 상세히 설명하고, 일단 main() 함수가 시작되기 전에 다른 변수 선언문과 함께 배열 선언문을 위치시키도록 하자.  배열의 요소는 배열이 아닌 동일한 형태의 변수를 사용할 수 있는 곳이면 프로그램 내에서 어디든지 사용할 수 있다. 배열의 각 요소는 배열의 이름과 대괄호 내에 포함된 요소의 첨자에 의해서 사용된다. 예를 들어, 다음 문장은 두 번째  배열 요소에 89.95의 값을 저장한다. 배열의 처 번째 요소는 expenses[1]이 아니라 expenses[0]이라는 것을 기억하자.


  expenses[1] = 89.95;


비슷하게, 다음의 문장은


  expenses[10] = expenses[11];


배열 오소 expenses[11]에 저장된 값을 배열 요소 expenses[10]에 할당한다. 배열의 요소를 지정할 때 배열의 첨자는 이런 예제에서 사용되었듯이 실제 상수{literal constant)이다. 그러나 프로그램에서는 C의 정수형 변수나 수식, 심지어 다른 배열 요소를 첨자로 사용할 수도 있다. 다음은 몇 가지 예이다.


  float expenses[100];

  int a[10];


  /* 그 밖의 프로그램 문장 */


  expenses[i] = 100;    /* i는 정수형 변수이다. */

  expenses[2 = 3] = 100;   /* expenses[5]와 동일하다. */

  expenses[a[2]] = 100;   / * a[]는 정수형 배열이다. */


마지막 줄에 대해서는 약간의 보충 설명이 필요하다. 예를 들어, a[]라는 이름의 정수형 배열이 있고 a[2]에는 8의 값이 저장되어 있다고 하자. 다음 문장은


  expenses[a[2]]


다음과 같은 내용이 될 것이다.


  expenses[8];


배열을 사용할 때에는 배열 요소의 개수를 기억하도록 하자. n개의 요소를 가지는 배열에서 사용할 수 있는 첨자는 0부터 n-1까지이다. 만약 첨자로 n을 사용하면 프로그램에서는 문제가 발생한다. C 컴파일러는 프로그램에서 범위를 벗어난 배열의 첨자가 사용되는지 확인하지 않는다. 프로그램은 정상적으로 컴파일되고 링크되지만, 범위를 벗어난 첨자의 사용은 잘못된 결과를 가져올 것이다. 가끔 프로그램에서는 배열이 1부터 n까지의 요소를 가지는 것처럼 다루기 원할 것이다. 예를 들어, 앞에서 설명한 예제의 경우에는 1월의 지출액을 expenses[1]에, 2월의 지출액을 expenses[2]에 저장하는 것이 더욱 자연스러울 것이다. 이렇게 하는 가장 간단한 방법은 필요한 것보다 하나 더 많은 요소를 가지는 배열을 선언하고, 첫 번째 요소인 0을 무시하는 것이다. 이때 다음과 같이 배열을 선언할 수 있다. 또한 관련 데이터, 예를 들어 연간 지출 합계를 요소 0에 저장할 수 있을 것이다.


  float expenses[13];


<리스트 8.1>에 있는 프로그램 EXPENSES.C는 배열의 사용 예를 보여준다. 이것은 실제로 사용되지 않는 간단한 프로그램이지만 배열의 사용 예를 보여주기에는 저당...


<리스트 8.1> EXPENSES.C는 배열의 사용 예를 보여준다.

 

1.2 다차원 배열
: 다차원 배열은 하나 이상의 첨자를 가진다. 이차원 배열(two-dimensional array)은 두 개의 첨자를 가지고 삼차원 배열(three-dimensional array)은 세 개의 첨자를 가진다. C에서 배열의 차원에는 아무런 제한이 없다. 그러나 이 장의 후반부에서 설명하겠지만 전체적인 배열의 크기에는 제한이 있다. 예를 들어, 보드 게임을 수행하는 프로그램을 작성한다고 가정하자. 보드는 8행 8열의 64개 요소를 가지고 격자 형태로 구성된다. 프로그램은 다음과 같이 이차원 배열의 형태로 보드를 정의할 수 있을 것이다.

  int checker[8][8];

결과적으로, 배열은 64개의 요소를 가지게 된다. 각각의 요소는 checker[0][0], checker[0][1] …, checker[7][7]이 된다. 다음은 이 차원 배열의 구조를 나타낸다

 

비슷하게, 3차원 배열은 입체로 생각할 수 있다. 4차원 이상의 배열은 아마도 상상하기 힘들 것이다. 배열은 몇 차원으로 정의하든지 관계없이 메모리에 순서대로 저장된다. 배열이 메모리에 저장되는 방법에 대한 상세한 내용은 낭중에 다루겠다.

 

2. 배열의 이름을 지정하고 선언하는 방법

: 배열의 이름을 지정할 때에는 앞서 공부한 "데이터 저장하기 : 변수와 상수"에서 설명했던 변수 이름에 대한 것과 같은 규칙이 적용된다. 배열의 이름은 독특해야 한다. 즉, 다른 배열이나 어떤 변수, 상수 등에서 사용된 이름을 배열에서 다시 사용할 수 없다. 짐작할 수 있듯이, 배열의 선언은 배열의 이름 다음에 요소의 수를 지정해주는 것을 제외하고는 일반적인 변수를 선언하는 것과 동일한 형식을 따른다. 배열은 선언할 때에는 앞의 예제에서 사용했듯이 실제 상수를 사용하거나 또는 #define 지시어로 생성된 기호 상수를 사용하여 요소의 수를 지정할 수 있다.


  #define MONTHS 12

  int array[MONTHS];


앞의 문장은 다음과 같은 뜻을 가진다.


  int array[12];


그러나 대부분의 컴파일러에서는 const키워드를 통해서 생성된 기호 상수를 사용하여 배열을 선언할 수 없다.


  const int MONTHS = 12;

  int array[MONTHS];  /* 사용할 수 없다 */


<리스트 8.2>의 GRADES.C는 1차원 배열의 사용 예를 보여주는 다른 하나의 프로그램이다. GRADES.C에서는 10개의 점수를 저장하는 배열이 사용된다.

 

2.1 배열의 초기화
: 배열을 처음 선언할 때에는 전체나 일부분을 초기화할 수 있다. 배열의 선언에서 등호와 함께 중괄호 내에 포함되며 쉼표로 구분되는 초기화 값의 목록을 입력하면 된다. 입력되는 값은 첫 번째 요소(0)부터 시작하여 배열의 각 요소에 할당한다. 예를 들어, 다음 문장은 array[0]에 100, array[1]에 200, array[2]에 300, array[3]에 400의 값을 저장한다.

  int array[4] = {100, 200, 300, 400};

배열의 크기를 지정하지 않으면 컴파일러는 단지 초기화 값을 저장하기에 충분히 큰 배열을 생성한다. 그래서 다음 문장은 이전의 배열 선언문과 동일한 결과를 나타낼 것이다.

  int array[] = {100, 200, 300, 400};

그러나 배열을 선언할 때에는 다음 문장에서처럼 필요한 초기값만을 입력할 수도 있다.

  int array[10] = {1, 2, 3}

배열의 요소를 분명하게 초기화하지 않는다면 프로그램이 실행될 때 배열에 어떤 값이 저장되어 있는지 알 수 없을 것이다. 또한, 배열의 요소의 수보다 많은 초기값을 입력하면 컴파일러는 에러를 발생할 것이다.

2.2 다차원 배열의 초기화
: 다차원 배열도 초기화할 수 있다. 주어진 초기값은 뒷 부분의 첨자가 먼저 증가하는 순서로 배열의 요소에 할당된다. 예를 들어, 다음 문장은

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

다음과 같은 뜻을 가진다.

  array[0][0] = 1
  array[0][1] = 2
   …
  array[3][1] = 11
  array[3][2] = 12

다차원 배열을 초기화할 때에는 초기값을 구분하기 위해서 추가적인 중괄호를 사용하고 여러 줄에 걸쳐서 표현하여 소스 코드를 더욱 이해하기 쉽게 만들 수 있다. 다음의 초기화 문장은 앞에서 사용한 것과 동일하다.

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

초기값이 쉼표에 의해 구분되어야 한다는 것을 기억하자. 초기값 사이에 중괄호가 있을 때에도 쉼표로 구분해야 하는 것은 당연하다. 또한, 모든 중괄호는 쌍으로 사용해야 하고, 그렇지 않은 경우에는 컴파일러가 에러 메시지를 출력한다.

이제 배열의 장점을 보여주는 예제를 살펴보도록 하자. <리스트 8.3>에 있는 프로그램 RANDOM.C는 1,000개의 요소를 가지는 3차원 배열을 생성하고 무작위로 값을 저장한다. 그리고 나서 프로그램은 하면 상에 배열의 요소를 출력한다. 배열이 아닌 일반적인 변수를 사용할 경우에는 이런 작업을 수행하기 위해서 얼마나 많은 변수가 필요할지 상상해보기 바란다. 이 프로그램에서는 새로운 라이브러리 함수 getch()가 사용된다. getch() 함수는 키보드에서 하나의 문자를 읽어들인다. <리스트 8.3>에서 getch()는 하나의 키가 눌러질 때까지 프로그램의 실행을 정지시킨다. getch()는 낭중에 더욱 상세히 설명하겠음....

<리스트 8.3> RANCOM.C는 다차원 배열을 생성한다..

 

2.3 배열의 최대 크기
: 바이트 단위로 표현되는 배열의 크기는 각 요소의 크기뿐 아니라 배열 요소의 개수에 의해서 결정된다. 각 요소의 크기는 배열의 데이터형과 컴퓨터의 환경에 따라 결정된다. <표 8.1>에는 <표 3.2>에서 설명했던 각 숫자 데이터형의 크기가 편의를 돕기 위해서 다시 나타나 있다. 대부분의 PC에서 사용되는 데이터형의 크기이다.

<표 8.1> 대부분의 PC에서 사용되는 숫자 데이터형의 저장 공간 요구량.

 

배열에서 필요한 저장 영역을 계산하기 위해서는 요소의 크기에 배열 내의 요소의 개수를 곱하면 된다. 예를 들어, 500개의 요소를 가지는 float형 배열은 500 * 4를 계산하여 2,000바이트의 저장 영역을 요구한다는 사실을 알 수 있다.


C의 sizeof() 연산자를 사용하면 프로그램 내에서 저장 영역의 크기를 계산할 수 있다. sizeof()는 함수가 아니라 단항 연산자인데, 변수의 이름이나 데이터형을 인수로 받아들이고 인수로 사용된 변수나 데이터형의 크기를 바이트단위로 계산하여 돌려준다. sizeof()의 사용 예는 <리스트 8.4>에 나타나 있다.


<리스트 8.4> 배열에서 필요로 하는 저장 영역의 양을 계산하기 위해 sizeof() 연산자를 사용하는 프로그램.

 

 /* sizeof() 연산자의 사용 예 */

 

 #include <stdio.h>

 

/* 100개의 요소를 가지는 여러 개의 배열 선언 */

 

 int intarray[100];

 float floatarray[100];

 double doublearray[100];

 

 main{}

 {

    /* 숫자 데이터형의 크기 출력 */

 

    printf("\n\nSize of int = %d bytes", sizeof(int));

    printf("\nSize of short = %d bytes", sizeof(short));

    printf("\nSize of long = %d bytes", sizeof(long));

    printf("\nSize of float = %d bytes", sizeof(float));

    printf("\nSize of double = %d bytes", sizeof(double));

 

    /* 세 배열의 크기 출력 */

 

    printf("\nSize of intarray = %d bytes", sizeof(intarray));

    printf("\nSize of floatarray = %d bytes", sizeof(floatarray));

    printf("\nSize of doublearray = %d bytes", sizeof(doublearray));

 

    return 0;

 }

'pc관련 > C언어' 카테고리의 다른 글

C언어_숫자배열사용  (0) 2019.03.24
입출력의 기초  (0) 2019.03.20
C언어-프로그램제어문  (0) 2019.03.17
함수의 기본  (0) 2019.03.16
데이터 저장하기 : 변수와 상수  (0) 2019.03.11
Posted by 둥이파파^^
pc관련/C언어2019. 3. 17. 20:18

번째 강의에서, "문장, 수식, 연산자"에서는 프로그램의 흐름은 제어하게 해주는 if문에 대해서 설명했다. 그러나 여전히 프로그램을 제어하기 위해서는 단순히 참과 거짓의 결과에 따라 동작하는 if문 외에도 다른 것들이 필요할 것이다. 이 장에서는 프로그램의 흐름을 제어하기 위한 새로운 방법을 소개할 것이다.


- 간단한 배열을 사용하는 방법

- 문장을 여러 번 실행하기 위해서 for, while, do...while문을 사용하는 방법.

- 프로그램의 제어문을 종속시킬 수 있는 방법.


이 장에서는 모든 내용을 상세히 설명하지는 않겠지만, 실제로 프로그램을 작성할 수 있을 정도의 충분한 내용을 다룰 것이다.

 

1. 배열의 기초

 : for문에 대한 내용을 다루기 전에 배열에 대해서 알아보도록 하자. C에서 for문과 배열은 밀접한 관련이 있으므로 하나를 설명하지 않고 다른 하나를 소개하는 것은 힘들다. 이 장에서 설명할 for문의 예제를 이해할 수 있도록 배열에 대한 내용을 간단히 살펴보도록 하겠다.


 배열(array)은 같은 이름을 사용하지만 이름 뒤의 괄호에 나타나는 첨자(subscript)나 색인(index)에 의해서 서로 구분되는 집단적인 데이터 저장 영역의 명칭이다. 이런 개념은 배열에 대한 내용을 계속해서 접하게 되면 분명하게 이해할 수 있을 것이다. C의 다른 변수와 마찬가지로 배열을 사용하기 위해서는 먼저 선언해야 한다. 배열의 선언은 배열의 데이터형과 크기를 지정하는 것으로 가능하다. 배열의 크기는 배열 내에 포함되는 요소의 수를 뜻한다. 예를 들어, 다음 문장은 1000개의 int형 요소를 가지는 data라는 이름의 배열을 선언한다.


 int data[1000];


각각의 요소는 data[0]에서부터 data[999]까지 첨자에 의해서 지정된다. 첫 번째 요소는 data[1]이 아니라 data[0]이다. BASIC이나 다른 언어에서는 배열의 첫 번째 요소가 1이다. 그러나 C에서는 그렇지 않다. 이런 배열의 각 요소는 일반적인 정수형 변수와 동일하고, 똑같은 방법으로 사용할 수 있다. 배열의 첨자로는 다음과 같이 C의 다른 변수를 사용할 수 있다.


 int data[1000];

 int count;

 count = 100;

 data[count] = 12;    /* data[100] = 12와 동일한 문장이다. */


 지금까지 설명한 내용이 배열에 대한 기본적인 사항들이다. 이런 개념을 이해한다면 이 장에서 사용되는 예제 프로그램의 내용을 이해할 수 있을 것이다. 배열의 개념을 정확하게 이해하지 못하더라도 걱정할 필요는 없다.


2. 프로그램 실행의 제어

 : C프로그램은 기본적으로 입력된 순서대로 실행된다.프로그램은 main()함수의 처음부터 시작하여 main()의 마지막까지 문장 단위로 실행된다. 그러나 이런 실행 순서는 실제 프로그램에서 거의 나타나지 않는다. C에서는 프로그램의 실행 순서를 제어하게 해주는 다양한 프로그램 제어문이 제공된다. 앞에서는 이미 C의 가장 기본적인 제어문인 if문에 대해서 배웠으므로, 여기에서는 더욱 유용하게 사용할 수 있는 세가지 제어문에 대해서 알아보도록 하자.


2.1 for문

 : for문은 하나 이상의 블록을 지정된 횟수만큼 실행하는 C의 제어문이다. for문은 프로그램의 특정 부분을 한 번 이상 반복 실행하므로 가끔 for 순환문(for loop) 이라고도 한다. 이제 for문에 대한 상세한 내용을 알아보도록 하자. for문은 다음과 같은 구조를 가진다.


  for(initial; condition; increment)

     statement;


initial, condition, increment는 모두 C의 수식이고, statement는 C의 단일문이나 복합문이다. 프로그램이 실행되는 동안 for문을 만나게 되면 다음과 같은 동작이 수행된다.


 ① 수식 initial이 평가된다. initial은 대개 특정 값을 변수에 저장하는 할당문이다.

 ② 수식 condition이 평가된다. condition은 일반적으로 관계 수식이다.

 ③ condition이 0의 값에 해당하는 거짓으로 평가되면 for문이 종료되고 

    프로그램의 제어는 statement의 바로 다음에 잇는 문장으로 전달된다.

 ④ condition이 참으로 평가되면 statement에 포함되어 있는 C의 문장이 실행된다.

 ⑤ 수식 increment가 사용되고 나면 제어는 다시 두 번째 단계로 돌아간다.


condition이 처음에 거짓으로 평가되면 statement는 한 번도 실행되지 않는다는 것을 기억하자. 간단한 예제를 살펴보자. <리스트 6.1>에 있는 프로그램은 1부터 20까지의 수를 출력하기 위해서 for문을 사용한다. 만약 printf()문을 20번 사용한다면 여기에 나타나 있는 것보다 더욱 복잡한 프로그램이 되었을 것이다.

-> 출력
 1
 2
 3
 ·
 ·
 ·
 19
 20

=> for문은 앞에 있는 예제와 마찬가지로 어떤 값을 증가시키며 '수를 세기 위해서' 종종 사용 된다. 또한, 값을 증가시키는 대신에 감소시키며 '수를 세기 위해' 카운터 변수를 사용할 수도 있다.

 for(count = 100; count > 0; count--)

또한, 다음 예제와 같이 1이 아닌 값을 단위로 하여 '수를 셀 수도' 있다. for(count = 0; count < 1000; count +=5) for문은 매우 융통적이다. 예를 들어, 초기화 변수의 갑이 프로그램의 앞 부분에서 이미 초기화되었다면 for문에서는 카운터 변수의 초기화를 생략할 수도 있다.

2.2 종속된 for문
 : for문은 다른 for문 내에서 사용될 수 있다. 이것을 종속된 for문의 사용이라고 한다. 종속된 for문을 사용하면 복잡한 프로그램을 간결하게 작성할 수 있다. <리스트 6.2>는 복잡한 프로그램이 아니지만 두 개의 종속된 for문이 사용되는 것을 보여준다.

<리스트 6.2> 종속된 for문

-> 출력
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

2.3 while문
 : while 순환문이라고도 하는 while문은 지정된 조건이 참이라면 계속해서 주어진 문장을 실행한다. while운은 다음과 같은 형식으로 사용된다.

  while ( condition )
       statement;

condition은 C의 수식이고, statement는 C의 단순문이나 복합문이다. 프로그램에서 while문이 사용될 때에는 다음과 같은 동작이 수행된다.

 ① 수식 condition이 평가된다.
 ② condition이 거짓(0)으로 평가되면 while문은 종료되고,
    제어는 statement 바로 다음의 문장으로 전달된다.
 ③ condition이 참으로 평가되면 statement에 포함되는 C 문장이 실행된다.
 ④ 제어는 다시 첫 번째 단계로 돌아간다.

<리스트 6.3>은 1부터 20까지의 숫자를 출력하기 위해서 while문을 사용하는 간단한 프로그램이다. 이것은 <리스트 6.1>에서 for문을 사용하여 수행한 것과 동일하다.

<리스트 6.3> while문

 

 
-> 출력
  1
  2
  .
  .
  20

=> 여기서 while문은 기본적으로 초기화 부분과 증감을 수행하는 부분을 포함하지 않은 for문과 동일하다는 것을 알 수 있을 것이다. 그래서 다음은

 for( ; condition; )

다음과 같이 사용할 수 있다.

 while(condition)

이런 유사성이 있으므로 for문을 사용할 수 있는 곳에서 while문을 대신 사용할 수 있는 것이다. while문을 사용할 때에는 초기화 부분에 해당하는 내용을 먼저 독립된 문장으로 포함시켜야 하고 값을 증감시키는 부분을 while문 내에 포함시켜야 한다. 대부분의 프로그래머들은 초기화 동작이나 값의 증감이 필요할 때 while문을 사용하지 않고 for문을 선호한다. 이것은 기본적으로 소스 코드를 더 이해하기 쉽기 때문이다. for문에서는 초기화 부분, 비교문, 증감 부분이 동시에 표현되므로 내용을 쉽게 알 수 있다. 그러나 while문에서는 각각의 내용이 따로 떨어져 있으므로 상대적으로 이해하기 어렵다.

2.4 종속된 while문
 : for문이나 if문과 마찬가지로 종속된 while문을 사용할 수도 있다. <리스트 6.4>는 종속된 while문의 사용 예를 보여준다. 이것이 while문을 사용하는 가장 좋은 예는 아니지만, 종속된 while문의 사용에 대한 개념을 이해할 수 있도록 도와줄 것이다.

<리스트 6.4> 종속된 while문 

 

-> 입력/출력
 This program prompts you to enter 5 numbers
 Each number should be from 1 to 10

 Enter number 1 of 5: 3
 Enter number 2 of 5: 6
 Enter number 3 of 5: 3
 Enter number 4 of 5: 9
 Enter number 5 of 5: 2

 Value 1 is 3
 Value 2 is 6
 Value 3 is 3
 Value 4 is 9
 Value 5 is 2

2.5 do...while문
 : C의 세 번째 제어문으로는 주어진 조건이 참으로 평가되는 한 계속해서 문장이나 블록을 실행하는 do...while문이 있다. do...while 순환문은 for문이나 while문과는 달리 순환문의 처음이 아니라 마지막 부분에서 조건을 확인한다. do...while문은 다음과 같은 형식을 사용한다.

   do
      statement;
   while(condition);

condition은 C의 수식이고, statement는 C의 단순문이나 복합문이다. 프로그램에서 do...while 문이 사용될 때에는 다음과 같은 동작이 수행된다.

 ① statement의 내용이 실행된다.
 ② condition이 평가된다. 조건이 참으로 평가되면 제어는 처음으로 돌아간다.

그러나 거짓이라면 순환문은 종료된다.
do...while문에 포함된 문장은 항상 최소한 한 번 실행된다. 이것은 비교문이 순환문의 처음이 아니라 마지막 부분에서 확인되기 때문이다. 반면에 for문이나 while문은 순환문의 처음에 조건을 확인하므로, 포함된 문장은 주어진 조건이 처음부터 거짓이라면 결코 실행되지 않음. do...while문은 while문이나 for문보다 자주 사용되지 않지만, 순환문 내에 있는 문장을 최소한 한 번 실행할 필요가 있을 때 유용하다. 물론 순환문이 시작될 때 주어진 조건이 참으로 평가되게 설정하여 while문에서도 동일한 작업을 수행할 수 있을 것이다. 그러나 do...while문이 더욱 효과적일 것이다. <리스트 6.5>에는 do...while문의 사용 예가 나타나 있다.

<리스트 6.5> 간단한 do...while문

 /* 간단한 do...while문의 사용 예 */


 #include <stdio.h>


 int get_menu_choice(void);


 main()

 {

    int choice;


    choice = get_menu_choice();


    printf("You chose Menu Option %d\n", choice);


    return 0;

 }


 int get_menu_choice(void)

 {

    int selection = 0;


    do

    {

       printf("\n");

       printf("\n1 - Add a Record");

       printf("\n2 - Change a record");

       printf("\n3 - Delete a record");

       printf("\n4 - Quit");

       printf("\n");

       printf("\nEnter a selection: ");


       scanf("%d", &selection);

    }while ( selection < 1 || selection > 4 );

    return selection;

 }

 

3. 종속된 순환문

 : 종속된 순환문(nested loop)은 하나의 순환문 내에 다른 순환문이 표함 된 것을 말한다. 앞에서는 종속된 순환문의 몇 가지 예를 보았다. C에서는 내부의 순환문이 바깥의 독립적으로 존재해야 한다는 것을 제외하고는 종속된 순환문의 사용에 아무런 제한을 두지 않는다. 그러나 순환문을 겹쳐서 사용할 수는 없다. 다음과 같은 내용은 허용되지 않는다.


 for(count = 1; count < 100; count++)

    {

       do

       {

          /* do...while 순환문 */

    }  /* 순환문의 끈 */

       } while(x != 0);


do...while문이 완전히 for문 내에 위치된다면 아무런 문제가 발생하지 않는다.


 for(count = 1; count < 100; count++)

    {

       do

       {

          /* do...while 순환문 */

       } while(x != 0);

    }  /* 순환문의 끝 */


종속된 순환문을 사용할 때에는 내부의 순환문에서 변경된 내용이 바깥쪽의 순환문에 영향을 줄 수 있다는 사실을 기억하도록 하자. 그러나 안쪽에 있는 순환문은 바깥쪽의 순환문에서 사용되는 변수에 대해서 독립적으로 존재한다는 것을 주의하자. 이 예제에서는 이런 상황이 없다. 앞의 예제에서는 do...while문에서 count값이 변경된다면 바깥쪽의 for문이 실행되는 횟수에 영향을 주게 될 것이다. 들려 쓰기를 적용해서 문장을 정리하는 것은 종속된 순환문이 사용된 프로그램을 더욱 이해하기 쉽게 도와준다. 순환문의 각각은 바깥쪽에 있는 것보다 한단계 들여 써져야 한다. 이렇게 하면 각 순환문에 관련되어 있는 문장을 분명하게 구분할 수 있다. 

'pc관련 > C언어' 카테고리의 다른 글

입출력의 기초  (0) 2019.03.20
숫자배열 사용하기  (0) 2019.03.20
함수의 기본  (0) 2019.03.16
데이터 저장하기 : 변수와 상수  (0) 2019.03.11
C 프로그램의 구성 요소  (0) 2019.03.10
Posted by 둥이파파^^
pc관련/C언어2019. 3. 16. 20:37

함수는 C에서 프로그램을 작성할 때 중심이 되는 부분이다. 앞에서는 이미 컴파일러의 일부분으로 제공되는 C의 라이브러리 함수의 몇 가지를 살펴보았다. 이 장에서는 프로그래머가 작성하는 사용자 정의 함수에 대해서 알아보도록 하겠다.


오늘은 다음과 같은 내용을 배울 것이다.

- 함수는 무엇이고, 어떤 내용으로 구성되는가?

- 함수와 구조화 프로그래밍의 장점

- 함수를 작성하는 방법

- 함수 내에서의 지역 변수 선언

- 함수에서 프로그램으로 결과 값을 돌려주는 방법

- 함수에 인수를 전달하는 방법


1. 함수는 무엇인가?

 : 이 장에서는 '함수란 무엇인가?'에 대한 해답을 두 가지 나누어 설명할 것이다. 우선, 함수가 무엇인지 설명하고 나서 함수의 사용 방법을 알아보도록 하자.


1.1 함수의 정의

 : 우선 함수가 무엇인지 알아보자. 함수(function)는 일정한 동작을 수행하고, 필요에 따라 함수를 호출했던 프로그램으로 결과 값을 돌려주는 C의 독립적인 코드이며, 독특한 이름을 가진다. 이것을 좀더 구체적으로 살펴보자.


 - 함수는 이름을 가진다.

: 모든 함수는 독특한 이름을 가지고 있다. 프로그램에서는 이런 이름을 사용하여 함수에 포함된 내용(코드)을 사용할 수 있게 된다. 이런 동작을 함수 호출이라고 한다. 함수는 또한 다른 함수 내에서 호출될 수 있다.


 - 함수는 독립적이다.

: 함수는 프로그램 내의 다른 부분에 의해서 영향을 받거나 또는 다른 부분에 영향을 주지 않고 주어진 동작을 수행한다.


 - 함수는 특정 동작을 수행한다.

: 이것은 아주 간단한 사실이다. 함수가 수행하는 동작은 프로그램에서 프린터로 텍스트 문장을 인쇄하거나 숫자 순서대로 배열을 정렬하고, 세제곱근을 구하는 것과 같이 프로그램의 전체적인 동작을 구성하는 일부분이자 독립된 작업 내용이다.


 - 함수는 호출한 프로그램으로 결과 값을 돌려줄 수 있다.

: 프로그램이 함수를 호출할 때에는 함수에 포함된 문장이 실행된다. 이런 함수 내의 문장은 필요할 때마다 호출한 프로그램으로 결과를 전달할 수 있다. 지금까지 설명한 내용이 함수의 '정의'이다. 함수에 대한 내용을 설명할 때 이런 내용을 기억하기 바람.


1.2 함수의 사용 예

<리스트 5.1>에 있는 프로그램에는 사용자 정의 함수가 포함되어 있다.


<리스트 5.1> 숫자의 세제곱을 계산하기 위해 함수를 사용하는 프로그램

 

-> 입력 / 출력
 Enter an integer value: 9
 The cube of 9 is 729.
=> cube()함수와 main()함수의 구조를 비교해본다면, 두 함수의 구조가 같다는 것을 알 수 있다. main()도 하나의 함수이다. 함수의 다른 예로는 앞에서 사용했던 printf()와 scanf()가 있다. printf()와 scanf()는 사용자 정의 함수와 달리 라이브러리 함수이지만, 사용자 정의 함수와 마찬가지로 인수를 사용하고 결과를 돌려주는 함수이다.

2. 함수의 사용
 : C 프로그램에서는 프로그램 내에서 함수를 호출할 때까지 함수 내의 문장을 실행하지 않는다. 프로그램은 함수를 호출할 때 하나 이상의 자료를 인수의 형식으로 함수에 전달할 수 있다. 인수(argument)는 함수에서 특정 동작을 수행하기 위해서 필요한 프로그램의 데이터이다. 인수를 전달받은 함수는 주어진 동작을 수행하기 위해 함수 내에 포함된 문장을 실행하게 된다. 함수 내의 모든 문장이 실행되고 나면, 프로그램에서 함수를 호출했던 부분이 다시 실행될 것이다. 함수는 필요한 경우에 복귀값의 형식으로 프로그램에 결과값을 전달할 수 있다. <그림 5.1>은 한 번씩 호출되는 세 개의 함수가 사용되는 프로그램을 보여주고 있다. 함수가 호출될 때마다 프로그램의 제어는 함수로 전달된다. 함수의 실행이 완료될 때 제어는 프로그램 내에서 함수를 호출한 부분으로 다시 전달된다. 함수는 필요한 만큼 여러 번 호출될 수 있으며, 어떤 순서로도 호출될 수 있다.

 

 

<그림 5.1> 프로그램에서 함수를 호출할 때 제어는 함수로 전달되며, 함수의 실행이 완료될 때 다시 프로그램으로 전달된다. 이제 함수에 대한 개념과 함수의 중요성을 이해할 수 있을 것이다. 잠시 후에 자신만의 함수를 작성하고 사용하는 방법을 살펴보도록 하자.

** 문법 - 함수의 작성과 사용


함수 원형

    return_type function_name(arg-type name-1, ..., arg-type name-n);


함수 정의

    return_type function_name(arg-type name-1, ..., arg-type name-n)

    {

         /* 함수의 여러 가지 문장들 */

    }


함수 원형(function prototype)은 프로그램에서 나중에 정의되는 함수에 대한 내용을 컴파일러에게 알려주고 함수가 돌려주는 복귀값의 형태를 포함한다. 또한 함수의 동작 내용을 설명하는 함수의 이름을 포함하고 있다. 함수 원형에는 함수에 전달되는 인수의 형태(arg-type)가 포함되고, 필요에 따라 함수에 전달되는 변수의 이름을 포함한다. 함수 원형은 항상 세미콜론으로 끝난다. 함수 정의(function definition)는 실제적인 함수의 내용이다. 함수 정의에는 실행될 문장이 포함된다. 함수 정의에서 첫 번째 부분은 함수 헤더 (function header)라고 하는데, 함수 원형에서 세미콜론을 제거한 것과 같은 내용을 가진다. 함수 헤더는 세미콜론으로 끝나지 않는다. 또한 함수 원형에서는 변수의 이름이 선택적으로 사용되지만, 함수 헤더에서는 반드시 포함되어야 한다. 헤더의 다음에는 함수가 수행하는 동작을 위해서 필요한 프로그램 문장이 포함된다. 함수의 주요 내용이 되는 이 부분은 중괄호 내에 포함되어야 한다. 또한, void가 아닌 다른 어떤 형태의 복귀값이 필요하다면, 지정된 형태에 일치하는 return문이 포함되어야 한다.


   함수 원형의 예


       double squared(double number);

       void print_report(int report_number);

       int get_menu_choice(void);


   함수 정의의 예


       double squared(double number)

       {

         return (number * number);

       }

 

       void print_report(int report_number)

       {

          if(report_number == 1)

            puts("Printing Report 1");

          else

            puts("Not printing Report 1");

       }

 


3. 함수와 구조화 프로그래밍

 : C 프로그램을 작성할 때에는 개별적인 프로그램 동작을 함수를 통해서 독립적으로 수행하는 구조화 프로그래밍(structured programming)을 구현할 수 있다. 프로그램의 독립된 부분에서 작업을 처리한다는 것은 앞에서도 설명한 함수의 특징과 동일하다. 함수와 구조화 프로그래밍은 아주 밀접한 관련이 있다.


3.1 구조화 프로그래밍의 장점

 : 구조화 프로그래밍이 왜 중요한가? 두 가지 중요한 이유가 있다.


- 복잡한 프로그래밍 문제를 여러 개의 단순하고 짧은 내용으로 분할하므로 구조화 프로그래밍을 통해 프로그램을 작성하는 것이 더 쉽다. 각각의 작업은 프로그램과 독립적으로 존재하는 함수에 의해서 수행된다. 이런 과정을 통해서 한 번에 하나씩 문제를 처리하며 상대적으로 쉽게 프로그램을 작성할 수 있다.


- 구조화 프로그래밍을 통해 작성한 프로그램은 디버깅이 쉽다. 만약 프로그램에 비정상적인 동작을 나타내는 버그(bug)가 포함되어 있다면, 구조화 프로그래밍에서는 특정 함수를 분리해서 문제를 쉽게 발견할 수 있다.


 구조화 프로그래밍과 관련된 다른 한 가지 장점은 시간을 절약할 수 있다는 것이다. 만약 어떤 프로그램에서 특정 동작을 수행하는 함수를 작성한다면, 이런 동작을 수행할 필요가 있는 다른 프로그램에서 그 함수를 쉽게 사용할 수 있을 것이다. 심지어 새로운 프로그램에서 약간 다른 내용의 동작을 수행할 필요가 있다면, 처음부터 새로운 함수를 작성하는 것보다 이전에 작성했던 함수를 변경하여 사용하는 것이 더 쉽다는 것을 알 수 있을 것이다. 실제로 이런 함수를 직접 작성해보지 않더라도, 지금까지 두 함수 printf()와 scanf()를 아주 많이 사용했다는 것을 생각해보기 바란다. 만약 함수가 한 가지 작업을 수행하도록 작성되어 있다면, 다른 프로그램에서 사용하기는 더 쉬어진다.


3.2 구조화 프로그래밍의 방법

 : 구조화 프로그래밍을 구현하기 원한다면, 우선 몇 가지 계획을 세울 필요가 있다. 이런 계획은 프로그램을 직접 작성하기 전에 세워야 하는 것으로, 종이와 연필만으로도 간단히 할 수 있는 일이다. 프로그래밍 계획에는 처리할 작업을 목록으로 나열해야 한다. 일단 전체적이고 포괄적인 프로그램의 동작을 생각해보자. 만약 이름과 주소록을 관리하기 위한 프로그램을 작성하려고 한다면, 프로그램이 어떤 동작을 수행해야 할까? 몇 가지 예를 들어보자.


- 새로운 이름과 주소를 입력한다.

- 현재의 내용을 변경한다.

- 성과 이름에 의한 순서대로 내용을 분류한다.

- 우편용 레이블(label)을 인쇄한다.


여기에서는 프로그램의 동작 내용을 네 가지 주요 작업으로 분할했다. 각각의 내용은 함수로 구성할 수 있다. 이제, 이것을 더욱 구체적으로 나누어보자. 예를 들어, '새로운 이름과 주소를 입력한다'는 동작은 다음과 같이 더욱 구체적으로 나타낼 수 있을 것이다.


- 디스크에서 현재의 주소록을 읽어들인다.

- 하나 이상의 새로운 내용을 입력하도록 해준다.

- 새로운 데이터를 목록에 추가한다.

- 갱신된 목록을 디스크에 저장한다.


이와 비슷하게 '현재의 내용을 변경한다.'는 동작은 다음과 같이 나누어볼 수 있다.


- 디스크에서 현재의 주소록을 읽어들인다.

- 하나 이상의 내용을 변경한다.

- 갱신된 목록을 디스크에 저장한다.


이런 두 가지 항목에서 공통적인 내용이 있다는 것을 알 수 있다. 디스크에서 데이터를 읽어들이고 갱신된 내용을 다시 디스크에 저장하는 동작은 두 가지 경우에 모두 사용된다. 그래서 '디스크에서 현재의 주소록을 읽어들이는' 하나의 함수를 작성하고, 이 함수를 '새로운 이름과 주소를 입력한다'는 함수와 '현재의 내용을 변경한다.'는 함수에서 호출할 수 있다. 또한, '갱신된 목록을 디스크에 저장하는' 함수도 똑같은 방법으로 사용할 수 있을 것이다.


여기서 구조화 프로그램의 한 가지 장점을 알 수 있을 것이다. 프로그램을 작업별로 세분화하면 프로그램 내에서 공통적으로 수행되는 작업이 무엇인지 알 수 있다. 그래서 프로그램을 작성하는데 소모되는 시간을 절약하고, 프로그램을 더욱 작고 효율적으로 만들 수 있으며, '공통적인' 디스크 작업 함수를 작성할 수 있다.이런 프로그래밍 방식은 계층적(hierarchical)으로 짜여진 프로그램의 구조를 형성한다. <그림 5.2>에서는 주소 목록 프로그램의 계층적인 프로그래밍 방식을 보여주고 있다.

 

<그림 5.2> 구조화 프로그래밍은 계층적으로 구성된다. 이런 과정에 의해서, 프로그램에서 수행할 필요가 있는 작업의 세분화도니 목록을 작성할 수 있다. 그리고 나서 모든 동작을 한번에 수행해야 하는 복잡한 프로그램을 작성하는 것보다는 상대적으로 더욱 간단한 각각의 함수를 한 번에 하나씩 작성할 수 있게 된다. 하나의 함수를 작성해서 정상적으로 동작하는지 확인하고 나면 다른 함수를 작성할 수 있다. 프로그램은 서서히 틀을 잡게 될 것이다.

3.3 하향식 접근

 : C 프로그래머들은 구조화 프로그래밍을 통해서 하향식 접근 방법(top-down approach)를 사용하게 된다. 이것은 프로그램의 구성 방법이 마치 나무를 뒤집어 놓은 것처럼 보이는 <그림 5.2>에서 설명한 내용이다. 프로그램의 실제 동작은 대부분 '가지의 끝 부분'에 해당하는 함수에서 수행된다. '상위'의 함수는 이런 여러 가지 함수들 중에서 필요한 것을 실행하도록 지시하는 역할을 한다. 결과적으로, 많은 C 프로그램은 프로그램의 주요 부분인 main() 함수에 적은 양의 코드만을 포함하게 되고, 프로그램의 골격을 이루는 내용은 여러 함수에 포함된다. main()에는 프로그램의 실행을 지시하는 몇 줄의 함수 호출문만 포함된다. 메뉴를 사용하는 프로그램에서는 종종 사용자가 선택한 사항에 따라서 프로그램의 실행을 분기하는 방법이 사용된다. 메뉴의 각 분기는 서로 다른 함수를 사용한다.


4. 함수의 작성

 : 함수를 작성하는 데 있어서의 첫 번째 단계는 함수의 용도를 결정하는 것이다. 일단 함수가 수행해야 하는 동작을 결정하고 나면 함수를 작성하는 실제 과정은 그리 어렵지 않을 것이다.


4.1 함수의 헤더

 : 모든 함수의 첫 번째 부분은 함수에 대한 특정 사항을 설명하는 세 개의 요소로 이루어지는 함수 헤더이다. 헤더는 <그림 5.3>에 나타나 있다. 각각의 요소에 대해서 하나씩 살펴보도록 하자.

<그림 5.3> 함수 헤더의 세 가지 요소

▶ 함수의 복귀형

 : 함수의 복귀형은 함수의 동작이 완료된 후에 함수를 호출했던 프로그램으로 돌려주는 데이터의 형태를 가리킨다. 복귀형은 C의 모든 데이터형이 될 수 있다. 즉, char, int, long. float, double이 사용될 수 있다. 또한, 아무런 값도 전달하지 않는 void형의 함수를 정의할 수 있다. 다음은 몇 가지 예이다.


        int   func1(...)    /* int형을 돌려준다. */

        float func2(...)    /* float형을 돌려준다. */

        void  func3(...)    /* 아무 것도 돌려주지 않는다. */


▶ 함수의 이름

 : 함수의 이름을 지정할 때에는 C의 변수 이름에 대한 규칙을 따라야 한다. 함수의 이름은 독특한 것이어야 하고, 다른 어떤 함수나 변수에 할당된 이름을 사용해서는 안된다. 함수가 수행하는 동작 내용을 알 수 있도록 함수의 이름을 지정하는 것이 좋다.


▶ 매개 변수 목록

 : 대부분의 함수에서는 프로그램에서 전달받는 인수(arguments)를 사용한다. 함수는 어떤 형태의 인수를 사용하는지 나타내어야 한다. 즉, 인수의 데이터가 지정되어야 한다. 함수에는 모든 C의 데이터형이 인수로 사용될 수 있다. 인수의 형태에 대한 내용은 함수 헤더에 매개 변수 목록의 형식으로 포함된다. 함수에 전달되는 각각의 인수에 대해서 매개 변수 목록에는 대응하는 내용이 포함되어야 한다. 이런 내용은 데이터형과 매개 변수의 이름이다. 예를 들어, 다음은 <리스트 5.1>의 함수에서 사용된 헤더이다.


        long cube(long x)


매개 변수 목록은 이 함수가 x라는 매개 변수로 표현되는 하나의 long형 인수를 사용한다는 것을 나타내기 위해서 long x라고 되어 있다. 하나 이상의 매개 변수가 존재한다면 각각의 변수를 쉼표로 구분해야 한다. 그래서 다음과 같은 헤더는


        void func1(int x, float y, char z)


x라는 이름의 int형 변수, y라는 이름의 float형 변수, z라는 이름의 char형 변수 등 세 개의 인수를 사용하는 함수를 정의한다. 어떤 함수에서는 아무런 인수도 사용하지 않으며, 이 때에는 매개 변수의 목록에 void를 사용해야 한다.


        void func2(void)


가끔 매개 변수와 인수의 차이점에 대해서 혼란스러울 것이다. 매개 변수(parameter)는 함수의 헤더에 포함되는 내용으로, 인수에 대응하여 '영역을 확보하는' 역할을 한다. 함수의 매개 변수는 고정적인 것이므로 프로그램이 실행되는 동안에 변경되지 않고 사용된다. 반면에, 인수는 함수를 호출하는 프로그램에 의해서 함수로 전달되는 실제값이다. 함수가 호출될 때마다 다른 인수값이 전달될 수 있다. 함수가 호출될 때마다 같은 수와 같은 형태의 인수를 전달해야 하지만, 인수의 실제 값은 달라질 수 있다. 함수는 인수에 대응하는 매개 변수의 이름을 통해서 값을 받아들인다. 예제를 통해서 이런 사실을 분명히 이해하도록 하자. <리스트 5.2>는 두 변 호출되는 하나의 함수가 포함되어 있는 아주 간단한 프로그램이다


<리스트 5.2> 인수와 매개 변수의 차이점

 

-> 출력

   The value of z = 1.750000

   The value of z = 32.555000


<그림 5.4>에는 인수와 매개 변수의 관계가 나타나 있다.

 <그림 5.4> 함수가 호출될 때마다 인수가 함수 내의 매개 변수에 전달된다.

4.2 함수의 몸체

 : 함수의 몸체(function body)는 중괄호 내에 포함되어 있으며 함수의 헤더 다음에 나타난다. 함수의 실제 동작은 여기서 수행된다. 함수가 호출될 때, 프로그램의 제어는 함수의 몸체에서 부터 시작해서 return문을 만나거나 닫는 중괄호가 나타날 때 다시 함수를 호출한 프로그램으로 돌아간다.


▶ 지역변수

 : 함수의 몸체에서는 변수를 선언할 수 있다. 함수 내에서 선언되는 변수를 지역 변수(local variables)라고 한다. 지역적(local)이라는 것은 변수가 함수 내에서만 사용되고 프로그램의 다른 부분에서 동일한 이름으로 선언된 변수와 구분된다는 것을 뜻한다. 이것이 지역 변수의 개념이다. 이제부터 지역 변수를 선언하는 방법을 살펴보자. 지역 변수는 다른 변수에서와 동일한 규칙을 사용하여 선언할 수 있다. 또한, 지역 변수는 선언하는 동시에 초기화할 수 있다. 함수 내에서는 C의 모든 변수형을 선언할 수 있다. 다음은 함수 내에서 선언되는 네 개의 지역 변수를 사용하는 예제이다.


        int func1(int y)

        {

            int a, b = 10;

            float rate;

            double cost = 12.55;

        }


여기에서는 함수 내부에서 사용되는 지역 변수 a, b, rate, cost를 선언한다. 함수의 매개 변수도 변수의 선언에 해당되는 것이므로, 매개 변수의 목록에 있는 변수도 함수 내에서 사용될 수 있다. 함수 내에서 변수를 선언하고 사용할 때, 이 지역 변수는 프로그램의 다른 부분에서 선언된 모든 변수에 대해서 독립적이다. 심지어 다른 변수가 같은 이름을 사용하고 있더라도 서로 다른 변수이다. <리스트 5.3>에 있는 프로그램은 이런 독립성을 보여주고 있다.


<리스트 5.3> 지역 변수의 사용 

-> 출력


   Before calling demi(), x = 1 and y = 2.

   Within demo(), x = 88 and y = 99.

   After calling demo(), x = 1 and y = 2.


여기에서 알 수 있듯이 함수의 지역 변수 x와 y는 함수의 바깥에서 선언된 전역 변수 x, y와 완전히 다르다. 함수 내에서 변수를 사용하는 경우에는 세가지 규칙이 적용된다.


- 함수에서 변수를 사용하기 위해서는 함수 헤더나 함수 내에서 선언해야 한다.

- 함수를 호출하는 프로그램에서 사용되는 값을 함수 내에서 사용하기 위해서는 인수의 형태로 전달해야 한다.

- 함수를 호출한 프로그램으로 함수의 결과값을 전달하기 위해서는 함수에서 결과값을 돌려주어야 한다.


▶ 함수의 내용

 : 함수에서 사용할 수 있는 문장의 종류나 내용에는 기본적으로 아무런 제한이 없다. 함수에서 수행할 수 없는 유일한 한가지 동작은 다른 함수를 정의하는 것이다. 그러나 순환문, if문, 할당문을 포함하여 함수에서는 C의 모든 문장을 사용할 수 있다. 또한 함수에서는 라이브러리 함수와 사용자 정의 함수를 호출할 수 있다. 함수의 길이에는 제한이 있을까? C는 함수의 길이에 대해서 아무런 제한을 두고 있지 않지만, 실용적인면에서 볼 때 함수는 상대적으로 짧은 것이 좋다. 구조화 프로그래밍에서 각각의 함수는 상대적으로 간단한 하나의 작업을 수행하는 것이 좋다는 사실을 기억하기 바란다. 함수의 길이가 상당히 길어진다면 하나의 함수로 여러 가지 동작을 수행하려고 했기 때문이다. 이런 함수는 아마도 두 개 이상의 더욱 세분화된 함수로 나눌 수 있을 것이다. 그렇다면, 어느 정도의 길이가 긴 것일까? 여기에 대한 정확한 해답은 없지만, 실제 경험에 의하면 25줄이나 30줄 이상의 함수를 작성하는 경우는 거의 없고 찾아보기도 힘들다. 함수의 길이에 대해서는 스스로 판단해야 한다. 어떤 프로그램에서는 긴 함수가 필요할 것이고, 대부분의 함수에서는 몇 줄의 문장만이 필요할 것이다. 프로그래밍 경험을 쌓아감에 따라, 더욱 세분화되니 함수를 사용해야 하는 경우와 어느 정도의 길이를 가지는 함수를 작성하여 사용해야 하는 경우를 구분할 수 있게 될 것이다.


▶ 값의 전달

 : 함수의 결과 값을 프로그램으로 돌렺주기 위해서는 return문과 함께 C의 수식을 사용해야 한다. 제어가 return문에 도달할 때, 수식은 평가되고 제어는 다시 원래의 프로그램으로 전달된다. 함수의 복귀값은 수식의 결과이다. 다음 함수를 살펴보자.


        int func1(int var)

        {

           int x;

           return x;

        }


이 함수가 호출될 때 return문까지의 함수의 내용이 실행된다. return은 함수를 마치고 처음에 함수를 호출했던 프로그램으로 x라는 값을 돌려준다. return에는 모든 C의 수식을 사용할 수 있다. 함수는 여러 개의 return문을 가질 수 있다. 그러나 실제로 효과를 나타내는 것은 가장 먼저 실행되는 return문이다. <리스트 5.4>에서 볼 수 있듯이 여러 개의 return문을 사용하는 것은 함수에서 다양한 값을 돌려주는 호과적인 방법이다.


<리스트 5.4> 함수에서 여러 개의 return문을 사용하는 예

 

-> 입력 / 출력

   Enter two different integer values:
   300
   200
   The larger value is 300.

4.3 함수의 원형
 : 프로그램에는 사용되는 각각의 함수에 대한 함수 원형이 포함되어야 한다. <리스트 5.1>의 4변째중에는 함수 원형이 나타나 있고 앞에서 다른 여러 프로그램의 함수 원형을 보았다. 함수 원형은 무엇이고 왜 사용해야 하는 것일까? 앞의 예제에서, 함수의 원형은 함수의 헤더에 세미콜론을 추가한 것과 같다는 것을 알 수 있다. 함수 헤더와 마찬가지로 함수의 원형에는 함수의 복귀형, 이름, 매개 변수에 대한 내용이 포함된다. 함수 원형이 하는 일은 컴파일러에게 함수의 복귀형, 이름, 매개 변수에 대해서 알려주는 것이다. 컴파일러는 이런 자료를 이용하여 소스 코드에서 함수를 호출할 때마다 함수를 찾아서 함수에 전달되는 인수의 수와 형태를 확인하고 함수가 돌려주는 복귀값이 정확한지 검사할 수 있다.
 만약 이런 과정에서 문제가 발생하면 컴파일러는 에러 메시지를 출력한다. 실제로 함수 원형이 함수의 헤더와 정확히 일치할 필요는 없다. 동일한 형태와 순서의 매개 변수를 사용하고 동일한 개수의 매개 변수를 포함시킨다면 변수의 이름은 달라질 수 있다. 헤더와 원형의 내용이 반드시 일치할 필요는 없다. 그러나 헤더와 원형을 똑같이 작성하면 프로그램을 더욱 이해하기 쉽다. 함수 정의를 마칠 때 함수의 원형을 작성하기 위해서 헤더를 복사해서 에디터에서 제공되는 잘라 붙이기 기능을 사용하면 된다. 마지막에는 반드시 세미콜론을 추가하기 바란다. 함수 원형은 소스 코드의 어디에 위치되어야 하는 것일까? 함수 원형은 main()이 시작되기 전이나 또는 첫 번째 함수가 정의되기 전에 포함되어야 한다. 프로그램을 이해하기 쉽게 하려면 모든 함수원형을 한군데 모아두는 것이 가장 좋다.

5. 함수에 인수 전달하기
 : 함수에 인수를 전달하기 위해서는 함수의 이름 다음에 나타나는 괄호 내에 인수를 포함시 켜야 한다. 인수의 수와 형태는 함수 헤더나 원형에 나타나 있는 것과 일치해야 한다. 예를 들어, 함수가 두 개의 int형 인수를 사용하도록 정의되어 있다면, 더 많거나 적지 않고 데이터형이 다르지 않은 두 개의 int형 인수를 전달해야 한다. 만약 함수에 부적절한 형태나 개수의 인수를 전달하려고 한다면, 컴파일러는 함수 원형에 나타나 있는 내용을 근거로 하여 문제를 찾을 것이다.
 함수가 여러 개의 인수를 사용할 경우 함수 호출에 포함되는 인수는 순서대로 함수의 매개 변수에 할당된다. <그림 5.5>에 나타나듯이 첫 번째 인수는 첫 번째 매개 변수에 대응하고, 두 번째 인수는 두번째 매개 변수에 대응하는 순서로 전달된다.

<그림 5.5> 여러 개의 인수는 순서대로 함수의 매개 변수에 할당된다.

각각의 인수는 C에서 사용할 수 있는 어떤 수식이든지 될 수 있다. 상수, 변수, 산술이나 논리 수식, 또는 심지어 하나의 return값을 가지는 다른 함수를 사용할 수도 있다. 예를 들어, half(), square(), third()가 모두 복귀값을 가지는 함수라면 다음과 같은 문장을 작성할 수 있다.


      x = half ( third ( square ( half ( y ) ) ) );


프로그램은 우선 y라는 인수를 전달하며 함수 half()를 호출한다. half()의 실행이 완료되면 half()의 복귀값을 인수로 사용하여 square()가 호출된다. 다음으로 square()의 복귀값을 인수로 사용하여 third()가 호출된다. 그리고 나서 다시 half()가 호출되며, 이번에는 third()의 복귀값을 인수로 사용하게 된다. 마지막으로 half()의 복귀값은 변수 x에 할당된다. 다음은 이와 같은 내용의 문장을 여러 개로 나눈 것이다.


       a = half(y);

       b = square(a);

       c = third(b);

       x = half(c);


6. 함수의 호출

 : 함수는 두 가지 방법으로 호출할 수 있다. 어떤 함수는 단순히 함수 이름과 인수만을 사용하여 한 문장으로 호출할 수 있다. 만약 함수가 복귀값을 가진다면 그 값은 버려진다.


       wait(12);


두 번째 방법은 복귀값을 생성하는 함수에서만 사용할 수 있다. 이런 함수는 결과적인 값, 즉 복귀값을 전달하므로 C의 수식으로 간주된다. 그래서 C의 수식을 사용할 수 있는 곳이라면 어디에서든지 사용할 수 있다. 앞에서는 이미 복귀값을 생성하며 할당문의 오른쪽에서 사용되는 수식의 예를 보았다. 몇 가지 다른 예를 살펴보도록 하자. 다음 예제에서 half_of()는 함수의 매개 변수이다.


       printf("Half of %d is %d." x, half_of(x));


우선, x를 인수로 하여 함수 half_of()가 호출되고, printf()는 x와 half_of(x)의 결과를 인수로 사용하여 호출된다. 두 번째 예에서는 하나의 수식 네에서 여러 함수를 사용하고 있다.

  

       y = half_of + half_of(z);


여기에서는 half_of()를 두 번 사용하고 있지만, 두 번째 함수는 다른 함수가 될 수도 있을 것이다. 동일한 내용을 여러 문장으로 나누면 다음과 같다.


       a = half_of(x);

       b = half_of(z);

       y = a + b;


마지막 두 예제는 함수의 복귀값을 사용하는 효과적인 방법을 보여준다. 다음은 if문 내에서 사용된 함수이다.


       if (half_of(x) > 10)

       {

          /* 어떤 문장이 될 수 있다 */

       }


함수의 복귀값이 기준에 일치한다면, 즉 half_of()가 10보다 큰 값을 돌려주면 if문은 참이 되고 그 아래에 있는 문장이 실행된다. 그러나 함수의 복귀값이 조건을 만족시키지 않는다면 if문의 내용은 실행되지 않는다. 다음은 더 좋은 예이다.


       if (do_a_process() != OKAY)

       {

          /* 에러 처리 루틴 */

       }


여기서 사용된 예제도 실제 프로그래밍 예제는 아니다. 그러나 이 문장은 함수가 정상적으로 실행되었는지를 확인하기 위해서 복귀값을 사용하는 중요한 예제이다. 만약 정상적이지 않은 결과가 나타나면 if문에서는 에러 처리 루틴이 실행된다. 이것은 파일에서 자료를 읽어들이고, 값을 비교하고, 메모리를 할당하는 경우에 흔히 사용되는 에러 처리 방법이다. 또한, void의 복귀형을 가지는 함수를 수식에서 사용하게 되면 컴파일러는 에러 메시지를 출력할 것이다.


6.1 재귀 용법

 : 재귀 용법(reacquisition)은 함수가 직접적으로나 간접적으로 그 자신을 호출하는 것을 말한다. 간접적인 재귀(indirect reacquisition)는 하나의 함수를 호출하는 다른 어떤 함수가 현재의 함수 내에서 호출되는 경우를 말한다. C에서는 재귀적인 함수의 사용을 허용하는데, 재귀 용법은 가끔 매우 유용하게 사용된다. 예를 들어, 재귀 용법은 숫자의 계승(factorial)을 계산하는 데 유용하다. 숫자 x의 계승은 x!로 표기하는데 다음과 같이 계산할 수 있다.


      x! = x * (x - 1) * (x - 2) * (x - 3) * ... * (2) * 1


x!는 다음과 같이 계산할 수 있다.


      x! = x * (x - 1)!


계산을 한 번 진행하면 (x-1)!은 동일한 방법으로 계산할 수 있을 것이다.


      (x - 1)! = (x - 1) * (x - 2)!


이런 공식을 사용하여 x가 1이 될 때까지 재귀적인 방법으로 계산을 진행하게 된다. <리스트 5.5>에 있는 프로그램은 계승을 계산하기 위해서 재귀적 함수를 사용한다. 프로그램이 unsigned int형을 사용하므로 계승을 구할 숫자값은 8까지만 허용된다. 9이상의 계승의 결과는 정수형 변수의 범위를 벗어난다.


<리스트 5.5> 계승을 계산하기 위한 재귀적 함수의 사용

 

-> 입력 / 출력


   Enter an integer value between 1 and 8:

   6

   6 factorial equals 720}


7. 함수의 위치

 : 프로그램에서 함수를 어디에 위치시키는 것이 좋을까? 여기에서는 함수를 main()과 같은 파일 내에서 main() 함수의 뒤에 위치시키는 것이 좋다고 알아 두기 바란다. <그림 5.6>에는 함수를 사용하는 프로그램의 기본적인 구조가 나타나 있다. 실제로는 사용자 정의 함수를 main()이 아닌 독립된 소스 코드 파일에 포함시킬 수 있다. 이런 방법은 큰 프로그램을 작성하거나 하나 이상의 프로그램에서 동일한 기능의 함수를 사용하기 원할 때 유용하다.

<그림 5.6> 함수의 원형을 main()의 앞 부분에 위치시키고, 함수 정의 내용을 main()의 뒷 부분에 위치시킨다.

 

 

'pc관련 > C언어' 카테고리의 다른 글

숫자배열 사용하기  (0) 2019.03.20
C언어-프로그램제어문  (0) 2019.03.17
데이터 저장하기 : 변수와 상수  (0) 2019.03.11
C 프로그램의 구성 요소  (0) 2019.03.10
c프로그램밍 기초  (0) 2019.03.09
Posted by 둥이파파^^