C言語の静的解析は、無料の gcc の警告と cppcheck で始められます。2つが見つけるバグは一部しか重ならないので、片方だけでは見逃すバグがあります。

比べたのは、バグを1つずつ入れた C のコード27本です。gcc の警告と cppcheck を当て、それぞれが見つけたバグと見逃したバグを、実行結果つきで示します。後半は、2つを Windows に入れて自分のコードに当て、VS Code のタスクで回すまでの手順です。

出力は、Windows 11 で gcc 16.1.0(MSYS2)と cppcheck 2.22.0 を実行した例で、マイコンのボードでは動かしていません。見つけた数は、この記事で用意した27本での結果です。

この記事を読むと分かること

  • gcc の警告と cppcheck を1回ずつ両方回すと、int が何ビットでもバグになる24種のうち21種を見つけた(gcc だけなら15種、cppcheck だけなら16種)
  • int が16ビットの AVR(Arduino Uno など)向けには、cppcheck に --platform=avr8 を足すと、16ビットのときだけのバグ3種も見つけた
  • 2つとも見逃した3種は、単体テストとレビューで拾う

Windows 11gcc 16.1.0cppcheck 2.22.0バグ入りのコード27本

目次

  1. 静的解析は、プログラムを動かさずにソースコードを調べる
    1. gccの警告も、静的解析の一つ
    2. この記事で使う道具は、gccとcppcheckの2つ
  2. 27種のバグに当てると、gccとcppcheckは見つけるバグが違った
    1. gccとcppcheckの両方が見つけた10種
    2. cppcheckだけが見つけた6種
    3. gccだけが見つけた5種
    4. 設定ごとに見つけた数
    5. 最適化と-fanalyzerで、gccが見つけるバグが変わる
  3. intが16ビットのAVR(Arduino Unoなど)は、cppcheckに--platform=avr8を付ける
  4. 2つとも見逃した3種は、単体テストとレビューで拾う
    1. データ次第のゼロ除算は、境界の値のテストでプログラムが止まる
    2. 8ビットのタイマの差は、境界の値のテストがFAILする
    3. volatileの付け忘れは、PCの単体テストでは区別できない
  5. gccとcppcheckをWindowsに入れる
    1. gccはMSYS2で入れる
    2. cppcheckは公式サイトのMSIで入れる
  6. 自分のコードに、gccとcppcheckを1回ずつ当てる
    1. 推奨の2コマンドで回す
    2. 記事に載せているコードに当てた結果
    3. --enableはwarning,styleまでにする
    4. 指摘を消すときは、--inline-supprか--suppressを使う
  7. VS Codeのタスクにして、問題パネルで指摘を見る
    1. tasks.jsonに、cppcheckとgccのタスクを書く
    2. 問題パネルに、cppcheckとgccの指摘が並ぶ
    3. makeやCIで止めるなら、終了コードを使う
    4. うまく動かないとき
  8. まとめ|gccの警告とcppcheckは両方回す
だだいき現役の車載組み込みソフトエンジニア車載マイコンのソフトウェアを業務でC言語を使って開発しています。応用情報技術者、C言語プログラミング能力検定1級。

静的解析は、プログラムを動かさずにソースコードを調べる

静的解析は、プログラムを実行せずにソースコードを解析して、バグの疑いがある箇所を見つける方法です。

テストのように、ビルドしたプログラムを動かして確かめる方法とは違い、ソースコードだけで解析します。そのため、マイコンのボードや動かす環境がそろう前から、ソースコードの問題を見つけられます。

その代わり、プログラムが仕様どおりに動くか(ロジックの正しさ)は確かめられないので、テストは別に要ります。

gccの警告も、静的解析の一つ

コンパイラの警告も、広い意味では静的解析に含まれます。gcc に -Wall や -Wextra を付けると、コンパイルのついでに、バグになりやすい書き方を警告します。

-Wall は「警告を全部出す」という意味ではありません。-Wextra を足すと、-Wall では有効にならない警告が増えます(GCC のマニュアルの警告オプションの節)。

-fanalyzer は、関数をまたいでプログラムの流れをたどり、メモリリークや NULL ポインタの参照などを調べる gcc のオプションです(GCC のマニュアルの静的解析の節)。

ただし、コンパイラの主な役目は翻訳なので、警告の範囲には限りがあります。そこで、静的解析の専用のツールを併用することも多いです。

この記事で使う道具は、gccとcppcheckの2つ

この記事で使うのは、gcc の警告(-fanalyzer を含む)と cppcheck の2つで、どちらも無料です。版は gcc 16.1.0(MSYS2)と、cppcheck 2.22.0(2026年9月19日公開)です。

道具 何を調べるか
gcc の警告 -Wall・-Wextra・-Wconversion で、バグになりやすい書き方を警告する
gcc の -fanalyzer プログラムの流れをたどり、メモリリークや NULL ポインタの参照を調べる
cppcheck C/C++ 専用の静的解析ツールで、未定義の動作と危険な書き方を見つける

cppcheck は、組み込みのプロジェクトでよくある非標準の構文を含むコードも、解析できるように作られています(cppcheck のマニュアル)。

