이 장에서는 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개의 수를 순서없이 입력한다. 그러면 그 중에서 가장 큰 값을 출력한다.

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

7장 입출력의 기초  (0) 2019.06.02
8장 숫자 배열 사용하기  (0) 2019.06.02
10장 문자와 문자열  (0) 2019.06.02
11장 구조체  (0) 2019.06.02
12장 변수의 범위  (0) 2019.06.02

+ Recent posts