ポインタって参照渡しのような機能なのか?(汗)
from Yendot…
コメントに「これはひどい…」とあったので、見てみるが…確かにこれはひどい
。タイトルは勢いあまってる程度とするにしても、解説の内容がとにかくひどい。つうか、サンプル動かねえw
C/C++のポインタの機能--参照渡しのような処理って、タイトルもさることながら、1ページ目から突っ込みどころ満載である。
ちょっと見てみよう。
ポインタ変数の宣言
ポインタ変数の宣言では、一般の変数の場合とは異なり、名称の先頭に*がつけられる。以下はnというポインタ変数を宣言する例である。ここでは、*nが表す値のデータ型はintとなるが、それ以外のデータ型でポインタ変数を宣言すれば、*nはその型の値を表すことになる。int *n; /* int型の値を表すポインタ変数nの宣言 */
int n; /* 一般の変数 n を宣言する場合 */
ここまではよい。単純に宣言の書き方と、その宣言が何を意味するか?ということを書いてあるからだ。
問題はその次からである。
続いて、*nを使って処理に用いる値を代入し、それを出力する例を示す。このときnには代入された値を記憶した場所(アドレス)が自動的に代入されるため、nそのものの値は実行されるごとに異なる可能性があるものの、*nが表す値は、処理中に変更されない限り5のままである。#include
int main( void ) {
int *n;
*n = 5; /* ポインタ変数nに値5を代入 */
printf( "%d\n", *n ); /* ポインタ変数nが持つ値(5)の出力 */
return 0;
}
えーと…どういえばいいのだろうか?
おそらく、#includeの後に何もないのは、<と>で囲まれた部分がタグであると解釈され、消し飛ばされたくらいに考えてるので、そこはご愛嬌(実際、HTMLのソースを見てみたら、<stdio.h>と書かれていた)。
問題は次である。
int *n;という宣言で実施されるのは、アドレスを格納するための変数(これをポインタ変数という)が宣言されているだけである。しかも単に宣言されただけである。
で、その次におもむろに *n = 5; とやっているが、これは「宣言されたnというポインタ変数で指し示される領域に、5という値(整数値)を入れる」という意味である。これは明らかにおかしい、というかまともに動くわけがない、と直感的に感じる。
オレが知らないうちにCの仕様が拡張されたか?とか間抜けなことを思いつつ、一応当該コードをコピペしてコンパイル(#include のあたりは普通に修正して)、起動すると、案の定以下のような感じに。
wakatono@kid:~/cnet$ !cc
cc sample.c
wakatono@kid:~/cnet$ ./a.out
Segmentation fault
wakatono@kid:~/cnet$
あたりまえである。
単に「アドレス入れる変数を宣言」して、確保された領域の先頭も何も教えずに、そこにデータを突っ込もうとしてるんだから、Segmentation faultでけられて当然である*1。
あまり筋がいいとは思わないが、たとえばこのプログラムは以下のような形で修正すれば、「とりあえずは」動くようになる。
ポインタ変数nを宣言し(int *n;)、nに対して、「malloc()で確保したintの大きさだけの領域の先頭アドレス」を渡し(n = malloc(sizeof(int));)、その後でそこの領域に5という値を入れる(*n = 5;)というふうにすれば、つじつまが合う。
int main( void )
{
int *n;
n = malloc(sizeof(int));
printf("%x\n",n);
*n = 5;
printf( "%d\n", *n );
return 0;
}
2ページ目についても同様の間違いがある。
int main( void ) {
int *n;
scanf( "%d", n ); /* *nの値をキーボードなどから入力(Enterで終了) */
printf( "%d\n", *n ); /* nの値を出力 */
return 0;
}
これもまた、同様の理由で動かない*2。
なんでこんな間違いをするのだろうか?と思ったが、次の文章にその答えが書いてあった。
プログラム内で用いる値は*nでを示しているのだが、nが示しているのは、その値を記憶させるメモリ上のアドレスだ。それ自体は自動的に設定されるため、開発者が具体的なアドレスを設定する必要はない。
???
これはおかしい。
初期化がすんでいない変数の値は不定である。そして、その領域が不幸にして使えない場合、結果はエラーとなる。使える場合には、値の代入が行われるわけだが、この結果は不定である。
実際、同じバイナリを別のLinuxカーネルなマシンの上で動作させたら、そちらでは動作してたりする。
あと、その次のプログラムはきちんと動く。
int main( void ) {
int n;
scanf( "%d", &n ); /* nの値をキーボードなどから入力(Enterで終了) */
printf( "%d\n", n ); /* nの値を出力 */
return 0;
}
これは、「すでに領域が確保された変数nのアドレス」を、scanf()で指定しているから、普通に動く。
以下は持論だが、ポインタを使うプログラムを書く場合には、ポインタを使うだけの理由が普通にあるのだから、どんな種類の値や、どういう関数/システムコールで取得した値が入るべきか、というのは気をつけるべき。でないと、思わぬバグを作りこむ結果になる。
そして、この記事で示されたプログラムだが、2ページ目の最後のもの以外は「実行結果が不定」という恐ろしいものになっている。
これが解説記事であるというのはちょっと認められない…。
2008/04/01追記:1ページ目が修正されました。
C/C++のポインタの機能--配列との関係…
これもひどい。
扱えないです…。
ポインタ変数と配列との深い関係を表す例を示そう。それは、配列の変数名をそのままポインタ変数名として扱えるということだ
配列の変数名は、配列の先頭アドレスを参照するためには使えても、それをそのまま変数としては扱えませぬ。
「変数名として扱える」のであれば、任意の値を入れることも可能なはず。ということで、検証してみました。
以下にサンプルソースを示します。これはあくまで「コンセプト」です。
これをコンパイルすると、以下のような結果が…。
main()
{
int n = { 1,2,3,4,5 };
int m = { 1,2,3,4,5 };
n = m;
printf("%d\n", *n);
}
5行目(n = m;)の部分で、エラーが出てはじかれる。
wakatono@kids:~/zdnet$ !cc
cc array.c
array.c: In function ‘main’:
array.c:5: error: incompatible types in assignment
array.c:6: warning: incompatible implicit declaration of built-in function ‘printf’
いいたかないが、のっけの説明から間違っている、というわけである。
あと、完全に筆者の方の理解が間違ってるところが…
ちなみに文字列は、sではなく*sに直接代入する記述も可能だ。以下に例を示す。両者とも処理の仕方は特に変わりないことがお分かりいただけるだろう。これは文字列のみに有効な記述であり、数値型でint *n = { 1, 2, 3 };などと記述することはできない。
オレが好かんだけならばまだしも、書いた人は「処理の仕方」としては、s = { 'H', 'e', 'l', 'l', 'o', '\0' } というのとはまったく違ってるということに気がついてないのでしょーか?
s="Hello"という記述は、Hello\0という文字列(6バイト)を格納する領域へのポインタを「初期化時に渡している」という意味であり、s = { 'H', 'e', 'l', 'l', 'o', '\0' } ってのは、s[0]、s[1]、s[2]、…、s[5]にそれぞれの文字コードを代入している、ということに気付いてないのでしょうか?
追記
sorry.間違いでしたorz
ありがとうございます>通りすがりさん
JISX3010を見て、仕様としては同じ&コンパイラの実装として異なるコードを出力することがある、ということも理解できました。
さらに面白いこともわかりました…。
アドレス値を
これについて、前者と後者について、アセンブリ言語に展開された例を示します。
s="Hello"と書いた場合のアセンブリ言語での展開例は以下のとおり(printf()の実行まで)
.file "array2a.c"
.section .rodata
.LC1:
.string "%c\n"
.LC0:
.string "Hello"
.text
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $36, %esp
movl .LC0, %eax
movl %eax, -10(%ebp)
movzwl .LC0+4, %eax
movw %ax, -6(%ebp)
movzbl -10(%ebp), %eax
movsbl %al,%eax
movl %eax, 4(%esp)
movl $.LC1, (%esp)
call printf
見ればわかるが、text領域に配列の初期化処理が入ってるか、それともデータ領域にHello\0というデータが入っていて、それを参照しているかがすでに違っている。
s = { 'H', 'e', 'l', 'l', 'o', '\0' } と書いた場合のアセンブリ言語での展開例(最初のprintf()の実行まで)
.file "array2.c"
.section .rodata
.LC0:
.string "%c\n"
.text
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $36, %esp
movb $72, -10(%ebp)
movb $101, -9(%ebp)
movb $108, -8(%ebp)
movb $108, -7(%ebp)
movb $111, -6(%ebp)
movb $0, -5(%ebp)
movzbl -10(%ebp), %eax
movsbl %al,%eax
movl %eax, 4(%esp)
movl $.LC0, (%esp)
call printf
追記:どちらかというと、コンパイラのほうがわかりにくい内容を出力してるともとれます。
JISX3010では、
とあります。
char s="abc", t[3]="abc";は、要素を単純文字列リテラルで定義された“単なる”char型の配列オブジェクトsおよびtを定義する。この宣言は、
char s = {'a','b','c','\0'},
t[3] = {'a','b','c'}
と同一となる。配列の内容は、変更できる。一方、宣言
char *p = "abc";
は、pを“charへのポインタ”型として定義し、要素が単純文字列リテラルで初期化され、長さが4のcharの配列”型オブジェクトを指すように初期化する。pを用いてその配列の内容を変更しようとした場合、その動作は未定義である。
…アセンブリのコード上、.string "Hello"と書かれているのですが、"Hello"を「長さ6のcharの配列」と認識できなかった…
余談ですが、char s = {'a','b','c','\0' } と書かれてるほうは、似たような内容のアセンブリを出力する「異なるコード」をかけました。
意味としては全く違うんで、ちょっと気をつけます…。
main()
{
char a='a';
char b='b';
char c='c';
char d='\0';
printf("%s\n",&a);
}
と
main()
{
char a={ 'a','b','c','\0'};
printf("%s\n",&a);
}
は、同じアセンブラのコードを出力します。
あと、「数値型でint *n = { 1, 2, 3 };などと記述することはできない。」という記述があったけど、これは単に*nは入れ物の場所を示しているだけに過ぎず、この状態でブツをどうにもできない、というだけであり、この変数に「すでに宣言された配列のアドレスを入れる」ことは可能。
微妙に中途半端…。実際に、以下のコードはコンパイル可能だし、実行も可能。
追記:もっとも、この場合のo[0]とかo[1]とかの場合は、前述のJISX0301の規定では「動作は未定義」であり、動作するしないは完全に処理系依存という感じかな。
さらに追記:「動作が未定義」なのは、ポインタを用いて「配列の内容を変更する」場合ですね。よく読むべきでした。
<追記>
main()
{
int n = { 1, 2, 3 };
int *o = n;
printf("%d\n",o[0]);
printf("%d\n",o[1]);
printf("%d\n",o[2]);}
動作が未定義なのは、以下の場合ですね。
ちなみにgcc 4.1.2では、o[2]=10;で、o[2]の値を10に変更でけました。
main()
{
int n = { 1, 2, 3 };
int *o = n;
printf("%d,%d,%d\n",o[0],o[1],o[2]);
o[2] = 10;
printf("%d,%d,%d\n",o[0],o[1],o[2]);
}
通りすがりさん、ありがとうございます。
main()
{
int *n = "Hello;
printf("%c,%c,%c\n",n[0],n[1],n[2]);
n[2] = 'W';
printf("%c,%c,%c\n",n[0],n[1],n[2]);
}
ちなみにこれは、"Hello"という文字列リテラルが読み出し専用のdataセクション(.rodataセクション)に配置されるため、n[2]='W'という処理をやろうとするとSegmentation Faultで落ちます。
アセンブリにすると以下のとおり。
ちなみに、上記の.rodataを.dataに変更すると、Segmentation Faultは出なくなります(あたりまえですが)。時間が出来るとアセンブラばっかり見てるなぁ。
.file "mai.c"
.section .rodata
.LC0:
.string "Hello"
</追記>
もうそろそろ疲れてきたので終わりにしたいんだけど、最後に…
「複数の文字列を要素とした配列」って何?
さきほど*sで1つの文字列を表していたが、この例では*sによって複数の文字列を要素とする配列を表している。ただ、さきほどs[]でも*sでも1つの文字列を表すことができ、処理するにあたって特に変わりないことを説明したところだ。
複数の文字列「が格納されるアドレス」を要素とした配列ならばわかるけど、この書法では、アドレスしか配列にならねえです(汗
…疲れた…