第1章 第6節 そして残ったのは…ポインタと配列


← 前に戻る | 次に進む →

それでは、CおよびC++では避けては通れない、ポインタと配列についての解説をはじめます。

よくCやC++は難しいと言われる原因として、このポインタがあります。だから、この第1章でも一番最後の第6節まで触れずに来ました。しかし、先ほども述べたようにこれは避けて通れない道なのです。一回でわからなかったら、繰り返し勉強することが肝心です。それも、自分でプログラムを組みながらです。

でも、あまり怖がらなくて大丈夫です。最初のうちはよく理解できないとしても、徐々に覚えれば済むことなんですから。そして、このポインタと配列を理解したとき、非常に大きな力を手に入れたことになるのです(^-^)。

では、下に示すプログラムを見てください。

List 1.15 配列の例

#include <iostream>
using namespace std;

int main()
{
    int a[5];
    int i;

    for (i = 0; i < 5; i++) a[i] = i;
    for (i = 0; i < 5; i++) cout  << a[i] << endl;

    return 0;
}


0
1
2
3
4

これは、int型の配列としてaを宣言し、for文で0から4までの数値をaの配列に代入するプログラムです。そして、代入されたことを確かめるために画面に配列aの中身を出力しています。

配列を宣言するときは、int a[5]のように宣言し、これは、int型の変数a[0]からa[4]までの5つを使用するということです。宣言ではint a[5]のように書きますが、a[5]自身には代入できないので気を付けて下さい。

どうですか。ここまでならそんなに難しくないでしょう。では、次のプログラムを見てください。

List 1.16 配列をポインタに代入

#include <iostream>
using namespace std;

int main()
{
    int a[5];
    int *p;
    int i;

    p = a;
    for (i = 0; i < 5; i++) a[i] = i;
    for (i = 0; i < 5; i++) cout  << p[i] << endl;

    return 0;
}


0
1
2
3
4

これは、List 1.15のプログラムで使用した配列を、int型のポインタで宣言した変数pに代入したものです。p = aとすることで、配列の先頭の位置(アドレス)を変数pに入れることができます。そう、ポインタ変数には、コンピュータの内部で使用される場所の位置(アドレス)が入るのです。

はじめてポインタを勉強している方は、この説明では良くわからないでしょうから、もう少し詳しく述べていきます。

まず、List 1.15で出てきた、配列a、つまり、a[0]、a[1]、…、a[4]にはint型整数値が入っています。では、括弧[]を付けないaには何が入っているのでしょうか。実は、このaには配列の先頭のアドレスが入っているのです(アドレス演算子&を用いて、&a[0]としても同じ)。つまり、配列a[0]〜a[4]が確保されている場所(メモリ)の先頭の位置です。例えば、a[2]はaの位置(アドレス)から2番目の場所に入っている数値を指しているのです。

これでわかってきたでしょうか。つまり、List 1.16で出てきた、p = aで、aのアドレスをpに代入したことになるのです。よって、p[2]のように使えば、pの位置(aの位置と一緒)から2番目の場所に入っている数値、つまり、a[2]を取り出せるのです。

では、ポインタで宣言した変数自身を配列のように使うためには、どのようにメモリを確保すればよいのでしょうか。下に示すプログラムを見てください。

List 1.17 ポインタ変数のメモリを確保

#include <iostream>
using namespace std;

int main()
{
    double *p1;
    int *p2;
    int i;

    p1 = new double;  // doubleで初期化.
    p2 = new int[5];  // int[5]で初期化.

    *p1 = 1.2345;
    cout << p1 << endl;   // アドレスを表示.
    cout << *p1 << endl;  // アドレスが示す中身を表示.

    for (i = 0; i < 5; i++) p2[i] = i;
    for (i = 0; i < 5; i++) cout  << p2[i] << endl;

    delete p1;  // p1の後始末.
    delete [] p2;  // p2の後始末.

    return 0;
}


004205F0
1.2345
0
1
2
3
4

ここで示すように、ポインタ変数で使用するメモリを確保するには、new演算子を使い、変数を使い終わったらdelete演算子で後始末をつけます。delete演算子を用いる際、配列形式のポインタ変数の後始末にはdelete [] p2;のように[]が必要になりますので、注意してください。

ここで、p1の中身を表すのに、間接演算子*を用いています。p1は中身の位置(アドレス)を示しており、*p1でそのアドレスの中身を表すわけです。出力結果では、p1のアドレスは004205F0となっていますが、これは実行する環境によってそれぞれ異なるでしょう。

後始末について

newで確保した領域は、それに対応するdeleteで後始末するのが基本ですが、メモリの割り当てと解放について言えば、効率などの面で不満を持つ人もいるでしょう。

例えば、巨大なリスト構造で用いたメモリブロックをすべて解放するなると、それだけで大変な作業になったり、確実にプログラムが終わる直前で、メモリの解放処理に非常に時間がかかる場合などは、効率の面で(OSが自動的に解放してくれるから)メモリ解放をする必要がない場合があります。

しかし、newとdeleteは、ただ領域を確保したり、その領域をプログラムで再利用できるようにするだけではなく、別の章で詳しく説明するクラスのコンストラクタ(構築)およびデストラクタ(解体)を実行するために呼ぶのです。このことより、deleteを呼ばないとデストラクタが実行されず、不都合が出てきます。

