Qaupot Blog
Software Engineering, Trip

401. 포인터

🕐 Wed, 05 Feb 2014 09:00:00 GMT 🕓 Mon, 23 Aug 2021 13:07:00 GMT

메모리의 주소

위의 그림을 어떤 함수의 지역변수가 위치한 메모리라고 정의해 보겠습니다.

메모리 주소가 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 형으로 쪼개어 읽는 것 역시 가능합니다.
메모리에 의미를 부여하는것은 프로그래머 자신이며, 메모리를 어떻게 읽고 쓰는지에 대해서 컴퓨터는 관여하지 않습니다.

이 블로그는 개인 블로그입니다. 게시글은 오류를 포함하고 있을 수 있지만, 저자는 오류를 해결하기 위해 노력하고 있습니다.
게시글에 별도의 고지가 없는 경우, 크리에이티브 커먼즈 저작자표시-비영리-변경금지 4.0 라이선스를 따릅니다.

This blog is personal blog. published posts may contain some errors, but author doing efforts to clear errors.
If post have not notice of license, it under creative commons Attribution-NonCommercial-NoDerivatives 4.0.