7日目 9日目

8日目

目標

課題 9

ポインタ (2)

ポインタを使ったメモリ空間の探索

先週の説明や例え話を、 実際のプログラムで確かめてみるところから始めよう。

/* pointer1.c */
/* ポインタを使ってメモリの様子を観察する */
#include <stdio.h>


int main(int argc, char *argv[])
{
	char a, b, c, d, e, f, g;
	char string[10];
	int  i, j, k, l, m, n;
	char *c_ptr;
	int  *i_ptr;

	printf("&a = %p\n", &a);
	printf("&b = %p\n", &b);
	printf("&c = %p\n", &c);
	printf("&d = %p\n", &d);
	printf("&e = %p\n", &e);
	printf("&f = %p\n", &f);
	printf("&g = %p\n", &g);
	printf("&string[0] = %p\n", &string[0]);
	printf("&string[1] = %p\n", &string[1]);
	printf("&string[2] = %p\n", &string[2]);
	printf("&string[3] = %p\n", &string[3]);
	printf("&string[4] = %p\n", &string[4]);
	printf("&string[5] = %p\n", &string[5]);
	printf("&string[6] = %p\n", &string[6]);
	printf("&string[7] = %p\n", &string[7]);
	printf("&string[8] = %p\n", &string[8]);
	printf("&string[9] = %p\n", &string[9]);
	printf("&i = %p\n", &i);
	printf("&j = %p\n", &j);
	printf("&k = %p\n", &k);
	printf("&l = %p\n", &l);
	printf("&m = %p\n", &m);
	printf("&n = %p\n", &n);
	printf("&c_ptr = %p\n", &c_ptr);
	printf("&i_ptr = %p\n", &i_ptr);

	/* 変数の初期化 */
	a = 1;
	b = 2;
	c = 3;
	d = 4;
	e = 5;
	f = 6;
	g = 7;

	i = 1234;
	j = 305419896;
	k = 291;
	l = 4660;
	m = 74565;
	n = 1193046;


	/* g のアドレスを c_ptr に代入 */
	c_ptr = &g;

	/* c_ptr が指しているアドレスと、
	 * そのアドレスに格納されている内容を表示 */
	/* printf("c_ptr = %p   *c_ptr = %x\n", c_ptr, *c_ptr); */

	/* c_ptr は変数なので演算ができる */
	c_ptr ++;  /* c_ptr を「1」ずらす */

	/* 何が表示される ? */
	/* printf("c_ptr = %p   *c_ptr = %x\n", c_ptr, *c_ptr); */

	/* 「*」演算子は後ろがアドレスであれば良い事の確認 */
	/* 出力されているものは何? */
	/*
	printf("c_ptr + 1 = %p   *(c_ptr + 1) = %x\n",
	       c_ptr + 1, *(c_ptr + 1)); */
	*/


	/* 同じ事を int でやってみよう */

	/* n のアドレスを i_ptr に代入 */
	i_ptr = &n;

	/* i_ptr が指しているアドレスと、
	 *そのアドレスに格納されている内容を表示 */
	/* printf("i_ptr = %p   *i_ptr = %x\n", i_ptr, *i_ptr); */

	/* i_ptr は変数なので演算ができる */
	i_ptr ++;  /* i_ptr を「1」ずらす */

	/* 何が表示される ? */
	/* printf("i_ptr = %p   *i_ptr = %x\n", i_ptr, *i_ptr); */

	/* 「*」演算子は後ろがアドレスであれば良い事の確認 */
	/* 出力されているものは何? */
	/*
	printf("i_ptr + 1 = %p   *(i_ptr + 1) = %x\n",
	       i_ptr + 1, *(i_ptr + 1));
	*/


	/* 配列とポインタ */
	/* 配列名だけで使うと配列の先頭の「アドレス」になっている */

	/*
	printf("string = %p\n", string);
	printf("&string[0] = %p\n", &string[0]);
	*/

	/* なので、配列名をポインタ変数に代入できる事になる */
	c_ptr = string;

	/*
	for (i = 0; i < 10; ++i) {
		string[i] = i;
		printf("i = %d   string[%d] = %d     *(string + %d)  = %d\n",
		       i, i, string[i], i, *(string + i));
	}
	*/


}

