パソコン活用研究シリコンバレー(C、C++、の活用研究)

ポインタ変数の取り扱い


ポインタ変数はたいへん便利ですが、ぼんやり使用していると、とんでもないミスの元になります。
初級レベルの時代に、どうも想定どおりにプログラムがうごかない、なんかバグてるけど、どこがまずい
かわからない、と言うときにはまずポインタ変数の使い方を疑ってみるのがいいでしょう。
特にBASICをバリバリに使いこなしてCを使い始めた人は、それなりに大きな凝ったプログラムをサクサク
作ってしまうでしょうが、気をつけないとポインタ変数のところでヘマするかもしれません。
特にBASICで、変数を宣言しなくてもいい、初期化しなくても自動的に初期化しておいてくれるという、
BASICの楽チンなところに慣れている人は、「C言語では変数は必ず宣言し、初期化する」と肝に銘じて
おくのがいいと思います。
今回は、ポインタ変数を宣言した時、値を代入した時にポインタ変数のポインタ(アドレス)と実際の
データがどう準備されるのか、を見てみます。

1 ポインタ変数は初期化すべし

まずは、ポインタ変数を初期化しないでいるとバグ、ミスを引き起こす元になる、という例について
とりあげてみます。
以下のプログラムはポインタ変数*a, *b, *cをchar型で宣言し
@初期化してない状態で、printfでポインタ変数の値と、ポインタ(アドレス)を表示
Aaに値を代入(初期化)
Bscanfでbに値を代入した状態でprintfでポインタ変数の値と、ポインタ(アドレス)を表示
Cscanfでcに値を代入した状態でprintfでポインタ変数の値と、ポインタ(アドレス)を表示
Db = a とした状態でprintfでポインタ変数の値と、ポインタ(アドレス)を表示
させます。

#include "stdio.h"

int main(int argc, char* argv[])
{    
    char *a,*b,*c;
printf("%d, %s, %d, %s, %d, %s\n",a,a,b,b,c,c);  /*@*/
a="abcde";                                       /*A*/               
scanf("%s",b);
printf("%d, %s, %d, %s, %d, %s\n",a,a,b,b,c,c);  /*B*/
scanf("%s",c);
printf("%d, %s, %d, %s, %d, %s\n",a,a,b,b,c,c);  /*C*/
b=a;
printf("%d, %s, %d, %s, %d, %s\n",a,a,b,b,c,c);  /*D*/

}

(1) LSI-C
まずはLSI-Cでコンパイルした場合の実行画面です。
@の行で、a,b,cの値が未定義という警告はでますがコンパイルはできます。
(他のコンパイラでコンパイルすると別の挙動になります。この後にBorland C++, Visual C++の例を
掲載します)


D:\win95\C>lcc pointa3.c
pointa3.c 6: 警告: a の値が未定義
pointa3.c 6: 警告: b の値が未定義
pointa3.c 6: 警告: c の値が未定義
lld @link.i

D:\win95\C>pointa3
5540, ナ・5i|、コヌ・f・・・6z・・セバ・,@Om, 5540, ナ・
5i|、コヌ・f・・・6z・・セバ・,@Om, 5540, ナ・5i|、
コヌ・f・・・6z・・セバ・,@Om
xyz
126, abcde, 5540, xyz, 5540, xyz
ghj
126, abcde, 5540, ghj, 5540, ghj
126, abcde, 126, abcde, 5540, ghj

@からDの状態でそれぞれのポインタ変数のポインタと値(データ)についてまとめてみます。

aポインタ aデータ bポインタ bデータ cポインタ cデータ
@ポインタ変数宣言(初期化せず) 5540 ゴミ 5540 ゴミ 5540 ゴミ
Aaに"abcde"代入 126 abcde 5540 ゴミ 5540 ゴミ
Bbに代入 (scanfで) 126 abcde 5540 xyz 5540 xyz
Ccに代入 (scanfで) 126 abcde 5540 ghj 5540 ghj
Db = a; 126 abcde 126 abcde 5540 ghj

