6日でできる 新C言語入門|C言語の値渡しとポインタ渡し

値を渡すか、アドレスを渡すか。
その違いが分かると、C言語の関数はもっと自由に使いこなせるようになります。

C言語の関数を学んでいくと、引数の受け渡し方法がとても大切だと分かってきます。
特に初学者のうちは、関数の中で変数の値を変えたのに、呼び出し元の変数は変わっていない、という場面で戸惑いやすいものです。

この理由を理解するカギになるのが、値渡しとポインタ渡しの違いです。

値渡しは、変数の中に入っている値そのものを関数へ渡す方法です。
一方、ポインタ渡しは、変数が置かれている場所の情報、つまりアドレスを関数へ渡す方法です。

この2つは見た目が少し似ていても、動き方は大きく異なります。
値渡しでは、関数の中で引数を変更しても、元の変数には影響しません。
ポインタ渡しでは、元の変数の場所をたどって値を書き換えられるので、呼び出し元の変数そのものを変更できます。

また、ポインタ渡しを使うと、関数から複数の結果を返すこともできます。
たとえば、和と差を同時に求めたいときのように、戻り値1つだけでは足りない場面でとても便利です。

ここでは、値渡しとポインタ渡しの基本から、実際のプログラム例、使いどころ、注意点まで、流れに沿ってやさしく整理していきます。

値渡しとポインタ渡しの違いを最初に整理しよう

まずは、2つの違いを表で見てみましょう。

種類何を渡すか関数内で変更した結果よくある使いどころ
値渡し値そのもの呼び出し元には反映されない単純な計算、値の参照
ポインタ渡し変数のアドレス呼び出し元に反映される値の入れ替え、複数結果の受け取り

値渡しでは、関数に渡されるのは元の変数のコピーです。
そのため、関数の中で引数を書き換えても、変わるのはコピー側だけです。

ポインタ渡しでは、変数そのものではなく、その変数が置かれている場所の情報を渡します。
関数は、そのアドレスを通じて元の変数へアクセスできるので、呼び出し元の値を直接変更できます。

図:値渡しとポインタ渡しの違い

この図から分かること

この図から分かるのは、値渡しでは関数側に渡るのがコピーであるのに対し、ポインタ渡しでは元の変数の場所そのものをたどれるという点です。

値渡しの引数は、元の変数と別の箱として扱われます。
そのため、関数内で値を変えても、呼び出し元には影響しません。

一方で、ポインタ渡しはアドレスを受け取るので、関数内から呼び出し元の変数へ直接アクセスできます。
この違いが、C言語の関数の使い分けでとても重要になります。

値渡しでは元の変数は変わらない

ポインタ渡しの便利さを理解するためには、まず値渡しの性質をしっかり押さえておくことが大切です。

たとえば、関数に int x を渡した場合、その x は呼び出し元の変数そのものではありません。
あくまで、その値を受け取った別の変数です。

つまり、関数内で x を 100 にしても、呼び出し元の変数が自動的に 100 になるわけではありません。

この性質は、関数が安全に動くという面ではメリットでもあります。
関数の中で誤って値を書き換えてしまっても、元の変数には影響しにくいからです。

一方で、呼び出し元の値を変えたい処理には向きません。
たとえば、2つの変数の値を入れ替えるような処理を値渡しだけで実現しようとしても、呼び出し元は変化しません。

そのような場面で活躍するのが、ポインタ渡しです。

ポインタ渡しでは呼び出し元の変数を変更できる

ポインタ渡しでは、関数に変数のアドレスを渡します。
そして、関数側ではポインタ引数を使って、そのアドレスが指している先の値を読み書きします。

たとえば、int型の変数 value のアドレスは &value で取得できます。
それを int* 型の引数で受け取ると、関数内では *p のように書いて元の変数の値へアクセスできます。

ここで大事なのは、関数が受け取っているのは値そのものではなく、値が置かれている場所の情報だということです。

そのため、*p = 50; のように書くと、関数内の一時的なコピーではなく、呼び出し元にある変数の中身が実際に書き換わります。

この仕組みは、C言語でとてもよく使われます。

  • 複数の値を返したいとき
  • 配列の中身を変更したいとき
  • 大きなデータをコピーせずに処理したいとき
  • 変数の値を直接入れ替えたいとき

こうした場面では、ポインタ渡しがとても有効です。

ポインタ渡しで2つの値を入れ替える

まずは、ポインタ渡しの代表例として、2つの整数値を入れ替える関数を見ていきましょう。

