第17問の答え

答え

これだと文字列の終端に'\0'が付かない場合がある!
  char truncatedText[length];
  strncpy(truncatedText, text, length);
文字列が確実に'\0'で終わるようにするのが正解!
  char truncatedText[length + 1];
  truncatedText[length] = '\0';
  strncpy(truncatedText, text, length);

解説

C言語では、文字列の終端に'\0'という特殊な文字を付ける決まりになっています。そのため、文字列を格納するには、表現したい長さ(文字数)よりも1バイト分だけ大きいメモリー領域が必要です。

'\0'は、いわゆる「ヌル文字」というやつね。
はい。そのへんは僕もC言語の入門書で勉強しましたよ。

次のようにして「文字列の長さ」と「文字列のサイズ」を表示してみれば、'\0'が付いている様子を理解できるでしょう。

サンプルコード
  char text[] = "Hello!";

  printf("Length of text = %zu\n", strlen(text));
  printf("Size of text = %zu\n", sizeof(text));
実行結果
Length of text = 6
Size of text = 7

問題のプログラムでは、次のようにして文字列を切り詰めようとしていました。

  char truncatedText[length];
  strncpy(truncatedText, text, length);

でも、これでは切り詰められた文字列に'\0'が付くとは限りません。strncpy()の仕様が次のようになっているためです。

  • 指定された長さの分だけ文字列をコピーするが、終端の'\0'は対象外
  • ただし、コピー元の文字数が足りないときは、残りを'\0'で埋める

つまり、文字列"Hello!"は6文字なので、終端に'\0'が付くのは長さ(length)に7以上を指定したときだけになってしまっているのです。場合によって'\0'の有無が変わるところが、少しややこしいですね。

このことを理解するために、実験としてプログラムに手を加えてみましょう。コピー先の配列の後方に、目印として1文字'X'を入れてみます。

サンプルコード
  char truncatedText[length + 2];
  truncatedText[length] = 'X';
  truncatedText[length + 1] = '\0';
  strncpy(truncatedText, text, length);
実行結果
length = 4: HellX
length = 5: HelloX
length = 6: Hello!X
length = 7: Hello!
length = 8: Hello!

strncpy()'\0'を付けてくれるかどうかは、長さによることが分かるでしょう。コピーする長さ(length)がコピー元の文字数(6文字)以下のときは'\0'が付かないので、文字列の終端を見つけられず後方の'X'まで表示されてしまっています。

今は実験として'X'の次に'\0'も入れてあるので安全ですが、問題のプログラムでは'\0'が見つかる保証はありませんでした。これは、配列のサイズを踏み越えて文字列が続いていると判断してしまうかもしれない不安定な状態です。その結果、プログラムは予期できない動作をし、場合によってはクラッシュする恐れもあったのです。

ここまで分かれば、あとは確実に'\0'を付けるにはどうしたらいいかを考えるだけです。方法は1つとは限りませんが、例えば次のようにすればいいですね。

  char truncatedText[length + 1];
  truncatedText[length] = '\0';
  strncpy(truncatedText, text, length);
ふぅ。今回の問題はけっこう難しかったですね。
そうね。難しく感じるのは、2つの要素が絡み合っているからじゃないかしら。
2つの要素……。'\0'strncpy()のことですか?
そういうこと!ごちゃ混ぜにしないで、それぞれを理解することが大切よ。
なるほどー。

今回の出題は、2つの要素が絡み合うものでした。

  • 文字列の終端には'\0'が必要なこと
  • strncpy()の仕様がややこしいこと

それぞれの要素について、きちんと理解しておくようにしましょう。そうすれば、今後似たような問題に出会ったとしても落ち着いて対処できますね。

ここがポイント!
文字列の操作では終端の'\0'を意識しよう!

修正後のプログラム

main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void PrintTruncated(const char *text, size_t length) {
  char truncatedText[length + 1];
  truncatedText[length] = '\0';
  strncpy(truncatedText, text, length);

  printf("length = %zu: %s\n", length, truncatedText);
}

int main(void) {
  for (int i=4; i<=8; i++) {
    PrintTruncated("Hello!", i);
  }

  return EXIT_SUCCESS;
}
実行結果
length = 4: Hell
length = 5: Hello
length = 6: Hello!
length = 7: Hello!
length = 8: Hello!