[C++] 포인터와 배열
포인터와 배열
배열의 이름을 포인터처럼 사용할 수 있으며 포인터를 배열의 이름처럼 사용할 수 있습니다.
즉, 배열의 이름이 주소로 해석되며, 배열의 첫 번째 요소의 주소와 같습니다.
배열의 이름을 포인터처럼 사용하는 예시는 다음과 같습니다.
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);
포인터를 배열의 이름처럼 사용하는 예시는 다음과 같습니다.
포인터를 배열의 이름처럼 사용했을 때와 배열의 인덱스를 각각 접근했을 때 같은 값이 출력 됩니다.
그 이유는 같은 메모리에 접근하고, 같은 자료형으로 해석하기 때문입니다.
포인터와 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])으로 이동합니다.
즉, matrix는 int (*)[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개를 가진 배열입니다.
댓글남기기