このプログラムを実行すると、例えば次のような出力を得る。 アドレス値(等号の右の部分)は、異なっていても構わない。

[user99@proge1]~/kadai09% ./pointer1
&a = 0xbfe117cf
&b = 0xbfe117ce
&c = 0xbfe117cd
&d = 0xbfe117cc
&e = 0xbfe117cb
&f = 0xbfe117ca
&g = 0xbfe117c9
&string[0] = 0xbfe117b0
&string[1] = 0xbfe117b1
&string[2] = 0xbfe117b2
&string[3] = 0xbfe117b3
&string[4] = 0xbfe117b4
&string[5] = 0xbfe117b5
&string[6] = 0xbfe117b6
&string[7] = 0xbfe117b7
&string[8] = 0xbfe117b8
&string[9] = 0xbfe117b9
&i = 0xbfe117ac
&j = 0xbfe117a8
&k = 0xbfe117a4
&l = 0xbfe117a0
&m = 0xbfe1179c
&n = 0xbfe11798
&c_ptr = 0xbfe11794
&i_ptr = 0xbfe11790

上の状態のとき、メモリ上の配置は次のようになっている事になる。

ポインタ変数 c_ptr はアドレスを格納するための「変数」であった。 したがって、当然 c_ptrに対しても、アドレスを参照する演算子&を作用させる事ができる。

memory map
つぎに、変数の宣言部分で
	char a, b, c, d, e, f, g;
	char string[10];
	int  i, j, k, l, m, n;

となっているところを、

	/* char a, b, c, d, e, f, g;*/
	char a, b, c;
	char string[10];
	int  i, j, k, l, m, n;
	char d, e, f, g;

などと書き換えて実行してみると、出力(変数の位置関係)はどうなるかを確認してみる。 (ProgrammingE の実行環境では、配置関係が変わるが、変わらない処理系も有る) たとえ、メモリ上の配置が変わったとしても、プログラムとしての動作は変わらず、 「変数」を導入する事で抽象化ができる事が確認できるはずである。

次に、宣言部分を元に戻し、後ろの方のコメントアウトされた部分を順に外して実行し見ていこう。

  1. 61行目: c_ptr に、char 型変数 g のアドレスを代入した後に、c_ptr の値と c_ptr の指し示すアドレスに格納されている値の両方を出力してみる。 c_ptr は、g のアドレスに、 *c_ptr は g の値になっているはずである。 (課題8の復習)

  2. 67行目: c_ptr も変数なので演算を行なう事ができる事はすでに説明した。 64行目では実際に、c_ptr の値を 1増やしており、その後の printf では何が表示されるかを確認してみる。 確認する際には、最初に表示される各変数のアドレスに注意する事。

  3. 71〜74行目: 今まで 「*」演算子は、ポインタ変数に作用すると説明してきたが、実際には「ポインタ」に作用する演算子である。 したがって「ポインタ変数の式」に対しても作用させる事もできる事を確認する。 ここでは、(c_ptr + 1) という「ポインタ変数の式」に対して * が演算されている。 結果はどうなるか? c_ptr の値自身は 64行目から変化していない事に注意。

  4. 84行目: 61行目と同じ事をこんどは int 型の変数を使って行なっている。

  5. 90行目: 67行目と同じ事であるが、 87行目で ++ を演算しているだけにも関わらず 90行目で表示される i_ptr の「値」はどれだけ増えているかに注意する事。 また、この「増分」は何に対応しているのか考えてみる。

  6. 95行目。71行目からの確認と同じ事の再確認である。 ただし、ここで式中で増やしている 「1」により、アドレスの値はどれだけ変化しているかに注意する事。

  7. 104, 105行目。配列名を添字をつけずに参照すると、第0番目の要素のアドレスに なっている事の確認である。

    さらに、このことと、5番、6番から判った、ポインタ変数の増減は、その変数が 指し示す型に応じて増減し、増減の1単位はその型のサイズになっていると言う事を 合わせて考えると、ポインタと配列がにている事に気づいただろうか?

    実際、「配列名」はポインタなのである。(変数ではないので代入はできない。) 109行目で、ポインタ変数への代入を行なっているが、警告も出ないはずである。

  8. 112行目からは、添字を経由して配列として扱うと同時に、 ポインタ変数として * 演算子を利用して扱い、どちらでも問題ない事を確認してみる。

