[CS50 2019] 4. Memory

모두를 위한 컴퓨터 과학

1. 포인터

  • 메모리 주소를 저장하는 변수로, 프로그램이 메모리 상의 위치추적하고 조작하는 데 사용
  • 다른 변수, 배열, 구조체 또는 함수 등의 주소를 저장하고 해당 메모리 위치에 직접 접근할 수 있게 해줌
  • 포인터를 선언할 때는 변수의 자료형포인터 연산자 *를 사용
  • 또한, 변수의 메모리 주소를 받아올 때에는 연산자 &을 사용
#include <stdio.h>

int main(void)
{
   int n = 50;
   int *p = &n;
   printf("%p\n", p);
   printf("%i\n", *p);
}
>>
0x7ff7bfeff1ec
50
  • 정수형 변수 n에는 50이라는 값이 저장됨
  • *p라는 포인터 변수에는 &n이라는 값, 즉 변수 n의 주소를 저장함

2. 문자열

char 포인터로 문자열 출력 가능

#include <stdio.h>

int main(void)
{
    char *s = "EMMA";
    printf("%s\n", s);
}
EMMA
  • char *s에서 s라는 변수는 문자에 대한 포인터를 저장
  • 더 상세히는 문자열의 가장 첫번째 문자, 즉 s[0]의 주소를 저장
  • printf 함수는 포인터 s가 가리키는 메모리 주소부터 널 종료 문자를 만날 때까지 문자를 출력


printf("%c\n", *s);         // "E"
printf("%c\n", *(s + 1));   // "M"
printf("%c\n", *(s + 2));   // "M"
printf("%c\n", *(s + 3));   // "A"
  • 문자열은 첫번째 문자를 시작으로 메모리상에서 바로 옆에 저장되어 있다.
  • 즉, 가장 첫번째 문자에 해당하는 주소값을 하나씩 증가시키면 바로 옆에 있는 문자의 값을 출력할 수 있다.

문자열 비교

if (s == t)
{
    printf("Same\n");
}
else
{
    printf("Different\n");
}
  • C 언어에서 단순히 == 연산자로 문자열을 비교하면 제대로 동작하지 않는다.
  • 왜냐하면 문자열이 저장된 변수를 바로 비교하게 되면 두 문자열의 주소를 비교하기 때문이다.

문자열 복사

printf("Enter a string: ");
gets(s);

char *t = s;
  • 문자열 s를 문자열 t에 복사할 때, 단순히 = 연산자로 복사하면 메모리의 주소가 저장되기 때문에 t와 s는 동일한 주소를 가리키게 되고 t를 통한 수정은 s에도 그대로 반영된다.
  • 따라서 malloc 함수를 사용하여 메모리를 할당해준 다음 루프를 돌면서 문자 하나 하나를 복사해줘야 한다.

3. 메모리

메모리 할당

malloc 함수

void* malloc(size_t size);
  • 메모리 동적 할당을 위해 사용되는 함수
  • 특정 크기의 메모리 블록을 할당하고, 해당 메모리 블록의 시작 주소를 반환

메모리 해제

free 함수

void free(void *ptr)
  • malloc 함수를 이용해 메모리를 할당한 후에는 free 함수로 메모리를 해제해줘야 함
  • 그렇지 않은 경우 메모리에 저장한 값은 쓰레기 값으로 남게 되어 메모리 용량의 낭비가 발생 (메모리 누수)

메모리 교환

#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 함수가 제대로 동작하지 않는다.
  • a와 b는 함수에 전달된 변수들(x, y)의 값을 복제하여 저장 (서로 다른 메모리 주소에 저장)
  • 즉, 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;
}

메모리 영역

머신 코드 (Machine Code)

  • 프로그램의 기계 코드가 저장되는 공간
  • CPU가 실행할 명령어들이 저장되어 있으며, 프로그램이 실행될 때 변경되지 않는 읽기 전용 메모리

전역 영역 (Globals)

  • 전역 변수정적 변수가 할당되는 영역
  • 프로그램 시작 시에 할당되고 프로그램 종료 시까지 유지
  • 초기화된 전역 변수는 초기값으로 초기화되며, 초기화되지 않은 전역 변수는 0 또는 null 값으로 초기화

힙 (Heap)

  • 동적으로 할당된 메모리가 저장되는 영역
  • 프로그램이 실행 중에 필요에 따라 메모리를 동적으로 할당하고 해제할 수 있음 (malloc,free 또는 new,delete를 통해 수행)
  • 힙은 사용자가 메모리를 직접 관리할 수 있는 영역으로, 크기가 동적으로 변할 수 있음

스택 (Stack)

  • 지역 변수함수 호출과 관련된 정보를 저장하는 영역
  • 함수가 호출될 때 지역 변수, 함수의 매개변수, 함수의 반환 주소 등이 스택에 저장되며, 함수가 종료되면 정보가 스택에서 제거
  • 스택은 LIFO(후입선출) 구조를 가지며, 메모리 할당 및 해제가 자동으로 이루어짐

메모리 오버플로우

버퍼 오버플로우 (Buffer Overflow)

  • 가장 일반적이고 잘 알려진 메모리 오버플로우 형태
  • 버퍼의 크기를 넘어서 데이터가 복사되거나 쓰여지는 상황
  • 주로 배열과 같은 자료 구조에서 발생

힙 오버플로우 (Heap Overflow)

  • 동적으로 할당된 메모리인 힙(Heap)에서 발생하는 오버플로우
  • malloc이나 free와 같은 함수를 사용할 때, 할당된 메모리 영역을 벗어나 데이터를 쓰려고 할 때 발생

스택 오버플로우 (Stack Overflow)

  • 스택 메모리에서 발생하는 오버플로우
  • 주로 재귀 호출이 깊게 이어지거나 지역 변수가 많아지면서 스택의 한계를 초과할 때 발생

4. 파일 입출력 함수

fopen: 파일 열기

FILE* fopen(const char* filename, const char* mode);
  • 지정된 파일을 열고 파일에 대한 포인터를 반환
  • 첫번째 인자는 파일의 이름, 두번째 인자는 모드로 r은 읽기, w는 쓰기, a는 덧붙이기를 의미


fclose: 파일 닫기

int fclose(FILE* stream);
  • 열려 있는 파일을 닫음
  • 함수의 인자는 닫을 파일에 대한 포인터


fprintf: 파일에 데이터 출력

int fprintf(FILE* stream, const char* format, ...);
  • 지정된 파일에서 서식화된 출력 수행
  • 첫번째 인자는 출력을 할 파일에 대한 포인터, 두번째 인자는 출력 형식을 지정하는 서식 문자열


fread: 파일에서 읽어오기

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
  • 지정된 파일로부터 특정 크기의 데이터지정된 개수만큼 읽어와서 메모리에 저장
  • 각 인자는 읽어온 데이터를 저장할 배열, 읽을 바이트 수, 읽을 횟수, 데이터를 읽어올 파일을 의미

예제 코드

파일을 읽고 JPEG 파일인지를 검사하는 프로그램

#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];   // 크기가 3인 char 배열 선언
    fread(bytes, 3, 1, file);   // 파일에서 첫 3byte 읽어오기
    
    // JPEG 파일의 파일 시그니처 검사
    if (bytes[0] == 0xff && bytes[1] == 0xd8 && bytes[2] == 0xff)
    {
        printf("Maybe\n");
    }
    else
    {
        printf("No\n");
    }
    fclose(file);   // 파일 닫기
}