Philip Withnall
philip.withnall@collabora.co.uk 2015
C언어에서 메모리 할당 및 해제 관리
조성호
shcho@gnome.org 2016-2018.

# 메모리 관리

그놈 스택은 근본적으로 C로 작성했으므로 동적 할당 메모리는 직접 관리합니다. GLib 편의 API를 사용하여 메모리 관리를 분명하게 처리할 수 있긴 하지만, 프로그래머는 그래도 코드를 작성할 때는 항상 메모리의 상태를 고려해야합니다.

독자 여러분이 `malloc()` 함수 및 `free()` 함수를 활용하여 메모리 힙 영역 할당에 익숙하고, 이에 동등한 GLib의 `g_malloc()` 함수와 `g_free()` 함수의 사용법을 알고 있음을 전제로 합니다.

#

        요약

        피해야 할 세가지 상황을 중요하지 않은 항목부터 나열해보았습니다:

          해제하고 나서 메모리를 활용(해제 후 활용).
          할당하기 전에 메모리를 활용.
          할당하고 나서 메모리를 해제하지 않음(누수).

        비순서 핵심 원칙:

          어떤 변수 참조를 소유하는 지 여부를 결정하고 문서로 남겨두십시오. 실행 시간에 이 결정이 바뀌면 안됩니다().
          함수 영역의 소유 참조 전달 여부를 결정하고 문서로 남겨두십시오().
          관련 소유 참조 전달이 일어나는 함수 호출 및 함수 반환시, 각 할당 동작 단계를 확인하십시오(, , ).
          가능하면 명시적으로 종결처리하기 보다는, 참조 카운팅을 활용하십시오().
          가능한 부분에 g_clear_object() 함수 같은 GLib 편의 함수를 활용하십시오().
          코드 흐름상 메모리 관리 방식을 별개로 처리하지 마십시오().
          거대 복잡 함수에서는 단일 흐름상 처리 방식을 활용하십시오().
          Valgrind 내지는 주소 정리기로 메모리 누수 상태를 확인해야합니다().

# 메모리 관리 원리

프로그래머가 보통 접근하는 메모리 관리 개념은 어떤 변수에 메모리를 할당했는지, 더 이상 필요하지 않은 변수를 직접 해제했는지 지켜보는 방식입니다. 이런 접근 방식이 올바른 방식이지만, 어떤 코드 부분에서 할당한 메모리(할당 구역) 일부를 해제 처리하는가(이를 테면, 함수, 구조체, 객체)소유 개념을 도입하여 분명히 할 수 있습니다. 각 할당 부분 별로 분명히 하나의 소유 주체가 있습니다. 이 소유 주체는 프로그램을 실행하는 동안, 다른 코드 부분으로 소유(참조)를 전달하여 바꿀 수 있습니다. 각 변수는 코드 범위에서 항상 참조를 소유하는지 여부에 따라 참조를 소유하거나, 비소유합니다. 각 함수 매개 변수와 반환 형식은 값을 전달하든 안 하든, 소유 참조를 전달합니다. 메모리에서 해제하지 않은 일부 메모리 공간을 활용하는 코드가 있다면, 해당 코드에서 메모리 누수가 발생하는 상황이 됩니다. 이미 할당한 메모리를 해제한 상황에서 또 해제하는 코드가 있다면, 중복 해제가 되며, 여기서 설명한 두가지 경우는 메모리 관리에 있어 바람직한 코드가 아닙니다.

어떤 변수에서 참조를 소유했는지 통계적으로 헤아려보면, 메모리 관리는 코드 영역을 벗어나기 전에 할당한 메모리 영역을 어떤 상태 확인 없이 해제하는 단순한 과정이 될 수 있으며, 비 소유 할당 메모리 공간을 해제하는 과정이 아닙니다( 참고). 이런 메모리 관리에 있어 답이 필요한 핵심 질문은 "이 메모리를 어떤 코드에서 다루는가?" 입니다.

몇가지 중요한 제한점이 있습니다. 변수 소유 상태는 실행 시간에 절대로 바꾸면 안됩니다. 이 제한 원칙은 메모리 관리 단순화의 핵심입니다.

예를 들어 다음 함수를 가정해보겠습니다:

gchar *generate_string (const gchar *template); void print_string (const gchar *str);

