본문 바로가기
Development/CS

[CS50] 5강 메모리

by _KHK 2021. 12. 23.
메모리 주소


컴퓨터는 숫자를 10진수나 2진수가 아닌 16진수로 표기할 때가 많다. 컴퓨터에서 데이터를 처리하기 위해 16진수를 사용할 때 장점이 있기 때문이다. 

이미지를 표현할 때 RGB형태로 데이터를 갖고 있는데 css를 다룰 때 한 번쯤은 봤을 #FFFFFF 문자열이다.

이것이 의미하는것이 16진수들인데 R : G : B = FF : FF : FF 인 것이다.

 

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

 

16진수 표현법

 

앞서 16진수를 말한 이유는 메모리의 주소는 16진수로 표현한다. 메모리에 수많은 주소를 표현하기에 아주 적절한 단위일 것이다.

 

 

 

포인터

C에서 포인터를 사용할 때는 아래처럼 변수명 앞에 ' * '를 사용한다.

int *p = 50;

포인터는 추상화를 위해 사용된다. p라는 화살표가 50을 가리킨다는 개념을 표현하는 것이다. 이 개념을 잘 이해하는 것이 중요하다. 포인터에는 50이 저장된 변수의 주소를 갖고 있지만 가리킨다라고 이해하고 있는 것이 중요하다.

포인터가 필요한 이유는 아주 정교한 자료형을 만들 수 있기 때문이다. 가계도나 배열 등 수많은 데이터를 관리하기 위해 사용하는 알고리즘의 기초가 된다.

 

 

 

 

문자열 String

위에서 포인터가 "화살표가 50을 가리킨다"라는 표현은 정확하게 다시 말하면 변수가 저장된 시작점의 주소를 가리킨다.

C에서는 기본적으로 string이라는 자료형이 존재하지 않는다. 하지만 어떻게 string이라는 자료형을 구현하고 사용할 수 있는 것일까? 정답은 바로 포인터를 이용한 것이다.

char *s = "EMMA";

위의 코드를 보면 s라는 포인터는 문자들 낱개의 'EMMA'를 저장한 시작점의 주소를 가리킨다.

이 강의에서 cs50.h라는 헤더 파일을 처음에 알려주는데 이 헤더 파일에 string이라고 불리는 새로운 자료형을 정의하고 사실 내용은 포인터가 저장되는 문자열의 첫 주소를 가리키는 것을 의미한다. 

 

printf("%c\n" , *s);
printf("%c\n" , *(s+1));
printf("%c\n" , *(s+2));
printf("%c\n" , *(s+3));

이렇게 작성하면 어떤 출력이 나올까? 답은

E

M

M

A

 

문자열 비교

서로 다른 숫자를 비교하는 함수를 작성하는 것은 쉽다.

하지만 문자열은 조금 상황이 다르다.

 

숫자를 비교하는 코드랑 똑같이 문자열을 비교하는 코드로 작성해본다. 

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    // 사용자로 입력을 s와 t에 저장
    string s = get_string("s: ");
    string t = get_string("t: ");

    // 문자열 비교
    if (s == t)
    {
        printf("같다\n");
    }
    else
    {
        printf("다르다\n");
    }
}

출력은 다르다고 나올 것이다.

왜냐하면 s와 t는 서로 자신이 저장한 문자열의 주소를 각각 저장하고 있기 때문이다.

 

그럼 어떻게 작성해야 문자열을 정확하게 비교할 수 있을까?

#include <stdio.h>
#include <cs50.h>

int main(void)
{
    // 사용자에게 문자열 입력받음
    char *s = get_string("s :");
    char *t = get_string("t :");

    // 두 문자열 비교
    for (int i = 0 ; s[i] != '\0' || t[i] != '\0' ; i++)
    {
    	if (s[i] != t[i])
        {
            printf("다르다\n");
            return 1;
        }
    }
    printf("같다\n");
    return 0;
}

포인터를 사용해서 s와 t의 주소를 알고 있고, 각 문자열이 null종단 문자를 만나기 전까지 각각의 자릿수에 해당하는 문자열을 비교하도록 코드를 작성한다. 반복문이 끝까지 도는 동안 반복되면 "같다"라는 출력을 확인할 수 있다.

 

 

 

문자열 복사

문자열을 비교해 보면서 string을 별생각 없이 복사 저장하는 코드를 작성하게 되면 포인터가 가리키는 방향(주소 값)이 복사된 다는 것을 알게 됐다.

그렇다면 문자열의 복사는 어떻게 할까?

 

malloc이라는 함수를 사용하게 되는데 malloc 함수는  heap이라는 메모리 지역에 데이터를 저장할 수 있도록 공간을 할당해주는 함수이다.

예를 들어 "emma"라는 문자열을 저장하려면 종단 문자를 포함해 5byte가 필요하기 때문에 malloc(5)라고 작성해 사용한다. "emma" 문자열을 복사하기 위해 메모리에서 필요한 만큼 5byte 땅을 분양받고 그 위에 한 칸씩 'e', 'm', 'm', 'a'라는 집을 짓는다. 집 마당의 끝은 경계를 표시하기 위해 '\0'라는 울타리를 긋는다. 

아무튼 이렇게 해서 문자열을 복사하기 위해서는 필요한 공간만큼의 메모리를 할당받고, 위에 작성한 코드처럼 반복문으로 문자열 하나씩 메모리에 복사하는 것이다.

 

 

 

 

 

 

 메모리 힙, 스택

 

