포인터의 형변환

요즘 해야 하는 공부는 산더미인데 그건 안 하고 맨날 C,C++ 문법을 공부중이다. 사실 그것도 나태하게 하고 있는 중이다. 엠생이다. 하지만 머릿속에 넣는 게 아예 없는 건 아니므로 그 중 하나를 여기에 남긴다. 앞으로도 그런 게 있으면 남기겠다.

C문법 같은 건 인터넷에 잘 정리된 글이 수없이 많으므로(나는 씹어먹는 C를 공부중이다)그런 건 생략하고, 내가 아름답다고 느끼는 부분이 있을 때마다 정리해서 적도록 하겠다. 대부분 내 뇌피셜을 팬티런 블로그를 쓰고 있는 다른 사람들이 잘 해석해서 답변해 준 결과를 정리한 것이다(대부분 190존잘C형카카오기만자까쁘). 사실 자기만족과 정리용으로 적는 거라 가독성이 병신같을 수 있다.

그럼 이제 포인터의 형변환에 관해서 써보겠다. 이 누추한 빤쓰런블로그까지 찾아올 정도면 포인터가 뭔지 정도는 알고 있을 것이다. 알다시피 포인터는 주소값을 가리킨다. 그런데 포인터 변수에도 타입이 존재한다. 만약 포인터가 단순히 주소를 저장하는 거라면 타입이 없는 void형 포인터만으로 충분하지 않을까? 왜 포인터 변수에도 타입이 존재하는 것일까? 그리고 포인터 변수의 타입은 무슨 역할을 하는 것일까?

그건 "포인터 변수를 *를 이용해서 역참조할 때, 역참조한 주소값에 있는 데이터를 어떻게 해석할지를 지정해 주는 것"이다. 예를 들어서

#include <iostream>
using namespace std;

int main() {
	int a = 3;
	int* p = &a;
	cout << a << "\n";
	cout << *p << "\n";
}

위와 같은 코드가 있다고 하자. 포인터를 아는 사람이라면 당연히 a, *p의 출력 결과가 3임을 예상할 수 있을 것이다. 그럼 왜 *p는 3인가? p에는 a의 주소값이 저장되어 있고 p가 int형 포인터이므로 *p에서 p에 저장된 주소값의 데이터를 해석할 때 int형의 데이터인 것으로 해석하기 때문이다. 그럼 만약 포인터의 형변환이 일어나면 어떻게 될까?

#include <iostream>
using namespace std;

int main() {
	float a = 3.1;
	int* p = (int*)&a;
	cout << a << "\n";
	cout << *p << "\n";
}

과연 출력 결과는? a의 출력값은 안 봐도 3.1일 것이다. C++의 문법을 알고 있는 사람이라면 당연하다. 그리고 *p는 3.1이 int형으로 변환된 값, 즉 3이 출력되지 않을까? 언뜻 보면 맞는 것 같은 생각이다. 그럼 이제 실제로 코드를 실행해 보자.

3.1
1078355558

이 나온다. 대부분의 컴파일러에서 비슷하게 나올 것이다. 처음 출력의 3.1은 예상한 대로다. 그런데 뒤쪽의 1078355558은 대체 어디서 튀어나온 것일까? 전혀 생각지도 못한 값이 나왔다.

처음으로 돌아가서 생각해 보자. 포인터의 타입은 그 포인터가 가리키는 주소에 저장된 데이터의 값을 어떻게 해석할지를 결정한다고 했다. 그런데 주소에는 우리가 지정한 값들이 어떻게 저장되고 있는 것일까? 컴퓨터는 0과 1밖에 모르는 기계라는 말을 한번쯤 들어 본 적이 있을 것이다. 없다면 이제부터 알아 놓자. 그러면 0과 1만으로 3이나 3.1같은 값들을 어떻게 저장하는 것일까? 자연수는 이진수로 표현해서 저장한다. 그리고 3.1같은 실수 값은 부동 소수점 방식을 이용해서 부호부,지수부,가수부를 나누는 IEEE-754 규약에 따라 저장하는데, 이에 대해서는 https://modoocode.com/17 의 '컴퓨터가 실수를 표현하는 원리' 문단을 참고로 하자. 그럼