다음 코드는 주체 전달이 일어나는 부분을 표시하려 주석을 달아두었습니다:

gchar *my_str = NULL; /* owned */ const gchar *template; /* unowned */ GValue value = G_VALUE_INIT; /* owned */ g_value_init (&value, G_TYPE_STRING);

/* Transfers ownership of a string from the function to the variable. */ template = "XXXXXX"; my_str = generate_string (template);

/* No ownership transfer. */ print_string (my_str);

/* Transfer ownership. We no longer have to free @my_str. */ g_value_take_string (&value, my_str);

/* We still have ownership of @value, so free it before it goes out of scope. */ g_value_unset (&value);

몇가지 주안점이 있습니다. 우선, 변수 선언의 ‘owned‘ 주석은 해당 변수를 자체 범위에서 소유했음을 나타내며, 코드 실행 후 실행 시점이 외부로 나가는 경우 메모리에서 해제해야합니다. 반대는 ‘unowned‘이며, 해당 코드 범위에서 소유하지 않아, 실행 시점이 외부로 나가더라도 해제하면 안됩니다. 이와 비슷하게, 메모리 할당시에는 소유 상태를 넘기면 안됩니다.

두번째로, 변수형 수정자는 소유 여부를 전달할 지 여부를 반영합니다. `my_str`는 자체 영역에서 소유하기 때문에 `gchar`고, 반면에 `template`는 소유하지 않음을 나타내려 `const`를 붙입니다. 이와 비슷하게, `generate_string()` 함수의 매개변수 `template`와 `print_string()` 함수의 매개 변수 `str`은 함수를 호출할 때, 소유 여부를 전달하지 않으므로 `const`를 붙입니다. `g_value_take_string()` 함수의 문자열 매개변수에는 소유권을 전달 하므로, 이 매개변수의 형식을 `gchar`로 넣을 수 있습니다.

(참고로 `const`를 붙이면 안되는 `GObject`와 하위 클래스에는 해당하지 않습니다. 문자열과 단순 `struct`에만 해당합니다.)

마지막으로, 일부 라이브러리에서는 소유 이전을 나타내려 함수 작명 원칙을 활용하는데, 함수 이름에 ‘take’가 있으면 `g_value_take_string()` 함수처럼 매개변수의 소유를 완전히 넘겨 받음을 나타냅니다. 참고로 다른 라이브러리는 아래와 같이 개별 원칙을 따릅니다:

함수 이름 | 형식 1 (표준) | 형식 2 (대안) | 형식 3 (gdbus-codegen)
--- | --- | --- | ---
get | 전달 안함 | 일부 전달 | 완전한 전달
dup | 완전한 전달 | 사용 안함 | 사용 안함
peek | 사용 안함 | 사용 안함 | 전달 안함
set | 전달 안함 | 전달 안함 | 전달 안함
take | 완전한 전달 | 사용 안함 | 사용 안함
steal | 완전한 전달 | 완전한 전달 | 완전한 전달

이상적으로, 모든 함수에는 관련 매개 변수에 `(transfer)` 인트로스펙션 주석이 있으며, 값을 반환합니다. 값 반환에 실패할 경우에 대해 반환 값 주체의 판단에 활용할 지침이 있습니다:

형식에 인트로스펙션 `(transfer)` 주석이 달려있다면, 해당 부분을 살펴보십시오.

형식이 `const`라면, 전달 값이 없습니다.

함수 문서에서 반드시 메모리상 해제를 해야 할 반환 값을 분명하게 정의했다면, 완전한 또는 컨테이너 전달이 이루어집니다.

함수 이름에 ‘dup’, ‘take’, ‘steal’이 붙어있다면, 완전한 또는 컨테이너 전달이 이루어집니다.

함수 이름에 ‘peek’이 붙어있다면, 전달 값이 없습니다.

위의 경우가 아니라면, 소유 주체를 전달하도록 했는지 여부를 확인하는 함수 코드를 살펴보아야 합니다. 그리고 해당 함수의 문서의 버그를 보고하고, 인트로스펙션 주석을 추가해야 한다고 요청하십시오.

