7日目 9日目

8日目

目標

課題


課題 7

ポインタ

メモリとアドレス、変数の関係

コンピュータは内部的には電気信号の on/off で物事を記憶している。 この記憶の最小単位の事を 1bit(1ビット)と呼ぶ。

コンピュータの内部には、そのような記憶する場所が膨大にあり、CPU はその記憶領域(メモリ)を用いて作業を行っている。 すべての記憶領域は、アドレス(address)といわれる番号が割り振られ、管理されている。 いままで用いてきた変数は、ある一定のアドレス範囲に対して名前を割り振り、人間が理解しやすいようにしたものである。 この番地は 1bit ごとではなく、8bit をまとめた 1byte(1バイト)ごとにつけられている。 1バイトで表される情報量は 2×2×2×2×2×2×2×2 = 28 = 256 通りである。

コンピュータが行っているプログラムは、特定のアドレスで保持されている内容を参照したり、指定された内容を特定のアドレスに保存している事になる。 しかしながら、人間がアドレスで考えるのはたいへん(「04番地から4バイトの領域に、12345 という値を代入せよ」とか。)なので、 その領域について名前をつけることにしたものが変数である。 したがって、変数は、その型(サイズ)によって、特定のアドレス範囲を確保することになる。 int 型の変数や char 型の配列はそれぞれ次のような形で確保されていたと考えれば良い。

memory map

こうすると、「04番地から4バイトの領域に、12345 という値を代入せよ」の代わりに、 「int 型変数 iに 12345 を代入する」という表現ですむことになる。


C では、この変数のアドレスを直接扱うことができる。 変数のアドレスを調べるためには、演算子&を用いる。 次のようなプログラムを使うと、変数のアドレスを調べることができる。

/* address.c */
/* 変数のアドレスを表示する */

int main() 
{
	int i;
	char a[6];
	
	for(i = 0; i < 6; ++i) {
		/* 変数 i のアドレスと、i の中身を表示 */
		printf("&i = %p, i = %d\n", &i, i);
	}
	
	printf("\n"); /* 区切りのための空白行 */

	for(i = 0; i < 6; ++i) {
		/* 配列変数 a の要素のアドレスを表示 */
		printf("&a[%d] = %p\n", i, &a[i]);
	}
}

実行するとどうなるだろうか? はじめの for で出力される内容を見ると、変数の中身は変わっても、その位置であるアドレスは変化していないことが分かる。 また、次の forで出力される内容をみると、配列要素のアドレスは連続している事も分かる。

問 7-1: int 型の配列の要素のアドレスはどのようになっているであろうか?

問 7-2: 宣言した要素数より大きな添字をつけると何が起こるか?


ポインター変数

ポインター(pointer)とは英和辞典を索くと「指す人(もの)」という意味を持つことがわかる。 C 言語で出てくるポインターとは「アドレスを入れる事ができる変数」の事である。 ポインタ型の変数を宣言する為には、

int *ptr;

の用に「*」をつけて宣言を行う。 宣言の際に前に書かれている型は、その内容であるアドレスに存在する変数の型、 言い換えると、そのポインタ変数でどの型の変数のアドレスを扱うのかを示している。

ポインタ変数の利用の仕方を順にあげる。

問 7-3: 次のプログラムを実行するとどのような出力を得られるか?

/* prob7-3.c */
#include <stdio.h>

int main() 
{
	char a[6] = "Hello";
	char *p;

	p = &a[0]; /* a[0] のアドレスを p に代入 */

	while (*p) {
		printf("%c\n", *p++);
	}
}

課題 8

ポインタ (2)

課題 7 では、ポインタの基本的な取り扱いについてのみ述べた。 次に、ポインタが出てくるいくつかの場面を取り上げて、具体的な利用方法について考える。


関数への参照渡

課題 3で取り上げた関数において、呼び出す側で関数に与えた引数と関数側で受けたものとは、全く独立であった。 実際、課題3や課題4では、main関数側では定数だったのにもかかわらず、関数側である calendarでは、変数として扱われており、実際内容も変更されている。 再確認するために、次のプログラムを実行してみよう。

/* functest1.c */
#include <stdio.h>


int test_sub1(int);