商用の静的解析ツールは、MISRA C などのコーディング規約への対応と、機能安全規格向けの認証が主な役目です。cppcheck にも MISRA C のアドオンがありますが、公式マニュアルは、オープンソース版の MISRA のチェックは規則を部分的にしかカバーしない、と書いています。このアドオンは、Windows の MSI では既定で入りません。

27種のバグに当てると、gccとcppcheckは見つけるバグが違った

推奨の2つのコマンドを1回ずつ当てると、int が何ビットでもバグになる24種のうち、両方が見つけたのは10種で、gcc だけが5種、cppcheck だけが6種を見つけました。

比べるために、C でよく踏むバグを1つずつ入れたコードを27本用意しました。1本に入れたバグは1つだけです。未初期化の変数・配列の範囲外・ゼロ除算・桁あふれ・= と == の取り違え・volatile の付け忘れなど、組み込みのコードで踏みやすいものを選んでいます。

当てたのは、gcc の -Wall -Wextra -O2 -fanalyzer -Wconversion と、cppcheck の --enable=warning,style です(どちらも C99 として解析)。ほかの設定で当てたときの数は、設定ごとに見つけた数にまとめています。

バグそのものを名指しした指摘だけを「見つけた」に数え、症状だけを言った指摘(たとえば「条件がいつも偽」)は数えていません。

27本のうち3本は、int が16ビットのときだけバグになる桁あふれとシフトです。PC の int は32ビットなので、この3本は PC 向けのコードではバグになりません。そこでこの節では、int が何ビットでもバグになる24種で比べ、16ビットの3種は次の節で扱います。

推奨の2コマンドで24種のバグを見つけたかどうかを、2つの円の重なりで示すと次のようになります。左の円が gcc の警告、右の円が cppcheck で、重なった部分が両方とも見つけたバグです。

int が何ビットでもバグになる24種のうち、gccの警告だけが見つけた5種、gccの警告とcppcheckの両方が見つけた10種、cppcheckだけが見つけた6種を2つの円の重なりで示し、どちらも見逃した3種を円の外に置いた図

重なった10種のほかに、gcc だけが5種、cppcheck だけが6種を見つけています。どちらか一方だけでは、この11種のどれかを見逃します。円の外の3種は、両方とも見逃しました。

gccとcppcheckの両方が見つけた10種

バグの中身 gcc の警告と cppcheck の id
ループの終わりを i <= 8 にして、8要素の配列の範囲外に書く gcc:-Waggressive-loop-optimizations(-O2 のとき)
cppcheck:arrayIndexOutOfBounds
配列の形で受けた引数に sizeof を使う gcc:-Wsizeof-array-argument
cppcheck:sizeofwithsilentarraypointer
free したあとに読む gcc:-Wuse-after-free
cppcheck:deallocret
uint8_t のカウンタで i < 256 まで回す gcc:-Wtype-limits
cppcheck:compareValueOutOfTypeRangeError
補正値の変数を、計算に入れ忘れる gcc:-Wunused-variable
cppcheck:unreadVariable
switch の case 0 で break を書き忘れる gcc:-Wimplicit-fallthrough=
cppcheck:redundantAssignInSwitch
条件によって戻り値を返さない gcc:-Wreturn-type
cppcheck:missingReturn
ローカル配列のアドレスを返す gcc:-Wreturn-local-addr
cppcheck:returnDanglingLifetime
12バイトの文字列を8バイトの配列へ strcpy gcc:-Wanalyzer-out-of-bounds ほか
cppcheck:bufferAccessOutOfBounds
status & READY_MASK == READY_MASK(& より == が先) gcc:-Wparentheses
cppcheck:bitwiseOnBoolean ほか

同じバグでも、gcc と cppcheck では指摘の文も、指す行も違います。戻り値を返さない関数では、gcc は関数の終わりの }(11行目)に control reaches end of non-void function と出しました。cppcheck は、else if のブロックを閉じる10行目の } に Found an exit path from function with non-void return type that has missing return statement と出しています。

cppcheckだけが見つけた6種

バグの中身 cppcheck の出力
条件によっては代入されないまま返す(未初期化) Uninitialized variable: value
件数に 0 を渡して平均を取る(0 で割る) Division by zero.
3,000,000ms をマイクロ秒にすると、32ビットの int を超える Signed integer overflow for expression 'ms*1000'.
uint32_t mask = 1 << 31;(int の1を符号ビットへずらす) Shifting signed 32-bit value by 31 bits is undefined behaviour
途中で戻る道で、free を忘れる(メモリリーク) Memory leak: buf
return の後に、LED を点ける行を書く Statements following 'return' will never be executed.

このうち未初期化とメモリリークの2種は、gcc でも -fanalyzer を最適化なしで回すと見つかりました。推奨の gcc では見つからなかった理由は、後の項(最適化と-fanalyzerで、gccが見つけるバグが変わる)で示します。

1 << 31 には、gcc も -Wconversion で値が変わるという警告を出しました。ただし、シフトがおかしいとは言っていないので、見つけたには数えていません。

定数の 0 で割る形は cppcheck が見つけましたが、割る数がデータで決まる形のゼロ除算は、2つとも見逃しました(データ次第のゼロ除算)。

gccだけが見つけた5種

