ポインタ

C/C++のポインタの概念を説明します。

アドレス

int i = 10;

というコードがあるとします。 これがコンパイルされたとき、EXEのなかではどう表現されているのでしょうか?

変数の値は、実際にはメモリのどこかに保存されています。 そして「どこに保存されているか」というメモリの住所は、番号で表現されます。 上記のコードは、たとえば

45450721番目のメモリに 10 を代入せよ

のようにコンパイルされます。 何番目のメモリを使うかは、コンパイラが勝手に決めてくれます。

C言語を使えば、このようにどの変数にどこのメモリを割り当てるかを 自動的に割り振ってくれるわけですね。

この、"何番目のメモリ" という番号のことを「アドレス」と呼びます。直訳すれば "住所" ですね。

ポインタ

さて、上記のコードにおいて、変数 i が何番目のメモリに格納されているかが知りたいときがあります。 用語を用いて言い換えれば、変数 i のアドレスが知りたいわけですね。

そういう時は &i と書けばおkです。上記の例では &i は 45450721 です。

C言語には、アドレスを格納するための特別な変数型があります。それを「ポインタ」と呼びます。 ポインタの宣言は

int *p;

のように書きます。たとえば

int i = 10;
int *p = &i;

などとします。この p には、たとえば 45450721 などが格納されます。 この例では、p のことを「i へのポインタ」といいます。 「i へのポインタ」とは、"i のアドレスが格納されているポインタ"のことです。

では 45450721番目のメモリに入っている値(すなわち i に格納されている値)が知りたいときはどうするかというと

printf("%d", *p);

と書きます。

*p のことを、「p が指す値」と呼びます。

ポインタの宣言

int i;

という宣言文は「i は int型である」と宣言しています。これと同じで

int *p;

という宣言文は「*p は int型である」、もしくは「pとは、*p が int型になるような型の変数である」と宣言しています。

ちょっと複雑な例を考えましょう。intへのポインタを10個並べた配列 ap が欲しいときはどうすればよいでしょうか?

ap[index] はポインタですね。その指す値(int型)は、 *(ap[index]) で得られます。ですから ap の宣言は

int *(ap[10]);

と書けばよいのです。

次に、intを10個並べた配列へのポインタ pa が欲しいときはどうすればよいでしょうか?

*pa は配列ですから、(*pa)[index] は int 型ですね。ですから

int (*pa)[10];

と宣言すればよいです。

関数の引数について

値渡し

#include <stdio.h>

void func(int a)
{
    a = 100;
}

int main()
{
    int b = 1;
    func(b);
    printf("%d", b);
}

というプログラムを実行すると、"1" と表示されます。決して "100" にはなりません。

具体的に追っていきましょう。&a は 45450721、&b は 6741 だとします。 main関数のなかでは、まず

int b = 1;

で 6741番目のメモリが 1 になります。次に

func(b);

では、45450721番目のメモリに 1 がコピーされて func の先頭にジャンプします。

func のなかで、45450721番目のメモリに 100 が代入されます。そうして

printf("%d", b);

に戻ってきたとき、6741番目のメモリには何が入っているでしょう?

1 ですね。

このように、関数の引数には、呼び出し元の変数の値がコピーされ(「値渡し」)、 関数内部ではそのコピーされたものしかいじれません

関数の引数にポインタを用いると

#include <stdio.h>

void func(int *p)
{
    *p = 100;
}

int main()
{
    int b = 1;
    func(&b);
    printf("%d", b);
}

を最初から追っていきましょう。&p は 45450721、&b は 6741 だとします。 まず

int b = 1;

で 6741番目のメモリが 1 になります。つぎに

func(&b);

で、45450721番目のメモリに 6471 がコピーされて、funcの先頭へジャンプします。

funcのなかで、45450721番目のメモリに格納されている番号(6471番)の メモリに 100 が代入されます。そうして

printf("%d", b);

に戻ってきたとき、6741番目のメモリには 100 が入っています。 ですから、画面には "100" と表示されることになります。

このように、ポインタを関数の引数に用いると、 関数内で呼び出し元の変数をいじることができるようになります。

配列

配列

char ac[100];

と配列を宣言したとします。配列は、メモリ上に連続して並べられます。 &(ac[0]) が 45450721 だとすると

