[C/C++] 배열명은 포인터가 아니다.
C++ 에서 &(앰퍼샌드)는 참조형? 주소?
족보를 보면서 자료구조 중간고사 공부 중 생긴 의문점. 다음은 의문점을 만들어준 코드이다.
- 다음 템플릿 함수를 완성하시오.
1
2
3
4
5
6
7
8
9
10
11
12
9. 다음 템플릿 함수를 완성하시오.
template <class T>
void ChangeSize1D( , const int oldSize, const int newSize)
{
if (newSize < 0) throw "New length must be >= 0";
T* temp = new T[newSize];
int number = min(oldSize, newSize);
copy(a, a + number, temp);
delete [] a;
a = temp;
}
저기 빈칸에 들어갈 정답이 챗 gpt한테 물어보니까 T*&a 라는데,
내가 알기로는 C언어에서 배열명은 곧 포인터.
그러니까 매개변수는 배열명만 적어도 되는 거 아닌가? 라는 의문점이 들었다.
도저히 이해가 안 돼서 폭풍 구글링 중 배열명만 적는 것은 함수 호출문에서 배열을 인자로 전달할 때나 배열명만 적는 거고, 정의문에 들어가는 매개변수에는 *(포인터)를 적거나, 배열을 의미하는 [ ]를 적어야 된다는 것을 망각했다..ㅎㅎ
아무튼, *을 적는 이유는 해결했고, 그럼 주소를 의미하는 &는 도대체 뭐임? 왜 씀??
구글링을 하다가 한 문서를 발견했다.
배열명은 포인터가 아니다?
C++ 07.15 - 참조형 변수 (Reference variable)
여기서 “C 스타일 배열에서 가장 귀찮은 문제 중 하나는 대부분이 평가될 때 포인터로 변환된다는 점이다. 그러나 C 스타일 배열을 참조로 전달하면 이러한 변환이 발생하지 않는다.” 라는 설명이 있는데, 코드 주석문에 이런 문장이 있다.
// we can now do this since the array won't decay
여기서 궁금점. decay가 뭐지?
바로 또 폭풍 구글링. 또 하나의 문서를 발견했다.
[C, C++] 배열(Array)은 포인터(Pointer)가 아니다!
포스팅의 제목이 눈에 확 들어왔다. ** 배열은 포인터가 아니다. **
????
나의 지식 체계에 혼돈이 왔다. 여태까지 배열명은 포인터라고 배우고 사용하고 있었는데, 사실 그게 아니라고?
글을 자세히 살펴보았다.
배열을 포인터라고 알고 있거나 배열의 이름이 포인터 내지는 포인터 상수라고 알고 있는 경우가 많다.
하지만 놀랍게도 배열은 포인터가 아니다!배열은 배열이고 포인터는 포인터일 뿐이다. 다만 대부분의 코드에서 배열의 이름이 포인터로 변환되기에 배열의 이름 = 포인터라고 알고 있는 것 같다.
배열을 포인터로 나타낼 수 있는 이유는 C와 C++의 암시적인 변환(Decay) 정책에 의해 가능한 일이다.Decay란?
암시적으로 배열이 포인터로 변환하는 과정에서 배열의 타입과 크기를 잃어버리는 것을 Decay라고 한다.
본 포스팅에선 Decay를 “변환”이란 단어로 대체했다.
이는 꽤나 충격적인 사실이었다. 언어가 배열명을 암시적으로 포인터처럼 사용할 수 있도록 변환을 시켜준 것이었구나.
배열명은 곧 배열의 첫 번째 인덱스의 주소를 가리키는 포인터로 변환된 것.
int* ptr = &arr[0]; 라고 했을 때, (ptr[2] = 1) == (arr[2] = 1)이 같은 의미인 이유는
배열의 역참조 연산자인 [ ]가 포인터에 기반하여 설계되었기 때문이다.
즉, 배열의 첫번째 원소의 주소이므로 arr[i]가 의미하는 것은 arr가 가리키는 것부터 i번째 원소만큼 이동해서 값을 가져오라는 것.
근데, 이렇게만 보면 배열명은 포인터를 의미한다는 말이랑 다를 것이 없다.
배열명이 포인터와 같지 않은 이유는 예외가 존재하기 때문이다.
1. sizeof() 연산자
sizeof 연산자의 피연산자로 배열을 받는다는 상황을 가정해보자. 만약 배열명이 항상 포인터로 평가된다면, sizeof 연산자로 배열명을 찍어보면 8바이트가 항상 나올 것이다. (포인터 변수의 크기는 8바이트)
하지만, 실제로 찍어보면 배열의 전체 크기를 반환하는 것을 알 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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는 포인터로 평가되어 int* 타입이지만,
&arr는 int (*)[3] 타입이다.
배열의 이름 arr는 배열의 시작 원소를 가리키지만, &arr는 배열 자체를 가리키는 포인터인 셈이다.
이미지 출처 : https://www.log2base2.com/C/pointer/arr+1-vs-address-of-arr+1.html
결론 : T*& a의 의미
이로써 모든 의문이 풀렸다.
a는 배열이다. 코드에 delete [] a; 가 있으니 a는 빼박 배열이다.
T는 템플릿이고, 사용자는 여기에 자료형을 입력할 것이다. 만약 a가 int형 배열이었다면, int*& a가 될 것이다.
*은 배열을 매개변수로 받기 위해 사용된 것이다. 배열명이 인자로 전달될 때, 대부분은 포인터로 평가되도록 decay되므로 a의 자료형은 T* 이 되는 것이다. 만약 *을 쓰기 싫었다면, T& a[] 이렇게 적어도 같은 의미가 될 것이다. 단지, gpt와 교안은 단지 포인터로 표현했을 뿐이다.
&는 참조자의 의미로 사용된 것이다. 포인터형 변수를 참조했을 뿐인 것이다.
혹시나 해서 강의 교안을 찾아보니 해당 내용이 있더라. (처음부터 찾아볼 걸…)
즉, T*& a의 의미를 다시 보면, T*(&a) = &arr[0]; 또는 T &a[] = arr; 와 같은 것이다.
굳이 &연산자를 사용하지 않고, T* a로 사용해도 오류가 발생하지는 않으나, 참조형은 한 번 초기화된 후에는 다른 변수를 참조할 수 없는 이점이 있기 때문에 참조형으로 받은 것 같다.
그냥 단지, &가 참조형이 아닌 주소값을 받는 연산자로 사용된 것인지 궁금해서 검색을 시작했던 것이었는데, 몰랐던 원리들과 문법들을 정확히 이해할 수 있게 되어 기쁘다.
댓글남기기