본문 바로가기

CS50

5. 메모리

모두를 위한 컴퓨터 과학(CS50 2019) 강의를 듣고 요약한 내용입니다. 해당 글에는 퀴즈에 대한 정답 및 풀이도 있습니다. 아직 퀴즈를 풀지 못하신 분들은 퀴즈를 풀고 난 후에 해당 포스팅을 읽으시길 바랍니다.

 

목차

1) 메모리 주소

2) 포인터

3) 문자열

4) 문자열 비교

5) 문자열 복사

6) 메모리 할당과 해제

7) 메모리 교환, 스택, 힙

8) 파일 쓰기

9) 파일 읽기

1. 메모리 주소

학습목표: 16진법을 읽고 쓸 수 있습니다. 메모리 주소에 접근하고 값ㅇ르 받아오는 코드를 C로 작성할 수 있습니다.

 

16진수

- 컴퓨터 과학에서는 10진수나 2진수 대신 16진수(Hexadecimal)로 표현하는 경우가 많습니다. 

- 컴퓨터에서 데이터를 처리하기 위해 16진수를 사용할 때 장점이 있습니다. 16진수를 사용하면 10진수보다 2진수를 간단하게 나타낼 수 있습니다. 

 

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

- JPG 이미지 파일은 항상 255 216 255 로 시작되고 이것인 10진수 입니다.

- 255, 216, 255를 2진수로 나타내면 아래 그림과 같습니다만 2진수로 모든 데이터를 표현하기에는 너무 길어지기 때문에 16진수로 바꾸어보겠습니다. 

- 2⁴이 16이기 때문에 4bits씩 두 덩어리로 나누어 보면 0000 부터 1111 까지는 16진수로 표현할 수 있다는 것을 알 수 있습니다.

- 10은 a, 11은 b, ..., 15는 f를 대입하여 사용합니다. 4bits씩 16진수로 변환 후 0x를 붙여 뒤에 오는 문자들이 16진수임을 알려줍니다.

 

16진수의 유용성

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

 

메모리 주소

- 정수형 변수 n에 50이라는 값을 저장하고 출력한다고 생각해 봅시다. n은 정수이므로 int 타입이고, 컴퓨터의 메모리 어딘가에 4바이트 만큼의 자리를 차지하며 저장되어 있을 것입니다.

 

 

- C에서는 변수의 메모리상 주소를 받기 위해 '&'이라는 연산자를 사용할 수 있습니다. 

 

#include <stdio.h>

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

 

- 위 코드를 실행하면 '0x7ffdadb52cdc' 와 같은 값을 얻을 수 있고, 이는 변수 n의 16진법으로 표현된 메모리 주소입니다.

- 반대로 ‘*’를 사용하면 그 메모리 주소에 있는 실제 값을 얻을 수 있습니다.

2. 메모리 주소

학습 목표: 앞서 배운 메모리 주소를 직접 관리하기는 쉽지 않을 수 있습니다. C에는 포인터 라는 개념을 통해서 변수의 주소를 쉽게 저장하고 접근할 수 있게 해줍니다. 포인터가 무엇인지, 어떻게 사용하는지에 대해 배워보겠습니다. 포인터 변수를 정의하고 사용할 수 있습니다.

 

- '*' 연산자는 어떤 메모리 주소에 있는 값을 받아오게 해줍니다. 이 연산자를 이용해서 포인터 역할을 하는 변수를 선언할 수도 있습니다.

 

#include <stdio.h>

int main(void)
{
  int n = 50;
  int *p = &n;
  
  printf("%p\n", p); // 메모리 주소 출력
  printf("%i\n", *p); // 메모리 주소에 저장된 값 출력
}

 

- *p 라는 포인터 변수에 &n 이라는 값, 즉 변수 n의 주소를 저장합니다.

- int *p에서 p 앞의 *는 이 변수가 포인터라는 의미입니다. 

- 첫 번째 printf문과 같이 포인터 p의 값, 즉 변수 n의 주소를 출력하거나, 두 번째 printf문과 같이 포인터 p가 가리키는 변수의 값, 즉 변수 n의 값을 출력할 수도 있습니다. 

 

3. 문자열

학습 목표: "EMMA"와 같은 문자열을 저장하기 위해서 string 이라는 자료형을 사용했습니다. 하지만 이는 실제로 C에서 존재하지 않는 자료형입니다. 문자열이 실제로 메모리상에 어떻게 저장되어 있는지, 문자열을 손쉽게 저장하고 접근하기 위한 방법을 배울 수 있습니다.

 