&(ac[0]) = 45450721
&(ac[1]) = 45450722
&(ac[2]) = 45450723
&(ac[3]) = 45450724
.
.
.

とならびます。そうして、ac を単独で用いると、&(ac[0]) に変換されます

printf("%p\n", ac);       // %d が int を表示するように、%p は アドレスを表示する
printf("%p\n", &(ac[0]));

このコードを実行すると、同じ値が表示されます。 このように、配列の識別子は、その配列の先頭のアドレスに変換されます。 変換されるだけで、配列がアドレスそのもの、というわけではありません。 また、先頭のアドレスが格納されているポインタから、 配列へ逆変換することはできません

配列とポインタ

さて、上記の ac を使って

*(ac + 3);

は何を表すでしょう? ac + 3 は 45450724 ですから、 それに * をつけると ac[3] にアクセスできますね。 一般に

ac[index] ⇔ *(ac + index)

の関係が成り立ちます。

ac は &(ac[0]) に変換されるといいましたが、ac[0] は char 型ですから、 &(ac[0]) は char へのポインタです:

char ac[100];
char *pc = ac;

と書くことができます。配列をポインタに代入しているように見えますが、 実際には配列の先頭のアドレスをポインタに代入しているだけです。

このポインタを使って、ac の要素にアクセスすることを考えましょう。 たとえば ac[3] にアクセスしたければ

*(pc + 3)

と書けばよいですね。この節を先頭から読み直すと、これが

pc[3]

と書ける気がしませんか? 書けます。ですが pc[3] は、単なる *(pc + 3) の言い換えに過ぎません

ac[3] は、「配列 ac の 3 番目の要素」を指しますが、 pc[3] は、単に*(pc + 3) を言い換えただけです。

この違いは今は気になりませんが、2次元配列を考えたときに表に浮上します。

何はともあれ、ポインタをつかって配列と同じような書き方ができるのですね。 でもポインタは(ここでは)配列の要素を指し示すものであって、配列そのものとは異なります。次に

char *pc = ac + 5; // &(ac[0]) + 5 のこと

とすれば、pc[3] ⇔ *(pc + 3) ⇔ *(ac + 5 + 3) ⇔ ac[5 + 3] となります。 このように、ポインタを使って、配列の先頭をずらしたように 見せかけることができます。あくまで見せかけるだけです。

配列を関数に渡すには

配列を関数に渡したいとき、仮引数の宣言は 2 種類あります。

void func(char pc[]);
void func(char *pc);

これはどちらも全く同じです。ポインタの宣言になっています。

そしてこの関数を呼び出すとき、配列そのものを渡すことはできません。 代わりに配列の先頭の要素のアドレスを渡します。

char ac[10];
func(ac); // func(&(ac[0])); だとみなされる

それでもこれまで見てきたように、func のなかで このポインタをあたかも配列のように用いることができるので、問題ありません。

オブジェクトのサイズ

話は変わりまして、今度はオブジェクト(≒変数)のサイズの話に移ります。

char, short, int, long

これらは全て整数を表しますが、どうしてこんなに沢山の種類があるのでしょうか?

整数をノートに記録するとき、 大きな数は沢山の桁数が必要です。 "4" なら1桁ですみますが、"123456" なら 6 桁も必要です。

コンピュータでも同じで、大きな数を表すには沢山のメモリが必要です。 使用するメモリ量の順に char ≤ short ≤ int ≤ long となります。 つまり、char より long のほうが、大きな数まで表すことができるのです。

メモリ量の最小単位を「1バイト」といいます。

たとえば、Intel 32bit CPU ならば、int は4バイトです。

int i = 200000000;

と書くと、i の内容は 4 つに分割して格納されます。 たとえば &i が 45450721 なら、45450721番 ~ 4545074番のメモリが使用されます。

そうして、&i は使用されているメモリのうち、先頭の番号を表すのです。

これまで「○○番目のメモリに格納される」と表現してきましたが、 これは正しくは、「○○番から××番までのメモリに格納される」と表現するべきです。 (が、誤解の生じない限り、これからも従来の表現を用います)

ポインタの加減算

上のほうで

char ac[100];

と宣言したら、5 番目の要素のアドレスは ac + 5 で得られる、といいました。 これは char 型が1バイトだからです。ですが、int 型の配列を

