【C++中級編】C++の落とし穴と回避方法

アイキャッチ画像

2025年1月入社のT.Nです。

今回はC++を使ってこれから開発する方に向けて、

危険な落とし穴とその回避方法を記事にしました!

皆さまの役に立てたら幸いです!

 

■■落とし穴

■1.メモリ解放について

 C++では、動的にメモリを割り当てる際にnew演算子を使用し、解放時にはdelete演算子を使用します。

 もし解放を忘れてしまうと、メモリリークが発生してしまいます。

  

 これは聞いた話ですが、他の言語(Java)の経験者がC++でPCゲームを作成したところ、

 メモリ解放出来ていなかったため、メモリリークにより途中でPCがクラッシュし、使えなくなったそうです。。。

  

 C++で製造するときメモリ解放は必ず注意しなければならないものです!

 ただ、メモリリークが怖いから必要以上に解放しようとすると、メモリの破壊やプログラムのクラッシュを引き起こす可能性に繋がります。

  

  ◯落とし穴

  それなら処理の一番最後に1回だけメモリ解放の処理をまとめて書けばよいのでは?

  と思われたかもしれませんが、ここに落とし穴があります。

  それは処理内にループ処理がある場合です。

  

  ループ処理中に変数を定義した場合、ループの回数分、変数がどんどん溜まります。

  最後に一回だけ解放しても、溜まった分が解放されないのでメモリリークが発生してしまいます。

  大切なのはメモリ解放する場所です。

  ◯回避方法

  変数を使用し終わったら解放するようにしましょう。

  ※処理中にループが無いなら、最後に一括で解放しても問題はありませんが、

   膨大な処理になった場合、変数の数が多いため解放漏れが発生する恐れがあるので気を付けて下さい。

  

