[CS] TIL 14. 메모리주소, 포인터


  1. 16진법을 읽고 쓸 수 있어야 한다.
  2. 메모리 주소에 접근하고 값을 받아오는 코드를 C로 작성할 수 있어야 한다.


boostcourse모두를 위한 컴퓨터 과학 (CS50 2019) 강의를 듣고 정리한 필기입니다. 😀


1. 메모리주소

이번주는 저번주에 배웠던 알고리즘의 개념적인 부분이 아니라, 조금 더 실용적이고, 체계적인 부분에 집중한다. 제일 먼저 배울 것은 수를 세는 새로운 방식이다. 우리가 첫주차 때 배웠던 내용을 상기해보면 10진법, 2진법을 배웠다. 10과 2의 거듭제곱 외에도 다른 숫자를 진수로 사용하는 진법도 있었다. 이 사실에 대해서 언급한 이유는 이번 주에 우리가 배우게될 컴퓨터 메모리와 파일을 공부할 때 매우 유용하기 때문이다. 메모리와 파일은 컴퓨터나 휴대폰에 있는 이미지와 같은 파일을 만들거나 수정할 때 사용하는 컴퓨터(혹은 휴대폰) 속 메모리의 위치(혹은 메모리가 저장된 곳)를 말한다.

1.1. 16진수

title

이는 컴퓨터과학가 숫자를 10진수나 2진수가 아니라 16진법(Hexadecimal)로 표현하는 경우가 많다. hex은 16을 의미한다. 그 이유를 알아보기 위해서 2진법과 10진법, 16진법을 각각 살펴볼 것이다. 2진법에는 0과 1이 있고, 10 진법에는 0에서 9까지 있다. 16진수에는 0부터 F까지 있다.

1.1.1. 2진수

title

빠르게 더듬어 보면 2진수는 위 사진처럼 표현했다. 0bit가 8개 있고, 각각의 bit는 한 2진수를 의미했다. 위에 2의 거듭제곱이 표시되어있다.

title

각 거듭제곱을 계선해서 적으면 왼쪽부터 128, 64, 32, 16, 8, 4, 2,1 이 된다.

1.1.2. 10진수

title

이 숫자를 10진수로 표현하면 위 사진처럼 된다. 0을 1로 바꿔서 8 비트로 표현할 수 있는 가장 큰 숫자는 225였다. 226이라고 생각할 수도 있지만. 0부터 세기 때문에 0을 하나의 수로 사용하여 225가 8비트로 셀 수 있는 가장 큰 값이다. 직접 계산해보면 (126 × 1) + (64 × 1) + ... + (1 × 1)이 계산 식이 되기 때문에 225가 나온다.

title

10진법에서는 각 자리수가 10의 거듭제곱을 의미한다. 예를 들어 위 사진처럼 1, 10, 100 이다.

1.1.3. 16진수

16진수를 컴퓨터에서 데이터(글, 그림, 음악 등등) 처리하기 위해 사용하는 이유는 장점이 있기 때문이다. 16진수와 일상생활에서 우리가 사용하는 10진수와 비교하면 그 차이를 알 수 있다. 또한, 16진수를 사용하면 10진수보다 2진수를 간단하게 나타낼 수 있다. 16진수로 값을 표현하는 방법을 이해하고 나면 16진수, 2진수, 10진수를 변환하는 프로그램을 만들어볼 수 있다.

1.1.3.1 16진수 표기법

title

224를 16진수로 표현하면 위와 같다. 2나 10의 거듭제곱이 아니라 16의 거듭제곱을 사용한다. 컴퓨터가 연상할 때 이렇게 표현하면 매우 편리하다고 한다. 맨 오른쪽은 16의 0승 즉, 1의 자리고, 두번째는 16의 1승 즉, 16의 자리이다. 참고로 F는 16진수에서 15이다.

title

위 숫자는 16진수로 0이다. (16 × 0) + (1 × 0) 은 0이기 때문이다. 10진법은 1의 자리에서 0부터 9까지의 숫자만을 넣을 수 있지만, 16진수는 10을 초과할 수 있다. 그렇다면 9보다 큰 수는 어떻게 셀까?

title

처음보았던 메모리처럼 각각의 바이트에 부여된 숫자를 보면 알 수 있다. 16진수는 1~9까지는 10진수와 동일한 숫자로, 10~15까지는 A, B, C, D, E, F 쓴다.

