C言語のポインタインクリメントの話
新年あけましておめでとうございます。 今年は特に勉学への時間を費やしたいと思います。
さて、最近は競技プログラミングというか一からプログラミングとコンピュータの仕組みについて改めて勉強しているので今回は そこでふと疑問に思った点について書いてみました。
それはC言語のポインタの仕様です。
仕様というよりはコンパイラの解釈の方なのですが・・・ なにはともあれまずは次のCで書いたソースをご覧ください
#include <stdio.h> #include <string.h> #include <stdlib.h> void A_reverse(char *str); int main(void){ char str[] = "abcdef"; A_reverse(str); return 0; } void A_reverse(char *str){ char* first=str; char* end=str; char tmp; int i,count=0; if(str){ while(*end){ ++end; } --end; while(str<end){ tmp = *str; *str++ = *end; *end-- = tmp; count++; } printf("%s\n",first); } }
このソースコード、まぁ読めばわかると思いますが文字列を逆に置換するものです。
str[]に文字を設定し、A_reverseメソッドでポインタを張り替え置換します。
ここで何が引っかかったかというと
while(str<end){
tmp = *str;
*str++ = *end;
*end-- = tmp;
count++;
}
このループ部分の
*str++ = *end; *end-- = tmp;
この辺りです。 まず、strには文字列の先頭のアドレスが、endには文字列の終端が格納されています。 endは上の処理でヌル文字の一個手前のアドレスを指すようにしています。(今回の文字列をわかりやすくすると'[a][b][c][d][e][f][¥0]'でendは'[f]'を指します)
そしてもう一つ説明としてこの処理が属するwhileループは先頭と終端からお互いのアドレスを張り替えていき、中央で張り替えを中止します。
a b c d e f str→a end→f
↓
f b c d e a str→b end→e
↓
f e c d b a str→c end→d
↓
f e d c b a
↓
strのアドレスがendより大きくなったのでここで終わり!
となっています。この張り替えの処理が
tmp = *str; *str++ = *end; *end-- = tmp;
です。
その時重要なのがポインタ演算子の評価順です。
上記のソースの *str++ = *end
の部分ですがこれだけパッと見ると、strをインクリメントしたアドレスにendのアドレスを代入する って僕は思ってました。
ですが、実際には
strの指すアドレスの内容をendのアドレスの内容に変更 → strインクリメント という流れらしいです。ややこしい
しかも最初のstr書き換えた後にstrをインクリメントしたらそれってendのアドレスから一個進むってことじゃねえの?って思ったら、なんと*str++
と書くことでインクリメント前の値を取り出して参照変更して複写、そしてアドレスをインクリメントするそうです
なんというかスマートなコードではありますが少しわかりづらいので結構議論のタネになったりするそうです。
こういう記法は後置記法というそうで、*++str
というような前置記法もできます。
この場合はインクリメントしてから値を取り出すという処理のようです。
これはコーディング問題の一つだったのですが思わぬ落とし穴があったので取り上げてみました。
ちなみにこれ以外にも落とし穴はあってメイン関数で宣言している str[] を *strで宣言したりするとプログラムがBus error 10という忌まわしい文字を吐いて落ちます。
というわけでC言語を書き始めて3年以上が経っても未だに嵌る時は嵌るといったものでした。1時間ぐらい調べました。恐るべしポインタ。
大学の授業ではそこまで詰まった記憶はないのですがやっぱりまだまだ詰めが甘いですね。 常に精進しろというお達しだと思ってがんばります。
それでは今回はこの辺りで筆を置かせていただきます。