バグの中身 gcc の出力(有効にしたオプション)
NULL を返しうる関数の戻り値を、確かめずに使う dereference of NULL '0' [CWE-476](-fanalyzer)
if (cmd == ~inv)(uint8_t の ~ は int になる) comparison of promoted bitwise complement of an unsigned value with unsigned(-Wextra)
10ビットの AD 値を、そのまま uint8_t へ入れる conversion from 'uint16_t' {aka 'short unsigned int'} to 'uint8_t' {aka 'unsigned char'} may change value(-Wconversion)
if (mode = 0)(= と == の取り違え) suggest parentheses around assignment used as truth value(-Wall)
負の気温(int)と unsigned int を比べる comparison of integer expressions of different signedness: 'int' and 'unsigned int'(-Wextra)

if (mode = 0) には、cppcheck も Condition 'mode=0' is always false(条件がいつも偽)と出しました。ただし代入を書いていることは指していないので、見つけたには数えていません。= と == の違いは、C言語 変数・型・演算子の練習問題7問で扱っています。

10ビットの値の切り捨ては、-Wconversion を足したときにだけ見つかりました。推奨の gcc から -Wconversion を外すと、見つけるバグはこの1種だけ減ります。

NULL の戻り値は、-fanalyzer でだけ見つかっています。ポインタが指す先を確かめずに使う誤りは、C言語 ポインタの練習問題7問でも、住所を入れる前のポインタの例で扱っています。

残りの6種のうち、int が16ビットのときだけバグになる3種は次の節で、2つとも見逃した3種はその次の節で扱います。

設定ごとに見つけた数

gcc と cppcheck の設定を変えて27本に当てると、1つの設定で見つけた数は次のとおりでした。gcc はすべて -std=c99 -c を付けています。

gcc の設定 27本のうち見つけた数
警告オプションなし 3
-Wall 8
-Wall -Wextra 12
-Wall -Wextra -O2 13
-Wall -Wextra -Wconversion 13
-Wall -Wextra -fanalyzer 15
-Wall -Wextra -O2 -fanalyzer -Wconversion(推奨) 15
avr-gcc 7.3.0 の -mmcu=atmega328p -Wall -Wextra -Os 15
cppcheck の設定 27本のうち見つけた数
既定(--enable なし) 10
--enable=warning 11
--enable=style 16
--enable=warning,style(推奨) 16
--enable=all 16
--enable=all --check-level=exhaustive 16
--enable=warning,style --platform=avr8 19
--enable=all --platform=avr8 19

1つの設定で一番多かったのは cppcheck の --platform=avr8 を付けた19種で、これには int が16ビットのときだけの3種が入っています。PC の gcc では、-fanalyzer を付けた設定の15種が最多でした。

推奨の gcc は24種のうち15種、cppcheck は16種で、2つを合わせると21種になります。

--enable=all は style と同じ16種で、増えた指摘はバグではない所へのものだけでした。その中身は、後の項(--enableはwarning,styleまでにする)で示します。

最適化と-fanalyzerで、gccが見つけるバグが変わる

gcc の警告は、最適化のオプションで出たり出なかったりします。この27本では、配列の範囲外(i <= 8 で8要素の配列に書く)を gcc が見つけたのは、最適化を付けたとき(-O2 など)だけでした。

逆に未初期化の変数(片方の分岐でだけ値を入れて返す形)は、-Wall -Wextra だと、-O0・-Og・-O1・-O2・-O3・-Os のどれでも警告が出ませんでした。-fanalyzer も、最適化なし(-O0)のときだけ見つけています。

ただし、代入する値を定数から外部の入力に替えた形では、向きが逆になりました。最適化なしでは出ず、最適化を付けると 'value' may be used uninitialized が出ます。

GCC のマニュアルも、未初期化の警告は最適化に依存し、どの変数に警告が出るかは最適化のオプションと GCC の版で変わる、と書いています。どの最適化で出るかはコードの形でも変わり、ここに書いたのは、このコードで試した結果です。

推奨の gcc は -O2 と -fanalyzer を1回で回すので、最適化なしの -fanalyzer で見つかった未初期化とメモリリークの2種が、gcc 側から消えます。2種とも cppcheck が見つけるので、2つのコマンドを合わせた結果は変わりません。gcc を -O2 -Wconversion の回と、-fanalyzer(最適化なし)の回の2回に分けると、gcc だけで17種になりました。

intが16ビットのAVR(Arduino Unoなど)は、cppcheckに--platform=avr8を付ける

Arduino Uno などの AVR マイコンは int が16ビットなので、cppcheck に --platform=avr8 を付けると、16ビットのときだけ起きる桁あふれとシフトの幅超えを見つけます。

cppcheck は、--platform を指定しないと、実行している PC の型の大きさで解析します。公式マニュアルは、ターゲットの環境に合う設定を使うよう勧めています。

avr8 は、8ビットの AVR マイコン向けの設定です。cppcheck 2.22.0 には、pic8・pic16 や MSP430 向けなど、ほかのマイコン向けの設定も入っています。この記事で確かめたのは、そのうち avr8 と、int が32ビットの ARM 向けの arm32-wchar_t4 の2つです。arm32-wchar_t4 を付けた結果は、27本とも --platform を付けないときと同じでした。

d05b_int16_overflow.c