int main() 
{
	int var;
	int result;

	/* main 関数で値を設定する */
	var = 10;

	/* 当然出力される値は上で設定したもの */
	printf("main 1: var=%d\n", var);

	/* 関数呼び出しを実行 */
	result = test_sub1(var);

	/* 何が出力されるか? */
	printf("main 2: var=%d\n", var);
	return (0);
}


int test_sub1(int parameter) 
{
	/* main 関数で設定した値がでるはず */
	printf("test_sub1 1: parameter=%d\n", parameter);

	/* 関数の中で値を変更した */
	parameter = 20;

	/* 変更した値がでるはず */
	printf("test_sub1 2: parameter=%d\n", parameter);
	return (0);
}

コンパイルして実行する前に、どのような出力が得られるかを考えてみること。

実際に実行してみると、test_sub1での変更が呼び出し元の mainには伝わっていないことが分かる。 これは、関数呼び出しの際に、引数の内容(値)が関数側で受ける変数にコピーされているためである。 確かめるためには、main での変数 var のアドレスと、test_sub1での変数 parameterのアドレスを比較してみれば良い。

calendar関数の場合は、これでも良かったが、 関数内での変更が呼び出し元にまで及ぶようになっている方が便利な場合もある。 呼び出し元で変数のアドレスを引数として設定し、受け側ではポインタとして受けると、 呼び出した関数側での変更を引き継ぐことができる。 functest1.c を少し変更した次のプログラムを見てみよう。

/* functest2.c */
#include <stdio.h>


/* 関数宣言: 引数はポインタ */
int test_sub2(int *);

int main() 
{
	int var;
	int result;

	/* main 関数で値を設定する */
	var = 10;

	/* 当然出力される値は上で設定したもの */
	printf("main 1: var=%d\n", var);

	/* 関数呼び出しを実行 (渡すものは変数のアドレス)*/
	result = test_sub2(&var);

	/* 何が出力されるか? */
	printf("main 2: var=%d\n", var);
	return (0);
}


int test_sub2(int *parameter) 
{

	/* main 関数で設定した値がでるはず */
	/* 
	 * parameter がポインタ変数なので、
	 * 値を得るには * 演算子が必要 
	 */
	printf("test_sub1 1: parameter=%d\n", *parameter);

	/* 関数の中で値を変更した */
	*parameter = 20;

	/* 変更した値がでるはず */
	printf("test_sub1 2: parameter=%d\n", *parameter);
	return (0);
}

functest2の場合、呼び出された関数で扱っている変数は main関数中の変数 varを指し示すポインタであるため、 呼び出された関数での変更を main関数にまで伝えることができている。 また、最近のコンピュータ環境では気にする必要が少なくなってきてはいるが、参照渡の場合は、データのコピーが発生しない分関数の呼び出しにかかるオーバヘッドが軽減される。


文字列の表現

文字列は、char 型の配列で、終端が '\0'(null)になっているものであった。 配列は連続したメモリ領域に確保されるので、文字列は null で終わる一連の文字の最初の文字のアドレスで示すことができる。 文字列の長さと、配列の大きさは無関係であることに注意すること。 次の例では、配列の大きさは10であるが、文字列の長さは5である。

なお、配列の名前を添字をつけずに用いると、「配列の先頭要素のアドレス」という意味になる。

次の関数は、与えられた文字列の長さを調べる関数 strlen の簡単な実装例である。

int strlen(char *str)
{
        char *s;

        for (s = str; *s != '\0'; ++s) {
                ;
        }
        return(s - str);
}

はじめに、与えられた文字列の先頭アドレスを sにセットし、一つずつ増やしていく。 参照しているアドレスの中身が '\0' になったら forループを終了する。 ループの中で何も実行する必要はないので空文となっている。 結果的に、 forループを終了した時に、sが指しているアドレスは '\0' つまり文字列の終端となり、 先頭アドレスとの差を計算することで文字列の長さを求めることができている。


複数の返り値を返す関数

C では、関数は呼び出された場所に高々1つの返り値しか返すことができない。 複数の値を返したい場合は、ポインタを引数とするように関数を定義し、呼び出し側から返り値を格納する変数のアドレスを渡してやることで、実現できる。


ポインタはこれ以外にも「データの動的な割当て」や「リンクデータ構造」などにも 用いられるが、それは後の機会に譲る事にする。


7日目 表紙 9日目

tacha@tack.fukui-med.ac.jp
$Id$