C言語のきほん|構造体を関数でやり取りする

構造体の受け渡しがわかると、関数づくりがもっと自然になる。

プログラムが少しずつ大きくなってくると、関連するデータをひとまとまりにしたまま関数へ渡したり、関数の中でまとめて扱ったりしたくなる場面が増えてきます。そんなときに活躍するのが構造体です。

たとえば、名前と点数、商品名と価格、日付の年・月・日など、複数の値を1つのまとまりとして扱いたいことはとても多いですよね。構造体を使えば、そのようなデータを見やすく整理できます。そして、その構造体を関数に渡せるようになると、処理を役割ごとに分けやすくなり、読みやすいプログラムが作りやすくなります。

ここでは、構造体を関数に渡すときの基本として、まずは値渡しの考え方をしっかり押さえていきます。値渡しでは、関数に渡された構造体はコピーとして扱われます。そのため、元のデータを守りながら処理したいときにとてもわかりやすい方法です。

まずは、構造体を関数に渡すと何が起きるのかを、順番にやさしく見ていきましょう。

構造体を関数に渡すとはどういうことか

構造体を関数に渡すというのは、複数のメンバを持ったデータのまとまりを、関数の引数として渡すことです。

たとえば、次のような構造体があったとします。

typedef struct {
    char title[30];
    int price;
} Book;

この Book 型は、本のタイトルと価格を1つにまとめた構造体です。
このようなデータを関数に渡せば、タイトルと価格を別々に渡すよりも、整理された形で扱えます。

構造体を関数に渡す方法には、主に次の2つがあります。

渡し方特徴元の構造体への影響
値渡し構造体全体をコピーして渡す影響しない
アドレス渡し構造体の場所を渡す影響することがある

今回は、このうち最初の基本となる値渡しに焦点を当てて説明します。

構造体を関数に値渡しする

値渡しでは、関数に渡されるのは元の構造体そのものではなく、そのコピーです。
つまり、関数の中では別の構造体として受け取ることになります。

この仕組みは、普通の int 型や double 型の変数を値渡しするのと考え方は同じです。
ただし構造体の場合は、中に複数のメンバが入っているため、それらがまとめてコピーされます。

そのため、関数内で受け取った構造体の値を変更しても、呼び出し元にある元の構造体は変わりません。

これはとても大事なポイントです。
「関数の中で少し試しに値を変えても、元データには影響しない」という安心感があります。

値渡しのサンプルプログラム

書籍情報を扱うプログラムを例に説明します。

ファイル名:15_11_1.c

#include <stdio.h>

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

// 書籍情報を表示する関数
void print_book(Book b);

int main(void)
{
    Book book = {"C言語入門", 2800};

    print_book(book);

    return 0;
}

void print_book(Book b)
{
    printf("書籍名: %s\n", b.title);
    printf("価格: %d円\n", b.price);
}

実行結果例

書籍名: C言語入門
価格: 2800円

このプログラムの流れ

このプログラムでは、main 関数の中で Book 型の変数 book を作っています。

Book book = {"C言語入門", 2800};

この変数 book を、次のように関数へ渡しています。

print_book(book);

ここで関数 print_book は、Book 型の引数 b を受け取ります。

void print_book(Book b)

この b は、main 関数の book と同じものではありません。
book の内容をコピーした、別の構造体です。

そのため、print_book の中で b.title や b.price を使って表示しても、それはコピーされたデータを見ていることになります。

値渡しではコピーが作られる

構造体の値渡しでいちばん大切なのは、この「コピーが作られる」という点です。

イメージとしては、main 関数にある構造体をそのまま渡すのではなく、中身をそっくり複製して関数へ渡しているような感じです。

図にすると、次のような流れになります。

  • main 関数の中に元の構造体がある
  • 関数呼び出しのときにその内容がコピーされる
  • 関数側ではそのコピーを受け取る
  • 関数内で使うのはコピーのほう

このため、関数内で受け取った構造体を変更しても、呼び出し元の変数は変わりません。

値渡しの性質を確認する例

値渡しでは元のデータが変わらないことを、はっきり確認できる例も見ておきましょう。

ファイル名:15_11_2.c

#include <stdio.h>

// 会員情報を表す構造体
typedef struct {
    char name[30];   // 名前
    int point;       // ポイント
} Member;

// ポイントを変更して表示する関数
void show_changed_member(Member m);

int main(void)
{
    Member member = {"高橋さくら", 50};

    printf("関数呼び出し前: %sさんのポイントは%dです。\n", member.name, member.point);

    show_changed_member(member);

    printf("関数呼び出し後: %sさんのポイントは%dです。\n", member.name, member.point);

    return 0;
}

void show_changed_member(Member m)
{
    m.point = 100;
    printf("関数の中: %sさんのポイントを%dに変更しました。\n", m.name, m.point);
}

