2.3 ポインタ演算  1999.03.24(初版)


2.3.1 ポインタの算術演算  ポインタは加算と減算の算術演算ができます。行える演算は、ポインタに対する整数 の足し算と引き算(+、+=、-、-=)、インクリメント(++)、デクリメント(--)、及びポ インタ同士の引き算だけです。また、この様な演算結果は同一型のポインタに代入でき ます。これらの演算はアドレスについての演算ですから、数の算術演算とは違います。  例えば整数型が4バイトの処理系で、int型へのポインタに1を加える演算は、アド レスを4バイト増やします。つまり現在のアドレスの値にポインタが指すデータ型のサ イズだけアドレス値を加算することを意味します。 int x; // int型が4バイトのマシン int *ptr = &x; ptr + 7; は、ptr のアドレス値に (int型のサイズ)×7 = sizeof(*ptr)×7 = 4×7 =28バイト を加える演算を行います。つまりポインタが指している変数のサイズがかけ算されます。 次のプログラムを見てください。
// lst02_01.cpp
// ポインタ演算:実験のためだけの無意味なプログラム   
#include <iostream.h>

int main()
{
    int x = 5;
    int *ptr = &x;
    cout << "ptrの値: " << ptr << endl;

    ptr++; // ptr = ptr+1; と同じ
    cout << "ptrの値: " << ptr << endl;

    ptr+=2; // ptr = ptr+2; と同じ
    cout << "ptrの値: " << ptr << endl;

    ptr--; // ptr = ptr-1; と同じ
    cout << "ptrの値: " << ptr << endl;

    return 0;
}

ソースファイルlst02_02.cpp
[実行結果]
ptrの値: 0x0063FDF0                   
ptrの値: 0x0063FDF4
ptrの値: 0x0063FDFC
ptrの値: 0x0063FDF8
最初のポインタ演算、インクリメント ptr++ は ptr = ptr + 1 と同じです。ptrが指 しているアドレスに1を加えて、その結果をptrに代入しています。アドレスの値は4バ イト増えます。  次の演算、ptr+=2は ptr = ptr + 2 と同じです。アドレス値はさらに4×2バイト 増加します。最後のデクリメント ptr-- で4バイト減ります。この様にポインタ演算は、 メモリ内のアドレスをデータ型のサイズ単位で移動します。  しかし、この実験プログラムは無意味なプログラムです。ここで意味のある唯一のデー タはアドレス0x0063FDF0に格納された変数xの値5だけです。それ以外の場所をポインタ が指しても意味を持ちません。

2.3.2 ポインタ演算は配列と共に  つまり、ポインタ演算は配列を指すときに意味を持ちます。なぜなら、配列は一連のデ ータを連続に格納するものだからです。ポインタ演算を使って配列の要素から別の要素へ ポインタを移動させることができます。次のプログラムは、ポインタを使った配列要素の 間接参照を示しています。
// lst02_02.cpp
// ポインタ演算:配列を指すポインタ
#include <iostream.h>

int main()
{
    int a[5] = { 11, 12, 13, 14, 15 };
    cout << "配列要素(1):配列を直接参照" << endl;
    int i;
    for(i=0; i<5; i++)
        cout << "a[" << i << "] = " << a[i] << endl;

    int *ptr = &a[0];

    cout << endl << "配列要素(2):間接参照" << endl;
    for(i=0; i<5; i++)
        cout << "*(ptr+" << i << ") = " << *(ptr+i) << endl;

    cout << endl << "配列要素(3):間接参照" << endl;
    for(i=0; i<5; i++)
        cout << "*ptr++  = " << *ptr++ << endl;

    return 0;
}
[実行結果]                ソースファイルlst02_02.cpp
配列要素(1):配列を直接参照           
a[0] = 11 
a[1] = 12 
a[2] = 13 
a[3] = 14 
a[4] = 15 

配列要素(2):間接参照                 
*(ptr+0) = 11 
*(ptr+1) = 12 
*(ptr+2) = 13 
*(ptr+3) = 14 
*(ptr+4) = 15 

配列要素(3):間接参照                  
*ptr++  = 11 
*ptr++  = 12 
*ptr++  = 13 
*ptr++  = 14 
*ptr++  = 15 
これはまず、ポインタを配列の先頭(要素)のアドレス &a[0] で初期化しています。 int *ptr = &a[0]; 配列の先頭位置を起点にして、配列要素を添字で a[i]と直接参照するのと同じようにポ インタで参照しています。*( ptr + i ) は a[i] と同じです。  ポインタを使った間接参照の2つの方法の違いに注目しましょう。はじめのバージョン ではポインタ ptr が指している変数は常にa[0]で変わりません。後のバージョンではポ インタが指す位置はa[0]からa[4]へと移動しています。 *( ptr + i ) の括弧()の中は演算結果です。ptrのアドレス値にインデクスiを足し算した結果に間 接参照演算*を行っています。つまり *(計算結果のアドレス)を標準出力しているの であって、ptrの値は変わっていません。一方、 *ptr++ は、ポインタがインクリメントされています。ptrのアドレスを1つ進めて(4バイト増 やして)います。 【演習問題2.3】ポインタを配列の最後の要素を指すようにして、それを起点にして各要  素を後ろから前へ順に間接参照するプログラムを書きなさい。

2.3.3 ポインタのインクリメント(デクリメント)  次はインクリメント演算子++をポインタの前に置いた場合と後に置いた場合の違いを 示しています。
// lst02_03.cpp
// ポインタ演算の実験:ポインタのインクリメント
#include <iostream.h>

