본문 바로가기

별걸다하는 IT/프로그래밍언어

[C언어 C++ 강좌] 포인터배열과 배열포인터, 배열포인터 선언방식. 배열포인터는 2차원에만 존재?? ㄴㄴ

[프로그래밍 언어 C, C++ 강좌 목차]

안녕하세요 양햄찌 주인장입니다.

오늘 들고 온 주제는 포인터 배열과 배열 포인터예요.

포인터 배열, 배열 포인터 비슷하니 헷갈리죠?? 오늘 포스팅으로 확실히 정리해봅시다.

 

무우울론~~~ 사전에 배우는 배열과 포인터에 대한 개념은 꽉 잡고 있어야해요!

기본기가 흔들린다 하시는 분들은 목차가서 꼭 사전학습하고 보기~!

포인터 배열이란? Pointer Array

포인터의 배열. 뒤의 단어가 핵심입니다! 배열배열배열!! 앞의 포인터는 어떤 배열인지 설명해주는 수식어에 불과해요.

배열은 연속적으로 공간을 여러개 할당하는 걸 배열이라 하잖아요?

포인터 포인터 포인터.. 이렇게 포인터가 배열로 있는 것을, 포인터 배열이라고 합니다.

우리가 알고 있는 배열과 동일해요.

반면 미리 말하자면, 배열 포인터는 배열의 포인터, 즉 배열을 가리키는 포인터를 의미해요.

포인터 배열 

int ary[3]는 3개의 정수형 공간을 일렬로 할당하는 배열을 선언하는 코드죠.

float ary[2]는 2개의 실수형 공간을 일렬로 할당하는 배열을 선언하는 거예요.

그럼 정수나 실수가 아닌, 타입이 포인터인 배열을 선언하고 싶을 경우 어떻게 해야할까요?

 

똑같습니다! 앞에 타입을 적어주시고. 그 다음 배열명과 공간의 크기를 []연산자 안에 명시해주면 돼요

int* ptrary[3]이나, float* ptrary[2] 이런 선언 방식이 되겠죠?

 

간단하게 확인을 위한 테스트를 진행해볼게요.

#include <iostream>
using std::cout;
using std::endl;
int main()
{
    int a = 10, b = 20, c = 30;
    int* ptrary[3] = { &a, &b, &c };
    for (int index = 0; index < _countof(ptrary); index++) 
        cout << index << "원소 :" << ptrary[index] << "\t역참조 :" << *ptrary[index] << endl;
    return 0;
}

3개짜리 포인터 배열을 선언 및 초기화 해준 후, 배열의 각 원소 값과 그 값에 저장된 주소를 역참조한 값을 출력해봤어요.

주소와 값이 잘 출력되는 것을 확인하실 수 있습니다.

 

문득, 배열의 원소에서 역참조를 할때 *(ptrary[index])이렇게 괄호쳐줘야 하는거 아니예요? 생각할 수도 있어요.

다행이(?)  [ ]대괄호 연산자가 포인터 연산자(*)보다 우선순위가 높습니다.

그래서 *ptrary[index]이렇게 그냥 써줘도 *(ptrary[index]) 한 것과 동일해요.

 

뭐,, 배열이란 워낙 앞에서 배웠던 것이고 익숙해서 이해하기 쉬워요.

그럼 포인터배열과 많이 비교되는 배열포인터는 무엇일까요??

배열 포인터란? Array Pointer

역시 뒤가 중심. 포인터 포인터 포인터입니다!

근데 정수 포인터, 실수 포인터, 함수를 가리키는 함수 포인터 처럼 배열 포인터는 배열을 가리키는 포인터를 의미해요.

포인터 배열이 공간을 특정 개수만큼 잡았다면, 배열 포인터의 포인터이기 때문에 주소를 저장하는 공간 하나가 할당되는거죠. 

 

배열을 가리키는 포인터를 그럼 하나 선언하려면 어떻게 해야할까요?

'int[3] *aryptr' 하면 될까요? ㄴㄴ

요렇게 선언해주면 됩니다. 포인터변수명 뒤에 []가 붙었어요. 앞에 타입과 합쳐서 int[3] 즉 정수형 3공간을 가지는 배열을 가리키는 포인터 aryptr! 이렇게 생각하시면 됩니다.

 

