C言語で構造体を初期化するパターンを考えます。呼び出し元のスタック上に配置する方法と、呼び出し先でヒープ上に確保する方法に大別できると理解しています。
以下の例では、foo_*_t
は初期化時にメンバ変数bar
を設定し、その値を2倍にして返す関数foo_*_double
を備えます。ただし、bar
の値は100以下に制限したいものとします。
呼び出し元のスタック上に配置する
紳士協定によって必ずfoo_stack_init
関数で初期化するものと定めます。
#ifndef FOO_STACK_H
#define FOO_STACK_H
#ifdef __cplusplus
extern "C"
{
#endif
#define __STDC_WANT_LIB_EXT1__ 1
#include <errno.h>
#ifndef __STDC_LIB_EXT1__
typedef int errno_t;
#endif
#include <stddef.h>
typedef struct foo_stack_t
{
int bar;
} foo_stack_t;
errno_t foo_stack_init(foo_stack_t *foo, int bar);
errno_t foo_stack_double(foo_stack_t *foo, int *out);
#ifdef __cplusplus
}
#endif
#endif /* FOO_STACK_H */
#include "foo_stack.h"
errno_t foo_stack_init(foo_stack_t *foo, int bar)
{
if (foo == NULL ||
100 < bar)
{
return EINVAL;
}
foo->bar = bar;
return 0;
}
errno_t foo_stack_double(foo_stack_t *foo, int *out)
{
if (foo == NULL ||
out == NULL)
{
return EINVAL;
}
*out = foo->bar * 2;
return 0;
}
#include "foo_stack.h"
#include <assert.h>
int main_stack(void)
{
foo_stack_t foo;
errno_t err = foo_stack_init(&foo, 1);
if (err != 0)
{
return 1;
}
int out = 0;
err = foo_stack_double(&foo, &out);
if (err != 0)
{
return 1;
}
assert(out == 2);
return 0;
}
呼び出し先でヒープ上に確保する
Opaqueな型を利用してメンバ変数bar
を隠蔽しています。foo_heap_t
は不完全型になり、それ自体を宣言することはできません。
#ifndef FOO_HEAP_H
#define FOO_HEAP_H
#ifdef __cplusplus
extern "C"
{
#endif
#define __STDC_WANT_LIB_EXT1__ 1
#include <errno.h>
#ifndef __STDC_LIB_EXT1__
typedef int errno_t;
#endif
#include <stddef.h>
#include <stdlib.h>
typedef struct foo_heap_t foo_heap_t;
errno_t foo_heap_new(foo_heap_t **foo, int bar);
errno_t foo_heap_double(foo_heap_t *foo, int *out);
void foo_heap_destroy(foo_heap_t *foo);
#ifdef __cplusplus
}
#endif
#endif /* FOO_HEAP_H */
#include "foo_heap.h"
struct foo_heap_t
{
int bar;
};
errno_t foo_heap_new(foo_heap_t **foo, int bar)
{
if (foo == NULL ||
100 < bar)
{
return EINVAL;
}
foo_heap_t *foo_ = (foo_heap_t *)malloc(sizeof(foo_heap_t));
if (foo_ == NULL)
{
return ENOMEM;
}
foo_->bar = bar;
*foo = foo_;
return 0;
}
errno_t foo_heap_double(foo_heap_t *foo, int *out)
{
if (foo == NULL ||
out == NULL)
{
return EINVAL;
}
*out = foo->bar * 2;
return 0;
}
void foo_heap_destroy(foo_heap_t *foo)
{
free(foo);
}
#include "foo_heap.h"
#include <assert.h>
int main_heap(void)
{
foo_heap_t *foo = NULL;
errno_t err = foo_heap_new(&foo, 1);
if (err != 0)
{
return 1;
}
int out = 0;
err = foo_heap_double(foo, &out);
if (err != 0)
{
return 1;
}
assert(out == 2);
foo_heap_destroy(foo);
foo = NULL;
return 0;
}
所感
スタックに配置する場合、変数foo
のメモリは関数の終了時に自動的に破棄されます。メモリアロケーションに失敗する可能性がなく、メモリリークの危険性が低いのがよいと思います。メモリ以外に明示的に破棄処理を書きたい場合にはfoo_stack_deinit
のような関数を用意することになります。
スタックに配置すると、foo_stack_init
を呼び出し忘れたことを後続の関数で検知し止める方法がありません。
スタックに配置するには構造体定義をヘッダーに記述する必要があり、メンバ変数bar
は呼び出し元からも見えることになります。bar
を直接利用した誤った操作をされる可能性があり、例のようなシンプルなものだとfoo_stack_init
で初期化するのは冗長に感じられます。
ヒープに配置する方法では、foo_heap_t *
をNULL
で初期化すれば、万が一foo_heap_new
関数を経ないで利用しようとした場合に後続の関数で止まります。bar
が見えないのでfoo_heap_new
関数の利用を強制できているのはよいと思います。
foo_heap_destroy
関数を忘れるとメモリリークが発生します。が、メモリの解放以外に明示的な破棄処理が必要ならfoo_stack_deinit
でも忘れるのは同じことのようにも思います。