int main()
{
    int a[5] = { 11, 12, 13, 14, 15};
    int *ptr = &a[0];

    int i;
    for(i=0; i<5; i++)
        cout << "*ptr++  = " << *ptr++ << endl;
    cout << endl;

    ptr = &a[0]; // 再び配列の先頭にセット
    for(i=0; i<5; i++)
        cout << "*++ptr  = " << *++ptr << endl;

    return 0;
}

ソースファイル lst02_03.cpp
[実行結果]
*ptr++  = 11                           
 *ptr++  = 12 
 *ptr++  = 13 
 *ptr++  = 14 
 *ptr++  = 15 

 *++ptr  = 12 
 *++ptr  = 13 
 *++ptr  = 14 
 *++ptr  = 15 
 *++ptr  = 6553144 
この違いは、インクリメント演算子の前置と後置の性質だけによるものです。整数変数を 例にすると、 int x,y; x = 5; y = x++; // yは5 y = ++x; // yは7 は、1つの文で2つのことを行っています。 (1)xの値に1を加える(インクリメント)。 (2)yにxの値を代入する。 この2つは、どちらが先に実行されるかによってyの値は結果が変わります。C/C++は次の 規則をとります。 x++ は、(2)が先で(1)が後(代入が先だからyは5) ++x は、(1)が先で(2)が後(インクリメントが先だからyは7)  ポインタ演算も同じです。*ptr++では、先に現在のptrに対する *(ptr) が評価され、 その後でインクリメントされます。*++ptrでは、先にインクリメントされてから、間接参 照*されます。  前置インクリメント演算の最後の値は、ポインタが存在しない変数a[5]を参照していま す。ポインタがこの様な意味のないアドレスを参照すると不定の値(ゴミ:gerbage)を 返します。 【演習問題2.4】ポインタのデクリメント、*ptr--,*--ptrをテストする同様の実験プロ  グラム書いて実行してみなさい。 【演習問題2.5】2つのポインタを使って、それぞれが別の配列要素を指すようにして、  ポインタ同士の引き算を出力するプログラムを書きなさい。実行結果からポインタ同  士の引き算の意味を考えなさい。

2.3.4 ポインタの学び方  メモリアドレスを操作することは、プログラマがコンパイラの領域に立ち入る行為です。 ポインタ演算はデータが存在しないメモリ領域や、別のデータ領域に容易にアクセスでき ます。この様な不正なメモリアクセスはエラーを起こします。それは実行時に起こり(コ ンパイラは助けてくれない)、その起こり方はシステムに依存します。  プログラムがすぐに暴走しシステムエラーのために停止するかもしれません。もっと深 刻なのは、普通に動いてしまうプログラムです。例えば、ここでの「実験のための無意味 なプログラム」は確かに動きます。動かないプログラムは、エラーがあることを教えてく れるだけ救われます。  しかし動くプログラムは、正常に見えながらどこかのデータを破壊し、どこかで誤った データを吐き出し、そして最後に突然あなたの心臓を襲うかもしれません。  ポインタは危険な面を持ちますが、C/C++のもっとも強力な機能でもあります。C/C++で は文字列(文字の並び)を扱う組み込みのデータ型を持ちません。文字列はヌル文字('\0') で終わる文字の配列として表現されます。これは驚きです。配列は添字 [i]を使って、要 素単位で読み書きできますが、配列全体の操作、たとえば配列を配列に直接コピーするこ とはできません(文字列の場合は文字列操作ライブラリを使った関数呼び出しで行います)。 しかし、ポインタを使うことで文字操作や文字列操作の柔軟なプログラミングが可能です。  ポインタを使いこなすにはそのための充分なトレーニングが必要です。そしてつぎ込ん だ労力は報われます。ポインタはプログラミング言語によっては使用を禁止されています (プログラマに見えないところで使っている)が、C/C++ではプログラミングを楽しむ自 由を提供します。とはいえ何を学ぶかが重要です。  ここで述べるささやかなアドバイスは、読みやすくて、わかりやすいプログラミングを 目指すべきであるということです。  ポインタの効用として簡潔で短いコードを書くことができ、プログラムの効率を良くす るといわれます。この主張は正しくもあり誤りでもあります。&と*さらに++といった ポインタ演算がいくつも組み合わさった暗号のようなプログラムは混乱を招きます。しか もそのようなプログラムが速いとは限りません。最近のコンパイラは多くの場合、ポイン タを使わずとも同様のバイナリコードに最適化してくれます。  さらに現在の標準C++(ANSI/ISO C++)では、ポインタに代わる機能をC++標準ラ イブラリで提供しています。例えば文字列操作にはポインタchar*の代わりにstringと いったライブラリ型があります。  プログラミング言語は発展途上にあります。将来はいまとは違ったプログラミングスタ イルが生まれている可能性もあります。そのときに(現在でもそうですが)存在するのは 過去のスタイルで書かれた(有用性を秘めた)膨大なプログラムコードです。そのときに 古文書のような暗号のコードよりも、わかりやすいプログラムの方が遙かに有用です。  ポインタを学ぶときは、その基本概念を徹底して理解することに努めましょう。普通概 念はいくつかの概念同士が結びつき合って意味を形成します。ポインタの場合は、配列 (文字列を含む)や関数などと関係付けられて機能が生かされます。その関係をよく見る ことです。  C++の設計者であるBjarne Stroustrupが著書"The C++ Programing Language (Third Edition)"で述べているように、 "The most important thing to do when learning C++ is to focus on concepts and not get lost in language-technical details." (意識を言語技術の詳細にとらわれずに概念に集中させること) ということです。

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

Copyright(c) 1999 Yamada,K