C言語のきほん|構造体と関数の実践問題

構造体と関数を使いこなせると、C言語のプログラムはぐっと実践的になる。

ここまで学んできた構造体と関数の知識は、文法として理解するだけでも大切ですが、実際に問題を解きながら使ってみることで、ぐっと身につきやすくなります。特に、構造体を値渡しするのか、ポインタで渡すのか、あるいは関数から構造体を返すのかといった判断は、実践問題を通して整理するととてもわかりやすくなります。

構造体は、関連するデータをひとまとめにして扱える便利な仕組みです。そして関数と組み合わせると、処理の役割を分けながら、まとまりのあるデータを自然に受け渡しできるようになります。たとえば、商品の情報をまとめて計算したり、座標をまとめて移動させたり、複数のデータの中から条件に合うものを探したりといった処理は、構造体と関数の組み合わせがとてもよく合います。

このあたりは、読むだけだとなんとなくわかったつもりになりやすいところです。ですが、実際に問題として考えてみると、どの型を使うべきか、どの関数宣言にするべきか、戻り値にするのか引数にするのか、といった点がはっきり見えてきます。つまり、実践問題は知識の確認だけでなく、考え方の整理にもとても役立つわけです。

ここでは、まず構造体と関数に関する学習内容を軽く整理したあとで、元の問題と似た雰囲気を持つ新しい実践問題を1問作成し、その解答例と解説を丁寧に見ていきます。さらに最後に、確認問題も新しく1セット用意して、理解の定着を目指します。

実践問題に取り組む前に整理しておきたいこと

構造体と関数の組み合わせでは、特に次の3つを区別して考えることが大切です。

使い方何をするか特徴
構造体を値渡しする構造体をコピーして関数に渡す元のデータは基本的に変わらない
構造体をアドレス渡しする構造体の場所を関数に渡す元のデータを直接変更できる
構造体を関数から返す構造体の結果を戻り値として返す複数の値をまとめて返せる

この3つをしっかり区別できるようになると、問題文を読んだときに、どの書き方を使えばよいか判断しやすくなります。

たとえば、

  • 計算結果だけを返したいなら int などの基本型を返す
  • 元の構造体を変更したいならポインタを受け取る
  • 複数の結果をまとめて返したいなら構造体を返す

というように考えると、かなり整理しやすいです。

実践問題では何を見ればよいか

実践問題を解くときは、ただコードを書くのではなく、問題文から次の点を読み取ることが大切です。

見るポイント確認したい内容
関数宣言戻り値の型は何か
引数構造体そのものか、ポインタか
機能読み取りだけか、更新するか
返却値何を返すのか
main関数での表示どこで結果を確認するのか

実践問題

次の仕様に従って関数を作成し、main関数から呼び出して結果を確認してください。

関数宣言:

Book find_book_by_code(const Book *books, int size, int code);

機能:
書籍情報を管理する構造体 Book の配列 books から、指定した code を持つ書籍を探して返す。

返却値:
成功:code に一致する Book の構造体
失敗:code = -1 の Book の構造体

補足:Book構造体

typedef struct {
    int code;          // 書籍コード
    char title[40];    // 書籍名
    int price;         // 価格
} Book;

探索方法は、先頭から順番に調べる線形探索で考えてみましょう。

実行結果例

検索する書籍コードを入力してください > 102

書籍コード:102
書籍名:やさしいC言語
価格:2400円

一致するものが見つからなかった場合の実行例

検索する書籍コードを入力してください > 999

該当する書籍は見つかりませんでした。

解答例

ファイル名:15_13_1.c

#include <stdio.h>

// 書籍情報を表す構造体
typedef struct {
    int code;          // 書籍コード
    char title[40];    // 書籍名
    int price;         // 価格
} Book;

// 書籍コードで検索する関数
Book find_book_by_code(const Book *books, int size, int code);

int main(void)
{
    Book books[] = {
        {101, "プログラミング基礎", 2200},
        {102, "やさしいC言語", 2400},
        {103, "アルゴリズム入門", 2800}
    };

    int size = sizeof(books) / sizeof(books[0]);
    int code;
    Book result;

    printf("検索する書籍コードを入力してください > ");
    scanf("%d", &code);

    result = find_book_by_code(books, size, code);

    if (result.code == -1) {
        printf("\n該当する書籍は見つかりませんでした。\n");
    } else {
        printf("\n書籍コード:%d\n", result.code);
        printf("書籍名:%s\n", result.title);
        printf("価格:%d円\n", result.price);
    }

    return 0;
}

