반응형

결론

 

“배열”이 포인터인 것이 아니라 배열은 배열이고, 포인터는 포인터인데 “배열의 이름”이 포인터로 변환되어 동작하는 것일 뿐, 배열과 포인터는 다른 타입이고 컴파일러 관점에서도 다르게 처리한다.

 


 

배열은 배열이고, 포인터는 포인터이다.

 

Array Decay(Array to Pointer Decay)는 배열이 암시적인 변환에 의해 포인터로 변환되어 처리되는 현상을 말하는데

배열은 배열이고, 포인터는 포인터이지만 실제로는 Array Decay로 인해 배열의 이름이 포인터로 변환되는 것이다.

(Decay : 배열의 크기 정보를 잃어버리고 포인터로 붕괴한다는 의미에서 유래됐다.)

 

즉, 배열의 이름이 포인터로 변환되어 동작하기 때문에 흔히 배열의 이름은 포인터라고 말하는데

결론적으로 말하자면 무조건 완전히 틀린 말이라고 할 수는 없겠지만

과정적으로 엄밀히 말하자면 배열의 이름이 포인터로 변환하여 동작하는 것이기 때문에 배열은 배열이고 포인터는 포인터인 것이다.

 


 

비유적으로 표현하자면 배열을 아파트에 비유하자면, 포인터는 아파트의 주소이다.

 

예시를 들어 아파트 면에서 말하자면 몇 층 몇 층 이런식으로 표현하지만

포인터 면에서 말하자면 어떠한 번지 수로부터 얼마큼의 거리가 떨어져 있는 곳 이런식으로 표현한다.

 

배열은 인덱스를 지정해 값을 할당할 수 있지만, 포인터는 그저 메모리 주소를 가리킬 수 있을 뿐이다.

int arr[5]; // int형 배열 arr 선언
arr[4] = 5; // int형 배열 arr의 4번째 인덱스에 1을 넣는다.

int *ptr1 = &arr[0]; // 포인터 ptr1은 arr[0]의 주소를 가리킬 뿐이다.
int *ptr2 = arr; // 바로 윗줄의 코드와 동일한 의미이다.

 

위의 코드에서는 int형 포인터 ptr1, ptr2를 따로 선언하여 arr의 주소를 가리키게 했다.

 

위와 같이 arr의 주소를 가리키게 했을 경우 아래와 같이 사용할 수 있다.

ptr[0] = 1;

 

즉, 포인터를 마치 배열처럼 사용하는 것이다.

 

하지만 따로 포인터를 선언하여 배열의 주소를 지정하는 작업을 하지 않아도 이미 우리는 포인터를 배열처럼 사용하고 있다.

 

배열의 이름이 배열의 첫 번째 원소의 주소를 가리키는 포인터로 변환되어 동작하기 때문이다.

 

아래의 코드를 보면 배열의 이름으로 인덱싱하는 것과 포인터를 선언해 배열의 주소를 지정한 후 인덱싱하는 코드인데 이 둘은 차이가 없다.

 

포인터를 선언하여 배열의 주소를 지정하는 경우 배열의 이름 arr 대신 ptr이라는 별칭을 사용하겠다 라는 의미와 비슷하다고 볼 수 있다.

int arr[5]; // int형 배열 arr 선언
arr[1] = 2; // 배열 arr의 시작 주소로부터 4byte 떨어진 곳에 2넣는다.

int *ptr = arr; // int형 포인터 ptr에 배열 arr의 주소를 지정
ptr[2] = 3; // 배열 arr의 시작 주소로부터 8byte 떨어진 곳에 3을 넣는다.

 

포인터를 선언해 배열의 주소를 지정했을 때 인덱싱이 가능한 이유는 배열의 역참조 연산자인 “ [ ] “가 포인터에 기반해 정의되어있기 때문이다.

 


배열과 포인터의 확실한 차이

 

아래 5가지 경우에는 배열의 이름이 포인터로 변환되지 않고, 포인터가 아닌 배열 그 자체를 의미한다.

  • sizeof() 연산자를 사용했을 때
  • & 연산자를 사용했을 때
  • 문자 배열을 선언하여 문자열로 초기화 할 때
  • C11에서 alianof를 쓸 때
  • C23에서 typeof와 typeof_unqual를 쓸 때

 

sizeof()

sizeof()에 포인터를 인자로 주면 타입에 상관없이 시스템에 따라 4byte 또는 8byte로 일정한 값이 출력될 것이다.

#include <stdio.h>