- 아래 코드와 같이 CS50 라이브러리에 포함된 string 자료형을 사용해 변수 s에 "EMMA" 값을 저장한다고 가정해봅시다.

- 문자열은 문자의 배열이고, 

- 가장 마지막의 \0 은 0으로 이루어진 바이트로, 문자열의 끝을 표시하는 약속입니다.

 

 

- 여기서 변수 s는 결국 이러한 문자열을 가리키는 포인터가 됩니다.

- 더 상세히는 문자열의 가장 첫번째 문자, 즉 주소 0x123에 있는 s[0]을 가리기케 됩니다.

 

 

- 아래 두 코드는 동일하게 동작합니다.

 

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

int main(void)
{
  string s = "EMMA";
  printf("%s\n", s);
}

 

#include <stdio.h>

int main(void)
{
  char *s = "EMMA";
  printf("%s\n", s);
}

4. 문자열 비교

학습 목표: 문자열이 저장되어 있는 방식에 근거해서 문자열을 비교하는 방법에 대해 설명할 수 있습니다.

 

#include <stdio.h>

int main(void)
{
  char *s = "EMMA";
  printf("%p\n", s);
}

 

- 위 코드를 실행하면, s라는 포인터의 값, 즉 "EMMA" 라는 문자열의 가장 첫 값인 "E"에 해당하는 메모리 주소를 출력하게 될 것입니다.

- 문자열은 첫번째 문자를 시작으로 메모리상에서 바로 옆에 저장되어 있습니다. 다시 말해, 가장 첫 번째 문자에 해당하는 주소값을 하나씩 증가시키면 바로 옆에 있는 문자의 값을 출력할 수 있습니다.

 

#include <stdio.h>

int main(void)
{
  char *s = "EMMA";
  printf("%p\n", s);
  
  printf("%c\n", *s); // E
  printf("%c\n", *(s+1)); // M
  printf("%c\n", *(s+2)); // M 
  printf("%c\n", *(s+3)); // 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("Same\n");
    }
    else
    {
        printf("Different\n"); // Different 프린트 됨
    }
}

5. 문자열 복사

학습 목표: 문자열을 복사할 수 있습니다.

 

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

int main(void)
{
    string s = get_string("s: "); // hi 입력
    string t = s;

    t[0] = toupper(t[0]);

    printf("s: %s\n", s); // Hi 출력
    printf("t: %s\n", t); // Hi 출력
}

 

- 입력값으로 "emma"를 주게 된다면, s와 t 변수 모두 "Emma"라고 출력이 됩니다.

- 그 이유는 s라는 변수에는 "emma" 문자열이 아닌 그 문자열이 있는 메모리의 주소가 저장되기 때문이죠. string s 는 char *s 와 동일한 의미라는 걸 떠올리면 됩니다.

- 따라서 t도 s와 동일한 주소를 가리키고 있고, t를 통한 수정은 s에도 그대로 반영이 되게 되는 것입니다.

 

- 두 문자열을 실제로 메모리상에서 복사하려면 아래 코드와 같이 메모리 할당 함수를 사용하면 됩니다.

 

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

int main(void)
{
  char *s = get_string("s: ");
  char *t = malloc(strlen(s) + 1);
  
  for (int i = 0; n = strlen(s); i < n + 1; i++)
  {
    t[i] = s[i];
  }
  
  t[0] = toupper(t[0]);
  
  printf("s: %s\n", s);
  printf("t: %s\n", t);
}

 

- malloc 이라는 함수를 이용해서 t를 정의했습니다. malloc 이라는 함수는 정해진 크기만큼 메모리를 할당하는 함수 입니다. 

- 즉, s 문자열의 길이에 널 종단 문자(\0)에 해당하는 1을 더한 만큼 메모리를 할당합니다.

6. 메모리 할당과 해제

학습 목표: 메모리를 할당하고 해제할 수 있습니다.

 

- malloc 함수를 이용하여 메모리를 할당한 후에는 free 라는 함수를 이용하여 메모리를 해제해줘야 합니다.

- 그렇지 않은 경우 메모리에 저장한 값은 쓰레기 값으로 남게 되어 메모리 용량의 낭비가 발생하게 됩니다. 이러한 현상을 '메모리 누수'라고 일컫습니다.

 

- valgrind 라는 프로그램을 사용하면 우리가 작성한 코드에서 메모리와 관련된 문제가 있는지를 쉽게 확인할 수 있습니다.

- 터미널에서 아래 명령어를 사용하면 파일에 대한 valgrind 검사 내용을 확인할 수 있습니다.

