ユーニックス総合研究所

  • home
  • archives
  • c-struct-func-2

【C言語】関数と構造体を組み合わせて使う

  • 作成日: 2022-07-22
  • 更新日: 2023-12-25
  • カテゴリ: C言語

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

この記事ではC言語の関数で構造体を扱う方法を解説します。
関数と構造体は組み合わせて使われることが非常に多いです。
ですのでこれらの組み合わせの方法について理解しておくことはC言語を学ぶ上で重要と言えます。

関連記事

自作関数read_fileでファイルを読み込み【ファイル入出力, コマンド】
目が覚めるC言語のdo-while文の使い方【ループ処理、初心者向け】
明快!C言語のcontinue文の使い方

目が覚めるC言語のdo-while文の使い方【ループ処理、初心者向け】
明快!C言語のcontinue文の使い方
君はまだC言語のdefineのすべてを知らない【マクロ、プリプロセス】

前提とする構造体

今回の解説で使用する構造体の定義は↓になります。

struct Monster {  
    int age;  // 年齢  
    double height;  // 身長  
    char name[20];  // 名前  
};  

構造体Monsterはモンスターを表現する構造体です。
ゲームなどで使いそうな感じですね。

🦝 < モンスターをコードで表現

構造体変数を引数で渡す

構造体の変数を関数の引数に渡してその引数を関数内で使うというケースがあります。
構造体変数を関数に渡す場合、コピー渡しポインタ渡しの2つの方法があります。

  • コピー渡し
  • ポインタ渡し

コピー渡しは構造体のメンバを丸ごと引数にコピーする方法です。
コピーが発生する分、ポインタ渡しより遅くなる場合があります。

いっぽうポインタ渡しは構造体変数のアドレスを引数のポインタ変数に渡す方法です。
ポインタ渡しでは余分なメンバのコピーが発生しないのでコピー渡しと比べて速度的に有利になる場合があります。

構造体変数をコピーするケースは必要になる時がありますがどちらかというとポインタ渡しの方がよく行われます。
C言語でプログラムを作るのは速度面で有利だからですが、構造体変数のコピー渡しを繰り返してプログラムが遅くなったら本末転倒です。

コピー渡し

まずコピー渡しです。
これは関数の引数に普通に構造体変数を渡すだけです。

#include <stdio.h>  

struct Monster {  
    int age;  // 年齢  
    double height;  // 身長  
    char name[20];  // 名前  
};  

void func(struct Monster arg) {  
    // argはコピー済みの引数  
    printf("%d\n", arg.age);  // 20  
    printf("%f\n", arg.height);  // 170.12  
    printf("%s\n", arg.name);  // Tama  
}  

int main(void) {  
    // monsterを定義する  
    struct Monster monster = { 20, 170.12, "Tama" };  

    func(monster);  // ここでmonsterがargにコピーされる  

    return 0;  
}  

↑のコードではfunc()というのが関数です。
この関数にmain関数内で定義したmonster変数を渡します。
この時にfunc()の引数argmonsterがコピーされます。

コピーされた構造体変数のメンバはfunc()内でアクセスできます。

ポインタ渡し

次にポインタ渡しです。
これは関数の引数をポインタ変数にしておきます。
そして関数を呼び出すときに引数に構造体変数のアドレスを渡します。

#include <stdio.h>  

struct Monster {  
    int age;  // 年齢  
    double height;  // 身長  
    char name[20];  // 名前  
};  

void func(struct Monster *arg) {  
    // argはポインタ  
    printf("%d\n", arg->age);  // 20  
    printf("%f\n", arg->height);  // 170.12  
    printf("%s\n", arg->name);  // Tama  
}  

int main(void) {  
    // monsterを定義する  
    struct Monster monster = { 20, 170.12, "Tama" };  

    func(&monster);  // ここでmonsterのアドレスをfunc()に渡す  

    return 0;  
}  

↑の場合func()に構造体変数monsterのアドレスを渡しています。
monsterのアドレスはfunc()の引数のargにコピーされます。
そうするとargからはアロー演算子(->)を伸ばすとmonsterのメンバにアクセスできます。

↑のコードではfunc(struct Monster *arg)となっています。
これはargのメンバに対して変更を加えられる状態です。
たとえば↓のようにfunc()内でargのメンバを変更することができます。

void func1(struct Monster *arg) {  
    arg->age = 30;  
}  

このメンバの書き換えは関数の呼び出し元のもとになっている変数monsterにも影響します。
関数を呼び出した後はmonster.argの値が書き換えられて変更されています。

このようなメンバの書き換えを制限したい、つまり書き換えを行えないようにしたい場合はconstを使います。
↓のように関数の引数にconstを付けるとメンバが保護されます。

