C言語のstatic constの使い分け

組み込み開発では、RAMの空き容量とビルド後のメモリ配置を意識しながら定数を定義する場面があります。限られたメモリをどう節約するかは、規模を問わず多くの現場での課題です。

constとstaticは定数定義の文脈であわせて語られることが多いものの、変数に与える意味は異なります。この記事では役割の違いからフラッシュ(不揮発メモリ。以降、ROMと表記)への配置の仕組み、#defineやenumとの使い分けまで、定数定義の基準について解説します。

constとstaticでは変数に課す制約が異なる

constは、「値を書き換えない」という約束をコンパイラに宣言する型修飾子です。const付きの変数へ代入するコードは、コンパイル時にエラー(または警告)となります。一方のstaticは記憶クラス指定子と呼ばれ、ファイルスコープでは内部結合を指定し、関数内の変数にはプログラムの終了時までの寿命(静的記憶域期間)を与えます。

値の変更を制約するconstと、結合や寿命を指定するstaticでは役割が異なるため、併用できます。値を変更しない変数には、const宣言で意図を示すのが一般的な作法です。

記述順については、static constでもconst staticでも文法上は同じ意味ですが、実務ではstatic constを用いることが一般的です。コーディング規約で語順まで定めている現場もあるため、規約や既存コードの表記に合わせて統一しておくことが推奨されます。

constを付けた定数はROMに配置できる

一般的な組み込み処理系では、初期値付きのグローバル変数は書き換えられる前提でRAM上に実体を持ちます。初期値そのものはROMに保存されており、起動時のスタートアップ処理でROMからRAMへコピーされます。

なお、初期値のない(または0で初期化された)グローバル変数は.bssという領域に置かれ、起動時にゼロクリアされるだけなので、ROM側に初期値の分の領域は必要ありません。

constを付けた変数は、通常の代入では書き換えられません。多くのマイコン向けツールチェーン(OSを介さないベアメタル構成)では、const付きの変数は読み取り専用セクション(.rodataなど)へ配置され、リンカスクリプトの設定によってはROMに常駐します。

その場合、起動時に.data(初期値付き変数の領域)のようにROMからRAMへコピーされる対象にならず、RAM使用量の削減に有効です(最終的な配置はマップファイルで確認します)。組み込み向けの開発環境のなかには、const宣言をROM化とRAM節約の手段として明記しているものもあります。

実際にどこへ置かれたかは、リンカが出力するマップファイルで確認できます。命令コードの「.text」、初期値付き変数の「.data」、初期値なし変数の「.bss」といったセクションごとの配置先とサイズを確認できるため、RAM不足の調査ではマップファイルの参照が有効です。

定数やテーブルを追加した際に確認すれば、メモリ使用量の変化を早期に把握できます。

staticは変数の参照範囲をファイル内に限定する

ファイルスコープの変数にstaticを付けると、内部結合(internal linkage)という扱いになり、別のソースファイル(.cファイル)からは参照できなくなります。別のファイルでexternを宣言してもリンク時に解決されないため、外部から同じ識別子を直接参照できません。

たとえば、通信ドライバの内部状態や作業用バッファなど、モジュールの外へ見せる必要がない変数が対象です。別のファイルに同名の変数が定義されていても、それぞれ独立した変数として扱われます。

上述の考え方は変数に限らず、外部へ公開しない関数にも同じように適用できます。外部結合が不要な変数や関数にstatic宣言を付けるのは、C言語のコーディング作法として一般的に推奨されています。

公開してもよい変数や関数だけをヘッダに載せ、非公開の対象はstaticで隠すという線引きが、ファイル単位でモジュールを設計する基本です。

ファイル内だけで使う定数はstatic constで定義する

特定のソースファイル内だけで使う定数や設定テーブルは、実務ではstatic constと書くことが一般的です。センサの補正係数表や、通信設定の初期値テーブルなどが典型例です。参照範囲がファイル内に限定されるため、定義箇所と利用箇所を追跡しやすくなります。

