포인터와 배열

배열의 이름을 포인터처럼 사용할 수 있으며 포인터를 배열의 이름처럼 사용할 수 있습니다.
즉, 배열의 이름이 주소로 해석되며, 배열의 첫 번째 요소의 주소와 같습니다.

배열의 이름을 포인터처럼 사용하는 예시는 다음과 같습니다.

int arr[3] = {1, 2, 3};

std::cout << arr[0] << arr[1] << arr[2] << std::endl;
std::cout << *(arr+0) << *(arr+1) << *(arr+2);

배열의 인덱스를 각각 접근했을 때와 배열의 이름을 포인터처럼 사용했을 때 출력값은 각각 같습니다.
메모리에 접근을 하고, 배열의 자료형으로 메모리를 해석하기 때문에 같은 값이 출력됩니다.

다음과 같은 공식이 성립하며 다차원 배열에서도 성립합니다.

// arr이 배열의 이름이거나 포인터이고, n이 정수라면
arr + n == &arr[n];
arr[n] == *(arr + n);

포인터를 배열의 이름처럼 사용하는 예시는 다음과 같습니다.

Pointer_Array

포인터를 배열의 이름처럼 사용했을 때와 배열의 인덱스를 각각 접근했을 때 같은 값이 출력 됩니다.
그 이유는 같은 메모리에 접근하고, 같은 자료형으로 해석하기 때문입니다.

포인터와 2차원 배열

C++에서 2차원 배열은 단순한 2차원 구조라기보다 배열의 배열이라는 것을 이해하는게 중요합니다.

예를 들어 다음 배열이 있다고 하겠습니다.

int matrix[3][3] = 
{
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

이때 matrix의 배열 타입 자체는 int [3][3]입니다.
즉, int[3]타입의 배열 3개를 원소로 가지는 배열입니다.

다만 식에서 matrix를 사용하면 대부분의 경우 배열은 첫 번째 원소를 가리키는 포인터로 변환되며, 이 경우 타입은 int (*)[3]가 됩니다.
즉, int 3개짜리 배열을 가리키는 포인터로 해석됩니다.

2차원 배열은 메모리가 연속적이라고 해도, 타입은 단순한 int* 또는 int**가 아닙니다.

다음 코드는 타입이 맞지 않아 오류가 발생합니다.

int* p = matrix;    // 타입 불일치
int** pp  = matrix; // 타입 불일치
  • matrix는 포인터로 변환(decay/array-to-pointer decay)되면 int (*)[3]
  • int*int하나를 가리키는 포인터
  • int**int*를 가리키는 포인터

하지만, 다음 코드는 가능합니다.

int* p = &matrix[0][0];
int* p2 = *matrix; // &matrix[0][0] 로 해석

matrix는 첫 번째 행을 가리키는 포인터처럼 동작하고, *matrix는 첫 번째 행(matrix[0])을 의미합니다.
그리고 matrix[0]도 식에서 사용되면 int*로 변환되므로 &matrix[0][0]와 같은 의미가 됩니다.

다음 아래 코드는 같은 주소값을 출력합니다.

std::cout << &matrix[0][0] << std::endl; // 같은 주소 출력
std::cout << *matrix << std::endl;       // 같은 주소 출력
std::cout << matrix << std::endl;        // 같은 주소 출력

이유는 다음과 같습니다.

  • &matrix[0][0]는 첫 번째 원소의 주소입니다.
  • *matrix는 첫 번째 행(matrix[0])이며, 식에서 int*로 변환되어 첫 번째 원소의 주소처럼 사용됩니다.
  • matrix는 첫 번째 행을 가리키는 포인터(int (*)[3])로 변환됩니다.

세 값은 숫자로 표현한 시작 주소는 같지만, 타입과 의미는 서로 다릅니다.

2차원 배열의 모든 원소는 메모리상에 연속적으로 저장됩니다.
따라서 첫 번째 원소의 주소를 int*로 받아 전체를 1차원 배열처럼 순회하는 것은 가능합니다.

int* ptrMat = &matrix[0][0];

std::cout << "1차원 배열처럼 출력 " << std::endl;
for (int i = 0; i < 9; i++)
{
    if (0 == (i % 3))
    {
        std::cout << '\n';
    }
    std::cout << ptrMat[i] << ", ";
}

이렇게 사용하는 방식은 배열 전체가 연속 저장된다는 점을 이용하는 것이지, matrix 자체의 타입이 int*라는 뜻은 아닙니다.

2차원 배열을 행 단위로 이동하는 예시는 다음과 같습니다.

int* p00 = *matrix; // matrix[0][0] 의 주소
int* p10 = *(matrix + 1); // matrix[1][0] 의 주소
int* p20 = *(matrix + 2); // matrix[2][0] 의 주소

여기서 matrix + 1은 다음 int로 이동하는 것이 아니라, 다음 행(int[3])으로 이동합니다.
즉, matrixint (*)[3] 타입이므로 1 증가할 때마다 int 3개 크기만큼 이동합니다.

아래 코드는 2차원 배열 전체를 1차원 배열처럼 순서대로 접근하는 것처럼 보이는 코드입니다.

(*matrix)[0] = 100; // matrix[0][0] = 100;
(*matrix)[1] = 200; // matrix[0][1] = 200;
(*matrix)[2] = 300; // matrix[0][2] = 300;
(*matrix)[3] = 400; // matrix[1][0] = 400;
(*matrix)[4] = 500; // matrix[1][1] = 500;
(*matrix)[5] = 600; // matrix[1][2] = 600;
(*matrix)[6] = 700; // matrix[2][0] = 700;
(*matrix)[7] = 800; // matrix[2][1] = 800;
(*matrix)[8] = 900; // matrix[2][2] = 900;

2차원 배열의 원소들이 메모리상에 연속해서 저장되기 때문에 접근이 가능합니다.
하지만, 타입 기준으로 봤을 때 *matrix는 첫 번째 행 하나만 의미하는 것이고, (*matrix)[3]는 첫 번째 행에서 벗어나는 범위를 접근하는 것입니다.
이 코드는 메모리상에서는 동작할 수 있지만, C++ 표준에서는 정의되지 않은 동작(Undefined Behavior)이며 일반적으로 좋은 코드라고 할 수 없습니다.

다음 코드는 2차원 배열을 가리키는 포인터입니다.

int (*pMarix2)[3] = matrix2; // 행을 가리키는 포인터
int* pMarix22[3] = matrix2; // 포인터 3개짜리 배열

두 선언은 완전히 다릅니다.

  • 첫 번째 선언은 2차원 배열용 포인터입니다.
  • 두 번째 선언은 int* 3개를 가진 배열입니다.

Date:     Updated:

카테고리:

태그:

Cpp 카테고리 내 다른 글 보러가기

댓글남기기