// 書籍コードが一致する書籍を返す関数
Book find_book_by_code(const Book *books, int size, int code)
{
    for (int i = 0; i < size; i++) {
        if (books[i].code == code) {
            return books[i];
        }
    }

    Book not_found = {-1, "", 0};
    return not_found;
}

この問題で確認したい考え方

この問題では、構造体の配列を関数に渡し、その中から条件に合う構造体を1つ返しています。つまり、

  • 配列はポインタとして受け取る
  • 配列の中身は関数内で参照する
  • 条件に一致した構造体は戻り値として返す

という3つの考え方が組み合わさっています。

この流れは実務的にもとても自然です。複数の候補の中から1件を取り出したい場面は、実際のプログラムでもよくあります。

関数宣言の意味を確認しよう

関数宣言は次のようになっています。

Book find_book_by_code(const Book *books, int size, int code);

これを分解すると、意味は次の通りです。

部分意味
Book戻り値として Book 型の構造体を返す
const Book *booksBook 型の配列の先頭アドレスを受け取る
int size配列の要素数を受け取る
int code探したい書籍コードを受け取る

ここで const が付いているのは、この関数が books の内容を書き換えないからです。検索だけを行う関数なので、読み取り専用であることを明示しています。

この const は、コードの安全性と読みやすさの両方を高めてくれます。

配列を受け取るときにポインタになる理由

main 関数では、次のように関数を呼び出しています。

result = find_book_by_code(books, size, code);

ここで books は配列ですが、関数に渡すときには先頭要素のアドレスとして扱われます。
そのため、関数側では次のようにポインタで受け取っています。

const Book *books

あるいは、次のように書いても意味はほぼ同じです。

const Book books[]

関数の引数としての配列は、実質的にはポインタとして扱われる、というのがここでのポイントです。

線形探索の流れ

この問題で使っている探索方法は、線形探索です。
線形探索は、配列の先頭から順番に1つずつ確認していくシンプルな方法です。

今回の関数では、次の部分がその処理にあたります。

for (int i = 0; i < size; i++) {
    if (books[i].code == code) {
        return books[i];
    }
}

流れとしては、こうなります。

手順内容
1先頭要素から調べる
2code が一致するか確認する
3一致したらその構造体を返す
4最後まで見つからなければ失敗用の構造体を返す

アルゴリズムとしては単純ですが、構造体の配列と関数の組み合わせを練習するにはとてもよい題材です。

見つからなかったときの返し方

この問題では、見つからなかったときに code = -1 の構造体を返しています。

Book not_found = {-1, "", 0};
return not_found;

このように、「見つからなかったことを表す特別な構造体」を返しておくと、main 関数側で判定しやすくなります。

main 関数では、次のように確認しています。

if (result.code == -1) {
    printf("\n該当する書籍は見つかりませんでした。\n");
}

これはとてもよく使われる考え方です。
失敗を表す専用の値をメンバに入れておくことで、呼び出し元が処理を分けやすくなります。

この図では、main 関数から配列全体そのものを丸ごと移動させているわけではなく、配列の先頭を基準に関数が順番に要素を調べている、という見方が大切です。そして、条件に一致した1件が戻り値として返される流れになっています。

ここで意識したいのは、関数が返しているのは配列そのものではなく、その中の1つの構造体だという点です。つまり、配列を調べる処理と、結果を返す処理がきれいに分かれているわけです。

この解答例のよいところ

この解答例には、学習上のよいポイントがいくつかあります。

よい点理由
const を付けている関数が配列を変更しないことが明確
size を渡している配列の要素数を安全に扱える
見つからない場合の構造体を返す呼び出し元で判定しやすい
main で表示している関数の役割が検索に絞られている

特に、関数の役割を「探すこと」に絞っているのが大事です。表示まで関数の中でやってしまうと、検索ロジックと表示ロジックが混ざってしまいます。今回は main 関数で結果を表示しているので、役割分担がきれいです。

応用するとどんな問題に広がるか

この問題の考え方は、いろいろな形に広げられます。

たとえば、

  • 商品番号で商品を探す
  • 学籍番号で学生情報を探す
  • 日付で予定を探す
  • 名前で登録データを探す

といった問題にそのまま応用できます。

