LoginSignup
2
0

構造体の初期化パターン

Last updated at Posted at 2024-03-15

C言語で構造体を初期化するパターンを考えます。呼び出し元のスタック上に配置する方法と、呼び出し先でヒープ上に確保する方法に大別できると理解しています。

以下の例では、foo_*_tは初期化時にメンバ変数barを設定し、その値を2倍にして返す関数foo_*_doubleを備えます。ただし、barの値は100以下に制限したいものとします。

呼び出し元のスタック上に配置する

紳士協定によって必ずfoo_stack_init関数で初期化するものと定めます。

foo_stack.h
#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 */
foo_stack.c
#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;
}
main.c
#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は不完全型になり、それ自体を宣言することはできません。

foo_heap.h
#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 */
foo_heap.c
#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);
}
main.c
#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でも忘れるのは同じことのようにも思います。

2
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0