/* d05b 符号付きオーバーフロー(int が16ビットのときだけ):3300 x 20 = 66000 は 16ビットの int に入らない */
#include <stdint.h>

static int32_t scaled_mv(void)
{
    int mv = 3300;
    int gain = 20;
    int32_t out = mv * gain;   /* DEFECT */
    return out;
}

int main(void)
{
    return (int)(scaled_mv() & 0xFF);
}

3300 × 20 は 66000 で、int が16ビットだと入りません。PC の int は32ビットなので、--platform を付けずに解析すると、何も出ませんでした。

cppcheck(--platform なし)

$ cppcheck --enable=warning,style "--template={file}:{line}:{column}: {severity}: {message} [{id}]" --std=c99 d05b_int16_overflow.c
Checking d05b_int16_overflow.c ...
cppcheck(--platform=avr8)

$ cppcheck --enable=warning,style --platform=avr8 "--template={file}:{line}:{column}: {severity}: {message} [{id}]" --std=c99 d05b_int16_overflow.c
Checking d05b_int16_overflow.c ...
d05b_int16_overflow.c:8:22: error: Signed integer overflow for expression 'mv*gain'. [integerOverflow]
d05b_int16_overflow.c:8:17: style: int result is assigned to long variable. If the variable is long to avoid loss of information, then you have loss of information. [truncLongCastAssignment]

--platform=avr8 を付けたときの1行目、Signed integer overflow for expression 'mv*gain'. が、このバグの指摘です。2行目の truncLongCastAssignment は、int で計算した結果を long の変数(ここでは int32_t の out)に入れている、という補足です。

int が16ビットのときだけバグになる3種と、それを見つけた指摘は次のとおりです。

バグの中身 cppcheck(--platform=avr8)と avr-gcc(-Wall -Wextra)の指摘
変数どうしの mv * gain(3300 × 20) cppcheck:integerOverflow
avr-gcc:出ない
定数式の 60 * 1000 cppcheck:integerOverflow
avr-gcc:integer overflow in expression
uint32_t mask = 1 << 20; cppcheck:shiftTooManyBits
avr-gcc:left shift count >= width of type

定数どうしの2種は、avr-gcc の -Wall -Wextra でも警告が出ました。cppcheck の --platform=avr8 でしか見つからなかったのは、変数どうしの掛け算の1種だけです。

-Wall -Wextra は、Arduino IDE の「コンパイラの警告」を「全て」にしたときに付くオプションです。Arduino CLI では compile --warnings all が同じ設定です(Arduino CLIの使い方)。確かめたのは、IDE と同じオプションで .c のファイルをコンパイルしたところまでで、IDE の画面と、スケッチ(.ino)では試していません。

avr-gcc(-Wall -Wextra)と cppcheck(--platform=avr8)を組み合わせると、27本のうち5種を見逃しました。PC の推奨の gcc と cppcheck(--platform=avr8)の組み合わせで見逃したのは、次の節の3種だけです。差の2種は、-fanalyzer でだけ見つかる NULL の戻り値と、-Wconversion でだけ見つかる10ビットの値の切り捨てです。avr-gcc 7.3.0 に -fanalyzer を渡すと、unrecognized command line option '-fanalyzer' で止まります。

PC 向けのコードや、int が32ビットのマイコン向けのコードには、--platform=avr8 を付けません。

2つとも見逃した3種は、単体テストとレビューで拾う

データ次第のゼロ除算と8ビットのタイマの差は単体テストの境界の値で見つかり、volatile の付け忘れはレビューで見ます。

静的解析は、書き方からバグの疑いを見つける方法で、プログラムが仕様どおりに動くかまでは確かめられません。27本のうち次の3種は、推奨の2コマンドを含め、この記事で試したどの設定でも、バグとしては指摘されませんでした。

データ次第のゼロ除算は、境界の値のテストでプログラムが止まる

d04b_div_zero_data.c

/* d04b ゼロ除算(データ次第):有効なサンプルが0件だと 0 で割る */
volatile int g_samples[4];   /* センサーから入るデータに見立てる */

static int average_valid(void)
{
    int sum = 0;
    int count = 0;
    for (int i = 0; i < 4; i++) {
        if (g_samples[i] >= 0) {
            sum += g_samples[i];
            count++;
        }
    }
    return sum / count;   /* DEFECT */
}

int main(void)
{
    return average_valid();
}

有効なサンプル(0以上の値)が1件も無いと、count が 0 のまま割り算に入ります。割る数はセンサーから入るデータで決まり、0 になるのは、有効なサンプルが1件も来なかったときだけです。

この関数に、4件とも無効(-1)にした境界の値のテストを、Unity で書きました。0件のときに何を返すかはコードに決まりが無いので、このテストでは 0 を返す仕様にしています。

test_d04b_div_zero_data.c(境界の値のテスト)

/* 境界の値:4件とも無効(有効なサンプルが0件)。このテストで決めた仕様=0を返す */
void test_average_when_no_valid_samples(void) {
    g_samples[0] = -1;
    g_samples[1] = -1;
    g_samples[2] = -1;
    g_samples[3] = -1;
    TEST_ASSERT_EQUAL_INT(0, average_valid());
}
MSYS2 UCRT64

$ gcc -std=c99 -Wall -Wextra -Iunity -Idefects -o test_d04b test_d04b_div_zero_data.c unity/unity.c
$ ./test_d04b