また、返すものを構造体1件ではなく、見つかった位置の添字にしたり、ポインタにしたりする設計も考えられます。そうした違いを比べていくと、構造体と関数の設計力がさらに伸びていきます。

15章の確認問題

次の項目について、正しいものには○、間違っているものには×をつけてください。

① 構造体を値渡しすると、関数には元の構造体そのものが渡される。
② 構造体へのポインタを受け取る関数では、元の構造体のメンバを変更できる。
③ 構造体を戻り値として返すことはできない。
④ 構造体を引数に使う関数プロトタイプ宣言は、その構造体型の宣言より後に書く必要がある。
⑤ ポインタ p が構造体 Book を指しているとき、価格メンバ price には p.price でアクセスする。
⑥ 関数の引数としての Point p[] と Point *p は、同じ意味で扱われる。
⑦ 構造体の配列を関数に渡すと、すべての要素が必ずコピーされる。
⑧ 読み取り専用の構造体ポインタ引数には、const を付けると意図がわかりやすくなる。
⑨ 同じ構造体型どうしなら、代入はできる。
⑩ 関数内で作成したローカル変数の構造体は、そのアドレスを返しても安全である。

解答と解説

① ×:構造体を値渡しすると、関数には元の構造体そのものが渡される
値渡しでは、関数に渡されるのは元の構造体のコピーです。したがって、関数内で受け取った構造体を変更しても、呼び出し元の元データには通常影響しません。

② ○:構造体へのポインタを受け取る関数では、元の構造体のメンバを変更できる

ポインタを通して元の構造体にアクセスするため、関数内でメンバを書き換えると呼び出し元の構造体にも反映されます。座標の移動や値の更新などでよく使われます。

③ ×:構造体を戻り値として返すことはできない
C言語では、構造体を関数の戻り値にできます。複数の値を1つにまとめて返したいときにとても便利です。

④ ○:構造体を引数に使う関数プロトタイプ宣言は、その構造体型の宣言より後に書く必要がある
コンパイラが構造体型を知らない状態で関数宣言を書くことはできません。先に構造体型を定義してから、その型を使った関数宣言を書く必要があります。

⑤ ×:ポインタ p が構造体 Book を指しているとき、価格メンバ price には p.price でアクセスする
p は構造体そのものではなく、構造体を指すポインタです。したがって、メンバへアクセスするときは p->price と書きます。
p.price は、p 自身が構造体変数である場合の書き方です。

⑥ ○:関数の引数としての Point p[] と Point *p は、同じ意味で扱われる
関数の引数では、配列は先頭要素へのポインタとして扱われるため、Point p[] と Point *p は同じように使えます。

⑦ ×:構造体の配列を関数に渡すと、すべての要素が必ずコピーされる
関数に配列を渡すときは、通常は配列の先頭要素のアドレスが渡されます。したがって、配列全体が自動的にコピーされるわけではありません。

⑧ ○:読み取り専用の構造体ポインタ引数には、const を付けると意図がわかりやすくなる
const を付けることで、その関数が構造体の内容を書き換えないことを明示できます。安全性が上がるだけでなく、読む人にも目的が伝わりやすくなります。

⑨ ○:同じ構造体型どうしなら、代入はできる
同じ構造体型であれば、構造体どうしの代入ができます。たとえば a = b のような代入は可能です。ただし、== 演算子でそのまま比較することはできません。

⑩ ×:関数内で作成したローカル変数の構造体は、そのアドレスを返しても安全である
ローカル変数は関数終了とともに有効期間が終わるため、そのアドレスを返すのは危険です。返したい場合は、構造体そのものを戻り値として返す方法なら安全に扱えます。

補足として意識しておきたいこと

今回の実践問題と確認問題を通して、構造体と関数の関係はかなり整理できるはずです。特に大切なのは、関数に何を渡して、何を返すのかをはっきり意識することです。

たとえば、

  • 読み取りだけなら const を付ける
  • 更新したいならポインタを渡す
  • 複数の結果を返したいなら構造体を返す
  • 配列を扱うなら要素数も一緒に渡す

といった考え方が自然にできるようになると、コードの作り方がとても安定してきます。

構造体と関数は、C言語の中でも特に「実際のプログラムらしさ」が出てくる組み合わせです。問題を1問ずつ丁寧に考えていくと、文法の暗記ではなく、使い方として理解できるようになります。