C言語で構造体を関数に渡すとき、「そのまま渡すか」「アドレスを渡すか」で動作や効率が大きく変わります。
本記事では、構造体を題材に値渡しとアドレス渡しの違いを、基本から丁寧に解説します。
実用的なサンプルコードと図解を通じて、構造体配列を含めた「正しい渡し方」が自然に身につくように進めていきます。
C言語の構造体と引数の渡し方の基本
構造体とは

C言語の構造体(struct)は、関連する複数のデータを1つの「かたまり」として扱うための仕組みです。
たとえば、人を表すデータを扱うとき、年齢や身長、名前などを別々の変数で管理すると煩雑になります。
そこで構造体を使えば、これらをひとまとめにしたPersonという「型」を定義し、扱いやすく整理できます。
構造体の基本定義と利用例
まずは、人を表すPerson構造体を定義して使う例です。
#include <stdio.h>
// 人を表す構造体の定義
struct Person {
int age; // 年齢
double height; // 身長
char name[32]; // 名前(簡略のため固定長配列)
};
int main(void) {
// 構造体変数の宣言と初期化
struct Person alice = { 25, 160.5, "Alice" };
// メンバへアクセス(ドット演算子)
printf("Name: %s\n", alice.name);
printf("Age : %d\n", alice.age);
printf("Height: %.1f\n", alice.height);
return 0;
}
Name: Alice
Age : 25
Height: 160.5
ここではstruct Personという新しい型を定義し、その型の変数aliceを1つ用意しています。
ドット演算子.を使って、構造体の各メンバにアクセスしています。
構造体と配列・ポインタの違い

C言語では、構造体のほかに配列やポインタもよく使われますが、それぞれ役割や性質が異なります。
構造体と配列の違い
配列は「同じ型の要素が並んだデータ集合」です。
一方、構造体は「異なる型のメンバをまとめた1つのデータ」です。
たとえば、学生の点数なら「国語の点数だけの配列」「数学の点数だけの配列」のように、同じ型が並ぶので配列が向いています。
一方「名前・学年・点数」をセットで扱いたいなら、構造体を使うのが自然です。
構造体とポインタの違い
ポインタは「データそのもの」ではなく「データが置かれているアドレス(場所)」を保存する変数です。
構造体そのものを変数として持つ場合と、構造体へのポインタを持つ場合とでは、メモリの扱いや関数への渡し方に違いが出てきます。
この違いが、あとで説明する値渡しとアドレス渡しで重要になります。
C言語の引数の渡し方

C言語の関数に引数を渡す方法は、概念的に次の2種類で考えると理解しやすくなります。
- 値渡し(pass by value)
変数の中身をコピーして関数に渡す方法です。C言語の関数引数は、基本的にすべて値渡しです。 - アドレス渡し(参照渡し風、pass by pointer)
変数そのものではなく、変数のアドレスを渡す方法です。Cでは「参照渡し」という構文はないため、ポインタを渡すことで実現します。
構造体を引数に渡すときも、
struct Personを直接引数にするか、struct Person *のようにポインタとして渡すかで振る舞いが変わります。
この記事では、この違いを丁寧に追っていきます。
構造体の値渡し(コピーして渡す)の基礎
構造体を引数に値渡しする書き方

構造体を値渡しするには、関数の引数にstruct 型名をそのまま指定します。
具体的なコードを見てみます。
#include <stdio.h>
struct Person {
int age;
double height;
char name[32];
};
// 構造体を「値渡し」で受け取る関数
void print_person(struct Person p) {
// pは引数として受け取った構造体の「コピー」
printf("[print_person] name=%s, age=%d, height=%.1f\n",
p.name, p.age, p.height);
}
int main(void) {
struct Person alice = { 25, 160.5, "Alice" };
// aliceの内容がコピーされて、print_personへ渡される
print_person(alice);
return 0;
}
[print_person] name=Alice, age=25, height=160.5
このように、関数定義側でstruct Person pと書けば、構造体変数をそのまま引数として渡す(値渡し)ことができます。
値渡しで構造体を受け取るときの動作