title

16진수로 15를 표현할 때는 0F로 표기하지만, 16을 표기할 때는 10으로 표기한다. 여기서 10을 ‘십’이라고 읽어서는 안된다. 십은 10진수의 숫자이기 때문다. 우리는 16진수에서 **16을 말하는 10을 ‘일 영’으로 읽어야 한다. **

1.1.3.2. 10진수를 16진수로 바꾸어보기

title

위 그림은 [CS] TIL1의 그림, 영상, 음악의 표현에서 보았던 것이다. 컴퓨터와 TV 등은 당시 배웠던 RGB라는 체계에 의해서 표현된 것이다. 기본적으로 컴퓨터는 8비트의 JPG 포맷을 이용한다. JPG는 항상 225 216 255로 시작되고 이것은 10진수이다. 하지만 실제 컴퓨터 내에서는 10진수를 사용하지 않는다. 컴퓨터는 0과 1만을 이해할 수 있기 때문이다.

title

당시 그림 위을 포토샵에서 확인해보면 빨강 72, 초록 73, 파랑 33은 10 진수가 아니라 16진수로 표시된 것임을 알 수 있다. 때문에 화면에 표시된 색에 빨강만 있으면 #FF0000으로 표시될 것이고, 초록은 #00FF00, 파랑은 #0000FF로 표시된다. 이러한 표기는 게임, 웹, 혹은 모바일을 개발하게 되면 흔한 기법이라는 것을 알게될 것이다.

title

다시 돌아와서 메모리의 생김세를 보면 격자로 구성된 바이트이다. 맨 처음은 0이라고 하고, 마지막은 1F라고 한다. 알파벳이 붙지 않은 숫자를 보면 이게 10진수인지 16진수인지 구분하기 힘들다. 때문에 16진수로 표기된 각 숫자들 앞에는 0x를 붙이기로 약속했으며 0x는 수학적으로 아무런 의미가 없다.

1.1.3.3. 16진수의 유용성

title

ASCII 코드에 의해 “A, B, C”는 10진수로 65, 66, 67에 해당한다. 컴퓨터는 10진수를 이해할 수 없으므로 2진수로 표현해보면 “01000001 01000010 01000011"이 된다. 컴퓨터가 처리할 수 있어야 하기 때문에 어쩔 수 없지만 그 길이가 너무 긴 것을 알 수 있다.

하지만 16진수로 표현하면 2진수로 표현했을 때 보다 훨씬 간단해진다. 또한 컴퓨터는 8개의 비트가 모인 바이트 단위로 정보를 표현한다. 2개의 16진수는 1byte의 2진수로 변환되기 때문에 정보를 표현하기 매우 유용하다.

1.2 메모리 주소

방금 배운 16진수를 어떻게 사용하는지 살펴볼 것이다. 컴퓨터 메모리 속에서 실제로 어떤 일이 벌어지는지를 살피고 메모리를 다룰 때 16진수가 왜 유용한지를 알아보고, 그 메모리 세계에서 어떻게 메모리를 다루는지 살펴보고자 한다.

1.2.1 프로그램 만들기

정수형 변수 n에 50이라는 값을 저장하고 출력한다고 생각하는 프로그램을 만들 것이다.

#include <stdio.h>

int main(void)
{
    int n = 50;
    printf("%i\n", n);
}

우리는 지금까지 배운 것을 바탕으로 위와 같은 코드를 작성할 수 있는데, make를 이용해 컴파일을 하고, 출력을 해보면 50이라는 수가 정확히 출력되는 것을 알 수 있다. 이제 컴퓨터 메모리 속에서 어떤 일을 벌어지는 추론해보자.

1.2.2 메모리상 주소 받기(&%p)

title

위와 같은 컴퓨터 메모리 어딘가에 변수 n이 있을 것이다. 이 값은 int 타입이므로, 컴퓨터의 메모리 어딘가에 4바이트 만큼의 자리를 차지할 것이다. 변수 n에는 50이라는 값이 저장되어있을 것 같지만, 정말 더 깊게 내려가보면 50이 32bits로 구성된 0과 1이 50을 표현하고 있을 것이다.