テストは FAIL にならず、FAIL の行も、最後の集計の行(直した版では 2 Tests 0 Failures 0 Ignored)も出ないまま、プログラムが止まりました。同じテストを Windows から直接動かすと、終了コードは 0xC0000094(整数を 0 で割った)でした。止まった場所は、14行目の return sum / count; です。

0件のときに 0 を返すように直すと、同じテストが2本とも PASS しました。

8ビットのタイマの差は、境界の値のテストがFAILする

d08_u8_wraparound.c

/* d08 整数昇格(引き算):8ビットのタイマ値の差。now が一周すると int では負になり、時間切れを見落とす */
#include <stdint.h>

volatile uint8_t g_now;     /* 現在のタイマ値に見立てる */
volatile uint8_t g_start;   /* 開始時のタイマ値に見立てる */

static int timed_out(uint8_t now, uint8_t start)
{
    if ((now - start) >= 10) {   /* DEFECT */
        return 1;
    }
    return 0;
}

int main(void)
{
    return timed_out(g_now, g_start);
}

now と start は uint8_t ですが、引き算は、暗黙の変換で int にしてから計算されます(整数昇格)。そのため、タイマが 255 から 0 へ一周すると、差が負になります。開始が 250・現在が 4 なら、経過は 10 カウントなのに、now - start は -246 です。

この境界の値のテストを含めて3本を回すと、境界のテストだけが FAIL しました。

test_d08_u8_wraparound.c(境界の値のテスト)

/* 境界の値:タイマが 255 から 0 へ一周をまたぐ。開始 250・現在 4 は 10 カウント経過(256 - 250 + 4)で、時間切れ */
void test_timeout_across_wraparound(void) {
    TEST_ASSERT_EQUAL_INT(1, timed_out(4, 250));
}
MSYS2 UCRT64

$ gcc -std=c99 -Wall -Wextra -Iunity -Idefects -o test_d08 test_d08_u8_wraparound.c unity/unity.c
$ ./test_d08
test_d08_u8_wraparound.c:32:test_timeout_after_10_counts:PASS
test_d08_u8_wraparound.c:33:test_no_timeout_after_9_counts:PASS
test_d08_u8_wraparound.c:27:test_timeout_across_wraparound:FAIL: Expected 1 Was 0

-----------------------
3 Tests 1 Failures 0 Ignored
FAIL
$ echo $?
1

Expected 1 Was 0 は、時間切れ(1)を期待したのに 0 が返った、という意味です。ふつうの値の2本は PASS しているので、一周をまたぐ値を入れないと、このバグはテストでも見えません。

差を (uint8_t)(now - start) で8ビットに戻すと、3本とも PASS しました。

C言語の単体テストをPCで回す

Unity でテストを書いて MSYS2 の gcc で回す手順と、gcov でテストが通っていない分岐を見つける方法は、単体テストの記事にまとめています。

C言語の単体テストを読む

volatileの付け忘れは、PCの単体テストでは区別できない

d18_missing_volatile.c

/* d18 volatile の付け忘れ:割り込みで書き換えるフラグを、メインループで待つ */
#include <stdint.h>

static uint8_t g_flag;   /* DEFECT */

void TIMER1_COMPA_vect(void);

/* 割り込み処理に見立てる(AVR では ISR(TIMER1_COMPA_vect) と書く) */
void TIMER1_COMPA_vect(void)
{
    g_flag = 1;
}

int main(void)
{
    while (g_flag == 0) {   /* RELATED */
        /* 割り込みを待つ */
    }
    return 0;
}

割り込みで書き換える g_flag に volatile が付いていないと、コンパイラは、待ちのループの中で g_flag が変わらないものとして命令を作れます。Arduino IDE に入っている avr-gcc 7.3.0 で、-Os を付けてアセンブリを出すと、main の待ちのループは次のようになりました(main の部分の抜粋)。

d18_missing_volatile.s(volatile なし)

    lds r24,g_flag
    cpse r24,__zero_reg__
    rjmp .L6
.L5:
    rjmp .L5
.L6:
    ldi r25,0
    ldi r24,0
d18v_with_volatile.s(volatile あり。違いは g_flag の宣言の1行)

.L3:
    lds r24,g_flag
    tst r24
    breq .L3
    ldi r25,0
    ldi r24,0

volatile が無いと、g_flag を読む lds はループの前の1回だけで、ループ(.L5: rjmp .L5)の中では読み直していません。volatile を付けると、ループ(.L3)の中で毎回 lds で読み直しています。

PC の単体テストでは、割り込みの関数を先に呼んでから main を呼ぶ形になります。この形では、volatile のある・なしのどちらも PASS し、区別できませんでした。

gcc の -fanalyzer は、このループに infinite loop [CWE-835](無限ループ)とだけ出しました。volatile が足りないとは言っていないので、見つけたには数えていません。

割り込みと共有する変数に volatile が付いているかは、ツールに任せず、レビューで見ます。ここで確かめたのはコンパイラが出した命令の並びまでで、実機(Arduino)では動かしていません。

gccとcppcheckをWindowsに入れる

gcc は MSYS2 で、cppcheck は公式サイトの MSI(2.22.0)で入れます。

