C言語のきほん|構造体をポインタで関数に渡す

構造体を丸ごと渡さないから、速くてわかりやすい。ポインタ渡しで学ぶ、C言語らしい関数設計。

ここでは、構造体を関数に渡すときに、構造体そのものをコピーして渡す方法ではなく、構造体の場所を表すアドレスを渡す方法を見ていきます。

構造体は、複数のデータをひとまとめにできる便利な仕組みです。ただし、構造体の中身が大きくなると、関数を呼び出すたびにコピーが発生する書き方は少しもったいなくなります。そんなときに役立つのが、構造体をポインタで関数に渡す方法です。

この方法を使うと、関数は呼び出し元にある元の構造体を直接見ることができます。さらに、必要ならその中身を書き換えることもできます。C言語ではとてもよく使われる書き方なので、ここでしっかり慣れておくと、この先の学習がぐっと楽になります。

今回は、1個の構造体をポインタで渡す場合と、構造体の配列をポインタで渡す場合の両方を、やさしく丁寧に確認していきましょう。

構造体をポインタで渡すと何が起こるのか

まず大事なのは、値渡しとアドレス渡しの違いです。

渡し方関数に渡るもの元のデータへの影響特徴
値渡し構造体のコピー基本的に影響しない安全だがコピーが発生する
ポインタ渡し構造体のアドレス影響することがあるコピー不要で効率がよい

値渡しでは、関数に渡されるのは構造体の複製です。関数の中で変更しても、元の構造体は変わりません。

一方、ポインタ渡しでは、関数に渡されるのは構造体が置かれている場所です。関数はその場所をたどって、元の構造体そのものにアクセスします。そのため、コピー処理を避けられますし、必要なら元データの更新もできます。

この「元のデータを直接扱える」という点が、ポインタ渡しの最大のポイントです。

1個の構造体をポインタで関数に渡す

商品情報を表す構造体で考えてみましょう。

サンプルプログラム

ファイル名:15_12_1.c

#include <stdio.h>

// 商品情報を表す構造体
typedef struct {
    char name[30];   // 商品名
    int price;       // 価格
} Item;

// 商品情報を表示する関数
void print_item(const Item *p);

int main(void)
{
    Item item = {"ノート", 250};

    print_item(&item);

    return 0;
}

void print_item(const Item *p)
{
    printf("商品名: %s\n", p->name);
    printf("価格: %d円\n", p->price);
}

実行結果例

商品名: ノート
価格: 250円

このプログラムの見方

このプログラムでは、main 関数の中で Item 型の変数 item を用意しています。

Item item = {"ノート", 250};

この item を関数に渡すとき、次のように書いています。

print_item(&item);

ここで使われている &item は、item のアドレスです。つまり、item がメモリのどこに置かれているかを表す情報です。

そして、受け取る側の関数は次のようになっています。

void print_item(const Item *p)

この Item *p は、「Item 型のデータがある場所を指すポインタ」です。
つまり p の中には、item のアドレスが入っています。

関数の中でメンバを使うときは、次のように書いています。

p->name
p->price

これは、ポインタが指している先の構造体のメンバにアクセスする書き方です。

アロー演算子とは何か

構造体変数そのものを使うときは、ドット演算子を使います。

item.name
item.price

でも、ポインタ p が指している先の構造体のメンバを使いたいときは、アロー演算子を使います。

p->name
p->price

この違いはとても大切です。

書き方意味
item.name構造体変数 item の name メンバ
p->nameポインタ p が指す構造体の name メンバ

なお、p->name は次の書き方を省略したものです。

(*p).name

意味は同じですが、通常は見やすい -> を使います。

const を付ける意味

関数の仮引数はこうなっていました。

この const は、「この関数の中では、p が指している構造体の内容を書き換えません」という約束です。

つまり、この関数は表示専用です。商品名や価格をうっかり変更しないようにできます。

たとえば、もし関数内で次のように書こうとすると、エラーになります。

p->price = 300;

これはとても良いことです。表示だけの関数なのに、間違ってデータを書き換えてしまう事故を防げるからです。

表示だけを行う関数では、const を付ける習慣をつけると、プログラムが読みやすくなり、安全性も上がります。

この図では、左にある item そのものが右へ移動するわけではありません。右へ渡るのは item の場所を表す情報だけです。
そのため、データ全体をコピーする必要がありません。

つまり、

  • 実データは main 側にある
  • 関数側はその実データを指す
  • 関数は指している先の中身を見る

という関係になります。

この考え方がつかめると、ポインタ渡しがかなり自然に感じられるようになります。

構造体の配列をポインタで関数に渡す

次は、構造体が1個ではなく、配列になっている場合です。
配列も関数に渡すときは、先頭要素のアドレスを渡す形になります。

サンプルプログラム

ファイル名:15_12_2.c

#include <stdio.h>

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

// ポイントを加算する関数
void add_bonus_point(Member *p, int size);

// 会員情報を表示する関数
void print_member(const Member *p, int size);

int main(void)
{
    Member member[] = {
        {"佐藤 花子", 10},
        {"田中 恒一", 15},
        {"中村 美咲", 20}
    };

    int size = sizeof(member) / sizeof(member[0]);

    printf("加算前\n");
    print_member(member, size);

    add_bonus_point(member, size);

    printf("\n加算後\n");
    print_member(member, size);

    return 0;
}

// 全員のポイントを5増やす関数
void add_bonus_point(Member *p, int size)
{
    for (int i = 0; i < size; i++) {
        (p + i)->point += 5;
    }
}