また、関数内に大きな参照用テーブルを置く場合にもstatic constが有効です。局所変数のままだと、多くの処理系では呼び出しのたびにスタック領域に確保されます。大きなテーブルでは、スタックを圧迫する原因になりがちです。static constにすれば実体は1つになり、呼び出しごとのスタック領域の確保が発生しなくなります。

ヘッダのstatic constは翻訳単位ごとに別定義になる

共有したい定数をヘッダにstatic constで書く場合には注意が必要です。インクルードした翻訳単位ごとに別実体になり得るため、設定テーブルなどではコード/ROM使用量が増えることがあります。最終結果はマップファイルで確認します。

複数ファイルで共有する定数は、ヘッダにはextern const宣言だけを書き、実体の定義は1つのソースファイルに置きます。ヘッダに実体まで書く形と、宣言と定義を分ける形を、対比として覚えておくとよいでしょう。

extern constの形にする場合、実体は1つに保たれ、参照側はヘッダ経由で型情報も共有できます。共有する定数の実体をヘッダに記述しない設計は、重複定義を避ける基本的な方法です。

#define・enum・static constの使い分けを押さえる

#defineはプリプロセッサによるトークン置換で、マクロ自体は型を持ちません。展開後の式はコンパイラの型チェックを受けますが、標準的なデバッグビルドでは変数と同じデバッグシンボルが残りにくく、デバッグ時に名前で追跡しづらいことがあります。数値リテラルを意味のある定数名に置き換えておけば、値の意図が伝わりやすくなり、複数箇所を修正する際の変更漏れも防ぎやすくなります。

ファイル内だけで使う定数には、型を持つstatic constが適しています。ただし、C言語では(C++と異なり)、const付き変数はコンパイル時の整数定数式としては扱われない点に注意が必要です(※処理系の拡張で通ることもありますが、標準Cとしては「const変数=整数定数式」ではありません)。

ファイルスコープの配列サイズやswitch文のcaseラベルのように、整数定数式が必要な場面ではenum(またはマクロ)を使うのが安全です。enumの列挙定数は整数定数式として使用できるため、変数のための記憶領域を必要としません。

一方、#ifや#ifdefによるビルド切り換えのように、前処理で判定する場面ではマクロを使います。「型の有無」、「整数定数式の要否」、「前処理で使うか」を基準に使い分けるのが現実的です。

定義する場所ごとに使い分けの基準を決めておく

constの有効範囲が広がる順に3つ並べた図。左から、関数内で使うconstの局所変数、1つの.cファイル内で共有するstatic const、ヘッダを介して複数の.cファイルで共有するextern const宣言の順で、右に行くほど使える範囲が広がることを矢印で示している。

定数定義の基準は「関数内」、「ファイル内」、「複数ファイル共有」という3つの場面で決めておくと迷いにくくなります。関数内だけで使う値のうち、単一の値であればconstの局所変数が使われることが多いですが、初期化のオーバーヘッド(余分な処理時間)を避けるためstatic constが適していることもあります。大きなテーブルの場合はstatic constにしてスタック領域の確保を避けます。

ファイル内で利用する定数は、static constを使うのが原則です。複数ファイルで共有する定数だけ、ヘッダのextern const宣言とソースファイルでの定義に切り換えます。

迷ったときには、できるだけスコープを限定するのが判断のポイントです。参照範囲が狭くなるほど影響調査は楽になり、名前の衝突も生じにくくなります。あわせてマップファイルでメモリ配置を確認すれば、RAM不足の予防にも役立つでしょう。

既存コードの定数をこの基準で見直すだけでも、メモリの無駄や公開範囲が広すぎる変数が見つかることがあります。定義時にスコープと配置を確認することが、保守性とメモリ効率の両立につながります。

組み込みソフトの世界 トップへ戻る