ポインタと参照

参照演算子(Reference Operators)

 まずは,C++ではどのように実行ファイルが作られるのかを知る必要がある.エディタで書いたプログラム(ソースプログラム(source program))をコンピュータの理解できる機械語に翻訳する(コンパイル(compile)).このときできたプログラムをオブジェクト(object)という.このオブジェクトモジュールを連結させることをリンク(link)といい,連結させるプログラムをリンカ(linker)という.こうして実行可能形式ファイル(ロードモジュール(load module))ができる.ロードモジュールはファイルとしてハードディスクなどに格納されているで主記憶装置(メモリ)に展開しなければならない.この動作をロード(load)といい,OSによって行われる.ロードされたロードモジュールは,その先頭から実行を開始される.一般に,メモリは連続して並んだバイトの列で,ロードモジュールはメモリ上に連続的にロードされる.このバイトの列には,それぞれアドレス(address),もしくは番地と呼ばれる連続した番号がふってある.例えば,256MBのRAMは268,435,456バイトの列を保持している.このアドレスを目印にしてプログラムの実行を制御したり,データの読み込みや書き出しを行う.多くの場合,ロードモジュールは変数や定数などのデータを格納しているデータ部とプログラムのアルゴリズムを記述したコード部から構成されている.

さて,今まで型宣言とか定義という言葉を使ってきたが,定義された変数はメモリ上のアドレスに割り当てられる.  変数を宣言すると3つの基本的な属性,つまり,変数名,型,そして記憶番地,が変数に割り当てられる. 例えば,
int n;
の宣言により,変数名 n,型 int,そしてメモリ内のどこかのアドレスが割り当てられる.もし,このアドレスが0x0064fdf0だとすると,変数nを次の図のようにして理解することができる. これより,変数nを箱を用いて表現することがよくある.

\includegraphics[width=4.6cm]{CPPPIC/fig7-1.eps} \includegraphics[width=4.4cm]{CPPPIC/fig7-3-1.eps}

多くのコンピュータでは,変数の型int はメモリを4バイト使用する.よって,変数nは右図のように,0x0064fdf0から初めて0x0064fdf3までを保持する.ここで,オブジェクトのアドレスはオブジェクトが格納されているメモリの最初のアドレスであることに注意する.

もし,変数が
int n = 44;
というように,初期化されたときには,変数nは次のようにして理解することができる.

\includegraphics[width=4.4cm]{CPPPIC/fig7-3-2.eps} \includegraphics[width=4.2cm]{CPPPIC/fig7-2-1.eps}

アドレス演算子(Address Operator)

int x;
と宣言したとき,変数xが配置されるメモリはコンパイラが勝手に決めてしまう.このアドレスを知るには,参照演算子(またはアドレス演算子とよばれる)である&(ampersand)を用いて
&x
とすることで,求めることができる.

例題 7..1   アドレス
変数xの配置アドレスを表示するプログラムを作成せよ.

解答

#include <iostream>
using namespace std;
int main()
{
    int x;
    cout << "Address = " << &x;
}

実行結果

\begin{figure}% latex2html id marker 2251
\centering
\includegraphics[width=7.2cm]{CPPPIC/reidai7-1.eps}
\end{figure}

参照(References)

参照とはある変数の別名を持った変数であり,次の構文で宣言される.

データ型& 参照する名前 = 変数名;
と記述する.例えば,

int n = 44;
int& rn = n;
と宣言すると,rnはnの参照である.素人の言葉で説明すると,参照とは芸名みたいなもので, 名前は違うけれど,名前が指している人は同じ人ということである.

\begin{figure}\centering
\includegraphics[width=5.6cm]{CPPPIC/reference.eps}
\end{figure}

例題 7..2   参照の使い方
このプログラムでは
rnnの参照として宣言する.実行結果を考えよ.

解答

#include <iostream>
using namespace std;
int main()
{
    int n = 44;
    int& rn=n; //rnはnの参照
    cout << "n = " << n << ", rn = " << rn << endl;
    -n;
    cout << "n = " << n << ", rn = " << rn << endl;
    rn *= 2;
    cout << "n = " << n << ", rn = " << rn << endl;
}

実行結果

