Skip to content

28. 메모리(Memory)


1. 키워드

  • 메모리(Memory)
  • malloc(Memory Allocation)
  • 동적 메모리 할당(Dynamic Memory Allocation)
  • 스택(Stack)과 힙(Heap)
  • 메모리 누수(Memory Leak)
  • memset(Memory Set)
  • 널 포인터(NULL Pointer)


2. 메모리 사용하기

  • 이번에는 포인터에 원하는 만큼 메모리 공간을 할당받아 사용하는 방법을 알아보자.


  • 먼저 다음과 같이 메모리는 malloc사용free 패턴으로 사용한다.


001


3. 메모리 할당하기

  • 메모리를 사용하려면 malloc 함수로 사용할 메모리 공간을 확보해야 한다.
  • 이때 필요한 메모리 크기는 바이트 단위로 지정한다.
  • 메모리 할당과 해제 함수는 stdlib.h 헤더 파일에 선언되어 있다.


포인터 = malloc(크기);
성공하면 메모리 주소를 반환. 실패하면 NULL을 반환


  • 다음 내용을 입력한 뒤 실행해 보자.
  • 만약 컴파일할 때 "error C2440: '=': 'void *'에서 'int *'(으)로 변환할 수 없습니다." 에러가 발생한다면 파일의 확장자가 .c가 맞는지 확인한다.
  • 파일 확장자가 .cpp이면 C++ 컴파일러를 사용하므로 에러가 발생한다.


#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일

int main()
{
    int num1 = 20; // int 타입 변수 선언
    int *numPtr1;  // int 타입 포인터 선언

    numPtr1 = &num1; // num1의 메모리 주소를 구하여 numPtr에 할당

    int *numPtr2; // int 타입 포인터 선언

    numPtr2 = malloc(sizeof(int)); // int 타입의 크기 4바이트만큼 동적 메모리 할당

    printf("%p\n", numPtr1); // 변수 num1의 메모리 주소 출력
                             // 컴퓨터마다, 실행할 때마다 달라짐

    printf("%p\n", numPtr2); // 새로 할당된 메모리의 주소 출력
                             // 컴퓨터마다, 실행할 때마다 달라짐

    free(numPtr2); // 동적으로 할당한 메모리 해제

    return 0;
}

// 0x006bfa60
// 0x009659f0


  • 메모리를 할당할 때는 malloc 함수를 사용하며 할당할 메모리 공간의 크기를 넣어준다.
  • 여기서는 sizeof 연산자를 사용하여 int타입의 크기(4바이트)만큼 메모리를 할당했다.
  • 특히 원하는 시점에 원하는 만큼 메모리를 할당할 수 있다고 하여 동적 메모리 할당이라 부른다.


numPtr2 = malloc(sizeof(int)); // int의 크기 4바이트만큼 동적 메모리 할당


  • printf 함수로 numPtr1numPtr2의 값을 출력해 보면 메모리 주소가 출력된다.


printf("%p\n", numPtr1); // 변수 num1의 메모리 주소 출력
                         // 컴퓨터마다, 실행할 때마다 달라짐

printf("%p\n", numPtr2); // 새로 할당된 메모리의 주소 출력
                         // 컴퓨터마다, 실행할 때마다 달라짐


  • 여기서 numPtr1에는 일반 변수의 메모리 주소를 할당했고, numPtr2에는 malloc 함수로 메모리를 할당했다.
  • 같은 메모리 주소라도 내부적으로는 약간의 차이가 있다.
  • 스택과 힙 두 가지인데 변수는 스택에 생성되며 malloc 함수는 힙 부분의 메모리를 사용한다.
  • 이때 스택, 힙의 위치와 커지는 방향은 OS 및 플랫폼에 따라 달라질 수 있다.


002


  • 스택과 힙의 큰 차이점은 메모리 해제에 있다.
  • 스택에 생성된 변수는 사용한 뒤 따로 처리를 해주지 않아도 되지만, malloc 함수를 사용하여 힙에서 할당한 메모리는 반드시 해제를 해줘야 한다.
  • 따라서 다음과 같이 free 함수로 메모리를 해제한다.


free(포인터);
free(numPtr2); // 동적으로 할당한 메모리 해제


  • 메모리 해제는 선택이 아닌 필수이다.
  • 메모리를 할당만 하고 해제를 해주지 않으면 결국에는 시스템의 메모리가 부족해지므로 OS가 프로그램을 강제로 종료시키거나 메모리 할당에 실패하게 된다.
  • 특히 메모리를 해제하지 않아 메모리 사용량이 계속 증가하는 현상을 메모리 누수라고 부른다.


4. 메모리에 값 저장하기

  • 이번에는 할당한 메모리에 값을 저장해 보자.


#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일

int main()
{
    int *numPtr; // int 타입 포인터 선언

    numPtr = malloc(sizeof(int)); // int 타입의 크기 4바이트만큼 동적 메모리 할당

    *numPtr = 10; // 포인터를 역참조한 뒤 값 할당

    printf("%d\n", *numPtr); // 포인터를 역참조하여 메모리에 저장된 값 출력

    free(numPtr); // 동적 메모리 해제

    return 0;
}

// 10


  • malloc 함수로 할당한 메모리에 값을 저장할 때는 *numPtr = 10;처럼 포인터를 역참조한 뒤 값을 저장하면 된다.
  • 마찬가지로 printf 함수로 값을 출력할 때도 포인터를 역참조하여 값을 가져오면 된다.
  • 메모리를 할당하고 사용한 뒤에는 반드시 free 함수로 해제를 해준다.


malloc 함수에 sizeof(int) * 1024 * 1024 * 1024를 넣으면 어떻게 될까?

  • 다음과 같이 작성해 보자.