つまり、この点で、C言語でメモリの割り当ておよび再利用に使われるmallocおよびfreeとは、大きく違うのです。少し突っ込んだ話になってしまいますが、mallocとfreeに対応するC++の機能として、operator newとoperator deleteがあります。これは、メモリを確保するだけでコンストラクタおよびデストラクタは呼びません。少々複雑な話になるので、ここではこれ以上の説明は省きます。

どうしても上述のようなメモリ割り当ての制御が必要になる場合は、operator newやoperator deleteの再定義などを行うことで、newとdeleteとを対応させたままメモリの制御ができるようになります。

また、他人がプログラムを見たときに、deleteが記述されていれば、そのオブジェクトが、それ以降もう使われないということがわかります。このように、newとdeleteを対応付けていれば、プログラムの可読性も上がるのです。

そして、なによりも、deleteが必要な場合にdeleteしていないときに起きるメモリリークが一番厄介です。特に、長時間起動させておくプログラムの場合、このメモリリークが起きると致命的なエラーに繋がります。すぐには気が付かないので、原因を特定するのも非常に苦労するでしょう。

よって、newを行ったらそれに対応するdeleteを実行してください。

ポインタは配列と違って、動的にメモリを確保できるので配列より柔軟性があります。つまり、配列はプログラムの実行中にメモリ領域を変更できませんが、ポインタは自由にメモリ領域を確保できるのです。

では、ポインタと配列は以下に示すプログラムのように、初めから領域を確保して使った場合、まったく同じものになるのでしょうか。

List 1.18 ポインタと配列

#include <iostream>
using namespace std;

int main()
{
    int a[5];
    int *p = new int[5];  // 宣言と初期化を一緒に行う.
    int i;

    for (i = 0; i < 5; i++) a[i] = p[i] = i;
    for (i = 0; i < 5; i++) cout  << a[i] << ", " << p[i] << endl;

    delete [] p;

    return 0;
}


0, 0
1, 1
2, 2
3, 3
4, 4

この答えは、YESでありNOである、ということになるでしょう。これでは余計わからないでしょうから、ちゃんと説明しましょう。それは、普通に配列として使う分には違いがないので、その点でYESですが、コンピュータ内のメモリが確保される場所が異なるという点で、NOとなるからです。

つまり、配列aでは、a自身の位置にそのままメモリが確保されているのに対して、ポインタpでは、p自身の位置は別の場所にあり、pに入っているアドレスで示される場所が確保されたメモリの先頭となるのです。図で示すと以下のようになります

  配列 a:             ポインタ p:
  a  =  a[0]          p --> p[0]
        a[1]                p[1]
        a[2]                p[2]
        a[3]                p[3]
        a[4]                p[4]

なんだ、それだったらポインタと配列はまったく同じように扱えるね、と思われる人もいるかもしれませんが、そうとは一概に言えないのです。特に、文字列を扱う場合には以下の例のように注意が必要です。

List 1.19 ポインタと配列の違い

#include <iostream>
using namespace std;

int main()
{
    char a[] = "Hello";
    char *p  = "Hello";

    cout << a << endl;
    cout << p << endl;

    a[4] = '!';
    cout << a << endl;

    p[4] = '!';
    cout << p << endl;

    return 0;
}


Hello
Hello
Hell!
(Hell!またはエラー表示)

プログラムの解説に入る前に、文字列を扱うルールの説明をします。この例では、a[]に"Hello"という文字列が代入されるわけですが、a[0]には'H'、a[1]には'e'、a[2]には'l'という具合に配列一つ一つに一文字ずつ入っていきます。そして、最後の文字'o'の後に、文字列の終わりを示す'¥0'がa[5]に入ります。また、配列の宣言と初期化を同時に行う場合は、配列の大きさを指定しなくてもコンパイラが自動的に設定してくれます。

このプログラムでは、配列aとポインタpとに代入された"Hello"という文字列の5番目の文字(a[4]およびp[4])を'o'から'!'に変えています。そして、変更した文字列を再び画面に表示するわけです。

では、早速コンパイルして実行してみましょう。あれれ? pを変更しようとするところでエラーが出て止まってしまいました(環境によってはエラーとならない場合もあります[注5])。これは、配列で扱っている文字列は、それが読み書きできるメモリに配置されるのに対し、ポインタの初期化に用いた文字列は読み書きのできない静的なメモリに配置されるからです。

駆け足で、ポインタと配列の説明を行いましたが、よくわからないうちはできるだけポインタは避けちゃいましょう(^-^;。最初に、ポインタは避けて通れないと述べたのに、なんだか矛盾しているようにも思えますが、とりあえず、C++に慣れるまではポインタを避けるのも一つの手です。Cでは、文字列などを扱うのにポインタと配列は絶対必要なものでしたが、幸いC++では前述のstringクラスがありますし、標準テンプレートライブラリ(STL)というポインタを極力使わずに済む便利なクラスライブラリも存在するのです。というわけで、避けられるポインタは避けようというのが、この節で述べた一番重要なことかもしれませんね(^-^;;。


← 前に戻る | 次に進む →


Copyright (C) Noriyuki Futatsugi/Foota Software, Japan.
All rights reserved.
d2VibWFzdGVyQGZ1dGF0c3VnaS5uZXQ=