\begin{figure}% latex2html id marker 2268
\centering
\includegraphics[width=8.0cm]{CPPPIC/reidai7-2.eps}
\end{figure}

参照は宣言されたときに,初期化されなければならない.しかし,定数の初期化と違い,参照は変数を用いて初期化されなければならない.つまり,

int& rn = 44;
は誤りである.

練習問題 7..1   int &rn = 44とすると,どんなエラーがでるか確かめよ.

C++では,参照演算子&は2つの異なる目的に利用される.オブジェクト名の前に用いられると,そのオブジェクトのアドレスを意味する.しかし,型Tの後に用いられると"Tへの参照型"という派生型の意味になる.例えば,int&と書くと整数型への参照型となる.つまり,上の例題で

int& rn = n;
は,nは整数型を持っていると宣言され,rnは整数型への参照型を持っていると宣言される.

練習問題 7..2   参照の使い方
例題7.2のプログラムを実行せよ.

ポインタ(Pointers)

参照演算子&は変数のメモリアドレスを返す.このアドレスを格納できる変数の型をポインタという.ポインタ変数は"型Tへのポインタ"という派生型を持っていて,T*と表される.例えば,整数型の変数のアドレスを格納するポインタ変数はint*の型を持っている.

\begin{figure}\centering\includegraphics[width=10.6cm ]{CPPPIC/fig-pointa.eps}
\end{figure}

&xで得られたアドレスを保持できるポインタ変数pを定義すると

int* p;
となる.

例題 7..3   ポインタ
変数nの配置アドレスをポインタを用いて表示するプログラムを作成せよ.

解答

#include <>iostream>
using namespace std;
int main()
{
    int n;
    int* pn;
    n = 10;
    pn = &n;
    cout << "nのアドレス =" << pn << endl;
}

実行結果

\begin{figure}% latex2html id marker 2309
\centering
\includegraphics[width=8.0cm]{CPPPIC/reidai7-3.eps}
\end{figure}

変数nは10に初期化され,そのアドレスは0x22fedcである.変数pnはnのアドレスを値として初期化された.したがって,pnの値は0x22fedcとなり,確かに,pnはnのアドレスを値として持っている変数であることが分かる.

間接参照演算子(Dereference Operators)

pnがnのポインタであるとき*pnは,ポインタ変数pnが指し示す内容(この場合,変数nの値)を直接pnから得ることができる. この「*」を間接参照演算子(dereferencing operator)と呼ぶ.

練習問題 7..3   上の例題で,$\ast$pnの値はいくつか.プログラムを書いて確かめよ.

例題 7..4   間接参照演算子
変数nの配置アドレスと,変数nの値をポインタを用いて表示するプログラムを作成せよ.

解答 変数nのアドレスは&nで取り出すことができる.また,pnをnのポインタ変数とすると,つまり,pnはnのアドレスを格納する変数とすると,$\ast$pnはnのアドレスを頼りに,変数nの値を表す.つまり,$\ast$pnはnの別名である.では,プログラムを作成しよう.

#include <iostream>
using namespace std;
int main()
{
    int n = 10;
    int* pn = &n;
    cout << "nのアドレス =" << pn << endl;
    cout << "nの値 =" << *pn << endl;
}

実行結果

\begin{figure}% latex2html id marker 2334
\centering
\includegraphics[width=8.0c...
.../reidai7-4.eps}
\includegraphics[width=5.2cm]{CPPPIC/pointer.eps}
\end{figure}

間接参照演算子によって,pnが指し示す場所の内容.つまり,nの値10が得られた.

ポインタへのポインタ(Pointers to Pointers)

ポインタは別のポインタを指すことができる.

例題 7..5   ポインタへのポインタ
変数nのポインタ変数pnのアドレスを値として持つポインタ変数をppnとする.このとき,ppnの値,アドレス,間接参照,間接の間接参照を表示するプログラムを作成せよ.

==============================================

#include $<$iostream$>$
using namespace std;
int main()
{
int n = 10;
int$\ast$ pn = &n;
cout $<<$ "nのアドレス =" $<<$ pn $<<$ endl;
cout $<<$ "nの値 =" $<<$ *pn $<<$ endl;
int$\ast\ast$ ppn = &pn;
cout $<<$ "ppn =" $<<$ ppn $<<$ endl;
cout $<<$ "&ppn =" $<<$ &ppn $<<$ endl;
cout $<<$ "*ppn =" $<<$ *ppn $<<$ endl;
cout $<<$ "**ppn =" $<<$ **ppn $<<$ endl;
}
==============================================