주의해야할 점은, 여기서 괄호를 빼먹으면 안돼요!!

괄호를 빼먹어버리면 int * aryptr[3]으로 위에서 살펴보았던 포인터배열이 되버립니다. ㅎㅎ

앞서 말했듯이 대괄호 연산자( [ ] )는 포인터연산자보다 우선순위가 높으니, 배열포인터를 선언할 때에는 괄호를 꼭 표기해주기!

 

[배열포인터 선언 및 값 체크]

이번에도 간단하게 배열포인터를 코드를 통해 확인해봅시다.

#include <iostream>
using std::cout;
using std::endl;
int main()
{
	int arr[3] = { 1,2,3 }; //1차원배열.
	int(*aryptr)[3] = &arr;  //포인터니까 배열의 주소값을 넣어줌.

	cout << "arr\t" << arr << endl;
	cout << "aryptr\t" << aryptr << endl;

	cout << "sizeof(arr)\t" << sizeof(arr) << endl;
	cout << "sizeof(*aryptr)\t" << sizeof(*aryptr) << endl;
	return 0;
}

1차원 배열 하나를 선언및초기화 해준 후 배열포인터에다가 대입해봤어요.

1차원 배열 arr의 시작주소는 004FFD8C!

arr을 가리키는 배열포인터 aryptr에 저장된 값 역시 004FFD8C!

 

얘네 둘이 정말 같은지 확인하려면 당근 타입의 크기를 확인해야겠죠?

둘다 체크해보니 12바이트, integer 3개를 가리키는 배열이니까 4*3 = 12바이트! 배열을 가리키는게 역시 맞네요!

 

[배열포인터 역참조값 확인해보기]

#include <iostream>
using std::cout;
using std::endl;
int main()
{
	int arr[3] = { 1,2,3 }; //1차원배열.
	int(*aryptr)[3] = &arr;  //포인터니까 배열의 주소값을 넣어줌.

	cout << "aryptr\t" << aryptr << endl;
	cout << "*aryptr\t" << *aryptr << endl; 

	//둘이 같아보이지만 달라! sizeof로 타입을 확인해보자.
	cout << sizeof(*aryptr) << endl;
	cout << sizeof(**aryptr) << endl;
	return 0;
}

그럼 배열을 가리키는 포인터의 역참조 값은 무엇일까요? 

aryptr는 주소를 저장하는 포인터이니 역참조한 결과는 배열을 가리켜야할 거예요.

배열의 이름은 배열의 시작주소였잖아요. 따라서 역참조 결과는 배열의 시작주소입니다.

그럼 aryptr값과 *aryptr값이 동일한게 맞냐고요?! ㄴㄴ 그렇지 않아요 이 둘은 타입이 다릅니다.

우리,, 이전 배열과 포인터의 관계 포스팅에서 배웠듯, 배열의 시작주소는 4바이트를 가리키는 포인터 상수라 생각할 수 있다했었죠. aryptr은 배열을 가리키니까 aryptr의 사이즈는 12, 역참조한 배열의 이름은 크기가 4가 됨을 확인할 수 있어요.

 

[배열포인터로 배열의 각 원소에 접근하기]

배열포인터는 역참조를 통해 결국 배열을 가리키니, 배열의 원소에 또한 접근할 수 있습니다.

#include <iostream>
using std::cout;
using std::endl;
int main()
{
	int arr[3] = { 1,2,3 }; //1차원배열.
	int(*aryptr)[3] = &arr;  //포인터니까 배열의 주소값을 넣어줌.

	cout << arr << endl;	// 배열의 시작주소.
	cout << *aryptr << endl;  // 배열의 시작주소.

	cout << arr[0] << endl;		//arr 첫번째 원소 (인덱스: 0)
	cout << (*aryptr)[0] << endl;

	cout << arr[1] << endl;		//arr 두번째 원소 (인덱스: 1)
	cout << (*aryptr)[1] << endl;

	cout << arr[2] << endl;		//arr 세번째 원소 (인덱스: 2)
	cout << (*aryptr)[2] << endl;	

	return 0;
}

arr이나 *aryptr이나 결국 가리키는게 같으니 arr[i]는 (*aryptr)[i]로 생각해도 되겠네요?!

