2 분 소요

배열 포인터

형식

타입 (* 변수명)[크기] 로 지정

  • 어떤 의미로는 함수 포인터와 유사하다
    반환타입 (*변수명)(인자목록)

의미

타입 * 크기 만큼의 Size를 갖는
‘배열’을 가리키는 포인터

  • 이전에 말하였듯
    포인터는 무언가를 가리키는 주소값이며
    그 가리키는 위치를 얼마만큼 읽어야 하는지에 대한
    타입(크기 값 추정)이 있어야 한다

포인터 크기 자체는 void* 처럼
다른 포인터 크기와 같다
(32 : 4Byte, 64 : 8Byte)

예시

int arr[3] = { 100, 200, 300 };
int (*ptr)[3] = &arr; // 배열 포인터 선언

// 배열 포인터를 이용하여 배열 요소 접근
cout << "(*ptr)[0]: " << (*ptr)[0] << endl; // 100
cout << "(*ptr)[1]: " << (*ptr)[1] << endl; // 200
cout << "(*ptr)[2]: " << (*ptr)[2] << endl; // 300

포인터 연산 시

포인터는 +1이나 ++을 하면
자신이 가진 Type의 크기만큼 이동한다

따라서 배열 포인터는
선언한 타입 x 크기 만큼 이동

배열 포인터의 특징

  • 다차원 배열의 특정 행을 포인터로 표현이 가능하기에
    타입 크기를 정확히 알 수 있음
    (ex : void func(int (p)[4]) → p는 한 행이 int 4개로 구성된 배열 포인터)
    (void func(int arr[3])은 실제로는 int
    로 변환되기에 실제로는 위험하다)

  • 배열 크기를 정확히 구할 수 있기에
    (타입 x 길이)
    다른 배열과 잘 호환되지 않음
    (int (*p)[3] = &arr4; → arr4가 int[4]이면 컴파일 에러)

다만… 주의할 점이 있다
말그대로 ‘포인터’ 이기에
실제 함수로 전달되었을 땐
이게 1차원 배열이 전달되었는지 2차원 배열이 전달되었는지
판단하기 힘들다!


int arr[3] = { 100, 200, 300 };
int (*ptr)[3] = &arr; // 배열 포인터 선언

func(ptr);

void func(int (*p)[3])
{
   int x = p[0][0]; // 100에 접근
}

위 예시에서 보면 알겠지만 이 뉘앙스는 사실

int b = 10;
int* a = &b;

func(a);

void func(int* arr)
{
  int x = arr[0]; // 10에 접근
}

과 매우 유사하다

그렇기에 실제로는

int arr[2][3] = {1,2,3,4,5,6};
int (*ptr)[3] = arr; // 첫번째 행을 가리킴

or

int (*ptr)[2][3] = &arr; // [2][3]을 가리키지만 [0][0][0] 등으로 가리키거나 (*ptr)[0] 등으로 써야 한다

이것처럼 그냥 그 ‘크기’의 배열을 가리킬 수 있는
‘포인터’처럼 인식해야 한다

예시처럼 (*ptr)[0] 등으로 사용하면
가독성이 괜찮기는 한데…

실제 사용처?

사실 이 포인터는 그닥… 사용되진 않는다
위에서 보았듯이 결국 ‘기존 포인터’와 비슷하게 동작하기도 하며
특유의 ‘복잡한 표현’ (함수 포인터와 비슷한) 때문에

딱히 사용할만한 메리트가 없다는 것…

매개변수로 전달 시
보통 이러한 방식이 더 가독성이 좋다

void(int* arr, size_t size) -> 아 arr이 배열이고 size가 그 사이즈를 넣으면 되겠구나
void(int arr[3]) -> 아 arr은 실제로 int* 이지만 그래도 배열 크기를 3이여야 겠구나

또한 C++에서는 STL 과 같은 컨테이너를
더 자주 사용하기에 그냥 stl 컨테이너를
const reference 타입으로 넘겨 받거나
필요에 따라 move 등을 쓰는 편이 더 효율적이고 안전할 것 같다

  • 동적 배열이라면 vector가 존재하며
    정적 배열이라면 array<T,N>을 사용하면
    더 가독성 좋고 안전하게 사용할 수 있다

댓글남기기