소유 주체와 전달 기반이 주어졌다면, 올바른 메모리 할당 문제 접근 방식으로 각 상황을 기술적으로 밝힐 수 있습니다. 각 경우에, `copy()` 함수는 데이터 형식에 알맞아야 하는데, 이를테면, `g_strdup()` 함수는 문자열에, `g_object_ref()` 함수는 GObject 객체에 알맞아야 합니다.

소유 주체 전달을 생각할 때, `malloc()`/`free()`와 참조 카운팅은 동일하다고 보시면 됩니다. 보통의 경우 새로 할당한 일부 힙 메모리 영역을 전달합니다. 그 다음 새로 증가한 참조를 전달합니다. 을 참고하십시오.

## 할당

할당 목적/대상 | 소유 대상 | 비소유 대상
--- | --- | ---
소유 원본 | 원본을 대상으로 복제하거나 이동합니다. `owned_dest = copy (owned_src)` `owned_dest = owned_src; owned_src = NULL` | 소유 대상을 메모리에서 해제하기 전에는 비소유 대상을 활용하지 않음을 전제로 한 일반적인 할당 방식입니다. `unowned_dest = owned_src`
비소유 원본 | 원본을 대상에 복제합니다. `owned_dest = copy (unowned_src)` | 일반적인 할당 방식입니다. `unowned_dest = unowned_src`

## 함수 호출

호출 위치/대상 | 전체 매개 변수 보내기 | 매개변수 보내지 않기
--- | --- | ---
소유 원본 | 매개변수의 원본을 복제하거나 이동합니다. `function_call (copy (owned_src))` `function_call (owned_src); owned_src = NULL` | 일반적인 매개변수 전달 방식입니다. `function_call (owned_src)`
비소유 원본 | 매개변수의 원본을 복제합니다. `function_call (copy (unowned_src))` | 일반적인 매개변수 전달 방식입니다. `function_call (unowned_src)`

## 함수 반환 처리

반환 위치/대상 | 전체 반환값 보내기 | 반환값 보내지 않기
--- | --- | ---
소유 원본 | 일반적인 값 반환 방식입니다. `return owned_src` | 잘못됐습니다. 원본을 메모리에서 해제하며, 메모리에서 해제한 반환 값을 사용합니다. 해제 후 사용 오류입니다.
비소유 원본 | 반환할 원본 값을 복제합니다. `return copy (unowned_src)` | 일반적인 변수 전달 방식입니다. `return unowned_src`

# 문서

각 함수 매개 변수의 소유 이전 및 값 반환, 각 변수의 소유권 문서화는 중요합니다. 코드를 작성할 때는 분명하겠지만, 몇 달 지나고 나면 무슨 이야기인지 불분명하며 API 사용자는 전혀 알 수 없을지도 모릅니다. 해당 항목도 문서화해야합니다.

소유 전달을 문서화 하는 최선의 방법은 gobject-introspection에서 도입한 `(transfer)` 주석의 사용입니다. 각 함수 매개변수 및 반환 형식 부분을 다루는 API 문서 주석에 이 주석을 넣으십시오. 만약 함수를 공용 API에 넣지 않더라도, 어쨌든 문서 주석을 작성하고, `(transfer)` 주석을 넣으십시오. 이렇게 하면 인트로스펙션 도구에서 이 주석을 인식하고 API를 올바르게 인트로스펙션할 때 활용할 수 있습니다.

예제:

/** * g_value_take_string: * @value: (transfer none): an initialized #GValue * @str: (transfer full): string to set it to * * Function documentation goes here. */

/** * generate_string: * @template: (transfer none): a template to follow when generating the string * * Function documentation goes here. * * Returns: (transfer full): a newly generated string */

변수 소유권은 인라인 문서로 문서 기록 처리할 수 있습니다. 이 방식은 표준이 아니며 어떤 도구에서도 읽지 않지만, 일관되게 적는다면 일정한 형식을 갖출 수 있습니다.

GObject *some_owned_object = NULL; /* owned */ GObject *some_unowned_object; /* unowned */

의 문서는 기존의 내용과 유사합니다. 포함 구성 요소의 형식도 함께 들어갑니다:

GPtrArray/*<owned gchar*>*/ *some_unowned_string_array; /* unowned */ GPtrArray/*<owned gchar*>*/ *some_owned_string_array = NULL; /* owned */ GPtrArray/*<unowned GObject*>*/ *some_owned_object_array = NULL; /* owned */