// 会員情報を表示する関数
void print_member(const Member *p, int size)
{
    for (int i = 0; i < size; i++) {
        printf("%s : %dポイント\n", (p + i)->name, (p + i)->point);
    }
}

実行結果例

加算前
佐藤 花子 : 10ポイント
田中 恒一 : 15ポイント
中村 美咲 : 20ポイント

加算後
佐藤 花子 : 15ポイント
田中 恒一 : 20ポイント
中村 美咲 : 25ポイント

配列を渡すときに起きていること

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

print_member(member, size);
add_bonus_point(member, size);

ここで渡している member は、配列全体をコピーしているわけではありません。
実際には、配列の先頭要素のアドレスが渡されます。

つまり、関数側の p は、member[0] を指している状態です。

そこから、

  • p + 0 は 1番目の要素
  • p + 1 は 2番目の要素
  • p + 2 は 3番目の要素

というように、順番に各要素へアクセスできます。

(p + i)->point という書き方

この部分は最初少し見慣れないかもしれません。

(p + i)->point

これは、「先頭要素を指す p から i 個先に進んだ構造体の point メンバ」という意味です。

たとえば i が 1 なら、2番目の要素の point を表します。

流れを分解すると次のようになります。

意味
p先頭要素のアドレス
p + ii 個先の要素のアドレス
(p + i)->pointその要素の point メンバ

C言語のポインタ演算では、単なる数字の加算ではなく、型のサイズを考慮して次の要素へ進みます。
そのため、構造体配列でも正しく各要素をたどれます。

関数の中で元の配列が書き換わる理由

add_bonus_point 関数では、元の配列の中身が変わっています。

void add_bonus_point(Member *p, int size)
{
    for (int i = 0; i < size; i++) {
        (p + i)->point += 5;
    }
}

これは、p がコピーされた配列ではなく、呼び出し元の実際の配列を指しているからです。
つまり、関数内で point を変更すると、main 側の member 配列の中身もそのまま変わります。

この性質はとても便利です。
たとえば、複数の構造体データをまとめて更新する処理では、ポインタ渡しが自然で効率的です。

表示専用の関数と更新する関数の違い

今回の2つの関数には、役割の違いがあります。

関数名役割const の有無
print_memberデータを表示するconst あり
add_bonus_pointデータを更新するconst なし

print_member では、表示だけを行うので const を付けています。

void print_member(const Member *p, int size)

これにより、「この関数は読み取り専用です」という意図がはっきりします。

一方、add_bonus_point ではポイントを増やす必要があるので、const は付けません。

void add_bonus_point(Member *p, int size)

このように、関数が「読むだけ」なのか「変更する」のかを、引数宣言から読み取れるようにしておくと、とても見やすいプログラムになります。

添字を使う書き方にも置き換えられる

構造体配列では、ポインタ演算を使わず、添字で書くこともできます。
同じ処理を、よりなじみやすい形で書くと次のようになります。

void add_bonus_point(Member p[], int size)
{
    for (int i = 0; i < size; i++) {
        p[i].point += 5;
    }
}

void print_member(const Member p[], int size)
{
    for (int i = 0; i < size; i++) {
        printf("%s : %dポイント\n", p[i].name, p[i].point);
    }
}

こちらのほうが読みやすいと感じる人も多いです。

実は、配列引数の Member p[] は、関数の引数としては Member *p とほぼ同じ意味で扱われます。
そのため、次の2つは引数として同じように使えます。

void print_member(Member *p, int size)
void print_member(Member p[], int size)

どちらで書いてもよいですが、

  • ポインタとして渡していることを強調したいなら Member *p
  • 配列として扱うことをわかりやすく見せたいなら Member p[]

という使い分けができます。

この図で特に大事なのは、「配列全部が移動するわけではない」という点です。
渡されるのは先頭要素の場所だけです。

そこを出発点として、p + i で配列の各要素にアクセスしていきます。
つまり、配列の処理でも本質は1個の構造体をポインタで渡すときと同じです。

違うのは、「先頭から何番目か」をずらしながら使っていることだけです。

構造体をポインタで渡すメリット

構造体をポインタで渡す方法には、いくつかの大きな利点があります。

メリット内容
コピーが不要大きな構造体でも無駄なコピーを避けられる
元データを直接扱える関数内で更新が可能
効率がよい配列や多数のデータを扱うときに有利
意図を明確にできるconst によって読み取り専用を表現できる

特に、配列や大きめの構造体を扱う場面では、ポインタ渡しは実用的です。
C言語らしい書き方として、これから何度も登場します。

気をつけたいポイント

便利な反面、注意したい点もあります。

まず、ポインタが無効な場所を指していると、正しく動きません。
また、関数の中で内容を書き換えるつもりがないのに const を付けていないと、意図が伝わりにくくなります。

さらに、. と -> を混同しやすいので、次の区別はしっかり意識しておくと安心です。

対象メンバアクセス
構造体変数.
構造体へのポインタ->

この2つをきちんと使い分けられるようになると、構造体とポインタの理解がかなり深まります。

覚えておきたい見方

最後に、このテーマで特に大切な見方を整理しておきます。

構造体を関数にポインタで渡すとは、構造体そのものを渡すのではなく、「その構造体がある場所」を渡すことです。
そのため、関数は元のデータを直接参照できます。
読み取りだけなら const を付ける、更新するなら const を外す、という使い分けも重要です。

1個の構造体でも、構造体の配列でも、考え方の基本は同じです。
「アドレスを渡して、その先のメンバにアクセスする」
この流れをつかめれば、構造体と関数の組み合わせがぐっと自然に使えるようになります。