組み込みソフト開発101 ~ C言語

言い訳

「C言語」というタイトルでくくるにはトピックが希薄すぎるのですが、他に適当なタイトルを思いつかないので…。

寿命とスコープ

変数の寿命

変数の寿命とは、その変数の値を格納する領域が確保されている期間のことです。例えばunsigned char型の変数であれば、その値を確保するために1バイトの領域が必要です。その領域が、その変数用に使える期間のことです。別の見方をすると、変数の寿命が終わるということは、変数に代入した値が忘れられてしまうということです。

変数の寿命には以下の2種類があります。

  • プログラム全体 …プログラム開始から終了までずっと生きている
  • ブロック内 …ブロック実行中のみ生きている

ブロックとは、波括弧{}で囲まれたコードのことです。関数はブロックの代表例です。

変数の寿命は、以下の2つの要素で決まります。

  • 変数の宣言場所が、関数の中か外か
  • 変数の宣言にstaticが付くか付かないか

これら2つの要素と変数の寿命との対応は以下の通りです。

宣言場所
関数外関数内
staticなしプログラム全体ブロック内
staticありプログラム全体プログラム全体

変数のスコープ

変数のスコープとは、プログラムのどこからなら、その変数の値を読み書きできるか、という話しです。例えばグローバル変数はプログラムのどこからでもアクセスできるので、スコープはプログラム全体です。一方、ある関数の中で宣言したローカル変数を別の変数が読み書きすることはできないので、その関数内がスコープになります。ここではひとまず、ポインタを使ったり関数の引数やreturn値として渡すことは考えません(こういった手段は、本来のスコープを破るためのテクニックです)。

