본문 바로가기
모던 C 언어/사소하지만 유용한 C언어 매크로들

2-1. C언어 스마트포인터

by 커널패닉 2022. 1. 14.
반응형

C언어에서 메모리 관리는 개발자를 따라다니면서 괴롭히는 문제이다. 그 중에서 할당한 메모리를 올바른 때에 회수하는 일은 숙련된 개발자라도 실수를 할 수 있다. 실수를 하지 않는다고 하더라도, 할당된 메모리를 해제하고 이로인해 댕글링 포인터 이슈가 없게 하는 것은 귀찮은 일임에 틀림없다.

이번 포스트에서는 GCC 확장 기능으로 스마트포인터를 사용하는 방법과 이를 매크로로 등록해서 편하게 활용하는 바법에 대해서 살펴보려 한다.

  1. 스마트포인터란?(C++의 스마트포인터)
  2. GCC cleanup 속성
  3. 스마트포인터 매크로로 제작 (및 경고 문구 회피)

 

1. 스마트포인터란?(C++의 스마트포인터)

동적할달된 메모리를 매번 해제해야 하는 불편을 회피하기 위해서 C++에는 스마트포인터를 제공한다. 스마트 포인터는 힙에 동적할당된 객체가 할당된 스코프를 벗어나면 자동으로 해제한다. 스마트포인터를 사용하는 예제 코드는 아래에 접어 두었다.

더보기