또한 소유 변수도 항상 초기화한 후 해제하는게 나중에 더 간편해짐을 참고하십시오. 링크를 참고하십시오.

또한, 문자열과 같은 기본 C 형식 같은 일부 형식에 `const`를 붙이지 않았을 경우, 키워드를 붙여서 (`const` 키워드를 굳이 사용할 필요가 없는) 기존의 참조 소유 변수에 다른 변수 값을 할당할 경우 컴파일러 경고를 띄우는 이점을 취하십시오.

# 참조 카운팅

기존의 `malloc()`/`free()` 형식 함수처럼, GLib에 참조 카운팅 방식의 함수가 있습니다. 핵심 예제로 `GObject`가 있습니다.

소유 및 전달 개념은 할당한 형식에 따라 처리하는 참조 카운팅 형식 그대로 반영합니다. 해당 범위는(예를 들자면, `g_object_ref()` 함수를 호출하여) 인스턴스에 강 참조를 할당했을 경우 참조 카운팅 형식을 부여 합니다. `g_object_ref()` 함수를 다시 호출하면 인스턴스를 `복제`할 수 있습니다. `g_object_unref()` 함수로 소유 참조를 해제할 수 있습니다. 실제로 인스턴스를 종결짓지 않을 수도 있지만, 현재 코드 범위에서 인스턴스의 소유 관계를 정리(해제)합니다.

GObject 참조를 다루는 간편한 방법은 링크를 참고하십시오.

GLib에는 `GHashTable` ( `g_hash_table_ref()` 함수 및 `g_hash_table_unref()` 함수 활용), `GVariant` ( `g_variant_ref()` 함수, `g_variant_unref()` 함수) 같은 참조 카운팅 형식이 있습니다. `GHashTable` 같은 일부 형식에는 참조 카운팅 및 명시적 종결 처리를 지원합니다. 참조 카운팅은 취향에 따라 활용해야 하는데, 인스턴스 다중 복제 객체를 할당하지 않고도, 다양한 범위(각 부분에서 자체 참조를 지님)에서 인스턴스를 공유할 수 있기 때문입니다. 이런 인스턴스 공유 참조 방식으로 메모리를 절약합니다.

## 플로팅 참조

`GObject`에 반해 `GInitiallyUnowned` 에서 상속받은 클래스는 플로팅 초기 참조를 지니는데, 이는 어떤 코드도 참조를 보유하지 않는다는 의미입니다. 객체에서 `g_object_ref_sink()` 함수를 호출하면, 플로팅 참조에서 강 참조로 바뀌며, 호출 코드에서 객체에 소유권이 있다고 간주합니다.

GTK+와 같이 여러 개의 객체를 만들고 계층 관계를 형성해야 하는 API에서 C를 사용할 때, 플로팅 참조는 간편한 수단입니다. 이 경우 모든 강 참조를 쳐내는 `g_object_unref()` 함수를 호출하면 대부분의 코드에서 결과가 나옵니다.

플로팅 참조는 다음 코드를 단순화할 수 있습니다:

GtkWidget *new_widget;

new_widget = gtk_some_widget_new (); gtk_container_add (some_container, new_widget); g_object_unref (new_widget);

대신, 플로팅 참조 소유를 가정하는 `GtkContainer`로 다음 코드를 사용할 수 있습니다:

gtk_container_add (some_container, gtk_some_widget_new ());

플로팅 참조는 `GtkWidget`과 하위 클래스를 수반한 일부 API에서만 사용합니다. 어떤 API에서 지원하는지, 어떤 API에서 플로팅 참조를 활용하고, 상호간 활용하는지 알아야 합니다.

참고로 `g_object_ref_sink()` 함수는 비 플로팅 참조를 호출할 경우 `g_object_ref()` 함수와 동일하며, 이 경우 다른 함수와 `gtk_container_add()` 함수간 사용상 차이가 없습니다.

자세한 플로팅 참조 정보는 GObject 설명서를 참고하십시오.

# 편의 함수

GLib에서는 GObject에 맞춰 메모리 관리를 편리하게 하는 다양한 편의 함수를 제공합니다. 여기선 세가지를 다루지만, 다른 함수도 있습니다. 자세한 내용은 GLib API 문서를 살펴보십시오. 보통 여기 세가지 함수와 유사한 작명 형식을 따릅니다(함수 이름에 ‘_full’ 접미사 또는 ‘clear’ 동사 사용).

