2.4 配列とポインタ  1999.03.28(初版)

2.4.1 ポインタと配列の関係  ポインタと配列には密接な関係があります。しかしこれらは別物です。その違いは次の ように要約できます。  「ポインタ宣言はアドレスを記憶するための領域を確保するが、ポインタが指すデータ 領域は確保しない。一方、配列の宣言は要素数のデータ領域を確保する。このとき配列名 は、配列の先頭アドレスを表す定数ポインタとして扱われる」。  これはすでに学んだことの(忘れないための)確認事項と、「配列名は定数ポインタで ある」という新しい規則を述べています。まずは確認事項を整理します。次の配列の宣言、 int a[] = { 1, 2, 3, 4, 5 }; // 宣言時に初期化 は配列の初期化リストの要素の数だけのデータの領域がメモリ内につくられて、その値で 初期化されます。ところが、次のポインタ宣言はエラーとなります。 int *ptr = { 1, 2, 3, 4, 5 }; // エラー ポインタの宣言ではポインタ変数の値(アドレス)の領域しかつくられません。しかも初 期化はアドレスで行わなければならないので構文エラーになります。唯一の例外は文字列 の場合です。 char *ptr = "Que Sera, Sera"; // OK は有効です。この場合は文字列定数が確保され、ポインタはその先頭アドレスを指します。 ただし、この文字列は定数ですから書き換えることができません。  つまりポインタは次のように、ポインタが指すデータ領域が別に確保されているときに 意味を持ちます。 int a[10]; // 10個のint型の連続領域(配列)が確保される int *ptr = a; // ポインタはすでに存在する配列の先頭要素を指す。 は、10個の int型の連続領域が確保されます。そして(int *)型のポインタ変数の領 域が別の場所に確保され、ポインタの値は配列の先頭要素のアドレスになります(初期化)。 ここで配列名はその先頭要素を指す定数ポインタであるということを使っています。これ はこれまで用いてきた表現、 int *ptr = &a[0]; と同じことです。  「配列名はアドレス」であることは実に辻褄が合う話です。ポインタが配列の先頭を指 すときに、添字 i (i = 0〜9)に対して、a[i]と*(ptr + i)とは共に配列の(i+1)番目 の要素(内容)を意味しました。配列要素を添字で参照するa[i]とは先頭アドレス aから iだけ後ろのアドレスの内容ということで、(a + i)というアドレス計算を行っているこ とを意味します。従って、配列はポインタの様に書き表すことができます。つまり a[i] は *( a + i ) と書くことができます。  同じように、ポインタも配列のように扱えます。*( ptr + i ) は ptr[i] と書けます。 この場合、次の表現はすべて同じです( ptrは aを指しているとして)。 a[i], *( a + i), *( ptr + i ), ptr[i], [注]配列を直接参照する括弧[]を添字演算子といいます(つまりこれは演算子です)。 実際、コンパイラは配列要素 a[i]を *(a+i)とコンパイル時に書き換えます。すると配列 の添字番号が0(ゼロ)から始まるわけは理解できます。1番目の要素は、先頭アドレスa から0だけ後ろ(それそのもの)で、 a + 0のアドレスの内容は *( a + 0 )つまり a[0] です。サンプルを示します。
// lst02_04.cpp
// 配列とポインタの関係
#include <iostream.h>

int main()
{
    int a[] = {1, 2, 3, 4, 5};
    int *ptr = a; // &a[0] と同じ

    int i;
    cout << "---- 配列要素を添字演算で直接参照する ----" << endl << endl;
    for(i = 0; i < 5; i++)
        cout << "a[" << i << "] = " << a[i] << ", ";
    cout << endl << endl;

    cout << "---- 配列要素をポインタのようにして参照する ----" << endl << endl;
    for(i = 0; i < 5; i++)
        cout << "*(a+" << i << ") = " << *(a+i) << ", ";
    cout << endl << endl;

    cout << "---- ポインタで間接参照する ----" << endl << endl;
    for(i = 0; i < 5; i++)
        cout << "*(ptr+" << i << ") = " << *(ptr+i) << ", ";
    cout << endl << endl;

    cout << "---- ポインタで配列のようにして参照する ----" << endl << endl;
    for(i = 0; i < 5; i++)
        cout << "ptr[" << i << "] = " << ptr[i] << ", ";
    cout << endl;


    return 0;
}

ソースファイル lst02_04.cpp
[実行結果]
---- 配列要素を添字演算で直接参照する ---- 

 a[0] = 1, a[1] = 2, a[2] = 3, a[3] = 4, a[4] = 5, 

 ---- 配列要素をポインタのようにして参照する ---- 

 *(a+0) = 1, *(a+1) = 2, *(a+2) = 3, *(a+3) = 4, *(a+4) = 5, 

 ---- ポインタで間接参照する ---- 

 *(ptr+0) = 1, *(ptr+1) = 2, *(ptr+2) = 3, *(ptr+3) = 4, *(ptr+4) = 5,     

 ---- ポインタで配列のようにして参照する ---- 

 ptr[0] = 1, ptr[1] = 2, ptr[2] = 3, ptr[3] = 4, ptr[4] = 5, 
 話はまだ終わりません。配列名は式の中では定数ポインタなので、代入することは禁止さ れます。つまり、配列 aに対して次のようなことはできません。 a++, a--, ++a, --a, a+=2, // すべてエラー これらはすべて、定数アドレスを変更しようとしているので構文エラーになります。同じよ うにポインタを配列に代入することはできません。 a = ptr; // エラー:配列には代入できない このために配列を別の配列に代入することはできません。配列の代入は要素ごとに1つ1つ コピーすることになります。  この逆のことは、すでにご存じのようにできます。つまり、ポインタの初期化 int *ptr = a; // aは整数型配列 は、配列名(定数ポインタ)からポインタへ暗黙に型変換していると見なすとができます。 この性質については後で配列を関数に渡す議論で再び取り上げます。

2.4.2 補足:添字番号が1から始まる配列をつくる  ポインタは配列名と違って変数ですから、値を変えることができます。このことを使う と、FORTRANでの配列の様にn個の要素からなる配列の添字が1〜nとなるように表現で きます。
// lst02_05.cpp
// 配列の添字を1から始めるには
#include <iostream.h>

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

    for(int i = 1; i <= 5; i++)
        cout << "ptr[" << i << "] = " << ptr[i] << endl;          

    return 0;
}

ソースファイル lst02_05.cpp
[実行結果]
ptr[1] = 1                            
ptr[2] = 2 
ptr[3] = 3 
ptr[4] = 4 
ptr[5] = 5
つまりポインタが指すアドレスとして、配列の先頭の1つ前のアドレス&a[-1]を指定する ことで、1つずつ添字番号をずらすことができます。要素 a[-1]は実際には存在してませ んから読み書することはできません。もちろんこの様な書き方を奨めているのではありま せん。ある種の人たちはある目的で、この様な添字の範囲を使います。例えばC言語での 数値計算アルゴリズムを解説した、 W.H. Press, S. A. Teukolsky, W. T. Vetterling and B. P. Flannery: "NUMERICAL RECIPES in C", Cambridge Univ. Press 1988 ではこれを徹底して使っています(邦訳が技術評論社から出ています。英語版はオンライン で全文が読めます。)

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

Copyright(c) 1999 Yamada,K