プロジェクト/ファイル名: Lesson51_1/main.c

#include <stdio.h>

// 2つの整数値を入れ替える関数
void swap_numbers(int* left, int* right);

int main(void) {
    int scoreA = 18;
    int scoreB = 42;

    printf("入れ替え前: scoreA = %d, scoreB = %d\n", scoreA, scoreB);

    // 変数そのものではなく、アドレスを渡す
    swap_numbers(&scoreA, &scoreB);

    printf("入れ替え後: scoreA = %d, scoreB = %d\n", scoreA, scoreB);

    return 0;
}

// ポインタ経由で元の変数を書き換える
void swap_numbers(int* left, int* right) {
    int temp;

    temp = *left;
    *left = *right;
    *right = temp;
}

実行結果

入れ替え前: scoreA = 18, scoreB = 42
入れ替え後: scoreA = 42, scoreB = 18

このプログラムのポイントは、main関数から swap_numbers 関数を呼び出すときに、scoreA と scoreB ではなく、&scoreA と &scoreB を渡しているところです。

swap_numbers(&scoreA, &scoreB);

ここで渡しているのは、2つの変数のアドレスです。

関数側では、left と right がそれぞれ int* 型のポインタとして受け取ります。
そのため、*left は scoreA の値、*right は scoreB の値を表します。

temp = *left;
*left = *right;
*right = temp;

この3行は、呼び出し元の変数そのものを入れ替えている処理です。
コピーの値ではなく、本物の変数に対して操作しているので、main関数へ戻ったあとも値が入れ替わったままになります。

swap_numbers関数の流れを順番に確認しよう

入れ替え処理は、1行ずつ追うと理解しやすくなります。

ステップ*left の値*right の値temp の値内容
11842なし初期状態
2184218temp = *left
3424218*left = *right
4421818*right = temp

このように、いったん temp という一時変数へ値を退避してから入れ替えています。

もし temp を使わずに直接代入すると、片方の元の値が失われてしまうので、入れ替え処理では一時変数がよく使われます。

図:ポインタ渡しで値を入れ替える流れ

この図から分かること

この図から分かるのは、関数が left と right というポインタを通じて、scoreA と scoreB の中身を直接操作していることです。

一時変数 temp にいったん片方の値を保存し、その後で値を順番に入れ替えることで、安全に交換できます。
値渡しではこのような元の変数の入れ替えはできませんが、ポインタ渡しなら実現できます。

ポインタ渡しのメリットは入れ替えだけではない

ポインタ渡しの便利さは、値の交換だけにとどまりません。
実務や学習の中では、次のようなメリットがあります。

呼び出し元の値を直接変更できる

これは今見た通りです。
関数の中で元の変数を書き換えたいときに役立ちます。

複数の結果を同時に返せる

C言語の関数の戻り値は基本的に1つです。
しかし、ポインタ引数を使えば、結果を複数の変数へ書き込むことができます。

大きなデータを効率よく扱える

構造体や配列のようにサイズの大きなデータを値渡しすると、コピーにコストがかかります。
ポインタ渡しなら、アドレスだけを渡せばよいので効率的です。

配列との相性が良い

C言語では配列を関数へ渡すとき、先頭要素のアドレスを渡す形になります。
そのため、配列処理の多くはポインタの考え方と深くつながっています。

複数の結果を受け取る関数の例

次は、ポインタ渡しを使って、2つの実数の和と差を同時に求める例を見てみましょう。

プロジェクト/ファイル名: Lesson51_2/main.c

#include <stdio.h>

// 2つの実数の和と差を求める関数
void calc_total_gap(double x, double y, double* total, double* gap);

int main(void) {
    double price1 = 8.4;
    double price2 = 2.1;
    double totalValue;
    double gapValue;

    calc_total_gap(price1, price2, &totalValue, &gapValue);

    printf("和: %.2f\n", totalValue);
    printf("差: %.2f\n", gapValue);

    return 0;
}

// 和と差をポインタ引数へ書き込む
void calc_total_gap(double x, double y, double* total, double* gap) {
    *total = x + y;
    *gap = x - y;
}

実行結果

和: 10.50
差: 6.30

この例では、x と y は普通の値渡しです。
計算のもとになる数値を渡しています。

一方で、計算結果を書き込む先である total と gap は、ポインタとして受け取っています。

void calc_total_gap(double x, double y, double* total, double* gap)

関数の中では、*total と *gap に対して値を代入しています。

