プログラミングで実用的なC言語コードを書くには、値をコピーするだけでなく「アドレスを渡す」テクニックが欠かせません。
この記事では、関数・構造体・配列を中心に、アドレス渡し(pointer)の基本から実践的な書き方まで詳しく解説します。
図解やサンプルコードを多用しながら、初学者でも理解しやすいように丁寧に説明していきます。
アドレス渡しとは
アドレス渡し(pointer)の基本概念

C言語でアドレス渡しと呼ばれるのは、関数に「値そのもの」ではなく変数が置かれているメモリの場所(アドレス)を渡すことです。
このとき使うのがポインタ(pointer)です。
ポインタ変数は、次のように宣言します。
#include <stdio.h>
int main(void) {
int x = 10; // ふつうのint変数
int *p = &x; // xのアドレスをpに代入
printf("xの値: %d\n", x);
printf("xのアドレス(&x): %p\n", (void *)&x);
printf("pの値(中身のアドレス): %p\n", (void *)p);
printf("*pの値(ポインタ先の中身): %d\n", *p);
return 0;
}
xの値: 10
xのアドレス(&x): 0x7ffee1f9a9ac ← 実行ごとに変わります
pの値(中身のアドレス): 0x7ffee1f9a9ac
*pの値(ポインタ先の中身): 10
ここで重要なのは、ポインタ変数p自身は別の変数であり、その中に「指し示す先のアドレス」が入っているという点です。
*pと書くことで、「アドレスpが指す先の実体の値」にアクセスできます。
値渡しとの違いと使い分け

C言語では、関数の引数は基本的に「値渡し」です。
値渡しとアドレス渡しの違いを整理すると、次のようになります。
| 種類 | 関数に渡すもの | 関数内から元の変数を直接変えられるか | 主な用途 |
|---|---|---|---|
| 値渡し | 値のコピー | できない | 計算・表示など読み取り専用 |
| アドレス渡し | 変数のアドレス(pointer) | できる | 値の更新・複数値の返却・大きなデータ |
値渡しでは、元の変数の値がコピーされるだけなので、関数内で値を変更しても呼び出し元には影響しません。
一方、アドレス渡しでは、元の変数のアドレスを渡すため、関数の中から元の変数そのものを更新することができます。
使い分けとしては、次のように考えると分かりやすいです。
- 読み取りだけなら値渡し
- 呼び出し元の変数を更新したいならアドレス渡し
- 大きな構造体や配列はアドレス渡しで効率よく扱う
なぜアドレス渡しが必要なのか

アドレス渡しを使う主な理由は、次の3つです。
1つ目は、呼び出し元の変数を関数内で直接変更したい場合です。
たとえばswap関数で2つの変数の値を入れ替えたいとき、値渡しでは入れ替わった結果が呼び出し元に反映されません。
アドレス渡しであれば、元の変数自体を書き換えられます。
2つ目は、関数から複数の値を返したい場合です。
C言語の戻り値は1つだけですが、ポインタ引数を通して実質的にいくつでも返すことができます。
3つ目は、大きなデータを効率よく扱いたい場合です。
大きな構造体や配列を値渡しすると、コピーコストが高くなります。
アドレス渡しならアドレス1つ分しか渡さないので、高速でメモリも節約できます。
関数でのアドレス渡し
関数引数にポインタを使う書き方

関数でアドレス渡しを行う場合、次の2点がポイントになります。
- 関数の仮引数をポインタ型にする
- 呼び出し時にアドレス演算子
&を付けて呼ぶ
具体的な宣言と呼び出しの例です。
#include <stdio.h>
// 仮引数を「intのポインタ」として受け取る
void set_to_zero(int *p) {
*p = 0; // ポインタ先の変数を0にする
}
int main(void) {
int x = 123;
printf("関数呼び出し前: x = %d\n", x);
set_to_zero(&x); // xのアドレスを渡す(アドレス渡し)
printf("関数呼び出し後: x = %d\n", x);
return 0;
}
関数呼び出し前: x = 123
関数呼び出し後: x = 0
関数側ではint *pと宣言し、呼び出し側では&xを渡す、というペアで覚えると理解しやすくなります。
変数の値を更新するアドレス渡しの例

2つの変数の値を入れ替えるswap関数の例で、アドレス渡しの威力を確認します。
#include <stdio.h>
// 2つのint変数の値を入れ替える関数(アドレス渡し)
void swap(int *x, int *y) {
int temp;
temp = *x; // xが指す変数の値を退避
*x = *y; // yが指す変数の値をxへ
*y = temp; // 退避しておいた値をyへ
}
int main(void) {
int a = 10;
int b = 20;
printf("swap前: a = %d, b = %d\n", a, b);
// aとbのアドレスを渡す
swap(&a, &b);
printf("swap後: a = %d, b = %d\n", a, b);
return 0;
}
swap前: a = 10, b = 20
swap後: a = 20, b = 10
この例では、関数内から変数aとbそのものを入れ替えています。
値渡しでは絶対に実現できない動きであり、アドレス渡しの基本的な使い方として非常に重要です。
複数の戻り値を返すテクニック

