staticとexternとは?変数のスコープ・寿命・リンケージを初心者向けに解説

組み込み基礎

C/C++でプログラムを書いていると、変数や関数の前にstaticやexternというキーワードが付いていることがあります。

static int count = 0;

extern int systemStatus;

Arduinoの短いスケッチではあまり意識しないかもしれませんが、プログラムが大きくなり、複数の.cppや.hファイルへ分割するようになると非常に重要になります。

staticとexternを理解するには、まずスコープ・寿命・リンケージという3つの考え方を区別する必要があります。

この記事では、ローカル変数とグローバル変数の違いから始めて、staticローカル変数、ファイルスコープのstatic、externによる別ファイルからの参照まで順番に解説します。

C言語とC++のstaticとexternの仕組み。変数のスコープと寿命、staticローカル変数、ファイルスコープのstatic、externによる別ファイルからの参照を示した図
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は対照的に見えます。

staticexternを使った宣言
主な目的実体をその翻訳単位の内部に限定する別の場所で定義された実体を参照する
別ファイルからその名前では参照できない参照できる
典型的な用途内部実装共有する変数

ただし、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_ADDRESS
  • lastTemperature
  • readSensorRaw()

を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から使えるのか」を判断できるようになると、ドライバやライブラリのソースコードも格段に読みやすくなります。

KUMITATE

読むだけでなく、
実際に動かして学びませんか?

クミタテは、組み込み開発や電子工作を 実践しながら学べる学習プラットフォームです。 ESP32を使ったプログラミングから、電子回路、センサー、 通信、基板設計まで、自分のペースで学習できます。

  • 無料で学べる実践的な教材を掲載
  • すぐに試せるサンプルプログラム付き
  • Googleアカウントですぐに登録可能
クミタテに無料登録する
組み込み基礎
スポンサーリンク