넵 맞습니다. ㅎㅎ  가끔 배열포인터가 2차원에서만 가능하다고 생각하고 계시는 분이 있는데, 배열포인터가 배열을 가리키는 것인만큼 1차원 배열또한 가리킬 수 있어요.

 

[배열포인터 2차원배열 연관시켜 생각해보기]

그런데 잠깐! 문득 생각해보니 우리 *(ptr + i ) = ptr[i] 이 될 수 있다 하지 않았었나요??

그럼 *aryptr은 aryptr[0]로 확장해서 생각해볼 수 있을까요~?

근데 저기 ptr은 int 포인터변수였을 때였고(int*)

저기 aryptr은 확인했다시피, 크기가 배열 전체인 12바이트 포인터 변수 입니다. (int[] *)

즉 aryptr을 저렇게 배열처럼 쓰려면 12바이트짜리 타입의 공간이 여러개 있는 배열이다라고 생각해야 맞는 것이죠.

 

그래서 사실 재밌는 트릭을 한 번 살펴보자면,

#include <iostream>
using std::cout;
using std::endl;
int main()
{
	int ary2d[2][3]{ 1,2,3,10,20,30 };
	cout << "ary2d 배열이름 값:\t\t" << ary2d << endl;
	cout << "ary2d 배열이름 타입크기:\t" << sizeof(*ary2d) << endl;
	cout << endl;

	int(*aryptr)[3] = ary2d;
	cout << "aryptr 위치:\t" << &aryptr << endl;
	cout << "aryptr 값:\t" << aryptr << endl << endl;

	cout << "ary2d:\t\t" << ary2d << endl;
	cout << "ary2d+1:\t" << ary2d + 1 << endl;
	cout << (size_t)(ary2d + 1) - (size_t)(ary2d) << endl << endl;

	cout << "ary2d + 1:\t\t" << ary2d + 1 << endl;
	cout << "*(ary2d + 1):\t\t" << *(ary2d + 1) << endl;
	cout << "sizeof(*(ary2d + 1)):\t" << sizeof(*(ary2d + 1)) << endl;
	cout << "sizeof(**(ary2d + 1)):\t" << sizeof(**(ary2d + 1)) << endl;
	cout << "(*(ary2d + 1))[0]:\t" << (*(ary2d + 1))[0] << endl << endl;

	int(*arr)[3] = ary2d + 1;
	cout << "*arr:\t\t" << *arr << endl;
	cout << "(*arr)[0]:\t" << (*arr)[0] << endl;
	return 0;
}

2행 3열의 2차원 배열 ary2d를 위와 같이 정의했을 때 각각 출력이 어떻게 진행될지 추측해봅시다.

또 결과값을 통해 2차원 배열이 어떻게 메모리에 구성되는지 상상해보세요! 

요렇게~~ 그림으로 그려보면 한결 더 쉽게 이해하실 수 있답니다.

 

[2차원 배열 포인터로 2차원배열 접근하기]

#include <iostream>
using namespace std;
int main()
{
	int arr[2][3] = { 1,2,3,4,5,6 };
	cout << arr << endl;
	cout << &arr << endl;
	cout << sizeof(*arr) << endl;		//12
	cout << sizeof(arr) << endl <<endl;	//24

	int(*ptr)[2][3]  = &arr;
	cout << sizeof(*ptr) << endl;		//24 
	cout << (*ptr)[0] << endl;			//*ptr의 원소 접근, arr의 원소인 행배열 접근 
	cout << *(*ptr)[0] << endl;			//행배열에서 원소 접근. 
	cout << sizeof(*(*ptr)[0]) << endl;	//4바이트 

	return 0;
}

뭐 이런식으로 확인해볼 수 있겠죠?! 처음에 개념잡기가 어렵지만 한 번 잡아놓으면 아~~! 할 수 있답니다.

 

오늘은 간단하게 포인터배열과 배열포인터에 대해 정리해보았어요.ㅎㅎ

공감/댓글/광고보답은 더 나은 정보공유를 위해 힘쓰는 작성자에게 큰 동기부여가 됩니다!

다음 포스팅에서 또 봐요~ 오늘하루도 화이팅!