スタックに確保した変数の有効範囲に気をつける
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つ目の方法はそのクラスと、メンバ変数として持ってるクラス全てにコピーコンストラクタを書いていないと、
中途半端にコピーされ、ß予期せぬエラーに繋がります。