ポインタと参照型

概要
ポインタ とは,ある変数が保存されているメモリ領域のアドレスを保持する型だ.動的に割り当てたメモリ領域にアクセスする場合など,さまざまな場面で利用される. 参照型 も,実態はメモリアドレスだが,ポインタよりも制約が強く,安全に扱える.
目次

アドレスの話

計算機で行われる「計算」の基本は,ビット列(010001...)の演算である.計算機で処理する実数値や画像は,ビット列に符号化されて,データとして保持されている.ビット列(データ)はメモリ上に記憶され,プログラムがこれを参照したり書き換えたりする.したがって,プログラムのもっとも低レベルの処理は,メモリ上のデータを参照する(アクセスする)ことと,その変更である.このため,参照したり書き換えたりしたいデータがメモリ上のどこに配置されているかが明確である必要があり,そのためにメモリ空間には アドレス が設けられている(現実世界のアドレス=住所と同じ役割).例えば,1024番地に保存されているデータと1028番地に保存されているデータを足して,その結果を1064番地に保存する,という処理を行ったりする.

C/C++で行う処理は一般的にもっと高レベルなものであり,コンパイラがこの処理を計算機に理解できる低レベル処理(機械語で記述される)に変換する.しかし,C/C++においても,アドレスを直接扱った方がよいケースが存在する.本節の目的は,アドレスを扱うことのメリットと,その方法を学ぶことである.

アドレスを記憶するための変数型が,次節で扱う ポインタ である.つまりポインタにはアドレスを代入することができる.計算機の低レベル処理で言うアドレスと異なる点は, ポインタが指すアドレスに記憶されているデータの型(整数型,構造体,など)も扱う 点である.

アドレスと演算子

変数の構成要素は,

  • 名前(識別子)
  • アドレス

の4つである.例えば double 型の変数 x があったとすると,この識別子と型はもちろん x と double で,アドレスはコンパイラが決め,値は実行時に動的に変化する. x の値を知りたかったら,

  cout << x << endl;

と書けば,標準出力に x の値が表示されることは既に学んだ.

アドレスを知りたいときは,次のように書く.

  cout << &x << endl;

違いは, x の前についた & である.これは演算子の一種で,ある変数(型は何でもよい)のアドレスを取得するために使う. & を アドレス演算子 と呼ぶ.

次のコードを実行してみよう.

アドレス演算子:

  #include <iostream>

  using namespace std;

  int main(int argc, char**argv)

  {

    double x = 2.0;

    cout << x << endl;

    cout << &x << endl;

    return 0;

  }

結果::

 2
 0xbf811a38

2行目の結果は,処理系(コンパイラ)によって異なるが,いずれにしてもアドレスを表している.アドレスは16進数で出力されていて,8桁なので,32ビットの整数であることがわかる.これは環境が32ビットOSだからである.

アドレス演算子 & は,「変数からアドレスを取得」する.逆に,「アドレスから変数の値を取得」する演算子もある.それは, 間接演算子 * である.注意しなければならないのは,乗算演算子 * とは異なるものだと言うことである.乗算演算子 * は, x*2.0 のように前後に変数や定数が位置するのに対して,間接演算子 * は *x のように,演算子の後ろにしか変数や定数が位置しない.このようなことから,乗算演算子 * は 二項演算子 の一種,間接演算子 * は 単項演算子 の一種である.

double 型の変数 x のアドレスは &x であるから,このアドレスから変数の値を取得するには, *(&x) のようにすればよい.この意味は,変数 x の実体があるアドレス(そこには変数 x の値が記憶されている)を取得し,さらにそのアドレスから値を読み取る,という操作である. つまり *(&x) と x はまったく等価である .以下のプログラムで確かめよう.

アドレス演算子と間接演算子:

  #include <iostream>

  using namespace std;

  int main(int argc, char**argv)

  {

    double x = 2.0;

    cout << x << endl;

    cout << &x << endl;

    cout << *(&x) << endl;

    return 0;

  }

結果

 2
 0xbfbb65d8
 2

x の出力と *(&x) の出力はまったく同じになっている.