gccはMSYS2で入れる

gcc は、MSYS2 の UCRT64 の環境に入れます。手順はC言語の単体テストのやり方と同じで、MSYS2 を入れたあと、UCRT64 のターミナルで次のコマンドを打ちます。

MSYS2 UCRT64

$ pacman -S mingw-w64-ucrt-x86_64-gcc

入ったら、--version で版を確かめます。この記事は gcc 16.1.0 で確かめました。

gcc の版

$ gcc.exe --version
gcc.exe (Rev5, Built by MSYS2 project) 16.1.0
Copyright (C) 2026 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

cppcheckは公式サイトのMSIで入れる

  1. MSI を取る
    cppcheck の公式サイトの Download 欄にある、Windows 64-bit の Installer(cppcheck-2.22.0-x64-Setup.msi・約22.8MiB)を取ります。リンク先は GitHub の配布ページです。
  2. MSI を実行する
    既定の入れ先は C:\Program Files\Cppcheck です。これは MSI のテーブルで確かめた既定値で、インストーラの画面は動かしていません。
  3. 版を確かめる
    コマンドプロンプトで、フルパスで --version を打ちます。

    コマンドプロンプトで打つコマンド

    "C:\Program Files\Cppcheck\cppcheck.exe" --version

    Cppcheck 2.22.0 と版が表示されれば、使える状態です。この記事の検証では、MSI を作業フォルダに展開した cppcheck.exe で、この表示を確かめました。

この MSI は、PATH を足す設定を持っていません(MSI のテーブルで確かめた範囲)。コマンドで cppcheck と打って見つからないときは、フルパスで呼ぶか、C:\Program Files\Cppcheck を自分で PATH に足します。

cppcheck は、MSYS2 の pacman でも入ります(pacman -S mingw-w64-ucrt-x86_64-cppcheck)。ただし、2026年10月11日の時点で MSYS2 の cppcheck は 2.21.0-1 で、公式の 2.22.0 より1つ前の版です(MSYS2 のパッケージのページ)。この記事の結果は 2.22.0 で取りました。

自分のコードに、gccとcppcheckを1回ずつ当てる

gcc は -Wall -Wextra -O2 -fanalyzer -Wconversion、cppcheck は --enable=warning,style で、自分のコードに1回ずつ当てます。

推奨の2コマンドで回す

C99 のコードなら、次の2つです。ヘッダを別のフォルダに置いているときは、gcc と同じく -I でフォルダを渡します。

推奨の2コマンド

gcc -std=c99 -Wall -Wextra -O2 -fanalyzer -Wconversion -c ファイル名.c
cppcheck --enable=warning,style --std=c99 "--template={file}:{line}:{column}: {severity}: {message} [{id}]" ファイル名.c

cppcheck の --template は、1件を1行で出す指定です。行の中に、重大度(error・warning・style など)と、指摘の種類を表す id が入ります。

int が16ビットの AVR(Arduino Uno など)向けのコードなら、cppcheck に --platform=avr8 を足します。

標準ライブラリのヘッダ(stdio.h など)は、渡さなくても解析できます。標準ヘッダが見つからないときに cppcheck が出す知らせ(missingIncludeSystem)にも、標準ライブラリのヘッダは渡さなくてよい、と書かれています。

記事に載せているコードに当てた結果

このブログの記事に載せているコード(C言語ドリル8記事・C言語 総合実力テスト・単体テストの記事の C と、Arduino 総合演習の C++)に、同じ2つを当てました。11記事・83ファイル・1,905行で、gcc は C の10記事だけです。

推奨の2コマンドで出たのは、gcc が3件、cppcheck が36件でした。1件ずつ見ると、実行結果を変えるものはありませんでした。次の出力は、C言語 変数・型・演算子の練習問題7問のコード7ファイルに当てたものです。

cppcheck の指摘の5行(変数・型・演算子の練習問題のコード)

01-example.c:7:22: style: Condition 'a>b' is always true [knownConditionTrueFalse]
06-q5.c:6:22: style: Condition 'x==3' is always false [knownConditionTrueFalse]
06-q5.c:9:29: style: Condition 'temp>=25' is always true [knownConditionTrueFalse]
06-q5.c:9:45: style: Condition 'temp<=30' is always true [knownConditionTrueFalse]
06-q5.c:5:22: style: The comparison 'x == 5' is always true. [knownConditionTrueFalse]

1行目の 01-example.c の7行目は、a > b の結果を表示する例題です。a と b に 5 と 3 を入れているので、cppcheck には結果が決まって見え、always true(いつも真)と出ます。

gcc の警告(同じ記事の 05-q4.c)

$ gcc -std=c99 -Wall -Wextra -O2 -fanalyzer -Wconversion -c 05-q4.c
05-q4.c: In function 'main':
05-q4.c:7:31: warning: conversion from 'int' to 'float' may change value [-Wconversion]
    7 |     printf("%.1f\n", (float)a / b);  // 3.5:キャストで小数の割り算に
      |                               ^

gcc の -Wconversion は、(float)a / b の b(int)を float に変えるところで出ています。GCC のマニュアルは、-Wconversion を、値が変わるかもしれない暗黙の型変換に出す警告と書いています。(float)a のような明示的なキャストには出しません。この例題では b が 2 なので、float に変えても値は変わりません。

