배열과 포인터

수많은 사람들의 머리를 터뜨려버린 배열과 포인터의 관계에 대한 나의 장렬한 전쟁기록을 남겨두기 위해서 이 글을 쓴다. 결론은 뻔하지만 배열은 배열이고 포인터는 포인터라는 것이다. 이 부분에 대해서 글을 쓴 어느 블로그에 들어가도 나와 있는 결론이다. 하지만 이걸 마음속 깊이 깨닫기 위해서는 누구나 머릿속에서 전쟁을 한번쯤 치르게 된다. 만약 이게 술술 이해되는 사람이라면 컴공과에 필히 가야 한다.

일단 배열과 포인터가 다르다는 걸 보이기 위해서 얄팍한 sizeof 따위를 보여주는 건 생략한다.

내가 C언어를 배우면서 파이썬 같은 언어에 비해 정말 아름답다고 느꼈던 때 중 하나가, 변수의 주소를 받아서 그 주소를 역참조한 후 변수의 데이터를 직접 조작할 수 있는 함수를 처음 봤을 때였다.

void swap(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

이런 거 말이다

그럼 이런 함수의 개념을 알고, 배열의 이름이 배열의 시작 주소를 가리킨다는 것을 아는 사람은 이런 생각을 할 법 하다.

'배열의 원소를 하나하나 바꿀 필요 없이, 배열의 시작 주소만 바꿔치기해버리면 배열 전체를 바꿀 수 있지 않을까?'

나도 그런 생각을 했고 이런 함수를 짰다.

int aswap(int **pa, int **pb) {
	int* temp = *pa;
	*pa = *pb;
	*pb = temp;
	return 0;
}

여기에 어떻게든 배열의 시작 주소를 가리키는 포인터를 쑤셔넣기만 하면 크기가 10인 배열이든, 100만인 배열이든 O(1)만에 배열을 바꿀 수 있지 않을까(주소 하나만 바꾸면 되니까)? 그놈의 시간복잡도 한단계 줄이는 것에 온 인생을 거는 ps판에 새끼발가락 하나 정도는 담근 나로서는 이걸 생각 안 해볼 수가 없었다.

하지만 어떻게 해도 잘 되지 않았다. 아예 컴파일이 안 되거나 어떻게든 컴파일은 되게 만들어도 배열의 원소는 바뀌지 않았다.

그것은 정말로 배열은 배열이고 포인터는 포인터이기 때문이다. 배열의 이름이 배열의 시작 주소를 가리킨다는 것은 암묵적으로 그렇게 변환이 된다는 것이지 배열 이름=배열의 시작 주소가 아니었던 것이다. 이 암묵적인 변환이 일어나지 않을 때를 보면 이해가 명확해진다. 이는 씹어먹는 C(https://modoocode.com/25) 를 보고 배운 내용이다.

만약 배열의 이름이 배열의 시작 주소를 가리킨다고 하자. 그러면

int a[3]={1,3,5};

이러한 코드에서 &a 의 타입은 무엇일까? a는 &a[0](즉 int* 타입)일 테니 &a의 타입은 int** 라고 보는 게 일견 자연스럽다. 하지만

int **pa=&a;

이와 같은 대입을 시도해 보면 에러가 뜬다. &a의 타입이 int**가 아니라는 것이다. 왜?

이는 바로 주소값 연산자 &가 붙을 경우가, 배열 이름이 배열 시작 주소의 포인터로 암묵적으로 변환되는 것의 '예외' 이기 때문이다. 이때의 a는 원래의 뜻, 즉 크기 3인 배열 a의 이름으로 취급되고, 따라서 &a는 크기 3인 배열 a의 주소값을 나타내게 되는 것이다. 원래 이게 정상이지만 배열의 이름이 배열 시작 주소를 가리키는 포인터로 암묵적 변환된다는 것이 '같다' 로 오해받으면서 이런 일이 생긴다.

위의 &a를 어딘가에 대입하려면 다음과 같이 써 줘야 한다.

int (*pa)[3]=&a;

배열의 이름은 그냥 배열의 이름이다. 당신이 하는 대부분의 작업에서 포인터처럼 쓸 수 있을 뿐이다..