実行結果

\begin{figure}% latex2html id marker 2370
\centering
\includegraphics[width=8.0c...
...eps}
\includegraphics[width=8.3cm]{CPPPIC/pointer-to-pointer.eps}
\end{figure}

変数ppnはpnを指し,pnはnを指す.したがって,*ppnはpnの別名で,*pnはnの別名となる.したがって,**ppnはnの別名である.

参照と間接参照は表裏(Referencing is the opposite of Dereferencing)

例題 7..6   参照と間接参照
変数nのポインタをpnとし,$\ast$pnの参照をrpnとするとき,nの値,nのアドレス,rpnの値と,rpnのアドレスを表示するプログラムを作成せよ.

解答 pnはnのアドレスを格納し, rpnは$\ast$pnの別名に注意すると,

==============================================

#include $<$iostream$>$
using namespace std;
int main()
{
int n = 10;
int$\ast$ pn = &n;
cout $<<$ "nのアドレス =" $<<$ pn $<<$ endl;
cout $<<$ "nの値 =" $<<$ *pn $<<$ endl;
int& rpn = $\ast$pn;
cout $<<$ "rpn =" $<<$ rpn $<<$ endl;
cout $<<$ "&rpn =" $<<$ &rpn $<<$ endl;
}
==============================================

実行結果

\begin{figure}% latex2html id marker 2402
\centering
\includegraphics[width=8.0c...
...}
\includegraphics[width=7.2cm]{CPPPIC/reference-dereference.eps}
\end{figure}

変数pnはnを指し,rpnは*pnの参照によって初期化されている.つまり,rpnはnの別名ということである.したがって,rpn = 10となる.

練習問題 7..4   参照,間接参照
 
int n = 44;
int* pn = &n;
int& rn = n;
int m = (*pn)++;
int* q = pn - 1;
r = *(–pn) + 1;
 ++*q;
のとき,n,m,&n, *pn, rn, *qの値をプログラムを組まずに求めたあと,プログラムを書いて確認せよ.ただし,&n = 0x22fedcとする.

派生型(Derived Types)

参照演算子&と同様に,間接参照演算子*も2つの異なる目的に用いられる.オブジェクトの前に用いられている場合,つまり,オブジェクトへのポインタとして用いられるときは,そのオブジェクトの持っている値を表す.また,型Tの後ろに用いられる場合は,"Tへのポインタ"型を表す.

C++には次のような5つの派生型がある.

const int C = 33; // 整数型の定数
int& rn = n; // 整数型への参照
int* pn = n; // 整数型へのポインタ
int a[] = {33, 44}; // 整数型の配列
int f() = {return 55;} // 整数型の戻り値

派生型はどの型からも作ることができるので,いろいろな組み合わせが可能である.

int* const pn = 44; // 整数型の定数ポインタ
const int* pn = &n; // 整数型定数へのポインタ
const int* const pn = &n; // 整数型定数への定数ポインタ
float& a[] = {x,y}; // フロートへの2つの参照配列
float* a[] = {&x,&y}; // フロートへの2つのポインタ配列

ポインタと配列(Pointers and Arrays)

C++ではポインタと配列の間に強い関係がある.配列の添え字を使って実行できるような操作は,ポインタでできるし,ポインタを使うほうが一般に高速である.しかし,初心者にとっては分かりにくいといわれている.どうだろうか.

宣言

int a[10];

は,大きさが10の配列aを定義するものである.これはa[0],a[1],…a[9]という名の10個連続したオブジェクトからなるブロックと考えることができる. a[i]と書くと,先頭からi番目の位置の配列要素を参照する.

paが整数へのポインタだとすると,

int* pa;
と宣言され,代入
pa = &a[0];
によってaの0番目の要素(配列aの先頭)を指すようにpaがセットされる.つまりpaはa[0]のアドレスを保持する.これで代入文
int x = *pa;
によりa[0]の内容がxにコピーされることになる.