그리고 이 변수를 출력하고 하려고 하면 우리는 컴퓨터에 있는 수십억 바이트의 어딘가에 있는 n을 찾아야 한다. 예를들어 변수 n과 그 안에 있는 50이라는 값은 ‘0x7ffe00b3adbc’이라는 위치에 있다고 가정해보자. (좌표는 중요하지 않으니 임의의 큰 수로 지정했다.) C 언는 n이 저장된 좌표를 정확히 확인할 수 있다.

#include <stdio.h>

int main(void)
{
    int n = 50;
    printf("%p\n", &n);
}

이렇게 변수의 메모리상 주소를 받기 위해 &(~의 주소)이라는 연산자를 사용해야한다. 그리고 %i 대신에 주소를 출력하는 %p를 쓸 수 있다. 예를 들어, 위와 같은 코드를 실행하면 변수 n의 좌표인 ‘0x7ffe00b3adbc’와 같은 값을 얻을 수 있고, 이는 변수 n의 16진법으로 표현된 메모리의 주소이다.

우리는 이러한 방법을 이용해 컴퓨터에 특정값의 주소를 요구하고, 그 값을 가리키는 포인터 값을 돌려받을 수 있다. 포인터는 컴퓨터 메모리의 주소리를 가리키는 것이다. 그래서 %i가 아니라 %p를 쓴다. 그리고 항상 16진수로 출력해준다.

1.2.3 메모리 주소에 있는 실제값을 받기(*)

#include <stdio.h>

int main(void)
{
    int n = 50;
    printf("%i\n", *&n);
}

주소값이 아니라 50을 출력하고 싶으면 *을 사용하면 된다. &n의 의미는 변수 n의 주소를 달라는 것이다. 사실 C언어에는 반대 역할을 하는 연산자도 있다. 단순히 *은 곱셈 연산자로 생각하겠지만, 다른 문맥에서는 의미가 달라진다. 여기서는 &와 반대의 동작을 하라는 의미이다.

&의 의미가 주소를 묻는 것이면, *은 그 주소로 가달라는 의미이다. 즉, 즉각적으로 연산을 원래대로 돌리는 것이다. 실제로 이렇게 쓰진 않아도 이런 식으로 우리가 하는 기본적인 연산들에 대해 알아야한다. 그리고 주소가 아닌 문자 그대로의 n의 값을 출력하고 하면 %p가 아니라 %i를 써야 한다. (각각 변수 안에 들어가 있는 자료형에 따라 다르게 쓰이겠지만 지금 변수 n에는 숫자형 int가 들어가 있기 때문에 %i를 쓴다.)

1.3 질문

Q1. 메모리의 주소를 직접 사용할 수 있을까?

▶ 예를 들어 0x12345678라는 주소를 기억하여 이 주소를 직접 사용하고, 그 주소로 가라고 할 수 있다. 문법과 형변환을 해야하지만 충분히 가능하다.

Q2. 만약 변수의 자료형을 모른다면 어떤 형식 지정자를 사용해야할까?

▶이것은 직접 결정해야한다. 컴퓨터에게 메모리는 0과 1이다. 그것을 어떻게 표현(즉, 어떤 자료형)할지는 우리에게 달리는 것이다. 안타깝게도 C언어에서는 자료형을 알려주는 기능은 없다. 어떤 자료형인지 모른다면 추정하거나 우리가 컴퓨터에게 무엇이든 알려줘야 한다. char 혹은 float, 혹은 int 등등.



2. 포인터

2.1 코드로 알아보기

더 나아가 정보를 어디에 저장할 수 있는지 정확하게 알아보자. * 연산자는 어떤 메모리 주소에 있는 값을 받아오게 해준다. 이 연산자를 이용해서 주소나 변수 즉, 포인터 역할을 하는 변수를 선언할 수도 있다.

#include <stdio.h>

int main(void)
{
   int n = 50;
   printf("%i\n", n);
}

위 코드를 보면 변수 n은 정수형으로 선언됐으며 50이라는 값이 저장되어있다. 이것을 printf함수로 출력할 때는 &를 굳이 붙일 필요가 없다.

#include <stdio.h>

int main(void)
{
    int n = 50;
    int p = &n;
}

