C++言語解説:1-5-1.ポインタとは
2002-06-30

[概要] ポインタはC/C++で最も重要な機能であると同時に、最も問題を起こしやすい側面
    を持っています。しかし、動的メモリ割り当てやオブジェクト指向の高度な機能
    はポインタによって実現されています。ここではポインタの性質や必要性を考え
    ていきます。

[構成]・アドレスについて考える
     * 「変数を使う」とは
     * 「変数を定義する」とは
   ・ポインタ
     * 場所情報だけもらう
     * ポインタ演算


アドレスについて考える

 「変数を使う」とは
 
  ・ポインタの前に変数について考えてみたいと思います。「変数を使う」とはどんな
   ことなのか。

  
  ・とりあえず変数を定義してみます。
     int a;
   この変数に対して、いつでも値を入れる事ができます。
     a = 3;
   そして、その値をいつでも取り出す事ができます。
     cout << a;
  
  ・さてどうしてこういった操作ができるのでしょうか。実際にデータがメモリ上に置
   かれている様をイメージしてみましょう。

   ┌───────────────────────────────────┐
     . . 0 0 . . . . . 0 0 0 0 0 0 0 1 1 . . . . .
       ← 上位24bit → ← 下位 8bit →
       ←-------- 整数32bit --------→
   └───────────────────────────────────┘
   数値として「3」を保有しているので、最下位2bitに「1」が立っています。
  
  ・このメモリ領域に「7」を入れたくなったとします。つまり最下位3bitを「1 1 1」
   にしたいという意味です。
  
  ・どんな情報が必要か考えてみましょう。これは書き込む場所がわからないとどうし
   ようもないので
     先頭アドレス  : 最上位bitのメモリアドレス
     最下位アドレス : 最下位bitのメモリアドレス
   情報が必要です。ただしint型データは大きさが決まっている(例えば32bit)ので
     最下位アドレス → 先頭アドレス + サイズ(int型なら32bit)
   とも表現できます。
  
  ・よって「書き込み場所」を特定させるのに必要な情報は
   ┌───────────────────────────────────┐
      先頭アドレス : (最上位bitのメモリアドレス)
      サイズ    : (int型の例で32bit)
   └───────────────────────────────────┘
   の2種になります。
  
  ・これでコンピュータに対して
   ┌───────────────────────────────────┐
     先頭アドレス(最上位bitのメモリアドレス)から
      ↓
     指定したサイズ(int型 例えば32bit)に
      ↓
     整数7(0.....0111)を入れて下さい
   └───────────────────────────────────┘
   と操作できるわけです。ようやく変数に数値を代入することができました。


 「変数を定義する」とは

  ・変数を扱うのに必要な情報がわかったところで、「変数を定義する」ことについて
   考えてみます。
  
  ・場所を指定するの必要な情報は、先頭アドレスとサイズです。しかし、データを置
   くとなると「実際の置き場所」が必要です。他の変数やプロセスに邪魔されない、
   その変数だけが使える領域の確保
。いわゆる「露払い」作業。
  
  ・つまり「変数を定義する」とは、定義した変数を通じて「読み書きできる領域を
   確保」するということなのです。そして先頭アドレスは「確保した領域の場所」と
   して決まります。
  
  ・よって「int a;」の実際の操作とは
   ┌───────────────────────────────────┐
     変数aが使うメモリ領域を確保する
     確保メモリ領域の大きさは、変数のaの型による (この場合int型で32bit)
     メモリ領域確保後、先頭アドレスをaに渡す
   └───────────────────────────────────┘
   となります。強引は承知の上で言い切ってしまうと「aは先頭アドレスそのもの」
   のです。
   ┌───────────────────────────────────┐
    a ← 0x000010 : aはアドレス[0x000010]を保有する

    データは[0x000010]を先頭に(例えば)32bitの範囲でデータが置かれている
    
     アドレス データ
     0x000010 - 0
     0x000011 - 0
     0x000100 - 0
      :     :
     0x011111 - 0
     0x100000 - 1
     0x100001 - 1
     0x100010 - 1
   └───────────────────────────────────┘


