MMGamesロゴ  MMGames
Twitterシェアボタン  Facebookシェアボタン   
しんで覚えるC言語
しんで覚えるC言語

引数による情報の受け渡し

ポインタ型の引数
第11章では、自作関数の使い方と作り方を説明しましたが、
ここでは、関数から情報を返す方法として、戻り値を使った方法を説明しました。

戻り値を使って情報を返すのが、もっとも簡単な方法であることは間違いありませんが、
この方法では、常に1つの情報しか返すことができません。
2つ以上の情報を返したい時などは不便です。

そのような場合には、ポインタ型の引数を使って情報を返すことができます。
ポインタ型の引数と言っても、別段特別なことではありません。
単に、引数の型がポインタ型であるだけで、普通の引数となんら変わりません。

C言語では、関数へ情報を渡す場合、必ず元の変数の値のコピーを渡します。
この様な方法を値渡しと呼び、元の変数の値が変更されないことが特徴です。
ポインタ型の引数であっても、値のコピーが渡される原則に違いはありません。

それでもポインタ型を使うのは、ポインタ型はアドレスを受け取ることができるからです。
関数を呼び出す時に、すでに存在する変数のアドレスを指定すれば、
呼び出された関数で、受け取ったアドレスをポインタ変数に代入すれば、
後はポインタ変数を通常変数モードに切り替えて、返す情報を代入できます。
返された情報は、呼び出し側で指定した変数に記憶されていることになります。

次のプログラムは、実際にポインタ型の引数を使って情報を返す例です。

ソースコード
#include <stdio.h>

void func(int* pvalue); /* プロトタイプ宣言 */

int main(void)
{
    int value = 10;
    printf("&value = %p\n", &value);
    func(&value); /* アドレスを渡す */
    printf("value = %d\n", value);
    return 0;
}

void func(int* pvalue)
{
    printf("pvalue = %p\n", pvalue);
    *pvalue = 100; /* 通常変数モードに切り替えて代入 */
    return;
}

このプログラムの実行結果は、次のようになるかもしれません。
なお、これは LSIC-86 での結果なので、アドレスは2バイトになっています。

実行結果
&value = 0F68
pvalue = 0F68
value = 100

このプログラムでは、関数を呼び出す時に、変数valueのアドレスを渡しています。
func関数に渡されるのは、あくまでもアドレス値そのもの(今回は0F68)です。
func関数ではそのアドレス値がポインタ変数に代入されているので、
当然、func関数に渡したアドレスと受け取ったアドレスは同じになっています。

ポインタ変数にアドレス値が代入されている場合には、
通常変数モードに切り替えてそのメモリを自由に読み書きできるのだから、
結果として、呼び出された関数から、呼び出し元の変数の中身を書き換えられるわけです。

これまで&付きで呼び出していた関数は、すべて同様の仕組みです。
この使い方が、C言語でのもっともポピュラーなポインタの使い方です。
配列型の引数
これまでは取り扱ってきませんでしたが、配列を引数にすることもできます。
しかし、配列の場合、通常の引数とは異なる性質が多く、扱いにくくなります。
とりあえず、今まで通りの方法で配列型の引数を持つ関数を作ってみます。

引数はint型で要素10の配列とし、配列に代入された値の平均を求める関数を作ります。
今まで通りの方法で実装すると、次の通りになります。

ソースコード
#include <stdio.h>

int getaverage(int data[10]);

int main(void)
{
    int average, array[10] = { 15, 78, 98, 15, 98, 85, 17, 35, 42, 15 };
    average = getaverage(array);
    printf("%d\n", average);
    return 0;
}

int getaverage(int data[10])
{
    int i, average = 0;
    for (i = 0; i < 10; i++) {
        average += data[i];
    }
    return average / 10;
}

このプログラムの実行結果は次の通りになります。

実行結果
49

関数内では、配列の要素番号0~9までの値を変数に加算して、
最後にその結果を10で割って平均値を求めています。

この様に、一見すると配列も引数として渡せるように見えます。
配列型引数の奇妙な性質
前項では、配列を引数として使う方法を説明しましたが、
この関数は、今までの引数ではあり得なかった、奇妙な性質を持っています。

まず、配列の要素数は無視されてしまいます。
次のプログラムは、わざと要素数5の配列を渡してみる例です。

ソースコード
#include <stdio.h>

int getaverage(int data[10]);

int main(void)
{
    int average, array[5] = { 15, 98, 98, 17, 42 }; /* 要素数が5 */
    average = getaverage(array);
    printf("%d\n", average);
    return 0;
}

int getaverage(int data[10])
{
    int i, average = 0;
    for (i = 0; i < 10; i++) {
        average += data[i];
    }
    return average / 10;
}

このプログラムの実行結果は次のようになるかもしれません。

実行結果
202380394

引数の型は10要素になっているにもかかわらず、5個しか要素のない配列が渡せます。
その結果、関数側では強引に10個の要素を処理してしまい、おかしな結果となっています。

さらにおかしな現象として、関数内で配列の値を変えると呼び出し側まで変化します。
次のプログラムは、関数内で配列の値を変更してみる例です。