値渡しでは、構造体全体がコピーされるため、関数内で値を変更しても呼び出し元には影響しません。
具体的な例を見てみます。
#include <stdio.h>
struct Person {
int age;
double height;
char name[32];
};
// 値渡しで受け取った構造体の中身を変更してみる関数
void birthday(struct Person p) {
printf("[birthday] before: %s is %d years old.\n",
p.name, p.age);
// コピー側の年齢だけが1つ増える
p.age++;
printf("[birthday] after : %s is %d years old.\n",
p.name, p.age);
}
int main(void) {
struct Person alice = { 25, 160.5, "Alice" };
printf("[main] before : %s is %d years old.\n",
alice.name, alice.age);
birthday(alice); // aliceがコピーされて渡される
// aliceのageは変わっていない
printf("[main] after : %s is still %d years old.\n",
alice.name, alice.age);
return 0;
}
[main] before : Alice is 25 years old.
[birthday] before: Alice is 25 years old.
[birthday] after : Alice is 26 years old.
[main] after : Alice is still 25 years old.
この結果からわかるように、関数内で構造体のメンバを変更しても、元の変数には影響しません。
これは値渡しであるため、あくまでコピーを操作しているからです。
構造体の値渡しのメリット・デメリット
構造体を値渡しする場合の特徴を、表で整理しておきます。
| 項目 | 内容 |
|---|---|
| メリット | 呼び出し元の値が関数内で書き換えられないので、安全で直感的です。関数側がデータを「読むだけ」の場合にわかりやすいです。 |
| デメリット | 構造体全体をコピーするため、構造体が大きいほど処理コストやメモリ使用量が増える可能性があります。呼び出し回数が多いとパフォーマンスに影響が出ることがあります。 |
「呼び出し元を絶対に変えたくない読み取り専用の処理」には値渡しが適している一方で、構造体が大きくなるほど効率面で不利になっていきます。
大きな構造体を値渡しする場合の注意点

構造体のサイズが大きくなると、値渡しによるコピーコストが無視できなくなります。
たとえば、画像データや大きな配列をメンバに含む構造体を例に考えてみます。
#include <stdio.h>
#define DATA_SIZE 100000 // 大きな配列サイズの例
// 大きな構造体の例
struct BigData {
int id;
double values[DATA_SIZE]; // 大きな配列を持つ
};
// BigDataを「値渡し」で受け取る関数
void process_bigdata(struct BigData data) {
// ここへ来るまでに、BigData全体がコピーされていることに注意
printf("Processing BigData id=%d\n", data.id);
}
int main(void) {
struct BigData big = { 1, {0} };
// 毎回BigData全体をコピーして渡す
process_bigdata(big);
return 0;
}
このようなコードでは、関数呼び出しのたびに配列values全体がコピーされることになります。
パフォーマンスを重視する場合は、構造体のアドレスを渡す(ポインタ渡し)方が現実的です。
構造体のアドレス渡し(ポインタ渡し)の基礎
構造体のポインタを引数にする書き方

構造体のアドレスを渡す場合は、struct 型名 *というポインタ型を引数に指定します。
次の例では、先ほどのPerson構造体をポインタで受け取って表示しています。
#include <stdio.h>
struct Person {
int age;
double height;
char name[32];
};
// 構造体ポインタを受け取る関数
void print_person_ptr(const struct Person *p) {
// pは構造体の「アドレス」を受け取る
// constを付けることで、この関数では内容を書き換えないことを明示
printf("[print_person_ptr] name=%s, age=%d, height=%.1f\n",
p->name, p->age, p->height);
}
int main(void) {
struct Person alice = { 25, 160.5, "Alice" };
// &alice でaliceのアドレスを渡す
print_person_ptr(&alice);
return 0;
}
[print_person_ptr] name=Alice, age=25, height=160.5
引数側ではconst struct Person *pとして、構造体のアドレス(ポインタ)を受け取っています。
呼び出し側では&aliceのように、構造体変数のアドレス演算子&を付けて渡す点に注意してください。
アドレス渡しで構造体を変更する方法