関数の引数

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

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

int function(int);

int main()
{
        int a;

        /* main 関数で値を設定する */
        a = 1;

        /* 出力は当然上で設定した値。 */
        printf("main 1: a = %d   &a = %p\n", a, &a);

        /* 関数呼出を行なう */
        function(a);

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

int function(int a)
{
        /*
         * main 関数で設定した値がでるはず
         * (main の a と function の a は別である事をアドレスで確認)
         */
        printf("func 1: a = %d   &a = %p\n", a, &a);

        /* 関数の中で a を変更してみた */
        a = 2;

        /* ここでは、変更した値がでるはず */
        printf("func 2: a = %d   &a = %p\n", a, &a);

        return 0;
}

実行してみると、次のような出力を得る。

[user99@proge1]~/kadai09% ./pointer2
main 1: a = 1   &a = 0xbfeb2f84
func 1: a = 1   &a = 0xbfeb2f60
func 2: a = 2   &a = 0xbfeb2f60
main 2: a = 1   &a = 0xbfeb2f84

function 関数で変更した内容は main 関数には伝わっていない。 その理由は、変数のアドレスを確認すれば一目瞭然で、違うメモリ領域に対する操作であるからである。 つまり、関数呼び出しの際に、引数の内容(値)が関数側で受ける変数にコピーされているのである。 そのため、呼出元では定数であっても関数内では変数として扱う事すら可能である。 このような引数の渡し方を「値渡し」と呼ぶ。

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

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

int function2(int *);

int main()
{
        int a;

        /* main 関数で値を設定する */
        a = 1;

        /* 出力は当然上で設定した値。 */
        printf("main 1:  a = %d   &a = %p\n", a, &a);

        /* 関数呼出を行なう */
        function2(&a);

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

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

        /* 関数の中で *a を変更してみた */
        *a = 2;

        /* ここでは、変更した値がでるはず */
        printf("func 2: *a = %d    a = %p\n", *a, a);

        return 0;
}

変更したのは、関数の引数をポインタとし、main 関数から呼出す際にアドレスを渡している点である。 (それにともない、function2 の中での a の操作も変更されている。) 実行してみると次のような結果が得られる。

[user99@proge1]~/kadai09% ./pointer3
main 1:  a = 1   &a = 0xbff2b234
func 1: *a = 1    a = 0xbff2b234
func 2: *a = 2    a = 0xbff2b234
main 2:  a = 2   &a = 0xbff2b234

function2 の場合、呼び出された関数で扱っている変数は main関数中の変数 a を指し示すポインタであるため、 呼び出された関数での変更を main関数にまで伝えることができている。 このような引数の渡し方を「参照渡し」と呼ぶ。 実際に操作の対象は同じメモリアドレスである事が確認できる。

厳密にいえば、C 言語は、値渡しの言語であり、ポインタを使った「参照渡し」は、 値渡しを用いた参照渡しのエミュレーションでしかないと考えるべきである。 が、ProgrammingE の範囲内では気にする必要はない(と思う)。


文字列の表現

文字列は、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$