float a = 3.1;
int* p = (int*)&a;

이 코드는 '&a에는 float값이 저장되어 있지만 그 저장된 데이터를 int 형으로 해석하겠다' 즉, float가 부동 소수점 방식으로 저장되어 있는데 이를 그냥 int형으로 읽어 버리겠다는 말이다. 이게 진짜인지 확인해 보자.

먼저 https://www.h-schmidt.net/FloatConverter/IEEE754.html 이 사이트에서 3.1 이 IEEE-754에 따르면 어떻게 저장되어 있는지 확인해 보자. 그러면

01000000010001100110011001100110

이 나온다. 이를 10진수로 변환하면 아까 우리가 보았던

1078355558

이 나온다. float*형을 int*형으로 변환하는 건 float로 메모리에 저장되어 있는 값을 int로 그냥 읽어오게 하겠다는 뜻인 것이다!

(비슷하게, int a=1000000000 정도의 꽤 큰 값을 주고 a의 주소를 가리키는 포인터를 (float*)&a 를 출력하면 0.00472379가 나오는데 이를 IEEE-754 규약에 따라 해석하면 00111011100110101100101000000110이다. 이는 10진수로 1000000006인데 부동소수점 오차를 생각하면 거의 비슷한 값이라고 할 수 있겠다. 다른 값으로 실험해 보자. 단, a를 너무 작게 주면 7.00649e-45 같은 너무 작은 값이 나오니까 a를 충분히 큰 값으로 주자. 왜 a가 작으면 float형으로 해석한 값도 작은지는 잘 생각해 보면 알 수 있다)

그러면 이번에는 정수형의 포인터끼리 변환을 한다고 생각해 보자. 정수형의 값들은 메모리에 같은 방식으로 저장될 테니 포인터끼리 형변환을 해도 해석된 값은 같지 않을까?

#include <iostream>
using namespace std;

int main() {
	long long a = 3;
	int* p = (int*)&a;
	cout << *p << "\n";
}

long long형의 포인터를 int형으로 형변환한 후 출력해 주었다. 이러면 결과값은 3으로 잘 출력된다. long long도 int 도 둘 다 정수형이니 long long 으로 저장된 값을 int로 읽어와도 잘 읽히는 것이다. 그럼 반대의 경우는 어떨까?

int main() {
	int a = 3;
	long long* p = (long long*)&a;
	cout << a << "\n";
	cout << *p << "\n";
}

실행하면?

3
-3689348818177884157

(컴파일러에 따라 좀 다를 수 있음. 나는 비주얼 스튜디오 2019)

????????????????????

왜 int* 를 long long* 으로 형변환하면 이런 결과가 만들어지는가? 그건 int 와 long long의 크기 차이에 있다. C 혹은 C++ 을 배운 사람이라면 변수 타입별 메모리의 크기에 대해서 한번쯤 들어본 적이 있을 것이다. char는 1바이트, int, float는 4바이트, long long, double은 8바이트 등등. 즉, 생각해 보면 int*형은 포인터에 저장된 주소부터 4바이트를 정수형으로 해석한다는 뜻이고, float*형은 주소부터 4바이트를 실수형으로 해석한다는 뜻, long long*형은 주소부터 "8바이트"를 정수형으로 해석한다는 뜻이다. 즉 저런 이상한 값이 나오는 이유는, 우리가

int a = 3;