次に, *(pa + 1)はどんなことを意味するのだろうか.

まず,paはa[0]を指し示している.そして,配列の要素が整数型なら,pa+1はpa+1の値を整数型のサイズ分だけ増やす.したがって,pa+1はa[1]を指し示す.よって,*(pa+1)はa[1]の内容を参照する.また,pa+1はa[1]のアドレスである.

pa = &a[0];
と書くと,paとaは同じ値をもつ.配列の名前はその先頭の要素の位置と同義であるから,pa = &a[0]という代入文は次のようにも書ける.
pa = a;

new演算子(new Operator)

ポインタが

float* p;
のように宣言されたとき,ポインタ自身にメモリを割り当てるだけである.ポインタの値はある記憶番地であるが,その番地にはメモリはまだ割り当てられていない.ということは,他の変数により,この記憶番地がすでに使われているかもしれない.この場合,pは初期化されないので,pが指し示すメモリへのアクセスはエラーとなる.つまり,
*p = 2.7182;
はエラーとなる.この問題を回避する最も良い方法は,ポインタを宣言したときには初期化も行うことである.つまり,

float x = 2.7182;
float* p = &x;
cout << *p;
のように書くと,*pへのアクセスは問題ではなくなる.

この問題を回避するもう一つの方法は,ポインタ自身へメモリを明示的に割り当てる方法である.これはnew 演算子を用いて行う.

float* q;
q = new float;
*q = 2.7182;

new演算子は,メモリ上の割り当てられていないフロート型(浮動小数点型)のサイズsバイトのアドレスを返す.そのアドレスをqに代入する*qは他の変数により使われていることはない.

float* q;
q = new float;
はまとめて
float* q = new float;
と書くことができる.実は,3行をまとめて
float* q = new float(2.7182);
と書くこともできる.

練習問題 7..5   次のコードの間違いを見つけよ.
int* p = new int;
int* q = new int;
cout $<<$ " p = " $<<$ p $<<$ ", p + q =" $<<$ p + q $<<$ endl;

delete演算子(delete Operators)

delete演算子はnew演算子と反対のことを行う.つまり,割り当てられたメモリを開放する.delete演算子はnew演算子と対で用いる.例えば,

float* q = new float(2.7182);
delete q;
*q = 2.7182;
の3行目はエラーである.なぜなら,2行目でqに割り当てられたメモリは開放されて,他のオブジェクトが使えるようになっている.つまり,qの指す場所はなくなっているので,*q= 2.7182はエラーとなる.
定数へのポインタは削除してはいけない.
例えば,

const int* p = new int;
delete p;
の2行目はエラーである.これは,定数は変えることができないという原理に沿っている.

delete演算子を基本型(char, int, float, double, etc)に用いることは推奨できない.

動的配列(Dynamic Arrays)

静的結合

float a[20];

float* const p = new float[20];

と宣言すると,aとpは20個のfloat型の要素を持つ配列への定数ポインタである.これらのポインタは,例えプログラム実行中一度も使われなくても,コンパイル時にメモリに確保される.これを静的結合という.

動的結合

float* p = new float[20];と宣言すると,pは20個のfloat型の要素を持つ動的配列へのポインタである.ポインタpはメモリの確保をプログラムが実行されるまで延期する.このようにして宣言された配列を動的配列(dynamic array)という.

例題 7..7   定数ポインタと定数へのポインタ(Constant Pointers and Pointers to Constants)
次のコードは何を表すか説明せよ.
 int n = 44;
int* p = &n;
 ++(*p);
 ++p;
int* const cp = &n;
 ++(*cp);
 ++cp;

解答
int n = 44; により変数nはint型でその値は44となる
int* p = &n; によりpはnへのポインタとなる
 ++(*p);により(*p)つまりnの値を1増やす
 ++p;によりポインタpの値,つまりnのアドレスを1増やす
int* const cp = &n;によりcpはnへの定数ポインタ
 ++(*cp);により(*cp)つまりnの値を1増やす
 ++cp;によりポインタcpを1増やすといいたいが,cpは定数なので不可能

ポインタの配列(Array of Pointers)

配列の成分がポインタであってもよい.例えば,
double* p[4];
とすると,配列pの4つの成分はdouble型へのポインタである.

