05 月 29 日(金)1-2h

配列とポインタとアドレス

Cをマスターするため (CをCらしく使いこなし,効率的にプログラミングするため) には,ポインタの理解が不可欠だ. 今回は特に,気合いを入れて取り組むこと.

ただし, 前回までの内容を理解していなければ, 今回の内容を理解するのはとても難しい. 復習しながら取り組もう. (特に,前回の内容と強く関連している.)


ポインタとアドレス(1)

ポインタ(pointer)は, 他のデータのアドレスを記録するための変数である. ポインタを使うと, 任意のメモリ領域に記録されているデータへ間接的にアクセスできるので, 一個のポインタ変数だけで複数のデータを操作できるようになる. これをうまく活用すれば,効率的なプログラミングが可能となる.

一方,これまで使ってきた通常の変数では,データの値そのものを記録し, データへ直接的にアクセスする. つまり,一個の変数は一個のデータだけしか取り扱えない. 複数のデータを操作したければ, 複数の変数を用意し, それらに対応して, 複数のコードを書く必要がある.

直接アクセス(通常の変数)の場合, たとえば,変数 x は変数 x の値しか読み書きできない. これはまぁ,当たり前だが...

間接アクセス(ポインタ変数)の場合, その変数(ポインタ)は,他のどの変数のデータであっても読み書きできる. たとえば, 複数個の変数 xyz とか 複数の配列要素 a[0]a[1]a[2] の値を1個のポインタ変数 p だけを使って読み書きできる.

では,List 1 を試してみよう. これは,1個のポインタ p で 2個の変数 ab を間接的に操作するプログラムである.

List 1. ポインタの動作確認(簡易版)ptr1.c
main()
{
	int  a=1, b=2;
	int  *p;	// ポインタの宣言
	printf("初期値: a = %3d , b = %3d\n", a, b);

	p = &a;		// a のアドレスを代入(a を参照)
	*p = 100;	// a への間接的な代入
	printf("結果1: a = %3d , b = %3d\n", a, b);

	p = &b;		// b のアドレスを代入(b を参照)
	*p = 200;	// b への間接的な代入
	printf("結果2: a = %3d , b = %3d\n", a, b);
}

実行結果:

$  ./ptr1
初期値: a =   1 , b =   2
結果1: a = 100 , b =   2		# *pに代入したハズなのに?
結果2: a = 100 , b = 200		#   〃

ソースコードでは,p 以外の変数については, 書き換えていないように見える. しかし実際には, 変数 ab の値が書き換えられている. これがポインタによる間接アクセスの効果だ.

ちなみに,プログラミング言語のポインタは, 自然言語の指示代名詞(これ,それ,あれ,等)に似ている. 同じ語でも状況によって対象物が変わるという意味で.

なお,ポインタがアドレスを記録している状態について, そのアドレスにあるデータを「参照している」 or「指して(さして)いる」と言う.

また,ポインタ変数を宣言するときには, 変数名の前にポインタ宣言子*」を付ける. また,参照先(そのアドレスに記録されているデータ) にアクセスするときにも, 同じ記号の間接演算子*」を使う.

Cでは, 異なる意味(ポインタ宣言,間接アクセス,かけ算)に対して, 同じ記号「*」を使っており,混乱しそうだ. 状況に応じて,どの意味なのか?判別しよう.

データサイズ

一旦,話題を変えます...

List 2 を実行し, データの種類によるメモリ領域の大きさの違いについて, 確認しておこう.