*total = x + y;
*gap = x - y;

こうすることで、main関数側にある totalValue と gapValue に計算結果が書き込まれます。

値渡しだけでは、戻り値1つしか返せません。
しかし、ポインタ渡しを使うことで、和と差という2つの結果を同時に受け取れるようになります。

なぜこの例では値渡しとポインタ渡しを両方使うのか

このプログラムは、値渡しとポインタ渡しの役割分担が分かりやすい例です。

引数渡し方役割
x, y値渡し計算のもとになる入力値
total, gapポインタ渡し計算結果の書き込み先

入力値は、関数の中で読み取れればよいので、値渡しで十分です。
一方、結果は呼び出し元に返したいので、ポインタ渡しで変数のアドレスを渡しています。

この考え方は、C言語の関数設計でとてもよく使われます。

  • 入力用の引数は値渡し
  • 出力用の引数はポインタ渡し

こう整理すると、関数の役割がぐっと分かりやすくなります。

図:ポインタ渡しで複数の結果を返す

この図から分かること

この図から分かるのは、入力値は普通に関数へ渡し、結果だけをポインタ経由で呼び出し元へ返していることです。

戻り値が1つしか使えない場面でも、結果の保存先のアドレスを渡しておけば、複数の出力を受け取れます。
この考え方は、C言語で関数を実用的に使ううえでとても重要です。

ポインタ渡しを使うときの注意点

ポインタ渡しは便利ですが、気をつけたい点もいくつかあります。

アドレスを渡し忘れないこと

ポインタ引数を使う関数に対して、普通の変数をそのまま渡してしまうと型が合いません。

たとえば、次のような関数があるとします。

void swap_numbers(int* left, int* right);

このとき、呼び出し側は次のように書く必要があります。

swap_numbers(&scoreA, &scoreB);

ここで scoreA, scoreB と書いてしまうと、値を渡すことになってしまい、関数が期待している int* 型にはなりません。

NULLを渡さないこと

ポインタがどこも指していない状態を NULL といいます。
もし関数へ NULL を渡し、その関数の中で *p のように参照すると、エラーや異常動作の原因になります。

必要に応じて、関数の中で NULL チェックを入れると安全です。

if (left == NULL || right == NULL) {
    return;
}

学習段階でも、ポインタは有効な場所を指していることを意識しておくと安心です。

型を一致させること

int型の変数のアドレスは int* で受け取ります。
double型の変数のアドレスは double* で受け取ります。

型が合わないポインタを使うと、正しく値を扱えません。

変数の型対応するポインタ型
intint*
doubledouble*
charchar*

ポインタを使うときは、何の型の変数を指しているのかを必ず意識しましょう。

値渡しとポインタ渡しをどう使い分けるか

最後に、使い分けの考え方を整理しておきましょう。

値渡しが向いている場面

  • 関数の中で値を変更しても、呼び出し元へ影響を与えたくないとき
  • 単純な計算だけを行うとき
  • 入力値を読み取るだけの関数にしたいとき

ポインタ渡しが向いている場面

  • 呼び出し元の変数を書き換えたいとき
  • 複数の結果を返したいとき
  • 配列や構造体など大きなデータを効率よく扱いたいとき
  • 関数の外にあるデータを直接操作したいとき

つまり、値渡しは安全で分かりやすく、ポインタ渡しは柔軟で強力です。
どちらが優れているというより、目的に応じて選ぶことが大切です。

値とアドレスの違いを意識すると理解が深まる

C言語の関数をしっかり使いこなすためには、値を渡しているのか、それともアドレスを渡しているのかをいつも意識することが大切です。

値渡しでは、関数へ渡るのはコピーなので、元の変数は変わりません。
ポインタ渡しでは、変数のアドレスを通じて元の場所へアクセスできるため、呼び出し元の値を書き換えたり、複数の結果を受け取ったりできます。

2つの整数を入れ替える関数では、ポインタ渡しによって元の変数そのものを交換できました。
和と差を同時に求める関数では、結果の保存先をポインタで渡すことで、1つの関数から2つの値を返せました。

このように、ポインタ引数はC言語らしい強力な機能です。
最初は少し難しく感じるかもしれませんが、値とアドレスの違い、* と & の役割を丁寧に追っていけば、自然と使い分けられるようになります。

関数を書くときに、この処理は値渡しでよいか、それともポインタ渡しが必要かを考えながら練習していくと、C言語の理解がぐっと深まっていきます。