cppcheck の36件のうち18件は、static や const を付けられるという改善の提案でした。17件は、例題で変数に決まった値を入れているための指摘です。残りの1件は、前置と後置の違いを見せるために値を入れ直している行への指摘で、教材の書き方によるものでした。

指摘は、そのまま直すべきバグとは限りません。出た行を読んで、実行結果を変えるかを確かめてから直します。

記事のコードは、cppcheck の既定(--enable なし)と --enable=warning では、11記事とも0件でした。gcc の -Wall -Wextra でも、10記事とも0件です。

--enableはwarning,styleまでにする

cppcheck の --enable は、既定で無効の指摘を足すオプションです。style を指定すると、warning・performance・portability も一緒に有効になります(ヘルプの説明)。そのため、--enable=style と --enable=warning,style で有効になる指摘は同じで、27本で見つけた数も同じ16種でした。

--enable 27本で見つけた数 記事のコードの指摘
なし(既定) 10 0件
warning 11 0件
style 16 36件
all 16 72件(ほかに知らせが90件)

--enable=all は、27本で見つけた数は style と同じで、記事のコードでは指摘が36件増えました。増えたのは、static にできる関数を知らせる staticFunction が24件と、使われていない関数を知らせる unusedFunction が12件です。

unusedFunction の12件は、ファイルを1つずつ解析したために出たものです。別のファイル(テストのファイルや main.cpp)から呼ばれる関数を、「使われていない」と判定していました。27本でも、バグではない所への指摘は、--enable=all のときに割り込みの関数へ出た unusedFunction だけでした。cppcheck のヘルプも、unusedFunction はプログラム全体を解析するときだけ有効にするよう勧めています。

--enable=all では、標準ヘッダが見つからないという知らせ(missingIncludeSystem)も、記事のコードで78件出ました。

指摘を消すときは、--inline-supprか--suppressを使う

直さないと決めた指摘は、抑止できます。指摘が出る行の直前に、次のコメントを書きます。

d14_suppressed.c(抜粋)

static int read_temp(int raw)
{
    // cppcheck-suppress unreadVariable
    int offset = 5;   /* DEFECT */
    return raw / 2;
}

このコメントで指摘が消えるのは、cppcheck に --inline-suppr を付けたときだけです。付けないと、コメントがあっても指摘は出たままでした。

コメントを書かずに、コマンドで --suppress=unreadVariable:d14_unused_var.c:6 のように id・ファイル・行で指定しても消えます。行を間違えて :7 にすると、消えませんでした。

VS Codeのタスクにして、問題パネルで指摘を見る

tasks.json に cppcheck と gcc のタスクを書くと、開いているファイルへの指摘が、VS Code の問題パネルに並びます。

tasks.jsonに、cppcheckとgccのタスクを書く

ワークスペースの .vscode\tasks.json に、次の3つのタスクを書きます。cppcheck のタスク、gcc のタスク、その2つをまとめて走らせるタスクです(書き方は VS Code の Tasks の公式文書)。

.vscode\tasks.json

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "cppcheck: 開いているファイル",
      "type": "process",
      "command": "C:\\Program Files\\Cppcheck\\cppcheck.exe",
      "args": [
        "--enable=warning,style",
        "--std=c99",
        "--template={file}:{line}:{column}: {severity}: {message} [{id}]",
        "${relativeFile}"
      ],
      "options": {
        "cwd": "${workspaceFolder}"
      },
      "problemMatcher": {
        "owner": "cppcheck",
        "source": "cppcheck",
        "fileLocation": ["relative", "${workspaceFolder}"],
        "severity": "warning",
        "pattern": {
          "regexp": "^(.+?):(\\d+):(\\d+): (error|warning|style|performance|portability): (.*) \\[(\\w+)\\]$",
          "file": 1,
          "line": 2,
          "column": 3,
          "severity": 4,
          "message": 5,
          "code": 6
        }
      }
    },
    {
      "label": "gcc: 警告だけ見る(開いているファイル)",
      "type": "process",
      "command": "C:\\msys64\\ucrt64\\bin\\gcc.exe",
      "args": [
        "-std=c99",
        "-Wall",
        "-Wextra",
        "-c",
        "${relativeFile}",
        "-o",
        "NUL"
      ],
      "options": {
        "cwd": "${workspaceFolder}",
        "env": {
          "PATH": "C:\\msys64\\ucrt64\\bin;${env:PATH}"
        }
      },
      "problemMatcher": {
        "owner": "gcc",
        "source": "gcc",
        "fileLocation": ["relative", "${workspaceFolder}"],
        "pattern": {
          "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
          "file": 1,
          "line": 2,
          "column": 3,
          "severity": 4,
          "message": 5
        }
      }
    },
    {
      "label": "静的解析: cppcheck と gcc",
      "dependsOn": [
        "cppcheck: 開いているファイル",
        "gcc: 警告だけ見る(開いているファイル)"
      ],
      "problemMatcher": []
    }
  ]
}

problemMatcher は、コマンドが出した行を正規表現で読み取り、問題パネルの項目にする設定です。VS Code は出力を1行ずつ problemMatcher に渡します。