로 4바이트(int의 크기)만 초기화했는데, 이 주소를 가리키는 포인터를 long long*형으로 변환함으로써 8바이트를 읽어오게 만들었고, 따라서 우리가 초기화하지 않은 다른 4바이트까지 읽어오게 만들었다는 것이다. 이게 참인지 보자. 먼저 출력값인 -3689348818177884157 에서 음수 부호를 떼고 이진수로 변환해 보자. 11001100110011001100110011001111111111111111111111111111111101 이다. 1100이 묘하게 반복되는 것 같지 않은가? 아마 당신이 위의 코드에 a에 어떤 값을 대입하고, 포인터를 long long*으로 변환하고, 출력된 값을 이진수로 변환해도 앞부분의 저 1100은 반복될 것이다. 저 1100은 16진수로 C이고, 마이크로소프트 비주얼 C++에서 초기화되지 않은 스택 영역의 값은 0xCC가 되기 때문이다. 즉 우리는 int* 를 강제로 long long* 으로 형변환함으로써 초기화되지 않은 영역까지 의도치 않게 읽어온 것이다!

위에서 long long*을 int* 로 변환하는 건 잘 되었던 이유는 8바이트를 읽어 오던 걸 4바이트를 읽어 오는 걸로 바꾸는 건 저장된 값을 읽어오는 데 큰 문제가 없었기 때문이다. 단, 원래 주소에 저장되어 있던 값이 int 범위를 넘어가는 값이었을 경우 long long* -> int* 변환은 long long에 저장된 값의 뒤쪽 4바이트만 읽어와서 이상한 값을 출력한다. 위의 경우 a가 3으로 작은 값이었기에 잘 처리된 것이다.

#include <iostream>
using namespace std;

int main() {
	long long a=4294967296+100LL;
    //4294967296 is 2^33+1. Execute the code and guess why such result follows.
	cout << a << "\n";
	cout << *(int*)(&a) << "\n";
}

포인터 변수의 타입은 그 포인터에 저장되어 있는 데이터를 어떻게 읽어올 것이냐를 정하는 것이라는 건 다른 여러 가지 방식으로 보여줄 수 있다. 가령

#include <iostream>
using namespace std;

int main() {
	int a = 65;
	char* p = (char*)&a;
	cout << a << "\n";
	cout << *p << "\n";
}

다음과 같은 코드는

65
A

를 출력한다. 65가 char로 해석되면 아스키 코드 A이기 때문이다. 반면 반대의 경우는 어떨까?

#include <iostream>
using namespace std;

int main() {
	char a = 'A';
	int* p = (int*)&a;
	cout << a << "\n";
	cout << *p << "\n";
}

이 경우에는 보기에는 이상한 값이 출력되지만, 위에서 int*를 long long*으로 변환할 때 일어났던 문제와 같다는 걸 잘 살펴보면 알 수 있다. 저 코드를 실행하면 어떤 음수가 나올 것이다. 그걸 이진수로 변환해 보고 2의 보수(컴퓨터에서 음수를 어떻게 저장하는지에 관련됨. 인터넷 찾아봐라)를 잘 생각해 봐라. 1바이트인 char를 4바이트인 int로 해석하느라 초기화되지 않은 값(마이크로소프트 비주얼 C++에선 0xCC) 을 읽어왔을 뿐이다. A의 아스키 코드값인 65의 흔적을 잘 살펴보면 찾을 수 있다..

자, 그럼 이제 머릿속에 박아놓아라. "포인터 타입은 그 포인터 주소에 저장되어 있는 데이터를 어느 정도의 크기만큼, 어떤 방식으로 읽어올지를 지정한다" 이것을 머릿속에 잘 넣어놓으면 포인터의 형변환에 대해서 잘 이해할 수 있다.

그런데 여기서 의문점이 하나 생길 수 있다. 우리가

long long a=3;

처럼 값을 지정하면 a의 주소에는 000...0011 (3은 2진수로 11)로 a의 값이 저장되어 있을 것이다. 그런데 이를

int* p = (int*)&a;

로 4비트만 읽어오게 시키면 앞부터 00..000이 읽혀서 00..0011은 잘리고, *p에는 0이 들어가 있어야 하지 않을까? 어째서 뒤부터 읽어서 000..0011 을 읽어오는 걸까? 모든 것을 아는 윤에 따르면 리틀 엔디안이라는 데이터 저장 방식과 관련이 있다고 한다. 이건 사실 나도 잘 모르는 부분이므로 여기서 글을 마친다. 포인터 타입이 무엇을 위한 것인지가 목적이었으니..