見出し画像

C言語教室 番外編3 - 第6回の課題について

今回も投稿早々にAkio van der Meerさんから答案を提出して頂いてしまいました。毎回ありがとうございます。

(答案提出)C言語教室 第6回 - 文字列操作

答案に対するコメントは最後にするとして、さっそく課題の答え合わせをしてみましょう。今回の課題は前回の教室でほぼ説明してある内容なので、さほど苦労しなかったと思います。


課題1:文字列のコピーする関数を書いてください。

さっそくコードを書いてみます。

#include <stdio.h>

void string_copy(char *s, ch ar *d) {
 while ((*d++ = *s++) != '\0')
   ;
}

void main() {
  char s1[] = "abcdefg";
  char s2[8];

  string_copy(s1, s2);

  printf("s1:%s\n", s1);
  printf("s2:%s\n", s2);
}

関数の仕様としては最初の引き数で渡されてた文字列を次の引き数の領域にコピーする形にしました。大事なことは呼び出し側でコピーされる領域をきちんと確保してから関数を呼び出さないといけないことです。

s1:abcdefg
s2:abcdefg

char [] と char * の微妙な関係ですが、少し補足しておきます。

[] は定数で、この型の名前に対して参照することはできますが代入をすることはできません(式の右辺になれますが左辺になれません)。* は変数で参照することはもちろん代入することもできます(式の左辺にも右辺にもなれます)。関数を呼び出す時は参照するだけなので [] でも * のどちらでも使えます。呼び出される関数では代入されるので * しか使えません。同じ型の []から * への代入は安全な型変換で安心して使えます。

C言語の定数と言えば普通はプログラムに埋め込まれた数値や文字、文字列(それに列挙型の値)くらいなのですが [] は特別です。他には参照(&)も代入はできません。まだ説明していませんが const型修飾子が定数に見えるのですが、これはどちらかというと代入ができない(初期化はできる)変数と解釈したほうがわかりやすいです(型変換をすることで値が変更されることはあり得る)。いやぁC言語ややこしいですね。


課題2:文字列を連結する関数を書いてください。

コピーが書けたのであれば、連結も同じように書けますね。

#include <stdio.h>

void string_concat(char *s1, char *s2, char *d) {
  while ( *s1 != '\0' ) *d++ = *s1++;
  while ( *s2 != '\0' ) *d++ = *s2++;
  *d = '\0';
}

void main() {
  char s1[] = "abcdefg";
  char s2[] = "hijklmn";
  char s3[15];

  string_concat(s1, s2, s3);

  printf("s1:%s\n", s1);
  printf("s2:%s\n", s2);
  printf("concat:%s\n", s3);
}

このループの回し方だと最後にヌル文字をつけてあげる必要がありますね。

s1:abcdefg
s2:hijklmn
concat:abcdefghijklmn

さてAkio van der Meerさんからのコメントにあった確保した領域を超えて値を代入しても大丈夫にみえる件ですが、C言語では領域の管理はすべてプログラムに任されていて、作法を守らなくても何も教えてくれません。書いてある通りの領域に値は書き込まれ、その結果、何が起こるかは運次第です。

領域を壊していることを示すコードをどうしようかと考えたのですが、配列は連続した領域に確保されることは仕様で決まっているので、これを使ってみましょう。

#include <stdio.h>

void string_concat(char *s1, char *s2, char *d) {
  while ( *s1 != '\0' ) *d++ = *s1++;
  while ( *s2 != '\0' ) *d++ = *s2++;
  *d = '\0';
}

void main() {
  char s1[] = "abcdefg";
  char s2[] = "hijklmn";
  char s3[2][8];

  string_concat(s1, s2, s3[0]);

  printf("s1:%s\n", s1);
  printf("s2:%s\n", s2);
  printf("concat:%s\n", s3[0]);
  printf("happen:%s\n", s3[1]);
}

これを実行すると

s1:abcdefg
s2:hijklmn
concat:abcdefghijklmn
happen:ijklmn

と出力されるはずです。このようにs3[0]を超えてs3[1]の領域まで値が書き込まれていることが確認できると思います。一見s3[0]は予定通りに見えますが、s3[1]に何も処理をしていないのにも関わらず、値が書き込まれてしまっています。このようにコードに誤りがあるとまったく関係の無い領域が書き換わってしまうことがあるのがC言語の欠点であり危険なところです。

ローカル変数は一般的に宣言順にスタック上に確保されるのですが、必ずしもビッシリと割り当てるのではなく、普通はCPUにとって都合の良い隙間をあけて(イマドキのCPUであれば4または8バイト単位)割り当てることが多いです。ですので、運が良ければ少しばかり余計にメモリを使ってしまっても影響が無いこともシバシバです。所詮ローカル変数は関数の終了とともに捨てられてしまうので、もし一番最初か最後(これはケースバイケース)の変数が領域を超えていても誰にも迷惑をかけずに天寿を全うします。

C言語 スタックメモリ【ローカル変数が確保される仕組みを解説】

変数とメモリの関係

今回の例のような使い方をする時に、代入する文字列の領域を予め数えて確保しておくのは面倒ですし、使いにくいですよね。これについては次回の教室をお楽しみに^^;。

なお、今週は我が家の生徒たちが定期試験と発表のため、教室がお休みだったので次回は再来週になります。


答案に関するコメント

今回は完璧です。関数呼び出しの際は配列の名前のまま使っても大丈夫なことは説明しましたが、いったんポインタに代入しても問題はありません。また最後のケースでヌル文字を後から付与する必要が無いというご指摘ありがとうございます。判定前に代入が済んでいましたね。変形した時にそのままにしてしまいました(私もやらかしました^^;)。


ヘッダ画像は、以下のところのものを使わせていただきました。
https://photosku.com/photo/2615/


いいなと思ったら応援しよう!

kzn
頂いたチップは記事を書くための資料を揃えるために使わせていただきます!