Code Fix

上級

delete後のポインタ使用が「たまたま動いてしまう」危険性

deleteした直後の同じポインタを使って読み書きしてしまうバグです。未定義動作であるにもかかわらず、解放直後のメモリはすぐには再利用されないことが多く、正しい値が読めてしまうことがあります。

なぜエラーが出ないのか

出力: 実行のたびに変わりうる無関係な値(クラッシュはしない)
(エラーなし)
C++は何も報告しません。コンパイルも実行も正常に完了するためです
出力: 実行のたびに変わりうる無関係な値(クラッシュはしない)
実際の挙動 — 期待した結果と食い違っている箇所
見つけ方
エラーが出ないので、出力を目で確かめるか、コードを目で追うしかありません。この種の誤りが最も発見が遅れます

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

パターン1

 1  #include <iostream>
 2  int main() {
 3      int *p = new int(42);
 4      delete p;
 5      std::cout << *p << std::endl;    
                        ^
 6      return 0;
 7  }
出力: 実行のたびに変わりうる無関係な値(クラッシュはしない)危険性: 未定義動作。たまたま動いているだけ

deleteは確保していた領域を「他の用途に使ってよい」と管理システムに伝えるだけで、中身を即座に上書きするとは限りません。解放直後のメモリはすぐには再利用されないことが多く、たまたま動いてしまうことがあります。

直し方: std::cout << *p << std::endl; を std::cout << "done" << std::endl; にします。

この問題を解いてみる →

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

パターン2

 1  #include <iostream>
 2  int main() {
 3      int *value = new int(100);
 4      delete value;
 5      std::cout << *value << std::endl;    
                          ^
 6      return 0;
 7  }
出力: 実行のたびに変わりうる無関係な値(クラッシュはしない)危険性: 未定義動作。たまたま動いているだけ

「エラーなく動いたのだから解放は間違っていなかった」というのは誤解です。プログラムが複雑になり別の場所で確保・解放が起きるようになると、突然壊れた値が現れることがあります。

直し方: std::cout << *value << std::endl; を std::cout << "finished" << std::endl; にします。

この問題を解いてみる →

パターン3

 1  #include <iostream>
 2  int main() {
 3      int *n = new int(7);
 4      delete n;
 5      std::cout << *n << std::endl;  
                       ^
 6      return 0;
 7  }
出力: 実行のたびに変わりうる無関係な値(クラッシュはしない)危険性: 未定義動作。たまたま動いているだけ

delete後のポインタを一切使わない、または直後にnullptrを入れておくのが安全な習慣です。使ってしまっている行自体を削除するのが最も確実な直し方です。

直し方: std::cout << *n << std::endl; を std::cout << "ok" << std::endl; にします。

この問題を解いてみる →

パターン4

 1  #include <iostream>
 2  int main() {
 3      int *x = new int(21);
 4      delete x;
 5      std::cout << *x << std::endl;        
                          ^
 6      return 0;
 7  }
出力: 実行のたびに変わりうる無関係な値(クラッシュはしない)危険性: 未定義動作。たまたま動いているだけ

delete後のポインタを読む行そのものを削除するのが最も確実な直し方です。

直し方: std::cout << *x << std::endl; を std::cout << "complete" << std::endl; にします。

この問題を解いてみる →

パターン5

 1  #include <iostream>
 2  int main() {
 3      int *level = new int(4);
 4      delete level;
 5      std::cout << *level << std::endl;
                        ^
 6      return 0;
 7  }
出力: 実行のたびに変わりうる無関係な値(クラッシュはしない)危険性: 未定義動作。たまたま動いているだけ

クラッシュしないため気づきにくいですが、解放済みの領域を読んだ値は信用できません。

直し方: std::cout << *level << std::endl; を std::cout << "end" << std::endl; にします。

この問題を解いてみる →

よくある誤解

「エラーなく動いたのだから解放は間違っていなかった」というのは誤解です。deleteは確保していた領域を「他の用途に使ってよい」と管理システムに伝えるだけで、その領域の中身自体を即座に上書きするとは限りません。プログラムが複雑になり別の場所で確保・解放が起きるようになると、突然壊れた値が現れることがあります。

実務での勘所

delete直後にポインタをnullptrへ明示的に代入しておく習慣は、二重解放だけでなくuse-after-freeの発見にも役立ちます。もしその後どこかでうっかりこのポインタを使ってしまっても、nullptrの参照は(未定義動作ではあるものの)ほぼ確実にその場で即座にクラッシュするため、ずっと後になって無関係な場所で不可解な値化けとして現れるより、原因の特定がはるかに簡単になります。危険な操作を、遅れて起きる曖昧なバグではなくその場で確実に落ちるバグに変えるという考え方は、手動メモリ管理全般で有効な防御的な習慣です。

演習をはじめる

関連するエラー

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