例題 7..8   間接バブルソート
次のデータを昇順に並べるプログラムを作成せよ.

$\displaystyle 55.4, 22.1, 66.5, 44.3, 99.8, 88.7, 77.6, 33.2$

% latex2html id marker 18779
\includegraphics[width=4cm]{CPPPIC/Fig-reidai7-6.eps}

解答

#include <iostream>
using namespace std;
void print(double* a[],int);
void sort(double* a[],int);
int main()
{
    double* a[8];
    a[0] = new double(55.4);
    a[1] = new double(22.1);
    a[2] = new double(66.5);
    a[3] = new double(44.3);
    a[4] = new double(99.8);
    a[5] = new double(88.7);
    a[6] = new double(77.6);
    a[7] = new double(33.2);
    print(a,8);
    sort(a,8);
    print(a,8);
}
void sort(double* a[], int n)
{
    double* temp;
    for (int i=1; i < n; i++){
        for(int j=0;j<n-i;j++){
            if(*a[j] > *a[j+1]) {
                temp = a[j];
                a[j] = a[j+1];
                a[j+1] = temp;
            }
        }
    }
}
void print(double* a[], int n)
{
    for (int i=0;i < n;i++){
        cout << *a[i] << " ";
    }
    cout << endl;
}

実行結果

\begin{figure}% latex2html id marker 2489
\centering
\includegraphics[width=9.4cm]{CPPPIC/reidai7-8.eps}
\end{figure}

このプログラムでは,ポインタの移動を行うことにより,値の移動を行っている.

ポインタと関数引数(Pointers and Function Argument)

C/C++では関数に対して,引数を値で渡すから,呼ばれた関数のほうで,呼び出した関数内の変数を直接変更することはできないことはすでに学んだ.また,標準C++汎用アルゴリズムとしてのswap()関数を用いて,実引数の値を変更することもすでに学んだ.さらには,練習問題[*]と練習問題[*]でアドレス渡しおよび参照を用いたswap()関数の作成を行なった人もいるだろうが,もう一度,ユーザ関数としてのswap()関数を作成する.

例題 7..9   アドレス渡しによるswap関数
整数a,bを読み込み,main関数から&a,&bをユーザ関数swap(int* a,int* b)に渡して,*aと*bの値を入れ替える演算を行い,その結果をmainに戻して表示するプログラムを作成せよ.

解答 呼び出し側のプログラムで変更すべき値のアドレスを渡すようにする必要がある.つまり,
swap(&a,&b);

演算子&は変数のアドレスを与えるから,&aはaへのポインタである.ポインタ引数を使えば,関数の中でそれを呼んだ関数の中のオブジェクトをアクセスし変更することが可能になる.次に示すように,swap自身内では引数はポインタとして宣言され,実際の被演算数はそのポインタを通して間接的にアクセスされる.

#include <stdio.h>
void swap(int *,int *); //プロトタイプ宣言
int main()
{
    int a,b;
    printf("整数を2つ入力してください\n");
    scanf("%d %d",&a ,&b);
    if(a < b){
        swap(&a,&b);
    }
    printf("%d %d",a,b);
}
void swap(int *px, int *py)
{
    int temp;
    temp = *px;
    *px = *py;
    *py = temp;
}

実行結果

\begin{figure}% latex2html id marker 2503
\centering
\includegraphics[width=7.3cm]{CPPPIC/reidai7-9.eps}
\end{figure}

例題 7..10   参照を用いたswap関数
整数a,bを読み込み,main関数からa,bをユーザ関数swap(&a, &b)に渡して,aとbの値を入れ替える演算を行い,その結果をmainに戻して表示するプログラムを作成せよ.

解答 C++では,コンパイラに指示してポインタ値による呼び出しコードを自動的に生成させ,関数の特定なパラメター(値ではない)に参照で値を渡すことができる.それには,関数宣言の中で,パラメター名の前に&を付ければよい.
void swap(int&x, int& y);
でパラメターは参照パラメターで与えられている. 呼び出しはswap(a,b)と実引数を指定する.