cppcheck は、MSI の既定の入れ先をフルパスで書いています(MSI が PATH を足す設定を持っていないため)。gcc は、options.env.PATH で C:\msys64\ucrt64\bin を PATH の先頭に足しています。このフォルダが PATH に無いと、gcc が何も表示せずに終わります(うまく動かないとき)。

"type": "process" にしているのは、--template= の { } や [ ] をシェルに解釈させず、そのまま cppcheck に渡すためです。

この gcc のタスクは -Wall -Wextra だけです。推奨の -O2 -fanalyzer -Wconversion も問題パネルに出したいときは、args に足します。-fanalyzer の出力は、problemMatcher の正規表現に一致するのが1行目だけで、続く道筋の図は一致しません。

問題パネルに、cppcheckとgccの指摘が並ぶ

タスクは、メニューの「ターミナル」から「タスクの実行」を選び、出てきた一覧で「静的解析: cppcheck と gcc」を選んで実行します。2つのタスクは、それぞれのターミナルで走ります。

記事の tasks.json のままだと、タスクを実行したときに前に出るのはターミナルで、問題パネルは前に出ません。問題パネルは、Ctrl+Shift+M か、下のステータスバーの指摘の数をクリックすると開きます。

指摘があったときに問題パネルを前に出したいなら、2つのタスクに次の設定を足します(既定は "never")。

2つのタスクに足す設定

"presentation": {"revealProblems": "onProblem"}

この設定を足して、戻り値を返さない関数(d19_missing_return.c)を開いてタスクを実行すると、問題パネルは次のようになりました。

VS Code で d19_missing_return.c を開き、静的解析のタスクを実行したあとの画面。問題パネルに、cppcheck の赤い行「Found an exit path from function with non-void return type that has missing return statement」(10行目)と、gcc の黄色い行「control reaches end of non-void function」(11行目)の2行が並んでいる

赤(ⓧ)の行が cppcheck の error、黄(⚠)の行が gcc の warning です。行の右には、出した道具(cppcheck の行は cppcheck(missingReturn) と id つき)と、[Ln 10、Col 5] のような行と列が出ます。エディタの10行目と11行目にも波線が付きました。

cppcheck の style の指摘は、problemMatcher に "severity": "warning" を書いているので、黄(Warning)で出ます。この1行を消すと、style の指摘が赤(Error)で出ました。VS Code が読める重大度の名前に、style が無いためです。

画面は、C/C++ 拡張を入れていない VS Code 1.130.0 のもので、cppcheck は MSI を展開した場所から呼んでいます。

makeやCIで止めるなら、終了コードを使う

cppcheck は、指摘があっても、既定では終了コード 0 を返します。--error-exitcode=1 を付けると、指摘があるとき 1 を返すので、make や CI の処理をそこで止められます。

ヘルプは「errors が見つかったら」と書いていますが、style の指摘だけでも 1 になりました。--enable=all にすると、2つの知らせだけでも 1 を返しました。標準ヘッダが見つからないという missingIncludeSystem と、使われなかった抑止の unmatchedSuppression です。--error-exitcode は、--enable=warning,style と組み合わせます。

gcc は、警告だけなら終了コード 0 です。-Werror を付けると、警告がエラーとして扱われ、1 を返します。

うまく動かないとき

gcc のタスクが何も出さずに終わり、問題パネルが空のまま
PATH に C:\msys64\ucrt64\bin が無いと、gcc.exe をフルパスで呼んでも、何も表示せずに終了コード 1 で終わりました。tasks.json の options.env.PATH に、このフォルダを足します。
-fsyntax-only で回すと、出るはずの警告が出ない
-fsyntax-only は、オブジェクトファイルを作らずに警告だけを出すオプションです。ただし、27本のうち4件の警告(free 後の使用・break 忘れ・戻り値の返し忘れ・strcpy の溢れ)と、-fanalyzer の指摘が出なくなりました。-c を付けて、出力を -o NUL で捨てます。
cppcheck の style の指摘が、赤(エラー)で出る
cppcheck の problemMatcher に "severity": "warning" を書きます(問題パネルに、cppcheckとgccの指摘が並ぶ)。
コマンドで cppcheck と打っても見つからない
公式の MSI は、PATH を足す設定を持っていません。"C:\Program Files\Cppcheck\cppcheck.exe" のようにフルパスで呼ぶか、このフォルダを PATH に足します。

まとめ|gccの警告とcppcheckは両方回す

推奨の2コマンド

gcc -std=c99 -Wall -Wextra -O2 -fanalyzer -Wconversion -c ファイル名.c
cppcheck --enable=warning,style --std=c99 "--template={file}:{line}:{column}: {severity}: {message} [{id}]" ファイル名.c
  • gcc の警告と cppcheck は、見つけるバグが違います。この27本では、int が何ビットでもバグになる24種のうち、片方だけが見つけたバグが11種あり、2つを1回ずつ回すと21種でした
  • int が16ビットの AVR(Arduino Uno など)向けのコードは、cppcheck に --platform=avr8 を足します
  • 2つとも見逃した3種のうち、データ次第のゼロ除算と8ビットのタイマの差は単体テストの境界の値で、volatile の付け忘れはレビューで拾います
  • cppcheck の --enable=all は、27本で見つけた数が増えず、バグではない所への指摘が増えます