help50 valgrind ./filename

 

#include <stdlib.h>

void f(void)
{
  int *x = malloc(10 * sizeof(int));
  x[10] = 0;
}

int main(void)
{
  f();
  return 0;
}

 

- 버퍼 오버플로우 발생: x[10] = 0; 코드로 인해 발생합니다. 우리는 10개의 int 형의 배열을 만들었는데 배열의 인덱스가 0부터 시작한다는 점을 감안하면 인덱스 10은 11번째 인덱스에 접근하겠다는 의미이고, 이는 정의되지 않은 것이기 때문에 버퍼 오버플로우가 발생합니다.

- 메모리 누수 문제: x라는 포인터를 통해 할당한 메모리를 해제하기 위해 free(x) 라는 코드를 추가해줌으로써 해결할 수 있습니다.(?)

7. 메모리 교환, 스택, 힙

학습 목표: 메모리에 저장된 두 값을 교환하는 코드를 작성할 수 있습니다.

 

#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;
}

 

- 위 코드를 실행하면 첫번째 출력값은 "x is 1, y is 2", 두번째 출력값은 "x is 2, y is 1" 라는 기대와 다르게 똑같은 결과 값이 출력된다.

 

 

- swap 함수는 교환 작업을 제대로 수행하고 있지만, 문제는 교환하는 대상이 x, y 그 자체가 아닌 함수 내에서 새롭게 정의된 a, b라는 것입니다. 

- a와 b는 각각 x와 y의 값을 복제하여 가지게 됩니다. 서로 다른 메모리의 주소에 저장되는 것이죠.

- a와 b를 각각 x와 y를 가리키는 포인터로 지정함으로써 이 문제를 쉽게 해결할 수 있습니다.

 

#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;
}

 

※ 데이터 저장 구역

- 메모리 안에는 데이터 저장되는 구여기 나눠져 있습니다.

- 머신 코드 영역에는 우리 프로그램이 실행될 때 그 프로그램이 컴파일된 바이너리가 저장됩니다.

- 글로벌 영역에는 프로그램 안에서 저장된 전역 변수가 저장됩니다.

- 힙 영역에는 malloc으로 할당된 메모리 데이터가 저장됩니다.

- 스택 영역에는 프로그램 내의 함수와 관련된 것들이 저장됩니다.

 

8. 파일 쓰기

학습 목표: 사용자로부터 값을 입력받아 파일에 출력하는 프로그램을 작성할 수 있습니다.

 

- 힙 영역에서는 malloc 에 의해 메모리가 더 할당 될수록, 점점 사용하는 메모리의 범위가 아래로 늘어납니다.

- 마찬가지로 스택 영역에서도 함수가 더 많이 호출 될수록 사용하는 메모리의 범위가 점점 위로 늘어납니다.

- 이렇게 점점 늘어나다 보면 제한된 메모리 용량 하에서는 기존의 값을 침범하는

- 상황도 발생할 것입니다. 이를 힙 오버플로우, 스택 오버플로우 라고 일컫습니다.

 

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

int main(void)
{
  FILE *file = fopen("phonebook.csv", "a");
  char *name = get_string("Name: ");
  char *number = get_string("Number: ");
  fprintf(file, "%s, $s\n", name, number);
  fclose(file);
}

 

- fopen이라는 함수를 이용하면 파일을 FILE이라는 자료형으로 불러올 수 있습니다.

- fopen 함수의 첫번째 인자는 파일의 이름, 두번째 인자는 모드로 r은 읽기, w는 쓰기, a는 덧붙이기를 의미합니다.

- 사용자에게 name과 number라는 문자열을 입력 받고, 이를 fprintf 함수를 이용하여 printf에서처럼 파일에 직접 내용을 출력할 수 있습니다.

- 작업이 끝난 후에는 fclose함수로 파일에 대한 작업을 종료해줘야 합니다.

9. 파일 읽기

학습 목표: 사용자로부터 값을 입력받아 파일에 출력하는 프로그램을 작성할 수 있습니다.

 

- 힙 영역에서는 malloc 에 의해 메모리가 더 할당 될수록, 점점 사용하는 메모리의 범위가 아래로 늘어납니다.

- 마찬가지로 스택 영역에서도 함수가 더 많이 호출 될수록 사용하는 메모리의 범위가 점점 위로 늘어납니다.

- 이렇게 점점 늘어나다 보면 제한된 메모리 용량 하에서는 기존의 값을 침범하는

