WisdomSoft - for your serial experiences.

6.1 ポインタ

ポインタの基本的な考え方と応用についてコードを用いて解説します。ポインタはアドレス演算子から取得したアドレスを保存し、間接演算子を用いて元の記憶領域にアクセスします。.

6.1.1 記憶領域の参照

よくC言語の初心者が挫折しやすい難関のひとつがポインタだと言われています。ですが、ポインタを理解するために数学的な知識は必要ありませんし、特別に難しい理論を学習するようなこともありません。原理さえ理解してしまえば、ポインタは難しいものではないのです。大切なのは順番に、少しずつ、確実に理解していくように努力することでしょう。

では、本題に入ります。ポインタとは、記憶領域の場所を指す変数の一種です。これまで私たちは変数を扱ってきました。変数で文字列を扱うこともできるようになりました。ポインタも変数です。難しいことはありません。

問題は、ポインタは何を格納するのかということです。通常の変数は特定のメモリ領域に何らかの値を保存するために利用してきましたが、ポインタは値の格納のために使うものではないのです。ポインタとはメモリアドレスを保存するための変数なのです。

変数は値をメモリのどこかに一時的に保存するものです。メモリは 1 バイトと単位でアドレス(住所)が割り当てられており、コンピュータはこのアドレスを用いてメモリに保存されている値にアクセスします。実際に、純粋な機械語には変数などというものは存在せず、値を保存したり、取得するにはアドレスを使いっています。アドレスは先頭から順番に数値で表現されます。

図1 メモリの構造
図1 メモリの構造

図1は、メモリを図で簡単に説明しています。メモリは値を保存することができます。文字定数や浮動小数点なども最終的には値(図では分かりやすいように10進数で表現していますが、本当は2進数で保存されています)として記録されているのです。値がどのような形で保存されるかは、コンピュータ・アーキテクチャによって異なりますが、メモリアドレスはバイト単位で割り当てられ、順に単純増加していきます。メモリアドレスが何バイトで表されるかといった問題も、コンピュータによって異なります。

ポインタは、メモリにメモリアドレスを表す値を保存することで、特定の変数(メモリに保存されている値)の位置情報を交換する手段として用いられるのです。「メモリにメモリアドレスを保存」という考え方がややこしいですが、これはすぐ後でC言語で実践して確認しましょう。

では、変数がメモリのどこに保存されているかを実際に見てみましょう。変数のアドレスを取得するには変数名の前にアンパサンド "&"をつけます。これをアドレス演算子と呼びます。

C言語では変数名を式に用いた場合はその変数の内容(保存されている値)を返しますが、アドレス演算子を変数の前に指定した場合は、その変数のメモリアドレスを返します。アドレス演算子は scanf("%d" , &iVariable) というように scanf() 関数ですでに使ったことがあります。このときの & 記号は変数のアドレスを関数に渡していたのです。

アドレス演算子 & は論理演算子の & とは異なるので注意してください。単項演算子として & が用いられた場合はアドレス演算子であり、2項演算子として & が用いられると論理演算子だと解釈されます。

アドレスの表現は処理系に依存する問題です。機械語レベルで考えればアドレスは整数型で表現されていますが、実装に依存した処理は避けるべきでしょう。アドレスを printf() 関数を使って表示する場合、%X などの書式指定を使って表示することも可能ですが、より確実に表示する場合は %p を使って処理系依存の表現で出力します。 

コード1
#include <stdio.h>

int main() {
	char chVar = 'G';
	int iVar = 10;

	printf("chVar : 内容 = %c,アドレス = %p\n" , chVar , &chVar);
	printf("iVar : 内容 = %d,アドレス = %p\n" , iVar , &iVar);
	return 0;
}
実行結果
コード1 実行結果

各変数の内容は、これまでの通り変数を初期化したときに格納した値です。これは、変数が示すメモリに保存されていた情報です。これに対してアドレスは、この変数が示している(つまり、この値が保存されている)メモリアドレスです。printf() 関数の引数にアドレス演算子 & を使っていることに注目してください。

表示される実際の数字は、実行するコンピュータやそのときのメモリの状況によって異なります。メモリアドレスはアプリケーションを実行した時に、システムが割り当てた時に決定されるため、静的に固定することはできません。現代の多くのコンピュータでは、メモリを自由に扱うことができる権限を持つのは基本ソフトウェア(オペレーティングシステム)だけです。

6.1.2 アドレスの保存

メモリアドレスの取得の方法はわかりましたが、それだけでは何もできません。メモリアドレスの数値を利用するのはコンピュータであって、開発者や利用者にとって、メモリアドレスはそれほど意味がありません。

そこでポインタが出てきます。ポインタは、メモリアドレスを格納することを専門とする特殊な変数です。ポインタにアドレスを代入することによって、そのポインタ変数は直接そのメモリアドレスにアクセスできるようになるのです。遠隔から変数の内容を操作するようなイメージです。

ポインタの宣言は、基本的通常の変数と同じですが、変数名の前にアスタリスク "*" をつけます。 

ポインタの宣言
 *変数名

こうすると、ポインタ変数に対してアドレスを格納することができます。例えば int *iPo と宣言した場合、iPo 変数は int 型変数へのポインタであることを表しています。メモリアドレスは整数であることは説明しました。ポインタ変数はアドレス保存するための変数であり、実体としては整数型であることを意味しています。

ポインタ変数が格納しているアドレスを辿って、元の変数(アドレスが指すメモリの値)を得るには間接演算子 * を利用します。これをポインタ変数の前に指定することで、ポインタ変数が格納するアドレスの値を取得することができます。間接演算子 * は乗算演算子 * とは違うので区別してください。間接演算子は単項演算子です。

