본문 바로가기

윤성우의 C Programming

Chapter 25 : 메모리 관리와 메모리의 동적 할당

1. C 언어의 메모리 구조 

옷장안의 서랍장에는 양말, 속옷, 하의, 상의를 구분하여 넣어두는 것과 같이 메모리 구조 역시 함수의 쓰임과 특징에 따라 메모리 구조를 구분하여 할당한다. 

  • 메모리의 구조 
    • 코드 영역 : 실행할 프로그램의 코드가 저장되는 메모리 공간이다. CPU가 코드 명령문을 하나씩 가져가서 실행한다.
    • 데이터 영역: 데이터 영역에는 한번 선언하면 프로그램이 종료될 때까지 남아있는 변수들이 할당된다.
      • 전역변수: 어느 함수에서나 접근이 가능한 변수 
      • static 변수: 특정 함수 안에서만 동작 가능한 변수 
    • 스택 영역: 선언된 함수를 빠져나가면 소멸되는 변수들이 할당된다.
      • 지역변수: 지역변수는 함수 내에서만 접근 가능한 변수
      • 매개변수: 함수 호출 시에 외부로부터 받아오는 변수
    • 힙 영역: 변수의 생성과 소멸의 시점을 프로그래머가 정할 수 있는 변수들이 할당된다. 
  • 프로그램 실행에 따른 메모리의 상태 변화
#include <stdio.h>

// 전역 변수 - 데이터 영역에 저장
int globalVar = 10;

void function(int arg) {
    // 지역 변수 - 스택 영역에 저장
    int localVar = 20;
    
    printf("전역 변수: %d\n", globalVar);
    printf("매개변수: %d\n", arg);
    printf("지역 변수: %d\n", localVar);
}

int main() {
    // 함수 호출 - 스택 영역에 매개변수와 지역 변수가 할당됨
    function(30);
    
    return 0;
}

 

함수를 호출할 때 매개변수가 먼저 스택에 푸시되고, 그 다음 함수의 지역 변수가 스택에 푸시된다.

main 함수의 return 문이 실행되면서 프로그램이 종료되고, 그때 전역변수가 소멸된다. 

함수 호출 과정에서 스택 영역에 쌓이는 순서를 간단하게 도식화 하면 다음과 같다. 

  1. 전역 변수 (globalVar=10;): 데이터 영역에 저장된다.
  2. 함수 (function) 호출 시:
    • 매개변수 (arg = 30): 스택 영역에 먼저 저장된다.
    • 지역 변수 (localVar = 20): 매개변수 위에 스택 영역에 저장된다. 

2. 메모리의 동적 할당 , 힙 영역 

힙 영역에는 전역변수와 지역변수만으로 동작할 수 없는 프로그램을 구현할 때 사용한다. 

지역변수는 특정 함수내에서 빠져나오면 값이 소멸한다는 특징이 있고 전역변수는 호출할 때마다 이전 값이 새로운 값에 덮어씌어져 버린다는 특징이 있다.

 

만약 함수가 매번 호출될 때마다 이전 값은 저장되고 새롭게 할당되면서 또 함수를 빠져나가도 값이 소멸되지 않는 변수가 필요할 때는 인자로 전달된 정수 값에 해당하는 바이트 크기의 메모리 공간을 힙 영역에 할당하고 , 이 메모리 공간의 주소 값을 반환하는 malloc 과 호출을 해제함을 알리는 free 함수를 사용하면 된다. 

 

2-1 힙 영역에 메모리 공간을 할당하는 함수 

 

2-1-1 malloc 함수 

malloc 함수는 함수가 호출될 때마다 문자열 저장을 위한 메모리 공간의 할당이 가능하게 하고,
이 메모리 공간은 함수를 빠져나가도 소멸되지 않고 존재하게 만든다.
#include <stdlib.h> 
void * malloc(size_t size); // 힙 영역으로의 메모리 공간 할당 
void free(void * ptr);  // 힙 영역에 할당된 메모리 공간 해제 

- malloc 함수는 성공 시 할당된 메모리의 주소 값, 실패 시 NULL 반환

 

힙에 할당된 메모리 공간은 포인터 변수를 이용해서 접근하는 방법을 사용한다. 왜냐하면 malloc 과 같은 함수는 값을 전달하는 기능만 담당하고 그 값이 int인지, double 인지는 포인터로 표현해야 하기 때문이다. 

 

void 포인터형은 주소 값을 담는 바구니라고 할 수 있는데 malloc 함수의 반환형은 void 형 포인터이기 때문에 함수의 반환 값에 아무런 가공도 가하지 않으면, 이를 이용해서는 할당된 메모리 공간에 접근이 불가능하다. 

 

그렇다면 왜 void 포인터형을 쓰는 걸까?  

 