void func2(const struct Monster *arg) {  
    // arg->age = 30; // error!  
}  

しかしconst修飾子も絶対ではありません。
const外しという荒業があります。
これを使うとconstを外すことができます。

void func3(const struct Monster *arg) {  
    struct Monster *var = (struct Monster *) arg;  
    var->age = 40;  
}  

const外しはプログラムの設計が失敗している時によく発生すると言われています。
頻繁にconst外しが必要になっている場合は設計を見直したほうが良いでしょう。

構造体変数を返り値で返す

関数と言えば入力と処理と出力です。
関数への入力は引数で行います。
関数から出力は返り値で行います。

ここでは関数から構造体を返す方法について解説していきます。

返り値はコピーされる

関数内の構造体変数をそのままreturnで返すと変数はコピーされます。

#include <stdio.h>  

struct Monster {  
    int age;  // 年齢  
    double height;  // 身長  
    char name[20];  // 名前  
};  

struct Monster create_monster(void) {  
    struct Monster ret = { 20, 170.12, "Tama" };  
    return ret;  // コピーが発生  
}  

int main(void) {  
    struct Monster monster = create_monster();  // コピーが発生  

    printf("%d\n", monster.age);  // 20  
    printf("%f\n", monster.height);  // 170.12  
    printf("%s\n", monster.name);  // Tama  

    return 0;  
}  

↑の場合、関数はcreate_monster()です。
create_monster()の返り値はstruct Monsterになっています。
この関数から関数内の構造体変数をそのまま返すと値はコピーされます。

↑のコードではcreate_monster()内のret変数が返り値になってますが、これは呼び出し元のmonsterに内容がコピーされています。
ですのでcreate_monster()の呼び出しもとでmonsterのメンバを参照することができます。

動的なメモリ確保でポインタを返す

動的なメモリ確保で構造体変数のメモリを確保し、そのポインタを関数から返す設計も一般的です。
たとえば↓のようにです。

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

struct Monster {  
    int age;  // 年齢  
    double height;  // 身長  
    char name[20];  // 名前  
};  

struct Monster *create_monster(void) {  
    // malloc()で動的メモリ確保  
    struct Monster *ret = malloc(sizeof(*ret));  
    if (ret == NULL) {  
        return NULL;  
    }  

    // メンバを初期化  
    ret->age = 20;  
    ret->height = 170.12;  
    strcpy(ret->name, "Tama");  

    return ret;  
}  

int main(void) {  
    struct Monster *monster = create_monster();  
    if (monster == NULL) {  
        perror("create_monster");  
        return 1;  
    }  

    printf("%d\n", monster->age);  // 20  
    printf("%f\n", monster->height);  // 170.12  
    printf("%s\n", monster->name);  // Tama  

    free(monster);  // 忘れずにメモリを開放する  

    return 0;  
}  

malloc()系の関数などで構造体変数のメモリを確保してメンバを初期化します。
そしてそのメモリのポインタを返り値で返します。
create_monster()の呼び出し元には動的にメモリ確保されたポインタが返ってきます。
アロー演算子(->)でメンバにアクセスして値を参照できます。

確保したメモリはfree()関数などでちゃんと解放しておく必要があります。
メモリの開放を忘れるとメモリリークというバグになります。

静的な構造体変数のポインタを返す

静的(static)な構造体変数のポインタを関数から返すという方法もあまり一般的ではありませんがあります。

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

struct Monster {  
    int age;  // 年齢  
    double height;  // 身長  
    char name[20];  // 名前  
};  

struct Monster *get_static_monster(void) {  
    static struct Monster ret = { 20, 170.12, "Tama" };  
    return &ret;  
}  

int main(void) {  
    struct Monster *monster = get_static_monster();  

    printf("%d\n", monster->age);  // 20  
    printf("%f\n", monster->height);  // 170.12  
    printf("%s\n", monster->name);  // Tama  

    return 0;  
}  

↑の場合get_static_monster()内で定義されるretは静的に1回だけ初期化されます。
この静的な変数のメモリの寿命はプログラムが終了するまでです。
その静的な変数のアドレスを返り値で返して呼び出し元で使うという感じです。

これは今までの方法と比べるとあまり一般的な設計ではありません
すこし特殊な設計です。
使いどころも特殊なわけですが使いようはありそうですね。

おわりに

今回はC言語で関数と構造体を使う方法を解説しました。
これらの組み合わせは非常によく使われます。
押さえておいていつでも使えるようにしておきましょう。

🦝 < C言語で関数と構造体を使う

🐭 < 色々なバリエーションがあるよ