@ポインタ変数を宣言した段階では、ポインタは5540を差しており、ここはゴミが入っています。
5540が何かは説明できませんが、まあ、ポインタ変数が特定のアドレス(住所)を持たないとき
(=初期化されていない=特定のデータが代入されていない)住所不定のときにとりあえず差す
アドレスと考えればいいでしょうか。
すなわち、まだポインタ変数は使えない状態です。BASICプログラマはこの点注意して下さい。
変数を宣言しただけで、初期化されている(数値変数なら0, 文字変数なら空文字)というBASIC流で
いると、とんでもないバグのもとになります。
ポインタ変数は、できるだけ初期化(すなわち定数を代入)して使うようにするといいです。

Aa に"abcde"を代入し、ポインタ変数*aは初期化されました。ポインタもデータも決定されました。

Bscanfでbに文字列"xyz"を入力してみました。すると、bの値(データ)は"xyz"になりましたが、ポインタ
は5540のままでした。cのポインタも5540のままなので、cの値も"xyz"になってしまっています。

Cscanfでcに"ghj"を入力してみました。bの値が"ghj"になってしまっています。
このプログラムはLSICでコンパイルしたので、他のC言語コンパイラだと違う仕様なのかもしれませんが、
scanfで値を入力してもポインタ変数のポインタは5540のままで、いわゆる一般的な代入(初期化)である
a = "abcde" とは違っています。すなわち、scanfで値を入力しても、変数を初期化したことにはなって
いない、
と言うことです。

BASIC流に言うとまだ値の代入されていない(宣言だけされちる)、変数b,cがあって
Input b
とした時に、なにもいじってないcの値がbと同じになってしまう、という妙な現象が発生しているわけです。

ここら辺の挙動は、C言語で初心者時代にバグを出しやすいところです。いずれにしろ、変数は初期化して
使え
、ということです。

Dこれは通常のポインタの代入ですが、文字列の場合はここも注意点です。BASICでは文字変数a,bの
代入でb = aと記述しますが、C言語では、文字列bに文字列aをコピーする場合は、strcpy(b, a)という
関数を使います。Dの結果をみると、一見 b = a で問題なさそうに見えますが、C言語では
strcpy(b, a) とb = a の結果は異なります。b = a はポインタの代入なので、bのポインタにaのポインタ126
が代入されています。その結果としてbのデータも"abcde"になっています。すなわちaもbも同じポインタを
さしている状態(ポインタ126)です。この状態では、aまたはbの値を変化させると他方の変数の値も同時
に変わることになってしまいます。
strcpyでは、もちろん値のみのコピーなので、a, bは異なるポインタを持っています。
strcpy(b, a) と b = aの違いについて理解できたでしょうか。特に文字列の扱いはBASICに慣れている
人にはバグを出しやすい部分なので、十分に理解してもらいたい点です。


(2) Borland C++
Borland C++ 5.5 でコンパイルすると少し違った挙動になります。

@ポインタ変数宣言(初期化せず) 
3235840, , 4247748, , 0, (null) となっていますが、
宣言をした段階でポインタ変数のアドレスが用意されたようにも見えます。 ただし、3つめのポインタ変数*cの
アドレスが0で値が(null)となっています。

Aaに"abcde"代入すると、ポインタ変数*aのアドレスが4247884になり、abcdeの値が保持されています。
*bに割り当てられたアドレスから136後ろの番地で、このあたりは変数の値が保存される領域のように見えます。

B*bにscanfで代入すると、4247748番地からxyzが保存されました。このあたりの挙動がLSI-Cと異なります。

C*cにscanfで代入したらプログラムが異状終了しました。

c:\bcc55\Bin>pointa3
3235840, , 4247748, , 0, (null)
xyz
4247884, abcde, 4247748, xyz, 0, (null)
ghj

c:\bcc55\Bin>

(3) MicroSoftのVisual C++ 2022
scanfを使うとセキュリティ強化版のscanf_fを使えというエラーが出るので、ソースコード1行目に
#pragma warning(disable: 4996)
と記述してscanfを使えるようにしておく。

#pragma warning(disable: 4996)
#include "stdio.h"

int main(int argc, char* argv[])
{
char *a, *b, *c;
printf("%d, %s, %d, %s, %d, %s\n", a, a, b, b, c, c);
a = "abcde";
scanf("%s", b);
printf("%d, %s, %d, %s, %d, %s\n", a, a, b, b, c, c);
scanf("%s", c);
printf("%d, %s, %d, %s, %d, %s\n", a, a, b, b, c, c);
b = a;
printf("%d, %s, %d, %s, %d, %s\n", a, a, b, b, c, c);

}