実行結果例

関数呼び出し前: 高橋さくらさんのポイントは50です。
関数の中: 高橋さくらさんのポイントを100に変更しました。
関数呼び出し後: 高橋さくらさんのポイントは50です。

この結果を見ると、関数の中では 100 に変更されているのに、main 関数に戻ると 50 のままです。
これは、show_changed_member 関数が受け取っているのが元の member ではなく、そのコピーだからです。

この動きが理解できると、値渡しの意味がかなりはっきり見えてきます。

この図で見てほしいのは、main 側の book と、print_book 側の b が別物として描かれているところです。
内容は同じでも、同じ箱ではありません。

つまり、関数に渡した瞬間に、中身が複製されて別の変数として扱われるわけです。
この違いが、値渡しの理解ではとても重要です。

プロトタイプ宣言の位置に注意する

構造体を引数に取る関数を宣言するときは、その構造体の型が先に定義されていなければなりません。

たとえば、次の順番は正しい書き方です。

typedef struct {
    char title[30];
    int price;
} Book;

void print_book(Book b);

これに対して、構造体の型宣言より先に Book を使ってしまうと、コンパイラは Book という型をまだ知らないため、エラーになります。

つまり、順番としては次のように覚えるとよいです。

順番内容
1構造体の型を定義する
2その構造体型を使う関数を宣言する
3main 関数や関数定義を書く

この順番は初学者がつまずきやすいところなので、意識しておくと安心です。

メンバへのアクセス方法

値渡しされた構造体は、普通の構造体変数として扱います。
そのため、メンバにアクセスするときはドット演算子を使います。

b.title
b.price

これは、変数 book に対して book.title や book.price と書くのと同じ考え方です。

ここではまだポインタではないので、アロー演算子は使いません。
値渡しの段階では、あくまで「関数内にある普通の構造体変数」として扱うことがポイントです。

値渡しのメリット

構造体の値渡しには、わかりやすい利点があります。

メリット内容
元データが変わらない関数内で値を変更しても呼び出し元に影響しない
考え方がシンプル普通の変数の値渡しと同じ感覚で理解しやすい
安全に扱いやすい間違って元の構造体を書き換えにくい

特に、表示専用の関数や、一時的に内容を参照するだけの関数では、値渡しはとても直感的です。

値渡しのデメリット

一方で、構造体の値渡しには注意点もあります。

構造体はメンバが多くなるほどサイズが大きくなります。
その大きな構造体を何度も関数に渡すと、そのたびにコピーが発生します。

その結果、

  • 実行効率が下がることがある
  • メモリ使用量が増えることがある

という点には気をつける必要があります。

特に、大きな配列を内部に持つ構造体や、何度も繰り返し関数に渡すような場面では、値渡しは少し不利になることがあります。

値渡しが向いている場面

では、どんなときに値渡しが向いているのでしょうか。
代表的には次のような場面です。

向いている場面理由
小さな構造体を扱うときコピーの負担が小さい
表示や確認だけを行うとき元データを守りやすい
学習の初期段階動きがわかりやすい

最初は、構造体を関数へ渡す基本として値渡しを理解するのがとても大切です。
そのうえで、効率の面が気になるときにアドレス渡しへ進むと、学習の流れがとても自然になります。

値渡しとアドレス渡しの違いを先に軽く押さえる

今回の中心は値渡しですが、この先の理解のために違いを少しだけ見ておくと整理しやすいです。

項目値渡しアドレス渡し
渡されるもの構造体のコピー構造体のアドレス
関数内の変更元には反映されない元に反映される
効率コピー分の負担があるコピー不要で効率的
メンバアクセス. を使う-> を使うことが多い

この表を見ると、値渡しは安全でわかりやすく、アドレス渡しは効率がよい、という違いが見えてきます。

関数で構造体をやり取りする意味

構造体を関数でやり取りできるようになると、複数の関連データを1つのまとまりとして扱えるため、関数の設計がとても自然になります。

たとえば、名前と得点を1つずつ別々に渡すより、Student 型の構造体としてまとめて渡したほうが、何のデータを扱っているのかがはっきりします。
引数が増えすぎるのも防げますし、後からメンバを追加したいときにも対応しやすくなります。

このように、構造体と関数を組み合わせることで、C言語のプログラムはより整理された形になっていきます。

次に意識したいこと

ここまでで、構造体を関数に値渡しすると、関数側にはコピーが渡されること、そのため元の構造体は変わらないことが見えてきたと思います。

この考え方がしっかり身につくと、次に学ぶアドレス渡しとの違いもとても理解しやすくなります。
構造体を関数で扱うときは、まず

  • コピーを渡したいのか
  • 元のデータを直接扱いたいのか

を意識することが大切です。

この視点を持てるようになると、関数の作り方がぐっと上達していきます。