## `g_clear_object()`

`g_clear_object()` 함수는 GObject 참조를 해제하고 포인터를 `NULL`로 만드는 `g_object_unref()` 함수 버전입니다.

이 함수는 GObject 포인터를 `NULL` 로 만들거나 GObject의 소유권을 확보(하지만 아무 것도 없는 GObject를 가리키진 않음)하도록 코드를 쉽게 작성할 수 있게 합니다.

에 언급한 대로, 보유한 모든 GObject 포인터를 `NULL`로 초기화하여, 어떠한 검사 과정 없이 코드 범위의 마지막에서 할당을 해제하는건 `g_clear_object()` 함수를 호출하는 방식으로 쉽게 끝낼 수 있습니다:

void my_function (void) { GObject *some_object = NULL; /* owned */

if (rand ()) { some_object = create_new_object (); /* do something with the object */ }

g_clear_object (&some_object); }

## `g_list_free_full()`

`g_list_free_full()` 함수는 연결 리스트 자료 구조상 모든 항목과 데이터를 메모리에서 해제합니다. 리스트를 순차적으로 확인하고, `g_list_free()` 함수를 호출하여 `GList` 구성 항목 자체를 메모리에서 해제하여 모든 항목의 데이터를 메모리에서 해제하는 것 보다 훨씬 간단합니다.

## `g_hash_table_new_full()`

`g_hash_table_new_full()` 함수는 해시 테이블의 각각의 키-값을 없댈 수 있는 설정 함수 `g_hash_table_new()`의 새 버전입니다. 이 함수는 해시 테이블을 해제했거나, `g_hash_table_remove()` 함수로 항목을 제거했을 경우 모든 키-값을 대상으로 자동으로 호출합니다.

궁극적으로, 해시 테이블에 키 및 값이 있는지 여부에 대한 키 및 값을 저장하는 메모리 관리를 단순화합니다. 컨테이너 형식에 들어있는 구성 요소의 소유 관계를 알아보려면 링크를 참고하십시오.

`GPtrArray`를 다루는 유사한 `g_ptr_array_new_with_free_func()` 함수가 있습니다.

# 컨테이너 형식

`GPtrArray` 또는 `GList`와 같은 컨테이너 형식을 사용할 때, 소유권의 추가 레벨 개념을 도입합니다. 컨테이너 인스턴스의 소유권과 마찬가지로, 컨테이너의 각 구성 요소는 소유 상태가 되거나 아닐 수 있습니다. 컨테이너를 중첩하면 다중 레벨 소유권을 추적해야합니다. 소유한 구성요소의 소유권은 컨테이너가 가지고 있습니다. 컨테이너의 소유권은 컨테이너가 있는 코드 범위에서 가집니다(아마 다른 컨테이너겠죠).

동일한 컨테이너의 모든 구성 요소 소유권 부여가 단순화의 핵심 원칙입니다. 어떤 주체가 모두 소유하거나 모두 소유하지 않습니다. `GPtrArray` 및 `GHashTable` 형식에 대해 일반 를 사용한다면 자동으로 처리합니다.

컨테이너의 구성 요소가 소유 상태가 되면, 해당 컨테이너로의 구성 요소 추가는 소유권 이전의 본질적 과정입니다. 예를 들어 문자열 배열의 경우 구성 요소를 어떤 주체가 소유하면, 실질적으로 `g_ptr_array_add()` 함수를 정의합니다:

/** * g_ptr_array_add: * @array: a #GPtrArray * @str: (transfer full): string to add */ void g_ptr_array_add (GPtrArray *array, gchar *str);

따라서, 예를 들면, 상수(비소유) 문자열은 `g_ptr_array_add (array, g_strdup ("constant string"))` 코드로 배열에 추가해야합니다.

반면, 구성 요소 소유 주체가 없다면, 정의는 다음과 같습니다:

/** * g_ptr_array_add: * @array: a #GPtrArray * @str: (transfer none): string to add */ void g_ptr_array_add (GPtrArray *array, const gchar *str);

`g_ptr_array_add (array, "constant string")` 코드로 정적 문자열을 복제하지 않고 추가할 수 있습니다.