malloc 함수를 이용해 필요한 만큼의 메모리를 할당받았다. 하지만 이 malloc이라는 함수는 해체를 하지 않게 되면 메모리 공간에 프로그램이 종료될 때까지 남아있게 된다. 그래서 필요한 함수를 사용하고 나면 free라는 함수로 저장한 변수를 해체한다. 

 

메모리의 맨 위는 머신 코드 즉, 컴파일러가 인코딩해 실행할 수 있는 머신 코드가 놓이고, 그 아래에는 global 코드가 저장된다. global 코드는 함수 밖에서 선언되는 전역 변수와 데이터 같은 정보들이 저장된다.

이제 그 아래에 힙(heap)이라고 불리는 특별한 메모리가 저장된다. 힙은 메모리를 할당받을 수 있는 특별한 공간이다.

힙 메모리는 위에서 아래로 내려가면서 사용된다.

함수 안의 지역변수들은 스택(stack)이라는 제일 아래의 영역에 저장된다. 함수가 호출될 때 함수 안의 지역변수들이 저장되는 곳이다. 이곳은 맨 아래에서 쌓이기 때문에 차곡차곡 위로 올라가면서 데이터를 축적한다.

메모리 저장 공간

 

스택은 아래에서 위로 쌓이고, 위에서부터 꺼내진다. 꺼내진 다라는 뜻은 다시 말해 함수 수행을 마친 후 사라진다는 것을 의미한다.

 

 

 

문자열 교환

 

서로 다른 변수에 저장된 a = '1', b = '2'가 있다. 이 둘을 교환하려면 임시로 다른 공간이 필요하다.

왜냐하면 b=a , 라는 코드를 먼저 작성하게 되면 a는 이미 2가 되어버려 1이라는 숫자는 사라지게 되기 때문이다.

그래서 tmp라고 임시로 저장공간을 열어두고 tmp라는 공간에 1을 저장하고 뒤이어 b=a라는 함수를 작성하게 되면 tmp라는 공간에 1이 사라지지 않고 남아있기 때문에 b에 1을 저장할 수 있다.

 

#include <stdio.h>

void swap(int a, int b);

int main(void)
{
    int x = 1;
    int y = 2;

    printf("x is %i, y is %i\n", x, y);
    swap(x, y);
    printf("x is %i, y is %i\n", x, y);
}

void swap(int a, int b)
{
    int tmp = a;
    a = b;
    b = tmp;
}

그렇다면 이렇게 작성된 코드는 정상적으로 동작할까?

잠시 생각해보고 아래를 보자

 

 

 

 

"답은 그렇지 않다"이다.

 

이유를 설명해보자면 먼저 swap이라는 함수에서 전달받는 x와 y는 말 그대로 값만 전달이 된다. swap이라는 함수에 a는 숫자 1이 들어올 거고, b는 y의 값인 2가 저장이 된다. 그렇게 해서 a변수와 b변수에 말한 대로 변화를 줬다.

근데 그게 끝이다. swap함수는 위에서 설명한 stack함수로 호출이 된다. main함수가 항상 먼저 호출되기 때문에 stack메모리에 맨 아래는 main함수에 저장되어있는 지역 변수 x, y가 저장되어 있을 것이다. 그리고 swap 함수는 main함수 메모리 위에서 호출되었을 것이다. 스택 메모리는 아래에서부터 쌓이고 위에서 먼저 꺼내진다. swap함수는 역할을 다하고 끝이 났다. 호출되고 역할을 다하고 사라졌다. 그리고 남은 것은 그대로 남아있는 변수 x, y일 뿐인 것이다.

 

이때 다시 필요한 것은 포인터다. 포인터를 이해하기로 화살표로서 메모리의 저장 위치를 알려준다.

만약 swap함수가 인자를 전달받을 때 값이 아닌 주소(포인터)를 전달받았다면 어떻게 될까?

포인터를 전달 받았을 때 추상할 수 있는 이미지

 

 

a가 가리키는 1을 tmp에 저장한다. 그리고 b가 가리키는 2를 a가 가리키고 있는 곳에 변경할 수 있다.

b가 가리키고 있는 곳에 tmp가 저장하고 있는 1을 넣어준다.

#include <stdio.h>

void swap(int *a, int *b);

int main(void)
{
    int x = 1;
    int y = 2;

    printf("x is %i, y is %i\n", x, y);
    swap(&x, &y); // &은 주소값을 표현한다.
    printf("x is %i, y is %i\n", x, y);
}

void swap(int *a, int *b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

내가 헷갈렸던 부분은 주소 값을 전달했는데 어떻게 저장된 값이 변경되는지 주소 값과 값이 헷갈렸다.

간단한 부분인데 swap함수는 int 로서 포인터 *a, *b를 전달받고 있다. 결론은 a와 b는 int로서 값을 전달받지만 포인터로서 가리키는 int값을 갖고 있다. 그래서 *a = *b 코드는 b가 가리키고 있는 int 값을 a가 가리키고 있는 int 값에 대입한다.라고 표현될 수 있는 것이다. 

 

 

 

'Development > CS' 카테고리의 다른 글

[CS50] 6강 자료구조  (0) 2021.12.28
[CS50] 제 4강 알고리즘  (0) 2021.12.20
[CS50] 제 3-2강 배열  (0) 2021.12.20
[CS50] 제 3-1강 배열  (0) 2021.12.19
[CS50] 제 2강 - C언어  (0) 2021.12.18

댓글