왜냐하면 어떠한 자료형 타입에도 사용할 수 있어야 하기 때문에 void 포인터형을 반환하고 특정한 자료형을 사용할 때는 사용자가 자료형을 변환해서 사용할 수 있게 해둔 것이다. 그래서 malloc 함수는 자료형이 무엇이든 상관없이 "결과물" 주소값 만 배달한다고 생각하면 된다.  

 

그래서 void 형으로 반환되는 주소 값을 다음과 같이 적절히 형 변환해서 할당된 메모리 공간에 접근해야 한다. 

int * ptr = (int*)malloc(sizeof(int);
double * ptr2 = (double *)malloc(sizeof(double)); 
int * ptr3 = (int *)malloc(sizeof(int)*7);  // int형 배열 선언
double * ptr4 = (double *)malloc(sizeof(double)*9); //double형 배열 선언

 

참고로 malloc 함수는 메모리 공간의 할당에 실패하면 NULL 을 반환한다. 그래서 메모리 할당을 성공했는지 실패했는지 확인하고 싶다면 if 문으로 코드를 작성해서 확인해볼 수 있다. 

int * ptr = (int *)malloc(sizeof(int));
if(ptr == NULL)
{
printf("메모리 할당 실패")
}

 

free 함수를 호출하지 않으면 프로그램 종료 후에도 메모리가 남게 되는걸까 ? 

결론적으로 메모리 공간은 프로그램이 종료되면 운영체제에 의해서 전부 해제가 되기 때문에 "아니오" 가 정답이다.

그러나 함수가 복잡해지면 free 함수를 호출하지 않았을 때의 잠재적 문제가 크기 때문에 반드시 malloc 과 free 는 한 쌍이라고 생각하는 것이 좋다. 

만능 엔지니어 malloc 함수와 현관문 열쇠를 쥐고 있는 free 함수

 

2-1-2 calloc 함수 

malloc 함수와의 차이점은 메모리 공간의 할당을 위한 인자의 전달방식에 있다. 

#include <stdio.h>
void * calloc(할당할 블록의 갯수, 블록 하나당 바이트 크기);
// 성공 시 할당된 메모리의 주소 값, 실패 시 NULL 반환

 

  • malloc 함수는 할당할 메모리의 전체 바이트 크기를 인자로 받는다. 이는 마치 편의점에서 "담배 40개피를 주세요"라고 하는 것과 비슷하다고 할 수 있다. 여기서는 총 필요한 양만 요청한다.int 형을 40바이트 할당하려면, int가 보통 4바이트를 차지한다고 가정할 때, 10개의 int 형 데이터를 저장할 수 있는 공간이 필요하다. 
  • calloc 함수는 할당할 메모리 블록의 개수와 각 블록의 크기를 인자로 받는다. 이는 "20개피가 들어 있는 담배 2갑 주세요"와 비슷한데, 여기서는 각 담배 갑(블록)에 몇 개피(단위 크기)가 들어있는지와 몇 갑(블록의 개수)이 필요한지를 명시한다. 

malloc 사용 예:

// 40바이트를 힙 영역에 할당해주세요. (int는 보통 4바이트이므로, int 10개)
int *ptr = (int *)malloc(40); // 또는 sizeof(int) * 10

 

calloc 사용 예:

// 4바이트형 블록 10개를 힙 영역에 할당해주세요.
int *ptr = (int *)calloc(10, sizeof(int));

 

또한 malloc 함수는 할당된 메모리 공간을 별도의 값으로 초기화하지 않는다. 따라서 할당된 메모리 공간이 쓰레기 값으로 채워지지만 calloc 함수는 할당된 메모리 공간의 모든 비트를 0으로 초기화시킨다. 이러한 이유로 malloc 말고 calloc을 쓰는 경우가 많다. 

 

2-1-3 메모리 평수를 늘려줄 때 사용하는 realloc 함수 

일반적인 경우 메모리 공간을 갑자기 늘리는건 불가능하지만 예외적으로 힙의 영역에서 realloc 을 사용한다면 가능하다. 

void *realloc(void *ptr, size_t newSize);

 

 

<예시>

int *ptr = malloc(10 * sizeof(int)); // 초기 메모리 할당
if (ptr != NULL) {
    ptr = realloc(ptr, 20 * sizeof(int)); // 메모리 크기 조정
    if (ptr == NULL) {
        // 메모리 재할당에 실패한 경우의 처리
    }
}

 

위 코드에서 실행결과는 malloc 함수가 반환한 주소 값의 공간의 여유가 얼마나 있느냐에 따라 달라진다. 

공간의 여유가 충분하면 malloc 함수가 반환한 주소 값과 realloc 함수가 반환한 주소 값이 같다. 

공간의 여유가 충분하지 않으면 realloc 은 새로운 장소에 메모리 영역을 할당하고 기존 블록을 해제하기 떄문에 malloc 함수가 반환한 주소 값과 realloc 함수가 반환한 주소 값이 다르다.  이 과정에서 원본 포인터(ptr)는 더 이상 유효하지 않게 될 수 있으며, realloc 함수는 새로 할당된 메모리 블록의 포인터를 반환한다.