C/C++でプログラムを書いていると、変数や関数の前にstaticやexternというキーワードが付いていることがあります。
static int count = 0;
extern int systemStatus;
Arduinoの短いスケッチではあまり意識しないかもしれませんが、プログラムが大きくなり、複数の.cppや.hファイルへ分割するようになると非常に重要になります。
staticとexternを理解するには、まずスコープ・寿命・リンケージという3つの考え方を区別する必要があります。
この記事では、ローカル変数とグローバル変数の違いから始めて、staticローカル変数、ファイルスコープのstatic、externによる別ファイルからの参照まで順番に解説します。
まずスコープと寿命を理解しよう
変数について考えるときには、少なくとも次の2つを分けて考える必要があります。
- スコープ:その名前をコード上のどこから使えるか
- 寿命:そのオブジェクトがいつからいつまで存在するか
この2つは似ているように見えますが、別の概念です。
ローカル変数とは?
関数やブロックの中で宣言した変数は、一般にローカル変数と呼ばれます。
void test()
{
int count = 0;
count++;
Serial.println(count);
}
このcountという名前を使えるのは、宣言されたブロックの中だけです。
通常の自動記憶域期間を持つローカル変数は、関数やブロックへ入ったときにその実体が用意され、そこから抜けると寿命を終えます。
そのため、再びtest()を呼び出すと、通常は新しいcountとして扱われます。
staticを付けるとどうなる?
先ほどのローカル変数へstaticを付けてみます。
void test()
{
static int count = 0;
count++;
Serial.println(count);
}
この場合、countという名前を使える範囲は依然としてtest()の中だけです。
しかし、変数そのものの寿命が変わります。
通常のローカル変数 test()開始 ↓ countを用意 ↓ 処理 ↓ test()終了 ↓ countの寿命が終了 staticローカル変数 プログラム中に実体が存在 ↓ test()を呼ぶ ↓ 前回の値を利用 ↓ test()終了 ↓ 値は保持される ↓ 次回も同じcountを利用
staticローカル変数の動作を確認
次のコードを考えてみましょう。
void countUp()
{
static int count = 0;
count++;
Serial.println(count);
}
void loop()
{
countUp();
delay(1000);
}
実行すると、概念的には次のようになります。
1 2 3 4 5 ...
countUp()を抜けてもcountの値が保持されるためです。
staticローカル変数のスコープは広がらない
ここは重要なポイントです。
staticを付けても、ローカル変数の名前がプログラム全体から見えるようになるわけではありません。
void functionA()
{
static int count = 0;
}
void functionB()
{
// count++; // ここからは使えない
}
countのスコープはfunctionA()の中だけですが、その実体は関数を抜けても寿命を終えません。
つまり、スコープは狭いまま、寿命だけが長いという特徴があります。
グローバル変数とは?
関数の外側で変数を定義すると、ファイルスコープを持つ変数になります。
int count = 0;
void setup()
{
count = 10;
}
void loop()
{
count++;
}
このような変数は一般にグローバル変数と呼ばれます。
静的記憶域期間を持つため、プログラムの実行中は値を保持します。
ファイルスコープでstaticを使うと意味が変わる
今度は関数の外側でstaticを付けてみましょう。
static int count = 0;
この場合、staticローカル変数とは役割が少し異なります。
ファイルスコープの変数へstaticを付けると、その名前は内部リンケージを持ち、基本的にそのソースファイル内部で使用するものになります。
main.cpp
static int count = 0;
↑
main.cpp内部で使用
other.cpp
count
↑
main.cppのstaticなcountを
この名前で参照することはできない
リンケージとは?
ここでリンケージ(linkage)という言葉が登場します。
簡単に言えば、異なる場所に現れる同じ名前の宣言が、同じ実体を指すものとして結び付けられるかに関係する仕組みです。
| 種類 | イメージ |
|---|---|
| 外部リンケージ | 別のソースファイルからも同じ実体を参照できる |
| 内部リンケージ | その翻訳単位の中だけで同じ実体として扱う |
| リンケージなし | ローカル変数など、他のスコープの宣言と結び付かない |
初心者の段階では、ファイルスコープのstaticについて「この.cppの内部だけで使う」と覚えると理解しやすいでしょう。
なぜファイルスコープのstaticを使うの?
大きなプログラムでは、別のファイルから触ってほしくない変数や関数がたくさんあります。
たとえばモータードライバを作ったとします。
static int motorState = 0;
この変数をモータードライバ内部だけで管理したいなら、ファイルスコープのstaticが使えます。
外部へ公開する必要のない実装を隠すことで、プログラムの構造を整理しやすくなります。
関数にもstaticを付けられる
ファイルスコープでは、関数にもstaticを付けることができます。
static void updateMotor()
{
// モーター制御
}
この関数も内部リンケージを持ち、そのソースファイル内部の実装として扱えます。
ドライバやライブラリを作るときには、
外部へ公開する関数
↓
motorStart()
motorStop()
内部だけで使う関数
↓
static updateMotor()
static checkError()
のように分けることができます。
externとは?
externは、別の場所で定義されている変数や関数を参照するときに使われます。
たとえばmain.cppで次の変数を定義したとします。
// main.cpp
int systemStatus = 0;
別のdisplay.cppからこの変数を使いたい場合、次のように宣言できます。
// display.cpp
extern int systemStatus;
void showStatus()
{
Serial.println(systemStatus);
}
extern int systemStatus;は、簡単に言えば「systemStatusというint型の変数は別の場所で定義されています」とコンパイラへ伝える宣言です。
宣言と定義の違い
externを理解するときには、宣言と定義の違いが重要です。
extern int systemStatus;
これは通常、変数の存在と型を知らせるための宣言として使います。
int systemStatus = 0;
こちらは変数の実体を用意する定義です。
宣言 extern int systemStatus; 「こういう変数があります」 定義 int systemStatus = 0; 「実際の変数をここに用意します」
変数の定義は基本的に1か所
複数のソースファイルで同じグローバル変数を共有する場合、実体となる定義は基本的に1か所にします。
// main.cpp
int systemStatus = 0;
別のファイルでは、
// display.cpp
extern int systemStatus;
として同じ実体を参照します。
複数のソースファイルで同じ外部リンケージの変数をそれぞれ定義すると、多重定義としてリンク時に問題になる場合があります。
ヘッダーファイルを使う場合
実際のプロジェクトでは、共有する変数の宣言をヘッダーファイルへ書くことがあります。
たとえば、
// system_state.h
#pragma once
extern int systemStatus;
そして、1つの.cppで実体を定義します。
// system_state.cpp
#include "system_state.h"
int systemStatus = 0;
利用する側ではヘッダーファイルをインクルードします。
// display.cpp
#include "system_state.h"
void showStatus()
{
Serial.println(systemStatus);
}
このように、
system_state.h
↓
externで宣言
system_state.cpp
↓
実体を定義
display.cpp
↓
ヘッダーをincludeして利用
という構成にできます。
なぜヘッダーに変数を直接定義しないの?
たとえばヘッダーファイルへ単純に次のような定義を書いたとします。
// system_state.h
int systemStatus = 0;
このヘッダーを複数の.cppからインクルードすると、それぞれの翻訳単位に定義が現れ、多重定義などの問題につながる可能性があります。
そのため、従来の典型的な構成では、
.h ↓ externで宣言 .cpp ↓ 1か所で定義
と分けます。
なお、C++17以降にはinline変数など別の設計方法もありますが、まずはこの基本形を理解しておけばよいでしょう。
関数にもexternはある?
関数も別ファイルから利用できます。
たとえばヘッダーファイルで、
// motor.h
void motorStart();
void motorStop();
と宣言し、
// motor.cpp
#include "motor.h"
void motorStart()
{
// モーター開始
}
void motorStop()
{
// モーター停止
}
と定義できます。
通常の関数宣言は外部リンケージを持つ関数を宣言できるため、一般的なケースでは関数宣言へわざわざexternを書く必要はありません。
staticとexternは反対なの?
ファイルスコープで考えると、staticとexternは対照的に見えます。
| static | externを使った宣言 | |
|---|---|---|
| 主な目的 | 実体をその翻訳単位の内部に限定する | 別の場所で定義された実体を参照する |
| 別ファイルから | その名前では参照できない | 参照できる |
| 典型的な用途 | 内部実装 | 共有する変数 |
ただし、staticにはローカル変数の寿命を変える役割もあるため、単純に「staticとexternは完全な反対」と覚えるのは正確ではありません。
staticには2つの使い方があると考えよう
初心者が混乱しやすいのがここです。
| 場所 | staticの主な効果 |
|---|---|
| 関数内のローカル変数 | 静的記憶域期間となり、関数を抜けても値を保持する |
| ファイルスコープの変数・関数 | 内部リンケージとなり、翻訳単位内部の実装にできる |
同じstaticというキーワードでも、どこに書かれているかを確認することが重要です。
static変数はスタックに置かれる?
以前の記事でスタックとヒープについて学習しました。
通常の自動ローカル変数は、典型的な実装ではスタック上に置かれることがあります。
一方、staticローカル変数は静的記憶域期間を持つため、通常の自動ローカル変数とは異なる領域で管理されます。
初期値を持つ静的変数は.data、ゼロ初期化される静的変数は.bssなどに配置されることがあります。
ただし、実際の配置はツールチェーンやリンカスクリプトなどによって決まります。
スタックとヒープについては「スタックとヒープとは?組み込み開発のメモリの仕組みを初心者向けに解説」も参照してください。
staticとconstは何が違う?
staticとconstも意味が異なります。
static int count = 0;
const int MAX_COUNT = 100;
staticは記憶域期間やリンケージなどに関係します。
constは、そのアクセスを通して値を変更しないことを表します。
組み合わせることもできます。
static const uint8_t SENSOR_ADDRESS = 0x44;
ファイルスコープでこのように書いた場合、そのファイル内部で利用する変更不可の値として扱えます。
constについては「constとvolatileとは?組み込み開発での意味と使い方を初心者向けに解説」で詳しく解説しています。
staticとvolatileを組み合わせることもある
組み込み開発では、staticとvolatileを組み合わせる場合もあります。
static volatile bool interruptFlag = false;
ファイルスコープでこのように書けば、概念的には、
static→ この翻訳単位内部で使うvolatile→ 割り込みなど通常の処理以外から値が変化する可能性がある
という2つの性質を表現できます。
KUMITATE-C3で考えてみよう
KUMITATE-C3で少し大きなプログラムを作る場合を考えてみましょう。
たとえばI2Cセンサーの処理をsensor.cppへ分離するとします。
// sensor.cpp
static const uint8_t SENSOR_ADDRESS = 0x44;
static float lastTemperature = 0.0f;
static void readSensorRaw()
{
// I2C通信
}
float getTemperature()
{
readSensorRaw();
return lastTemperature;
}
ここでは、
SENSOR_ADDRESSlastTemperaturereadSensorRaw()
をsensor.cpp内部の実装として隠しています。
外部には、
float getTemperature();
だけを公開できます。
全部グローバル変数にするのは避けよう
Arduinoでは、スケッチの上部に変数を並べれば簡単にどこからでも利用できます。
int count;
bool flag;
float temperature;
int mode;
小さなプログラムなら問題にならないこともありますが、規模が大きくなると「どの処理がこの変数を書き換えているのか」が分かりにくくなります。
必要以上に外部へ公開せず、
関数内だけで必要
↓
ローカル変数
関数内で値を保持したい
↓
staticローカル変数
.cpp内部だけで必要
↓
ファイルスコープのstatic
複数ファイルから共有する必要がある
↓
必要に応じて外部リンケージ
+ extern宣言
というように、必要な範囲だけからアクセスできるように設計するとコードを管理しやすくなります。
externを使いすぎるのも注意
externを使えば、さまざまなファイルから同じグローバル変数へアクセスできます。
しかし、何でもexternで共有すると、プログラム全体から値を書き換えられる状態になり、依存関係が分かりにくくなることがあります。
場合によっては、変数そのものを公開するのではなく、関数を通してアクセスさせる方法もあります。
static int systemStatus = 0;
int getSystemStatus()
{
return systemStatus;
}
void setSystemStatus(int status)
{
systemStatus = status;
}
こうすると、内部の変数を直接公開せずに状態を管理できます。
よくある間違い
- staticを付けると変数がグローバルになると思う
- staticローカル変数のスコープまで広がると思う
- ファイルスコープのstaticとstaticローカル変数を同じ意味だと思う
- extern宣言をしただけで変数の実体も作られると思う
- 同じ外部変数を複数の.cppで定義する
- 共有変数を何でもexternにする
- ヘッダーファイルへ安易にグローバル変数の定義を書く
static・extern・const・volatileを整理
| キーワード | 主に何を表す? |
|---|---|
static | 使う場所によって、静的記憶域期間や内部リンケージなどを指定する |
extern | 別の場所で定義される外部リンケージの実体を宣言する際などに使う |
const | そのアクセスを通した変更を禁止する |
volatile | 通常の処理だけでは予測できないアクセスや変化があることを示す |
これらは似たような場所に書かれるため混乱しやすいですが、それぞれ役割が異なります。
これまで学んだ内容とのつながり
staticとexternまで理解すると、変数についてかなり広い視点で考えられるようになります。
データ型 ↓ データサイズ ↓ 変数 ↓ スコープ ↓ 寿命 ↓ リンケージ ↓ static / extern
そしてメモリの視点では、
ローカル変数
↓
スタックなど
static変数
↓
静的記憶域
ポインタ
↓
それらのアドレスを扱う
という形で、以前学習したスタック・ヒープやポインタともつながっています。
まとめ
staticとexternは、プログラムの規模が大きくなるほど重要になるキーワードです。
- スコープは「名前をどこから使えるか」を表す
- 寿命は「オブジェクトがいつまで存在するか」を表す
- リンケージは「別の宣言と同じ実体として結び付くか」に関係する
- 通常の自動ローカル変数はブロックを抜けると寿命を終える
- staticローカル変数はスコープを狭く保ったまま値を保持できる
- ファイルスコープのstaticは内部リンケージを持つ
- staticは関数にも使用できる
- externは別の場所で定義された変数などを宣言するときに使われる
- 共有する外部変数の実体は基本的に1か所で定義する
- 何でもグローバル変数やexternにせず、必要な範囲だけ公開する
Arduinoの小さなスケッチから本格的な組み込みソフトウェアへ進むと、プログラムを複数ファイルへ分割するようになります。
そのとき、「この変数はどこから見えるのか」「いつまで存在するのか」「別の.cppから使えるのか」を判断できるようになると、ドライバやライブラリのソースコードも格段に読みやすくなります。