- 상황도 발생할 것입니다. 이를 힙 오버플로우, 스택 오버플로우 라고 일컫습니다.

 

#include <stdio.h>

int main(int argc, char *argv[])
{
  if (argc != 2)
  {
    return 1;
  }
  
  FILE *file = fopen(argv[1], "r");
  
  if (file == NULL)
  {
    return 1;
  }
  
  unsigned char bytes[3];
  fread(bytes, 3, 1, file);
  
  if (bytes[0] == 0xff && bytes[1] == 0xd8 && bytes[2] == 0xff)
  {
    printf("Maybe\n");
  }
  else
  {
    printf("No\n");
  }
  
  fclose(file);
}

 

- main 함수는 사용자의 입력을 받는 것을 알 수 있습니다. 여기서는 파일의 이름을 입력으로 받을 예정입니다.

- 만약 argc가 2가 아니라면, 파일명이 입력되지 않았거나 파일명 외의 다른 인자가 입력되었기 때문에 1(오류)을 리턴하고 프로그램을 종료합니다. 

- 만약 argc가 2라면 입력받은 파일명(argv[1])을 ‘읽기(r)’ 모드로 불러옵니다.

- 만약 파일이 제대로 열리지 않으면 fopen 함수는 NULL을 리턴하기 때문에 이를 검사해서 file을 제대로 쓸 수 있는지를 검사하고, 아니라면 역시 1(오류)를 리턴하고 프로그램을 종료합니다.

 

- 그 후 크기가 3인 문자 배열을 만들고, fread 함수를 이용해서 파일에서 첫 3바이트를 읽어옵니다. fread 함수의 각 인자는 (배열, 읽을 바이트 수, 읽을 횟수, 읽을 파일)을 의미합니다.

- 마지막으로 읽어들인 각 바이트가 각각 0xFF, 0xD8, 0xFF 인지를 확인합니다. 이는 JPEG 형식의 파일을 정의할 때 만든 약속으로, JPEG 파일의 시작점에 꼭 포함되어 있어야 합니다. 따라서 이를 검사하면 JPEG 파일인지를 확인할 수 있습니다.

퀴즈

1. 아래 코드를 실행하면 어떤 결과가 출력될까요?

 

#include <stdio.h>

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

 

> 20

 

2. 아래와 같이 변수 n과 p을 생성했습니다. 변수 n의 메모리 주소를 출력하는 올바른 코드는 다음 중 무엇인가요?

 

int n = 5;
int *p = &n;

 

> printf("%p\n", p);

 

3. CS50 라이브러리의 string 자료형을 사용해 변수 s를 생성했습니다. string s = "CS50" 이와 동일한 의미를 지니는 코드는 무엇인가요?

>  char *s = "CS50"

 

4. char *s = "EDWIDTH"; 와 같이 변수 s를 생성했을 때, 문자 "W"를 출력하는 코드는 무엇인가요?

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

 

5. 아래와 같이 변수 s를 생성한 뒤, 새로운 변수 t에 "EDWIDTH" 문자를 복사하려고 합니다. malloc 함수를 이용해 변수 t를 생성할 때 총 몇 바이트의 메모리를 할당해야 할까요?

> 7바이트 (널 종단 문자도 포함해야 함)

 

6. 할당된 메모리를 해제 하기 위해 사용하는 함수는 무엇인가요?

> free()

 

7. malloc() 함수를 통해 할당받은 메모리는 어디에 위치하게 될까요?

> 힙(heap)

 

8. data.csv 파일에 값을 입력하는 프로그램을 작성하려고 합니다. 이때 필요한 함수가 아닌 것은 무엇인가요?

> fcreate()

 

9. 아래와 같은 코드가 있을 때 출력되는 값은 얼마일까요?

 

int main(void)
{
  x = 5;
  func(&x);
  printf("%i", x);
}

void func(int *y)
{
  *y = 10;
}

 

> 10

 

10. 아래 코드와 같이 swap 함수를 통해 메모리에 저장된 x와 y의 값을 교환하려고 합니다. 즉, swap 함수가 호출된 이후 x는 5, y는 3의 값을 가져야 합니다. main 함수에서 호출되는 swap 함수의 괄호에 포함되어야 할 코드로 적절한 것은 무엇인가요?

 

 

> &x, &y

  •  

 

'CS50' 카테고리의 다른 글

4. 알고리즘  (0) 2023.08.22
3. 배열  (0) 2023.08.17
2. C 언어  (0) 2022.12.14
1. 컴퓨팅 사고  (0) 2022.12.13