어제 p라는 새로운 편수를 선언하고 그 안에 n의 주소를 저장한다. 여기서는 &n을 사용함으로써 n의 주소를 가지고 올 수 있다. 하지만 변수의 주소를 가지고 올 때는 정수형이나 소수형 등의 변수를 선언할 때와는 다른 방법으로 해줘야 한다.

#include <stdio.h>

int main(void)
{
    int n = 50;
    int *p = &n;
}

int *p 에서 p앞의 *는 이 변수가 포인터라는 의미이고, int 는 이 포인터가 int 타입의 변수를 가리킨다는 의미이다. (나중에 보겠지만 int 뿐 아니라 floatchar 등 다양한 자료형에 대한 포인터도 쓸 것이다.) 즉, 한문장으로 정리하면 *p라는 포인터 변수&n 이라는 값, 즉 변수 n의 주소를 저장한다는 것이다.

#include <stdio.h>

int main(void)
{
   int n = 50;
   int *p = &n;
   printf("%p\n", p);  // => 0x7FFF3977662C 
   printf("%i\n", *p); // => 50
}

위 코드는 포인터 변수 *p%p%i를 이용해 출력한것이다.

첫번째 printf는 포인터 p의 값, 즉 변수 n의 주소를 출력한다. 결과값은 ‘0x7FFF3977662C’ 이다. 위에서(1. 메모리 주소)에서 배웠을 때랑 변수명(n)도 같고, n에 저장한 값도 같은데 출력된 n의 주소가 다른 이유는 보안상의 문제로 컴퓨터 내에서 메모리를 여기저기로 바꾸기 때문이다. 어찌되었던 여전히 암호같은 16진수 주소이다.

두 번째 printf문과 같이 포인터 p가 가리키는 변수의 값, 즉 변수 n의 값을 출력한다. 이때는 p가 아니라 *p를 입력해야하며, 형식 지정자 %pint에 해당하는 %i로 바꿔야 한다. 그리고 출력하려는 %ip에 있는 값이다. *는 가리키는 주소 p로 가라는 의미가 된다. 결과 값은 50이 된다.

2.2 그림으로 알아보기

이제 그림을 보면서 int n = 50int *p = &n가 무엇을 하는지 살펴보자,

title

실제 컴퓨터 메모리에서 변수 p는 아래와 같이 저장될 수 있다. n은 메모리 어딘가에 존재하고, n이라고 부르는 값은 50을 가진다. 이 50은 임의의 위치에 있지만 ‘0x12345678’에 있다고 가정하자.

title

p 또한 메모리의 어딘가에 저장될 것이다. 그리고 역시나변수이기 때문에 bit로 데이터를 저장한다. 가지고 있는 값은 말그대로 ‘0x12345678’이라는 변수 n의 주소이다.

title

굉장히 쉬운 내용이지만 사실 포인터는 추상화를 위해서 사용된다. pn의 주소를 저장하고 있는 것이 아니라 n에 접근하고 있다는 것이다. 즉, 위 그림과 같이 실제로 p에는 주소 자체를 일일이 적지 않고 바로 화살표를 그려서 , 추상적으로 단지 p가 n을 가리키고 있다는 개념을 표현한다. 마치 보물주소 같은 것이다.

2.3 질문

Q1. &n*을 붙여서 선언한 포인트 변수(int *p)가 아닌, 일반 자료형 변수(int p)에 할당하면 어떻게 될까?

▶ 컴파일러가 경고를 한다. 왜냐하면 & 키워드가 1, 2, 3과 같은 정수(int)가 아니가 아니라 주소를 저장하려고 하기 때문이다. 사실 주소는 숫자값으로 되어있지만, 컴파일러는 생각보다 똑똑해서 주소는 반드시 포인터에 저장해야한다.

Q2. 본문에서 예로든 그림을 보면 포인터(p)의 크기는 그 포인터가 가리키는 n의 두배이다. 이런식으로 꼭 포인터의 크기는 두배여야 할까?

title

▶꼭 그렇지만은 않지만 대부분 그렇게 작동한다. 최신 컴퓨터는 64bits 포인터를 사용한다. 두째주에 배운 long 타입과 같은 크이다. 그리서 위 그림도 8비트 혹은 64비트로 그렸다.

title

그리고 정수형은 4바이트 또는 32바이트로 그렸다. 현대 하드위어가 대부분 그럴 뿐, 더 그럴 필요가 없다.




© 2020. by RIVER

Powered by RIVER