int main(void)
{
	char * cp;
	short * sp;
	int * ip;
	float * fp;
	double * dp;
	
	printf("char* size : %d\n", sizeof(cp));
	printf("short* size : %d\n", sizeof(sp));
	printf("int* size : %d\n", sizeof(ip));
	printf("float* size : %d\n", sizeof(fp));
	printf("double* size : %d\n", sizeof(dp));

	return 0;
}

 

그렇다면 배열도 배열의 이름이 포인터로 변환되어 동작하므로 sizeof(arr)와 같이 배열의 타입 사이즈를 구하면 일반적인 포인터처럼 4byte, 8byte와 같은 값이 나와야 하지만 실제로는 배열의 전체 크기가 출력된다.

#include <stdio.h>

int main(void)
{
	short arr1[4] = {1,2,3,4};
	int arr2[5] = {1,2,3,4,5};

	printf("arr1 size : %d\n", sizeof(arr1)); // 2 * 4 = 8 출력
	printf("arr2 size : %d\n", sizeof(arr2)); // 4 * 5 = 20 출력

	return 0;
}

 

& 연산자

&arr와 arr는 같은 값을 가지지만 이 둘의 데이터 타입은 다르다.

 

int형 arr를 선언한 뒤 arr는 int* 타입이지만, &arr는 int(*)[n] 타입이다.

(n은 원소의 개수이다.)

 

배열의 이름이 배열의 첫 번째 원소를 가리킨다면, & 연산자가 붙은 배열의 이름은 배열 전체 즉 데이터들이 쭉 나열되어 있는 그 공간 자체를 가리키는 포인터이다.

 

실제로 arr + 1 연산은 해당 배열의 타입에 따라 해당 타입의 크기 만큼 증가하지만

&arr + 1은 해당 배열의 타입 사이즈 즉, sizeof(arr) 했을 때의 크기인 배열의 전체 크기만큼 증가하게 된다.

#include <stdio.h>

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

	printf("int형 arr 배열의 주소 : %p\n", arr); // 배열의 이름이 가리키는 주소
	printf("int형 arr 배열의 첫 번째 원소의 주소 : %p\n", &arr[0]); // 배열의 첫 번째 주소 == 배열의 이름이 가리키는 주소
	printf("int형 &arr 주소 : %p\n", &arr); // 배열 자체의 주소
	printf("int형 arr + 1 : %p\n", arr + 1); // 배열의 이름 + 1(배열 자체의 주소 + 20)
	printf("int형 &arr + 1 : %p\n", &arr + 1); // 배열 자체의 주소 + 1

	return 0;
}

 

문자 배열을 선언하여 문자열로 초기화 할 때

#include <stdio.h>

int main(void)
{
	char str[] = "hello world";

	printf("str size : %d\n", sizeof(str));

	return 0;
}

 


함수의 인자

 

아래의 경우 func() 함수에 인자로 배열을 넘길 때에도 Array decay가 발생하는데

 

func() 함수에서 타입과 크기를 알려줬음에도 출력은 포인터의 크기로 출력이 된다.

#include <stdio.h>

void func(int func_arr[5])
{
	printf("func_arr size : %d\n", sizeof(func_arr));
}

int main()
{
	int arr[5] = {1,2,3,4,5};
	
	printf("arr size : %d\n", sizeof(arr));
	func(arr);
	return 0;
}

 

그렇기에 함수의 인자로 배열을 넘길 때에는

C에서는 배열의 크기를 받는 매개변수를 따로 선언하거나

C++에서는 참조자를 이용해 배열을 넘겨야 Array decay가 발생하지 않는다.

// C
#include <stdio.h>

void func1(int func1_arr[], int count)
{
	printf("func1_arr size : %d\n", count);
}

void func2(int *func2_arr, int count)
{
	printf("func2_arr size : %d\n", count);
}

int main()
{
	int arr[5] = {1,2,3,4,5};
	
	printf("arr size : %d\n", sizeof(arr));
	func1(arr, 20);
	func2(arr, 20);
	return 0;
}

 

// C++
#include <iostream>

void func(int (&func_arr)[5])
{
	cout << "func_arr size : " << sizeof(func_arr) << endl;
}

int main()
{
	int arr[5] = {1,2,3,4,5};
	
	cout << "arr size : " << sizeof(arr) << endl;
	func(arr);
	return 0;
}

 

추가로 함수의 인자로 배열을 넘길 때 함수의 매개변수는 아래의 3가지 표기법이 있는데 포인터를 이용한 선언을 추천한다.

func(int arr[]);
func(int arr[5]);
func(int * arr);

 


https://stackoverflow.com/questions/17752978/exceptions-to-array-decaying-into-a-pointer

 

https://int-i.github.io/cpp/2020-04-12/array-deacy/

 

https://kangworld.tistory.com/50?category=955881

반응형

+ Recent posts