C언어 포인터 개념 정리
C언어 포인터는 변수가 될 수도 있고 상수가 될 수도 있지만 오늘은 메모리 공간의 주소값을 저장하는 '포인터 변수'의 개념에 대해서 정리해본다.
메모리 주소값은 '정수'이며 32비트 시스템과 64비트 시스템에서는 각각 주소값을 32비트와 64비트로 표현한다. 물론 32비트에서도 주소값을 64비트로 표현할 수 있다. 하지만 비효율적이기 때문에 그러지 않는 것이다.
32비트, 64비트가 의미하는 건 한 번에 연산할 수 있는 데이터의 크기인데 32비트가 64비트의 주소값을 갖고 있다면 그것을 한 번에 처리하지 못하기 때문이다.
여하튼 메모리의 주소값은 '정수'이다. 그렇다면 int형 변수에 저장할 수도 있겠네? 물론 그렇다. 그런데 왜 포인터 변수에 저장을 할까? 하나씩 정리하면서 이해해보자.
'tpye 변수'의 주소값은 'type 포인터 변수'에 저장해야 한다.
이 소제목의 의미는 int형 변수의 메모리 주소값은 int형 포인터 변수, double형 변수의 주소값은 double형 포인터 변수에 저장해야 한다는 것을 말한다.
type(int, double....)이 다르다고 해도 메모리의 주소값은 그것들이 자리하고 있는 메모리의 첫 번째 바이트 주소값으로 모두 정수 형태이다. 그런데 왜 이것들을 각각의 자료형에 맞춘 포인터에 저장해야 하는 걸까?
이 두가지 의문을 풀어보는데 초점을 맞추고 하나씩 정리해본다. 즉, 왜 int형 변수에 저장하지 않는지... 또 왜 자료형을 맞춘 포인터 변수에 저장을 해야 하는지를 이해하는데 목적을 두고 포인터 변수의 형태를 살펴보자.
#include <stdio.h>
int main(void)
{
int su = 100;
int* p = &su;
printf("%p\n", p);
return 0;
}
예제에서는 int su를 100으로 초기화 하고 '&' 연산자를 이용해서 su의 주소값을 반환받은 후 p라는 포인터에 저장하고 그 값을 출력하는 모습이다.
여기서 알 수 있는 건 포인터 변수는 [자료형 * 이름]의 형태라는 점과 주소값을 반환하는 연산자는 '&' 그리고 그 값을 출력하기 위해서는 서식 문자 "%p"를 사용해야 한다는 것이다. 그리고 p 또한 변수이기 때문에 메모리 공간에 자리를 잡게 되는데 32비트에서는 4byte, 64비트에서는 8byte의 크기를 갖는다.
그 이유는 앞에서 정리했던 것처럼 시스템에 따라서 주소값의 크기가 다르기 때문이며 포인터 변수가 그것들을 저장해야 하기 때문에 해당 크기가 되는 것이다. 단 하드웨어, 시스템, 컴파일러가 모두 64비트를 사용할 경우에만 8byte의 크기를 갖게 된다.
의문 해결하기
앞에서 두 개의 의문이 있었다. 그것들을 하나씩 해결해보자.
우선 왜 정수인 메모리의 주소값을 int형 변수에 저장하지 않고 포인터에 저장을 할까? 만약 다음과 같이 int에 저장하면 어떻게 될까?
#include <stdio.h>
int main(void)
{
int su = 100;
int p = &su;
printf("%d\n", p);
return 0;
}
여기서는 [int * p] 대신 [int p] 변수에 주소값을 저장하고 "%d"로 출력해봤다. 그리고 아무 문제없이 실행되는 것도 확인을 했다. 이처럼 int 변수에도 주소값이 저장이 되는데 왜?
그 이유는 이렇게 int 변수에 그 값을 저장하면 위 예제처럼 값을 출력하는 정도밖에 할수가 없다. 그리고 그걸 출력해봐야 뭘 하겠나? 어떤 변수의 주소값을 알았다면 그곳에 직접 접근할 수 있어야 의미가 있지 않겠는가? 그리고 그것을 가능하게 해주는 것이 포인터 변수인 것이다.
그렇다면 어떻게 그곳으로 접근을 할 수 있는 것일까? 그 답은 '*'연산자이다.
'*' 연산자는 피연산자로 정수 두 개가 오면 곱셈을 하고, [자료형 * 변수 이름] 사이에 오면 포인터 선언에 사용되기도 하며 [*p]처럼 포인터 변수 앞에 위치하면 p가 저장하고 있는 메모리의 주소로 접근이 가능하다.
이 내용이 사실인지 다음과 같이 접근을 해보면 확인이 가능하다.
#include <stdio.h>
int main(void)
{
int su = 100;
int * p = &su;
*p = 200;
printf("%d\n", su);
return 0;
}
int su =100으로 초기화한 후 su의 주소값을 포인터 p에 저장하고 있다. 그리고 [*p = 200]을 통해서 값을 대입한 후 su를 출력해보면 200인걸 확인할 수 있다. 즉, p가 가리키고 있는 su가 위치한 메모리에 다가가서 그곳에 데이터 200을 저장한 것이며... 이처럼 [*포인터] 연산을 통해 메모리에 접근할 수 있는 것이다. 그리고 이는 [su = *p]라는 결론에 이르게 된다.
그렇다면 int p = 100 일 때.... 다시말해 p가 일반 변수일 때 *p = 200은 안 되는 걸까? 안된다.
그 이유는 p가 일반 변수이기 때문에 *p를 한다고 해도 그것이 가리키고 있는 메모리에 저장된 값이 int형이라는 정보가 없기 때문에 어떤 자료형으로 그 값(200)을 저장해야 할지 모르기 때문이다.
이로써 첫 번째 의문이 풀린다. [int * p]는 p가 가리키는 곳에 int형 데이터가 저장되어 있다는 걸 알 수 있지만 [int p]에서는 그것을 확인할 수가 없기 때문에 저장된 데이터의 type을 알기 위해서는 포인터 변수를 통해서 접근을 해야 하는 것이다.
그리고 두 번째 의문... 왜 데이터의 type과 포인터의 type를 맞춰야 할까? 그것 역시 [int * 변수이름], [double * 변수 이름]에서 int와 double이 의미하는 건 그곳에 저장된 데이터의 type이고 이를 기반으로 데이터를 저장해야 하기 때문이다.
결론은...
포인터는 변수의 주소값과 그곳에 저장된 데이터의 type에 관한 정보를 담고 있기 때문에 *연산자로 접근해서 데이터를 변경할 수 있다.