ポインタ

 場所情報だけもらう
 
  ・ここまで「変数を定義する」とは「領域を確保し、そのアドレスを管理する」こと
   と同義である事を学習してきました。
  
  ・ここである考えが浮かぶ方もいるでしょう。
    「場所と大きさの情報さえもらえば、データにアクセスできる...?」
   その通りです。その機能を実現するのがポインタなのです。
  
  ・ポインタ(pointer)は、メモリアドレスを保持する変数です。場所の情報をもらうの
   ですから当然ですね。それと大きさの情報も必要です。従ってポインタ変数の定義は
   以下の形式(*1)になります。
   ┌───────────────────────────────────┐
     型 *変数名;
     (例)int *ip_a;
        又は
       int* ip_a; //(*1)を読んで下さい
   └───────────────────────────────────┘
   この例では「int型の大きさが確保されたメモリ領域の先頭アドレスを受け取るポイ
   ンタ変数ip_aを定義」しています。

  ・この段階では、アドレスを受け取る入れ物を準備しただけです。対象の変数やオブ
   ジェクトからアドレスを受け取るには &演算子 を使用します。
   ┌───────────────────────────────────┐
    int i_a;
    int* ip_a; //この段階では、アドレスの入れ物だけ
    
    ip_a = &i_a; //アドレスを受け取った!!
   └───────────────────────────────────┘
  
  ・ポインタはアドレスを受け取って、初めて「場所」を指し示すようになります。ポ
   インタの指し示す場所のデータを操作するには、*演算子 を利用します。
   ┌───────────────────────────────────┐
    int i_a;
    int* ip_a;
    
    ip_a = &i_a;
    *ip_a = 7; //ip_aの示すアドレスのデータ領域に7を代入
   └───────────────────────────────────┘
   この操作は、ポインタを通じて変数aのデータを操作したことになります。あくまで
   用語の話ですが、このような動作を間接参照(indirection)と呼びます。
  
  ・それではポインタと&/*演算子に慣れるため、List1のコードを試してみて下さい。
  
   [List1.ポインタに慣れる]
   ┌───────────────────────────────────┐
    #include <iostream> //<iostream.h>
    using namespace std;
    
    int main()
    {
      int i_a = 10;
      int* ip_a; //int型ポインタ定義
      
      ip_a = &i_a; //アドレス渡し
      
      cout << "i_aのアドレス   : " << &i_a << endl;
      cout << "ip_aの中身    : " << ip_a << endl;
      cout << "i_aのデータ    : " << i_a << endl;
      cout << "ip_aポイントデータ: " << *ip_a << endl;
      
      i_a = 20; //変数i_aにデータ代入
      cout << "i_aのデータ    : " << i_a << endl;
      cout << "ip_aポイントデータ: " << *ip_a << endl;
      
      *ip_a = 30; //ポインタを介してデータ代入
      cout << "i_aのデータ    : " << i_a << endl;
      cout << "ip_aポイントデータ: " << *ip_a << endl;
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [実行結果]
     i_aのアドレス   : 0012FF88
     ip_aの中身    : 0012FF88
     i_aのデータ    : 10
     ip_aポイントデータ: 10
     i_aのデータ    : 20
     ip_aポイントデータ: 20
     i_aのデータ    : 30
     ip_aポイントデータ: 30
   └───────────────────────────────────┘


 ポインタ演算
 
  ・ポインタに適用できる演算子は
     ++, --, +, -
   の4種類です。ポインタのデータはメモリアドレスなので「アドレスに対する演算」
   になるのですが、このとき重要なのはポインタの基本型です。
  
  ・例えばインクリメント演算の場合どうなるか。実際にやってみましょう。
  
   [List2.ポインタの演算]
   ┌───────────────────────────────────┐
    #include <iostream> //<iostream.h>
    using namespace std;
    
    int main()
    {
      int i_a = 10;
      int* ip_a;
      
      double d_b = 1.0;
      double* dp_b;
      
      ip_a = &i_a;
      dp_b = &d_b;
      
      cout << "演算前のアドレス値ip_a " << ip_a << endl;
      cout << "演算前のアドレス値dp_b " << dp_b << endl;
      
      ip_a++; //ポインタをインクリメント
      dp_b++;
      
      cout << "演算後のアドレス値ip_a " << ip_a << endl;
      cout << "演算後のアドレス値dp_b " << dp_b << endl;
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [実行結果]
     演算前のアドレス値ip_a 0012FF88
     演算前のアドレス値dp_b 0012FF80
     演算後のアドレス値ip_a 0012FF8C
     演算後のアドレス値dp_b 0012FF88
   └───────────────────────────────────┘

  ・List2はポインタの指すアドレスを表示しています。単位は1byteです。「インクリ
   メント」という演算の後、int型ポインタでは4byte, double型ポインタでは8byte増
   えていることがわかります。
  
  ・つまりポインタ演算の単位は基本型のメモリサイズなのです。int型ポインタip_aに
   おいて「ip_a + 2」や「ip_a - 2」という演算は、ip_aの指すアドレスから、基本
   型のメモリサイズを単位にして演算が実行されます。
   ┌───────────────────────────────────┐
     (基本型がintならば)
     ┬───┬───┬───┬───┬───┬───┬
     │4byte │4byte │4byte │4byte │4byte │4byte │
     ┴───┴───┴───┴───┴───┴───┴
         ↑       ↑       ↑
        ip_a-2      ip_a      ip_a+2
   └───────────────────────────────────┘

  ・ならば、ポインタ同士の加減算はできるのか。答えは以下です。
     ポインタ同士の加算:×
     ポインタ同士の減算:○
  
  ・ポインタの持つ値はメモリアドレスです。よってポインタ同士を加算するとアドレ
   スがオーバーフローするかもしれないので、ポインタ同士の加算は禁止になります。
   実質的に、ポインタに関する演算はオフセット演算のみ許されることを意味してい
   ます。
   
  ・減算の場合、2つのアドレスの間のオフセットが基本型サイズを単位として表されま
   す。
このとき結果はint型で戻ります。具体例をList3に示します。

    [List3.ポインタ同士の減算]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    int main()
    {
      int i_a = 8;
      int* ip_a1;
      int* ip_a2;
      int offset;
      
      ip_a1 = &i_a;
      ip_a2 = &i_a;
      
      ip_a2 = ip_a2 + 2; //int型×2のオフセット
      
      offset = ip_a2 - ip_a1; //int型ポインタ減算
      
      cout << "アドレスip_a1 : " << ip_a1 << endl;
      cout << "アドレスip_a2 : " << ip_a2 << endl;
      cout << "ip_a2 - ip_a1 : " << offset << endl;
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [実行結果]
     アドレスip_a1 : 0012FF88
     アドレスip_a2 : 0012FF90
     ip_a2 - ip_a1 : 2
   └───────────────────────────────────┘


(*1)ポインタの定義には2種類の記述が許されています。
    int *ip_a; 又は int* ip_a;
  ただし、後者を使用する場合、注意の必要な点があります。例えば
    int* ip_a, ip_b;
  と記述したとき、実際の解釈は
    int *ip_a;
    int ip_b;
  となってしまいます。しかしこの表記は原著のStrup氏が好んで使用しており、実は筆
  者も後者表記を使っています。理由は「ポインタ型変数を定義している」イメージを
  持ちやすいからです。実際のところは好みで2分されていますが、本テキストでは
    int* ip_a;
  タイプの表記が多用されるので、混乱しないようにして下さい。

[Revision Table]
 |Revision |Date    |Comments
 |----------|-----------|-----------------------------------------------------
 |1.00   |2002-05-26 |初版
 |1.01   |2002-06-02 |リンク追加、誤字修正
 |1.02   |2002-06-30 |語句修正
[end]
Copyright(C) 2002 Altmo