C言語の構造体を関数で扱う

380, 2021-12-31

目次

C言語の構造体を関数で扱う

C言語の構造体を関数で扱う場合、値渡しとアドレス渡しの違いについて理解しておく必要があります。
関数の引数や返り値(戻り値)で構造体を値渡し、つまりコピーする場合は速度に影響が出る場合があります。
プログラムの処理速度を上げたい場合は関数の引数や返り値にはアドレス渡しを使うのが一般的です。

まずは関数の引数と構造体について見ていきたいと思います。

関数の引数に構造体を渡す

関数の引数に構造体を渡す場合は↓の4つの方法があります。

  • 値渡し(コピー)

  • constを付けた値渡し

  • アドレス渡し(参照渡し)

  • constを付けたアドレス渡し

最初に構造体を値渡しする方法について見てみます。

値渡しと構造体

↓のコードが構造体の値渡しのサンプルです。

#include <stdio.h>

// 動物を表す構造体
struct Animal {
    int age;  // 年齢
    char name[40];  // 名前
};

// 引数animalは値渡しされる
void func(struct Animal animal) {
    printf("age[%d] name[%s]\n", animal.age, animal.name);
    // age[20] name[Tama]
    // メンバの値が全てコピーされている
}

int main(void) {
    // 構造体変数animalを定義する
    struct Animal animal = { 20, "Tama" };

    // funcに値渡しでanimalを渡す
    func(animal);

    return 0;
}

Animalという構造体はメンバにagenameを持ちます。
main関数内でこの構造体の変数animalを定義しています。
その変数animalを関数funcに渡しています。

この時、引数に&などの演算子を付けない場合、それは値渡しになります。
値渡しでは構造体のメンバはすべて関数の引数にコピーされます
Animalagenameがコピーされるということです。

値渡しによるコピーは、変数の独立性を保つという意味でよく使われることがあります。
しかし構造体のメンバをすべてコピーするので、速度的にはあまりよろしくありません
プログラムの速度を上げたい場合は、後述するアドレス渡しを使います。

func()の引数にconstを付けると、その引数は読み取り専用の変数になります。

void func(const struct Animal animal) {
    animal.age = 30;  // error
}

構造体のメンバを保護したい場合はconstを付けておくといいでしょう。
ただしconstは全体的な設計にも影響を与えます。
ゲッターにはconstを付けてセッターにはconstを付けない、といったような統一的な設計が必要になります。

アドレス渡しと構造体

↓は関数に構造体をアドレス渡しするサンプルです。

#include <stdio.h>

// 動物を表す構造体
struct Animal {
    int age;  // 年齢
    char name[40];  // 名前
};

// 引数animalはアドレス渡しされる
void func(struct Animal *animal) {
    printf("age[%d] name[%s]\n", animal->age, animal->name);
    // age[20] name[Tama]
    // メンバの値が全てコピーされている
}

int main(void) {
    // 構造体変数animalを定義する
    struct Animal animal = { 20, "Tama" };

    // funcにアドレス渡しでanimalを渡す
    // '&'がついているのが特徴
    func(&animal);

    return 0;
}

構造体変数animalの先頭に&というアドレス演算子を付けると、その変数のアドレス(ポインタ)を取り出せます。
このアドレスをfuncに渡すことでアドレス渡しが成立します。
func()側の引数では「struct Animal *animal」というように、引数をポインタにしておきます。

func()の内部では引数animalはポインタです。
そのためanimalのメンバにアクセスするにはアロー演算子(->)を使います。

値渡しとアドレス渡しの違いは、まずメンバのコピーの有無です。
値渡しでは構造体のメンバはすべてコピーされますが、アドレス渡しではアドレスがポインタにコピーされるだけです。
つまりメンバのagenameはコピーされず、ポインタを通じて間接的に参照されるようになります。

間接的な参照なので、値渡しと違ってメンバの値を書き換えると、関数呼び出し側の変数の内容も変わります
そのためこのようなアドレス渡しは、関数に渡した構造体を関数内部で変更したい時によく使われます。

参照だけで書き換えを不可にしたい場合はfunc()側にconstを付けます。

void func(const struct Animal *animal) {
    animal->age = 30;  // error
}

関数の返り値と構造体

関数の返り値で構造体の変数を返すことができます。

#include <stdio.h>

// 動物を表す構造体
struct Animal {
    int age;  // 年齢
    char name[40];  // 名前
};

// 返り値の型がstruct Animalな関数func
struct Animal func(void) {
    struct Animal animal = { 20, "Tama" };
    return animal;  // 値渡しで返す
}

int main(void) {
    // funcの返り値を構造体変数で受け取る
    struct Animal animal = func();

    printf("age[%d] name[%s]\n", animal.age, animal.name);
    // age[20] name[Tama]

    return 0;
}

関数の返り値の型(funcの返り値の型)がstruct Animalになっています。
この場合、func()内でreturnされる値は値渡しになります。

(^ _ ^)

値渡しというか値返し?

(・ v ・)

そんな用語は聞かんぞ

この返り値の場合も構造体のメンバは全てコピーされて返されます
ですので速度が欲しい場合にはあまり向いてない設計です。
しかし、頻繁に呼び出される関数でない場合はこの設計も有りかと思います。

たとえばcreate_animal()という関数を作って、構造体をコピーで返す設計などが考えられます。
しかしcreate_animal()が頻繁に呼び出される関数の場合は↓のようにinit_animal()などに変更したほうがいいでしょう。

void init_animal(struct Animal *animal) {
    animal->age = 20;  // 初期化処理
}

int main(void) {
    struct Animal animal;
    init_animal(&animal);
    return 0;
}

このような設計はcreate_animal()と比べて一般的です。

おわりに

今回はC言語の構造体と関数について解説しました。
C言語の構造体はメンバをコピーしているとプログラムが遅くなることがあります。
特に頻繁に呼び出される関数でそういう設計になると、ボトルネックになる可能性が高まります。

C言語の構造体を関数で使う場合は値渡しとアドレス渡しを用途に合わせて使うようにすると吉です。

(^ _ ^)

コピーは安全

(・ v ・)

ポインタは速い



この記事のアンケートを送信する