C言語の関数は戻り値を1つしか返せませんが、ポインタを使えば「実質的に複数の値を返す」ことができます。
例えば、整数の割り算で商と余りを同時に返したい場合の例です。
#include <stdio.h>
// numをdenで割った結果、商を戻り値、余りをポインタ引数で返す
int div_mod(int num, int den, int *remainder) {
int q = num / den; // 商
int r = num % den; // 余り
*remainder = r; // 余りを呼び出し元の変数へ書き込む
return q; // 商は戻り値として返す
}
int main(void) {
int n = 17;
int d = 5;
int r; // 余りを受け取る変数
int q; // 商を受け取る変数
q = div_mod(n, d, &r); // rのアドレスを渡す
printf("%d / %d = %d ... %d\n", n, d, q, r);
return 0;
}
17 / 5 = 3 ... 2
このように、戻り値とポインタ引数を組み合わせることで、機能的には複数の値を戻す関数を作ることができます。
構造体のアドレス渡し
構造体を値渡しする場合の注意点

構造体を引数として関数に渡すとき、何も意識しないと値渡しになります。
つまり、構造体全体がコピーされることになります。
#include <stdio.h>
typedef struct {
char name[32];
int age;
} Person;
// 構造体を値渡ししている例(コピーが作られる)
void print_person(Person p) {
printf("名前: %s, 年齢: %d\n", p.name, p.age);
}
int main(void) {
Person taro = {"Taro", 20};
print_person(taro); // taroのコピーが関数に渡される
return 0;
}
値渡し自体は問題ありませんが、構造体が大きくなるほどコピーに時間とメモリがかかります。
読み取りだけならまだしも、関数内で構造体を更新したい場合には元の構造体は変わらないという点にも注意が必要です。
構造体のポインタ渡しで高速化する方法

構造体を効率よく扱うには、構造体のポインタを引数に渡す方法が一般的です。
こうすることで、関数に渡すのはアドレス1つ分だけになり、高速でメモリ使用量も抑えられます。
また、ポインタ渡しにすれば構造体の中身を関数内から書き換えることもできます。
#include <stdio.h>
typedef struct {
char name[32];
int age;
} Person;
// 構造体のポインタを受け取り、中身を更新する
void have_birthday(Person *p) {
p->age += 1; // 誕生日なので1歳増やす
}
int main(void) {
Person hanako = {"Hanako", 25};
printf("誕生日の前: %s, %d歳\n", hanako.name, hanako.age);
// 構造体のアドレスを渡す
have_birthday(&hanako);
printf("誕生日の後: %s, %d歳\n", hanako.name, hanako.age);
return 0;
}
誕生日の前: Hanako, 25歳
誕生日の後: Hanako, 26歳
このように、構造体のポインタ渡しは性能面と機能面の両方でメリットがあります。
構造体ポインタとメンバアクセス演算子(->)の使い方

構造体のポインタからメンバにアクセスするには、メンバアクセス演算子->を使います。
これは次のような書き方の省略形です。
(*p).ageとp->ageは同じ意味
ドット演算子と矢印演算子の違いを整理します。
| 使う演算子 | 左側に来るもの | 例 |
|---|---|---|
. | 構造体そのもの | person.age |
-> | 構造体へのポインタ | person_ptr->age |
実際のコード例です。
#include <stdio.h>
typedef struct {
char name[32];
int age;
} Person;
int main(void) {
Person alice = {"Alice", 30};
Person *p = &alice; // aliceのアドレスをpに格納
// ドット演算子でアクセス(構造体変数そのもの)
printf("alice.name = %s, alice.age = %d\n", alice.name, alice.age);
// 矢印演算子でアクセス(構造体ポインタから)
printf("p->name = %s, p->age = %d\n", p->name, p->age);
// 書き換えも可能
p->age += 5;
printf("5年後: %s, %d歳\n", p->name, p->age);
return 0;
}
alice.name = Alice, alice.age = 30
p->name = Alice, p->age = 30
5年後: Alice, 35歳
構造体ポインタを扱うなら->演算子は必須なので、早めになじんでおくと良いです。
配列のアドレス渡し
配列とポインタの関係