#include <iostream>
using namespace std;
void swap(int &x,int &y);
int main()
{
    int a,b;
    cout << "整数を2つ入力してください" << endl;
    cin >> a >> b;
    if(a < b){
        swap(a,b);
    }
    cout << a << " " << b << endl;
}
void swap(int &x, int &y)
{
    int temp;
    temp = x;
    x = y;
    y = temp;
}

実行結果

\begin{figure}% latex2html id marker 2515
\centering
\includegraphics[width=7.3cm]{CPPPIC/reidai7-10.eps}
\end{figure}

関数へのポインタ(Pointer to Function)

配列の名前が定数ポインタであるように,関数の名前も定数ポインタである.関数へのポインタは関数名のアドレスを値とするポインタである.関数名自身がポインタなので関数へのポインタは定数ポインタへのポインタである.例えば,
int f(int); // 関数fの宣言
int (*pf)(int); // 関数へのポインタpfの宣言
pf = &f; // fのアドレスをポインタpfに代入

つまり,図で表すと次のようになる.

\begin{figure}\centering
\includegraphics[width=10.5cm]{CPPPIC/pointer-to-func.eps}
\end{figure}

例題 7..11   関数の和
$f(k) = k^{2}$のとき, $\sum_{k=1}^{n}f(k)$の値を求めるプログラムを関数へのポインタを用いて作成せよ.

解答 関数fへのポインタpfと整数nを引数に持つユーザ関数sum()を考える.つまり,
int sum(int (*pf)(int), int n);を考える.この関数を呼び出すには,
sum(f, n);が必要となる.

==============================================

int sum(int (*)(int), int);
int square(int);
int main()
{
cout $<<$ sum(square,4) $<<$ endl;
}
int sum(int (*pf)(int), int n)
{
int s=0;
for(int i = 1; i $<$= n; i++)
s += (*pf)(i);
return s;
}
int square(int k)
{
return k*k;
}
==============================================

実行結果

\begin{figure}% latex2html id marker 2537
\centering\includegraphics[width=7.3cm]{CPPPIC/reidai7-11.eps}
\end{figure}

確認問題 7..1  

1. 変数のアドレスはどのように呼び出すことができるか.

2. ポインタ変数に格納されたアドレスを持っている変数の値はどのようにして呼び出すことができるか.

3. 次の宣言の違いを説明せよ.

int n1 = n;
int& n2 = n;

4. 次の&の使い方の違いを説明せよ.

int& r = n;
p = & n;

5. 正しいか間違っているか理由を述べよ.

a. x == yならば &x == &y
b. x == yならば *x == *y

6. 次のコードはなぜいけないのか.

a. int& r = 22;
b. int* p = &44;
c. char p = & c;

7. 次のコードは何がいけないのか.

short a[20];

for(int i=0; i $<$ 20; i++)

*a++ = i*i;

8. 次のコードは何がいけないのか.

float x = 3.1415;

float* p = &x;

short d = 44;

short* q = &d;

p = q;

9. pとqがint型のポインタでnはint型のとき,どちらが可能か.

p + q;

p - q;

10. 次の事柄を宣言しなさい.

a. 8個のfloat型を要素にもつ配列

b. float型へのポインタを要素にもつ配列

c. 8個のfloat型を要素にもつ配列へのポインタ

d. 8個のfloat型へのポインタを要素にもつ配列へのポインタ

演習問題 7..1  

1. double型の配列をコピーする関数をポインタを用いて作成せよ.

2. Riemann和を用いて関数 $f(x) = x^{3}$を積分するriemann()関数を実装せよ.

$\displaystyle \int_{a}^{b}f(x)dx = \sum_{j=1}^{n}f(a + jh)h, h = (b-a)/n$

3. 関数 $f(x) = \sqrt{x}$$x = a$における微分係数を数値微分を用いて計算する関数を作成せよ.

$\displaystyle f'(a) \approx \frac{f(a + h) - f(a - h)}{2h}$

4. riemann()関数を用いて次の関数を指定された範囲で積分せよ.

a. sqrt() [1,4]

b. $\cos()$ [0, $\pi/2$]

c. $\exp()$ [0,1]

d. $\log()$ [1,e]

5. 2分法で方程式$f(x) = 0$を求めるため,次の関数を完成せよ.

double root(double (*pf)(double), double a, double b, int n)

ここで,pfはfのポインタである.