Programming/C

[C] 배열 포인터란?

임아톰 2023. 2. 26. 12:09

배열 포인터란

배열 포인터는 배열을 가리키는 포인터입니다.

포인터는 변수의 주소를 저장하기 위한 변수형이며, 저장한 주소를 해석하는 방법을 의미합니다. 즉, int형 포인터는 int 형 주소를 저장하기 위한 변수형이고 저장한 주소를 int형으로 해석합니다.

이와 마찬가지로 배열 포인터는 배열의 주소를 저장하고 있고 저장한 주소를 배열로 해석합니다.

 

배열 포인터 선언

크기가 3인 int형 배열 x의 주소를 담는 포인터 변수 p는 아래와 같이 선언합니다.

int x[3];
int(*p)[3] = &x;

조금 복잡해보이지만 배열 포인터를 선언하는 규칙은 다른 int, double 포인터와 동일합니다.

 

예를 들면, int, double의 포인터 변수는 다음과 같이 선언합니다.

int n;
int* p1 = &n;

double d;
double* p2 = &d;

 

 

포인터 변수를 선언하는 규칙을 요약하면,

1) 포인터 변수는 자신이 담을 변수와 동일한 타입의 변수를 선언하고 (int p[3])
2) 변수 이름 앞에 *를 붙인다. (int *p[3])

다만 여기에 int(*p)[3]와 같이 괄호를 추가해야한다. 괄호를 추가하지 않으면 연산자 우선순위에 의해 배열 포인터를 선언한게 아니라 int*의 배열을 선언한게 됩니다. 이는 연산자 우선순위가 * 보다 [가 높아서 발생한 것입니다. 따라서 int(*p)[3]와 같이 꼭 괄호를 해야합니다.

 

배열의 이름과 배열 포인터의 차이

C언어에서 배열의 이름은 배열의 첫 번째 요소의 주소를 가리킵니다. 배열 포인터가 담고 있는 주소도 배열의 첫번 째 요소의 주소와 동일합니다. 아래 예시에서 p1은 배열의 첫번째 요소를 가리키고 p2는 배열을 가리킵니다. 

#include <stdio.h>

int main()
{
    int x[4] = { 1, 2, 3, 4};
    int* p1 = x; 
    int(*p2)[4] = &x;
    printf("%p\n", p1);
    printf("%p\n", p2);
}

예시를 실행하면 동일한 결과가 나온 것을 확인할 수 있습니다. 이 둘은 동일한 주소를 담고 있습니다.

 

그렇다면 배열의 이름과 배열 포인터의 차이는? sizeof 연산자를 사용하여 확인할 수 있습니다.

#include <stdio.h>

int main()
{
    int x[4] = { 1, 2, 3, 4};
    int* p1 = x; 
    int(*p2)[4] = &x;
    printf("sizeof(p1): %lu\n", sizeof(*p1));
    printf("sizeof(p2): %lu\n", sizeof(*p2));
}

p1은 int형 주소를 담고 있으니 int의 크기인 4가 나오고 p2는 요소가 4개인 int 배열을 가리키고 있으니 결과가 16이 나온다. 컴파일러는 p2가 가리키는 곳을 배열로 인식하여 sizeof 연산자 결과를 처리합니다.

 

이 둘의 차이는 더하기 연산자를 사용하였을 때도 나타난다. 아래 결과를 확인해보자.

#include <stdio.h>

int main()
{
    int x[4] = { 1, 2, 3, 4};
    int* p1 = x; 
    int(*p2)[4] = &x;
    printf("x: %p\n", x);
    printf("p1 + 1: %p\n", (p1 + 1));
    printf("p2 + 2: %p\n", (p2 + 1));
}

p1은 int형 주소를 담고 있으므로 1을 더했을 때 x보다 4가 커집니다. 반면 p2는 배열의 주소를 담고 있으므로 더하기 1을 했을 때 배열의 크기(16)만큼 증가합니다.

 

배열 포인터와 2차원 배열

2차원 배열도 1차원 배열과 마찬가지로 배열 포인터에 주소를 담을 수 있습니다. 2차원 배열 포인터도 아래와 같이 동일한 규칙으로 선언합니다.

int x[2][3] = {{1, 2, 3,}, {4, 5, 6}};
int(*p)[2][3] = &x;

 

배열 포인터를 2차원 배열처럼 사용하고 싶을 때는 1차원 배열 포인터를 사용하면 됩니다.

 

예를 들어, 아래와 같이 2차원 배열 x가 있습니다. 그리고 1차원 배열 포인터 p를 x로 초기화합니다.

#include <stdio.h>

int main()
{
    int x[2][3] = {{1, 2, 3,},
                   {4, 5, 6}};
    int(*p)[3] = x;
}

2차원 배열의 배열명 x는 첫 번째 요소의 주소를 가리킵니다. 2차원 배열의 첫 번째 요소는 1차원 배열이고 위의 예시에서 { 1, 2, 3}에 해당합니다. 따라서 1차원 배열 포인터 p를 초기화하는 게 가능합니다.

 

따라서 아래 코드와 같이 p를 이용하여 2차원 배열에 접근하는 게 가능합니다.

#include <stdio.h>

int main()
{
    int x[2][3] = {{1, 2, 3,},
                   {4, 5, 6}};
    int(*p)[3] = x;
    for (int i  = 0; i < 2; ++i)
    {
        for (int j = 0; j < 3; ++j)
        {
            printf("%d ", p[i][j]);
        }
        printf("\n");
    }
}

 

 

이를 활용하면 동적할당 받은 배열을 2차원 배열처럼 사용할 수 있습니다.

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int(*p)[3] = (int(*)[3])malloc(2 * 3 *sizeof(int));
    
    int a = 1;
    for (int i  = 0; i < 2; ++i)
    {
        for (int j = 0; j < 3; ++j)
        {
            p[i][j] = a++;
        }
    }

    for (int i  = 0; i < 2; ++i)
    {
        for (int j = 0; j < 3; ++j)
        {
            printf("%d ", p[i][j]);
        }
        printf("\n");
    }
}

 

또, 이를 활용하여 함수에서 2차원 배열을 인자로 받는 것도 가능합니다.

#include <stdio.h>

void foo(int(*p)[3])
{
    for (int i  = 0; i < 2; ++i)
    {
        for (int j = 0; j < 3; ++j)
        {
            printf("%d ", p[i][j]);
        }
        printf("\n");
    }
}

int main()
{
    int x[2][3] = {{1, 2, 3},
                   {4, 5, 6}};
    foo(x);
}

 

반응형