C言語では、配列名とポインタの間には密接な関係があります。
多くの場面で、配列名は「先頭要素へのポインタ」として振る舞います。
例えばint a[5];という配列があるとき、次のような関係があります。
aは、ほぼ&a[0]と同じ意味a[i]は、*(a + i)と同じ意味
簡単な確認コードです。
#include <stdio.h>
int main(void) {
int a[3] = {10, 20, 30};
int *p = a; // aは先頭要素へのポインタとして扱われる
printf("a[0] = %d, *(a+0) = %d, *p = %d\n", a[0], *(a+0), *p);
printf("a[1] = %d, *(a+1) = %d, *(p+1) = %d\n", a[1], *(a+1), *(p+1));
printf("a[2] = %d, *(a+2) = %d, *(p+2) = %d\n", a[2], *(a+2), *(p+2));
return 0;
}
a[0] = 10, *(a+0) = 10, *p = 10
a[1] = 20, *(a+1) = 20, *(p+1) = 20
a[2] = 30, *(a+2) = 30, *(p+2) = 30
この性質があるため、配列を関数に渡すときは、実質的に「アドレス渡し」になっていると考えることができます。
関数引数に配列を渡す方法と注意点

配列を関数に渡すとき、引数の書き方にはいくつかバリエーションがあります。
次の2つは実質的に同じ意味です。
int sum(int a[], int n);int sum(int *a, int n);
どちらも「intへのポインタ」を受け取っていると考えられます。
よく使われる「配列を渡して合計を計算する」関数の例です。
#include <stdio.h>
// 配列と要素数を受け取り、合計を返す関数
int sum(int *arr, int n) {
int i;
int s = 0;
for (i = 0; i < n; i++) {
s += arr[i]; // arr[i] は *(arr + i) と同じ
}
return s;
}
int main(void) {
int data[5] = {1, 2, 3, 4, 5};
int total;
// 配列名 data は先頭要素へのアドレスとして渡される
total = sum(data, 5);
printf("合計: %d\n", total);
return 0;
}
合計: 15
ここでの注意点は、配列全体がコピーされるわけではないということです。
関数の中でarr[i]を書き換えると、呼び出し元のdata[i]も変更されます。
#include <stdio.h>
// 配列の全要素を2倍にする
void double_array(int *arr, int n) {
int i;
for (i = 0; i < n; i++) {
arr[i] *= 2; // 呼び出し元の配列も変更される
}
}
int main(void) {
int data[3] = {10, 20, 30};
int i;
double_array(data, 3); // dataの先頭アドレスを渡す
for (i = 0; i < 3; i++) {
printf("data[%d] = %d\n", i, data[i]);
}
return 0;
}
data[0] = 20
data[1] = 40
data[2] = 60
「配列を渡したつもりが、関数の中で勝手に書き換えられていた」というのは初心者がつまずきやすいポイントです。
読み取り専用にしたい場合は、const int *arrのようにconst修飾子を付けておくと安全です。
2次元配列をアドレス渡しするサンプルコード

2次元配列を関数に渡すときは、少しだけ書き方に注意が必要です。
典型的な行列出力の例を見てみます。
#include <stdio.h>
// 2x3の行列を受け取って表示する関数
void print_matrix(int a[2][3]) {
int i, j;
for (i = 0; i < 2; i++) {
for (j = 0; j < 3; j++) {
printf("%3d ", a[i][j]);
}
printf("\n");
}
}
// 行数を引数で受け取り、列数は固定3という形でもよい
void print_matrix_var_rows(int rows, int a[][3]) {
int i, j;
for (i = 0; i < rows; i++) {
for (j = 0; j < 3; j++) {
printf("%3d ", a[i][j]);
}
printf("\n");
}
}
int main(void) {
int m[2][3] = {
{ 1, 2, 3 },
{ 4, 5, 6 }
};
printf("print_matrix:\n");
print_matrix(m); // 2x3行列として渡す
printf("print_matrix_var_rows:\n");
print_matrix_var_rows(2, m); // 行数を別途渡す
return 0;
}
print_matrix:
1 2 3
4 5 6
print_matrix_var_rows:
1 2 3
4 5 6
ここで重要なのは、2次元配列を引数に書くとき、最後の次元(列数)は必ず指定しなければならないという点です。
void f(int a[][3]);← OKvoid f(int a[][]);← NG
より柔軟に扱いたい場合には、「1次元配列+自前でインデックス計算」や「ポインタ配列」などのテクニックもありますが、まずは上記のような基本形から慣れていくと良いです。
まとめ
アドレス渡し(pointer)は、C言語で実用的なプログラムを書くうえで欠かせない技術です。
値渡しとの違いを理解すると、なぜポインタを使うのかが明確になります。
関数にアドレスを渡せば、呼び出し元の変数や構造体、配列を直接更新できますし、大きなデータを効率よく扱うことも可能です。
構造体ポインタには->、配列は先頭アドレスとして扱われるなど、基本ルールを押さえておけば応用も難しくありません。
この記事のコードと図解を参考に、ぜひ自分でもアドレス渡しのサンプルを書いて感覚をつかんでみてください。
