[C언어] 배열과 포인터 : 관계

조아리 ㅣ 2024. 9. 5. 11:11

반응형

배열명으로 배열 요소 사용하기

주소는 정수처럼 보이지만 자료형에 관한 정보를 가지고 있는 특별한 값입니다. 따라서 연산을 자유롭게 할 수 없고 정해진 연산만 가능합니다.

 

예를 들어, 주소에 정수를 더하는 연산은 다음과 같이 이루어집니다.

 

주소 + 정수 = 주소 + (정수 * 주소를 구한 변수의 크기)

 

이를 쉽게 설명하기 위해 예를 들어보겠습니다.

반응형

 

만약 int형 변수 a가 주소 100번지 저장되어 있다면, a의 주소에 1을 더한 값은 101이 아닌 104가 됩니다. 이는 int형 변수의 크기는 4바이트이기 때문에 100 + ( 1 * 4 ) 로 계산되기 때문입니다. 마찬가지로 a의 주소에 2를 더한 값은 108이 됩니다. ( 100 + (2 * 4))

 

이 원리는 배열에서도 동일하게 적용됩니다. 배열명도 주소이므로, 정수를 더하면 배열의 각 요소에 대한 주소를 알 수 있으며, 간접 참조 연산을 통해 모든 배열 요소를 사용할 수 있습니다.

 

#include <stdio.h>

int main(void){
    int arr[3];
    int i;

    *(arr + 0) = 10;
    *(arr + 1) = *(arr + 0) + 10;

    printf("세 번째 배열 요소에 키보드 입력 : "):
    scanf("%d", arr + 2);

    for(i = 0; i < 3; i++){
    	printf("%5d", *(arr + i));
    }

    return 0;
}

 

실행 결과

세 번째 배열 요소에 키보드 입력 : 30
   10   20   30

 

위 코드에서는 배열 arr을 선언한 뒤에 간접 참조 연산(*)과 배열명을 사용해서 배열 요소에 값을 할당하고 있습니다. arr의 주소에 0을 더하면 첫 번째 요소(index 0)에 접근하게 되고, 1을 더하면 두 번째 요소(index 1)에 접근하게 됩니다. 세 번째 요소(index 2)는 scanf 함수를 이용해 값을 입력받고 있으며, scanf에는 직접 주소를 쓰니 asterisk(*)를 쓰지 않았습니다. 

 

 

배열명 역할을 하는 포인터

배열은 주소이므로 포인터에 저장할 수 있습니다. 이 경우 포인터로도 연산식이나 대괄호를 써서 배열 요소를 쉽게 사용할 수 있습니다. 예제를 통해 살펴보겠습니다.

 

#include <stdio.h>

int main(void){
    int arr[3];
    int *pa = arr;
    int i;

    *pa = 10;
    *(pa + 1) = 20;
    pa[2] = pa[0] + pa[1];

    for(i = 0; i < 3; i++){
    	printf("%5d", pa[i]);
    }

    return 0;
}

 

실행 결과

   10   20   30

 

이전에 배열명을 이용한 것과 매우 유사한 것을 확인하실 수 있습니다. 포인터만 썼을 뿐이지 원리는 같습니다. 주소를 가리키는 것과 주소 연산 방식을 따르는 것까지 모두 같습니다. 포인터 또한 배열처럼 표기할 수 있는 것 또한 확인할 수 있습니다 (pa[0])

 

주의할 점

다음 코드를 예시로 살펴보겠습니다.
int arr[5] = {1, 2, 3, 4, 5};
int *pa = arr;

이때 *pa는 배열 arr의 첫 번째 요소인 1을 가리킵니다. 이제 *(++pa)의 값은 2가 됩니다. 이 연산은 포인터 pa를 다음 요소로 이동한 후, 그 주소에 저장된 값을 간접 참조 연산자를 통해 가져오기 때문입니다. 그러나 이 연산을 수행해도 배열 arr의 요소들은 여전히 {1, 2, 3, 4, 5}로 그대로 유지됩니다.