まとめ [#b39bb39e] 変数の値は,メモリ上のある場所に記憶されていて,その場所はアドレスで特定される.変数が記憶されているアドレスを得るにはアドレス演算子 & を,アドレスから値を取得するには間接演算子 * を使う.

ポインタ

ポインタとは, 型つきの ,アドレスを記憶する変数型である.「型つき」と言っているのは,前述したように, ポインタが指すアドレスに記憶されているデータの型(整数型,構造体,など)もポインタは含んでいるから である.アドレスがメモリ上の「位置」を表すことは何度も述べた.しかし,そこからデータを取り出す(参照する)とき, データのサイズ が必要なことには,まだ言及していなかった.メモリは ...0101101101... のように,ビットの列である.アドレスはそのうちのひとつの位置を示すだけで,データを取り出すためには, いくつのビットを取り出せばよいか ,つまりデータのサイズが必要であることは容易にわかるだろう.このデータのサイズとは,例えばデータが int 型なら(処理系に依るが)32ビット, double 型なら(同)64ビット,ユーザ定義の構造体 struct hoehoe 型なら sizeof(hoehoe) バイトである.したがって型の情報はデータのサイズを決定するので, アドレスが指し示しているデータの型は何か という情報が,データにアクセスするために必要なのである.このため, ポインタとは,型つきの,アドレスを記憶する変数型 なのである.

ポインタ変数の定義

例として, double 型の変数 x を指すポインタを考えよう.このポインタは,「 double 型のデータを指し示すアドレスを記憶できる変数型」であり,次のように定義する

  double *p;

もっと一般的には,ある変数型 X 型(構造体でもよい)の変数の定義が,

  X x;

だったとして,この型のデータを指し示すポインタの定義は,

  X *x;

となる.以下では,「 X 型のデータを指し示すポインタ」を X 型のポインタ と言うことにする.

ポインタの基礎的な使い方

double 型のポインタ p が, double 型の変数 x を指し示すようにするには, x のアドレス &x を, p に代入すればよい.ポインタ p が指し示しているデータを参照するには,間接演算子を用いて, *p とすればよい. *p は,式中では x と同じように振る舞う.次のサンプルプログラムで確認してみよう.

ポインタの基礎的な使い方:

  #include <iostream>

  using namespace std;

  int main(int argc, char**argv)

  {

    double x = 2.0;

    double *p=NULL;

    p = &x

    cout << "x=\t" << x << endl;

    cout << "&x=\t" << &x << endl;

    cout << "p=\t" << p << endl;

    cout << "*p=\t" << *p << endl;

    (*p) = 3.0;

    cout << "after (*p) = 3.0;" << endl;

    cout << "x=\t" << x << endl;

    cout << "&x=\t" << &x << endl;

    cout << "p=\t" << p << endl;

    cout << "*p=\t" << *p << endl;

    return 0;

  }

結果::

 x=      2
 &x=     0xbfc13638
 p=      0xbfc13638
 *p=     2
 after (*p) = 3.0;
 x=      3
 &x=     0xbfc13638
 p=      0xbfc13638
 *p=     3

double型のポインタ p に double 型の変数 x のアドレス &x が代入されているとき, *p と x は同じ値を示す.さらに, *p をひとつの変数だと思って,これに値を代入すると, x の値も変わる [1]

上のプログラムでは,ポインタp は NULL で初期化されているが,この NULL とは,「メモリ上のどの領域もポイントしていない」ことを表すポインタ(無効なポインタ)である [2] . NULL ポインタを参照しようとすると,セグメントフォルトエラーで強制終了するため, ポインタを NULL で初期化しておくと,発見しにくいバグをある程度防ぐことができる

  • [1] C/C++では, *p のような式の結果にも代入ができる.このような値(代入ができる値)を 左辺値 と呼ぶ.逆に, x+1 のように結果が一時変数となる式の結果には代入できず, 右辺値 と呼ばれる.
  • [2] NULL は「ナル」と呼ぶ.日本では習慣的に ヌル と呼ぶ人もいるが,JIS規格には ナル と規定されている.

構造体の要素へアクセスする方法

ある構造体 A が要素 x を持つとき,この要素を参照するには A.x と書けばよかった.いま, B が A を指すポインタ(つまり B==&A )であるとすると, B に間接演算子を付けた *B は構造体 A と同じ値を取る.だから, A.x を参照するには (*B).x とすればよい.さらに,C/C++では, B->x という書き方も規定している.これは (*B).x と完全に等価である.

ポインタの使用例

この節では,ポインタの使用例を2つ示す.ただし,これらの使い方を実践で行うこともあるが,多くのケースではポインタの代わりに参照型を使う ことも覚えておいて欲しい (cf. 参照型).

`ポインタの使用例1: 関数呼び出し時のオーバーヘッド軽減`

通常,C++では,関数に値を渡すとき, 値渡し で渡される.例えば,

  double func1 (double x)

  {

    return x*10.0;

  }

という関数を func1(a) のように呼び出したとき,変数 a の値は,関数 func1 用のメモリ領域に コピー されてから,関数 func1 が実行される.これに対して,ポインタを使った関数

  double func2 (double *x)

  {

    return (*x)*10.0;

  }

を func2(&a) のように呼び出したとき,変数 a の アドレス が,関数 func2 用のメモリ領域にコピーされてから,関数 func2 が実行される.両者の違いは,(32ビットマシン,gcc/g++なら)変数 a の値は64ビットなのに対して,変数 a の アドレス は32ビットのメモリ領域しか必要としない.つまり, コピーされるメモリの大きさが減る .この例だと2倍の差で,あまりありがたみを感じないかも知れない.しかし,次の例をみてみよう.

LIST-A:

  #include <iostream>

  using namespace std;

  struct TValue

  {

    double a,b,c,d,e,f,g,h;

  };

  double func1 (TValue v)

  {

    return v.a+2.0+v.b;

  }

  int main (int argc, char**argv)

  {

    cout << "sizeof(TValue)=" << sizeof(TValue) << endl;

    cout << "sizeof(TValue*)=" << sizeof(TValue*) << endl;

    TValue v = {0.8,0.7,0.6,0.5,0.4,0.3,0.2,0.1};

    int i;

    for (i=0;i<10000000;++i)

      v.a = func1(v);

    cout << v.a << endl;

  }

LIST-B:

  #include <iostream>

  using namespace std;

  struct TValue

  {

    double a,b,c,d,e,f,g,h;

  };

  double func2 (const TValue *v)

  {

    return v->a+2.0+v->b;

  }

  int main (int argc, char**argv)

  {

    cout << "sizeof(TValue)=" << sizeof(TValue) << endl;

    cout << "sizeof(TValue*)=" << sizeof(TValue*) << endl;

    TValue v = {0.8,0.7,0.6,0.5,0.4,0.3,0.2,0.1};

    int i;

    for (i=0;i<10000000;++i)

      v.a = func2(&v);

    cout << v.a << endl;

  }

これら二つのプログラムの実行時間を比較[3] すると, LIST-A は約0.760秒なのに対して, LIST-B は約0.064秒しか掛かっていない. 10分の1以下の実行時間 である.この理由は,変数 v の値は512ビット(64バイト)なのに対して,変数 v の アドレス は32ビット(4バイト)のメモリ領域しか必要としないから,関数呼び出し時のメモリのコピー量が大幅に減っているためである.

このように,ポインタを使って引数を渡すことで,関数呼び出し時のオーバーヘッドを軽減できる.

  • [3]time ./a.out のようにして実行時間を計測できる.

`ポインタの使用例2: 関数で値の変更`

変数のポインタ(アドレス)を関数に引数として渡してやることで,その変数を関数で変更できるようになる.次のプログラムで確かめよう.

引数に代入:

  #include <iostream>

  using namespace std;

  void func1 (double x)

  {

    x = 10.0;

  }

  void func2 (double *x)

  {

    (*x) = 10.0;

  }

  int main (int argc, char**argv)

  {

    double x(2.0);

    cout << "x= " << x << endl;

    func1 (x);

    cout << "x= " << x << endl;

    func2 (&x);

    cout << "x= " << x << endl;

  }

結果

 x= 2
 x= 2
 x= 10

この結果から, func1(x) を実行しても x の値は変わらないが, func2(&x) を実行すると x の値は変化する.これは, func1(x) では値を関数に渡すとき, x がコピーされるのに対して, func2(&x) ではポインタ(アドレス)を渡すから,そのポインタが指しているアドレスは, main の x のアドレスだからである.これは, 関数で複数の値を変更したいときあるいは複数の返り値を持たせたいとき に使える.

関数ポインタ

汎用性のあるプログラムを書こうとすると,一部の処理を任意に変えられるようにしたい場合が出てくる.つまり,関数(処理)を引数として関数に渡したり,関数(処理)をクラスのメンバ変数として持つようなことが必要となる場合がある.関数ポインタを使えば,そのような要求を満たすことができる.

ノイマン型コンピュータでは(現在ほぼすべてのPCがノイマン型)プログラムもデータとして扱われる.すなわち変数と同じように,メモリ空間にプログラムがおかれ,実行される.よってプログラムの構成要素である関数もメモリ空間上に配置されており, 関数のアドレスを取得し,変数に代入 することができる.関数のアドレスの型もポインタ型の一種であり,特に関数ポインタと呼ぶこともある.

関数は,

 戻り値の型  関数名 (引数1の型, 引数2の型, ...);

のように宣言される.この関数へのポインタ変数の宣言は,

 戻り値の型  (*変数名) (引数1の型, 引数2の型, ...);

のように書く.また,ある関数のアドレスを得るには,変数の場合と同様に関数名の前にアンパサンド (&) をつけて取得する.具体例を見てみよう

  #include <iostream>

[[#include <cfloat>  // for DBL]]MAX

  using namespace std;

[[//! array[0,...,array]]size-1] を合計する関数

[[double sum]]up (const double *array, int array_size)

  {

    double res(0.0);

[[const double *end]]of_array (array+array_size);

[[// array[array]]size-1] の次のアドレスへのポインタ

[[for (; array!=end]]of_array; ++array)

      res+= (*array);  // array のすべての要素を加算

    return res;

  }

[[//! array[0,...,array]]size-1] の最大値を返す関数

[[double max]]elem (const double *array, int array_size)

  {

[[double res(-DBL]]MAX);

[[const double *end]]of_array (array+array_size);

[[// array[array]]size-1] の次のアドレスへのポインタ

[[for (; array!=end]]of_array; ++array)

      if ((*array)>res)  res= *array;

    return res;

  }

  int main(int argc, char**argv)

  {

    double vals[]= {1.0, 3.0, 5.0, 1.0};

[[double (*func) (const double *array, int array]]size);

      // 戻り値: double, 引数リスト: (const double*, int) の関数へのポインタ

[[func=]]up;    // 関数 sum_up へのポインタを func に代入する

    cout<< "func(vals,4)= " << func(vals,4) << endl;

[[func= &max]]elem;  // 関数 max_elem へのポインタを func に代入する

    cout<< "func(vals,4)= " << func(vals,4) << endl;

    return 0;

  }

結果

 func(vals,4)= 10
 func(vals,4)= 5

このプログラムではふたつの関数sumup と max_elem を定義し(詳細は気にしなくてよい),それぞれへのポインタを main 関数で変数 func に代入している.sumup が代入されているときは func はsumup として振る舞い, max_elem が代入されているときは max_elem として振る舞う.このように, 戻り値と引数リストが同じであれば,同じ関数ポインタに代入できる のである.

関数ポインタは,コールバック関数 を定義する場合など,関数やクラスのメソッドにおける処理の一部を(その関数やクラスの使用者が)任意に変更できるようなインターフェイスを提供する場合に使用される.例えばスレッドを作るときに用いられる. pthread create 関数のプロトタイプは次のようになっている:

int  pthread create (pthread_t  *  thread, pthread_attr_t * attr,

void *(*start routine)(void *), void * arg);

この関数の3番目の引数は

void *(*start routine)(void *)

となっており, void 型の戻り値, (void*) 型の引数リストを持つ関数へのポインタ型であることがわかる.ここにスレッド化する関数(任意)を代入するようになっている.

ポインタと配列

この節はやや高度な話題を扱うので,読み飛ばしても構わない.

いくつかのケースでは,ポインタと配列は同じように振る舞う.ここでは共通点と相違点について述べる.なお,「ポインタと配列は同じもの」と解説されることもあるが,これは言い過ぎで,本当はポインタ型と配列型は別物である.

ポインタと整数の加算

どんな型のポインタであっても,そのポインタと整数の和を計算することができる.ある型 X のポインタ p と整数 a の加算結果は, X 型のポインタとなる.つまり, p+a は X 型を指し示すアドレスを表している.注意が必要なのは,整数 a の単位がバイトやビットではなく, sizeof(X) バイトであるという点だ.例えば p が double 型(8バイト)のポインタなら, p+3 はアドレス p に 8\times{}3=24 バイト足したアドレスを指し, p が int 型(4バイト)のポインタなら, p+3 はアドレス p に 4\times{}3=12 バイト足したアドレスを指す.もちろん, p+3 を double 型のポインタに代入することもできる.

配列は,式中ではポインタとして振る舞う

ポインタと整数の加算は,演算子 [] を理解するために必要である.ある型 X の配列 a に対して式中で a[10] などと書くと,それは配列 a の11番目の要素を表すことは既に学習している.実際には,この式はコンパイラによって *(a+10) に置き換えられる.式中で(演算子 [] などを伴わずに)配列 a を使用した場合, 配列 a は a が格納されているメモリ領域の先頭アドレスを指す X 型のポインタ として振る舞う.したがって a+10 は,配列 a の先頭アドレスに 10*sizeof(X) バイト加えたアドレス(つまり配列 a の,11番目の要素の先頭アドレス)を指すことになり,そのポインタを間接演算子 * で参照する *(a+10) と,11番目の要素が得られる.以下のプログラムを確認しよう.

配列のポインタ的振る舞い:

  #include <iostream>

  using namespace std;

  int main(int argc, char**argv)

  {

    int a[10];

    int i;

    int *p(NULL);

    cout << "a[0..9]= ";

    for (i=0; i<10; ++i)

    {

      a[i] = i+1;

      cout << "  " << a[i];

    }

    cout << endl;

    cout << "a[5]=\t" << a[5] << endl;

    cout << "&(a[5])=" << &(a[5]) << endl;

    cout << "a=\t" << a << endl;

    cout << "a+5=\t" << a+5 << endl;

    cout << "*(a+5)=\t" << *(a+5) << endl;

    cout << "(int)(a+5)-(int)a=" << (int)(a+5)-(int)a << endl;

    p = a+5;  /* int型のポインタ p に a+5 を代入 */

    cout << "p=\t" << p << endl;

    cout << "*p=\t" << *p << endl;

    *p = 100;

    cout << "after *p = 100;" << endl;

    cout << "*p=\t" << *p << endl;

    cout << "a[5]=\t" << a[5] << endl;

    cout << "*(a+5)=\t" << *(a+5) << endl;

    return 0;

  }

結果::

 a[0..9]=   1  2  3  4  5  6  7  8  9  10
 a[5]=   6
 &(a[5])=0xbfe4d858
 a=      0xbfe4d844
 a+5=    0xbfe4d858
 *(a+5)= 6
 (int)(a+5)-(int)a=20
 p=      0xbfe4d858
 *p=     6
 after *p = 100;
 *p=     100
 a[5]=   100
 *(a+5)= 100

(int)(a+5)-(int)a の結果が 20==5*sizeof(int) であることも確認しておこう.

この配列がポインタのように振る舞う性質を利用して,高速な配列のサーチを行うことができる.ポインタに対しては,インクリメント演算子 ++ も有効である.

ただし, 例外 もある.ポインタに対して sizeof 演算子を使うと(32ビットマシンなら)4バイト(32ビット)であるが,配列に対して sizeof 演算子を使うと,その配列がメモリを占有するサイズ(バイト)となる.

sizeof array:

  #include <iostream>

  using namespace std;

  int main(int argc, char**argv)

  {

    int a[10];

    int *p(NULL);

    p = a;

    cout << "sizeof(a)=\t" << sizeof(a) << endl;

    cout << "sizeof(p)=\t" << sizeof(p) << endl;

    return 0;

  }

結果

 sizeof(a)=      40
 sizeof(p)=      4

文字列とポインタと配列

文字列の宣言には2通りある.

  char str1[] = "ding ming";

  const char *str2  = "ding ming";

定義から明らかなように, str1 は 文字列を格納した配列 で, str2 は 文字列定数へのポインタ である.したがって, str1 は(関数で定義された場合)関数に割り当てられたメモリ領域に記憶され, 書き換え可能 であるのに対し, str2 は(多くのコンパイラの場合)定数領域に記憶され,書き換え不可能である.だから,上の例では const char * とすることによって, str2 に代入すると コンパイルエラーが発生 するようにしている.もし, const をつけずに文字列を宣言して変更した場合, gcc/g++ では Segmentation fault エラーとなる.

文字列とポインタと配列

  #include <iostream>

  using namespace std;

  int main(int argc, char**argv)

  {

    char str1[] = "ding ming";

    char *str2  = "ding ming";

    cout << "str1=\t" << str1 << endl;

    cout << "str2=\t" << str2 << endl;

    cout << "sizeof(str1)=\t" << sizeof(str1) << endl;

    cout << "sizeof(str2)=\t" << sizeof(str2) << endl;

    str1[4] = '+';

    cout << "str1=\t" << str1 << endl;

    str2[4] = '+';  // ここがセグメントエラー

    cout << "str2=\t" << str2 << endl;

    return 0;

  }

結果

 str1=   ding ming
 str2=   ding ming
 sizeof(str1)=   10
 sizeof(str2)=   4
 str1=   ding+ming
 セグメントエラー (coreを出力しました)

coreファイルを gdb で解析してみると,どこでエラーが起きたかわかる [4]

なお,最新の g++ (g++ (GCC) 4.2) の場合,

  char *str2  = "ding ming";

に対して

 warning: deprecated conversion from string constant to 'char*'

文字列定数を,書き換え可能な文字列へのポインタに代入しようとしているという警告が出力される.この警告にしたがって

  const char *str2  = "ding ming";

と変更すれば, str2[4] = '+'; で コンパイルエラー が発生し,セグメントエラーといった実行時エラーを防ぐことができる.

  • [4]コンパイラオプションに -g (デバッグ情報を生成)をつける.csh/tcshでは, limit coredumpsize unlimited で core ファイルを生成できるようになる. gdb a.out core で,デバッガが起動する.

配列を関数に渡すときはポインタになる

配列として定義された変数を関数に渡すとき,その 配列型の変数は必ずポインタとして扱われる .逆に言うと, 関数の引数に配列を持たせることはできない

配列を関数に渡す

  #include <iostream>

  using namespace std;

  void func1 (int a[3])

  {

    cout << "sizeof(a)= " << sizeof(a) << endl;

    a[1] = 10;

  }

  void func2 (int a[])

  {

    cout << "sizeof(a)= " << sizeof(a) << endl;

    a[1] = 20;

  }

  void func3 (int *a)

  {

    cout << "sizeof(a)= " << sizeof(a) << endl;

    a[1] = 30;

  }

  int main(int argc, char**argv)

  {

    int A[] = {1,2,3};

    cout << "A= {"<<A[0]<<", "<<A[1]<<", "<<A[2]<<"}"<< endl;

    cout << "sizeof(A)= " << sizeof(A) << endl;

    func1(A);

    cout << "A= {"<<A[0]<<", "<<A[1]<<", "<<A[2]<<"}"<< endl;

    func2(A);

    cout << "A= {"<<A[0]<<", "<<A[1]<<", "<<A[2]<<"}"<< endl;

    func3(A);

    cout << "A= {"<<A[0]<<", "<<A[1]<<", "<<A[2]<<"}"<< endl;

  }

結果

 A= {1, 2, 3}
 sizeof(A)= 12
 sizeof(a)= 4
 A= {1, 10, 3}
 sizeof(a)= 4
 A= {1, 20, 3}
 sizeof(a)= 4
 A= {1, 30, 3}

この例では3種類の引数の宣言をテストしているが, いずれも等価 であり, a は int 型のポインタである.このような変数の渡し方を値渡しに対して 参照渡し と呼ぶ.なお,どうしても配列を値渡ししたい場合は(非効率なので止めた方がよいが),配列を含む構造体を定義して,その構造体を値渡しすればよい.

多次元配列を関数に渡す

多次元配列を関数に渡す場合もやはりポインタになる.このとき,引数内の多次元配列の宣言で, 一番内側の要素数は省略可能だが,それ以外の要素数は省略できない ことに注意しておく必要がある.

多次元配列を関数に渡す:

  void func1 (int a[2][3])

  {

    a[1][2]++;

  }

  void func2 (int a[][3])

  {

    a[1][2]++;

  }

  void func3 (int a[][])

  {

    a[1][2]++;

  }

  void func4 (int a[2][])

  {

    a[1][2]++;

  }

func1 や func2 の引数宣言は問題ないが, func3 や func4 の引数宣言はエラー ( error: declaration of 'a' as multidimensional array must have bounds for all dimensions except the first ) になる.これは, 要素数がわからないと,各要素のアドレスが計算できないから である.

そのほかの注意点

file1:

  extern int *a;

file2:

  int a[100];

これらのファイルをリンクしたとき,file1の a がfile2の a を参照できると思ってはならない. 宣言が異なる ため,別の変数として扱われる.

まとめ: ポインタのメリット

本節で述べたポインタの用途

  • 関数呼び出し時のオーバーヘッド軽減
  • 関数で値の変更
  • 関数ポインタ

以外にも,

  • リストやグラフのようなデータ構造を作るときに使う
  • オブジェクトの間でデータを共有するときに使う
  • 動的メモリを確保するときに使う

などがある.「動的メモリ」はほかのページで解説している.そのほかのトピックスについては,各自,調べてみよう.

参照型

ポインタはアドレスを直接扱うため,幅広い用途を持つが,予想外のバグを引き起こす,デバッグでバグを発見しにくいなどのデメリットもある.関数呼び出し時のオーバーヘッドの軽減や,関数で引数の値を変更するといった用途をより安全に実現するために,C++では 参照型 がポインタ型に対して用意されている.

参照型の用法

「参照型」は,変数を別の名前で参照する型,つまり変数の別名を定義するための型である.すべての変数は「型」を持つから,参照型はポインタ型と同様に,「○型を参照する変数型」というふうに定義される.以下のプログラムで double の参照型の定義と振る舞いを見てみよう:

  double a;       // 普通の double 型変数 a の定義

  double &x (a);  // a を参照する double の参照型変数 x の定義

  a= 3.14;        // a に代入すると,...

  cout<<"a= "<<a<<endl;  // もちろん a= 3.14 が表示される

  cout<<"x= "<<x<<endl;  // x は a の「別名」なので, x= 3.14 が表示される

  x= 6.28;

  cout<<"x= "<<x<<endl;  // もちろん x= 6.28

  cout<<"a= "<<a<<endl;  // やはり a= 3.14

このように,参照型の定義は参照したい変数 (a) の型 (int) にアンパサンド (&) をつけて行う.このとき,参照したい変数によって初期化する.初期化の方法には 型 変数名(初期値) と 型 変数名=初期値 の2通りがあるから,参照型変数の定義も,以下の2通りがある

 参照したい変数の型  & 変数名 (参照したい変数);
 参照したい変数の型  & 変数名= 参照したい変数;

いずれの場合も,定義した「変数名」は,参照したい変数に別名をつけたかのように振る舞う.

定数の参照方法は?

以下のようにして定数を参照しようとすると,コンパイルエラーとなる

  double &x (3.14);

gcc の場合,

 invalid initialization of non-const reference of
 type 'double&' from a temporary of type 'double'

のようなエラーを出力する.これは,定数 3.14 が const double 型なのに double & で参照しようとしているから発生するエラーであり,次のように, const double & で参照すれば問題ない

  const double &y (3.14);

参照型の使用例

1: 関数呼び出し時のオーバーヘッド軽減

関数内で引数の値を変更しない場合は,引数の型を「constつきの参照型」にしよう. これによって, ポインタの使用例1: 関数呼び出し時のオーバーヘッド軽減 で述べた効果をより簡単に実現できる.

例えば,

  double func1 (double x)

  {

    return x*10.0;

  }

という関数は,

  double func2 (const double &x)

  {

    return x*10.0;

  }

のように,変更しよう. const と & をつけるだけだ.参照型は,実は裏でポインタの受渡しをしているから,ポインタを使った場合と同じように,関数呼び出し時のオーバーヘッドを軽減することができる.また,関数のソースコードを変更することなく,値渡しの関数から参照渡しの関数に変更することができ,手軽である.

2: 関数で値の変更

ポインタの使用例2:関数で値の変更 と同じことが,参照型を使っても実現できる.具体的には,以下のようにする:

  #include <iostream>

  using namespace std;

  void func1 (double x)  // x を値渡し

  {

    x = 10.0;

  }

  void func2 (double &x) // x を参照渡し

  {

    x = 10.0;

  }

  int main (int argc, char**argv)

  {

    double x(2.0);

    cout << "x= " << x << endl;

    func1 (x);

    cout << "x= " << x << endl;

    func2 (x);

    cout << "x= " << x << endl;

  }

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2015-05-21 (木) 14:53:46 (1155d)