関節演算子を使って、ポインタが指すメモリの値を取得することを間接参照するといいます。

図2 アドレスからの間接参照
図2 アドレスからの間接参照

図2はポインタが格納するアドレスを使って間接参照をする状況を表しています。ポインタも変数の一種であり、メモリのどこかに整数値でアドレスを保存しています。ポインタはこれを使って元の変数にアクセスするのです。

コード2
#include <stdio.h>

int main() {
	int iVar = 100;
	int *iPo = &iVar;

	printf("iPo = %p : &iVar = %p : *iPo = %d\n" , iPo , &iVar , *iPo);
	return 0;
}
実行結果
コード2 実行結果

iPo 変数は int 型変数へのポインタです。ポインタが保存しているのはメモリアドレスであり、iPo 変数の内容を printf() 関数で表示すると保存しているメモリアドレスであることがわかります。これは &iVar と同じ値であることが確認できますね。

ポインタが格納しているアドレスが指すメモリの値を取得するには間接演算子 * を使います。printf() 関数の最後に *iPo を指定して、iPo 変数に代入したアドレスを使って間接参照をしています。こうすることで iPo に格納されているアドレス、すなわち iVar の値を間接的に取得できるのです。

間接演算子を用いれば、アドレスを間接参照するだけではなく、ポインタが表すアドレスに値を間接代入することもできます。つまり、ポインタを通じて変数に値を代入します。

コード3
#include <stdio.h>

int main() {
	int iVar = 0;
	int *iPo = &iVar;

	*iPo = 100;
	printf("iVar = %d\n" , iVar);
	return 0;
}
実行結果
コード3 実行結果

このプログラムでは iPo ポインタ変数に iVar int型変数のアドレスを代入しています。その後、*iPo = 100; というようにポインタを使って iVar に間接代入を行っているのです。その結果、printf() 関数では iVar の値は 100 になっていることが確認できます。

このような間接代入に何の意味があるか理解に苦しむかもしれません。確かにコード3を見ると、値は直接 iVar 変数に代入すればよいだけのことで、ポインタを利用する意義を感じさせません。

ポインタは、異なる関数同士で自動変数を共有する時に威力を発揮します。関数の中で宣言された変数は自動変数となり、変数は宣言されたブロックの内部でしか使うことができませんでした。関数の引数として渡すことができるのは値のコピーであり、他の関数から自動変数を操作する方法はないのです。

そこで、関数の引数にポインタを渡します。そうすると、異なる関数の自動変数を遠隔操作することができるようになります。ポインタを渡せば、関数はこれを間接参照して異なる関数の自動変数に値を代入するということができるようになるのです。このように、関数にポインタを渡し、間接的に情報にアクセスする手法を参照渡しと呼び、単純に値をコピーして渡す手法を値渡しと呼びます。

例えば、「2 つの変数の値を入れ替える関数を作りなさい」という課題が与えられた場合、従来の方法では実現することができません。関数の戻り値は常にひとつの値しかありませんし、パラメータの値を交換しても、関数の呼び出し元には何の影響も与えられません。このような場合にポインタをパラメータで受け取るという方法が考えられます。

コード4
#include <stdio.h>

void swapInt(int * , int *);

int main() {
	int iVar1 = 100 , iVar2 = 1000;
	swapInt(&iVar1 , &iVar2);

	printf("iVar1 = %d : iVar2 = %d\n" , iVar1 , iVar2);
	return 0;
}

void swapInt(int *iPo1 , int *iPo2) {
	*iPo2 ^= *iPo1 , *iPo1 ^= *iPo2 , *iPo2 ^= *iPo1;
}
実行結果
コード4 実行結果

コード4の swapInt() 関数は、2 つの int 型のポインタを受け取ります。この関数は渡されたポインタから間接参照を行って呼び出し元が指定した変数の値を変更させることができるのです。プログラムを実行すれば iVar1 = 1000 : iVar2 = 100 という結果を得ることができるはずです。

ところで、ポインタ変数にもデータ型が存在します。char 型の変数のアドレスは char 型のポインタに、int 型の変数のアドレスは int 型のポインタに代入しなければなりません。しかし、ポインタとはメモリアドレスを表す整数を保存するためのもので、ポインタ変数が占有するメモリ領域は常に同じサイズです。ポインタ型は、Cコンパイラがポインタの指す変数が何バイトであるかを認識するために使われます。これで、間接参照による代入や取得を行ったとき、何バイト単位で値をコピーしたり代入するかが決定されます。このため、特殊なケースを除いて型の異なるポインタ変数にアドレスを代入したりすることはありません。

コード5
#include <stdio.h>

int main(){
	int iVar1 = 0xFFF , iVar2;
	unsigned char *chPo = &iVar1;

	iVar2 = *chPo;

	printf("iVar2 = %d\n" , iVar2);
	return 0;
}
実行結果
コード5 実行結果

このプログラムは符号無し char 型のポインタ chPo に int 型変数 iVar1 のアドレスを代入しています。確かに chPo は iVar1 のアドレスを間違いなく保存することができますが、iVar2 = *chPo で間接参照した時、コンパイラはこのポインタの参照先は 1 バイト(char 型)であると判断します。iVar1 に格納されている値は16進数 0xFFF ですが、char 型として間接参照した場合は、このうちの下位 1 バイトしか見えないため、iVar2 には16進数の 0xFF すなわち10進数の 255 が代入されます。