わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

strcatとstrcpyの違いにケリをつける

一昨年の暮れに書いたstrcatとstrcpyの違い - わさっきについて,ちょくちょくアクセスがあります.
思うことあって,そこに挙げた問題(4つのソース)に,解説をつけることにしました.
まず,strcat(s,t) を実行したときのメモリの状態を,「素朴な解釈」として図にしてみます.

ちなみに「素朴な」という修飾語がついたら,より精密な解釈なり手法なりがあると考えるのが普通で,このエントリでも,あとで別の解釈を示します.
ともあれ,同様に strcpy(s,t),strcat(s+1,t),strcpy(s+1,t) を図にしてみましょう.



こうして見ると,strcat(s,t) と strcat(s+1,t) を実行したときの振る舞いが同じなのが気になります.引数の違いを,図でどのように表せばいいのでしょうか?
ここで,4つのプログラムの前に書いたことを見直します.

strcatは文字列を連結する関数,strcpyは文字列をコピーする関数です.

ともに引数を二つとります.オンラインマニュアルなどを見て知ることのできる「仮引数」は,文字列の先頭を指すポインタ変数になります.

strcatとstrcpyの違い - わさっき

太字のところが重要です.ここでは仮引数(関数の側の変数)について言及していますが,実引数,すなわち呼び出し側も,同じ型でなければならないので,実は4つのプログラムで引数に与える「s」または「s+1」は,ポインタでなければならないのです.
関数の呼び出し関係から,いわば逆算して,「s」と「s+1」がポインタであるともっていきましたが,この流れは自然ではなく,ここは,なじみの『C言語によるプログラミング―スーパーリファレンス編』を見直しまして…p.319の『左辺値以外に用いられた配列名は,その配列の先頭要素を指すポインタに変換される』というルールを適用したと考えるほうが適切です.
ともあれ,引数としての s と s+1 がポインタであることがわかるように,図に書き加えてみることにします.ついでに,t は明らかにポインタ変数ですので,「配列領域」は黒箱で,「ポインタが指す領域」は青箱で描き分けることにします.strcat(s+1,t) は次のようになります.

なお,黒箱と青箱が重なるところで,青箱をずらしていますが,これは見る際の利便というもので,配列の要素としては,ぴったり重なります.
この図のように,strcat(s+1,t) という呼び出しをすると,strcat の内部では,s+1 が指し示す,"bc" と等価な(しかも書き換え可能な)文字列領域があって,その末尾に,t が指し示す文字列 "123" を付け加えて「連結」をします.そこでは s[0] の値に関心が払われません.'a' であろうが,'\0' であろうがかまいません.
そして strcat(s+1,t) の処理を終えると,main関数の中では,s+1 という式は出てきません.printfを使って,配列 s*1の中身,すなわち図にあるように「abc123」(と改行)を出力することになります.ここがもし「printf("%s\n", s + 1);」だったら,「bc123」(と改行)が出力され,これは文字列の連結として正しい振る舞いです.
strcat(s,t) はどういう図になるか…こうです.

ここでも,strcatの引数に渡す際には,ポインタになります.s は配列変数ですが,いくつかの例外を除き,式の中で s はポインタになること,すなわち s のダブルミーニングが,この問題を理解する鍵なのでした.
この描き分けができたら,strcpy(s,t) と strcpy(s+1,t) の図も同様ですので,つけておきます.

*1:ここの s も,「配列 s の先頭要素を指すポインタ」となります.