List 2. データサイズの確認 size.c
main()
{
	char   vc, *pc;		// char 型の通常変数と char 型へのポインタ
	int    vi, *pi;		// int 型の通常変数と int 型へのポインタ
	double vd, *pd;		// double 型の通常変数と double 型へのポインタ

	printf("[通常変数のデータサイズ]\n");
	printf("sizeof(vc)  = %d\n", sizeof(vc));
	printf("sizeof(vi)  = %d\n", sizeof(vi));
	printf("sizeof(vd)  = %d\n", sizeof(vd));
	printf("\n");

	printf("[ポインタのデータサイズ]\n");
	printf("sizeof(pc)  = %d\n", sizeof(pc));
	printf("sizeof(pi)  = %d\n", sizeof(pi));
	printf("sizeof(pd)  = %d\n", sizeof(pd));
	printf("\n");

	printf("[ポインタの参照先のデータサイズ]\n");
	printf("sizeof(*pc) = %d\n", sizeof(*pc));
	printf("sizeof(*pi) = %d\n", sizeof(*pi));
	printf("sizeof(*pd) = %d\n", sizeof(*pd));
}
sizeof はデータサイズ(バイト数)を調べるための演算子. 使い方は,sizeof(変数名) の他,sizeof(型名) とかでもよい.

実行結果:

$  ./size
[通常変数のデータサイズ]
sizeof(vc)  = 1		# char 型  → 1 byte
sizeof(vi)  = 4		# int 型   → 4 byte
sizeof(vd)  = 8		# double 型→ 8 byteポインタのデータサイズ]
sizeof(pc)  = 8		# char 型へのポインタ
sizeof(pi)  = 8		# int 型へのポインタ
sizeof(pd)  = 8		# double 型へのポインタ

[ポインタの参照先のデータサイズ]
sizeof(*pc) = 1		# char 型
sizeof(*pi) = 4		# int 型
sizeof(*pd) = 8		# double 型

解説:

たとえ話:(倉庫内のダンボール箱)

ポインタとアドレス(2)

List 1 のプログラムの実行中,一体,メモリマップ内でどんな変化があったのか? 次に,List 3 のプログラムを利用して,詳しく調べてみよう. これは,List 1 を元にして,アドレス表示などを追加したものである.

List 3. ポインタの動作確認(詳細版)ptr2.c
main()
{
	int  a=1, b=2;
	int  *p;

	printf("[初期状態のメモリマップ]\n");
	printf("&a = 0x%08X : a = %d\n", &a, a);
	printf("&b = 0x%08X : b = %d\n", &b, b);
	printf("&p = 0x%08X : p = 0x%08X\n\n", &p, p);	// pの値はゴミ

	printf("[ポインタ p による変数 a への間接操作]\n");
	p = &a;				// a を参照
	printf("p = 0x%08X\n", p);	// a のアドレスを表示
	printf("*p = %d\n", *p); 	// a の値を表示
	*p = 100;			// a の値を書換
	printf("*p = %d\n\n", *p); 	// a の値を表示

	printf("[a の間接操作結果のメモリマップ]\n");
	printf("&a = 0x%08X : a = %d\n", &a, a);
	printf("&b = 0x%08X : b = %d\n", &b, b);
	printf("&p = 0x%08X : p = 0x%08X\n\n", &p, p);

	printf("[ポインタ p による変数 b への間接操作]\n");
	p = &b;				// b を参照
	printf("p = 0x%08X\n", p);
	printf("*p = %d\n", *p);
	*p = 200;			// b の値を書換
	printf("*p = %d\n\n", *p); 

	printf("[b の間接操作結果のメモリマップ]\n");
	printf("&a = 0x%08X : a = %d\n", &a, a);
	printf("&b = 0x%08X : b = %d\n", &b, b);
	printf("&p = 0x%08X : p = 0x%08X\n\n", &p, p);
}								

特に,p*p のちがいに注意.

それと,実験室の PC は 64bit OS なので, ポインタのサイズは実際には 8 byte(16進数で16桁)ではあるが, ちょっと長すぎるので,この授業では, 下位の 4 byte 分(16進数で8桁)だけを表示している.

実行例:(ソースコードと実行結果とを注意深く比較・観察しよう.)

$  ./ptr2
[初期状態のメモリマップ]
&a = 0xBFC738E4 : a = 1
&b = 0xBFC738E0 : b = 2
&p = 0xBFC738D8 : p = 0xXXXXXXXX	# p の値は初期化していないのでゴミ
# a, b, p は異なるメモリ領域を使用(p の値は初期化していないのでゴミ)