int ai[100];

と宣言したとしましょう。そして &(ai[0]) は 45450721 だとしましょう。 すると、(1 つの要素の大きさが 4 バイトなので) 5 番目の要素のアドレスは 45450721 + (5 * 4) になります。

では 5 番目の要素のアドレスは ai + (5 * 4) と書かないといけないのでしょうか?

実はこれは間違いです。単に

ai + 5

と書くだけでよいのです。x4 の部分は、コンパイラが勝手に行ってくれます。

同様に

int ai[100];
int* pi = ai;
++pi;

というコードを実行すると、&(ai[0]) が 45450721 ならば、pi は 45450725 に なります。++pi; は単なるインクリメントではなく、 ポインタが指す型のサイズ分だけ加算されるのです。

逆に言えば、ポインタを使うときは、コンパイラがポインタが指す型のサイズを 知っている必要があります。

配列再び

今度は2次元配列について考えます。

2次元配列

char aac[3][2];

と2次元配列を宣言したとします。するとこれは 「要素が2つの配列を、3つ並べたもの」になります。

aac[0][0], aac[0][1], aac[1][0], aac[1][1], aac[2][0], aac[2][1]

の順に、全ての要素が連続して並べられます。

そして &aac[0][0] が 45450721 だとすると、aac[x][y] のアドレスは

45450721 + (x * 2) + y

で計算できます。

aac[x] と一つ目の添え字だけを指定すると、{ aac[x][0], aac[x][1] } とならんでいる部分を char 型の配列だとみなしたものが得られます。

すなわち aac[x] は &(aac[x][0]) に変換されます。

char* pc1 = aac[1];

などと書けるわけです。pc1 の値は、実際には

45450721 + (1 * 2)

と計算されます。

そうして pc1[x] ⇔ aac[1][x] になります。

では aac を単独で用いたときは、何に変換されるのでしょうか?

aac[x] が char 型の配列なのですから、aac は「char型の配列」の配列です。 ですから、aac は、「char 型の配列」へのポインタに変換されるべきです。

配列へのポインタは上のほうでやりましたね。

char (*pac)[2] = aac;

と変換できるわけです。型はややこしいですが、値は 45450721 です。 この場合、++pac; などと書けば、要素 2 つの配列の長さ分だけ加算されて 45450723 になります。すると pac[x][y] は aac[x + 1][y] と同じものを指すようになります。

気づいた方がおられるでしょうか?

char aac[3][2];
char (*pac)[2] = aac;
char *pc0 = aac[0];

printf("%p\n%p\n", pac, pc0);

を実行すると、pac も pc0 も同じ値であることがわかります。しかしこの2つは、 値は同じでも型が違うのです。

pac[x] は char 型の配列になりますが、pc0[y] は char 型の変数になるのです。

関数に渡すには

関数の引数に int aai[3][2] を渡したいとしましょう。仮引数の宣言はどうすれば よいでしょうか?

配列を渡すには、その先頭の要素へのアドレスを受け渡しするようにすればよいのでしたね。

今回の場合、aai は「配列の」配列ですから、受け取る側は「配列の」ポインタで なければいけません。すなわち

void func(int (*pai)[2]);

と宣言します。もっと簡単に

void func(int pai[][2]);

と書いても同じです。この "2" を省略することはできません。なぜなら、

++pai;

というコードがあったとき、「要素 2 つの int 型配列」分の 大きさだけ加算する必要があります。同様に

pai[2]; // *(pai + 2) のこと

というコードがあったとき、「pai の値よりも『要素 2 つの int 型配列』分の大きさ x2 だけ 進んだところ」から始まる配列を取得しないといけません。

このように要素 2 つ、というのは重要な情報なのです。

似非2次元配列

お気づきの方はいるでしょうか? main 関数の宣言は

int main(int argc, char *(argv[])); // これは char *argv[] と同じ
int main(int argc, char (*argv)[]);
int main(int argc, char **argv);
int main(int argc, char argv[][]);

などと書きます(全部同じです。char **argv; が宣言されたことになります)。 最後の宣言方法を見るとわかりますが、argv[][ここがない] ですね。

