[C, C++] 배열(Array)은 포인터(Pointer)가 아니다!

인트로

배열을 포인터라고 알고 있거나 배열의 이름이 포인터 내지는 포인터 상수라고 알고 있는 경우가 많다. 

하지만 놀랍게도 배열은 포인터가 아니다!

 

배열은 배열이고 포인터는 포인터일 뿐이다. 다만 대부분의 코드에서 배열의 이름이 포인터로 변환되기에 배열의 이름 = 포인터라고 알고 있는 것 같다.

 

배열을 포인터로 나타낼 수 있는 이유는 C와 C++의 암시적인 변환(Decay) 정책에 의해 가능한 일이다.

 

Decay란?
암시적으로 배열이 포인터로 변환하는 과정에서 배열의 타입과 크기를 잃어버리는 것을 Decay라고 한다.

본 포스팅에선 Decay를 "변환"이란 단어로 대체했다.

 

배열과 포인터

여기 배열이 하나 있다.

int arr[3];

arr은 3개의 Integer를 담을 수 있는 메모리 공간이 있다. 그리고 우린 배열의 인덱스를 통해 값을 할당할 수 있다.

arr[2] = 1;

그리고 여기 포인터가 하나 있다.

int* ptr;

ptr은 3개의 Integer를 담을 공간이 없다. 단지 Integer의 메모리 주소를 가리킬 수 있다.

ptr은 arr의 한 Integer 공간을 가리킬 수 있을 뿐이다.

ptr = &arr[0];

우리를 혼란스럽게 하는 건 다음과 같이 코드를 작성해도 이전 코드와 동일하다는 것이다.

ptr = arr;

다만 확실한 건 다음과 같이 코드를 작성하는 것이 arr에 저장된 Integer들을 ptr에 복사한다는 의미는 아니란 것이다. 대신 배열의 이름 즉 arr이 배열의 첫 원소의 주소를 가리키는 포인터로 "변환"되고 이전 코드와 동일하게 동작되는 것이다.

 

이제 ptr을 마치 배열처럼 사용할 수 있다.

ptr[0] = 10;

이러한 작업이 가능한 이유는 C와 C++에서 배열의 역참조 연산자인 [ ]가 포인터에 기반해 정의되었기 때문이다.

arr[i]가 의미하는 뜻은 포인터 arr부터 시작해서 arr이 가리키는 것보다 i 원소만큼 이동해서 값을 가져오라는 의미이다. 

 

예를 들어 아래의 코드가 동작하는 일부 과정을 설명하면 배열의 이름인 arr은 반드시 포인터로 변환되어야 한다.

그다음 arr이 가리키는 곳에서 2개의 원소만큼 이동해서 값을 가져오게 된다.

 arr[2] = 1;

 

배열은 포인터가 아니다

정리하면 C와 C++ 기준으로 대부분의 상황에서 배열의 이름은 포인터로 변환된다. 이러한 이유로 배열 = 포인터로 알고 있는 경우가 많은 것 같다.

하지만 이를 반박하듯 몇 가지 예외적인 상황이 존재한다. 1)sizeof 연산자 또는 2)& 연산자의 피연산자로 배열의 이름이 사용될 경우 배열의 이름은 포인터로 변환되지 않는다.

 

 

1) sizeof를 사용할 때 배열이 포인터로 변환된다면 sizeof(arr)은 배열의 실제 크기가 아닌 포인터의 크기를 반환할 것이다. 하지만 다음 코드를 살펴보면 실제 결과는 그렇지 않다. sizeof를 사용하게 되면 배열의 전체 크기를 반환하는 것을 알 수 있다.

#include <iostream>

using namespace std;

void foo(int* arr)
{
	cout << "foo arr size : " << sizeof(arr) << endl;
}

int main(void)
{
	int arr[3] = { 1, 2, 3 };

	cout << "main arr size : " << sizeof(arr) << endl;

	foo(arr);
}

 

 

2) &arr과 arr은 같은 값을 가지지만 &arr과 arr은 데이터 타입이 다르다.

arr은 int* 타입이고 &arr은 int (*)[3] 타입이다. 

즉, &연산자에 사용된 배열의 이름이 포인터로 변환됐다면 &arr의 결과 값이 int (*)[3]이 될 수 없음은 자명한 사실이다.

#include <iostream>

using namespace std;

int main(void)
{
	int arr[3] = { 1, 2, 3 };
	int* p = arr;

	cout << "p : " << p << endl;
	cout << "typeof p : " << typeid(p).name() << endl << endl;

	cout << "&arr : " << &arr << endl;
	cout << "typeof &arr : " << typeid(&arr).name() << endl;
}

 

 

조금 더 설명하면 배열의 이름 arr이 배열의 시작 원소를 가리킨다면 &arr은 배열 자체를 가리키는 포인터라고 할 수 있다.

https://www.log2base2.com/C/pointer/arr+1-vs-address-of-arr+1.html

 

실제로 arr + 1 연산의 결괏값은 Integer형 포인터 변수의 크기(4)만큼 증가하지만

&arr + 1은 전체 배열의 크기(12)만큼 증가하게 된다.

#include <iostream>

using namespace std;

int main(void)
{
	int arr[3] = { 1, 2, 3 };

	cout << "배열의 이름 arr : " << arr << endl;
	cout << "&(배열의 이름) &arr : " << &arr << endl;
	cout << "첫 번째 원소의 주소 &arr : " << &arr[0] << endl << endl;

	cout << "arr : " << arr << endl;
	cout << "arr + 1 : " << arr + 1 << endl;
	cout << "&arr + 1 : " << &arr + 1 << endl;
}

 

 

따라서 sizeof와 & 연산자에 배열의 이름이 사용된다면 배열의 이름은 다수의 데이터를 저장하는 배열 그 자체일 뿐 결코 포인터가 아니다.

 

결론

배열은 배열일 뿐 포인터와 다르다.

많은 상황에서 배열이 포인터로 변환되지만 배열과 포인터는 다른 타입이며 두 타입은 컴파일러에게 다르게 처리된다.