2.7 配列を関数に渡す  (1999/04/06初版)

2.7.1 配列を関数に渡す方法  配列を引数に取る関数に配列を渡すとき、配列はポインタとして渡されます。この 規則から以下のことが導かれます。まず関数の呼び出しについて考えましょう。次の 配列 int a[] = { 1, 2, 3, 4, 5 }; を関数 func()に渡すとします。これが何を行う関数であるかその詳細に関わらず、 少なくとも次のような形式で呼び出せることが予想できます。 func( a, 5 ); 配列の名前は配列の先頭要素を指す定数ポインタを意味します。そして新たな規則「配 列はポインタとして関数に渡される」ことから、配列名で関数を呼び出すことができる はずです。  2番目の引数5とは配列の大きさ(次元)です。関数が受け取るのは配列の先頭アド レス値ですから、関数は元の配列の大きさについての情報を失っています。従って関数 の中で配列の要素を操作するには配列の大きさを関数に渡す必要があるでしょう。つま り関数のパラメータリストは、配列とその大きさの2つの引数を持たなければならない ことになります。  実際この推論は正しく、この場合の関数 func()の関数プロトタイプは次のように書 けて、 void func(int [], int); となります。ただしこの関数(まだ中身が決まっていない)は戻り値の無い void型だ としています。前のページで述べたように、関数プロトタイプがコンパイラに伝えるの は引数の数、型と順番ですから、コンパイラはプロトタイプの中の変数名を無視します。 ですから、パラメータリストで配列は、「 int a[] 」から名前を取った「 int [] 」 と書くことができます。もちろんわかりやすくするために名前を付けても構いません。  具体的な例で確かめましょう。次のプログラムは配列を関数に渡す方法と、ポインタ を使った値渡し(参照渡しのシミュレーション)の効果を示しています。
// lst02_10.cpp
// 配列を関数に渡す実験プログラム
#include <iostream.h>
#include <iomanip.h>

void func(int [], int); // 関数プロトタイプ宣言

int main()
{
    int a[] = { 1, 2, 3, 4, 5};
    int i;

    cout << "呼び出し側の配列 : a" << endl
         << "メモリ領域のサイズ: sizeof(a) = "
         << sizeof(a) << " バイト" << endl;

    cout << "配列の値:";
    for(i = 0; i < 5; i++)
        cout << setw(3) << a[i];

    func( a, 5 ); // 関数呼び出し

    cout << endl
         << "関数呼び出し後の配列の値:";
    for(i = 0; i < 5; i++)
        cout << setw(3) << a[i];
    cout << endl;

    return 0;
}

// 関数定義
void func(int x[], int size)
{
    int i;
    cout << endl << endl
         << "関数が受け取った配列: x" << endl
         << "メモリ領域のサイズ: sizeof(x) = "
         << sizeof(x) << " バイト" << endl;

    cout << "配列の値:";
    for(i = 0; i < size; i++)
        cout << setw(3) << x[i];

    cout << endl << endl
         << "*** 関数の中で各要素の値をインクリメント ***" << endl;
    for(i = 0; i < size; i++)
        x[i]++;

}

ソースファイル lst02_10.cpp
[実行結果]
呼び出し側の配列 : a 
 メモリ領域のサイズ: sizeof(a) = 20 バイト            
 配列の値:  1  2  3  4  5 

 関数が受け取った配列: x 
 メモリ領域のサイズ: sizeof(x) = 4 バイト 
 配列の値:  1  2  3  4  5 

 *** 関数の中で各要素の値をインクリメント *** 

 関数呼び出し後の配列の値:  2  3  4  5  6 
 まず、配列のメモリ領域のサイズに注目しましょう。呼び出し側で定義されている 配列aは、大きさ5の整数型配列です。配列のサイズは(int型)×5=4×5=20 バイトになります。  関数は配列を受け取るように宣言されています。実行結果を見ると、実際に関数に 渡されるのは配列そのもではなくてポインタであることが分かります。sizeof(x)は 4、つまり関数に渡されるとき配列はポインタに暗黙に型変換されます。渡されるの は配列の先頭アドレスで、要素自体はコピーされません。このために配列の大きさの 値も関数に渡してやる必要があります。  呼び出された関数は配列の先頭アドレスを受け取ることで、元の配列のメモリ内で の位置を知ることができます。そのため関数の中で配列要素の値を変更すると、元の 配列の値が変更されます。  以上のことから、配列を受け取る関数を次のようにポインタとして宣言できます。 void func(int *, int); // 関数プロトタイプ宣言 // 関数定義 void func(int *x, int size) { // 定義の中身 } つまり関数のパラメータリストでは、「int a[]」という宣言は「int *a」と同じ です。もちろんこのステートメントは他の文脈では正しくありません。 【演習問題 2.8】上のことを確かめなさい。つまり関数のパラメータリストで配列  宣言をポインタに書き換えて実行してみなさい。  (回答のプログラムはこちら ex02_08.cpp)  関数呼び出しで配列がポインタに変換されることは、ソフトウェアの効率の面で意 義があります。もし配列全体を直接渡すような言語仕様であったら、非常に大きなサ イズの配列の場合、関数呼び出しの度に各要素のコピーが作られて大変非効率です。 配列の先頭アドレスを渡すことでこの様なオーバーヘッドを避けています。