普段 argv[1][0] などと2次元配列と同じように用いていますが、 これは本来の2次元配列ならありえない話です。

なぜなら、argv[1] というのが、argv[0] から「要素いくつ分の char 型配列」分の大きさだけ 離れたところにあるか分からないのですから。

実は argv に渡されるのは、2次元配列ではなく、「ポインタがならんだ配列」なのです。

これの宣言は次のようにします。

char ac0[3];
char ac1[6];
char ac2[2];
char *(apc[3]) = { ac0, ac1, ac2 }; // { &(ac0[0]), &(ac1[0]) &(ac2[0]) }; と同じ

apc[x] は、配列先頭の要素へのポインタですから

*(apc[x] + y)

もしくは

apc[x][y]

と書けば、x 番目の配列の、第 y 要素が得られることになります。

この場合、コンパイラは要素にアクセスするために、アドレスを計算する必要がありません。 そのアドレスは最初から配列内に格納されているのですから。

そして、「ポインタがならんだ配列」を関数に渡したいときは、その配列の先頭要素(=ポインタ)の アドレスを渡すのでしたね。ですから関数の仮引数は「ポインタへのポインタ」として 宣言しないといけません。それが

void func(char **ppc);
void func(char *(ppc[]));

などとなるのです。そのほかの書き方でもかまいません。

ちなみに、今回の変数を使って

printf("%p\n%p\n", apc, apc[0]);

を実行してみてください。前回と違い、今度は違うアドレスが表示されるはずです。

関数ポインタ

関数のアドレス

プログラムが実行されているとき、変数の値はメモリのどこかに格納され、 そのメモリの番号をアドレスというのでした。 今回は、そのプログラムのコード自体に焦点を当てます。

実はプログラムというのは、メモリにロードされて実行されます。 つまり、プログラム自体もメモリのどこかに格納されているのです。

プログラム中で関数を使うなら、その関数自体がメモリのどこかに格納されています。

そこで、関数が格納されているメモリ領域の、先頭の番号を「関数のアドレス」と 呼ぶことにしましょう。

関数の呼び出され方

void func(void)
{
    printf("hello, world!\n");
}

int main()
{
    func();
}

というプログラムがあって、func のアドレス(func の先頭のアドレス) が 45450721 だとしましょう。すると main 関数内の

func();

という行は「45450721番目のメモリに格納されているコードへジャンプしろ」と翻訳 されます。

ですから、アドレスさえわかっていれば、その関数を呼び出すことが可能なのです。

関数ポインタ

そこで、関数のアドレスを格納するポインタ pfn があるとしましょう。 このポインタを使って、関数を呼び出すには

int result = (*pfn)(x, y);

などと書けばよいのです。ただし

int result = pfn(x, y);

と書いても全く同じに解釈されます。*をつけてもつけなくてもよいのですね。 この関数ポインタ独特の仕様は、関数ポインタをわかりにくくする一端を担っている 気がします。

さて、このように使う pfn を宣言するにはどうすればよいのでしょうか?

今までと同じで「(*pfn)(int型, int型) (の返り値)は int だ」と宣言すればよいのです:

int (*pfn)(int, int);

先ほど述べたように pfn(int, int); と呼び出してもよいのですが

× int pfn(int, int);

では、pfn という関数のプロトタイプ宣言になってしまうので、関数ポインタを宣言するときは 必ず前者の方法を取ります。

関数ポインタに関数のアドレスを代入するには

int (*pfn)(int, int) = func; // func は int func(int x, int y){ ... } となっている必要がある

と書けばよいです。簡単ですね。

関数ポインタの使い道

int Kakeru(int x, int y)
{
    return x * y;
}

int Tasu(int x, int y)
{
    return x + y;
}

int Enzan(x, y, int (*pfn)(int, int))
{
    return pfn(x, y);
}

int main()
{
    printf("%d, %d\n",
        Enzan(3, 4, Kakeru),
        Enzan(3, 4, Tasu)
    );
}

のように使えます。Enzan という関数ひとつで、掛け算と足し算のオプションが 選べるようになるわけです。

C++にステップアップしたときに、virtual 関数というものに出会うことでしょう。 それは関数ポインタを用いて実装されているのです。

加筆と訂正おながいします


トップ   編集 凍結 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2023-02-23 (木) 23:33:34