CS: Memory

이전에 들어놓은 것이 있어서 이어서 들으려고 한다. 정리하는 목적은 안 쓰면 까먹는 나에게 언제든지 다시 봐도 기억이 나게 하기 위함이다.

학습 목표

  • 문자열 복사
  • 메모리 할당과 해제
  • 메모리 교환, 스택, 힙
  • 파일 쓰기
  • 파일 읽기

문자열 복사

s에는 문자열이 아닌 메모리의 주소가 저장되기 때문에 변수 t에 그대로 s를 할당하게 되면 어느 한 곳에서 수정해도 둘이 동시에 바뀐다. 즉, 얕은 복사(Shallow Copy)가 된다.

char *s = "emma";
char *t = s;

그렇다면 이를 어떻게 실제 메모리상에서 복사할 수 있을까? 즉, 깊은 복사(Deep Copy)를 어떻게 할 수 있을까? 간단하다. 메모리 할당 함수를 사용하면 된다.

s 문자열의 길이에 널 종단 문자를 포함한 만큼 메모리를 할당한다. 그런 후 루프를 돌면서 일일이 복사해주면 된다.

#include <stdlib.h> // malloc
#include <string.h> // strlen

int main(void)
{
    char *s = "emma";
    char *t = malloc(strlen(s) + 1); // 메모리 할당

    for (int i = 0; i < strlen(s) + 1; i++)
    {
        t[i] = s[i];
    }
}

이러한 루프 방식으로 복사해주는 기능을 다른 사람이 만들어 놓은 함수가 있다.

#include <stdlib.h>
#include <string.h> // strcpy

int main(void)
{
    char *s = "emma";
    char *t = malloc(strlen(s) + 1);

    strcpy(t, s); // t에 s의 내용을 복사함.
}

메모리 할당과 해제

메모리를 할당했다면 변수를 다 사용한 후에는 해제해줘야 한다. 그렇지 않으면 쓰레기 값으로 남아서 메모리 누수가 발생한다.

메모리 누수를 확인하기 위해 디버깅 도구로 valgrind라는 프로그램을 추천한다.

다음과 같은 상황이 있다고 하자 배열은 0~9로 10개를 할당했는데 11번째, 즉 10 인덱스에 할당하는 경우 버퍼 오버플로우가 발생한다. malloc()로 할당했지만 해제하지 않아서 메모리 누수도 발생한다.

#include <stdlib.h>

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

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

해결 방법은 간단하다. 버퍼 오버플로우는 인덱스를 변경하면 되고 메모리 누수로는 해제하는 함수인 free()를 사용하면 된다.

#include <stdlib.h> // free

void f(void)
{
    int *x = malloc(10 * sizeof(int));
    x[9] = 0; // 인덱스 변경
    free(x);
}

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

메모리 교환, 스택, 힙

만약 swap을 구현하고 싶다고 생각해보자. 함수에 넣은 인자들은 값을 복제해서 다른 메모리 주소에 저장하기 때문에 일반 변수로는 swap이 되지 않는다. 그렇다면 주소를 인자로 넘겨줘서 포인터를 이용해 메모리 주소를 swap하는 방식을 사용하여 해결할 수 있다.

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

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

    swap(&x, &y); // 주소를 넘겨준다.
}

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

자세히 살펴보자면 메모리에는 저장되는 구역이 나뉘어져 있다. Machine Code에는 컴파일된 바이너리가 저장되고 Globals에는 전역 변수가 저장된다.

메모리 데이터 구분

Heap에는 malloc로 할당된 메모리의 데이터가 저장되어 범위가 아래로 늘어나고 Stack에는 프로그램 내의 함수와 관련된 것들이 저장되어 범위가 위로 늘어난다. Stack에는 첫 번째로 x와 y 변수가 있을 테고 두 번째로 swap 함수 내의 tmp, a, b가 들어 있을 것이다. 이 둘은 따로 움직이기에 영향이 미치지 않는다. 그러나 포인터 변수를 사용하면 a와 b가 각각 x, y 변수에 연결되어 있으므로 원하는 결과물을 얻을 수 있다.

입력 받기

메모리에 대해서 알았으니 이젠 scanf()를 사용해볼 차례이다. 앞서 말했던 것처럼 도우미 함수나 다른 사람이 작성한 함수를 사용할 때는 주소를 넘겨줘야 저장이 된다.

int main(void)
{
    int x;
    scanf('%i', &x); // 사용자 입력
}

그렇다면 문자열은 어떻게 사용할 수 있을까?

예를 들어 아래와 같이 작성했다고 치자 작동하지 않는다. 왜냐하면 char *는 주소를 저장하는데 NULL로 초기화하면 결국엔 저장될 공간을 할당하지 못했다는 뜻이기 때문이다.

int main(void)
{
    char *s = NULL; // 초기화
    scanf('%s', &s);
}

그렇기 때문에 배열을 사용하면 해결할 수 있다. 문자열은 문자가 여러개인 것과 똑같기 때문이다. clang 컴파일러는 문자 배열을 포인터러철 다루기 때문에 주소가 아닌 그대로 입력해주면 된다.

int main(void)
{
    char s[5]; // 4글자
    scanf('%s', s);
}

파일 쓰기

fopen 두 번째 인자인 모드는 3가지가 있다. read, write, append. file이라는 포인터에 FILE 자료형으로 파일을 연 것이다. scanf를 사용하기에는 현재 에러를 처리해야하는 경우가 많으므로 다른 함수를 사용했다.

fprintf는 printf처럼 같은 기능이지만 file에 출력하는 것이다.

fclose는 파일을 닫는다. 작업을 종료한다는 뜻이다.

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

파일 읽기

파일을 읽어서 파일 형식이 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;
    }

    // 3 바이트만 읽는 것이다.
    // -128 ~ 127이 아닌 0 ~ 255를 의미하니 unsigned를 붙여준다.
    unsigned char bytes[3];
    fread(bytes, 3, 1, file); // 배열, 읽을 바이트 수, 읽을 횟수, 읽을 파일

    // 3 바이트가 이 형식으로 시작하면 아마 JPEG 파일일 것이다.
    if (bytes[0] == 0xff && bytes[1] == 0xd8 && bytes[2] == 0xff)
    {
        printf("Maybe\n");
    }
    else
    {
        printf("No\n");
    }

    // 파일 닫는다.
    fclose(file);
}

강의명

  • 부스트코스 : 모두를 위한 컴퓨터 과학 (CS50 2019)

카테고리:

업데이트:

댓글남기기