2.7.2 constポインタとはなんだろうか  呼び出された関数が配列を参照するだけで書き換える必要がない場合は、関数の引数 の宣言に const を付けましょう。つまり「int *」の代わりに「const int *」と します。次の例は、配列要素をディスプレイに表示する関数です。
// lst02_11.cpp
// 配列をプリントする関数
#include <iostream.h>

void print(const int *, int); // 関数プロトタイプ宣言

int main()
{
    int a[] = { 1, 2, 3, 4, 5};

    print( a, 5 ); // 関数呼び出し

    return 0;
}

// 配列要素をプリントする関数の定義
void print(const int *x, int size)
{
    for(int i = 0; i < size; i++)
        cout << "a[" << i << "] = " << x[i] << ", ";
    cout << endl;
}

ソースファイル lst02_11.cpp
[実行結果]
a[0] = 1, a[1] = 2, a[2] = 3, a[3] = 4, a[4] = 5,       
この場合配列要素を書き換える必要はありませんので、const を付けることで元の配 列が変更されることを禁止します。配列を渡す関数に対して、配列要素を「変更不可」 にすることで関数に制限を与えることができます。この様にすることで、ソフトウェア システムの安全性を高めます。もし関数内部で配列要素を変更すると、コンパイラはエ ラーを出して教えてくれます。 【演習問題 2.9】前の演習問題2.8のプログラムで、関数の定義を「const int *」に 書き換えてコンパイルしてみなさい。どのようなエラーメッセージが出るか確かめなさ い。  さて、関数の呼び出し時にポインタxは次のように配列の先頭アドレスで初期化され ます。 const int *x = a; ここで定数化を意味するconstはxではなくaを修飾します。定数扱いされて変更でき ないのは配列の方です。  しかしこれは少し混乱しそうな構文です。constはポインタのxに掛かりそうに見え ます。だけど、そうだとするとxは定数ポインタとなるわけで、これはxが別の変数を 指せない(xの中身のアドレス値を変更できない)ということになり、配列aの方は変 更可能になります。つまり意味が違います。  規則は次のようになっています。 「ポインタ宣言の先頭にconstが付くと、ポインタではなく、それが指す変数(オブジ ェクト)が定数になる。ポインタ自体を定数宣言したいときは、*ではなく、*const を使う」 ということです。つまり *constは、ただの *と同じく基本形の一部です。 const int *x = a; // aは定数 int *const x = a; // xは定数 この様に見た方が、constをどこに置くべきかと考えるよりわかりやすいと思います。

2.7.3 2次元配列を関数に渡す  2次元配列の場合を考えます。2次元配列は、配列を要素に持つ配列です。ですから これを指すポインタは「配列へのポインタ」でした。またこの場合は行と列の2つの大 きさを関数に渡す必要があります。つまり次のような2次元配列 int a[10][20]; を関数に渡すには, void func(int [][20], int, int);// 配列表現 または void func(int (*)[20], int, int);// ポインタ表現 という関数になります。列の大きさ20は必ず必要です。次に示すのは、行列要素をデ ィスプレイに表示するプログラムです。関数のパラメータリストをポインタ形式で宣言 しています。
// matrix05.cpp
//4×5行列を関数に渡す
#include <iostream.h>

const int ROW = 4; //行の次元
const int COL = 5; //列の次元

void print(const int (*)[COL], int, int);

int main()
{

    int mat[][COL] = { {11,12,13,14,15},
                       {21,22,23,24,25},
                       {31,32,33,34,35},
                       {41,42,43,44,45} };

    print(mat, ROW, COL);

    return 0;
}

void print(const int (*a)[COL], int row, int col)
{
    cout << "行列の表示\n";

    for(int i = 0; i < row; i++){
        for(int j = 0; j < col; j++)
            cout << " " << a[i][j];

        cout << endl;  //行の終わりに改行
    }
}

ソースファイル matrix05.cpp
[実行結果]
行列の表示
 11 12 13 14 15             
 21 22 23 24 25             
 31 32 33 34 35             
 41 42 43 44 45
関数print()は、行列を参照するだけなので、const int (*a)[COL]で宣言しています。 これで、関数の内部で元の2次元配列を書き換えてしまう心配はありません。 (*) 古いコンパイラにはこの様に const を使えないものがあります。その場合は、関数 のパラメータリストの宣言からキーワード const を削除してください。 [注]関数プロトタイプ宣言と、関数定義のヘッダ部の宣言は同じにしましょう。一方を配 列宣言で他方をポインタで宣言するのは危険です。 【演習問題 2.10】関数のパラメータリストを配列で宣言したプログラムを書きなさい。          (回答プログラムはこちら ex02_10.cpp.

| 目次 | 前のページ | 次のページ |

Copyright(c) 1999 Yamada,K