ポインタを使ってアドレス渡しをすると、関数の中から呼び出し元の構造体を直接書き換えることができます。
#include <stdio.h>
struct Person {
int age;
double height;
char name[32];
};
// 構造体のポインタを使い、呼び出し元のデータを変更する関数
void birthday_ptr(struct Person *p) {
printf("[birthday_ptr] before: %s is %d years old.\n",
p->name, p->age);
// 呼び出し元の構造体のageを直接変更
p->age++;
printf("[birthday_ptr] after : %s is %d years old.\n",
p->name, p->age);
}
int main(void) {
struct Person alice = { 25, 160.5, "Alice" };
printf("[main] before : %s is %d years old.\n",
alice.name, alice.age);
// アドレス渡し: &alice を渡す
birthday_ptr(&alice);
// aliceのageが変更されている
printf("[main] after : %s is now %d years old.\n",
alice.name, alice.age);
return 0;
}
[main] before : Alice is 25 years old.
[birthday_ptr] before: Alice is 25 years old.
[birthday_ptr] after : Alice is 26 years old.
[main] after : Alice is now 26 years old.
アドレス渡しでは、元の構造体そのものを関数から変更できるため、更新処理や初期化処理などで非常によく使われます。
アロー演算子(->)とドット(.)の使い分け

構造体のメンバにアクセスする演算子には.と->の2種類があります。
「実体」か「ポインタ」かで使い分けます。
- ドット演算子
.
構造体変数そのものからメンバにアクセスするときに使います。
例:alice.age,alice.name - アロー演算子
->
構造体のポインタからメンバにアクセスするときに使います。
例:p->age,p->name
実はp->ageは、(*p).ageの糖衣構文です。
次の短い例で確認してみましょう。
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point pt = { 10, 20 };
struct Point *p = &pt; // ptのアドレスをpに保存
// ドット演算子: 実体からメンバへ
printf("pt.x = %d, pt.y = %d\n", pt.x, pt.y);
// アロー演算子: ポインタからメンバへ
printf("p->x = %d, p->y = %d\n", p->x, p->y);
// (*p).x という書き方でも同じ意味
printf("(*p).x = %d, (*p).y = %d\n", (*p).x, (*p).y);
return 0;
}
pt.x = 10, pt.y = 20
p->x = 10, p->y = 20
(*p).x = 10, (*p).y = 20
実務ではほぼ必ず->を使うと言ってよいので、構造体ポインタ -> メンバという形に慣れておくとよいです。
構造体のアドレス渡しのメリット・デメリット
構造体をポインタで渡す場合の特徴も、表で整理しておきます。
| 項目 | 内容 |
|---|---|
| メリット | 構造体本体をコピーせず、小さなアドレス値だけをコピーするので効率的です。関数内から呼び出し元の構造体を直接変更できます。 |
| デメリット | 関数内の変更がそのまま呼び出し元に反映されるため、意図しない書き換えバグが起きやすくなります。ポインタのNULLチェックなど、安全面の配慮が必要です。 |
「データが大きい」「中身を変更したい」場合は、ポインタ渡し(アドレス渡し)が実用的です。
ただし、どの関数がどの構造体を変更しうるかを、コメントやconst修飾などで明確にしておくことが重要です。
構造体配列と関数への渡し方の実践
構造体配列を引数に渡す方法

構造体の「配列」を関数に渡す場合も、基本的には「アドレス(ポインタ)が渡される」という点が重要です。
次の例では、複数人の情報を構造体配列に格納し、関数で一覧表示しています。
#include <stdio.h>
struct Person {
int age;
double height;
char name[32];
};
// 構造体配列を受け取って一覧表示する関数
void print_people(const struct Person people[], int count) {
// peopleは const struct Person *people と同じ意味
for (int i = 0; i < count; i++) {
printf("[%d] name=%s, age=%d, height=%.1f\n",
i, people[i].name, people[i].age, people[i].height);
}
}
int main(void) {
struct Person people[3] = {
{ 25, 160.5, "Alice" },
{ 30, 175.2, "Bob" },
{ 22, 155.0, "Carol" }
};
// 配列名peopleは、先頭要素(&people[0])へのポインタとして渡される
print_people(people, 3);
return 0;
}
[0] name=Alice, age=25, height=160.5
[1] name=Bob, age=30, height=175.2
[2] name=Carol, age=22, height=155.0
関数側ではconst struct Person people[]と書いていますが、これはconst struct Person *peopleと同じ意味です。
配列を引数にするときは、暗黙的にポインタ渡しになる点を覚えておきましょう。
関数の中で構造体配列の中身を書き換えるには

