①関数
関数を理解するには何といってもスタック(メモリ)の動作である。
詳しくはスタックの項にあるが、
void B(int b,int c) { int d=4; } void A(void) { int a=1; B(2,3); }
このような関数を呼び出したときに
| | | | | | |4 |d |復帰情報 |2 |b |3 |c |1 |a | |  ̄
このような形でスタックに積まれる。
よく見てみると引数b,cは復帰情報の前に書いてある。
ならばこれらは関数Aから参照できるのではないか?
ということでやってみた。
#include<stdio.h> void fanc(int b) { printf("b=%d\n",&b); b=12; } void main() { int a=0; printf("a=%d\n",*(&a+1)); fanc(a); } 結果 a=1245112 b=1245060
b=12の値をaのアドレスを増やすことで表示させてみたかった。
イメージ図 | | | | | | | | |復帰情報 |12|b |0 |a← これにアドレスを1足して12を表示する。 | |  ̄
だがどうにもうまくいかなかった。
しかしながらアドレスに1を足した「値」がアドレスっぽくなっていることからとりあえずアドレス計算が間違っていると判断する。
では実際a,b,cがこうなっていたらどんなアドレスをとるか見てみる。
#include<stdio.h> void fanc(int c) { printf("c=%d\n",&c); c=12; } void main() { int a=0; int b; printf("a=%d\n",&a); printf("b=%d\n",&b); fanc(b); } 結果 a=1245064 b=1245060 c=1245056
すると宣言が行われた順に「アドレスが減っている!」
これはスタックセグメントの特徴でヒープその他のセグメントは増えていき、
スタックセグメントに拡張されたデータが減っていくことでデータの衝突を抑えているからである。因みにローカル変数はスタックセグメントに格納される
スタックセグメント | ←ローカル変数を格納 |
------------------------ | ↓減っていく |
------------------------ | ↑増えていく |
ヒープセグメント | ←mallocの動的な値を格納。 |
------------------------ | |
BSSセグメント | |
------------------------ | |
データセグメント | |
------------------------ | |
テキストセグメント |
さて、原因が分かったところで先ほどの結果が
イメージ図 | | | | | | | | |復帰情報 |12|b |0 |a ← 12を表示したかった。 | |← つもりがここのアドレスを指してしまっていた。  ̄
ということがなんとなく理解だろうか?
従って*(&a+1)ではなく*(&a-1)にすればbの値を得るはずだ。
#include<stdio.h> void fanc(int b); void main() { int a=0; fanc(a); printf("a=%d\n",*(&a-1)); } void fanc(int b) { printf("b=%d\n",&b); b=12; } 結果 a=12
aの値でbを得ることが出来た。
イメージ図 | | | | | | | | |復帰情報 |12|b←この値が表示された。 |0 |a← これにアドレスを1引く。 | |  ̄
戻り値
関数において戻り値は非常に重要な概念である。
関数は戻り値を一つだけ呼び出し元に返すことが出来る。
return 0; return a;
等は見慣れている。
返さなくても良い。
そして戻り値の型で関数の型が決まる。
return 0;ならint fanc(){}
return a;でaがint型ならint fanc(){},char型ならchar fanc(){}
戻り値がないならvoid fanc(){}
といった具合である。
ここで戻り値がポインタならどうなるのだろうか?
この場合文字列を返すことが挙げられる。
当然関数もchar *fanc(){}のようになる。
#include<stdio.h> char *str(int a) { char buf[10]; sprintf(buf,"[%d]",a); printf("buf=%s\n",buf); return buf; } int main(void){ printf("main str=%s\n",str(100)); return 0; } 結果 buf=[100] main str=
int型を文字列として出力するのはキャストでこちゃこちゃやるよりsprintf関数を覚えたほうが早い。
といってもmainの方に帰ってこなかった。これはbufをローカル変数で宣言していることによる。
変数宣言の項でやったがローカル変数はそのブロックの終了と共に消える。従って、main関数に帰ってきたときには既にbufは消えてしまっているからだ。
ではstaticにするとどうなるか?
#include<stdio.h> char *str(int a) { static char buf[10]; sprintf(buf,"[%d]",a); printf("buf=%s\n",buf); return buf; } int main(void){ printf("main str=%s\n",str(100)); return 0; } 結果 buf=[100] main str=[100]
あっさりうまくいった。だがこれではこんな問題が生じる。
#include<stdio.h> char *str(int a) { static char buf[10]; sprintf(buf,"[%d]",a); printf("buf=%s\n",buf); return buf; } int main(void){ printf("main str1=%s\tmain str2=%s\n",str(230),str(100)); return 0; } buf=[100] buf=[230] main str1=[230] main str2=[230]
そう、mallocの項のfreeによる弊害に近い。
バッファが上書きされてしまうのだ。
つまり戻り値が一つしかないとはこういうことで戻り値のアドレスは常に同じなのだ。
別に打開策は単純で引数なら何度も取れるので引数をポインタで指定してやればよい。
#include<stdio.h> char *str(int a,char *buf) { sprintf(buf,"[%d]",a); printf("buf=%s\n",buf); return buf; } int main(void){ char *bufa,*bufb; printf("main bufa=%s\tmain bufb=%s\n",str(230,bufa),str(100,bufb)); return 0; } 結果 buf=[100] buf=[230] main bufa=[230] main bufb=[100]
ついでに結果からfanc(a,b)においてスタック上にb,aの順で積まれていることにも注目したい。
関数で文字列を返すのはやや初級から脱する。
変数がローカルなのか、グローバルなのか。
ヒープ上なのかスタック上なのかそれとも別の場所か。
しっかり把握しないと使うのは難しい。
関数をポインタにする
関数のアドレスを調べてみる。
#include<stdio.h> int fanc(){} int main(){ fanc(); printf("fanc=0x%p\tmain=0x%p\n",fanc,main); } 結果 fanc=0x00401150 main=0x00401155
mainとfancの違いが5バイト、
上の図を持ってくると
スタックセグメント | ←ローカル変数を格納 |
------------------------ | ↓減っていく |
------------------------ | ↑増えていく |
ヒープセグメント | ←mallocの動的な値を格納。 |
------------------------ | |
BSSセグメント | |
------------------------ | |
データセグメント | |
------------------------ | |
←main=0x...55 | |
テキストセグメント | ←fanc=0x...50 |
つまりfancは5バイトということが分かる。
ついでに色々増やしてみる。
#include<stdio.h> int fanc() { puts(""); } int main(){ fanc(); printf("fanc=0x%p\tmain=0x%p\n",fanc,main); } 結果 fanc=0x00401150 main=0x00401160
putsを入れたら16進なので12バイト増えた。
因みに中に定数を入れても変わらない。
定数はデータセグメントに入れられる。
#include<stdio.h> int fanc() { printf(""); } int main(){ fanc(); printf("fanc=0x%p\tmain=0x%p\n",fanc,main); } 結果 fanc=0x00401150 main=0x00401160
printf関数も12バイト増えた。
#include<stdio.h> int fanc() { int i; } int main(){ fanc(); printf("fanc=0x%p\tmain=0x%p\n",fanc,main); } 結果 fanc=0x00401150 main=0x00401155
宣言しただけでは変わらない。
まぁなんかそんな感じ。
では何がうれしいのか?というとアドレスがあるというのはすなわち「ポインタで言い換えれる!」ということである。
早速利用してみる。
#include<stdio.h> int fanc(int a) { printf("%d\n",a); } int main(){ int (*pfanc)(int);//*宣言*// pfanc=fanc; //*代入*// (*pfanc)(100); } 結果 100
関数も変数と同じように宣言することが出来た。
さらには配列と同じようにも扱える。
このとき必ず括弧でくくらなければ「戻り値の型がポインタ」とみなされてしまう。
さてこれを利用することによるメリットは結局何なのかという問題だが、
結果として構造体や配列などと組み合わせなければ利用価値はない。
使える場面としては
- GUI処理としてイベントがあったら関数を呼び出すというようなイベント駆動型のプログラム
- 割り込み処理プログラムに使われる。