[ポインタ p による変数 a への間接操作]
p = 0xBFC738E4		# p は a を参照している(p = &a)
*p = 1			# *p は a と同体なので...
*p = 100		# *p を書き換えると...

[a の間接操作結果のメモリマップ]
&a = 0xBFC738E4 : a = 100	# ... a の値が変わる
&b = 0xBFC738E0 : b = 2
&p = 0xBFC738D8 : p = 0xBFC738E4	# a, b, p のアドレスは不変

[ポインタ p による変数 b への間接操作]
p = 0xBFC738E0		# (b については自分で考えてみよう)
*p = 2
*p = 200

[b の間接操作結果のメモリマップ]
&a = 0xBFC738E4 : a = 100
&b = 0xBFC738E0 : b = 200
&p = 0xBFC738D8 : p = 0xBFC738E0
なお,前回の配列によるメモリ破壊と同様に, ポインタの誤用・悪用によっても, メモリ破壊やシステム誤動作の危険性があることについても理解しておこう.

この実行例におけるメモリマップは Table 1 の通りである. メモリ内のデータ値がプログラムの実行中に変化していることに注意しよう. List 1 と実行例と Table 1 とをよく比較してみよう.

Table 1. メモリマップ表
アドレス範囲変数名初期値 →a操作後の値 →b操作後の値
0x BFC7 38D8 〜 38DFpゴミ 0x BFC7 38E4 →0x BFC7 38E0
0x BFC7 38E0 〜 38E3b2 2 →200
0x BFC7 38E4 〜 38E7a1 100 100

ところで,ポインタも変数の一種なので, 通常の変数と同様に,ポインタもデータ型をもつ. 通常は,参照先のデータの型と参照元のポインタの型とを一致させる必要がある. たとえば, 次のソースコード断片は間違い:

int    a;
double *p;

p = &a;		// 型が違うのでダメ

なお,コンパイル時にはエラーではなく警告(warning)が発生するだけなので, 型が一致していなくても実行できてしまう. しかし,この場合,実行はできても,その結果が意図した通りになるとは限らない.

たまたま無事に動いたとしても,それは正しいプログラムではない. また,最初は動いていたプログラムが, 途中で,実行時エラーによって強制終了される場合もある.

配列とアドレス

Cでは,配列名自身が先頭要素のアドレスを表わすことになっている. たとえば,配列要素 a[0] のアドレスについては,a とだけ書けばよい. 長たらしく &a[0] と書く必要はない.

また,配列のメモリ領域については, 前回確認した通り, 配列のすべての要素のアドレスは連続している.

つまり,これらのことから,配列の各要素のアドレスを調べるには, 実は,アドレス演算子「&」を使う必要はまったく無い. List 4 のプログラムを実行し,確認してみよう.

List 4. 配列とアドレスの関連性の確認 aryadr.c
main()
{
	int a[3];

	printf("%x %x\n", &a[0], a);	// 先頭アドレス
	printf("%x %x\n", &a[1], a+1);	// 1 番目の要素のアドレス
	printf("%x %x\n", &a[2], a+2);	// 2 番目の要素のアドレス
}								

実行結果からわかる通り, &a[i]a+i とは, まったく等価である. これからは,配列のアドレス計算には, & を使わず,短かく書くとよい.

注意:& を省略できるのは,配列のアドレスの場合だけだ. 通常の変数のアドレス計算には,& が必要.

それと,ソースコードでは +1 とか +2 したハズなのに, 実行結果ではなぜか,+4 とか +8 になってしまっている. この謎については,本日の練習問題とする.


配列とポインタ

配列の名前がアドレス値であり, また,アドレスの変数がポインタだった. すると当然,配列とポインタの間にも大きな関連性がある. List 5 のプログラムを実行してみよう.