아래는 C++에서 스마트포인터를 사용하는 예제 코드이다. C++에 익숙하지 않아서 dydtjr1128님의 블로그에서 예제를 가져왔다. (https://dydtjr1128.github.io/cpp/2019/05/10/Cpp-smart-pointer.html)

 

#include <iostream>
#include <memory>
#include <string>

using namespace std;

class Monster
{

private:
	string name_;
	float hp_;
	float damage_;

public:
	Monster(const string& name, float hp, float damage); // 생성자 선언

	~Monster() { cout << "메모리가 해제되었습니다." << endl; }
	void PrintMonsterInfo();

};

Monster::Monster(const string& name, float hp, float damage) // 생성자 정의

{
	name_ = name;
	hp_ = hp;
	damage_ = damage;

	cout << "생성자 호출" << endl;

}

void Monster::PrintMonsterInfo()
{
	cout << "몬스터 이름 : " << name_ << endl;
	cout << "체력 : " << hp_ << endl;
	cout << "공격력 : " << damage_ << endl;
}
int main()
{
	unique_ptr<Monster> goblin = make_unique<Monster>("고블린", 100.f, 20.f);

	goblin->PrintMonsterInfo();

	return 0;
}

 

 

2. GCC cleanup 속성

이렇게 유용한 스마트포인터가 C에도 있을까? 표준 문법에는 없다. 하지만 GCC 확장에는 스마트 포인터와 유사한 cleanup이 있다. 아래는 cleanup 속성에 대한 GCC 문서의 설명이다.

cleanup (cleanup_function)
The cleanup attribute runs a function when the variable goes out of scope. This attribute can only be applied to auto function scope variables; it may not be applied to parameters or variables with static storage duration. The function must take one parameter, a pointer to a type compatible with the variable. The return value of the function (if any) is ignored.
cleanup 속성은 변수가 스코프를 벗어날 때 함수를 실행합니다. 이 속성은 자동으로 실행되는 함수의 스코프 변수(e.g. 지역변수)에만 적용할 수 있습니다. 구체적으로 말하자면 정적 저장 기간이 있는 매개변수 또는 변수(e.g. 인자, 전역변수)에는 적용되지 않을 수 있습니다. 함수는 변수와 호환되는 유형에 대한 포인터인 하나의 매개변수를 취해야 합니다. 함수의 반환 값(있는 경우)은 무시됩니다.

아래는 cleanup 함수를 사용하여 스택에서 해제되는 동적 메모리를 자동으로 해제하는 예시이다.

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

void free_ptr(void **ptr) {
    free(*ptr);
    printf("Free ptr\n");
}


void print_hello_world() {
    int buf_size = 1024;
    __attribute__((__cleanup__(free_ptr))) char *dynamic_string = calloc(1, sizeof(char) * buf_size);
    strncpy(dynamic_string, "Hello, world", buf_size);

    printf("%s\n", dynamic_string);
}

int main() {
    for (int i = 0; i < 10; i++) {
        print_hello_world();
    }

    return 0;
}

빌드 및 결과

~/Desktop ❯ gcc test.c
test.c: In function ‘print_hello_world’:
test.c:13:5: warning: passing argument 1 of ‘free_ptr’ from incompatible pointer type [-Wincompatible-pointer-types]
   13 |     char *dynamic_string __attribute__((__cleanup__(free_ptr))) = calloc(1, sizeof(char) * buf_size);
      |     ^~~~
      |     |
      |     char **
test.c:5:22: note: expected ‘void **’ but argument is of type ‘char **’
    5 | void free_ptr(void **ptr) {
      |               ~~~~~~~^~~
~/Desktop ❯ ./a.out
Hello, world
Free ptr
Hello, world
Free ptr
Hello, world
Free ptr
Hello, world
Free ptr
Hello, world
Free ptr
Hello, world
Free ptr
Hello, world
Free ptr
Hello, world
Free ptr
Hello, world
Free ptr
Hello, world
Free ptr

Hello, world가 출력된 이후에 함수에서 벗어나면(= 스택에서 변수가 해제되면) 힙에 동적 할당된 메모리도 해제되는 것을 확인할 수 있다.

 

 

스마트포인터 매크로로 제작 (및 경고 문구 회피)

 

우리는 앞선 예제에서 GCC cleanup 속성을 사용하면 C++의 스마트포인터와 유사한 효과를 낼 수 있음을 알게 되었다. 일시적으로 사용하는 동적할당된 메모리는 스마트 포인터를 사용하면 메모리 관리의 압박에서 어느정도 벗어날 수 있다. 또한 휴먼에러에 의한 버그를 예방하는 데에도 도움이 된다. 다만 위의 예제를 사용하기에는 다음 두 가지를 개선해야 한다.

  1. cleanup 속성을 사용하기 너무 번거롭다.
  2. 컴파일 시에 불필요한 경고가 발생한다.

우선 1번 문제는 매크로를 이용하면 쉽게 해결할 수 있다. 아래는 cleanup 속성을 이용하는 코드를 매크로로 변경한 예제이다.

#define autoptr __attribute__((__cleanup__(free_ptr)))

/* ... 생략 ...*/

void print_hello_world() {
    int buf_size = 1024;
    autoptr char *dynamic_string = calloc(1, sizeof(char) * buf_size);
    strncpy(dynamic_string, "Hello, world", buf_size);

    printf("%s\n", dynamic_string);
}

/* ... 생략 ...*/

 

매크로를 이용해서 autoptr을 gcc 속성을 사용하도록 지정해 두었다. 이제는 간단히 변수 선언 시에 autoptr 키워드를 붙이면 해당 변수는 스마트포인터가 된다.

다음으로 -Wincompatible-pointer-types에러를 보이지 않도록 해야 한다. 해당 에러는 free_ptr 함수에서 void ** 타입을 인자로 받아야 하지만 char ** 타입의 인자가 전달되면서 발생한다. 이를 해결하려면 void * 타입은 어떤 포인터 타입을 인자로 받아도 -Wincompatible-pointer-types 경고가 발생하지 않는 것을 이용해서 약간의 트릭을 사용해야 한다.

/* 이전 (경고발생 코드)
void free_ptr(void **ptr) {
    free(*ptr);
    printf("Free ptr\n");
}
*/

void free_ptr(void *ptr) {
    void **p = (void**)ptr;
    free(*p);
    printf("Free ptr\n");
}

void * 타입으로 인자를 받은 뒤, 내부적으로 필요한 void ** 타입으로 형 변환을 해서 사용하고 있다. 코드 동작은 변한것이 없지만 컴파일러 입장에서는 경고를 출력하지 않는다.

 

전체코드

더보기
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define autoptr __attribute__((__cleanup__(free_ptr)))

void free_ptr(void *ptr) {
    void **p = (void**)ptr;
    free(*p);
    printf("Free ptr\n");
}

void print_hello_world() {
    int buf_size = 1024;
    autoptr char *dynamic_string = calloc(1, sizeof(char) * buf_size);
    strncpy(dynamic_string, "Hello, world", buf_size);

    printf("%s\n", dynamic_string);
}

int main() {
    for (int i = 0; i < 10; i++) {
        print_hello_world();
    }

    return 0;
}
반응형