본문 바로가기

C++/C, C++, STL

이중 포인터와 동적 다차원 배열에 대한 이해

이중 포인터(Double pointer)란?

이중 포인터는 포인터에 대한 포인터(Pointer to Pointers)입니다.

포인터가 변수(Variables)의 주소 값을 저장한다면 이중 포인터는 포인터의 주소 값을 저장합니다.

int a = 10;
int* ap = &a;
int** app = ≈

std::cout << a << std::endl;
std::cout << *ap << std::endl;
std::cout << **app << std::endl;
코드 비고
Line 2:3 int* ap = &a 변수 a의 메모리 주소 값을 가리킵니다.
int** app = &ap 변수 ap의 메모리 주소 값을 가리킵니다.
Line 5:7 a 출력되는 값은 모두 변수 a에 저장된 10입니다.
*ap
**app

앞서 소개한 것처럼 포인터가 변수의 주소 값을 저장합니다.

마찬가지로 포인터 역시 변수이기 때문에 메모리로부터 주소 값을 할당 받습니다.

이중 포인터는 이 주소를 가리킵니다.

더보기

이중 포인터 또한 변수이기 때문에 주소 값을 갖습니다.

이중 포인터의 주소 값을 저장하는 포인터 변수를 만들면 이를 삼중 포인터라고 부릅니다. 하지만 사람이 이해하기 어려운 다차원 구조인데다가 보통은 이중 포인터로 충분해 사용되지는 않습니다.

배열과 포인터

C++에서 정적 배열을 선언하는 구문은 다음과 같습니다.

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

배열 또한 변수이기 때문에 주소 값을 갖습니다. 따라서 배열을 포인터로 가리킬 수 있습니다.

이때는 정적 배열 선언 구문 대신 new 키워드로 동적 배열 선언을 사용해야합니다.

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

4바이트 크기의 int 타입을 가리키는 포인터에 + - 증감 연산을 하면 주소 값을 4씩 이동합니다(이 블로그의 문서: 포인터(Pointer)와 주소 연산자(&), 역참조 연산자(*) 그리고 주소의 증감 연산 처리 참고).

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

std::cout << "address is " << (arr + 0) << " and value is " << *(arr + 0) << std::endl;
std::cout << "address is " << (arr + 1) << " and value is " << *(arr + 1) << std::endl;
std::cout << "address is " << (arr + 2) << " and value is " << *(arr + 2) << std::endl;

프로그램을 실행하면 주소가 int 타입의 크기만큼 이동하는 것을 알 수 있습니다.

address is 0x7b6f10 and value is 1
address is 0x7b6f14 and value is 2
address is 0x7b6f18 and value is 3

이중 배열과 이중 포인터

배열을 포인터로 가리키는 것처럼 이중 배열도 이중 포인터로 가리킬 수 있습니다.

마찬가지로 new 키워드로 동적 배열 선언을합니다.

int** arr = new int[2][3] { { 1, 2, 3 }, { 4, 5, 6 }};

하지만 위와 같은 예시 코드는 컴파일 오류를 발생시킵니다.

a value of type "int (*)[3]" cannot be used to initialize an entity of type "int **"C/C++(144)

이중 배열에 대한 포인터는 다음과 같이 선언합니다.

int (*arr)[3] = new int[2][3] { { 1, 2, 3 }, { 4, 5, 6 }};
더보기

C++ 11 버전부터는 auto 키워드를 사용하여 단순화 할 수 있습니다.

auto arr = new int[2][3] { { 1, 2, 3 }, { 4, 5, 6 }};

이중 배열을 가리키는 이중 포인터의 사용은 좀 더 복잡합니다.

int (*arr)[3] = new int[2][3] { { 1, 2, 3 }, { 4, 5, 6 }};

std::cout << "address is " << (*(arr + 0) + 0) << "and value is " << *(*(arr + 0) + 0) << std::endl;
std::cout << "address is " << (*(arr + 0) + 1) << "and value is " << *(*(arr + 0) + 1) << std::endl;
std::cout << "address is " << (*(arr + 0) + 2) << "and value is " << *(*(arr + 0) + 2) << std::endl;

std::cout << std::endl;
std::cout << "address is " << (*(arr + 1) + 0) << "and value is " << *(*(arr + 1) + 0) << std::endl;
std::cout << "address is " << (*(arr + 1) + 1) << "and value is " << *(*(arr + 1) + 1) << std::endl;
std::cout << "address is " << (*(arr + 1) + 2) << "and value is " << *(*(arr + 1) + 2) << std::endl;
코드 비고
Line 3 *(arr + 0) 외부 배열의 인덱스 0이 저장하는 값을 의미합니다. 이 값은 내부 배열의 주소입니다.
*(arr + 0) + 0 내부 배열의 주소에서 0번째 인덱스를 가리키는 주소로 이동합니다.
*(*(arr + 0) + 0) 마지막으로 이 주소의 값을 * 연산자로 접근하면 int[0][0]의 저장 값을 구할 수 있습니다.
Line 4 *(arr + 0) 외부 배열의 인덱스 0이 저장하는 값을 의미합니다. 이 값은 내부 배열의 주소입니다.
*(arr + 0) + 1 내부 배열의 주소에서 1번째 인덱스를 가리키는 주소로 이동합니다.
*(*(arr + 0) + 1) 마지막으로 이 주소의 값을 * 연산자로 접근하면 int[0][1]의 저장 값을 구할 수 있습니다.
Line 8 *(arr + 1) 외부 배열의 인덱스 1이 저장하는 값을 의미합니다. 이 값은 내부 배열의 주소입니다.
*(arr + 1) + 0 내부 배열의 주소에서 0번째 인덱스를 가리키는 주소로 이동합니다.
*(*(arr + 1) + 0) 마지막으로 이 주소의 값을 * 연산자로 접근하면 int[1][0]의 저장 값을 구할 수 있습니다.

정리 및 복습

  • 변수(Variables)는 메모리에 할당된 주소 값을 갖습니다.
  • 포인터(Pointer)는 변수가 사용하는 주소 값을 가리키는 변수입니다.
  • 포인터 역시 변수이기 때문에 주소 값을 갖습니다. 이중 포인터는 포인터의 주소 값을 가리키는 변수입니다.
  • 동적 배열은 포인터로 가리킬 수 있습니다.
int* arr = new int[3] { 1, 2, 3 };
  • 동적 이중 배열은 이중 포인터로 가리킬 수 있습니다. C++ 11 버전부터는 auto 키워드를 사용 할 수 있습니다.
int (*arr)[3] = new int[2][3] { { 1, 2, 3 }, { 4, 5, 6 }};