変数のスコープには以下の3種類があります。

  • プログラム全体 …プログラムのどこからでもアクセスできる
  • ファイル内 …あるソースファイル(*.c/*.cpp)内からのみアクセスできる
  • ブロック内 …あるブロック内からのみアクセスできる

変数のスコープは、変数の宣言場所とstaticの有無により、以下のように決まります。

宣言場所
関数外関数内
staticなしプログラム全体ブロック内
staticありファイル内ブロック内

関数の場合

ここまでは変数の話しでしたが、関数の場合はもっとシンプルです。関数の寿命は常にプログラム全体です。また関数のスコープはstaticが付くかどうかによって決まり、staticなしならプログラム全体、staticありならファイル内となります。

staticなしプログラム全体
staticありファイル内

foo.c
int Foo1;           // グローバル変数(寿命、スコープ共に、プログラム全体)。
static int Foo2;    // 寿命はプログラム全体、スコープはファイル内。

void Func(void)     // 外部関数(スコープはプログラム全体)。
{
    int foo3;         // ローカル変数(寿命、スコープ共に、ブロック内)。
    static int foo4;  // 寿命はプログラム全体、スコープはブロック内。
    func();
}

static void func(void)    // 内部関数(スコープはファイル内)。
{
    int i;
    for ( i = 0; i < 10; i++ ) {
        int foo5;         // ローカル変数(寿命、スコープ共に、ブロック内)。
        static int Foo6;  // 寿命はプログラム全体、スコープはブロック内。
        Foo += i;
    }
}
main.c
extern int Foo1;          // グローバル変数へアクセスする準備。
extern void Func(void);   // 外部関数へアクセスする準備。

void main(void)
{
    Foo1 = 10;
    Func();
}

設計上の注意 ~ 寿命とメモリ使用効率

一見すると変数の寿命は長いほど良いように思えますが、メモリの使用効率を考えるとそうとも言えません。変数の寿命がプログラム全体の場合、そのRAM上の領域はその変数専用になります。寿命がブロック内の場合は、その変数用の領域はスタック上に確保され、寿命が終わった後は他の変数に使うことができます(スタックについては「ハードウェアを参照)。よって、不必要にスコープを広げるのは避けた方が良いでしょう。

一方、スタック上に変数を置く場合にも注意が必要です。例えば構造体の配列のようなサイズの大きなローカル変数を関数内で宣言すると、スタックオーバーフローの危険が増します(スタックオーバーフローについてはテストとデバッグを参照)。こういう場合は、staticを付けるとか、変数をポインタにしてデータの実体をヒープに置くといった選択肢を検討する必要があります。

設計上の注意 ~ スコープとモジュールの独立性

プログラムの規模が小さいうちは、変数のスコープが広い方がプログラミングが楽に感じるでしょう。しかし規模が大きくなるほど、モジュールの独立性が重視されてきます。モジュールとは、プログラムを細かい単位に分けたものです。分け方に決まりは無く、関数単位に分けてもいいしソースファイル単位に分けても構いません。機能で分けて、1つのモジュールを複数のソースファイルで実装しても構いません。

いずれにしても、モジュール間の関連を少なくすることが重要です。言い換えれば、1つのモジュール内の変更が、他のモジュールに影響を与えないようにするということです。スコープの広い変数は、モジュールの独立性を下げる要因になります(他モジュールからアクセスされる危険があるから)。スコープをモジュール内に限定しておけば、その変数を変更することが直接的に他モジュールへ影響することはありません。

ただし、スコープが狭いことが常にベストとは言い切れません。可読性を上げるために独立性を犠牲にするケースもあると思います。ブロック内スコープに固執した例と、ファイル内スコープで妥協した例を示します。

ブロック内スコープに固執
void doSomething(void)
{
    int a = ...;
    int b = ...;
    int c = ...;
    
    sub1(a, b, c);
    sub2(a, b, c);
    sub3(a, b, c);
}

static void sub1(int a, int b, int c)
{
    ...
}

static void sub2(int a, int b, int c)
{
    ...
}

static void sub3(int a, int b, int c)
{
    ...
}
ファイル内スコープで妥協
static int a;
static int b;
static int c;

void doSomething(void)
{
    a = ...;
    b = ...;
    c = ...;
    
    sub1();
    sub2();
    sub3();
}

static void sub1(void)
{
    ...
}

static void sub2(void)
{
    ...
}

static void sub3(void)
{
    ...
}

個人的には、グローバル変数は悪だけど、ファイル内スコープはOKと考えています。ただし、1つのソースファイルの規模は小さく保つ(1000ステップ前後が目安)ことが条件です。

設計上の注意 ~ 多重性

変数をスタックに配置することには、多重性に対応するという効果もあります。ある関数がaというローカル(staticなし)変数を持っているとすると、この変数a用の領域は、常に1つ存在するわけではないし、1つ存在するときと1つも存在しないときがあるというわけでもありません(回りくどい言い方ですが)。要するに、2つ以上存在することがありうるのです。それは、関数が再帰的に呼ばれた場合や、マルチスレッド環境下で再入した場合に起こります(再帰と再入については設計を参照)。

ローカル変数にstaticを付けたり、ローカル変数をファイル内スコープの変数に変えることは、単に寿命やスコープを変えるだけでなく、変数の多重性を失うことを意味します。

データサイズ

enum

enum型の変数のサイズはint型と同じです。enum型のサイズなんて普通は気にしないかもしれませんが、構造体のメンバーにenum型を使うときは注意した方が良いでしょう(「処理系」のアライメントの項を参照)。int型のサイズは処理系によって異なりますが、一般的には4バイトです(少なくとも執筆時点では)。

union

一般的にunionの利用頻度は低いかもしれませんが、組み込みソフト開発では良く見かけます。unionは、複数の型(多くの場合は構造体型)から構成されます。unionのサイズは、それを構成する型の中で最も大きな型のサイズと同じです。

例を示します。

typedef struct {
    unsigned char type;
} Common;                 // 1バイト。

typedef struct {
    unsigned char type;
    unsigned char reserve[3];
    unsigned long message;
} ShortMessage;           // 8バイト。

typedef struct {
    unsigned char type;
    unsigned char reserve[3];
    unsigned char message[1024];
} LongMessage;            // 1028バイト。

typedef union {
    Common common;
    ShortMessage short;
    LogMessage long;
} Message;                // 1028バイト。

void func(Message* pmsg)
{
    if ( pmsg->common.type == MSGTYPE_LONG ) {
        // pmsg->long.messageを処理。
    }
    else {
        // pmsg->short.messageを処理。
    }
}

ビットフィールド

個人的にはビットフィールドを使ったことは無いのですが、たまに見かけて悩むことがあるので書いておきます。ビットフィールドは、構造体のメンバーを、型ではなくビット数で指定する手段です。

例を示します。

typedef struct {
    unsigned int serialNo : 4; // 4ビット。
    unsigned int length : 4;   // 4ビット。
    unsigned int state : 4;    // 4ビット。
    unsigned int isNew : 1;    // 1ビット。
} Message;                     // 4バイト(intが4バイトの場合)。

void func(void)
{
    Message m;
    m.serianNo = 0;   // 4ビット値としてアクセス。
    ...
}

リンケージ

プログラム内にCとC++が混在するケースがあります。例えば複数のチームに分かれて開発しており、一方がCを使い、他方がC++を使っているような場合です。このような環境では、C++で書かれた関数を、Cのプログラムから呼び出す必要があるかもしれません。そのためには、リンケージを指定する必要があります。リンケージ指定はC++の文法ですが、C言語の世界でプログラミングする人でもリンケージ指定は目にする機会があるので知っておいた方が良いでしょう。

C++では、func()という関数を定義してコンパイルしても、それはfuncという名前の関数にはなりません。なぜなら、C++には関数のオーバーロードという機能があるからです。関数オーバーロードとは、引数仕様の異なる同名の関数を定義することです。つまりC++では、引数のセマンティクス(型、数、順番)が異なる限り、同名の関数をいくつでも定義できるのです。

そこでコンパイラは、関数名の後ろにセマンティクスが区別できるような修飾子を付けて機械語に翻訳します。例えばfunc(int a, int b)なら、func_I_Iという名前の関数としてコンパイルするわけです。関数名の修飾は、たとえオーバーロードしなくても(funcという名前の関数が1つしかなくても)行われます。

このようにコンパイルされたC++の関数を、C側からfuncという名前で呼び出そうとしても見つかりません。だからといってfunc_I_Iという名前で呼び出すわけにもいきません(関数名の修飾方法は処理系依存なので)。

そこで登場するのがリンケージ指定です。C++側で関数を定義するときにリンケージとしてC言語を指定すれば、「この関数はCからリンクできるようにコンパイルしてね」とコンパイラへ伝えることができます。リンケージ指定の方法は2種類あります。

extern "C" void func(void);    // 単発指定。

extern "C" {                   // 複数まとめて指定。
    void func1(void);
    void func2(void);
}

C++で関数ライブラリを実装するときは、その外部ヘッダファイルにCリンケージを指定するのが一般的です。

#ifndef _HEADER_FOR_LIB_USER_        // 多重インクルード防止。
#define _HEADER_FOR_LIB_USER_

#ifdef __cplusplus
extern "C" {      // Cリンケージ開始。
#endif

// ここに、ライブラリのユーザ向けの関数プロトタイプ宣言などを書く。
...
...
...

#ifdef __cplusplus
}                 // Cリンケージ終了。
#endif

#endif  //#define _HEADER_FOR_LIB_USER_

__cplusplusというシンボルは、C++の処理系がデフォルトで定義します。このヘッダファイルをC言語のソースファイル内で利用した場合は、リンケージ指定の部分が削除されるのでコンパイルエラーにはなりません。結局、関数を実装するC++ソースと関数を利用するC/C++ソース全てがこのヘッダファイルを#includeすれば、問題なくリンクされます。

なお、当然ながら、Cリンケージと関数オーバーロードは併用できません。

Last modified:2011/05/18 14:02:06
Keyword(s):
References:[組み込みソフト開発101]
This page is frozen.