第22問の答え

答え

ローカル変数のアドレスをreturnで返してしまっている!
const char *ToUpperText(const char *pText) {
  ……
  return UpperText; /* ← ローカル変数のアドレス */
}
ローカル変数のアドレスを返さないようにするのが正解!
void ConvertToUpper(char *pText) {
  ……
}

解説

これは、変数の寿命に関する問題でした。ローカルスコープ(関数の内側など)で宣言された変数には「寿命」があり、ずっと有効なわけではないのです。

問題のプログラムをコンパイルしてみたんですけど……
うん。どうだった?
それが、警告が出ちゃうんですよ。
でしょ。その警告が、今回のプログラムを直すためのヒントなのよ。
え?コンパイラの警告って、デバッグのヒントになるんですか?
もちろん!警告をちゃんと読めばね。
なるほど。じゃあ、読んでみますね。
えっと……警告の内容はこれか。
Address of stack memory associated with local variable 'UpperText' returned
ざっと訳すと、「ローカル変数UpperTextにあたるスタックメモリーのアドレスが返却された」ですかね。
そうそう。最近のコンパイラは警告も優秀よねぇ。昔はもっと大変だったんだから……
え、ユキ先輩っていったい何年前からプログラミングやってるんですか?
なぁに?よく聞こえなかったわ。
あ。なんかスミマセン。(気を取り直して……)「スタック」ってなんですか?
スタックというのは、一時的なデータが格納されるメモリー領域のことよ。

今回コンパイラが出した警告は、関数ToUpperText()return文に対するものでした。ここで、スタックのアドレスが返却されてしまっているという内容です。

const char *ToUpperText(const char *pText) {
  size_t length = strlen(pText);
  char UpperText[length + 1]; /* ← この配列はローカル変数なのでスタックに確保される */

  for (int i=0; i<length; i++) {
    UpperText[i] = toupper(pText[i]);
  }
  UpperText[length] = '\0';

  return UpperText; /* ← ここでスタックのアドレスを返してしまった! */
}

関数の内側にあるローカル変数は、その関数が呼び出されている間しか使わない一時的なデータなので、スタックに確保されます。そして、関数の実行が完了するときに解放されます。ローカル変数は、役目を終えると自動的に消滅するということです。

今回のプログラムでは、returnでローカル変数の配列を返そうとしていました。でも、実際に返却されるのは配列全体ではなく、そのアドレスです。関数の呼び出し元からこのアドレスにアクセスしても、もう配列は存在しません。

なるほど。いくらアドレスを覚えていても、消滅してしまったデータにはアクセスできないわけですね。
そういうこと。
でも、「消滅する」といっても、本当にすぐメモリーからデータが消えちゃうんですか?
わざわざメモリーをクリアするわけではないから、returnした直後なら残ってるかもしれないのだけど……
ふむふむ。
解放されたメモリーは次の関数呼び出しで真っ先に再利用されるから、実質的にはすぐに消えちゃうわね。
そうなんですね。じゃあ、自動的に消滅しないようにするには……malloc()を使えばできますよね!
うん。それも一つの正解ね。でも、別の方法も考えてみるといいかもよ。

malloc()は、スタックではなく「ヒープ」と呼ばれる領域からメモリーを確保します。そのため、ローカル変数のように自動的にデータが消滅することはありません。今回のプログラムを修正するには、ヒープを使うのがもっとも直接的な方法だといえるでしょう。

でも、ヒープを使うと、気を配らなければならない点が増えてしまいます。

  • メモリーの確保に失敗する(malloc()が0を返す)ケースに対処しなければならない
  • メモリーを解放する(free()を呼び出す)タイミングについて考えなければならない

そこで今回は、別の方法を考えてみましょう。関数の仕様を変更するのです。

以下は、問題のプログラムから、関数ToUpperText()がどのように使われているのかが分かる部分だけを抜き出したものです。

const char *ToUpperText(const char *pText) {
  ……
}

int main(void) {
  char text[] = "The C Programming Language";
  puts(text);
  puts(ToUpperText(text));
  ……
}

この関数を、既存の文字列を上書きする仕様に変更したらどうなるでしょうか。

void ConvertToUpper(char *pText) {
  ……
}

int main(void) {
  char text[] = "The C Programming Language";
  puts(text);
  ConvertToUpper(text); /* 元のテキストを上書きして大文字に変換 */
  puts(text);
  ……
}
新しい文字列をreturnで返すのをやめて、引数の文字列そのものを変換する仕様にしたのが分かるかしら?
はい。関数の仕様が変わったから、呼び出し側にも少し変更が入ってるんですね。
そうそう。
えっと、引数からconstがなくなってますけど、これはたしか……
それは第04問でやったわね。
そっか。思い出しました!
それじゃあ、おまけの問題。仕様を変更した関数の中身がどうなるか考えて、修正後のプログラムを仕上げてみてね。
はーい、やってみます!
ここがポイント!
ローカル変数には「寿命」があって、自動的に消滅する!

修正後のプログラム

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

void ConvertToUpper(char *pText) {
  size_t length = strlen(pText);

  for (int i=0; i<length; i++) {
    pText[i] = toupper(pText[i]);
  }
}

int main(void) {
  char text[] = "The C Programming Language";
  puts(text);
  ConvertToUpper(text);
  puts(text);

  return EXIT_SUCCESS;
}
実行結果
The C Programming Language
THE C PROGRAMMING LANGUAGE