[C] 함수 내에서의 메모리 할당과 해제

함수에서의 메모리 할당과 해제

C에서의 메모리 할당/해제의 개념은 설명은 간단한데 실제 적용시에는 무척이나 어렵다. 물론 뉴비시절에 책 보고 따라 코드를 써볼 때에는 이게 그다지 와 닿지 않는다. 곰곰히 생각해보니 그런 책의 예제들은 그냥 숫자값만 다루다보니 그런 것 같다.

위 글에서 이어서 함수 하나를 만들어보자. 이 함수는 문자열 상수를 받아서 이를 대문자로 변경한 문자열을 만들어준다. 문자열 상수는 변경할 수 없으니, 새로운 문자열을 생성해야 한다. 기본적인 아이디어는 다음과 같이 구현된다.

char * cloneUpper(const char *origin){
    char *temp, *r;
    int l, i;
    temp = origin;
    l = strlen(origin);
    r = (char*)malloc(l+1);
    r[l] = 0;
    i = 0;
    while(*(temp+i)!=0)
    {
        *(r+i) = toupper(*temp+i);
        i++;
    }
    return r;
}

새로운 문자열을 위해 malloc을 사용하여 적당한 길이의 메모리 공간을 할당하고, 여기에 origin의 각 문자를 대문자로 만들어서 한글자씩 집어넣는 방식을 사용해 전체 문자열을 대문자화하여 넣어준다. 결국 원래 입력받은 문자열 상수를 모두 대문자로 변경한 문자열이 완성되고, 함수는 이 새로운 문자열의 포인터를 리턴한다.

문제는 malloc을 써서 메모리를 할당했기 때문에 해제를 해줘야 한다는 점이다. 그런데 return 문을 만나면 함수가 종료되므로 free 구문은 return 문 보다 먼저 나와야 하는데, 반환하기 전에 반환해야 할 값의 메모리를 해제해버리면 기껏 만들어둔 값이 망가져버리게 된다.

여기서 다시 C에서의 메모리 관리 원칙을 살펴보자.

  1. malloc(calloc 포함)과 free는 짝이 맞아야 한다.
  2. 메모리를 할당한 측에서 일차적으로 해제의 책임을 진다. 즉 함수내에서 할당했다면 해제의 책임도 함수가 진다.
  3. 2가 어렵다면 함수를 호출한 쪽에서 책임지고 free 해야 한다.

그리고 이게 너무 삶을 빡빡하게 만든다면 다음도 참고하는 게 좋겠다.

좋은 OS라면 프로그램이 종료될 때 프로그램이 할당한 메모리를 모두 파괴하고 해제해준다. 하지만 이런 기능이 없는 OS도 있고, 좋은 습관은 아니기에 추천하지 않는다.

그렇다 이렇게 함수를 만들었다면 이를 호출한 쪽에서 해제해주면 된다. 따라서 전체 프로그램 코드는 다음과 같아야 한다.

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

char * cloneUpper(const char *origin);

int main(void)
{
    char *s = "abcdefg";
    char *n = cloneUpper(s); // cloneUpper 함수는 메모리를 새롭게 할당한다.
    printf("%s\n", n);
    free(n); // 이 곳에서 해제해준다.
    return 0;
}

char * cloneUpper(const char *origin){
    char *temp, *r;
    int l, i;
    temp = origin;
    l = strlen(origin);
    r = (char*)malloc(l+1);
    r[l] = 0;
    i = 0;
    while(*(temp+i)!=0) {*(r+i) = toupper(*temp+i); i++; } return r;
}

여기서 잠깐 이와 하는 일이 비슷한 기본함수를 떠올려보자. 바로 문자열을 복사해주는 strcpy 이다. 이 함수는 문자열 상수를 다른 문자열 포인터로 복사해준다. 정확히 말하면 사본을 새롭게 생성하지는 않고, 사본을 생성하는 과정을 수행하는 함수이다. 이 함수는 string.h 라는 기본 라이브러리에 정의되어 있는데 그 원형은 다음과 같다.

char* __cdecl    strcpy (char*, const char*);

문자열 상수와 더불어 이 문자열을 복사될 사본의 포인터를 받는다. 즉, 이 함수를 사용하려면 ‘사본’의 메모리 영역을 미리 만들어주어야 한다는 의미이다. 이는 함수 내에서 메모리 할당을 피함으로써 호출하는 쪽에서 메모리 할당과 해제를 모두 책임지도록 하는 방법이다. 함수를 ‘편안히’ 활용하기에는 어렵지만, 그래도 호출할 때마다 해제를 해줘야 하는 것을 기억해야 하는 강박은 덜어주는 식으로 디자인했다.

같은 방식으로 cloneUpper의 디자인을 변경하면 다음과 같다. 훨씬 간결해진다.

char * cloneUpper(char *dest, const char *origin) {
    char *temp = origin;
    int i = 0;
    while(*(temp+i)!=0) {*(r+i) = toupper(*temp+i); i++; } return r;
}