List 5. 配列とポインタの関連性の確認 aryptr.c
main()
{
	int  a[5] = { 0, 1, 4, 9, 16 };	// 配列
	int  *p;			// ポインタ
	int  i;

	for (i = 0; i < 5; i++) {	// 配列への直接アクセス
		printf("a[%1d] : %d\n", i, a[i]);
	}
	printf("\n");

	p = a;
	for (i = 0; i < 5; i++) {	// 間接アクセス方法 #1
		printf("p[%1d] : %d\n", i, p[i]);
	}
	printf("\n");

	p = a;
	for (i = 0; i < 5; i++) {	// 間接アクセス方法 #2
		printf("*(p+%1d) : %d\n", i, *(p+i));
	}
	printf("\n");

	p = a;
	for (i = 0; i < 5; i++) {	// 間接アクセス方法 #3
		printf("*p : %d\n", *p);
		p++;
	}
	printf("\n");
}								

このように,配列要素へアクセスするには様々な方法がある. ここでは,特に重要な「間接アクセス方法 #3」の仕組を解説しておく:

実は,List 5 のように単純すぎる処理の場合, どの方法でも効率は大して変わらない. たとえば,方法#3 では, アドレスの足し算 a+i が不要になった一方, p++ が余分に必要となっていた.

しかし,より複雑な処理の場合, 同じ要素 a[i] に触る回数が増えるほど,効果も増す. たとえば,直接アクセス a[i] を n 回書くと, アドレス計算も n 回必要になる. 一方,関節アクセス *p なら何度書いても, アドレス計算は p++ の分の1回だけで済む.

チリも積もれば山となる.」 特に,最近話題のビッグデータの処理では,処理効率(計算速度)が重要. せっかく高性能なコンピュータがあっても, プログラムの効率が悪いと,台無しだ.

また,Fig.1 は,この方法 #3 でのメモリマップの変化のイメージである.

Fig.1. 配列要素への間接アクセス

教科書 pp.119-120

補足

List 5 や Fig.1 は,ひとつのポインタ変数 p によって, 複数の変数データ a[0]a[1],…,a[4] にアクセスできることを示している. つまり,「ポインタ=変数名を記憶する変数(変数の変数)」という考え方でも良い. (値を変えれば他の変数にアクセスできるような変数.)

しかしポインタは,実は,変数名の付けられていないメモリ領域にもアクセスできる. なのでやはり,「ポインタ=アドレスを記憶する変数」という考え方がより正しい. (無名メモリ領域については,後日どこかで説明したい.)


文字列の入出力

文字配列を使ってみよう. List 6 のプログラムは, 文字列をキーボードから入力し, char 型の配列に代入し, 画面に表示している. 本日の課題では,これを元にして,ポインタの練習プログラムを作成する.

List 6. 文字列の入出力 str.c
main()
{
	char  str[10];

	printf("文字列(9 文字以内) > ");
	scanf("%s", str);		// 文字列の入力

	printf("str = \"%s\"\n", str);	// 文字列の表示
}								
配列の要素数は 10 であるが, 正常に入力可能な文字数は 9 以内であることに注意. より長い文字列を入力すると,どうなるだろうか? そして,なぜだろうか? 後日,明らかになる予定.

なお,List 6 では,文字「"」自体を表示するために, エスケープ系列「\"」を使用している. 単独の文字「"」は特殊記号(文字列の開始・終了記号)であるため, そのままでは,文字列の途中では使えない. また,「%s」は,文字列の入力と表示の変換指定子である.


練習問題

  1. List 4 の配列 a の型を charintdouble に変更し, それぞれの実行結果について比較・考察せよ.
  2. 特に,a+1 しているにもかかわらず, 結果的には +1 ではない場合があるのはなぜか?
  3. List 6 を元にして,キーボード入力された文字列を 縦書き表示するプログラム tategaki を作成せよ.
  4. ただし,次の条件を満たすこと:

    実行例:

    $ ./tategaki
    文字列 > programming
    p
    r
    o
    g
    r
    a
    m
    m
    i
    n
    g
    

(c) 2015, yanagawa@kushiro-ct.ac.jp