第三回-01 : ポインタの使い方

このページでは、ポインタの基礎を取り扱う。ポインタは今後の授業でも 頻出するので、ここで理解してしまおう。


例1~どう使うか

まず、以下の例を見よう。samp クラスは第三回-02で取り扱うサンプルとほぼ同じである。

#include <iostream>
using namespace std;

class samp{
		int i;     // private なメンバ変数 (private キーワードの省略)
	public:
		samp(int n){i=n;}       // 引数つきのコンストラクタ
		void set_i(int n){i=n;} // i に値をセット
		int get_i(){return i;}  // i の値を取得
};

int main(){
  // 通常の変数の宣言
  int i=10;
  samp a(10);  // 初期値 10 でクラス samp のオブジェクト a を宣言
  int A[2] = {2, 4};  // 要素数 2 の配列

  // ポインタ変数の宣言
  int *p1;
  int *p2;
  samp *p3;
  int *p4;

  // ポインタの初期化
  p1 = &i;     // 変数からのアドレスの取り出し
  p2 = p1;     // p2 の指す先を p1 と同じにする
  p3 = &a;     // クラス変数からのアドレスの取り出し
  p4 = A;      // p4 = &A[0]; ともかける

  // ポインタの利用
  cout << "p1 の指す値は:" << *p1 << "\n";
  cout << "p2 の指す値は:" << *p2 << "\n";
  cout << "p3 の指すクラスのメンバ変数は:" << p3->get_i() << "\n";
  cout << "p4 の指す配列の要素は:" << p4[0] << "," << p4[1] << "\n";

  return 0;
}
実行結果:

p1 の指す値は:10
p2 の指す値は:10
p3 の指すクラスのメンバ変数は:10
p4 の指す配列の要素は:2,4


上記がポインタ利用の基本である。以下で図を用いて解説する。

まず、変数およびポインタ変数の宣言を行った直後のメモリ領域の模式図は下図の上のようになる。
(模式図なので、上下、左右などの関係に意味はない)

「int i=10」、「samp a(10)」、「A[2]={2,4}」については問題ないであろう。
一方、4 つのポインタの宣言であるが、ポインタとは変数を指し示すものであるから、図ではポインタ変数から矢印が延びている。
初期化しないポインタは、どこを指しているか不定である。そのため、矢印の先に「??」を描いた。この状態のままポインタを用いてはならない



次に、ポインタに対して「p1=&i」、「p2=p1」、「p3=&a」、「p4=A」でそれぞれ初期化を行ったときのメモリの模式図は、上図の下である。
初期化により、全てのポインタは、実体のある変数のどれかを指すようになった。
ここで重要な点は以下である。 このようにして指す先の確定したポインタを、今度は利用することを考える。
すなわち、ポインタ p1 から、p1 の指す先の値 (10) を取り出す、などである。
これに関し、main 関数から重要な点を抜き出すと以下のようにする。 まとめると、以下の表のようになる。「通常の変数」、「ポインタ変数」、「参照変数 (いずれ学ぶ)」に関してまとめてあるが,
まずは「通常の変数」、「ポインタ変数」の表を頭に叩きこんでおこう。

宣言
アドレス
通常の変数
int x
x
&x
ポインタ変数
int *x
*x
x
参照変数
int &x
x
&x


クラスの場合も同様であるが、こちらにはメンバー関数の呼び出しもあるのでそれも追加しておこう。
クラス名は sample 、メンバー関数名は func() とする。

宣言
実体
アドレス
メンバ関数呼び出し
通常の変数
sample o
o
&o
o.func()
ポインタ変数
sample *o
*o
o
o->func()
参照変数
sample &o
o
&o
o.func()

例2~なぜ使うのか

ポインタの使い方は上の例1で解説したが、ポインタがどのような場面で有効なのかは 上の例ではわかりにくいかもしれない。
ポインタが特に効果を発揮する場面 (の一つ) は、関数へ巨大なデータを受け渡す時であるので、以下でそれを見よう。

なお、この内容は今日の演習の残り(第三回-02第三回-03) で扱う内容にも関連するので、
良く分からなければ、先に進んでから後でこのページへ戻って来ても良いだろう。

では、例えば以下のような例を考える。

クラス samp は第三回-03の一つ目のプログラムに登場するものだが、
文字列を格納するための s[80] という配列をメンバ変数としてもつ。

全体としては、samp クラスのオブジェクト a および char 型の配列 A[80] をそれぞれ C 言語的な関数に渡し、
その内部で配列 (文字列) の内容を表示させている。
やはり例としてのわざとらしい使い方ではあるが、プログラミングを行っていると、このように関数に大きなデータを渡したいことがしばしばある。

#include <iostream>
#include <cstring>
using namespace std;

// クラス宣言
class samp{
  char s[80];// 80 文字以内の文字列をメンバ変数として持つ
public:
  // 以下のように簡単なメンバ関数であればクラス定義内で定義できる
  void show(){ cout << s << "\n";}    // クラス宣言部でも関数を定義できる
  void set(char *str){ strcpy(s,str); } // 80 文字以上の文字列をセットしないこと
};

// C 言語的な関数を 2 つ定義
void show_array(char *c){
  cout <<  c << "\n";
}

void show_class(samp *o){
  o->show();
}

int main(){

  char A[80]="This is a test of array.";
  samp a;

  a.set("This is a test of class.");

  show_array(A);
  show_class(&a);

  return 0;

}


やはり、以下のような模式図で考えよう。
上の例のように模式図であり、上下左右などの関係にはあまり意味はない。

まず、一つ理解しておいて欲しい重要な点は、「関数ごとに使用されるメモリ領域は異なる」という点である。
そうすると、「main 関数にある配列 A[80] やクラスオブジェクト a をどのように関数 show_array や show_class に渡すか」が問題となる。



図の左側のように、大きなデータ (a や A[80]) を関数にそのまま渡すことも (やろうと思えば) できる。
しかし、そうすると 80 個要素のある配列を一つ一つコピーすることが必要となる。これは非常に無駄が多い。

このように、大きなデータを関数にデータを渡す際には、関数にデータへのポインタを渡すようにすることが多い。
それが上のプログラムでやっていることであり、図の右側で描かれていることである。
こうすると、80 個の要素のコピーは必要なくなるため、ラフに言えば関数へのデータの引渡しが 80 倍高速になる。

実際の記述では、
「void show_array(char *c)」や「void show_class(samp *o)」などの定義に対し、
「show_array(A)」、「show_class(&a)」などとして関数を呼び出している。
何故これで図の右のような動作が可能なのか、例1の解説も参考にしながら理解せよ。



←第二回演習第三回-02 : 値渡し・アドレス引数・参照引数→

第三回トップページへ

クラスから入る C++ へ戻る