コンパイルすると3つのエラーと38個の警告
エラーは、printf("%d, %s, %d, %s, %d, %s\n", a, a, b, b, c, c); で初期化されてない、a,b,cが
使われているというもの。
警告はprintfの中で文字列の表示なのに、%dを指定しているというもの。


ポインタ変数を初期化せずに使おうとした時の挙動は、コンパイラによってかなり
異なることがわかります。




2 ポインタ変数の初期化についての注意点

ポインタ変数を宣言時に初期化することもできますが、この時の記述の仕方はちょっと注意が必要です。

まずは、以下のプログラムを見てください。
pointa4.c

#include <stdio.h>

main()
{ int i;
int *x;

i=5;
*x=i;

printf("*x %d: x %d: i %d: &i %d",*x,x,i,&i);
}

ポインタ変数 int *x; を宣言しておいて、後で*xに値を代入しています。これはLSIC86では警告は出ますが、コンパイルは
できてしまいます。Borland C++55では警告も出ずにコンパイルできてしまいますが異常終了します。

LSIC86でコンパイルして実行。
ポインタxの値は初期化していないので警告が出ている。
*xは5になっているが、xの値(ポインタ)は4196で、これはちゃんと初期化された値ではないので、このまま使うのは
よろしくない。



この場合は、x=&i のようにポインタ値をきちんと初期化して使わなければならない。


では次のプログラムを見てください。
pointa5.c

#include <stdio.h>

main()
{ int i=5;
int *x=i;
int *y=&i;

printf("%d ,%d",*x,*y);
}

宣言時に横着して?、初期化してしまおうというコードです。
普通の変数の宣言 int i=5; は問題ありません。
次に、ポインタ変数の宣言ですが、 int *x=i; int *y=&i; のどちらが正しいでしょうか。
pointa4.c から考えると、int *x=i; が正しいように見えます。文法的には、これが正しく思えますが
宣言時は、int *y=&i; としないと正しく初期化されません。
実行例をみてみます。

D:\win95\C>pointa5
18771 ,5

int *x=i; とすると、x (すなわちポインタ)に5が代入されて、*x はアドレス5番地に格納されている値
18771 となっているようです。
宣言時に同時に初期化する場合は、int *y=&i; のようにするのがC言語の正しい仕様です。

ここは、C言語の仕様が錯綜している感のある部分ですが、確かに char *c ="abcd";
とポインタ変数に文字列を宣言したときも、ポインタc には、文字列"abcd"の先頭アドレスが
格納されるという仕様になっています。

ここら辺は、ポインタ変数を使うとき、ミスやバグを誘発しやすい点です。C言語の書籍にも
あまり注意して書かれていません。おじさんがC言語でポインタ変数を使い始めたとき、
どうも挙動がおかしいという時は、ここらあたりに問題がありました。
C言語の仕様としてちょっと錯綜していて間違いやすい部分です。そういう仕様と割り切って使い
ましょう。

再度おさらいしてみると、
int *x;
と宣言すると、ポインタ変数*x のポインタxを格納するためのメモリーが確保される。
ただし、まだポインタxの値は未定(ゴミ)。ポインタ自体が未定(ゴミ)だから、*xの値はゴミ。

int i=5;
int *x=&i;
のように宣言すれば、ポインタxは初期化されて、ポインタxが決まり、そのポインタの差す先のメモリーに5が格納される。
というわけです。

あるいは
int i; *x;
と宣言だけしたら
i=5;
x=&i;
とする。


いずれにせよ、バグを出さないためには、ポインタ変数は初期化して使えということです。



3 ポインタ変数の注意点のまとめ

C言語で*(アスタリスク)は
(A) 乗算 5*3
(B) ポインタ変数の宣言 int *a;
(C) ポインタ変数の値を取り出す間接演算子 *a
の3つの使い方があって、(B)は(C)も同じように見えまずが、実際は違う機能です。

(B)のポインタ変数の宣言の int *a; としたときに宣言されているのは、*aなのかそれともaなのか
あるいは両方なのかがわかりづらいのですが、この時宣言されているのはaです。
つまりポインタ(アドレス)が宣言されています。

TopPage