Code Fix

中級

関数内のローカル変数のアドレスを返すと解放済み領域を指す

関数の中で宣言したローカル変数は、その関数を抜けると同時に領域が無効になります。そのアドレスをreturnで返すと、呼び出し元は既に無効な領域を指すポインタを受け取ります。

エラーメッセージの読み方

Segmentation fault
(エラーメッセージなし)
これはプログラム自身の出力ではありません。プログラムは何も言わずに落ちています
Segmentation fault
シェルが検知した異常終了の種類 — OSが「不正なメモリアクセス」などを検出しプロセスを強制終了させました
見つけ方
このメッセージだけでは行番号すら分かりません。原因はコードを読んで特定するしかありません

このエラーが出る典型パターン

パターン1

 1  #include <stdio.h>
 2  #include <stdlib.h>
 3  
 4  int *makeValue(int v) {
 5      int local = v;
 6      int *heap = malloc(sizeof(int));
 7      *heap = v;
 8      return &local;
                  ^
 9  }
10  
11  int main(void) {
12      int *p = makeValue(42);
13      printf("%d\n", *p);
14      return 0;
15  }
Segmentation fault

localは関数を抜けると同時に無効になるスタック上の変数です。そのアドレスを返すと、main側で別の関数(printf)を呼んだ瞬間に同じ領域が上書きされ、クラッシュします。malloc()でヒープに確保したheapは関数を抜けても無効になりません。

直し方: &local を heap にします。

この問題を解いてみる →

広告
広告スロット(未設定)

パターン2

 1  #include <stdio.h>
 2  #include <stdlib.h>
 3  
 4  int *makeCounter(int start) {
 5      int count = start;
 6      int *heap = malloc(sizeof(int));
 7      *heap = start;
 8      return &count;
                  ^
 9  }
10  
11  int main(void) {
12      int *c = makeCounter(10);
13      printf("%d\n", *c);
14      return 0;
15  }
Segmentation fault

malloc()でヒープに確保した領域は関数を抜けても無効になりません。スタック上のローカル変数のアドレスとの決定的な違いです。

直し方: &count を heap にします。

この問題を解いてみる →

パターン3

 1  #include <stdio.h>
 2  #include <stdlib.h>
 3  
 4  int *makeFlag(int v) {
 5      int flag = v;
 6      int *heap = malloc(sizeof(int));
 7      *heap = v;
 8      return &flag;
                 ^
 9  }
10  
11  int main(void) {
12      int *f = makeFlag(1);
13      printf("%d\n", *f);
14      return 0;
15  }
Segmentation fault

関数を抜けた直後は運よくクラッシュしないこともありますが、別の関数呼び出しを挟むと高い確率で同じ領域が上書きされます。

直し方: &flag を heap にします。

この問題を解いてみる →

パターン4

 1  #include <stdio.h>
 2  #include <stdlib.h>
 3  
 4  int *makeId(int v) {
 5      int local = v;
 6      int *heap = malloc(sizeof(int));
 7      *heap = v;
 8      return &local;
                  ^
 9  }
10  
11  int main(void) {
12      int *p = makeId(7);
13      printf("%d\n", *p);
14      return 0;
15  }
Segmentation fault

localは関数を抜けると無効になるスタック上の変数です。そのアドレスを返すとクラッシュにつながります。

直し方: &local を heap にします。

この問題を解いてみる →

パターン5

 1  #include <stdio.h>
 2  #include <stdlib.h>
 3  
 4  int *makeTotal(int v) {
 5      int total = v;
 6      int *heap = malloc(sizeof(int));
 7      *heap = v;
 8      return &total;
                  ^
 9  }
10  
11  int main(void) {
12      int *t = makeTotal(99);
13      printf("%d\n", *t);
14      return 0;
15  }
Segmentation fault

malloc()でヒープに確保した領域は関数を抜けても無効になりません。スタック変数との決定的な違いです。

直し方: &total を heap にします。

この問題を解いてみる →

よくある誤解

このサイトの検証環境では、関数を抜けた直後にmain側で別の関数(printfなど)を呼ぶと、解放済みのはずのスタック領域がその呼び出しのフレームに上書きされ、参照した瞬間にクラッシュすることを確認しています。「無効なだけで動くこともある」と侮らず、関数外に値を残したい場合は、呼び出し元の変数のアドレスを渡すか、mallocでヒープに確保します。

実務での勘所

安全な代替案は主に3つあります。呼び出し元に用意させた変数のアドレスを引数として渡してもらい、関数はそこに書き込むだけにする方法。構造体そのものを(ポインタではなく)戻り値としてコピーで返す方法。そしてmalloc()でヒープに確保し、呼び出し元がfree()する責任を持つ方法です。3つ目はメモリの解放忘れ(メモリリーク)の危険と引き換えなので、多くの場面では前者2つのほうが安全です。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)