■2.try-catch処理について

  try-catchとは、プログラム実行中に発生する例外(エラー)を処理するための仕組みです。

  異常終了により処理が継続できないということを防ぐため、本来であれば終了してしまうプログラムの「エラー」を制御します。

  実際の業務では異常終了を避けるため「メイン処理はtry-catch内で実装する」と決めてるところもあります。

  サンプルソースを使ってどんな事ができるか書いてみます。

  

  (サンプルソース例:開始)

  #include <iostream>

  #include <stdexcept>

  

  int main() {

    try{ // 例外が発生する可能性のあるコード

      int num = 0;

      if (num == 0) {

        throw std::runtime_error(“値が0です”);  // 例外を投げる

      }

      int result = 10 / num ;

    }

    catch (const std::runtime_error& e) { // 例外をキャッチして処理

      std::cerr << “例外発生: ” << e.what() << std::endl;

    }

    catch (…) { // 予想外の例外をキャッチして処理

      std::cerr << “予期せぬ例外発生” << std::endl;

    }

  return 0;

  }

  (サンプルソース例:終了)

  上記の実行結果は「例外発生: 値が0です」になります。

  処理として、変数numに初期値「0」を入れており、

  最初のif文で変数numが「0」の場合の処理に入るため、例外に投げられます。

  この時、throwの後ろにstd::runtime_errorと入れているため、

  「catch(const std::runtime_error& e)」の処理の方へ飛ばされています。

  今回サンプルソースでは途中に「int result = 10 / num;」処理がありますが、値を0で割ってしまうとプログラムがクラッシュします。

  このようにcatchにthrowさせることでクラッシュを回避し、

  意図的に例外エラーを発生させる事が出来ます。

  また、処理中に何かしらのエラーが発生した場合「予期せぬ例外発生」が出力されます。

  

  ◯落とし穴

  try-catchだけで例外エラーが全部取れるからどんなコードでも安心!

  と思われたかもしれませんが、ここに落とし穴があります。

  例えば、範囲外アクセスによるエラーです。

  範囲外アクセスの場合、try-catchでは捕捉されません。

  コンパイルエラーと検出されることもありますが、

  実行時プログラムの停止を引き起こす可能性があります。

  

  範囲外アクセスとは、主に2パターンあります。

  1.配列の範囲外アクセス:

  (サンプルソース例:開始)

  int arr[3] = {1, 2, 3}; //配列[0],[1],[2]を用意

  arr[4] = 10; //配列[3]を選択

  (サンプルソース例:終了)

  上記のソースでは、配列のサイズを超えてアクセスしようとしているため、

  コンパイルエラーになるか、実行時にバッファオーバーフローなどのエラーを引き起こします。

  

  2.ポインタの範囲外アクセス:

  (サンプルソース例:開始)

  int *ptr = new int[3];

  ptr[4] = 10;

  (サンプルソース例:終了)

  上記のソースでは、動的に確保した配列の範囲を超えてアクセスしようとしているため、同じく実行時にエラーを引き起こします。

  

  ◯回避方法

  配列やポインタのアクセス時に、範囲をチェックするコードを追加することで回避出来ます。

  

 ◯実際に現場であった範囲外アクセス

  これは私が経験した配列の範囲外アクセスによるエラーです。

  処理内容は、csvファイルにある日付項目が「YYYY/MM/DD」形式かチェックするというものです。

  下記の内容で実装してみました。

   ①「YYYY/MM/DD」が10桁か確認する

    ┗10桁でない場合、「日付をYYYY/MM/DD形式に修正して下さい」を出力する。

   ②「YYYY/MM/DD」を区切り文字”/”で分解して配列に格納する。

    ┗配列[0]にYYYY、配列[1]にMM、配列[2]にDDが格納される想定。

   ③格納された配列の桁数を確認する。(配列[0]が4桁、配列[1]が2桁、配列[2]が2桁)

    ┣桁数が違う場合、「日付をYYYY/MM/DD形式に修正して下さい」を出力する。

    ┗正しい桁数の場合、数字以外が含まれていないか確認する。

     ┣数字以外が含まれていた場合、「日付をYYYY/MM/DD形式に修正して下さい」を出力する。

     ┗数字のみの場合、メッセージ「日付項目YYYY/MM/DDを読み込みます」を出力する。

  

  実装後の複数のテストデータで実行してみました。

  テストパターンA:「2025/01/01」結果:成功

  テストパターンB:「202a/01/01」結果:意図したエラー

  テストパターンC:「2025/0a/01」結果:意図したエラー

  テストパターンD:「2025/01/0a」結果:意図したエラー

  テストパターンE:「20250/1/01」結果:意図したエラー

  テストパターンF:「2025/010/1」結果:意図したエラー

  テストパターンG:「2025/01/010」結果:意図したエラー

  テストパターンH:「2025001001」結果:バグが発生

  

  原因として、③の処理で「配列[0]が4桁以外、配列[1]が2桁以外、配列[2]が2桁以外の場合エラー」をしていましたが、テストパターンHのように区切り文字が存在しない場合、②処理で分解した結果、配列が1つしか存在していないため、配列[1]、配列[2]は範囲外アクセスになっていました。

  

  対応として、②と③の間に処理を入れて解決しました。

  最終的な実装内容は下記になります。

  

   ①「YYYY/MM/DD」が10桁か確認する

   ②「YYYY/MM/DD」を区切り文字”/”で分解して配列に格納する。

   ③分解した結果の配列が3つ存在するか確認する。

    ┗配列数が3以外の場合、「日付をYYYY/MM/DD形式に修正して下さい」を出力する。  

   ④格納された配列「YYYY」「MM」「DD」が正しい桁数か確認する。

    ┣桁数が違う場合、「日付をYYYY/MM/DD形式に修正して下さい」を出力する。

    ┗正しい桁数の場合、数字以外が含まれていないか確認する。

     ┣数字以外が含まれていた場合、「日付をYYYY/MM/DD形式に修正して下さい」を出力する。

     ┗数字のみの場合、メッセージ「日付項目YYYY/MM/DDを読み込みます」を出力する。

  

 いかがだったでしょうか。

 今回記事にした内容はC++で作業するうえで、必ず気を付けなければいけない事です。

 これからの製造でより良い成果物が出来るように頑張っていきましょう。