numPtr2 = malloc(sizeof(int) * 1024 * 1024 * 1024);


  • 문제없이 할당이 된다.
  • 계산을 해보면 4,294,967,296인데 GiB로는 4GiB이다.
  • 더 큰 메모리도 할당할 수 있지만 실제로 값을 저장할 때는 시스템의 한계 이상 저장할 수 없다.


5. 메모리 내용을 한꺼번에 설정하기

  • 포인터를 역참조한 뒤 값을 할당할 때는 해당 자료형 크기만큼만 할당할 수 있다.
  • string.h 헤더 파일에 선언되어 있는 memset 함수를 사용하면 메모리의 내용을 원하는 크기만큼 특정값으로 설정할 수 있다.
  • 이때 설정하는 크기는 바이트 단위이다.


memset(포인터, 설정할값, 크기);
 설정이 끝난 포인터를 반환


  • 다음 내용을 입력한 뒤 실행해 보자.


#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일
#include <string.h> // memset 함수가 선언된 헤더 파일

int main()
{
    long long *numPtr = malloc(sizeof(long long)); // long long 타입의 크기 8바이트만큼 동적 메모리 할당

    memset(numPtr, 0x27, 8); // numPtr이 가리키는 메모리를 8바이트만큼 0x27로 설정

    printf("0x%llx\n", *numPtr); // 27이 8개 들어가 있음

    free(numPtr); // 동적으로 할당한 메모리 해제

    return 0;
}

// 0x2727272727272727


  • 먼저 memset 함수를 사용하려면 string.h 또는 memory.h 헤더 파일을 포함해야 한다.
  • 그리고 memset 함수에 포인터, 설정할 값, 설정할 크기를 넣으면 된다.
  • 여기서는 memset(numPtr, 0x27, 8);과 같이 사용했으므로 numPtr이 가리키는 메모리에는 16진수 278개 들어가게 된다.


  • memset 함수는 주로 다음과 같이 설정할 값을 0으로 지정하여 메모리의 내용을 모두 0으로 만들 때 주로 사용한다.


memset(numPtr, 0, 8); // numPtr이 가리키는 메모리를 8바이트만큼 0으로 설정


자료형의 크기와 포인터의 크기

  • memset 함수에 설정할 크기를 지정할 때 보통 숫자 대신 sizeof를 사용한다.


long long *numPtr = malloc(sizeof(long long));

memset(numPtr, 0, sizeof(long long)); // numPtr이 가리키는 메모리를 long long 타입의 크기만큼 0으로 설정


  • 여기서 메모리를 sizeof(long long) 크기만큼 할당했으므로 설정할 크기도 sizeof(long long)과 같이 지정해야 하며 sizeof(long long *)과 같이 포인터의 크기를 지정하면 안 된다.
  • 포인터의 크기는 메모리 주소의 크기일 뿐 실제 메모리가 차지하는 크기가 아니다.
  • 이 부분은 char 타입 포인터에 메모리를 할당해 보면 잘 알 수 있다.


char *cPtr = malloc(sizeof(char)); // char 타입의 크기 1바이트만큼 동적 메모리 할당

memset(cPtr, 0, sizeof(char));   // char 타입의 크기 1바이트만큼 0으로 설정(올바른 방법)
memset(cPtr, 0, sizeof(char *)); // 32비트: char 타입 포인터의 크기 4바이트만큼 0으로 설정(잘못된 방법)
                                 // 64비트: char 타입 포인터의 크기 8바이트만큼 0으로 설정(잘못된 방법)

free(cPtr);


  • memset(numPtr, 0, sizeof(char));는 메모리를 1바이트만큼 0으로 설정하니 문제가 없다.
  • 하지만 memset(numPtr, 0, sizeof(char *));는 32비트에서 4바이트, 64비트에서 8바이트만큼 0으로 설정하므로 할당받은 메모리 크기를 넘어서게 된다.


  • 다른 예로 32비트에서 sizeof(long long)은 8바이트인데 sizeof(long long *)는 4바이트이므로 할당받은 메모리보다 작은 공간을 설정하게 된다.
  • 64비트에서 sizeof(long long *)는 8바이트로 그냥 크기가 일치한 것일 뿐 잘못된 방법이다.


long long *numPtr = malloc(sizeof(long long)); // long long 타입의 크기 8바이트만큼 동적 메모리 할당

memset(numPtr, 0, sizeof(long long));   // long long 타입의 크기 8바이트만큼 0으로 설정(올바른 방법)
memset(numPtr, 0, sizeof(long long *)); // 32비트: long long 타입 포인터의 크기 4바이트만큼 0으로 설정(잘못된 방법)
                                        // 64비트: long long 타입 포인터의 크기 8바이트만큼 0으로 설정(잘못된 방법)

free(numPtr);


  • memset 함수에서 sizeof를 사용할 때는 이러한 부분을 주의해야 한다.


6. 널 포인터 사용하기

  • 메모리가 할당된 포인터도 있지만, 메모리가 할당되지 않은 포인터도 있다.


#include <stdio.h>

int main()
{
    int *numPtr1 = NULL; // 포인터에 NULL 저장

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

    return 0;
}

// 0x0


  • NULL이 들어있는 포인터를 널 포인터라고 하며 아무것도 가리키지 않은 상태를 뜻한다.
  • 따라서 역참조는 할 수 없다.


  • 실무에서는 다음과 같이 포인터가 NULL인지 확인한 뒤 NULL이면 메모리를 할당하는 패턴을 주로 사용한다.


if (ptr == NULL) // ptr이 널 포인터라면
{
    ptr = malloc(1024); // 1024바이트만큼 메모리 할당
}

References