構造体配列を渡すと、先ほど説明したようにポインタとして渡されるため、関数の中から配列の要素を変更すると呼び出し元に反映されます。
#include <stdio.h>
struct Person {
int age;
double height;
char name[32];
};
// 構造体配列の要素を書き換える関数
void make_everyone_older(struct Person people[], int count) {
for (int i = 0; i < count; i++) {
printf("[before] %s: age=%d\n", people[i].name, people[i].age);
// 各人の年齢を+1する
people[i].age++;
printf("[after ] %s: age=%d\n", people[i].name, people[i].age);
}
}
int main(void) {
struct Person people[3] = {
{ 25, 160.5, "Alice" },
{ 30, 175.2, "Bob" },
{ 22, 155.0, "Carol" }
};
make_everyone_older(people, 3);
// 関数呼び出し後、main側から見ても年齢が更新されている
printf("--- after function call ---\n");
for (int i = 0; i < 3; i++) {
printf("%s: age=%d\n", people[i].name, people[i].age);
}
return 0;
}
[before] Alice: age=25
[after ] Alice: age=26
[before] Bob: age=30
[after ] Bob: age=31
[before] Carol: age=22
[after ] Carol: age=23
--- after function call ---
Alice: age=26
Bob: age=31
Carol: age=23
このように、配列を渡した場合は、その中身を書き換えることができます。
これは、構造体単体のアドレス渡しと同じ考え方です。
値渡しとアドレス渡しの使い分けの指針とコツ

ここまで見てきたように、構造体の引数には値渡しとアドレス渡しの2つのスタイルがあります。
それぞれをどのように使い分ければよいか、実務でも役に立つ指針を整理します。
1. 関数内で構造体を変更するかどうか
「関数が構造体の中身を変更すること」が目的ならアドレス渡し一択です。
初期化、更新、入力処理などはこのケースに当たります。
- 例:
init_person(struct Person *p) - 例:
read_person(struct Person *p)
引数名にもpやout_personのように、「ポインタであり、変更される可能性がある」ことがわかる名前を付けると読みやすくなります。
2. 関数は「読むだけ」かつ構造体が小さい場合
「読むだけ」で、構造体サイズが小さい(たとえばメンバ数が少なくて数十バイト程度)なら、値渡しでも問題ないことが多いです。
安全で直感的であり、コードもシンプルです。
- 例:
print_person(struct Person p) - 例:
calc_bmi(struct Person p)
ただし、「将来メンバが増えて大きくなるかも」という設計上の懸念がある場合は、const struct Person *で受け取る方法も選択肢に入ります。
3. 構造体が大きい、またはパフォーマンスが重要な場合
画像データや大量の数値配列など、サイズの大きな構造体を頻繁にやりとりする場合は、最初からconst付きポインタで渡す設計を検討します。
void process_bigdata(const struct BigData *data);
このようにすると、コピーのコストを避けながら「関数からは書き換えない」という意図も明示できます。
4. 構造体配列は基本的に「アドレス渡し」
構造体配列は、引数として渡すと自動的に先頭要素へのポインタになります。
したがって、配列の中身を書き換える関数ではそのまま渡せばよく、読み取り専用にしたい場合はconstを付けて安全性を高めます。
void update_people(struct Person people[], int count); // 書き換える
void show_people(const struct Person people[], int count); // 読むだけ
「書き換える関数か」「読むだけの関数か」を最初に決めて、その方針に従って値渡しかアドレス渡しかを選ぶクセを付けると、コード全体の見通しがよくなります。
まとめ
構造体を関数に渡すとき、値渡しは「コピーを渡す」、アドレス渡しは「場所を教える」というイメージで考えると整理しやすくなります。
読み取り専用なら小さな構造体は値渡し、大きな構造体や配列、変更目的の処理ではポインタを使うと、効率と安全性のバランスが取りやすくなります。
ドット演算子.とアロー演算子->の使い分け、配列引数が実質ポインタであることも合わせて理解すれば、構造体の引数の扱いで迷う場面は大きく減らせます。
