메모리의 주소
위의 그림을 어떤 함수의 지역변수가 위치한 메모리라고 정의해 보겠습니다.
메모리 주소가 0x로 시작합니다.
- 해당 수가 16진수(1 ~ 9 이후 A ~ F) 라는 것을 의미합니다.
- 컴퓨터는 2진수만을 사용하므로, 프로그램에서 숫자를 표현할 때는 사람이 인식하기 쉬운 10진수보다 2의 4승인 16진수를 쓰는 경우가 많습니다.
- 2의 8승인 1 byte 는 16진수 두 자리로 나타낼 수 있습니다. ex) 0xFF
다수 CPU 들은 1byte 를 하나의 주소에 매칭합니다.
만약 4 byte 를 하나의 번지에 매칭한다면, 메모리에 접근할때마다 최소 4byte 의 데이터를 가져올 필요가 있습니다.
- 모든 데이터가 4byte 인 구조라면 이는 매우 효율적인 방법이 됩니다.
- 1byte 단위로 주소를 매칭시키는것보다 주소를 표현하는데에 필요한 비용이 1/4로 줄어들 것입니다.
반대로, 1byte 를 기록하는데에 4byte 의 메모리 공간은 필요 없습니다.
이것은 CPU 의 목적 및 설계에 따르는 것이기 때문에, 어떤 방법이 좋다고 단정 할 수는 없습니다.
데이터의 해석
각 주소에 해당하는 byte 들은 각각 내용을 담고있습니다.
0x000000주소에 해당하는 데이터만을 읽는다면, 해당하는 byte 에는 0x58이 담겨져 있으며, 의미는 0x58입니다.
0x000000주소부터 0x000003주소를 하나의 블록으로 생각하여 읽을 수도 있습니다.
- 0x58 0x37 0x00 0x00 데이터를 가지는 byte 들의 나열으로, 곧 4byte 의 자료형이라고 할 수 있습니다.
이 byte 들의 나열을 어떤 4 byte 의 자료형으로 볼 것인지는 byte order 와 해석방법 (정수형, 실수형 등)에 따라서 바뀝니다.
위의 데이터는 little endian 으로 기록되어 있고, 정수형이라고 해 보겠습니다.
- 0x000000 부터 0x000003번까지 4byte 자료형이라고 보면 의미하는것은 0x00003758입니다.
- 0x000004 와 0x000005는 각각 1byte 자료형이라면 각각 0x0030과 0x0020을 의미할 것입니다.
- 0x000006 부터 0x000009까지 4byte 자료형이 선언되면 이것이 의미하는것은 0x00000004입니다.
0x00000A , 0x00000B, 0x00000C는 다시 1 byte 의 자료형이라고 생각해봅시다.
- 각각 0x0010 0x0002 0x0007을 의미합니다.
만약 0x000002부터 0x000005까지 4byte 로 선언되었다고 생각하면 어떻게 될까요?
- 0x20300000을 의미합니다.
포인터의 정의
메모리의 구조 상 일정한 주소가 매겨지고, 포인터는 그 주소를 담을 수 있는 형태의 자료형
입니다.
32bit OS 에서 지원하는 메모리 주소의 크기는 대체적으로 32bit, 2의 32승을 표현할 수 있습니다.
따라서 하나의 메모리 주소에 1 byte 가 매칭된다고 하면 메모리 전체의 크기는 4GB (2의 32승 byte) 까지입니다.
그러므로 포인터는 4 byte 의 크기가 되어야 합니다.
- 같은 논리로, 64bit 에서는 그 크기가 8byte 입니다.
그럼 위의 그림에서는 4byte 자료형으로 의미를 부여한 곳이 두 곳 있습니다.
만약 이 자료형이 int 형이라면, 이 값은 그대로 0x0004와 0x3758을 의미합니다.
그럼 이 자료형이 포인터라면?
이 값은 0x0004와 0x3758의 메모리 주소를 의미합니다.
컴퓨터는 메모리에 있는 값에 대해 그 형태가 포인터인지 int 인지, char 인지 관여하지 않습니다.
기본 단위는 byte 로 나뉘어 저장되어 있으며, 그 byte 는 일련의 bit 의 집합일뿐입니다.
결국 프로그램이 저장된 byte 들에 대해 의미를 어떻게 부여하는지가 중요합니다.
포인터의 선언 및 사용
#include <stdio.h>
int main(int argc,char** argv)
{
int IntValue = 200;
int* Pointer = &IntValue;
printf("Int Value %d\n",IntValue);
printf("Int Value By Pointer %d\n", *Pointer);
printf("IntValue's Memory Address %x\n",&IntValue);
printf("Pointer Value %x\n",Pointer);
printf("Pointer's Memory Address %x\n",&Pointer);
return 0;
}
먼저 main 함수에는 int IntValue 라는 변수가 있고 200이라는 값이 대입됩니다.
int* Pointer = &IntValue;
int 뒤에 *가 하나 붙었으며, 이것이 곧 포인터 변수
입니다.
- 어떠한 자료형(T라고 부르겠습니다)뒤에 *를 붙이면, 앞선 T형의 메모리를 담는 포인터 자료형이 됩니다.
만일 int*형의 포인터 자료형을 만들고 싶은 경우 뒤에 *을 하나 더 붙일 수 있습니다.
- int**형이 되며, 의미하는 것은 int*자료형을 T로 보고, 이 T의 메모리를 담는 포인터 자료형입니다.
- 같은 방법으로, 이론 상 *를 얼마든지 붙일 수 있습니다.
int* Pointer = &IntValue;
int* Pointer 에는 &IntValue 라는 값을 대입합니다.
변수이름 앞에 &를 붙일경우, 해당하는 변수의 메모리 주소
를 의미합니다.
IntValue 변수의 주소 값을 가져와 Pointer 라는 변수에 대입 한 것입니다.
IntValue 는 지역 변수이므로 스택 세그먼트의 어딘가에 존재하고 있을 것이며, 당연히 그 주소 또한 존재합니다.
printf("Int Value %d\n",IntValue);
printf("Int Value By Pointer %d\n", *Pointer);
printf 함수에 새롭게 %x라는 문자가 보입니다. 이것은 16진수 표기법에 맞추어 표기하라는 의미입니다.
Pointer 는 IntValue 의 주소값을 가지고 있으며,
포인터 변수의 앞에 *를 붙이는 것은 이 주소를 따라가서 그곳에 있는 값을 가져온다는 것을 의미합니다.
IntValue 의 주소를 따라가서 가져온 값은 IntValue 의 값으로, 200이 표기됩니다.
printf("IntValue's Memory Address %x\n",&IntValue);
printf("Pointer Value %x\n",Pointer);
printf("Pointer's Memory Address %x\n",&Pointer);
IntValue 에 &를 붙여 그 주소값을 가져오도록 해 보았습니다. 40f7f4가 들어왔습니다.
- 주소값 은 현재 실행되고 있는 컴퓨터나 프로세스의 상황에 따라 다를 것입니다.
Pointer 의 값을 그대로 출력해 보겠습니다.
40f7f4로 IntValue 의 주소 값과 똑같은 값입니다. Pointer 는 IntValue 의 주소를 가지고 있다는 사실을 알 수 있습니다.
Pointer 역시 4byte 자료형의 변수일 뿐이므로 &를 붙이면 그 변수의 주소를 가져올 수 있습니다. 그리고 그 값으로 40f7f8이 들어 왔습니다.
혹시 눈치 채셨나요? 현재 Pointer 와 IntValue 의 주소값 차이는 정확하게 4Byte 입니다.
즉 IntValue 의 데이터인 200이 4byte 에 걸쳐 기록되어 있으며, 그 뒤로 4byte 에 걸쳐 Pointer 가 기록되어 있을 것입니다.
이 두 메모리는 인접해 있는 상태이며, 스택 세그먼트에 할당되어 있습니다.
- OS 혹은 CPU 구조 등에 따라서 다를 수 있으며, 디버그 모드로 컴파일 했을 경우에는 사이에 추가적인 값을 기록할 수도 있습니다.
위를 정리하면, 현재 메모리의 구조는 위와 같을 것입니다.
Exercise
char 형 4개의 변수가 모두 붙어있고, PartB의 주소가 가장 낮은 위치에 있다는 전제조건이 필요합니다.
#include <stdio.h>
int main(int argc,char** argv)
{
char PartA = 0x11;
char PartB = 0x22;
char PartC = 0x33;
char PartD = 0x44;
printf("PartA Address %x\n",&PartA);
printf("PartB Address %x\n",&PartB);
printf("PartC Address %x\n",&PartC);
printf("PartD Address %x\n",&PartD);
printf("%x\n", *(int*)&PartB);
return 0;
}
순서대로 char 형(1byte)변수를 4개 만들었습니다.
- PartB가 가장 낮은 위치에 잡혔습니다.
- 4개의 주소들이 모두 모여 있습니다.
printf("%x\n", *(int*)&PartB);
마지막 출력문은 다소 복잡합니다. 먼저 &PartB로 PartB의 주소값을 가져옵니다.
그리고 int*형(int 포인터형)으로 캐스팅합니다.
- (자료형) 은 캐스팅 문법으로 뒤에 따르는 변수의 자료형을 원하는 자료형으로 바꿀 수 있습니다. 이를 명시적 강제 캐스팅 이라고 합니다.
따라서 (int*)&PartB을 이용하면 PartB의 주소값은 int*에 담긴것처럼 취급받습니다.
여기에 다시 *를 앞에 하나 붙여 이 주소를 따라가도록 합니다.
메모리 주소의 단위는 byte 입니다. 만들어낸 변수들은 인접한 주소에 배치되어 있었으며, 각각 값을 보관하고 있습니다.
가장 낮은곳에 있던 주소값을 시작점으로 int*형으로 캐스팅 했고, 다시 주소를 따라가서 그 값을 가져왔습니다. 결국은 0x25fc14 ~ 0x25fc17의 주소를 한번에 int 형으로 선언하고, 그곳에 0x11334422값을 대입한 것과 같은 결과를 보게 되었습니다.
이와는 반대로 int 형을 선언한 뒤, 그것을 char 형으로 쪼개어 읽는 것 역시 가능합니다.
메모리에 의미를 부여하는것은 프로그래머 자신이며, 메모리를 어떻게 읽고 쓰는지에 대해서 컴퓨터는 관여하지 않습니다.