C++にはスタックとヒープという、使い方の違うメモリ領域が存在します。
rubyとかjavaではこのような違いは基本的に意識する必要が無いため、間違った使い方をしてしまう場合があります。

例えば、普通の変数はスタックに積まれるため、スコープを抜けるとたとえ使っていても破棄されます。

以下に、関数の中で文字バッファをスタックに確保し、その参照を戻す関数の間違った例を上げます。

#include <string>
#include <stdio.h>

char *getTextFilename(const char* basename){
  char str[1024] = {0};
  strncpy(str, basename, strlen(basename)+1);
  strncat(str, ".txt", 5);
  return str;
}
int main(void){
  char *test_filename = getTextFilename("test");
  printf("%s\n", test_filename);

  char *example_filename = getTextFilename("example");
  printf("%s\n", example_filename);

  printf("%s\n", test_filename);

  return 0;
}

環境によりますが、おそらく二回目のtest_filenameの出力がおかしくなると思います。
私の場合は以下のように、test_filenameにexample_filenameの内容が書き込まれていました。

test.txt
example.txt
example.txt

getTextFilename内のstrはスタックに確保されるので、関数終了時に解放されます。
そのため、戻り値の指し示す文字列は解放済みメモリとなり、勝手に変更される可能性があります。

上の例ではたまたま同じアドレスが再利用されたため、同じ文字列が設定されました。
ですが、間に様々な処理を実行した場合は、謎の値が書き込まれるなどがあり得るため注意が必要です。

対策

このような場合、いくつかの対策があります。

引数で渡す

1つ目が、strncpyやstrncatのように、メモリ利用域を引数として受け取る方法です。
これにより、自分のスコープから外れても値を保持することができます。
先ほどの例ですと、main側でcharの配列を確保して関数の引数でそれを受け取るといった形です。

確保したスコープを抜けるとやはり解放されますが、確実に解放されるためとても楽です。

ヒープに確保する

2つ目はmallocやnewでヒープ領域に確保する方法です。
以下はnewで配列を確保するように書き換えました。

#include <string>
#include <stdio.h>

char *getTextFilename(const char* basename){
  char *str = new char[1024];
  strncpy(str, basename, strlen(basename)+1);
  strncat(str, ".txt", 5);
  return str;
}
int main(void){
  char *test_filename = getTextFilename("test");
  printf("%s\n", test_filename);

  char *example_filename = getTextFilename("example");
  printf("%s\n", example_filename);

  printf("%s\n", test_filename);

 // 明示的に解放しないと絶対に解放されない
  delete test_filename;
  delete example_filename;

  return 0;
}

newやmallocでメモリ領域を確保した場合、ヒープ領域に確保され、スコープを超えてメモリを確保し続けられます。
ただし、明示的に解放しないとメモリリークが起きるため、注意が必要です。

オブジェクトをコピーする

3つめがコピーを使う方法です。
今回の関数では配列を使っていますが、コピーコンストラクタをもつstd::stringを使うと、
スタック上の内容を関数の外に安全にコピー出来ます。

#include <string>
#include <stdio.h>

std::string getTextFilename(const char* basename){
  std::string text(basename);
  text.append(".txt");
  return text;
}
int main(void){
  std::string test_filename = getTextFilename("test");
  printf("%s\n", test_filename.c_str());

  std::string example_filename = getTextFilename("example");
  printf("%s\n", example_filename.c_str());

  printf("%s\n", test_filename.c_str());

  return 0;
}

関数の戻り値は戻り先にコピーされるまでは保持されるという仕様があるらしく、
コピー可能なものに関しては戻り値としてちゃんと返すことが出来ます。
(これが無い場合、return 0の0も変更される可能性が出てきてしまうため、当然の仕様と言えます)

ただし、コピー処理が走るため、オブジェクトが巨大な場合、
メモリ消費が増えたり、時間がかかる可能性があるので注意が必要です。

# まとめ スタックで確保した場合、解放忘れ等がないためとても簡単ですが、
スコープから抜けると解放されてしまうため、有効範囲を考えないといけません。

今回は文字列で例を挙げましたが、自作クラスなどでもほぼ同じ事がいえます。
ただし、3つ目の方法はそのクラスと、メンバ変数として持ってるクラス全てにコピーコンストラクタを書いていないと、 中途半端にコピーされ、ß予期せぬエラーに繋がります。  

このエントリーをはてなブックマークに追加