구성 요소 형식과 소유 주체를 표기하는 변수 정의 추가 주석 예제를 살펴보려면 링크를 참고하십시오.

# 단일 경로 정리

쓸 만한 더 복잡한 함수 설계 패턴은 호출 주체에 할당 요소를 제거(해제)하고 반환하는 단일 제어 경로의 확보입니다. 할당 추적을 엄청 단순화할 수 있어, 더 이상 코드 경로 상에서 어떤 할당 요소를 해제 했는지 신경써가며 해결할 필요가 없습니다. 모든 코드 경로는 동일한 지점에서 끝나므로, 모든 구성 요소를 해제합니다. 이러한 관점은 자체 변수를 더 많이 지닌 더 큰 함수에 굉장한 장점이 되지만, 작은 함수에 패턴을 적용하기엔 어울리지 않을 수도 있습니다.

이 접근 방식에 두가지 요구 사항이 있습니다:

함수는 단일 지점에서 반환하며, 다른 경로에서 해당 위치로 접근할 때 `goto` 문을 활용합니다.

자체 변수를 초기화하거나 소유권을 다른곳으로 보냈을 때는 `NULL`로 설정합니다.

아래 예제는 (간결성 목적) 소규모 함수를 보여주는게 목적이지만, 거대 함수 패턴의 프로그램의 원칙에도 반영해야합니다:

## 단일 경로 정리 예제

단일 함수의 단일 경로 정리 구현 예제
GObject * some_function (GError **error) { gchar *some_str = NULL; /* owned */ GObject *temp_object = NULL; /* owned */ const gchar *temp_str; GObject *my_object = NULL; /* owned */ GError *child_error = NULL; /* owned */

temp_object = generate_object (); temp_str = "example string";

if (rand ()) { some_str = g_strconcat (temp_str, temp_str, NULL); } else { some_operation_which_might_fail (&child_error);

if (child_error != NULL) { goto done; }

my_object = generate_wrapped_object (temp_object); }

done: /* Here, @some_str is either NULL or a string to be freed, so can be passed to * g_free() unconditionally. * * Similarly, @temp_object is either NULL or an object to be unreffed, so can * be passed to g_clear_object() unconditionally. */ g_free (some_str); g_clear_object (&temp_object);

/* The pattern can also be used to ensure that the function always returns * either an error or a return value (but never both). */ if (child_error != NULL) { g_propagate_error (error, child_error); g_clear_object (&my_object); }

return my_object; }

# 검증

메모리 누수는 정적 분석, 실행 시간 누수 검사 방식으로 검사할 수 있습니다.

Coverity 같은 정적 분석도구, Clang static analyzer 또는 Tartan에서 일부 메모리 누수 현상을 잡을 수 있지만, 코드에서 호출하는 모든 함수의 참조 소유 이전 내용을 알고 있어야합니다. Tartan과 같은 영역별 정적 분석기(GLib 메모리 할당 및 참조 이전을 알려줌)에서는 메모리 누수 검사를 더 잘 처리할 수 있지만, Tartan이 조금 더 최근에 나온 프로젝트이며 몇가지 과정이 빠져있습니다(낮은 참 양성 결과 도출율). 정적 분석기에 코드를 확인하는게 좋지만, 메모리 누수를 검사하는 근본 도구는 실행시간 메모리 누수 검사에 활용해야합니다.

실행 시간 메모리 누수 검사는 Valgrind와 여기에 있는 memcheck 도구로 진행할 수 있습니다. ‘분명한 메모리 사용 영역 분실’로 나타나는 일부 누수 현상은 반드시 수정해야합니다. ‘잠재적’ 메모리 사용영역 분실로 인한 대부분의 메모리 누수 현상은 실제 메모리 누수 현상이 아니며 무시 목록 파일에 추가해야합니다.

Clang 또는 GCC의 최근 버전으로 컴파일 할 경우 , 대신 주소 정리기를 활성화할 수 있으며, 이렇게 하면 실행 시간에 메모리 누수 현상과 오버플로우 문제를 찾습니다. 다만 제대로 된 환경에서 Valgrind 실행의 어려움은 없어집니다. 그러나, 나온 지 얼마 되지 않아 일부 경우에 잘못된 결과가 나타남을 참고하십시오.

Valgrind의 자세한 사용법은 링크를 참고하십시오.