ローカル変数への参照を返すとSegmentation faultになる原因
関数のローカル変数への参照を関数の外に返してしまうバグです。関数を抜けた時点でローカル変数の実体は破棄されるため、返された参照は存在しないメモリを指すことになります。
エラーメッセージの読み方
Segmentation fault
(エラーメッセージなし)- これはプログラム自身の出力ではありません。プログラムは何も言わずに落ちています
Segmentation fault- シェルが検知した異常終了の種類 — OSが「不正なメモリアクセス」などを検出しプロセスを強制終了させました
見つけ方- このメッセージだけでは行番号すら分かりません。原因はコードを読んで特定するしかありません
このエラーが出る典型パターン
パターン1
1 #include <iostream> 2 int& getRef() { 3 int local = 42; ^ 4 return local; 5 } 6 int noise() { 7 int junk[8] = {1,2,3,4,5,6,7,8}; 8 return junk[0]; 9 } 10 int main() { 11 int& r = getRef(); 12 noise(); 13 std::cout << r << std::endl; 14 return 0; 15 }
getRef()が返した参照は、関数を抜けた時点で破棄されたローカル変数localを指したままです。直後に別の関数noise()を呼ぶと、そのスタックフレームが同じメモリ領域を上書きするため、rを読んだ瞬間にクラッシュします。localをstaticにすればプログラム終了まで実体が残り続けるため、参照を安全に返せます。
直し方: int を static int にします。
パターン2
1 #include <iostream> 2 int& getValue() { 3 int temp = 99; ^ 4 return temp; 5 } 6 int fill() { 7 int buffer[8] = {9,9,9,9,9,9,9,9}; 8 return buffer[0]; 9 } 10 int main() { 11 int& v = getValue(); 12 fill(); 13 std::cout << v << std::endl; 14 return 0; 15 }
g++はこの種のミスに対して-Wreturn-local-addrという警告を出しますが、あくまで警告でありコンパイルは止まりません。static を付けてtempの寿命をプログラム終了まで延ばすのが直し方です。
直し方: int を static int にします。
パターン3
1 #include <iostream> 2 int& makeId() { 3 int id = 7; ^ 4 return id; 5 } 6 int overwrite() { 7 int stack_data[8] = {0,0,0,0,0,0,0,0}; 8 return stack_data[0]; 9 } 10 int main() { 11 int& id = makeId(); 12 overwrite(); 13 std::cout << id << std::endl; 14 return 0; 15 }
「関数を抜けてもすぐ上書きされなければ値は読める」という考えは危険です。別の関数呼び出しが挟まるだけで、同じスタック領域が再利用され壊れます。ローカル変数をstaticにすれば関数を抜けても実体が破棄されません。
直し方: int を static int にします。
よくある誤解
「関数を抜けてもすぐ上書きされなければ値は読める」という考えは危険です。関数を抜けた直後に別の関数を呼ぶと、その関数のスタックフレームが同じメモリ領域を上書きするため、参照した瞬間にクラッシュすることがあります。g++はこの種のミスに対して -Wreturn-local-addr という警告を出してくれますが、あくまで警告でありコンパイルは止まりません。
まとめ
ローカル変数への参照を返すとSegmentation faultになる原因は上級でつまずきやすい項目です。上の3パターンを実際に手で直すと、エラーメッセージのどこを読めばよいかが掴めます。