그렇다면 ++(*pa)는 어떻게 될까요? 이 경우에도 2가 출력됩니다. 그러나 이는 *(++pa)와는 다른 이유에서 입니다. ++(*pa)는 포인터 pa가 가리키고 있는 현재 값(즉, 1)을 먼저 가져온 후, 그 값을 1 증가시킵니다. 결과적으로 배열 arr의 첫 번째 요소가 2로 변경되며, 배열 arr의 요소들은 {2, 2, 3, 4, 5}로 바뀌게 됩니다.

정리하면, *(++pa)는 포인터 pa를 다음 주소로 이동시킨 후 해당 주소에 저장된 값을 가져오는 연산이고, ++(*pa)는 현재 포인터가 가리키는 값을 먼저 가져온 다음, 그 값을 1 증가시키는 연산입니다.

 

 

배열명과 포인터의 차이

여기까지 보셨으면 다른 것이 없어 보이지만 다른 점도 있습니다.

 

  1. sizeof 연산 결과가 다릅니다.
    배열명에 sizeof를 사용하면 배열 전체의 크기를 구하고 포인터에 사용하면 포인터 하나의 크기를 수합니다. 따라서 포인터로 배열 전체의 크기를 확인할 수 없게 됩니다.
  2. 변수와 상수의 차이가 있습니다.
    포인터는 변수이므로 그 값을 변경할 수 있지만 배열명은 상수이므로 값을 바꿀 수 없습니다.

 

포인터의 뺄셈과 관계 연산

포인터에는 정수 덧셈이나 증가 연산 외에도 다양한 연산을 수행할 수 있습니다. 예를 들어 가리키는 자료형이 같으면 포인터끼리의 뺄셈이 가능합니다. 물론 일반 뺄셈과는 다릅니다.

 

포인터 - 포인터 = 값의 차 / 가리키는 자료형의 크기

 

#include <stdio.h>

int main(void){
    int arr[5] = {10, 20, 30, 40, 50};
    int *pa = arr;
    int *pb = pa + 3;

    printf("pa : %u\n", pa);
    printf("pb : %u\n", pb);
    pa++;
    printf("pb - pa = %u\n", pb - pa);

    printf("앞에 있는 배열 요소의 값 출력 : ");
    if(pa < pb){
    	printf("%d\n", *pa);
    } else {
    	printf("%d\n", *pb);
    }

    return 0;
}

 

실행 결과

pa : 3799428
pb : 3799440
pb - pa : 2
앞에 있는 배열 요소의 값 출력 : 20

 

이 예제에서는 포인터 pa와 pb가 배열 arr의 서로 다른 요소를 가리키도록 설정한 후, 포인터 간의 뺄셈과 대소 비교 연산을 수행합니다. pa는 arr의 첫 번째 요소를 가리키고, pb는 pa보다 3개 뒤에 있는 arr의 네 번째 요소를 가리키고 있습니다. 배열 arr의 자료형은 int이므로, 각 요소는 4바이트의 크기를 차지하며, 따라서 pb의 주소값은 pa의 주소값에 12(3 * 4)가 더해진 값이 됩니다.

그 후, pa를 1 증가시키면, 포인터는 다음 배열 요소로 이동하여 주소값이 4(1 * 4) 증가합니다. 결과적으로 pa의 새로운 주소값은 3799432가 됩니다. 이제 pb에서 pa를 빼면, (3799440 - 3799432) / 4를 계산하여 2라는 결과를 얻게 됩니다. 이 값이 양수이므로 pb가 pa보다 큰 주소를 가리키고 있다는 의미입니다. 따라서 조건문에 의해 pa가 가리키는 배열 요소의 값이 출력됩니다. (pa++을 해준 결과 두 번째 요소를 가리키게 됐다.)

반응형