ソースコード
#include <stdio.h>

int getaverage(int data[10]);

int main(void)
{
    int average, array[10] = { 15, 78, 98, 15, 98, 85, 17, 35, 42, 15 };
    printf("array[3] = %d\n", array[3]);
    average = getaverage(array);
    printf("array[3] = %d\n", array[3]);
    printf("%d\n", average);
    return 0;
}

int getaverage(int data[10])
{
    int i, average = 0;
    for (i = 0; i < 10; i++) {
        average += data[i];
    }
    data[3] = 111; /* 引数の配列の値を変更 */
    return average / 10;
}

このプログラムの実行結果は次の通りになります。

実行結果
array[3] = 15
array[3] = 111
49

今までの引数では、呼び出された関数の中で引数の値を変更しても、
呼び出し元の引数の値が変わることはありませんでしたが、
配列では、なぜか、呼び出し先での変更が呼び出し元に影響しています。
この様なことは、値渡しではあり得ないことであるはずです。
アドレスを渡している
前項では、配列型の引数の持つ奇妙な性質を説明しました。
あのような現象は、配列が値渡しされていれば、あり得ないことです。
つまり、逆に言えば、配列自体は値渡しされていないのです。

しかし、実際に関数に配列を渡して平均値を計算することには成功しています。
つまり、なんらかの形で、配列が渡されていることは間違いのない事実です。

この点について検証するために、少し実験を行ってみましょう。
まず、前項で、配列型の引数では要素数は無視されていることはわかりました。
それならばいっそ、要素数を指定しなければどうなるでしょうか?
つまり、関数を、次のように変更してみるのです。

ソースコード
int getaverage(int data[])

この様に書き換えて実行しても、何の問題もなく動作します。
しかも、プロトタイプ宣言で要素数を指定せずに、
実際の関数の宣言では要素数をつけた場合ですら、何のエラーもでません。
このことからも、要素数は完全に無視されていることがわかります。

しかし、要素数を無視して、どうやって配列の値を渡しているのでしょうか?
普通に考えれば、配列を渡す場合、要素数の数だけ値をコピーすることになります。
しかし、要素数を無視している以上、そのような方法は使えません。

ここで、もう1つの実験を行ってみたいと思います。
前項で、呼び出された関数で配列の値を変更すると、呼び出し元まで変化しましたが、
この現象は、ポインタ型の引数を使った時と良く似ています。
つまり、配列ではなくアドレスを渡しているのではないかとも考えられます。
試しに、関数を、次のように変更してみました。

ソースコード
int getaverage(int *data);

驚くべきことに、これでも、何の問題もなく動作しました。
これで、先ほどまでの奇妙な現象の原因がすべて判明しました。
つまり、配列を渡していたのではなく、配列の先頭のアドレスを渡していたのです。

配列の先頭のアドレスを渡すだけならば、要素数などまったく関係ありません。
また、呼び出された関数での配列は、呼び出し元と同じメモリ領域を指すことになるので、
呼び出された関数で配列の値を変えると、呼び出し元も変更されるのは当然です。

このことについてまとめると、まず、次の3つは同じ意味の仮引数宣言です。
ただし、この3つが同じ意味になるのは関数の仮引数宣言の場合のみです。

ソースコード
int getaverage(int data[10]);
int getaverage(int data[]);
int getaverage(int* data);

そして、関数の中では、dataはいずれもポインタ型の変数です。
そして、呼び出し先と呼び出し元ではまったく同じメモリ領域の配列を使うことになります。

どれにするか
この3つが同じ意味だと、どれを使って良いのか迷う人もいるかもしれませんが、
筆者としては、2番目のように要素数を省略した形を使うことを勧めます。
なぜなら、3番目の宣言は、普通のポインタ型と紛らわしいからです。
2番目の宣言であれば、配列を受け取ることが明示的にわかります。
1番目の宣言は、C言語に慣れた人たちには幼稚な宣言に見えます。



本サイトについて

苦しんで覚えるC言語(苦C)は
C言語入門サイトの決定版です。
C言語の基本機能を体系立てて解説しており、
市販書籍と同等以上の完成度です。

第0部:プログラム概要編
  1. プログラムとは何か?
2章:プログラムの書き方
  1. 書き方のルール
  2. 書き方の慣習
  3. 練習問題2
3章:画面への表示
  1. 文字列の表示
  2. 改行文字
  3. 練習問題3
6章:キーボードからの入力
  1. 入力用の関数
  2. 入力の恐怖
  3. 練習問題6
9章:回数が決まっている繰り返し
  1. 繰り返しを行う文
  2. ループ動作の仕組み
  3. 練習問題9
10章:回数がわからない繰り返し
  1. 回数不明ループ
  2. 入力チェック
  3. 練習問題10
13章:複数の変数を一括して扱う
  1. 複数の変数をまとめて扱う
  2. 配列の使い方
  3. 練習問題13
20章:複数のソースファイル
  1. 最小限の分割
  2. 分割の定石
  3. 練習問題20

コメント
COMMENT

💬 コメント投稿欄を開く